From 40972ccac2130951ffbf81dc09330ededbb962a2 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Tue, 21 Oct 2025 15:11:51 -0700 Subject: [PATCH] create ExtendedMintInfo with internal_melts_only flag --- app/features/accounts/account-repository.ts | 4 +- app/features/shared/cashu.ts | 2 +- app/lib/cashu/extended-mint-info.ts | 178 ++++++++++++++++++++ app/lib/cashu/index.ts | 2 +- app/lib/cashu/mint-validation.ts | 20 +-- app/lib/cashu/types.ts | 7 - app/lib/cashu/utils.ts | 17 +- 7 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 app/lib/cashu/extended-mint-info.ts diff --git a/app/features/accounts/account-repository.ts b/app/features/accounts/account-repository.ts index d921b44b8..7c948d420 100644 --- a/app/features/accounts/account-repository.ts +++ b/app/features/accounts/account-repository.ts @@ -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, @@ -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; diff --git a/app/features/shared/cashu.ts b/app/features/shared/cashu.ts index 31b20891f..d2e308da0 100644 --- a/app/features/shared/cashu.ts +++ b/app/features/shared/cashu.ts @@ -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 }); diff --git a/app/lib/cashu/extended-mint-info.ts b/app/lib/cashu/extended-mint-info.ts new file mode 100644 index 000000000..5096fe6c2 --- /dev/null +++ b/app/lib/cashu/extended-mint-info.ts @@ -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. + * 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, + ); + } +} diff --git a/app/lib/cashu/index.ts b/app/lib/cashu/index.ts index f71b5caea..67792be60 100644 --- a/app/lib/cashu/index.ts +++ b/app/lib/cashu/index.ts @@ -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'; diff --git a/app/lib/cashu/mint-validation.ts b/app/lib/cashu/mint-validation.ts index 125392f40..246a9ae19 100644 --- a/app/lib/cashu/mint-validation.ts +++ b/app/lib/cashu/mint-validation.ts @@ -1,10 +1,6 @@ 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 } @@ -12,7 +8,7 @@ type NutValidationResult = type NutValidation = { nut: NUT; - validate: (info: MintInfo, unit: string) => NutValidationResult; + validate: (info: ExtendedMintInfo, unit: string) => NutValidationResult; }; type BuildMintValidatorOptions = { @@ -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)) { @@ -155,7 +151,7 @@ const createNutValidators = ({ }; const validateBolt11Support = ( - info: MintInfo, + info: ExtendedMintInfo, operation: 'minting' | 'melting', unit: string, ): NutValidationResult => { @@ -184,7 +180,7 @@ const validateBolt11Support = ( }; const validateGenericNut = ( - info: MintInfo, + info: ExtendedMintInfo, nut: Extract, message: string, ): NutValidationResult => { @@ -199,7 +195,7 @@ const validateGenericNut = ( }; const validateWebSocketSupport = ( - info: MintInfo, + info: ExtendedMintInfo, unit: string, requiredCommands: NUT17WebSocketCommand[], ): NutValidationResult => { @@ -229,7 +225,7 @@ const validateWebSocketSupport = ( }; const validateMintFeatures = ( - mintInfo: MintInfo, + mintInfo: ExtendedMintInfo, unit: string, nutValidators: NutValidation[], ): NutValidationResult => { diff --git a/app/lib/cashu/types.ts b/app/lib/cashu/types.ts index 5ee7ffea0..b6902bb58 100644 --- a/app/lib/cashu/types.ts +++ b/app/lib/cashu/types.ts @@ -1,4 +1,3 @@ -import type { CashuWallet } from '@cashu/cashu-ts'; import { z } from 'zod'; /** @@ -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>; - /** * 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. diff --git a/app/lib/cashu/utils.ts b/app/lib/cashu/utils.ts index 31ce75db2..6cddbba7a 100644 --- a/app/lib/cashu/utils.ts +++ b/app/lib/cashu/utils.ts @@ -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'; @@ -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[1], + options?: ConstructorParameters[1] & { + mintInfo?: ExtendedMintInfo; + }, ) { super(mint, options); this._bip39Seed = options?.bip39seed; + this._extendedMintInfo = options?.mintInfo; } get seed() { @@ -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( @@ -205,6 +219,7 @@ export const getCashuWallet = ( 'unit' > & { unit?: CurrencyUnit; + mintInfo?: ExtendedMintInfo; } = {}, ) => { const { unit, ...rest } = options;