mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 11:18:24 +00:00
Ensure up to date keysets on receive
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Decoding a token whose v2 keyset the wallet has not seen yet
|
||||
* (services/wallet/decodeToken.ts).
|
||||
*
|
||||
* A NUT-02 v2 keyset id is 33 bytes, but a v4 token carries only its first 8 — so
|
||||
* cashu-ts can hand a proof its real id only by matching that prefix against ids the
|
||||
* wallet already holds for the mint, and throws when none match. That is precisely
|
||||
* what a mint rotating keysets produces (nutshell -> cdk migrates the old `00…`
|
||||
* keysets as inactive and signs with a new `01…` one): every wallet that has not
|
||||
* touched the mint since holds a list that cannot decode the ecash now arriving,
|
||||
* including background lightning-address claims and nostr receives.
|
||||
*
|
||||
* These tests use REAL cashu-ts encoding — the truncation under test is its own — and
|
||||
* prove the decode heals itself by re-pulling the keysets exactly once, and only when
|
||||
* a decode has actually failed on an unmappable id.
|
||||
*
|
||||
* @jest-environment node
|
||||
*/
|
||||
jest.mock('../src/services/logService', () => ({
|
||||
log: {debug: jest.fn(), error: jest.fn(), info: jest.fn(), trace: jest.fn(), warn: jest.fn()},
|
||||
}))
|
||||
|
||||
// `mock`-prefixed so jest's hoisting of the factory below can reach them.
|
||||
const mockMintsStore = {findByUrl: jest.fn()}
|
||||
const mockWalletStore = {refreshKeysetsNow: jest.fn()}
|
||||
|
||||
jest.mock('../src/models', () => ({
|
||||
rootStoreInstance: {
|
||||
get mintsStore() {
|
||||
return mockMintsStore
|
||||
},
|
||||
get walletStore() {
|
||||
return mockWalletStore
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
import {deriveKeysetId, getDecodedToken, getEncodedToken} from '@cashu/cashu-ts'
|
||||
import {decodeTokenWithKeysets} from '../src/services/wallet/decodeToken'
|
||||
|
||||
const MINT_URL = 'https://mint.test/Bitcoin'
|
||||
const AMOUNTS = [1, 2, 4, 8]
|
||||
|
||||
/** The keyset that signed every proof before the migration — a v0 id, 8 bytes. */
|
||||
const LEGACY_KEYSET_ID = '00107937db0cc865'
|
||||
|
||||
/** The mint's new v2 keyset id, genuinely derived so it carries the `01` version. */
|
||||
const V2_KEYSET_ID = deriveKeysetId(
|
||||
Object.fromEntries(
|
||||
AMOUNTS.map((amount, i) => [
|
||||
String(amount),
|
||||
`02${String(i + 1).padStart(2, '0').repeat(31)}`,
|
||||
]),
|
||||
),
|
||||
{unit: 'sat', input_fee_ppk: 0, versionByte: 1},
|
||||
)
|
||||
|
||||
const encodeTokenFromKeyset = (keysetId: string): string =>
|
||||
getEncodedToken({
|
||||
mint: MINT_URL,
|
||||
unit: 'sat',
|
||||
proofs: [
|
||||
{
|
||||
id: keysetId,
|
||||
amount: 2,
|
||||
secret: 'a'.repeat(64),
|
||||
C: `02${'11'.repeat(32)}`,
|
||||
} as any,
|
||||
],
|
||||
})
|
||||
|
||||
/** Point the store at a mint holding exactly these keyset ids. */
|
||||
const setStoredKeysetIds = (keysetIds: string[]) => {
|
||||
mockMintsStore.findByUrl.mockReturnValue({mintUrl: MINT_URL, keysetIds})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('the failure being healed', () => {
|
||||
test('a v2 keyset id is truncated to 8 bytes by v4 encoding', () => {
|
||||
expect(V2_KEYSET_ID).toMatch(/^01[0-9a-f]{64}$/)
|
||||
|
||||
// Decoding against the FULL id works, which is the whole mechanism: the short id
|
||||
// in the token is a prefix of it.
|
||||
const decoded = getDecodedToken(encodeTokenFromKeyset(V2_KEYSET_ID), [V2_KEYSET_ID])
|
||||
expect(decoded.proofs[0].id).toBe(V2_KEYSET_ID)
|
||||
})
|
||||
|
||||
test('cashu-ts throws when the wallet holds only the pre-migration keyset', () => {
|
||||
expect(() =>
|
||||
getDecodedToken(encodeTokenFromKeyset(V2_KEYSET_ID), [LEGACY_KEYSET_ID]),
|
||||
).toThrow(/short keyset id/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('decodeTokenWithKeysets', () => {
|
||||
test('refreshes the keysets and retries when the id cannot be mapped', async () => {
|
||||
setStoredKeysetIds([LEGACY_KEYSET_ID])
|
||||
// The refresh is what teaches the wallet about the rotated-in keyset.
|
||||
mockWalletStore.refreshKeysetsNow.mockImplementation(async () => {
|
||||
setStoredKeysetIds([LEGACY_KEYSET_ID, V2_KEYSET_ID])
|
||||
})
|
||||
|
||||
const token = await decodeTokenWithKeysets(encodeTokenFromKeyset(V2_KEYSET_ID), MINT_URL)
|
||||
|
||||
// The proof carries the FULL id, not the 8-byte prefix — everything downstream
|
||||
// (counters, keys, DLEQ) is keyed by it.
|
||||
expect(token.proofs[0].id).toBe(V2_KEYSET_ID)
|
||||
expect(mockWalletStore.refreshKeysetsNow).toHaveBeenCalledTimes(1)
|
||||
expect(mockWalletStore.refreshKeysetsNow).toHaveBeenCalledWith(MINT_URL)
|
||||
})
|
||||
|
||||
test('costs nothing when the keyset is already known', async () => {
|
||||
setStoredKeysetIds([LEGACY_KEYSET_ID, V2_KEYSET_ID])
|
||||
|
||||
const token = await decodeTokenWithKeysets(encodeTokenFromKeyset(V2_KEYSET_ID), MINT_URL)
|
||||
|
||||
expect(token.proofs[0].id).toBe(V2_KEYSET_ID)
|
||||
expect(mockWalletStore.refreshKeysetsNow).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('does not touch the mint for a v0 keyset, which needs no mapping', async () => {
|
||||
// A stale list cannot break a `00…` id: it travels whole.
|
||||
setStoredKeysetIds([])
|
||||
|
||||
const token = await decodeTokenWithKeysets(encodeTokenFromKeyset(LEGACY_KEYSET_ID), MINT_URL)
|
||||
|
||||
expect(token.proofs[0].id).toBe(LEGACY_KEYSET_ID)
|
||||
expect(mockWalletStore.refreshKeysetsNow).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('propagates an unrelated decode failure without calling the mint', async () => {
|
||||
setStoredKeysetIds([LEGACY_KEYSET_ID])
|
||||
|
||||
await expect(decodeTokenWithKeysets('not-a-cashu-token', MINT_URL)).rejects.toThrow()
|
||||
expect(mockWalletStore.refreshKeysetsNow).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('surfaces a refresh failure rather than a confusing decode error', async () => {
|
||||
setStoredKeysetIds([LEGACY_KEYSET_ID])
|
||||
mockWalletStore.refreshKeysetsNow.mockRejectedValue(new Error('Mint is offline'))
|
||||
|
||||
await expect(
|
||||
decodeTokenWithKeysets(encodeTokenFromKeyset(V2_KEYSET_ID), MINT_URL),
|
||||
).rejects.toThrow('Mint is offline')
|
||||
})
|
||||
|
||||
test('still fails if the mint does not know the keyset either', async () => {
|
||||
setStoredKeysetIds([LEGACY_KEYSET_ID])
|
||||
// Refresh succeeds but brings nothing new — a token from a different mint, say.
|
||||
mockWalletStore.refreshKeysetsNow.mockResolvedValue(undefined)
|
||||
|
||||
await expect(
|
||||
decodeTokenWithKeysets(encodeTokenFromKeyset(V2_KEYSET_ID), MINT_URL),
|
||||
).rejects.toThrow(/short keyset id/i)
|
||||
expect(mockWalletStore.refreshKeysetsNow).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minibits_wallet",
|
||||
"version": "0.4.3-beta.17",
|
||||
"version": "0.4.3-beta.19",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android:clean": "cd android && ./gradlew clean",
|
||||
|
||||
+85
-18
@@ -34,7 +34,7 @@ import { MINT_INFO_TTL_SECONDS, isMintInfoStale } from './helpers/mintInfoStale'
|
||||
import { getRootStore } from './helpers/getRootStore'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
// refresh
|
||||
//
|
||||
|
||||
/*
|
||||
Not persisted, in-memory only model of the cashu-ts wallet instances and wallet keys persisted in the device secure store.
|
||||
@@ -273,6 +273,59 @@ export const WalletStoreModel = types
|
||||
log.warn('[WalletStore.refreshMintInfoIfStale]', {mintUrl, error: e.message})
|
||||
}
|
||||
}),
|
||||
/**
|
||||
* Pull the mint's keyset list into the Mint model.
|
||||
*
|
||||
* Extracted from getMint, which is still the only routine caller: keysets are
|
||||
* synced ONCE per process, on the first touch of a mint. There is deliberately
|
||||
* no periodic refresh — the wallet is told when its list is wrong instead. A
|
||||
* mint that rotates keysets (a nutshell -> cdk migration issues a new v2 `01…`
|
||||
* keyset and flips the old `00…` ones inactive) makes the stale list fail
|
||||
* loudly at exactly one place, the decode of an incoming token, because a v4
|
||||
* token carries only an 8-byte PREFIX of a v2 id and getDecodedToken can map
|
||||
* it back only through ids the wallet already knows. That failure calls
|
||||
* refreshKeysetsNow and retries; polling would spend a /v1/keysets GET on
|
||||
* every background wake to shorten a window that a cold start closes anyway.
|
||||
*
|
||||
* The keyset fetch runs BEFORE the model lookup on purpose: it is also how
|
||||
* getMint learns the mint is reachable at all, and callers rely on that
|
||||
* throw. `getKeys()` returns ACTIVE keys only (NUT-01), so it is worth
|
||||
* calling only when a keyset we have never seen showed up.
|
||||
*/
|
||||
syncKeysets: flow(function* syncKeysets(mintUrl: string, cashuMint: CashuMint) {
|
||||
// All keysets, active and inactive.
|
||||
const {keysets} = yield cashuMint.getKeySets() as Promise<GetKeysetsResponse>
|
||||
|
||||
// No model yet — this is a mint being added, and addMint seeds keysets and
|
||||
// keys itself.
|
||||
const mintInstance = self.getMintModelInstance(mintUrl)
|
||||
if (!mintInstance) return
|
||||
|
||||
const newKeysets = keysets.filter((freshKeyset: MintKeyset) => {
|
||||
return !mintInstance.keysets!.some((keyset: MintKeyset) => keyset.id === freshKeyset.id)
|
||||
})
|
||||
|
||||
if (newKeysets.length > 0) {
|
||||
const {keysets: keys} = yield cashuMint.getKeys() as Promise<GetKeysResponse>
|
||||
|
||||
// The mint may have been removed while the calls were in flight.
|
||||
if (!isAlive(mintInstance)) return
|
||||
|
||||
mintInstance.refreshKeys!(keys)
|
||||
}
|
||||
|
||||
if (!isAlive(mintInstance)) return
|
||||
|
||||
// Adds what is new, and syncs active flags and input fees on what is not.
|
||||
mintInstance.refreshKeysets!(keysets)
|
||||
|
||||
if (newKeysets.length > 0) {
|
||||
log.debug('[WalletStore.syncKeysets]', 'New keysets synced', {
|
||||
mintUrl,
|
||||
newKeysetIds: newKeysets.map((k: MintKeyset) => k.id),
|
||||
})
|
||||
}
|
||||
}),
|
||||
}))
|
||||
.actions(self => ({
|
||||
getMint: flow(function* getMint(mintUrl: string) {
|
||||
@@ -286,6 +339,12 @@ export const WalletStoreModel = types
|
||||
// once in a session never refreshed again for the rest of it. Fire-and-forget
|
||||
// so the caller's operation is not delayed by a getInfo() round trip;
|
||||
// observers re-render when fresher info lands.
|
||||
//
|
||||
// Keysets deliberately get NO periodic refresh to match: they are pulled
|
||||
// once per process (below) and then only when something proves the list
|
||||
// wrong — see refreshKeysetsNow. Polling them would put a /v1/keysets GET
|
||||
// on every NWC background wake to cover a window (a session alive across a
|
||||
// mint's keyset rotation) that a cold start closes by itself.
|
||||
void self.refreshMintInfoIfStale(mintUrl, mint as CashuMint)
|
||||
return mint as CashuMint
|
||||
}
|
||||
@@ -296,29 +355,16 @@ export const WalletStoreModel = types
|
||||
// create cashu-ts mint instance
|
||||
const newMint = new CashuMint(mintUrl)
|
||||
|
||||
// get fresh keysets - returns all keysets, both active and inactive
|
||||
const {keysets} = yield newMint.getKeySets()
|
||||
// First touch in this process: sync keysets, keys and active statuses.
|
||||
// Throws when the mint is unreachable, which is how callers learn it is
|
||||
// offline — so this is awaited, not fired off.
|
||||
yield self.syncKeysets(mintUrl, newMint)
|
||||
|
||||
// get persisted mint model from wallet state
|
||||
const mintInstance = self.getMintModelInstance(mintUrl)
|
||||
|
||||
// skip checks if this is new mint being added
|
||||
if(mintInstance) {
|
||||
const newKeysets = keysets.filter((freshKeyset: MintKeyset) => {
|
||||
return !mintInstance.keysets!.some((keyset: MintKeyset) => keyset.id === freshKeyset.id)
|
||||
})
|
||||
|
||||
if(newKeysets.length > 0) {
|
||||
// if we have new keysets, get and sync new keys
|
||||
// this, for perf reasons, returns ONLY active keys so
|
||||
// mintInstance can not be directly used to restore from inactive keysets
|
||||
const {keysets: keys} = yield newMint.getKeys() as Promise<GetKeysResponse>
|
||||
mintInstance.refreshKeys!(keys)
|
||||
}
|
||||
|
||||
// sync wallet state with fresh keysets, active statuses and keys
|
||||
mintInstance.refreshKeysets!(keysets)
|
||||
|
||||
// fetch and cache mintInfo if not already cached or gone stale
|
||||
if(isMintInfoStale(mintInstance.mintInfo)) {
|
||||
const info: GetInfoResponse = yield newMint.getInfo()
|
||||
@@ -333,6 +379,27 @@ export const WalletStoreModel = types
|
||||
})
|
||||
}))
|
||||
.actions(self => ({
|
||||
/**
|
||||
* Re-pull the mint's keyset list NOW, and wait for it.
|
||||
*
|
||||
* The wallet's one signal that its list has gone stale: a token decode that
|
||||
* failed because a v2 keyset id — which travels through a v4 token as an
|
||||
* 8-byte prefix — could not be expanded against any id the wallet holds. Since
|
||||
* keysets are otherwise pulled once per process, this is what closes the gap
|
||||
* after a mint rotates its keysets, and it is why nothing has to poll.
|
||||
*
|
||||
* getMint's own cold path has just synced when it had to CREATE the cashu-ts
|
||||
* instance, so the second pull is skipped in that case rather than fetching
|
||||
* the same list twice.
|
||||
*/
|
||||
refreshKeysetsNow: flow(function* refreshKeysetsNow(mintUrl: string) {
|
||||
const wasCached = self.mints.some(m => m.mintUrl === mintUrl)
|
||||
const cashuMint: CashuMint = yield self.getMint(mintUrl)
|
||||
|
||||
if (wasCached) {
|
||||
yield self.syncKeysets(mintUrl, cashuMint)
|
||||
}
|
||||
}),
|
||||
getWallet: flow(function* getWallet(
|
||||
mintUrl: string,
|
||||
unit: MintUnit,
|
||||
|
||||
@@ -38,7 +38,7 @@ import { TransactionStatus } from '../models/Transaction'
|
||||
import { maxTransactionsInHistory } from '../models/TransactionsStore'
|
||||
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
|
||||
|
||||
// refresh
|
||||
//
|
||||
|
||||
type Props = StaticScreenProps<undefined>
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import {Token, getDecodedToken} from '@cashu/cashu-ts'
|
||||
import {log} from '../logService'
|
||||
import {rootStoreInstance} from '../../models'
|
||||
|
||||
/**
|
||||
* Did this decode fail because a v2 keyset id could not be mapped?
|
||||
*
|
||||
* Matched on the message because cashu-ts raises both variants as a plain error with
|
||||
* no code to switch on. Kept narrow — any other decode failure (malformed token,
|
||||
* unsupported version) must propagate untouched rather than trigger a mint call.
|
||||
*/
|
||||
const isUnmappableKeysetIdError = function (e: any): boolean {
|
||||
const message: string = typeof e?.message === 'string' ? e.message : ''
|
||||
return /short keyset id/i.test(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an incoming cashu token, recovering from a keyset list that has gone stale.
|
||||
*
|
||||
* NUT-00 v4 tokens do not carry a full keyset id. A v0 id (`00…`) is 8 bytes and
|
||||
* survives the round trip intact, but a NUT-02 v2 id (`01…`) is 33 bytes and is
|
||||
* truncated to its first 8 bytes on encode — so cashu-ts can only give a proof its
|
||||
* real id by matching that prefix against the ids the wallet already holds for the
|
||||
* mint. Hand it a list without the match and it throws:
|
||||
*
|
||||
* Couldn't map short keyset ID 01fc0ec0e59cd6fa to any known keysets of the current Mint
|
||||
* A short keyset ID v2 was encountered, but got no keysets to map it to.
|
||||
*
|
||||
* Which is exactly what a mint issuing a new keyset produces (a nutshell -> cdk
|
||||
* migration flips the old `00…` keysets inactive and starts signing with a v2 one):
|
||||
* every wallet that has not touched that mint since the rotation holds a list that
|
||||
* cannot decode the ecash now being sent to it — background lightning-address claims
|
||||
* and nostr receives included. Nothing in the receive path needed the mint before the
|
||||
* decode, so nothing refreshed the list, and the user had no way to know that
|
||||
* performing some unrelated operation on the mint was what would fix it.
|
||||
*
|
||||
* So: try the list we have — the free path, and the only path a v0 id ever needs —
|
||||
* and only once a decode has PROVED the list wrong, re-pull the keysets and try
|
||||
* again. No extra request is spent on the common case, and the retry needs the ids
|
||||
* only, not the keys (keys for a proof's own keyset are loaded later, by
|
||||
* WalletStore.receive's ensureKeysetKeys).
|
||||
*/
|
||||
export const decodeTokenWithKeysets = async function (
|
||||
encodedToken: string,
|
||||
mintUrl: string,
|
||||
): Promise<Token> {
|
||||
const {mintsStore, walletStore} = rootStoreInstance
|
||||
|
||||
const keysetIds = () => mintsStore.findByUrl(mintUrl)?.keysetIds ?? []
|
||||
|
||||
try {
|
||||
return getDecodedToken(encodedToken, keysetIds())
|
||||
} catch (e: any) {
|
||||
if (!isUnmappableKeysetIdError(e)) {
|
||||
throw e
|
||||
}
|
||||
|
||||
log.info(
|
||||
'[decodeTokenWithKeysets]',
|
||||
'Token references an unknown keyset, refreshing mint keysets',
|
||||
{mintUrl, error: e.message},
|
||||
)
|
||||
|
||||
// A failure here surfaces: offline, or a mint that genuinely does not know
|
||||
// this keyset. Either way the decode below could not have succeeded.
|
||||
await walletStore.refreshKeysetsNow(mintUrl)
|
||||
|
||||
return getDecodedToken(encodedToken, keysetIds())
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {UnsignedEvent} from 'nostr-tools'
|
||||
import {
|
||||
PaymentRequestPayload,
|
||||
Token,
|
||||
getDecodedToken,
|
||||
getTokenMetadata,
|
||||
decodePaymentRequest,
|
||||
} from '@cashu/cashu-ts'
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
receiveTask,
|
||||
receiveByCashuPaymentRequestTask,
|
||||
} from '../receiveTask'
|
||||
import {decodeTokenWithKeysets} from '../decodeToken'
|
||||
import {WalletUtils} from '../utils'
|
||||
import {
|
||||
HANDLE_CLAIM_TASK,
|
||||
@@ -129,10 +129,16 @@ const handleClaimTask = async function (params: {
|
||||
}, Err.NOTFOUND_ERROR)
|
||||
}
|
||||
|
||||
decoded = getDecodedToken(encodedToken, mintKeysetIds)
|
||||
// Refreshes the keysets and retries if the server minted this on a keyset
|
||||
// the wallet has not seen yet — the claim is already consumed server-side
|
||||
// at this point, so a decode failure here strands the ecash.
|
||||
const decodedToken = await decodeTokenWithKeysets(encodedToken, tokenInfo.mint)
|
||||
// Mirrored into the outer binding purely so the catch below can still report
|
||||
// which mint the claim was for.
|
||||
decoded = decodedToken
|
||||
|
||||
const result: TransactionTaskResult = await receiveTask(
|
||||
decoded,
|
||||
decodedToken,
|
||||
Number(tokenInfo.amount),
|
||||
tokenInfo.memo || 'Received to Lightning address',
|
||||
encodedToken,
|
||||
@@ -160,11 +166,11 @@ const handleClaimTask = async function (params: {
|
||||
}
|
||||
|
||||
return {
|
||||
mintUrl: decoded.mint,
|
||||
mintUrl: decodedToken.mint,
|
||||
taskFunction: HANDLE_CLAIM_TASK,
|
||||
message: result.error ? result.error.message : 'Ecash sent to your lightning address has been received.',
|
||||
error: result.error || undefined,
|
||||
proofsCount: decoded.proofs.length,
|
||||
proofsCount: decodedToken.proofs.length,
|
||||
proofsAmount: result.transaction?.amount,
|
||||
} as WalletTaskResult
|
||||
|
||||
@@ -434,7 +440,10 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro
|
||||
}, Err.NOTFOUND_ERROR)
|
||||
}
|
||||
|
||||
const decoded = getDecodedToken(incoming.encoded, mintKeysetIds)
|
||||
// Refreshes the keysets and retries if the sender's ecash comes from a
|
||||
// keyset this wallet has not seen yet (a mint that rotated keysets since
|
||||
// the last time anything here touched it).
|
||||
const decoded = await decodeTokenWithKeysets(incoming.encoded as string, mintUrl)
|
||||
|
||||
const {transaction, receivedAmount} = await receiveTask(
|
||||
decoded,
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
normalizeProofAmounts,
|
||||
} from '@cashu/cashu-ts'
|
||||
import {log} from '../../logService'
|
||||
import {decodeTokenWithKeysets} from '../decodeToken'
|
||||
import {MintError, ValidationError, WalletError} from '../../../utils/AppError'
|
||||
import {rootStoreInstance} from '../../../models'
|
||||
import {
|
||||
@@ -304,7 +305,10 @@ async function execute(prepared: PreparedReceiveData): Promise<CompletedTransact
|
||||
throw new ValidationError('Missing mint', {mintUrl: prepared.mintUrl})
|
||||
}
|
||||
|
||||
const token = getDecodedToken(tx.inputToken, mintInstance.keysetIds ?? [])
|
||||
// Refreshes the keysets and retries when the stored token turns out to carry a
|
||||
// keyset id the wallet does not know — an offline-prepared receive can be
|
||||
// executed long after it was prepared, by which time the mint may have rotated.
|
||||
const token = await decodeTokenWithKeysets(tx.inputToken, mintInstance.mintUrl)
|
||||
|
||||
// ── Swap proofs with the mint (with outputs-error healing retry) ────
|
||||
const {proofs, swapFeePaid} = await _receiveWithHealing(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Token, TokenMetadata, getDecodedToken, getEncodedToken} from '@cashu/cashu-ts'
|
||||
import {Token, TokenMetadata, getEncodedToken} from '@cashu/cashu-ts'
|
||||
import {Mint} from '../../../models/Mint'
|
||||
import {CashuUtils} from '../../cashu/cashuUtils'
|
||||
import {decodeTokenWithKeysets} from '../decodeToken'
|
||||
import {MintUnit} from '../currency'
|
||||
import {
|
||||
receiveTask,
|
||||
@@ -93,7 +94,7 @@ const receiveQueueAwaitable = (
|
||||
taskFunction: useBatch ? 'receiveBatchTask' : 'receiveTask',
|
||||
timeoutMessage: 'receiveQueue timed out',
|
||||
task: async () => {
|
||||
const token = getDecodedToken(encodedToken, mint.keysetIds ?? [])
|
||||
const token = await decodeTokenWithKeysets(encodedToken, mint.mintUrl)
|
||||
return (useBatch
|
||||
? await receiveBatchTask(token, amount, memo || '', encodedToken)
|
||||
: await receiveTask(token, amount, memo || '', encodedToken)) as TransactionTaskResult
|
||||
|
||||
Reference in New Issue
Block a user