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
49 changes: 49 additions & 0 deletions modules/bitgo/test/v2/unit/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1710,6 +1710,55 @@ describe('V2 Wallet:', function () {
postProcessStub.restore();
});

it('should pass bridgingParams to tx/build for non-TSS BTC wallet', async function () {
const bridgingParams = {
sbtc: {
amount: 100000,
stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7',
maxFee: 5000,
lockTime: 144,
},
};
const expectedBuildParams = {
type: 'bridging',
txFormat: 'psbt',
recipients: [],
bridgingParams,
changeAddressType: ['p2trMusig2', 'p2wsh', 'p2shP2wsh', 'p2sh', 'p2tr'],
};

const scope = nock(bgUrl)
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`, expectedBuildParams)
.query({})
.reply(200, {});
const blockHeightStub = sinon.stub(basecoin, 'getLatestBlockHeight').resolves(100);
const postProcessStub = sinon.stub(basecoin, 'postProcessPrebuild').resolves({});
await wallet.prebuildTransaction({
type: 'bridging',
txFormat: 'psbt',
recipients: [],
bridgingParams,
});
postProcessStub.should.have.been.calledOnceWith({
blockHeight: 100,
wallet: wallet,
buildParams: expectedBuildParams,
});
scope.done();
blockHeightStub.restore();
postProcessStub.restore();
});

it('should throw error when bridgingParams is missing for bridging type', async function () {
await wallet
.prebuildTransaction({
type: 'bridging',
txFormat: 'psbt',
recipients: [],
})
.should.be.rejectedWith("'bridgingParams' is required for bridging intent");
});

it('prebuild should call build but not getLatestBlockHeight for account coins', async function () {
['txrp', 'txlm', 'teth'].forEach(async function (coin) {
const accountcoin = bitgo.coin(coin);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SignedTransactionRequestResponse,
EIP1559,
} from './coinSignTx';
import { BridgingParamsCodec } from './sendmany';

/**
* Request parameters for prebuild and sign transaction
Expand Down Expand Up @@ -384,6 +385,8 @@ export const PrebuildAndSignTransactionBody = {
eip1559: optional(EIP1559),
/** Gas limit */
gasLimit: optional(t.number),
/** Parameters for bridging transactions (e.g., BTC to sBTC). Used with type: 'bridging'. */
bridgingParams: optional(BridgingParamsCodec),
/** Low fee transaction ID for CPFP */
lowFeeTxid: optional(t.string),
/** Receive address */
Expand Down
24 changes: 24 additions & 0 deletions modules/express/src/typedRoutes/api/v2/sendmany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,27 @@ export const TokenEnablement = t.intersection([
}),
]);

/**
* sBTC bridging parameters codec
*/
export const SbtcBridgingParamsCodec = t.type({
Comment thread
veetragjain marked this conversation as resolved.
/** Amount in satoshis to bridge */
amount: t.union([t.number, t.string]),
/** Stacks recipient address */
stacksRecipient: t.string,
/** Maximum fee in satoshis */
maxFee: t.union([t.number, t.string]),
/** Lock time for the bridging transaction */
lockTime: t.number,
});

/**
* Bridging parameters codec for cross-chain bridging transactions (e.g., BTC to sBTC).
*/
export const BridgingParamsCodec = t.partial({
sbtc: SbtcBridgingParamsCodec,
});

/**
* Request body for sending to multiple recipients (v2)
*
Expand Down Expand Up @@ -344,6 +365,9 @@ export const SendManyRequestBody = {
/** Array of tokens to enable on the wallet */
enableTokens: optional(t.array(TokenEnablement)),

/** Parameters for bridging transactions (e.g., BTC to sBTC). Used with type: 'bridging'. */
bridgingParams: optional(BridgingParamsCodec),

/** Low fee transaction ID (for CPFP - Child Pays For Parent) */
lowFeeTxid: optional(t.string),

Expand Down
80 changes: 79 additions & 1 deletion modules/express/test/unit/typedRoutes/sendmany.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as assert from 'assert';
import { SendManyResponse, SendManyRequestParams, SendManyRequestBody } from '../../../src/typedRoutes/api/v2/sendmany';
import {
SendManyResponse,
SendManyRequestParams,
SendManyRequestBody,
BridgingParamsCodec,
} from '../../../src/typedRoutes/api/v2/sendmany';
import { assertDecode } from './common';
import * as t from 'io-ts';
import 'should';
Expand Down Expand Up @@ -1192,6 +1197,79 @@ describe('SendMany V2 codec tests', function () {
});
});

describe('BridgingParamsCodec', function () {
const validBridgingParams = {
sbtc: {
amount: 100000,
stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7',
maxFee: 5000,
lockTime: 144,
},
};

it('should validate valid bridgingParams with numeric amount and maxFee', function () {
const decoded = assertDecode(BridgingParamsCodec, validBridgingParams);
assert.deepStrictEqual(decoded, validBridgingParams);
});

it('should validate valid bridgingParams with string amount and maxFee', function () {
const validParams = {
sbtc: {
amount: '100000',
stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7',
maxFee: '5000',
lockTime: 144,
},
};

const decoded = assertDecode(BridgingParamsCodec, validParams);
assert.deepStrictEqual(decoded, validParams);
});

it('should validate empty bridgingParams object', function () {
const decoded = assertDecode(BridgingParamsCodec, {});
assert.deepStrictEqual(decoded, {});
});

it('should reject sbtc params with missing stacksRecipient', function () {
assert.throws(() => {
assertDecode(BridgingParamsCodec, {
sbtc: {
amount: 100000,
maxFee: 5000,
lockTime: 144,
},
});
});
});

it('should reject sbtc params with wrong lockTime type', function () {
assert.throws(() => {
assertDecode(BridgingParamsCodec, {
sbtc: {
amount: 100000,
stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7',
maxFee: 5000,
lockTime: '144',
},
});
});
});

it('should reject sbtc params with wrong amount type', function () {
assert.throws(() => {
assertDecode(BridgingParamsCodec, {
sbtc: {
amount: true,
stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7',
maxFee: 5000,
lockTime: 144,
},
});
});
});
});

describe('SendManyResponse', function () {
it('should validate response with status and tx', function () {
const validResponse = {
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/BuildParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export const BuildParams = t.exact(
aptosCustomTransactionParams: t.unknown,
isTestTransaction: t.unknown,
feeToken: t.unknown,
// Bridging parameters for cross-chain operations (e.g., BTC to sBTC)
bridgingParams: t.unknown,
}),
])
);
Expand Down
22 changes: 22 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ export type NftBalance = BaseBalance & {

export type ApiVersion = 'lite' | 'full';

/** Parameters for sBTC bridging (BTC to sBTC). */
export interface SbtcBridgingParams {
/** Amount in satoshis to bridge */
amount: number | string;
/** Stacks recipient address */
stacksRecipient: string;
/** Maximum fee in satoshis */
maxFee: number | string;
/** Lock time for the bridging transaction */
lockTime: number;
}

/** Parameters for cross-chain bridging transactions. */
export interface BridgingParams {
sbtc?: SbtcBridgingParams;
}

export interface PrebuildTransactionOptions {
reqId?: IRequestTracer;
recipients?: {
Expand Down Expand Up @@ -267,6 +284,11 @@ export interface PrebuildTransactionOptions {
* When specified, fees will be deducted in this token instead of the native currency.
*/
feeToken?: string;
/**
* Parameters for bridging transactions (e.g., BTC to sBTC).
* Used with type: 'bridging' for cross-chain bridging operations.
*/
bridgingParams?: BridgingParams;
}

export interface PrebuildAndSignTransactionOptions extends PrebuildTransactionOptions, WalletSignTransactionOptions {
Expand Down
4 changes: 4 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2051,6 +2051,10 @@ export class Wallet implements IWallet {
return this.prebuildTransactionTxRequests(params);
}

if (params.type === 'bridging') {
assert(params.bridgingParams, "'bridgingParams' is required for bridging intent");
}

// Whitelist params to build tx
const whitelistedParams = this.baseCoin.preprocessBuildParams(_.pick(params, this.prebuildWhitelistedParams()));
debug('prebuilding transaction: %O', whitelistedParams);
Expand Down
37 changes: 37 additions & 0 deletions modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,41 @@ describe('BuildParams', function () {
}
);
});

it('should whitelist bridgingParams', function () {
const bridgingParams = {
sbtc: {
amount: 100000,
stacksRecipient: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7',
maxFee: 5000,
lockTime: 144,
},
};
assert.deepStrictEqual(
BuildParams.encode({
type: 'bridging',
txFormat: 'psbt',
recipients: [],
bridgingParams,
} as any),
{
type: 'bridging',
txFormat: 'psbt',
recipients: [],
bridgingParams,
}
);
});

it('should strip unknown params while keeping bridgingParams', function () {
assert.deepStrictEqual(
BuildParams.encode({
bridgingParams: { sbtc: { amount: 50000, stacksRecipient: 'SP123', maxFee: 1000, lockTime: 100 } },
unknownField: 'should be stripped',
} as any),
{
bridgingParams: { sbtc: { amount: 50000, stacksRecipient: 'SP123', maxFee: 1000, lockTime: 100 } },
}
);
});
});
Loading