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
6 changes: 6 additions & 0 deletions modules/sdk-coin-stx/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,12 @@ export function stringifyCv(cv: ClarityValue): any {
return { type: cv.type, value: cv.value.toString() };
case ClarityType.OptionalSome:
return { type: cv.type, value: stringifyCv(cv.value) };
// ResponseOk/ResponseErr wrap a ClarityValue (e.g. register-for-bond's lockup tuple/list).
// Without these cases the inner ints reach `toJson()` as native BigInt, which JSON.stringify
// rejects — making the payload unserialisable for consumers such as the policy service (SI-1922).
case ClarityType.ResponseOk:
case ClarityType.ResponseErr:
return { type: cv.type, value: stringifyCv(cv.value) };
case ClarityType.Tuple:
return {
type: cv.type,
Expand Down
18 changes: 18 additions & 0 deletions modules/sdk-coin-stx/test/unit/registerForBondRealArgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Real `register-for-bond` Clarity args captured from staking-service on staging (SI-1922).
//
// Five hex-encoded Clarity values, in contract order:
// 1. bond-index (uint)
// 2. signer-manager (contract principal)
// 3. amount-ustx (uint)
// 4. btc-lockup — ResponseOk(tuple { outputs (list …), staker-unlock-bytes })
// 5. signer-calldata — OptionalSome(buff)
//
// Arg 4 previously leaked its nested uints as native BigInt through `stringifyCv`, making
// `toJson()` (and `explainTransaction().contractFunctionArgs`) non-JSON-serialisable.
export const pox5RegisterForBondArgsHex: string[] = [
'0x0100000000000000000000000000000156',
'0x061ad540a8a654c4c0f54f910212ff3b119cb2257bb80e7369676e65722d6d616e61676572',
'0x0100000000000000000000000005f5e100',
'0x070c00000002076f7574707574730b000000010c0000000906616d6f756e740100000000000000000000000005f5e1000668656164657202000000500000002097197b45436447b05fce01087c7e00d647ccfe96d709217f17fa1ab2000000000559689a4b0fc9383dc22fec3d477fa32f11b8efc7bd07d1dc3b0938a9197617134ecf6aebdd001db76b228706686569676874010000000000000000000000000000366c0b6c6561662d6861736865730b000000020200000020ba24cab9d1cca4f2d36603adbb0bd277c2396c6f6949acb4775dbf7232cd389f0200000020fdb16797c86264f28c53a7022ac9511838ab018b5b362030e4633085f35b1a220c6f75747075742d696e646578010000000000000000000000000000000002747802000003d5010000000001036d2667e568a7e45e36ea5f020d44e013255a100495f8e5fc0e6dcdb952864a7c0000000000fdffffff5e1bb42e7ea184d6c2df73cd12f4bbde8bd4dc26bff724d6f5ff376a9c6155ad0000000000fdffffff13b7a0017d2ed5f512871529655abdb29fbb6bbaaf4b0fda8f3128a0231812e80000000000fdffffff0200e1f50500000000220020b429ff60e345066883a1ec05bdeab240bce4788194b90763e6c8bb4ea2f8a54230ddfa0200000000220020a606117c1e4c37dffb31ec1ae28d81b192035e9f33b01c6c14c99d886ff4119b04004730440220134758278602aafe39266e236df96d34f91efdb1d5759a2477cc6eb5d84e0b7a02200916b7017bb4fa7806366b64a46ed06e5088bf2782852c77e69c7e76516ace5101483045022100dca9449c15181ba5cb99a4732cc00fb556ddd67f7aab6d1d01da5827f6d49ae8022007686588d3fb2978172989f129af3fa49451cf78336284f890b30bfa0aa5ba6f016952210330e26147bd8e7caff599299593b7a775dd71f651dfec452df745601ad37856da21031ac72b388de801b5bdc741436b7129dae65a75c4d2bf719976e9febc7e150177210253b816cf66a086bc49a2b23994e6d8672100f29f0e763814cb76006c12958c1053ae0400483045022100ef7a5fd52a85a90c6855ec3ca1fe84b5e1055aed5cc54af82cac2482eda39aca02207f1495943f8d3c9d451862c5d5d995abdf58f1ea087e2387bd6386fc3b4419e801483045022100aefc23feab383e12a88874c4171045b2f7e1b75b48cd382d6daf58b567cfe7d202200a6aebb83924acbff2add02aefd2cfc567b2b0a2fb30a7c45bf7265c2a0e8229016952210330e26147bd8e7caff599299593b7a775dd71f651dfec452df745601ad37856da21031ac72b388de801b5bdc741436b7129dae65a75c4d2bf719976e9febc7e150177210253b816cf66a086bc49a2b23994e6d8672100f29f0e763814cb76006c12958c1053ae040047304402200ac37c752d55c17ac01256893fc20d752b059926f887931912d9f59f083e62e302206c8bd733fd2d9175d66833009534d5cbc36b5cfe4641be9396318f5c17fd35fd01483045022100d0ca25c6c8f0632684ba8edcafa50785315204567611b509e96e4b11981aa11a02207ba500a6db5bb2a1437a8fd8c7871d99154010d293d8116f9c7799346701839f016952210330e26147bd8e7caff599299593b7a775dd71f651dfec452df745601ad37856da21031ac72b388de801b5bdc741436b7129dae65a75c4d2bf719976e9febc7e150177210253b816cf66a086bc49a2b23994e6d8672100f29f0e763814cb76006c12958c1053ae000000000874782d636f756e7401000000000000000000000000000000040874782d696e646578010000000000000000000000000000000112756e6c6f636b2d6275726e2d686569676874010000000000000000000000000000376e137374616b65722d756e6c6f636b2d6279746573020000006952210379bbe125f6554f3fb835ada018017c692d6a64a1bd40c8b6ccd129267e0b994d21028a64e3ea928cf8abe941a29ecbe21956bb08ffaea9c26f4af74b069fa449b8082102b0a5c91db10c5bb2b3e7f8c1924c33333769e8cf34ddbf9ca6872796bd614e5e53ae',
'0x0a02000000690c00000002076d61782d66656501000000000000000000000000000f424008706f782d616464720c0000000209686173686279746573020000002010af3de7d61f26b4fdf06276f7fde3c7298e2407c3b05ea1d94b687d6fcc9c8d0776657273696f6e020000000105',
];
28 changes: 27 additions & 1 deletion modules/sdk-coin-stx/test/unit/stx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { BitGoAPI } from '@bitgo/sdk-api';
import { ErrorNoInputToRecover, RecoveryProviderError, Wallet } from '@bitgo/sdk-core';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { coins } from '@bitgo/statics';
import { cvToString, pubKeyfromPrivKey, publicKeyToString } from '@stacks/transactions';
import { cvToString, deserializeCV, pubKeyfromPrivKey, publicKeyToString } from '@stacks/transactions';

import * as testData from '../fixtures';
import * as resources from './resources';
import { pox5RegisterForBondArgsHex } from './registerForBondRealArgs';
import { Stx, StxLib, Tstx } from '../../src';
import { RecoveryInfo, RecoveryOptions, RecoveryTransaction } from '../../src/lib/iface';

Expand Down Expand Up @@ -196,6 +197,31 @@ describe('STX:', function () {
explain.contractFunctionArgs[0].value.toString().should.equal(testData.txExplainedContract.functionArgs[0].value);
});

it('explains a register-for-bond transaction with JSON-serialisable contractFunctionArgs (SI-1922)', async function () {
const factory = new StxLib.TransactionBuilderFactory(coins.get(coinNameTest));
const functionArgs = pox5RegisterForBondArgsHex.map((hex) =>
deserializeCV(Buffer.from(hex.replace(/^0x/, ''), 'hex'))
);
const builder = factory.getPox5Builder();
builder.fee({ fee: '18810' });
builder.nonce(0);
builder.fromPubKey(resources.TX_SENDER.pub);
builder.numberSignatures(1);
builder.functionName('register-for-bond');
builder.functionArgs(functionArgs);
const tx = await builder.build();

const explain = await basecoin.explainTransaction({
txHex: tx.toBroadcastFormat(),
publicKeys: [resources.TX_SENDER.pub],
feeInfo: { fee: '' },
});
explain.contractName.should.equal('pox-5');
explain.contractFunction.should.equal('register-for-bond');
// the second consumer of these args (explainTransaction) must also be JSON-serialisable
assert.doesNotThrow(() => JSON.stringify(explain.contractFunctionArgs));
});

it('should explain a fungible token transfer transaction with memo', async function () {
const explain = await basecoin.explainTransaction({
txHex: testData.txForExplainFungibleTokenTransfer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import assert from 'assert';
import { ClarityType, ContractCallPayload, createAddress, cvToString, cvToValue } from '@stacks/transactions';
import {
ClarityType,
ContractCallPayload,
createAddress,
cvToString,
cvToValue,
deserializeCV,
} from '@stacks/transactions';
import { coins } from '@bitgo/statics';
import should from 'should';

import { BaseTransaction } from '@bitgo/sdk-core';
import { StxLib } from '../../../src';
import * as testData from '../resources';
import { pox5RegisterForBondArgsHex } from '../registerForBondRealArgs';

describe('Stacks: PoX-5 Builder', function () {
const factory = new StxLib.TransactionBuilderFactory(coins.get('tstx'));
Expand Down Expand Up @@ -69,12 +77,44 @@ describe('Stacks: PoX-5 Builder', function () {
should.equal(payload.functionArgs[3].type, ClarityType.ResponseOk);
should.equal(payload.functionArgs[4].type, ClarityType.OptionalSome);

// SI-1922: the register args must be JSON-serialisable (no BigInt leaks) — this object is sent
// to the policy service, where fast-safe-stringify otherwise replaces the whole body.
should.doesNotThrow(() => JSON.stringify(tx.toJson()));
should.equal(typeof payload.functionArgs[3].value.data.outputs.list[0].data.amount.value, 'string');

const rebuiltBuilder = factory.from(tx.toBroadcastFormat());
rebuiltBuilder.fromPubKey(testData.TX_SENDER.pub);
const rebuilt = await rebuiltBuilder.build();
should.equal(rebuilt.toBroadcastFormat(), tx.toBroadcastFormat());
});

it('pins the real staging register-for-bond args and keeps them JSON-serialisable (SI-1922)', async () => {
const functionArgs = pox5RegisterForBondArgsHex.map((hex) =>
deserializeCV(Buffer.from(hex.replace(/^0x/, ''), 'hex'))
);
const builder = configure(factory.getPox5Builder());
builder.functionName('register-for-bond');
builder.functionArgs(functionArgs);
const tx = await builder.build();
const payload = tx.toJson().payload as any;

should.equal(payload.functionArgs.length, 5);
should.equal(payload.functionArgs[0].type, ClarityType.UInt);
should.equal(payload.functionArgs[1].type, ClarityType.PrincipalContract);
should.equal(payload.functionArgs[3].type, ClarityType.ResponseOk);
should.equal(payload.functionArgs[4].type, ClarityType.OptionalSome);

// the btc-lockup (arg 4) nested uints must be stringified — no BigInt leaks into the payload
should.doesNotThrow(() => JSON.stringify(tx.toJson()));
should.equal(typeof payload.functionArgs[3].value.data.outputs.list[0].data.amount.value, 'string');
should.equal(typeof payload.functionArgs[3].value.data.outputs.list[0].data['unlock-burn-height'].value, 'string');

// the fee re-build (factory.from → initBuilder) must stay byte-identical
const rebuilt = factory.from(tx.toBroadcastFormat());
rebuilt.fromPubKey(testData.TX_SENDER.pub);
should.equal((await rebuilt.build()).toBroadcastFormat(), tx.toBroadcastFormat());
});

it('validates the PoX-5 contract address during factory routing', async () => {
const builder = configure(factory.getPox5Builder());
builder.registerForBond({
Expand Down Expand Up @@ -108,6 +148,7 @@ describe('Stacks: PoX-5 Builder', function () {
const payload = tx.toJson().payload as any;
should.equal(payload.functionArgs[3].type, ClarityType.ResponseErr);
should.equal(cvToValue(payload.functionArgs[3].value).toString(), '10000');
should.doesNotThrow(() => JSON.stringify(tx.toJson()));
});

it('builds validator update, early exit, and reward calls', async () => {
Expand Down
28 changes: 27 additions & 1 deletion modules/sdk-coin-stx/test/unit/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import assert from 'assert';
import should from 'should';
import { AddressVersion, ClarityType, IntCV, NoneCV, SomeCV, UIntCV, ListCV } from '@stacks/transactions';
import {
AddressVersion,
ClarityType,
IntCV,
NoneCV,
ResponseErrorCV,
ResponseOkCV,
SomeCV,
UIntCV,
ListCV,
} from '@stacks/transactions';
import BigNum from 'bn.js';
import * as testData from './resources';
import { StxLib } from '../../src';
Expand Down Expand Up @@ -305,6 +315,22 @@ describe('Stx util library', function () {
const input: ListCV = { type: ClarityType.List, list: [] };
Utils.stringifyCv(input).should.deepEqual({ type: 11, list: [] });
});

it('ResponseOk type with uint value', function () {
const input: ResponseOkCV = {
type: ClarityType.ResponseOk,
value: { type: ClarityType.UInt, value: BigInt('100000') },
};
Utils.stringifyCv(input).should.deepEqual({ type: 7, value: { type: 1, value: '100000' } });
});

it('ResponseErr type with uint value', function () {
const input: ResponseErrorCV = {
type: ClarityType.ResponseErr,
value: { type: ClarityType.UInt, value: BigInt('10000') },
};
Utils.stringifyCv(input).should.deepEqual({ type: 8, value: { type: 1, value: '10000' } });
});
});

describe('getAddressVersion', function () {
Expand Down
Loading