Skip to content

Commit 4aa0484

Browse files
Merge pull request #9211 from BitGo/venkatesh/defi-279-deposittovault-getvaultconfig
feat(sdk-core): depositToVault with getVaultConfig and Concrete BTC support
2 parents 477e83a + 47e9153 commit 4aa0484

6 files changed

Lines changed: 411 additions & 156 deletions

File tree

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ module.exports = {
8585
'CHALO-',
8686
'CECHO-',
8787
'CSHLD-',
88+
'DEFI-',
8889
'#', // Prefix used by GitHub issues
8990
],
9091
},

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

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
/**
22
* @prettier
33
*/
4+
import * as t from 'io-ts';
5+
import { GetVaultResponse, VaultProtocol } from '@bitgo/public-types';
46
import {
7+
ConcreteDepositResult,
8+
MorphoDepositResult,
59
DefiOperation,
610
DefiOperationListResult,
711
DepositResult,
812
DepositToVaultOptions,
913
GetOperationOptions,
14+
GetVaultConfigOptions,
1015
IDefiVault,
1116
ListOperationsOptions,
1217
ResumeDepositOptions,
1318
} from './iDefiVault';
1419
import { IWallet } from '../wallet';
1520
import { BitGoBase } from '../bitgoBase';
21+
import { decodeWithCodec } from '../utils';
1622

1723
/**
1824
* Error thrown when a concurrent active deposit already exists for the (wallet, vault) pair.
@@ -47,13 +53,26 @@ export class DefiVault implements IDefiVault {
4753
this.bitgo = wallet.bitgo;
4854
}
4955

56+
/**
57+
* Fetch vault config from defi-service. Used internally to determine
58+
* which deposit path to take (Concrete vs Morpho).
59+
*/
60+
async getVaultConfig(params: GetVaultConfigOptions): Promise<GetVaultResponse> {
61+
if (!params.vaultId) {
62+
throw new Error('vaultId is required');
63+
}
64+
const raw = await this.bitgo
65+
.get(this.bitgo.microservicesUrl(`/api/defi-service/v1/vaults/${params.vaultId}`))
66+
.result();
67+
return decodeWithCodec(GetVaultResponse, raw, 'getVaultConfig');
68+
}
69+
5070
/**
5171
* Deposit an amount of underlying asset into a vault.
5272
*
53-
* Internally issues two sendMany calls (approve + deposit) and returns the
54-
* operationId that links them. If the deposit sendMany fails after
55-
* the approve succeeds, the error propagates — the server-side reconciler
56-
* handles orphaned approvals.
73+
* Dispatches to the concrete or morpho path based on vault provider.
74+
* The concrete path returns a pendingApproval (custodial wallet).
75+
* The morpho path issues two sendMany calls (approve + deposit).
5776
*
5877
* @param params.vaultId - DeFi-service vault identifier
5978
* @param params.amount - amount in base units of the underlying asset
@@ -68,6 +87,41 @@ export class DefiVault implements IDefiVault {
6887
throw new Error('amount is required');
6988
}
7089

90+
const config = await this.getVaultConfig({ vaultId: params.vaultId });
91+
92+
if (config.protocol === VaultProtocol.CONCRETE_BTCCX) {
93+
return this.depositToConcreteVault(params);
94+
} else if (config.protocol === VaultProtocol.MORPHO) {
95+
return this.depositToMorphoVault(params);
96+
} else {
97+
throw new Error(`Unsupported vault protocol: ${config.protocol}`);
98+
}
99+
}
100+
101+
/**
102+
* Concrete BTC vault deposit path. The client BTC wallet is custodial, so
103+
* sendMany returns a pendingApproval rather than a signed transfer.
104+
* No recipients are sent — WP resolves the escrow destination server-side.
105+
*/
106+
private async depositToConcreteVault(params: DepositToVaultOptions): Promise<ConcreteDepositResult> {
107+
const sendManyResult = await this.wallet.sendMany({
108+
type: 'defi-deposit',
109+
defiParams: {
110+
vaultId: params.vaultId,
111+
amount: params.amount,
112+
actionType: 'defi-deposit',
113+
},
114+
...(params.walletPassphrase ? { walletPassphrase: params.walletPassphrase } : {}),
115+
});
116+
117+
return this.extractConcreteDepositResult(sendManyResult);
118+
}
119+
120+
/**
121+
* Morpho vault deposit path. Issues two sendMany calls (approve + deposit)
122+
* and returns the operationId that links them.
123+
*/
124+
private async depositToMorphoVault(params: DepositToVaultOptions): Promise<MorphoDepositResult> {
71125
// TODO(CGD-1709): Re-enable active operation pre-flight check once the
72126
// defi-service operations endpoint is deployed and returning active state.
73127
// const activeOps: DefiOperationListResult = await this.bitgo
@@ -256,6 +310,22 @@ export class DefiVault implements IDefiVault {
256310
return intent?.operationId as string | undefined;
257311
}
258312

313+
/**
314+
* Extracts {@link ConcreteDepositResult} from a custodial sendMany response.
315+
* Concrete BTC deposits return a `pendingApproval` instead of a txRequest —
316+
* throws if `pendingApproval.id` is absent, indicating an unexpected shape.
317+
*/
318+
private extractConcreteDepositResult(sendManyResult: Record<string, unknown>): ConcreteDepositResult {
319+
const SendManyConcreteResponse = t.type({
320+
pendingApproval: t.intersection([t.type({ id: t.string }), t.partial({ state: t.string })]),
321+
});
322+
const decoded = decodeWithCodec(SendManyConcreteResponse, sendManyResult, 'defi-deposit sendMany response');
323+
return {
324+
pendingApprovalId: decoded.pendingApproval.id,
325+
state: decoded.pendingApproval.state ?? 'awaitingSignature',
326+
};
327+
}
328+
259329
private operationsUrl(): string {
260330
return `/api/defi-service/v1/wallets/${this.wallet.id()}/operations`;
261331
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* @prettier
33
*/
44

5+
import { GetVaultResponse } from '@bitgo/public-types';
6+
57
export interface DepositToVaultOptions {
68
/** DeFi-service vault identifier */
79
vaultId: string;
@@ -32,6 +34,10 @@ export interface ListOperationsOptions {
3234
cursor?: string;
3335
}
3436

37+
export interface GetVaultConfigOptions {
38+
vaultId: string;
39+
}
40+
3541
export interface DefiOperation {
3642
operationId: string;
3743
walletId: string;
@@ -45,14 +51,18 @@ export interface DefiOperation {
4551
updatedAt: string;
4652
}
4753

48-
export interface DepositResult {
54+
export interface ConcreteDepositResult {
55+
pendingApprovalId: string;
56+
state: string;
57+
}
58+
59+
export interface MorphoDepositResult {
4960
operationId: string;
50-
txRequestIds: {
51-
approve: string;
52-
deposit: string;
53-
};
61+
txRequestIds: { approve: string; deposit: string };
5462
}
5563

64+
export type DepositResult = ConcreteDepositResult | MorphoDepositResult;
65+
5666
export interface DefiOperationListResult {
5767
items: DefiOperation[];
5868
nextCursor?: string;
@@ -63,4 +73,5 @@ export interface IDefiVault {
6373
resumeDeposit(params: ResumeDepositOptions): Promise<DepositResult>;
6474
getOperation(params: GetOperationOptions): Promise<DefiOperation>;
6575
listOperations(params: ListOperationsOptions): Promise<DefiOperationListResult>;
76+
getVaultConfig(params: GetVaultConfigOptions): Promise<GetVaultResponse>;
6677
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export const BuildParams = t.exact(
139139
feeToken: t.unknown,
140140
// Bridging parameters for cross-chain operations (e.g., BTC to sBTC)
141141
bridgingParams: t.unknown,
142+
defiParams: t.unknown,
142143
}),
143144
])
144145
);

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,13 @@ export interface SendManyOptions extends PrebuildAndSignTransactionOptions {
937937
eip1559?: EIP1559;
938938
gasLimit?: number;
939939
custodianTransactionId?: string;
940+
defiParams?: {
941+
vaultId?: string;
942+
amount?: string | number;
943+
actionType?: string;
944+
operationId?: string;
945+
clientIdempotencyKey?: string;
946+
};
940947
}
941948

942949
export interface FetchCrossChainUTXOsOptions {

0 commit comments

Comments
 (0)