Skip to content
Draft
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
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 @@ -158,6 +158,8 @@ export const BuildParams = t.exact(
validToBlock: t.unknown,
messageKey: t.unknown,
stakingParams: t.unknown,
// PoX-5 (UTXO) unstaking, e.g. { pox5: { branch: 'early-exit', principalPreimage } }
unstakingParams: t.unknown,
stakingOptions: t.unknown,
unstakingOptions: t.unknown,
eip1559: t.unknown,
Expand Down
4 changes: 4 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ export interface PrebuildTransactionOptions {
custodianTransactionId?: string;
apiVersion?: ApiVersion;
stakingParams?: unknown;
/**
* PoX-5 unstaking parameters, e.g. `{ pox5: { branch: 'early-exit', principalPreimage } }`.
*/
unstakingParams?: unknown;
/**
* If set to false, sweep all funds including the required minimums for address(es). E.g. Polkadot (DOT) requires 1 DOT minimum.
*/
Expand Down
11 changes: 8 additions & 3 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ type ManageUnspents = 'consolidate' | 'fanout';

const whitelistedSendParams = TxSendBody.type.types.flatMap((t) => Object.keys(t.props));

// TxSendBody in @bitgo/public-types does not declare unstakingParams yet, so /tx/initiate would
// drop it. Extend locally until the codec has it.
const TxInitiateBody = t.intersection([TxSendBody, t.partial({ unstakingParams: t.unknown })]);
const whitelistedInitiateParams = [...whitelistedSendParams, 'unstakingParams'];

export enum ManageUnspentsOptions {
BUILD_ONLY,
BUILD_SIGN_SEND,
Expand Down Expand Up @@ -5782,17 +5787,17 @@ export class Wallet implements IWallet {
).result();
}

private initiateTransaction(params: TxSendBody, reqId?: IRequestTracer) {
private initiateTransaction(params: t.TypeOf<typeof TxInitiateBody>, reqId?: IRequestTracer) {
// extract the whitelisted params from the top level, in case
// other invalid params are present that would fail encoding
// and fall back to the body params
const whitelistedParams = this.baseCoin.preprocessBuildParams(_.pick(params, whitelistedSendParams));
const whitelistedParams = this.baseCoin.preprocessBuildParams(_.pick(params, whitelistedInitiateParams));
const reqTracer = reqId || new RequestTracer();
this.bitgo.setRequestTracer(reqTracer);
return postWithCodec(
this.bitgo,
this.baseCoin.url('/wallet/' + this.id() + '/tx/initiate'),
TxSendBody,
TxInitiateBody,
whitelistedParams
).result();
}
Expand Down
56 changes: 56 additions & 0 deletions modules/sdk-core/test/unit/bitgo/wallet/custodialInitiate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as assert from 'assert';
import sinon from 'sinon';
import { Wallet } from '../../../../src';

describe('Wallet - custodial tx/initiate', function () {
const unstakingParams = {
pox5: { branch: 'early-exit', principalPreimage: '00'.repeat(32) },
};
const recipients = [{ address: 'tb1qexample', amount: 'max' }];

let wallet: Wallet;
let mockBitGo: any;
let sendStub: sinon.SinonStub;

beforeEach(function () {
sendStub = sinon.stub().returns({ result: sinon.stub().resolves({ status: 'accepted' }) });
mockBitGo = {
post: sinon.stub().returns({ send: sendStub }),
setRequestTracer: sinon.stub(),
};
const mockBaseCoin = {
url: sinon.stub().callsFake((path: string) => `/tbtcstxsignet${path}`),
checkRecipient: sinon.stub(),
preprocessBuildParams: sinon.stub().callsFake((params: unknown) => params),
getExtraPrebuildParams: sinon.stub().resolves({}),
supportsTss: sinon.stub().returns(false),
};
wallet = new Wallet(mockBitGo, mockBaseCoin as any, {
id: 'test-wallet-id',
keys: ['user-key'],
type: 'custodial',
multisigType: 'onchain',
});
});

afterEach(function () {
sinon.restore();
});

it('forwards unstakingParams from sendMany to the tx/initiate request body', async function () {
await wallet.sendMany({ recipients, txFormat: 'psbt', noSplitChange: true, unstakingParams } as any);

assert.strictEqual(mockBitGo.post.callCount, 1);
assert.strictEqual(mockBitGo.post.firstCall.args[0], '/tbtcstxsignet/wallet/test-wallet-id/tx/initiate');
const body = sendStub.firstCall.args[0];
assert.deepStrictEqual(body.unstakingParams, unstakingParams);
assert.deepStrictEqual(body.recipients, recipients);
assert.strictEqual(body.txFormat, 'psbt');
});

it('does not add unstakingParams when the caller did not set it', async function () {
await wallet.sendMany({ recipients } as any);

assert.ok(!('unstakingParams' in sendStub.firstCall.args[0]));
});
});
Loading