Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/features/accounts/account-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { type QueryClient, useQueryClient } from '@tanstack/react-query';
import type { DistributedOmit } from 'type-fest';
import {
type MintInfo,
type ExtendedMintInfo,
getCashuProtocolUnit,
getCashuUnit,
getCashuWallet,
Expand Down Expand Up @@ -199,7 +199,7 @@ export class AccountRepository {
private async getPreloadedWallet(mintUrl: string, currency: Currency) {
const seed = await this.getCashuWalletSeed?.();

let mintInfo: MintInfo;
let mintInfo: ExtendedMintInfo;
let allMintKeysets: MintAllKeysets;
let mintActiveKeys: MintActiveKeys;

Expand Down
2 changes: 1 addition & 1 deletion app/features/shared/cashu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const mintKeysQueryKey = (mintUrl: string, keysetId?: string) => [
export const mintInfoQueryOptions = (mintUrl: string) =>
queryOptions({
queryKey: mintInfoQueryKey(mintUrl),
queryFn: async () => getCashuWallet(mintUrl).getMintInfo(),
queryFn: async () => getCashuWallet(mintUrl).getExtendedMintInfo(),
staleTime: 1000 * 60 * 60, // 1 hour
});

Expand Down
178 changes: 178 additions & 0 deletions app/lib/cashu/extended-mint-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* This class was copied from cashu-ts v2.7.2 and extended with the following methods:
* - get iconUrl
* - get internalMeltsOnly
*
* As of cashu-ts v2.7.2, the MintInfo class is not exported, so we need to copy it here in order to extend it.
*/

import type {
GetInfoResponse,
MPPMethod,
SwapMethod,
WebSocketSupport,
} from '@cashu/cashu-ts';

/**
* A class that represents the data fetched from the mint's
* [NUT-06 info endpoint](https://github.com/cashubtc/nuts/blob/main/06.md)
*/
export class ExtendedMintInfo {
private readonly _mintInfo: GetInfoResponse;
private readonly _protectedEnpoints?: {
cache: {
[url: string]: boolean;
};
apiReturn: Array<{
method: 'GET' | 'POST';
regex: RegExp;
cachedValue?: boolean;
}>;
};

constructor(info: GetInfoResponse) {
this._mintInfo = info;
if (info.nuts[22]) {
this._protectedEnpoints = {
cache: {},
apiReturn: info.nuts[22].protected_endpoints.map((o) => ({
method: o.method,
regex: new RegExp(o.path),
})),
};
}
}

isSupported(num: 4 | 5): { disabled: boolean; params: SwapMethod[] };
isSupported(num: 7 | 8 | 9 | 10 | 11 | 12 | 14 | 20): { supported: boolean };
isSupported(num: 17): { supported: boolean; params?: WebSocketSupport[] };
isSupported(num: 15): { supported: boolean; params?: MPPMethod[] };
isSupported(num: number) {
switch (num) {
case 4:
case 5: {
return this.checkMintMelt(num);
}
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
case 14:
case 20: {
return this.checkGenericNut(num);
}
case 17: {
return this.checkNut17();
}
case 15: {
return this.checkNut15();
}
default: {
throw new Error('nut is not supported by cashu-ts');
}
}
}

requiresBlindAuthToken(path: string) {
if (!this._protectedEnpoints) {
return false;
}
if (typeof this._protectedEnpoints.cache[path] === 'boolean') {
return this._protectedEnpoints.cache[path];
}
const isProtectedEndpoint = this._protectedEnpoints.apiReturn.some((e) =>
e.regex.test(path),
);
this._protectedEnpoints.cache[path] = isProtectedEndpoint;
return isProtectedEndpoint;
}

private checkGenericNut(num: 7 | 8 | 9 | 10 | 11 | 12 | 14 | 20) {
if (this._mintInfo.nuts[num]?.supported) {
return { supported: true };
}
return { supported: false };
}
private checkMintMelt(num: 4 | 5) {
const mintMeltInfo = this._mintInfo.nuts[num];
if (
mintMeltInfo &&
mintMeltInfo.methods.length > 0 &&
!mintMeltInfo.disabled
) {
return { disabled: false, params: mintMeltInfo.methods };
}
return { disabled: true, params: mintMeltInfo.methods };
}
private checkNut17() {
if (
this._mintInfo.nuts[17] &&
this._mintInfo.nuts[17].supported.length > 0
) {
return { supported: true, params: this._mintInfo.nuts[17].supported };
}
return { supported: false };
}
private checkNut15() {
if (this._mintInfo.nuts[15] && this._mintInfo.nuts[15].methods.length > 0) {
return { supported: true, params: this._mintInfo.nuts[15].methods };
}
return { supported: false };
}

get contact() {
return this._mintInfo.contact;
}

get description() {
return this._mintInfo.description;
}

get description_long() {
return this._mintInfo.description_long;
}

get name() {
return this._mintInfo.name;
}

get pubkey() {
return this._mintInfo.pubkey;
}

get nuts() {
return this._mintInfo.nuts;
}

get version() {
return this._mintInfo.version;
}

get motd() {
return this._mintInfo.motd;
}

// Below methods are added in addition to what the cashu-ts MintInfo class provides

get iconUrl() {
return this._mintInfo.icon_url;
}

/**
* Whether the mint only allows internal melts.
*
* NOTE: This flag is not currently defined in the NUTs.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this something we will suggest them to add?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly, we would need to get it added to the spec. I haven't mentioned this idea to anyone else, but once we have a working demo I was going to show them and propose this new flag.

* Internal melts only is a feature that we have added to agicash mints
* for creating a closed-loop mint.
*/
get internalMeltsOnly() {
const methods = this._mintInfo.nuts[5].methods as (SwapMethod & {
options?: { internal_melts_only?: boolean };
})[];
return methods.some(
(method) => method.options?.internal_melts_only === true,
);
}
}
2 changes: 1 addition & 1 deletion app/lib/cashu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ export * from './secret';
export * from './token';
export * from './utils';
export * from './error-codes';
export type { MintInfo } from './types';
export * from './payment-request';
export * from './extended-mint-info';
20 changes: 8 additions & 12 deletions app/lib/cashu/mint-validation.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import type { MintKeyset, WebSocketSupport } from '@cashu/cashu-ts';
import type {
CashuProtocolUnit,
MintInfo,
NUT,
NUT17WebSocketCommand,
} from './types';
import type { ExtendedMintInfo } from './extended-mint-info';
import type { CashuProtocolUnit, NUT, NUT17WebSocketCommand } from './types';

type NutValidationResult =
| { isValid: false; message: string }
| { isValid: true };

type NutValidation = {
nut: NUT;
validate: (info: MintInfo, unit: string) => NutValidationResult;
validate: (info: ExtendedMintInfo, unit: string) => NutValidationResult;
};

type BuildMintValidatorOptions = {
Expand All @@ -36,7 +32,7 @@ export const buildMintValidator = (params: BuildMintValidatorOptions) => {
return (
mintUrl: string,
selectedUnit: CashuProtocolUnit,
mintInfo: MintInfo,
mintInfo: ExtendedMintInfo,
keysets: MintKeyset[],
): string | true => {
if (!/^https?:\/\/.+/.test(mintUrl)) {
Expand Down Expand Up @@ -155,7 +151,7 @@ const createNutValidators = ({
};

const validateBolt11Support = (
info: MintInfo,
info: ExtendedMintInfo,
operation: 'minting' | 'melting',
unit: string,
): NutValidationResult => {
Expand Down Expand Up @@ -184,7 +180,7 @@ const validateBolt11Support = (
};

const validateGenericNut = (
info: MintInfo,
info: ExtendedMintInfo,
nut: Extract<NUT, 7 | 8 | 9 | 10 | 11 | 12 | 20>,
message: string,
): NutValidationResult => {
Expand All @@ -199,7 +195,7 @@ const validateGenericNut = (
};

const validateWebSocketSupport = (
info: MintInfo,
info: ExtendedMintInfo,
unit: string,
requiredCommands: NUT17WebSocketCommand[],
): NutValidationResult => {
Expand Down Expand Up @@ -229,7 +225,7 @@ const validateWebSocketSupport = (
};

const validateMintFeatures = (
mintInfo: MintInfo,
mintInfo: ExtendedMintInfo,
unit: string,
nutValidators: NutValidation[],
): NutValidationResult => {
Expand Down
7 changes: 0 additions & 7 deletions app/lib/cashu/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { CashuWallet } from '@cashu/cashu-ts';
import { z } from 'zod';

/**
Expand Down Expand Up @@ -140,12 +139,6 @@ export type ProofSecret =
*/
export type P2PKSecret = NUT10Secret & { kind: 'P2PK' };

/**
* A class that represents the data fetched from the mint's
* [NUT-06 info endpoint](https://github.com/cashubtc/nuts/blob/main/06.md)
*/
export type MintInfo = Awaited<ReturnType<CashuWallet['getMintInfo']>>;

/**
* The units that are determined by the soft-consensus of cashu mints and wallets.
* These units are not definite as they are not defined in NUTs directly.
Expand Down
17 changes: 16 additions & 1 deletion app/lib/cashu/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Big from 'big.js';
import type { DistributedOmit } from 'type-fest';
import { decodeBolt11 } from '~/lib/bolt11';
import type { Currency, CurrencyUnit } from '../money';
import { ExtendedMintInfo } from './extended-mint-info';
import { sumProofs } from './proof';
import type { CashuProtocolUnit } from './types';

Expand Down Expand Up @@ -83,13 +84,17 @@ export const getWalletCurrency = (wallet: CashuWallet) => {
*/
export class ExtendedCashuWallet extends CashuWallet {
private _bip39Seed: Uint8Array | undefined;
private _extendedMintInfo: ExtendedMintInfo | undefined;

constructor(
mint: CashuMint,
options: ConstructorParameters<typeof CashuWallet>[1],
options?: ConstructorParameters<typeof CashuWallet>[1] & {
mintInfo?: ExtendedMintInfo;
},
) {
super(mint, options);
this._bip39Seed = options?.bip39seed;
this._extendedMintInfo = options?.mintInfo;
}

get seed() {
Expand Down Expand Up @@ -156,6 +161,15 @@ export class ExtendedCashuWallet extends CashuWallet {
return fee;
}

async getExtendedMintInfo() {
if (this._extendedMintInfo) {
return this._extendedMintInfo;
}
const info = new ExtendedMintInfo(await this.mint.getInfo());
this._extendedMintInfo = info;
return info;
}

private getMinNumberOfProofsForAmount(keys: Keys, amount: Big) {
const availableDenominations = Object.keys(keys).map((x) => new Big(x));
const biggestDenomination = availableDenominations.reduce(
Expand Down Expand Up @@ -205,6 +219,7 @@ export const getCashuWallet = (
'unit'
> & {
unit?: CurrencyUnit;
mintInfo?: ExtendedMintInfo;
} = {},
) => {
const { unit, ...rest } = options;
Expand Down