Skip to content

Commit cc8533e

Browse files
committed
feat(sdk-core): add withdrawFromVault() to DefiVault
DEFI-237
1 parent 4aa0484 commit cc8533e

10 files changed

Lines changed: 347 additions & 12 deletions

File tree

examples/ts/defi-vault-deposit.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Deposit into a DeFi ERC-4626 vault on staging.
3+
*
4+
* Usage:
5+
* STAGING_ACCESS_TOKEN=<token> \
6+
* STAGING_WALLET_ID=<walletId> \
7+
* STAGING_WALLET_PASSPHRASE=<passphrase> \
8+
* DEFI_VAULT_ID=<vaultId> \
9+
* DEFI_DEPOSIT_AMOUNT=<amountInBaseUnits> \
10+
* npx ts-node examples/ts/defi-vault-deposit.ts
11+
*
12+
* Copyright 2026, BitGo, Inc. All Rights Reserved.
13+
*/
14+
import { BitGo } from 'bitgo';
15+
16+
require('dotenv').config({ path: '../../.env' });
17+
18+
const config = {
19+
accessToken: '',
20+
env: 'staging',
21+
walletId: '',
22+
vaultId: 'tbaseeth-usdc-test',
23+
amount: 1000000, // 1 USDC
24+
passphrase: '',
25+
coin: 'tbaseeth',
26+
otp: '000000',
27+
};
28+
29+
const bitgoTest = new BitGo({
30+
env: 'staging',
31+
});
32+
33+
//bitgo.register('tbaseeth', TethLikeCoin.createInstance);
34+
35+
async function main() {
36+
console.log('Connecting to staging...');
37+
bitgoTest.authenticateWithAccessToken({ accessToken: config.accessToken });
38+
//await bitgoTest.unlock({ otp: config.otp, duration: 3600 });
39+
const wallet = await bitgoTest.coin('tbaseeth').wallets().get({ id: config.walletId });
40+
console.log('Wallet ID :', wallet.id());
41+
console.log('Vault ID :', config.vaultId);
42+
console.log('Amount :', config.amount, '(base units)');
43+
44+
console.log('\nStarting deposit...');
45+
const result = await wallet.defi.depositToVault({
46+
vaultId: config.vaultId,
47+
amount: config.amount.toString(),
48+
...(config.passphrase ? { walletPassphrase: config.passphrase } : {}),
49+
});
50+
51+
console.log('\nDeposit complete:');
52+
console.log(' operationId :', result.operationId);
53+
console.log(' approve txRequestId :', result.txRequestIds.approve);
54+
console.log(' deposit txRequestId :', result.txRequestIds.deposit);
55+
console.log('\nFull result:', JSON.stringify(result, null, 2));
56+
}
57+
58+
main().catch((e) => {
59+
console.error('Error:', e.message);
60+
if (e.stack) console.error(e.stack);
61+
process.exit(1);
62+
});

examples/ts/defi-vault-withdraw.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Withdraw vault shares from a DeFi ERC-4626 vault on staging.
3+
*
4+
* Run the deposit script first, then use this to withdraw.
5+
*
6+
* Usage:
7+
* STAGING_ACCESS_TOKEN=<token> \
8+
* STAGING_WALLET_ID=<walletId> \
9+
* STAGING_WALLET_PASSPHRASE=<passphrase> \
10+
* DEFI_VAULT_ID=<vaultId> \
11+
* DEFI_WITHDRAW_AMOUNT=<shareTokenAmountInBaseUnits> \
12+
* npx ts-node examples/ts/defi-vault-withdraw.ts
13+
*
14+
* Copyright 2026, BitGo, Inc. All Rights Reserved.
15+
*/
16+
import { BitGo } from 'bitgo';
17+
18+
require('dotenv').config({ path: '../../.env' });
19+
20+
const config = {
21+
accessToken: '',
22+
env: 'staging',
23+
walletId: '',
24+
vaultId: 'tbaseeth-usdc-test',
25+
amount: 1000000, // vault share base units
26+
passphrase: '',
27+
coin: 'tbaseeth',
28+
otp: '000000',
29+
};
30+
31+
const bitgoTest = new BitGo({
32+
env: 'staging',
33+
});
34+
35+
//bitgo.register('tbaseeth', TethLikeCoin.createInstance);
36+
37+
async function main() {
38+
console.log('Connecting to staging...');
39+
bitgoTest.authenticateWithAccessToken({ accessToken: config.accessToken });
40+
//await bitgoTest.unlock({ otp: config.otp, duration: 3600 });
41+
const wallet = await bitgoTest.coin('tbaseeth').wallets().get({ id: config.walletId });
42+
console.log('Wallet ID :', wallet.id());
43+
console.log('Vault ID :', config.vaultId);
44+
console.log('Amount :', config.amount, '(vault share base units)');
45+
46+
console.log('\nStarting withdrawal...');
47+
const result = await wallet.defi.withdrawFromVault({
48+
vaultId: config.vaultId,
49+
amount: config.amount.toString(),
50+
...(config.passphrase ? { walletPassphrase: config.passphrase } : {}),
51+
});
52+
53+
console.log('\nWithdrawal complete:');
54+
console.log(' operationId :', result.operationId);
55+
console.log(' txRequestId :', result.txRequestId);
56+
console.log('\nFull result:', JSON.stringify(result, null, 2));
57+
}
58+
59+
main().catch((e) => {
60+
console.error('Error:', e.message);
61+
if (e.stack) console.error(e.stack);
62+
process.exit(1);
63+
});

modules/sdk-core/src/bitgo/defi/defiVault.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
IDefiVault,
1616
ListOperationsOptions,
1717
ResumeDepositOptions,
18+
WithdrawFromVaultOptions,
19+
WithdrawResult,
1820
} from './iDefiVault';
1921
import { IWallet } from '../wallet';
2022
import { BitGoBase } from '../bitgoBase';
@@ -76,7 +78,6 @@ export class DefiVault implements IDefiVault {
7678
*
7779
* @param params.vaultId - DeFi-service vault identifier
7880
* @param params.amount - amount in base units of the underlying asset
79-
* @param params.clientIdempotencyKey - optional client idempotency key
8081
* @param params.walletPassphrase - required for hot wallets, omit for custody
8182
*/
8283
async depositToVault(params: DepositToVaultOptions): Promise<DepositResult> {
@@ -139,7 +140,6 @@ export class DefiVault implements IDefiVault {
139140
defiParams: {
140141
vaultId: params.vaultId,
141142
amount: params.amount,
142-
...(params.clientIdempotencyKey ? { clientIdempotencyKey: params.clientIdempotencyKey } : {}),
143143
},
144144
...(params.walletPassphrase ? { walletPassphrase: params.walletPassphrase } : {}),
145145
});
@@ -158,7 +158,6 @@ export class DefiVault implements IDefiVault {
158158
vaultId: params.vaultId,
159159
amount: params.amount,
160160
operationId,
161-
...(params.clientIdempotencyKey ? { clientIdempotencyKey: params.clientIdempotencyKey } : {}),
162161
},
163162
...(params.walletPassphrase ? { walletPassphrase: params.walletPassphrase } : {}),
164163
});
@@ -254,6 +253,44 @@ export class DefiVault implements IDefiVault {
254253
return await this.bitgo.get(this.bitgo.microservicesUrl(this.operationsUrl())).query(query).result();
255254
}
256255

256+
/**
257+
* Withdraw vault shares from a DeFi vault.
258+
*
259+
* Issues a single sendMany call (defiWithdraw) and returns the operationId
260+
* and txRequestId. The state machine for withdrawal is simpler than deposit:
261+
* CREATED → WITHDRAW_TX_REQUESTED → WITHDRAW_SIGNED → WITHDRAW_CONFIRMED → COMPLETED
262+
*
263+
* @param params.vaultId - DeFi-service vault identifier
264+
* @param params.amount - amount in base units of the vault share token
265+
* @param params.walletPassphrase - required for hot wallets, omit for custody
266+
*/
267+
async withdrawFromVault(params: WithdrawFromVaultOptions): Promise<WithdrawResult> {
268+
if (!params.vaultId) {
269+
throw new Error('vaultId is required');
270+
}
271+
if (!params.amount) {
272+
throw new Error('amount is required');
273+
}
274+
275+
const withdrawResult = await this.wallet.sendMany({
276+
type: 'defiWithdraw',
277+
defiParams: {
278+
vaultId: params.vaultId,
279+
amount: params.amount,
280+
},
281+
...(params.walletPassphrase ? { walletPassphrase: params.walletPassphrase } : {}),
282+
});
283+
284+
const txRequestId = this.extractTxRequestId(withdrawResult);
285+
const operationId = this.extractOperationId(withdrawResult);
286+
287+
if (!operationId) {
288+
throw new Error('operationId not found in withdraw txRequest response');
289+
}
290+
291+
return { operationId, txRequestId };
292+
}
293+
257294
// ── Internal helpers ────────────────────────────────────────────────
258295

259296
/**

modules/sdk-core/src/bitgo/defi/iDefiVault.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ export interface DepositToVaultOptions {
99
vaultId: string;
1010
/** Amount in base units of the underlying asset */
1111
amount: string;
12-
/** Optional client-supplied idempotency key */
13-
clientIdempotencyKey?: string;
1412
/** Wallet passphrase — required for hot wallets, omit for custody */
1513
walletPassphrase?: string;
1614
}
@@ -68,10 +66,25 @@ export interface DefiOperationListResult {
6866
nextCursor?: string;
6967
}
7068

69+
export interface WithdrawFromVaultOptions {
70+
/** DeFi-service vault identifier */
71+
vaultId: string;
72+
/** Amount in base units of the vault share token */
73+
amount: string;
74+
/** Wallet passphrase — required for hot wallets, omit for custody */
75+
walletPassphrase?: string;
76+
}
77+
78+
export interface WithdrawResult {
79+
operationId: string;
80+
txRequestId: string;
81+
}
82+
7183
export interface IDefiVault {
7284
depositToVault(params: DepositToVaultOptions): Promise<DepositResult>;
7385
resumeDeposit(params: ResumeDepositOptions): Promise<DepositResult>;
7486
getOperation(params: GetOperationOptions): Promise<DefiOperation>;
7587
listOperations(params: ListOperationsOptions): Promise<DefiOperationListResult>;
7688
getVaultConfig(params: GetVaultConfigOptions): Promise<GetVaultResponse>;
89+
withdrawFromVault(params: WithdrawFromVaultOptions): Promise<WithdrawResult>;
7790
}

modules/sdk-core/src/bitgo/utils/mpcUtils.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export abstract class MpcUtils {
221221
'cantonParticipantOnboardingRequest',
222222
'defi-approve',
223223
'defi-deposit',
224+
'defi-withdraw',
224225
].includes(params.intentType)
225226
) {
226227
assert(params.recipients, `'recipients' is a required parameter for ${params.intentType} intent`);
@@ -322,9 +323,15 @@ export abstract class MpcUtils {
322323
vaultId: params.defiParams.vaultId,
323324
amount: params.defiParams.amount,
324325
...(params.defiParams.operationId && { operationId: params.defiParams.operationId }),
325-
...(params.defiParams.clientIdempotencyKey && {
326-
clientIdempotencyKey: params.defiParams.clientIdempotencyKey,
327-
}),
326+
};
327+
}
328+
case 'defi-withdraw': {
329+
assert(params.defiParams, `'defiParams' is required for ${params.intentType} intent`);
330+
return {
331+
...baseIntent,
332+
vaultId: params.defiParams.vaultId,
333+
// DefiWithdrawIntent uses shareTokenAmount (vault shares, base units)
334+
shareTokenAmount: params.defiParams.amount,
328335
};
329336
}
330337
default:

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,16 +287,16 @@ interface IntentOptionsBase {
287287
export interface DefiIntentFields {
288288
vaultId?: string;
289289
amount?: { value: string; symbol: string } | string;
290+
/** Vault share token amount for defi-withdraw intent (base units) */
291+
shareTokenAmount?: { value: string; symbol: string } | string;
290292
operationId?: string;
291-
clientIdempotencyKey?: string;
292293
}
293294

294295
/** DeFi-specific intent parameters (input container for defiParams). */
295296
export interface DefiIntentParams {
296297
vaultId: string;
297298
amount: string;
298299
operationId?: string;
299-
clientIdempotencyKey?: string;
300300
}
301301

302302
export interface IntentOptionsForMessage extends IntentOptionsBase {

modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const NO_RECIPIENT_TX_TYPES = new Set([
3030
// DeFi vault operations — recipients/calldata built server-side from defiParams
3131
'defiApprove',
3232
'defiDeposit',
33+
'defiWithdraw',
3334
// Smart contract invocations with no explicit SDK-level recipients
3435
'contractCall',
3536

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
TokenType,
5252
TxRequest,
5353
} from '../utils';
54+
import { decodeWithCodec } from '../utils/codecs';
5455
import { postWithCodec } from '../utils/postWithCodec';
5556
import { txParamsFromIntent } from '../utils/tss/baseTSSUtils';
5657
import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
@@ -4445,7 +4446,6 @@ export class Wallet implements IWallet {
44454446
defiParams: params.defiParams as {
44464447
vaultId: string;
44474448
amount: string;
4448-
clientIdempotencyKey?: string;
44494449
},
44504450
},
44514451
apiVersion,
@@ -4461,13 +4461,29 @@ export class Wallet implements IWallet {
44614461
vaultId: string;
44624462
amount: string;
44634463
operationId?: string;
4464-
clientIdempotencyKey?: string;
44654464
},
44664465
},
44674466
apiVersion,
44684467
params.preview
44694468
);
44704469
break;
4470+
case 'defiWithdraw': {
4471+
const defiWithdrawParams = decodeWithCodec(
4472+
t.type({ vaultId: t.string, amount: t.string }),
4473+
params.defiParams,
4474+
'defiWithdraw.defiParams'
4475+
);
4476+
txRequest = await this.tssUtils!.prebuildTxWithIntent(
4477+
{
4478+
reqId,
4479+
intentType: 'defi-withdraw',
4480+
defiParams: defiWithdrawParams,
4481+
},
4482+
apiVersion,
4483+
params.preview
4484+
);
4485+
break;
4486+
}
44714487
default:
44724488
throw new Error(`transaction type not supported: ${params.type}`);
44734489
}

0 commit comments

Comments
 (0)