Skip to content
Open
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
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ module.exports = {
'CHALO-',
'CECHO-',
'CSHLD-',
'DEFI-',
'#', // Prefix used by GitHub issues
],
},
Expand Down
62 changes: 62 additions & 0 deletions examples/ts/defi-vault-deposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Deposit into a DeFi ERC-4626 vault on staging.
*
* Usage:
* STAGING_ACCESS_TOKEN=<token> \
* STAGING_WALLET_ID=<walletId> \
* STAGING_WALLET_PASSPHRASE=<passphrase> \
* DEFI_VAULT_ID=<vaultId> \
* DEFI_DEPOSIT_AMOUNT=<amountInBaseUnits> \
* npx ts-node examples/ts/defi-vault-deposit.ts
*
* Copyright 2026, BitGo, Inc. All Rights Reserved.
*/
import { BitGo } from 'bitgo';

require('dotenv').config({ path: '../../.env' });

const config = {
accessToken: '',
env: 'staging',
walletId: '',
vaultId: 'tbaseeth-usdc-test',
amount: 1000000, // 1 USDC
passphrase: '',
coin: 'tbaseeth',
otp: '000000',
};

const bitgoTest = new BitGo({
env: 'staging',
});

//bitgo.register('tbaseeth', TethLikeCoin.createInstance);

async function main() {
console.log('Connecting to staging...');
bitgoTest.authenticateWithAccessToken({ accessToken: config.accessToken });
//await bitgoTest.unlock({ otp: config.otp, duration: 3600 });
const wallet = await bitgoTest.coin('tbaseeth').wallets().get({ id: config.walletId });
console.log('Wallet ID :', wallet.id());
console.log('Vault ID :', config.vaultId);
console.log('Amount :', config.amount, '(base units)');

console.log('\nStarting deposit...');
const result = await wallet.defi.depositToVault({
vaultId: config.vaultId,
amount: config.amount.toString(),
...(config.passphrase ? { walletPassphrase: config.passphrase } : {}),
});

console.log('\nDeposit complete:');
console.log(' operationId :', result.operationId);
console.log(' approve txRequestId :', result.txRequestIds.approve);
console.log(' deposit txRequestId :', result.txRequestIds.deposit);
console.log('\nFull result:', JSON.stringify(result, null, 2));
}

main().catch((e) => {
console.error('Error:', e.message);
if (e.stack) console.error(e.stack);
process.exit(1);
});
63 changes: 63 additions & 0 deletions examples/ts/defi-vault-withdraw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Withdraw vault shares from a DeFi ERC-4626 vault on staging.
*
* Run the deposit script first, then use this to withdraw.
*
* Usage:
* STAGING_ACCESS_TOKEN=<token> \
* STAGING_WALLET_ID=<walletId> \
* STAGING_WALLET_PASSPHRASE=<passphrase> \
* DEFI_VAULT_ID=<vaultId> \
* DEFI_WITHDRAW_AMOUNT=<shareTokenAmountInBaseUnits> \
* npx ts-node examples/ts/defi-vault-withdraw.ts
*
* Copyright 2026, BitGo, Inc. All Rights Reserved.
*/
import { BitGo } from 'bitgo';

require('dotenv').config({ path: '../../.env' });

const config = {
accessToken: '',
env: 'staging',
walletId: '',
vaultId: 'tbaseeth-usdc-test',
amount: 1000000, // vault share base units
passphrase: '',
coin: 'tbaseeth',
otp: '000000',
};

const bitgoTest = new BitGo({
env: 'staging',
});

//bitgo.register('tbaseeth', TethLikeCoin.createInstance);

async function main() {
console.log('Connecting to staging...');
bitgoTest.authenticateWithAccessToken({ accessToken: config.accessToken });
//await bitgoTest.unlock({ otp: config.otp, duration: 3600 });
const wallet = await bitgoTest.coin('tbaseeth').wallets().get({ id: config.walletId });
console.log('Wallet ID :', wallet.id());
console.log('Vault ID :', config.vaultId);
console.log('Amount :', config.amount, '(vault share base units)');

console.log('\nStarting withdrawal...');
const result = await wallet.defi.withdrawFromVault({
vaultId: config.vaultId,
amount: config.amount.toString(),
...(config.passphrase ? { walletPassphrase: config.passphrase } : {}),
});

console.log('\nWithdrawal complete:');
console.log(' operationId :', result.operationId);
console.log(' txRequestId :', result.txRequestId);
console.log('\nFull result:', JSON.stringify(result, null, 2));
}

main().catch((e) => {
console.error('Error:', e.message);
if (e.stack) console.error(e.stack);
process.exit(1);
});
43 changes: 40 additions & 3 deletions modules/sdk-core/src/bitgo/defi/defiVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
IDefiVault,
ListOperationsOptions,
ResumeDepositOptions,
WithdrawFromVaultOptions,
WithdrawResult,
} from './iDefiVault';
import { IWallet } from '../wallet';
import { BitGoBase } from '../bitgoBase';
Expand Down Expand Up @@ -57,7 +59,6 @@ export class DefiVault implements IDefiVault {
*
* @param params.vaultId - DeFi-service vault identifier
* @param params.amount - amount in base units of the underlying asset
* @param params.clientIdempotencyKey - optional client idempotency key
* @param params.walletPassphrase - required for hot wallets, omit for custody
*/
async depositToVault(params: DepositToVaultOptions): Promise<DepositResult> {
Expand Down Expand Up @@ -85,7 +86,6 @@ export class DefiVault implements IDefiVault {
defiParams: {
vaultId: params.vaultId,
amount: params.amount,
...(params.clientIdempotencyKey ? { clientIdempotencyKey: params.clientIdempotencyKey } : {}),
},
...(params.walletPassphrase ? { walletPassphrase: params.walletPassphrase } : {}),
});
Expand All @@ -104,7 +104,6 @@ export class DefiVault implements IDefiVault {
vaultId: params.vaultId,
amount: params.amount,
operationId,
...(params.clientIdempotencyKey ? { clientIdempotencyKey: params.clientIdempotencyKey } : {}),
},
...(params.walletPassphrase ? { walletPassphrase: params.walletPassphrase } : {}),
});
Expand Down Expand Up @@ -200,6 +199,44 @@ export class DefiVault implements IDefiVault {
return await this.bitgo.get(this.bitgo.microservicesUrl(this.operationsUrl())).query(query).result();
}

/**
* Withdraw vault shares from a DeFi vault.
*
* Issues a single sendMany call (defiWithdraw) and returns the operationId
* and txRequestId. The state machine for withdrawal is simpler than deposit:
* CREATED → WITHDRAW_TX_REQUESTED → WITHDRAW_SIGNED → WITHDRAW_CONFIRMED → COMPLETED
*
* @param params.vaultId - DeFi-service vault identifier
* @param params.amount - amount in base units of the vault share token
* @param params.walletPassphrase - required for hot wallets, omit for custody
*/
async withdrawFromVault(params: WithdrawFromVaultOptions): Promise<WithdrawResult> {
if (!params.vaultId) {
throw new Error('vaultId is required');
}
if (!params.amount) {
throw new Error('amount is required');
}

const withdrawResult = await this.wallet.sendMany({
type: 'defiWithdraw',
defiParams: {
vaultId: params.vaultId,
amount: params.amount,
},
...(params.walletPassphrase ? { walletPassphrase: params.walletPassphrase } : {}),
});

const txRequestId = this.extractTxRequestId(withdrawResult);
const operationId = this.extractOperationId(withdrawResult);

if (!operationId) {
throw new Error('operationId not found in withdraw txRequest response');
}

return { operationId, txRequestId };
}

// ── Internal helpers ────────────────────────────────────────────────

/**
Expand Down
17 changes: 15 additions & 2 deletions modules/sdk-core/src/bitgo/defi/iDefiVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ export interface DepositToVaultOptions {
vaultId: string;
/** Amount in base units of the underlying asset */
amount: string;
/** Optional client-supplied idempotency key */
clientIdempotencyKey?: string;
/** Wallet passphrase — required for hot wallets, omit for custody */
walletPassphrase?: string;
}
Expand Down Expand Up @@ -58,9 +56,24 @@ export interface DefiOperationListResult {
nextCursor?: string;
}

export interface WithdrawFromVaultOptions {
/** DeFi-service vault identifier */
vaultId: string;
/** Amount in base units of the vault share token */
amount: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bigint

/** Wallet passphrase — required for hot wallets, omit for custody */
walletPassphrase?: string;
}

export interface WithdrawResult {
operationId: string;
txRequestId: string;
}

export interface IDefiVault {
depositToVault(params: DepositToVaultOptions): Promise<DepositResult>;
resumeDeposit(params: ResumeDepositOptions): Promise<DepositResult>;
getOperation(params: GetOperationOptions): Promise<DefiOperation>;
listOperations(params: ListOperationsOptions): Promise<DefiOperationListResult>;
withdrawFromVault(params: WithdrawFromVaultOptions): Promise<WithdrawResult>;
}
13 changes: 10 additions & 3 deletions modules/sdk-core/src/bitgo/utils/mpcUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export abstract class MpcUtils {
'cantonParticipantOnboardingRequest',
'defi-approve',
'defi-deposit',
'defi-withdraw',
].includes(params.intentType)
) {
assert(params.recipients, `'recipients' is a required parameter for ${params.intentType} intent`);
Expand Down Expand Up @@ -322,9 +323,15 @@ export abstract class MpcUtils {
vaultId: params.defiParams.vaultId,
amount: params.defiParams.amount,
...(params.defiParams.operationId && { operationId: params.defiParams.operationId }),
...(params.defiParams.clientIdempotencyKey && {
clientIdempotencyKey: params.defiParams.clientIdempotencyKey,
}),
};
}
case 'defi-withdraw': {
assert(params.defiParams, `'defiParams' is required for ${params.intentType} intent`);
return {
...baseIntent,
vaultId: params.defiParams.vaultId,
// DefiWithdrawIntent uses shareTokenAmount (vault shares, base units)
shareTokenAmount: params.defiParams.amount,
};
}
default:
Expand Down
4 changes: 2 additions & 2 deletions modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,16 @@ interface IntentOptionsBase {
export interface DefiIntentFields {
vaultId?: string;
amount?: { value: string; symbol: string } | string;
/** Vault share token amount for defi-withdraw intent (base units) */
shareTokenAmount?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bigint

operationId?: string;
clientIdempotencyKey?: string;
}

/** DeFi-specific intent parameters (input container for defiParams). */
export interface DefiIntentParams {
vaultId: string;
amount: string;
operationId?: string;
clientIdempotencyKey?: string;
}

export interface IntentOptionsForMessage extends IntentOptionsBase {
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const NO_RECIPIENT_TX_TYPES = new Set([
// DeFi vault operations — recipients/calldata built server-side from defiParams
'defiApprove',
'defiDeposit',
'defiWithdraw',
// Smart contract invocations with no explicit SDK-level recipients
'contractCall',

Expand Down
16 changes: 14 additions & 2 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4445,7 +4445,6 @@ export class Wallet implements IWallet {
defiParams: params.defiParams as {
vaultId: string;
amount: string;
clientIdempotencyKey?: string;
},
},
apiVersion,
Expand All @@ -4461,7 +4460,20 @@ export class Wallet implements IWallet {
vaultId: string;
amount: string;
operationId?: string;
clientIdempotencyKey?: string;
},
},
apiVersion,
params.preview
);
break;
case 'defiWithdraw':
txRequest = await this.tssUtils!.prebuildTxWithIntent(
{
reqId,
intentType: 'defi-withdraw',
defiParams: params.defiParams as {
vaultId: string;
amount: string;
},
Comment on lines +4474 to 4477

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't cast, validate (ideally using io-ts)

},
apiVersion,
Expand Down
Loading
Loading