diff --git a/modules/sdk-core/package.json b/modules/sdk-core/package.json index 0c01439ee8..7cd2ab488b 100644 --- a/modules/sdk-core/package.json +++ b/modules/sdk-core/package.json @@ -40,7 +40,7 @@ ] }, "dependencies": { - "@bitgo/public-types": "6.22.0", + "@bitgo/public-types": "^6.39.0", "@bitgo/sdk-lib-mpc": "^10.15.0", "@bitgo/secp256k1": "^1.11.0", "@bitgo/sjcl": "^1.1.0", diff --git a/modules/sdk-core/src/bitgo/defi/defiVault.ts b/modules/sdk-core/src/bitgo/defi/defiVault.ts index e26700d9de..1377631ffc 100644 --- a/modules/sdk-core/src/bitgo/defi/defiVault.ts +++ b/modules/sdk-core/src/bitgo/defi/defiVault.ts @@ -2,14 +2,17 @@ * @prettier */ import { + ConcreteDepositResult, DefiOperation, DefiOperationListResult, DepositResult, DepositToVaultOptions, GetOperationOptions, + GetVaultConfigOptions, IDefiVault, ListOperationsOptions, ResumeDepositOptions, + VaultConfig, } from './iDefiVault'; import { IWallet } from '../wallet'; import { BitGoBase } from '../bitgoBase'; @@ -47,13 +50,25 @@ export class DefiVault implements IDefiVault { this.bitgo = wallet.bitgo; } + /** + * Fetch vault config from defi-service. Used internally to determine + * which deposit path to take (Concrete vs Morpho). + */ + async getVaultConfig(params: GetVaultConfigOptions): Promise { + if (!params.vaultId) { + throw new Error('vaultId is required'); + } + return await this.bitgo + .get(this.bitgo.microservicesUrl(`/api/defi-service/v1/vaults/${params.vaultId}`)) + .result(); + } + /** * Deposit an amount of underlying asset into a vault. * - * Internally issues two sendMany calls (approve + deposit) and returns the - * operationId that links them. If the deposit sendMany fails after - * the approve succeeds, the error propagates — the server-side reconciler - * handles orphaned approvals. + * Dispatches to the concrete or morpho path based on vault provider. + * The concrete path returns a pendingApproval (custodial wallet). + * The morpho path issues two sendMany calls (approve + deposit). * * @param params.vaultId - DeFi-service vault identifier * @param params.amount - amount in base units of the underlying asset @@ -68,16 +83,52 @@ export class DefiVault implements IDefiVault { throw new Error('amount is required'); } + const config = await this.getVaultConfig({ vaultId: params.vaultId }); + + if (config.provider === 'concrete_btccx') { + return this.depositToConcreteVault(params); + } + + return this.depositToMorphoVault(params); + } + + /** + * Concrete BTC vault deposit path. The client BTC wallet is custodial, so + * sendMany returns a pendingApproval rather than a signed transfer. + * No recipients are sent — WP resolves the escrow destination server-side. + */ + private async depositToConcreteVault(params: DepositToVaultOptions): Promise { + const sendManyResult = await this.wallet.sendMany({ + type: 'defi-deposit', + defiParams: { + vaultId: params.vaultId, + amount: params.amount, + actionType: 'defi-deposit', + }, + ...(params.walletPassphrase ? { walletPassphrase: params.walletPassphrase } : {}), + }); + + return this.extractConcreteDepositResult(sendManyResult); + } + + private extractConcreteDepositResult(sendManyResult: unknown): ConcreteDepositResult { + const pendingApproval = (sendManyResult as { pendingApproval?: { id?: string; state?: string } })?.pendingApproval; + if (!pendingApproval?.id) { + throw new Error('Unexpected sendMany response for defi-deposit: no pendingApproval.id'); + } + return { pendingApprovalId: pendingApproval.id, state: pendingApproval.state ?? 'awaitingSignature' }; + } + + /** + * Morpho vault deposit path. Issues two sendMany calls (approve + deposit) + * and returns the operationId that links them. + */ + private async depositToMorphoVault(params: DepositToVaultOptions): Promise<{ + operationId: string; + txRequestIds: { approve: string; deposit: string }; + }> { // TODO(CGD-1709): Re-enable active operation pre-flight check once the // defi-service operations endpoint is deployed and returning active state. - // const activeOps: DefiOperationListResult = await this.bitgo - // .get(this.bitgo.microservicesUrl(this.operationsUrl())) - // .query({ vaultId: params.vaultId, state: 'active' }) - // .result(); - // - // if (activeOps.items && activeOps.items.length > 0) { - // throw new ActiveOperationExistsError(activeOps.items[0].operationId); - // } // Step 1: Approve txRequest via sendMany const approveResult = await this.wallet.sendMany({ diff --git a/modules/sdk-core/src/bitgo/defi/iDefiVault.ts b/modules/sdk-core/src/bitgo/defi/iDefiVault.ts index 6bc706b77a..93c9460563 100644 --- a/modules/sdk-core/src/bitgo/defi/iDefiVault.ts +++ b/modules/sdk-core/src/bitgo/defi/iDefiVault.ts @@ -32,6 +32,10 @@ export interface ListOperationsOptions { cursor?: string; } +export interface GetVaultConfigOptions { + vaultId: string; +} + export interface DefiOperation { operationId: string; walletId: string; @@ -45,14 +49,39 @@ export interface DefiOperation { updatedAt: string; } -export interface DepositResult { - operationId: string; - txRequestIds: { - approve: string; - deposit: string; - }; +export type VaultProvider = 'morpho' | 'concrete_btccx'; + +export interface ConcreteVaultConfig { + sourceWalletId: string; + escrowWalletId: string; + escrowDepositAddress: string; + positionWalletId: string; + positionBaseAddress: string; } +export interface VaultConfig { + id: string; + name: string; + provider: VaultProvider; + status: string; + coin: string; + assetToken: string; + shareToken: string; + riskManager: string; + custodyType: string; + vaultContractAddress?: string; + concreteConfig?: ConcreteVaultConfig; +} + +export interface ConcreteDepositResult { + pendingApprovalId: string; + state: string; +} + +export type DepositResult = + | { pendingApprovalId: string; state: string } // concrete_btccx + | { operationId: string; txRequestIds: { approve: string; deposit: string } }; // morpho + export interface DefiOperationListResult { items: DefiOperation[]; nextCursor?: string; @@ -63,4 +92,5 @@ export interface IDefiVault { resumeDeposit(params: ResumeDepositOptions): Promise; getOperation(params: GetOperationOptions): Promise; listOperations(params: ListOperationsOptions): Promise; + getVaultConfig(params: GetVaultConfigOptions): Promise; } diff --git a/modules/sdk-core/src/bitgo/wallet/BuildParams.ts b/modules/sdk-core/src/bitgo/wallet/BuildParams.ts index a635b2057b..40e591e7d2 100644 --- a/modules/sdk-core/src/bitgo/wallet/BuildParams.ts +++ b/modules/sdk-core/src/bitgo/wallet/BuildParams.ts @@ -135,6 +135,7 @@ export const BuildParams = t.exact( feeToken: t.unknown, // Bridging parameters for cross-chain operations (e.g., BTC to sBTC) bridgingParams: t.unknown, + defiParams: t.unknown, }), ]) ); diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index a9663e6bbc..bfea1321b8 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -937,6 +937,7 @@ export interface SendManyOptions extends PrebuildAndSignTransactionOptions { eip1559?: EIP1559; gasLimit?: number; custodianTransactionId?: string; + defiParams?: { vaultId?: string; amount?: string | number; actionType?: string; operationId?: string; clientIdempotencyKey?: string }; } export interface FetchCrossChainUTXOsOptions { diff --git a/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts b/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts index d23f81ab53..c8b2bde931 100644 --- a/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts +++ b/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts @@ -50,147 +50,325 @@ describe('DefiVault', function () { sinon.restore(); }); - describe('depositToVault', function () { - it('should call sendMany for approve and deposit on happy path', async function () { - const operationId = 'op-uuid-123'; - - // 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' }, - transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], + describe('getVaultConfig', function () { + it('should fetch vault config for a given vaultId', async function () { + const vaultConfig = { + id: 'vlt-concrete-1', + name: 'Concrete BTC Vault', + provider: 'concrete_btccx', + status: 'active', + coin: 'btc', + assetToken: 'btc', + shareToken: 'cbtc', + riskManager: 'manager-1', + custodyType: 'custodial', + concreteConfig: { + sourceWalletId: 'src-wallet', + escrowWalletId: 'escrow-wallet', + escrowDepositAddress: '1ExampleBtcAddress', + positionWalletId: 'pos-wallet', + positionBaseAddress: '1PositionAddress', }, - }); - sendManyStub.onSecondCall().resolves({ - txRequest: { - txRequestId: 'txreq-deposit-1', - intent: { intentType: 'defi-deposit' }, - transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], - }, - }); + }; - const result = await defiVault.depositToVault({ - vaultId: 'vlt-galaxy-usdc', - amount: '1000000', - }); + const req = mockRequest(vaultConfig); + mockBitGo.get.returns(req); - result.operationId.should.equal(operationId); - result.txRequestIds.approve.should.equal('txreq-approve-1'); - result.txRequestIds.deposit.should.equal('txreq-deposit-1'); + const result = await defiVault.getVaultConfig({ vaultId: 'vlt-concrete-1' }); - // Verify sendMany was called with correct params for approve - sendManyStub.calledTwice.should.be.true(); - const approveArgs: any = sendManyStub.firstCall.args[0]; - approveArgs.type.should.equal('defiApprove'); - approveArgs.defiParams.vaultId.should.equal('vlt-galaxy-usdc'); - approveArgs.defiParams.amount.should.equal('1000000'); + result.should.deepEqual(vaultConfig); + mockBitGo.get.calledWith('https://bitgo.com/api/defi-service/v1/vaults/vlt-concrete-1').should.be.true(); + }); - // Verify sendMany was called with correct params for deposit - const depositArgs: any = sendManyStub.secondCall.args[0]; - depositArgs.type.should.equal('defiDeposit'); - depositArgs.defiParams.operationId.should.equal(operationId); + it('should throw if vaultId is missing', async function () { + await assert.rejects(() => defiVault.getVaultConfig({ vaultId: '' }), { + message: 'vaultId is required', + }); }); - it('should extract operationId from the lite apiVersion coinSpecific', async function () { - const operationId = 'op-uuid-lite'; + it('should propagate errors from the API (e.g. 404)', async function () { + const req = mockRequest(null); + req.result.rejects(new Error('Not Found')); + mockBitGo.get.returns(req); - const sendManyStub = sinon.stub(wallet, 'sendMany'); - sendManyStub.onFirstCall().resolves({ - txRequest: { - txRequestId: 'txreq-approve-lite', - intent: { intentType: 'defi-approve' }, - unsignedTxs: [{ coinSpecific: { operationId } }], - }, + await assert.rejects(() => defiVault.getVaultConfig({ vaultId: 'vlt-not-found' }), { + message: 'Not Found', }); - sendManyStub.onSecondCall().resolves({ - txRequest: { txRequestId: 'txreq-deposit-lite' }, + }); + }); + + describe('depositToVault', function () { + describe('concrete_btccx provider', function () { + it('should call sendMany once with defi-deposit type and no recipients', async function () { + const vaultConfig = { id: 'vlt-btc-1', provider: 'concrete_btccx' }; + const getVaultConfigReq = mockRequest(vaultConfig); + mockBitGo.get.returns(getVaultConfigReq); + + const sendManyStub = sinon.stub(wallet, 'sendMany').resolves({ + pendingApproval: { + id: 'pa-uuid-123', + state: 'awaitingSignature', + }, + }); + + const result = await defiVault.depositToVault({ + vaultId: 'vlt-btc-1', + amount: '5000000', + }); + + // Returns ConcreteDepositResult shape + (result as any).pendingApprovalId.should.equal('pa-uuid-123'); + (result as any).state.should.equal('awaitingSignature'); + + // Only one sendMany call — no second deposit sendMany + sendManyStub.calledOnce.should.be.true(); + + const callArgs: any = sendManyStub.firstCall.args[0]; + callArgs.type.should.equal('defi-deposit'); + callArgs.defiParams.vaultId.should.equal('vlt-btc-1'); + callArgs.defiParams.amount.should.equal('5000000'); + callArgs.defiParams.actionType.should.equal('defi-deposit'); + // No recipients key at all + assert.strictEqual(callArgs.recipients, undefined); }); - const result = await defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }); + it('should pass walletPassphrase when provided', async function () { + const vaultConfig = { id: 'vlt-btc-2', provider: 'concrete_btccx' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); - result.operationId.should.equal(operationId); - const depositArgs: any = sendManyStub.secondCall.args[0]; - depositArgs.defiParams.operationId.should.equal(operationId); - }); + const sendManyStub = sinon.stub(wallet, 'sendMany').resolves({ + pendingApproval: { id: 'pa-uuid-456', state: 'awaitingSignature' }, + }); - it('should fall back to intent.operationId for forward-compat', async function () { - const operationId = 'op-uuid-intent'; + await defiVault.depositToVault({ + vaultId: 'vlt-btc-2', + amount: '1000000', + walletPassphrase: 'hunter2', + }); - const sendManyStub = sinon.stub(wallet, 'sendMany'); - sendManyStub.onFirstCall().resolves({ - txRequest: { - txRequestId: 'txreq-approve-intent', - intent: { intentType: 'defi-approve', operationId }, - }, + const callArgs: any = sendManyStub.firstCall.args[0]; + callArgs.walletPassphrase.should.equal('hunter2'); }); - sendManyStub.onSecondCall().resolves({ - txRequest: { txRequestId: 'txreq-deposit-intent' }, + + it('should default state to awaitingSignature when absent from pendingApproval', async function () { + const vaultConfig = { id: 'vlt-btc-3', provider: 'concrete_btccx' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); + + sinon.stub(wallet, 'sendMany').resolves({ + pendingApproval: { id: 'pa-no-state' }, + }); + + const result = await defiVault.depositToVault({ vaultId: 'vlt-btc-3', amount: '1000000' }); + (result as any).state.should.equal('awaitingSignature'); }); - const result = await defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }); + it('should throw when sendMany returns no pendingApproval', async function () { + const vaultConfig = { id: 'vlt-btc-4', provider: 'concrete_btccx' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); - result.operationId.should.equal(operationId); + sinon.stub(wallet, 'sendMany').resolves({ txRequest: { txRequestId: 'unexpected' } }); + + await assert.rejects( + () => defiVault.depositToVault({ vaultId: 'vlt-btc-4', amount: '1000000' }), + { message: 'Unexpected sendMany response for defi-deposit: no pendingApproval.id' } + ); + }); }); - 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: {} } }], - }, + describe('morpho provider', function () { + it('should call sendMany for approve and deposit on happy path', async function () { + const vaultConfig = { id: 'vlt-galaxy-usdc', provider: 'morpho' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); + + const operationId = 'op-uuid-123'; + + 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' }, + transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], + }, + }); + sendManyStub.onSecondCall().resolves({ + txRequest: { + txRequestId: 'txreq-deposit-1', + intent: { intentType: 'defi-deposit' }, + transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], + }, + }); + + const result = await defiVault.depositToVault({ + vaultId: 'vlt-galaxy-usdc', + amount: '1000000', + }); + + (result as any).operationId.should.equal(operationId); + (result as any).txRequestIds.approve.should.equal('txreq-approve-1'); + (result as any).txRequestIds.deposit.should.equal('txreq-deposit-1'); + + // Verify sendMany was called with correct params for approve + sendManyStub.calledTwice.should.be.true(); + const approveArgs: any = sendManyStub.firstCall.args[0]; + approveArgs.type.should.equal('defiApprove'); + approveArgs.defiParams.vaultId.should.equal('vlt-galaxy-usdc'); + approveArgs.defiParams.amount.should.equal('1000000'); + + // Verify sendMany was called with correct params for deposit + const depositArgs: any = sendManyStub.secondCall.args[0]; + depositArgs.type.should.equal('defiDeposit'); + depositArgs.defiParams.operationId.should.equal(operationId); }); - await assert.rejects(() => defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }), { - message: 'operationId not found in approve txRequest response', + it('should extract operationId from the lite apiVersion coinSpecific', async function () { + const vaultConfig = { id: 'vlt-galaxy-usdc', provider: 'morpho' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); + + 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 as any).operationId.should.equal(operationId); + const depositArgs: any = sendManyStub.secondCall.args[0]; + depositArgs.defiParams.operationId.should.equal(operationId); }); - // Deposit sendMany must not be issued when the operationId is missing - sendManyStub.calledOnce.should.be.true(); - }); + it('should fall back to intent.operationId for forward-compat', async function () { + const vaultConfig = { id: 'vlt-galaxy-usdc', provider: 'morpho' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); + + 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' }, + }); - // 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({ - items: [{ operationId: 'existing-op-id', state: 'APPROVE_TX_REQUESTED' }], + const result = await defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }); + + (result as any).operationId.should.equal(operationId); }); - mockBitGo.get.returns(preflightReq); - - await assert.rejects( - () => defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }), - (err: Error) => { - (err instanceof ActiveOperationExistsError).should.be.true(); - (err as ActiveOperationExistsError).operationId.should.equal('existing-op-id'); - return true; - } - ); - }); - it('should propagate deposit sendMany failure without cleanup', async function () { - const operationId = 'op-uuid-456'; + it('should throw when operationId is absent from the approve txRequest', async function () { + const vaultConfig = { id: 'vlt-galaxy-usdc', provider: 'morpho' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); - // Mock sendMany: approve succeeds, deposit fails - const sendManyStub = sinon.stub(wallet, 'sendMany'); - sendManyStub.onFirstCall().resolves({ - txRequest: { - txRequestId: 'txreq-approve-2', - intent: { intentType: 'defi-approve' }, - transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], - }, + 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({ + items: [{ operationId: 'existing-op-id', state: 'APPROVE_TX_REQUESTED' }], + }); + mockBitGo.get.returns(preflightReq); + + await assert.rejects( + () => defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }), + (err: Error) => { + (err instanceof ActiveOperationExistsError).should.be.true(); + (err as ActiveOperationExistsError).operationId.should.equal('existing-op-id'); + return true; + } + ); }); - sendManyStub.onSecondCall().rejects(new Error('deposit creation failed')); - await assert.rejects(() => defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }), { - message: 'deposit creation failed', + it('should propagate deposit sendMany failure without cleanup', async function () { + const vaultConfig = { id: 'vlt-galaxy-usdc', provider: 'morpho' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); + + const operationId = 'op-uuid-456'; + + // Mock sendMany: approve succeeds, deposit fails + const sendManyStub = sinon.stub(wallet, 'sendMany'); + sendManyStub.onFirstCall().resolves({ + txRequest: { + txRequestId: 'txreq-approve-2', + intent: { intentType: 'defi-approve' }, + transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], + }, + }); + sendManyStub.onSecondCall().rejects(new Error('deposit creation failed')); + + await assert.rejects(() => defiVault.depositToVault({ vaultId: 'vlt-galaxy-usdc', amount: '1000000' }), { + message: 'deposit creation failed', + }); + + // SDK does not attempt cleanup — reconciler handles orphaned approvals + mockBitGo.del.called.should.be.false(); }); - // SDK does not attempt cleanup — reconciler handles orphaned approvals - mockBitGo.del.called.should.be.false(); + it('should pass clientIdempotencyKey and walletPassphrase when provided', async function () { + const vaultConfig = { id: 'vlt-galaxy-usdc', provider: 'morpho' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); + + const operationId = 'op-uuid-789'; + + const sendManyStub = sinon.stub(wallet, 'sendMany'); + sendManyStub.onFirstCall().resolves({ + txRequest: { + txRequestId: 'txreq-approve-3', + intent: { intentType: 'defi-approve' }, + transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], + }, + }); + sendManyStub.onSecondCall().resolves({ + txRequest: { + txRequestId: 'txreq-deposit-3', + intent: { intentType: 'defi-deposit' }, + transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], + }, + }); + + await defiVault.depositToVault({ + vaultId: 'vlt-galaxy-usdc', + amount: '1000000', + clientIdempotencyKey: 'idem-key-123', + walletPassphrase: 'test-passphrase', + }); + + const approveArgs: any = sendManyStub.firstCall.args[0]; + approveArgs.defiParams.clientIdempotencyKey.should.equal('idem-key-123'); + approveArgs.walletPassphrase.should.equal('test-passphrase'); + + const depositArgs: any = sendManyStub.secondCall.args[0]; + depositArgs.defiParams.clientIdempotencyKey.should.equal('idem-key-123'); + depositArgs.walletPassphrase.should.equal('test-passphrase'); + }); }); it('should throw if vaultId is missing', async function () { @@ -200,45 +378,13 @@ describe('DefiVault', function () { }); it('should throw if amount is missing', async function () { + const vaultConfig = { id: 'vlt-1', provider: 'morpho' }; + mockBitGo.get.returns(mockRequest(vaultConfig)); + await assert.rejects(() => defiVault.depositToVault({ vaultId: 'vlt-1', amount: '' }), { message: 'amount is required', }); }); - - it('should pass clientIdempotencyKey and walletPassphrase when provided', async function () { - const operationId = 'op-uuid-789'; - - const sendManyStub = sinon.stub(wallet, 'sendMany'); - sendManyStub.onFirstCall().resolves({ - txRequest: { - txRequestId: 'txreq-approve-3', - intent: { intentType: 'defi-approve' }, - transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], - }, - }); - sendManyStub.onSecondCall().resolves({ - txRequest: { - txRequestId: 'txreq-deposit-3', - intent: { intentType: 'defi-deposit' }, - transactions: [{ unsignedTx: { coinSpecific: { operationId } } }], - }, - }); - - await defiVault.depositToVault({ - vaultId: 'vlt-galaxy-usdc', - amount: '1000000', - clientIdempotencyKey: 'idem-key-123', - walletPassphrase: 'test-passphrase', - }); - - const approveArgs: any = sendManyStub.firstCall.args[0]; - approveArgs.defiParams.clientIdempotencyKey.should.equal('idem-key-123'); - approveArgs.walletPassphrase.should.equal('test-passphrase'); - - const depositArgs: any = sendManyStub.secondCall.args[0]; - depositArgs.defiParams.clientIdempotencyKey.should.equal('idem-key-123'); - depositArgs.walletPassphrase.should.equal('test-passphrase'); - }); }); describe('resumeDeposit', function () { @@ -271,9 +417,9 @@ describe('DefiVault', function () { const result = await defiVault.resumeDeposit({ operationId: 'op-resume-1' }); - result.operationId.should.equal('op-resume-1'); - result.txRequestIds.approve.should.equal('txreq-approve-existing'); - result.txRequestIds.deposit.should.equal('txreq-deposit-resume'); + (result as any).operationId.should.equal('op-resume-1'); + (result as any).txRequestIds.approve.should.equal('txreq-approve-existing'); + (result as any).txRequestIds.deposit.should.equal('txreq-deposit-resume'); // Verify sendMany was called with correct defiParams const depositArgs: any = sendManyStub.firstCall.args[0];