diff --git a/modules/bitgo/test/v2/unit/wallet.ts b/modules/bitgo/test/v2/unit/wallet.ts index 81013ade88..591ec1d38d 100644 --- a/modules/bitgo/test/v2/unit/wallet.ts +++ b/modules/bitgo/test/v2/unit/wallet.ts @@ -1572,6 +1572,62 @@ describe('V2 Wallet:', function () { }); }); + describe('Custody sBTC bridging/withdraw params pass-through', function () { + const bridgingParams = { + sbtc: { + amount: 100000, + stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7', + maxFee: 5000, + lockTime: 144, + }, + }; + const sbtcWithdrawParams = { + amount: '100000', + btcAddress: '2N9Ego9KidiZR8tMP82g6RaggQtcbR9zNzH', + maxFee: '5000', + }; + + afterEach(function () { + nock.cleanAll(); + }); + + it('should pass bridgingParams through sendMany to tx/initiate for custodial wallets', async function () { + const custodialWallet = new Wallet(bitgo, bitgo.coin('tbtc'), { + id: '5b34252f1bf349930e34020a', + coin: 'tbtc', + type: 'custodial', + keys: ['5b3424f91bf349930e340175'], + }); + + const initiatePath = `/api/v2/${custodialWallet.coin()}/wallet/${custodialWallet.id()}/tx/initiate`; + const response = nock(bgUrl) + .post(initiatePath, _.matches({ type: 'bridging', bridgingParams })) + .reply(200, { status: 'accepted', txRequestId: 'mock-tx-request-id' }); + + const result = await custodialWallet.sendMany({ type: 'bridging', bridgingParams }); + response.isDone().should.be.true(); + (result as any).status.should.equal('accepted'); + }); + + it('should pass sbtcWithdrawParams through sendMany to tx/initiate for custodial wallets', async function () { + const custodialWallet = new Wallet(bitgo, bitgo.coin('tbtc'), { + id: '5b34252f1bf349930e34020a', + coin: 'tbtc', + type: 'custodial', + keys: ['5b3424f91bf349930e340175'], + }); + + const initiatePath = `/api/v2/${custodialWallet.coin()}/wallet/${custodialWallet.id()}/tx/initiate`; + const response = nock(bgUrl) + .post(initiatePath, _.matches({ sbtcWithdrawParams })) + .reply(200, { status: 'accepted', txRequestId: 'mock-tx-request-id' }); + + const result = await custodialWallet.sendMany({ sbtcWithdrawParams } as any); + response.isDone().should.be.true(); + (result as any).status.should.equal('accepted'); + }); + }); + describe('Transaction prebuilds', function () { let ethWallet; diff --git a/modules/sdk-core/package.json b/modules/sdk-core/package.json index d23c5c1fff..a30d01b043 100644 --- a/modules/sdk-core/package.json +++ b/modules/sdk-core/package.json @@ -40,7 +40,7 @@ ] }, "dependencies": { - "@bitgo/public-types": "6.53.0", + "@bitgo/public-types": "6.56.0", "@bitgo/sdk-lib-mpc": "^10.15.0", "@bitgo/secp256k1": "^1.11.0", "@bitgo/sjcl": "^1.1.0", diff --git a/modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts b/modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts index e1f619b3ba..7390c1a6e0 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts @@ -1,5 +1,10 @@ import * as assert from 'assert'; -import { BuildParams, buildParamKeys, AttestationPayload } from '../../../../src/bitgo/wallet/BuildParams'; +import { + BuildParams, + buildParamKeys, + AttestationPayload, + SbtcWithdrawParams, +} from '../../../../src/bitgo/wallet/BuildParams'; describe('BuildParams', function () { it('enforces codec', function () { @@ -106,4 +111,10 @@ describe('BuildParams', function () { assert.strictEqual(AttestationPayload.is(valid), true); assert.strictEqual(AttestationPayload.is({ ...valid, signature: undefined }), false); }); + + it('SbtcWithdrawParams codec accepts partial string fields', function () { + assert.strictEqual(SbtcWithdrawParams.is({ amount: '100000', btcAddress: 'mtbtcaddr', maxFee: '5000' }), true); + assert.strictEqual(SbtcWithdrawParams.is({}), true); + assert.strictEqual(SbtcWithdrawParams.is({ amount: 100000 }), false); // must be string, not number + }); }); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/SendTransactionRequest.ts b/modules/sdk-core/test/unit/bitgo/wallet/SendTransactionRequest.ts index 12e3b09906..bd8c343cd3 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/SendTransactionRequest.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/SendTransactionRequest.ts @@ -33,4 +33,64 @@ describe('SendTransactionRequest', function () { attestation, }); }); + + it('preserves bridgingParams (CSHLD-1456: @bitgo/public-types TxSendBody declares it natively)', function () { + const bridgingParams = { + sbtc: { + amount: '100000', + stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7', + maxFee: '5000', + lockTime: 144, + }, + }; + + assert.deepStrictEqual(TxSendBody.encode({ txHex: '00', bridgingParams } as any), { + txHex: '00', + bridgingParams, + }); + }); + + it('preserves sbtcWithdrawParams (CSHLD-1456: @bitgo/public-types TxSendBody declares it natively)', function () { + const sbtcWithdrawParams = { amount: '100000', btcAddress: 'mtbtcaddr', maxFee: '5000' }; + + assert.deepStrictEqual(TxSendBody.encode({ txHex: '00', sbtcWithdrawParams } as any), { + txHex: '00', + sbtcWithdrawParams, + }); + }); + + it('validates bridgingParams.sbtc as a complete shape', function () { + const valid = { + sbtc: { + amount: '100000', + stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7', + maxFee: '5000', + lockTime: 144, + }, + }; + assert.strictEqual(TxSendBody.is({ bridgingParams: valid }), true); + assert.strictEqual(TxSendBody.is({}), true); // bridgingParams itself is optional + assert.strictEqual(TxSendBody.is({ bridgingParams: {} }), true); // sbtc is optional within bridgingParams + + // amount/maxFee accept string or number + assert.strictEqual( + TxSendBody.is({ bridgingParams: { sbtc: { ...valid.sbtc, amount: 100000, maxFee: 5000 } } }), + true + ); + + // sbtc, once present, requires all four fields + const missingAmount = { + stacksRecipient: valid.sbtc.stacksRecipient, + maxFee: valid.sbtc.maxFee, + lockTime: valid.sbtc.lockTime, + }; + assert.strictEqual(TxSendBody.is({ bridgingParams: { sbtc: missingAmount } }), false); + }); + + it('validates sbtcWithdrawParams fields as strings', function () { + const valid = { amount: '100000', btcAddress: 'mtbtcaddr', maxFee: '5000' }; + assert.strictEqual(TxSendBody.is({ sbtcWithdrawParams: valid }), true); + assert.strictEqual(TxSendBody.is({ sbtcWithdrawParams: {} }), true); // all fields optional + assert.strictEqual(TxSendBody.is({ sbtcWithdrawParams: { ...valid, amount: 100000 } }), false); // must be string + }); }); diff --git a/yarn.lock b/yarn.lock index 5274dfc11c..ae55d5d6a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1043,10 +1043,10 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" -"@bitgo/public-types@6.53.0": - version "6.53.0" - resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-6.53.0.tgz#fedaf8dbdc7f5fbe25279e560fd76ed25d9443ed" - integrity sha512-nei+2f2fmrnqVZnQ2GBVSbHy0O5bZwcfp/R1jKpuYFOWQgeZtFmblVMhnbN1sfFVyAAGOdacZtx7d56+qZ0TNA== +"@bitgo/public-types@6.56.0": + version "6.56.0" + resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-6.56.0.tgz#ddfdb65273334a320dfa11de3f4ad48beba23ce0" + integrity sha512-IF+qN5gqVfkyBLC8e1K85fxOX4chbYNF1EWQZexqMJjVQZsijMe3m6qkQG8IBJPCH34yhHLa3WcV/sEXKoCT2g== dependencies: fp-ts "^2.0.0" io-ts "npm:@bitgo-forks/io-ts@2.1.4"