Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions modules/sdk-core/src/bitgo/defi/defiVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,39 @@ export class DefiVault implements IDefiVault {
}

/**
* Extract operationId from the intent of a sendMany result.
* The WP populates operationId in the intent of the approve txRequest.
* Extract operationId from a sendMany result.
*
* The WP writes the defi-service-minted operationId into the built
* transaction's `coinSpecific` (alongside `assignedNonce`), not into the
* intent. Read it from there: the `full` apiVersion surfaces it at
* `transactions[0].unsignedTx.coinSpecific.operationId`, the `lite` version
* at `unsignedTxs[0].coinSpecific.operationId`. Fall back to
* `intent.operationId` for forward-compat in case the WP later also
* populates the intent.
*/
private extractOperationId(sendManyResult: Record<string, unknown>): string | undefined {
const txRequest = sendManyResult.txRequest as Record<string, unknown> | undefined;
const intent = txRequest?.intent as Record<string, unknown> | undefined;
if (!txRequest) {
return undefined;
}

// full apiVersion: transactions[0].unsignedTx.coinSpecific.operationId
const transactions = txRequest.transactions as Array<Record<string, unknown>> | undefined;
Comment thread
kamleshmugdiya marked this conversation as resolved.
const fullUnsignedTx = transactions?.[0]?.unsignedTx as Record<string, unknown> | undefined;
const fullCoinSpecific = fullUnsignedTx?.coinSpecific as Record<string, unknown> | undefined;
if (fullCoinSpecific?.operationId) {
return fullCoinSpecific.operationId as string;
}

// lite apiVersion: unsignedTxs[0].coinSpecific.operationId
const unsignedTxs = txRequest.unsignedTxs as Array<Record<string, unknown>> | undefined;
const liteCoinSpecific = unsignedTxs?.[0]?.coinSpecific as Record<string, unknown> | undefined;
if (liteCoinSpecific?.operationId) {
return liteCoinSpecific.operationId as string;
}

// forward-compat: intent.operationId (in case the WP later populates the intent)
const intent = txRequest.intent as Record<string, unknown> | undefined;
return intent?.operationId as string | undefined;
}

Expand Down
76 changes: 71 additions & 5 deletions modules/sdk-core/test/unit/bitgo/defi/defiVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,20 @@ describe('DefiVault', function () {

// Mock sendMany for approve and deposit
const sendManyStub = sinon.stub(wallet, 'sendMany');
// WP writes operationId into the built tx's coinSpecific (full apiVersion),
// not into the intent — mirror that real shape here.
sendManyStub.onFirstCall().resolves({
txRequest: {
txRequestId: 'txreq-approve-1',
intent: { intentType: 'defi-approve', operationId },
intent: { intentType: 'defi-approve' },
transactions: [{ unsignedTx: { coinSpecific: { operationId } } }],
},
});
sendManyStub.onSecondCall().resolves({
txRequest: {
txRequestId: 'txreq-deposit-1',
intent: { intentType: 'defi-deposit', operationId },
intent: { intentType: 'defi-deposit' },
transactions: [{ unsignedTx: { coinSpecific: { operationId } } }],
},
});

Expand All @@ -91,6 +95,65 @@ describe('DefiVault', function () {
depositArgs.defiParams.operationId.should.equal(operationId);
});

it('should extract operationId from the lite apiVersion coinSpecific', async function () {
const operationId = 'op-uuid-lite';

const sendManyStub = sinon.stub(wallet, 'sendMany');
sendManyStub.onFirstCall().resolves({
txRequest: {
txRequestId: 'txreq-approve-lite',
intent: { intentType: 'defi-approve' },
unsignedTxs: [{ coinSpecific: { operationId } }],
},
});
sendManyStub.onSecondCall().resolves({
txRequest: { txRequestId: 'txreq-deposit-lite' },
});

const result = await defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' });

result.operationId.should.equal(operationId);
const depositArgs: any = sendManyStub.secondCall.args[0];
depositArgs.defiParams.operationId.should.equal(operationId);
});

it('should fall back to intent.operationId for forward-compat', async function () {
const operationId = 'op-uuid-intent';

const sendManyStub = sinon.stub(wallet, 'sendMany');
sendManyStub.onFirstCall().resolves({
txRequest: {
txRequestId: 'txreq-approve-intent',
intent: { intentType: 'defi-approve', operationId },
},
});
sendManyStub.onSecondCall().resolves({
txRequest: { txRequestId: 'txreq-deposit-intent' },
});

const result = await defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' });

result.operationId.should.equal(operationId);
});

it('should throw when operationId is absent from the approve txRequest', async function () {
const sendManyStub = sinon.stub(wallet, 'sendMany');
sendManyStub.onFirstCall().resolves({
txRequest: {
txRequestId: 'txreq-approve-missing',
intent: { intentType: 'defi-approve' },
transactions: [{ unsignedTx: { coinSpecific: {} } }],
},
});

await assert.rejects(() => defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }), {
message: 'operationId not found in approve txRequest response',
});

// Deposit sendMany must not be issued when the operationId is missing
sendManyStub.calledOnce.should.be.true();
});

// TODO(CGD-1709): Re-enable when active operation pre-flight check is restored
xit('should reject when an active operation already exists', async function () {
const preflightReq = mockRequest({
Expand All @@ -116,7 +179,8 @@ describe('DefiVault', function () {
sendManyStub.onFirstCall().resolves({
txRequest: {
txRequestId: 'txreq-approve-2',
intent: { intentType: 'defi-approve', operationId },
intent: { intentType: 'defi-approve' },
transactions: [{ unsignedTx: { coinSpecific: { operationId } } }],
},
});
sendManyStub.onSecondCall().rejects(new Error('deposit creation failed'));
Expand Down Expand Up @@ -148,13 +212,15 @@ describe('DefiVault', function () {
sendManyStub.onFirstCall().resolves({
txRequest: {
txRequestId: 'txreq-approve-3',
intent: { intentType: 'defi-approve', operationId },
intent: { intentType: 'defi-approve' },
transactions: [{ unsignedTx: { coinSpecific: { operationId } } }],
},
});
sendManyStub.onSecondCall().resolves({
txRequest: {
txRequestId: 'txreq-deposit-3',
intent: { intentType: 'defi-deposit', operationId },
intent: { intentType: 'defi-deposit' },
transactions: [{ unsignedTx: { coinSpecific: { operationId } } }],
},
});

Expand Down
Loading