From ac35a1fc8712191d271e3e15988353dad609bf06 Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Fri, 28 Feb 2025 05:15:45 +0800 Subject: [PATCH] fix(wallet): base wallet will now immediately throw if tx is outside validity interval --- packages/wallet/src/Wallets/BaseWallet.ts | 31 +++++++++++ .../test/PersonalWallet/methods.test.ts | 13 +++++ .../test/services/TransactionsTracker.test.ts | 51 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/packages/wallet/src/Wallets/BaseWallet.ts b/packages/wallet/src/Wallets/BaseWallet.ts index 4738686e087..6e8cf31c2ff 100644 --- a/packages/wallet/src/Wallets/BaseWallet.ts +++ b/packages/wallet/src/Wallets/BaseWallet.ts @@ -65,9 +65,14 @@ import { EpochInfo, EraSummary, HandleProvider, + OutsideOfValidityIntervalData, + ProviderError, + ProviderFailure, RewardAccountInfoProvider, RewardsProvider, Serialization, + TxSubmissionError, + TxSubmissionErrorCode, TxSubmitProvider, UtxoProvider } from '@cardano-sdk/core'; @@ -697,6 +702,32 @@ export class BaseWallet implements ObservableWallet { { mightBeAlreadySubmitted }: SubmitTxOptions = {} ): Promise { this.#logger.debug(`Submitting transaction ${outgoingTx.id}`); + + // TODO: Workaround while we resolve LW-12394 + const { validityInterval } = outgoingTx.body; + if (validityInterval?.invalidHereafter) { + const slot = await firstValueFrom(this.tip$.pipe(map((tip) => tip.slot))); + + if (slot >= validityInterval?.invalidHereafter) { + const data: OutsideOfValidityIntervalData = { + currentSlot: slot, + validityInterval: { + invalidBefore: validityInterval?.invalidBefore, + invalidHereafter: validityInterval?.invalidHereafter + } + }; + + throw new ProviderError( + ProviderFailure.BadRequest, + new TxSubmissionError( + TxSubmissionErrorCode.OutsideOfValidityInterval, + data, + 'Not submitting transaction due to validity interval' + ) + ); + } + } + this.#newTransactions.submitting$.next(outgoingTx); try { await this.txSubmitProvider.submitTx({ diff --git a/packages/wallet/test/PersonalWallet/methods.test.ts b/packages/wallet/test/PersonalWallet/methods.test.ts index 29db2ab2b5a..2e37f5e3524 100644 --- a/packages/wallet/test/PersonalWallet/methods.test.ts +++ b/packages/wallet/test/PersonalWallet/methods.test.ts @@ -470,6 +470,19 @@ describe('BaseWallet methods', () => { expect(await txPending).toEqual(outgoingTx); }); + it('throws if transaction is outside validity interval', async () => { + const draftTx = await wallet.initializeTx(props); + draftTx.body.validityInterval = { + invalidHereafter: Cardano.Slot(1_000_000) + }; + const tx = await wallet.finalizeTx({ tx: draftTx }); + + await expect(wallet.submitTx(tx)).rejects.toThrow(); + + const txInFlight = await firstValueFrom(wallet.transactions.outgoing.inFlight$); + expect(txInFlight).toEqual([]); + }); + it('does not re-serialize the transaction to compute transaction id', async () => { // This transaction produces a different ID when round-tripping serialisation const cbor = Serialization.TxCBOR( diff --git a/packages/wallet/test/services/TransactionsTracker.test.ts b/packages/wallet/test/services/TransactionsTracker.test.ts index e5fb1c8840d..7110e8cb0e8 100644 --- a/packages/wallet/test/services/TransactionsTracker.test.ts +++ b/packages/wallet/test/services/TransactionsTracker.test.ts @@ -907,6 +907,57 @@ describe('TransactionsTracker', () => { }); }); + // TODO: Will be useful for LW-12394 investigation + it('emits timeout for transactions outside of the validity interval as failed$', async () => { + const outgoingTx = toOutgoingTx(queryTransactionsResult.pageResults[0]); + + createTestScheduler().run(({ cold, hot, expectObservable }) => { + const tip$ = hot('-a---|', { + a: { + blockNo: Cardano.BlockNo(1), + hash: '' as Cardano.BlockId, + slot: Cardano.Slot(outgoingTx.body.validityInterval!.invalidHereafter! * 2) + } + }); + const failedTx = { ...outgoingTx, id: 'x' as Cardano.TransactionId }; + const submitting$ = cold('-a---|', { a: failedTx }); + const pending$ = cold('-----|', { a: failedTx }); + const transactionsSource$ = cold('a--b-|', { a: [], b: [queryTransactionsResult.pageResults[0]] }); + const failedToSubmit$ = hot('-----|', { a: { ...failedTx, reason: TransactionFailure.FailedToSubmit } }); + const signed$ = hot('----|', {}); + const transactionsTracker = createTransactionsTracker( + { + addresses$, + chainHistoryProvider, + historicalTransactionsFetchLimit, + inFlightTransactionsStore, + logger, + newTransactions: { + failedToSubmit$, + pending$, + signed$, + submitting$ + }, + retryBackoffConfig, + signedTransactionsStore, + tip$, + transactionsHistoryStore: transactionsStore + }, + { + rollback$: NEVER, + transactionsSource$ + } + ); + expectObservable(transactionsTracker.outgoing.submitting$).toBe('-a---|', { a: failedTx }); + expectObservable(transactionsTracker.outgoing.pending$).toBe('-----|'); + expectObservable(transactionsTracker.outgoing.inFlight$).toBe('a(bc)|', { a: [], b: [failedTx], c: [] }); + expectObservable(transactionsTracker.outgoing.onChain$).toBe('-----|', { a: [failedTx] }); + expectObservable(transactionsTracker.outgoing.failed$).toBe('-a---|', { + a: { reason: TransactionFailure.Timeout, ...failedTx } + }); + }); + }); + it('emits at all relevant observable properties on transaction that failed to submit and merges reemit failures', async () => { const outgoingTx = toOutgoingTx(queryTransactionsResult.pageResults[0]); const outgoingTxReemit = toOutgoingTx(queryTransactionsResult.pageResults[1]);