From e452da3f1c6adf290338e258bd3837e4961a5eaa Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Tue, 12 Nov 2024 12:16:49 +0800 Subject: [PATCH] feat(wallet): input resolver now searches TX history if input cant be found in current UTXO set --- packages/wallet/src/global.d.ts | 15 +++++ packages/wallet/src/services/WalletUtil.ts | 9 +++ .../wallet/test/services/WalletUtil.test.ts | 64 +++++++++++++++++-- 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 packages/wallet/src/global.d.ts diff --git a/packages/wallet/src/global.d.ts b/packages/wallet/src/global.d.ts new file mode 100644 index 00000000000..2c41c10ca9d --- /dev/null +++ b/packages/wallet/src/global.d.ts @@ -0,0 +1,15 @@ +export {} + +declare global { + interface Array { + findLastIndex( + predicate: (value: T, index: number, obj: T[]) => unknown, + thisArg?: any + ): number; + + findLast( + predicate: (value: T, index: number, obj: T[]) => unknown, + thisArg?: any + ): T | undefined; + } +} diff --git a/packages/wallet/src/services/WalletUtil.ts b/packages/wallet/src/services/WalletUtil.ts index e363a3150ff..5908c0900cf 100644 --- a/packages/wallet/src/services/WalletUtil.ts +++ b/packages/wallet/src/services/WalletUtil.ts @@ -14,6 +14,7 @@ export interface InputResolverContext { available$: Observable; }; transactions: { + history$: Observable; outgoing: { signed$: Observable; }; @@ -30,6 +31,7 @@ export type WalletUtilContext = WalletOutputValidatorContext & export const createInputResolver = ({ utxo, transactions }: InputResolverContext): Cardano.InputResolver => ({ async resolveInput(input: Cardano.TxIn, options?: Cardano.ResolveOptions) { + const txHistory = await firstValueFrom(transactions.history$); const utxoAvailable = await firstValueFrom(utxo.available$, { defaultValue: [] }); const signedTransactions = await firstValueFrom(transactions.outgoing.signed$, { defaultValue: [] }); const utxoFromSigned = signedTransactions.flatMap(({ tx: signedTx }, signedTxIndex) => @@ -63,6 +65,13 @@ export const createInputResolver = ({ utxo, transactions }: InputResolverContext return tx.body.outputs[input.index]; } } + + const historyTx = txHistory.findLast((entry) => entry.id === input.txId); + + if (historyTx && historyTx.body.outputs.length > input.index) { + return historyTx.body.outputs[input.index]; + } + return null; } }); diff --git a/packages/wallet/test/services/WalletUtil.test.ts b/packages/wallet/test/services/WalletUtil.test.ts index bef28a7c523..c34fb2df592 100644 --- a/packages/wallet/test/services/WalletUtil.test.ts +++ b/packages/wallet/test/services/WalletUtil.test.ts @@ -55,7 +55,45 @@ describe('WalletUtil', () => { ] ]; const resolver = createInputResolver({ - transactions: { outgoing: { signed$: of() } }, + transactions: { history$: of([]), outgoing: { signed$: of() } }, + utxo: { available$: of(utxo) } + }); + expect( + await resolver.resolveInput({ + index: 0, + txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5') + }) + ).toEqual({ + address: 'addr_test1vr8nl4u0u6fmtfnawx2rxfz95dy7m46t6dhzdftp2uha87syeufdg', + value: { coins: 50_000_000n } + }); + expect( + await resolver.resolveInput({ + index: 0, + txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d4') + }) + ).toBeNull(); + }); + + it('resolveInput resolves inputs from tx history', async () => { + const utxo: Cardano.Utxo[] = []; + const resolver = createInputResolver({ + transactions: { + history$: of([ + { + body: { + outputs: [ + { + address: Cardano.PaymentAddress('addr_test1vr8nl4u0u6fmtfnawx2rxfz95dy7m46t6dhzdftp2uha87syeufdg'), + value: { coins: 50_000_000n } + } + ] + }, + id: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5') + } as Cardano.HydratedTx + ]), + outgoing: { signed$: of() } + }, utxo: { available$: of(utxo) } }); expect( @@ -97,7 +135,7 @@ describe('WalletUtil', () => { } as Cardano.HydratedTx; const resolver = createInputResolver({ - transactions: { outgoing: { signed$: of() } }, + transactions: { history$: of([]), outgoing: { signed$: of() } }, utxo: { available$: of([]) } }); @@ -134,7 +172,7 @@ describe('WalletUtil', () => { const signedTxs = mocks.queryTransactionsResult.pageResults.map(toSignedTx); const resolver = createInputResolver({ - transactions: { outgoing: { signed$: of(signedTxs) } }, + transactions: { history$: of([]), outgoing: { signed$: of(signedTxs) } }, utxo: { available$: of() } }); expect( @@ -273,7 +311,10 @@ describe('WalletUtil', () => { ] ]; const resolver = combineInputResolvers( - createInputResolver({ transactions: { outgoing: { signed$: of() } }, utxo: { available$: of(utxo) } }), + createInputResolver({ + transactions: { history$: of([]), outgoing: { signed$: of() } }, + utxo: { available$: of(utxo) } + }), createBackendInputResolver(createMockChainHistoryProvider()) ); @@ -315,7 +356,10 @@ describe('WalletUtil', () => { } as Cardano.HydratedTx; const resolver = combineInputResolvers( - createInputResolver({ transactions: { outgoing: { signed$: of() } }, utxo: { available$: of([]) } }), + createInputResolver({ + transactions: { history$: of([]), outgoing: { signed$: of() } }, + utxo: { available$: of([]) } + }), createBackendInputResolver(createMockChainHistoryProvider([tx])) ); @@ -395,7 +439,10 @@ describe('WalletUtil', () => { ]; const resolver = combineInputResolvers( - createInputResolver({ transactions: { outgoing: { signed$: of() } }, utxo: { available$: of(utxo) } }), + createInputResolver({ + transactions: { history$: of([]), outgoing: { signed$: of() } }, + utxo: { available$: of(utxo) } + }), createBackendInputResolver(createMockChainHistoryProvider([tx])) ); @@ -456,7 +503,10 @@ describe('WalletUtil', () => { it('resolveInput resolves to null if the input can not be found', async () => { const resolver = combineInputResolvers( - createInputResolver({ transactions: { outgoing: { signed$: of() } }, utxo: { available$: of([]) } }), + createInputResolver({ + transactions: { history$: of([]), outgoing: { signed$: of() } }, + utxo: { available$: of([]) } + }), createBackendInputResolver(createMockChainHistoryProvider()) );