From b6cdb15e1c3dd034e658eb29aaaf9916f012e3c4 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Wed, 26 Aug 2026 03:29:24 +0000 Subject: [PATCH] feat(sdk-core): support stuck transaction age filters Forward optional unconfirmed age filters through the wallet stuck transaction API so callers can avoid treating newly broadcast transactions as stuck without constructing query strings. Ticket: WCN-2443 Session-Id: 692d7cfe-5c58-429c-9202-245a2baf6534 Task-Id: 4b9b5bb9-b30d-415e-ac5f-9f15f753c9a2 --- modules/sdk-core/src/bitgo/wallet/iWallet.ts | 10 +++ modules/sdk-core/src/bitgo/wallet/wallet.ts | 20 +++++- .../unit/bitgo/wallet/getPotentialStuckTxs.ts | 67 +++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 modules/sdk-core/test/unit/bitgo/wallet/getPotentialStuckTxs.ts diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 06fca6da8a..a7d501ad94 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -467,6 +467,15 @@ export interface GetTransactionOptions extends PaginationOptions { includeRbf?: boolean; } +export interface GetPotentialStuckTxsOptions { + /** Only return transactions unconfirmed longer than this many minutes. */ + minUnconfirmedMinutes?: number; + /** Only return transactions unconfirmed longer than this many blocks. */ + minUnconfirmedBlocks?: number; + expandSendTransferMetadata?: boolean; + txid?: string; +} + export interface TransfersOptions extends PaginationOptions { txHash?: string; allTokens?: boolean; @@ -1198,6 +1207,7 @@ export interface IWallet { pendingApprovals(): IPendingApproval[]; refresh(params?: Record): Promise; transactions(params?: PaginationOptions): Promise; + getPotentialStuckTxs(params?: GetPotentialStuckTxsOptions): Promise; getTransaction(params?: GetTransactionOptions): Promise; transfers(params?: TransfersOptions): Promise; getTransfer(params?: GetTransferOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 4618486c50..519e9796d4 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -96,6 +96,7 @@ import { FundForwardersOptions, GetAddressOptions, GetPrvOptions, + GetPotentialStuckTxsOptions, GetTransactionOptions, GetTransferOptions, GetUserPrvOptions, @@ -502,7 +503,24 @@ export class Wallet implements IWallet { } /** - * Return a list of nft tokens for this wallet. Will always return undefined if the wallet + * List potentially stuck transactions for this wallet. + * Age thresholds are opt-in; when both are supplied, the API uses OR semantics. + */ + async getPotentialStuckTxs(params: GetPotentialStuckTxsOptions = {}): Promise { + const query: GetPotentialStuckTxsOptions = {}; + if (params.minUnconfirmedMinutes !== undefined) query.minUnconfirmedMinutes = params.minUnconfirmedMinutes; + if (params.minUnconfirmedBlocks !== undefined) query.minUnconfirmedBlocks = params.minUnconfirmedBlocks; + if (params.expandSendTransferMetadata !== undefined) { + query.expandSendTransferMetadata = params.expandSendTransferMetadata; + } + if (params.txid !== undefined) query.txid = params.txid; + + return this.bitgo + .get(this.baseCoin.url('/wallet/' + this._wallet.id + '/potentialStuckTxs')) + .query(query) + .result(); + } + * was not initialized with the allTokens flag. * * @returns {NftBalance[] | undefined} diff --git a/modules/sdk-core/test/unit/bitgo/wallet/getPotentialStuckTxs.ts b/modules/sdk-core/test/unit/bitgo/wallet/getPotentialStuckTxs.ts new file mode 100644 index 0000000000..66e6b7a6be --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/wallet/getPotentialStuckTxs.ts @@ -0,0 +1,67 @@ +import * as sinon from 'sinon'; +import 'should'; +import { Wallet } from '../../../../src'; + +describe('Wallet - getPotentialStuckTxs', function () { + let wallet: Wallet; + let mockBitGo: any; + let mockBaseCoin: any; + + beforeEach(function () { + mockBitGo = { get: sinon.stub() }; + mockBaseCoin = { + url: sinon.stub().returns('/api/v2/btc'), + supportsTss: sinon.stub().returns(false), + }; + wallet = new Wallet(mockBitGo, mockBaseCoin, { + id: 'test-wallet-id', + keys: ['user-key', 'backup-key', 'bitgo-key'], + }); + }); + + afterEach(function () { + sinon.restore(); + }); + + function stubGet() { + const response = [{ txId: 'tx-id' }]; + const resultStub = sinon.stub().resolves(response); + const queryStub = sinon.stub().returns({ result: resultStub }); + mockBitGo.get.returns({ query: queryStub }); + return { response, queryStub }; + } + + it('does not add age filters when no options are supplied', async function () { + const { response, queryStub } = stubGet(); + + const result = await wallet.getPotentialStuckTxs(); + + result.should.deepEqual(response); + sinon.assert.calledWith(mockBitGo.get, '/api/v2/btc/wallet/test-wallet-id/potentialStuckTxs'); + sinon.assert.calledWith(queryStub, {}); + }); + + it('forwards the minimum unconfirmed minutes filter', async function () { + const { queryStub } = stubGet(); + + await wallet.getPotentialStuckTxs({ minUnconfirmedMinutes: 30 }); + + sinon.assert.calledWith(queryStub, { minUnconfirmedMinutes: 30 }); + }); + + it('forwards the minimum unconfirmed blocks filter', async function () { + const { queryStub } = stubGet(); + + await wallet.getPotentialStuckTxs({ minUnconfirmedBlocks: 6 }); + + sinon.assert.calledWith(queryStub, { minUnconfirmedBlocks: 6 }); + }); + + it('forwards both age filters together', async function () { + const { queryStub } = stubGet(); + + await wallet.getPotentialStuckTxs({ minUnconfirmedMinutes: 30, minUnconfirmedBlocks: 6 }); + + sinon.assert.calledWith(queryStub, { minUnconfirmedMinutes: 30, minUnconfirmedBlocks: 6 }); + }); +});