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
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 @@ -15,6 +15,8 @@ import {
IDefiVault,
ListOperationsOptions,
ResumeDepositOptions,
WithdrawFromVaultOptions,
WithdrawResult,
} from './iDefiVault';
import { IWallet } from '../wallet';
import { BitGoBase } from '../bitgoBase';
Expand Down Expand Up @@ -76,7 +78,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 @@ -139,7 +140,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 @@ -158,7 +158,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 @@ -254,6 +253,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 @@ -9,8 +9,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 @@ -68,10 +66,25 @@ 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;
Comment thread
sachinBitgo marked this conversation as resolved.
/** 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>;
getVaultConfig(params: GetVaultConfigOptions): Promise<GetVaultResponse>;
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?: { value: string; symbol: string } | string;
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
1 change: 0 additions & 1 deletion modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,6 @@ export interface SendManyOptions extends PrebuildAndSignTransactionOptions {
amount?: string | number;
actionType?: string;
operationId?: string;
clientIdempotencyKey?: string;
};
}

Expand Down
21 changes: 19 additions & 2 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CoinFamily } from '@bitgo/statics';
import assert from 'assert';
import BigNumber from 'bignumber.js';
import * as t from 'io-ts';
import { BigIntFromString } from 'io-ts-types';
import * as _ from 'lodash';
import { EncryptionVersion, IRequestTracer } from '../../api';
import * as common from '../../common';
Expand Down Expand Up @@ -51,6 +52,7 @@ import {
TokenType,
TxRequest,
} from '../utils';
import { decodeWithCodec } from '../utils/codecs';
import { postWithCodec } from '../utils/postWithCodec';
import { txParamsFromIntent } from '../utils/tss/baseTSSUtils';
import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
Expand Down Expand Up @@ -4445,7 +4447,6 @@ export class Wallet implements IWallet {
defiParams: params.defiParams as {
vaultId: string;
amount: string;
clientIdempotencyKey?: string;
},
},
apiVersion,
Expand All @@ -4461,13 +4462,29 @@ export class Wallet implements IWallet {
vaultId: string;
amount: string;
operationId?: string;
clientIdempotencyKey?: string;
},
},
apiVersion,
params.preview
);
break;
case 'defiWithdraw': {
const defiWithdrawParams = decodeWithCodec(
t.type({ vaultId: t.string, amount: BigIntFromString }),
params.defiParams,
'defiWithdraw.defiParams'
);
txRequest = await this.tssUtils!.prebuildTxWithIntent(
{
reqId,
intentType: 'defi-withdraw',
defiParams: { ...defiWithdrawParams, amount: defiWithdrawParams.amount.toString() },
},
apiVersion,
params.preview
);
break;
}
default:
throw new Error(`transaction type not supported: ${params.type}`);
}
Expand Down
Loading
Loading