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
210 changes: 209 additions & 1 deletion modules/sdk-coin-stx/test/unit/transactionBuilder/pox5Builder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import assert from 'assert';
import { ClarityType, createAddress, cvToString, cvToValue } from '@stacks/transactions';
import { ClarityType, ContractCallPayload, createAddress, cvToString, cvToValue } 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';

Expand Down Expand Up @@ -258,4 +259,211 @@ describe('Stacks: PoX-5 Builder', function () {
/more than 14 merkle siblings/
);
});

describe('PoX-5 Builder on tstxsignet', function () {
// Stacks Foundation staking-testnet: chain ID 1280 (0x00000500) with the 0x80 testnet tx version
const signetFactory = new StxLib.TransactionBuilderFactory(coins.get('tstxsignet'));
const TSTXSIGNET_TX_PREFIX = '8000000500';
const POX5_BOOT_ADDRESS = 'ST000000000000000000002AMW42H';

function configure(builder: StxLib.Pox5Builder): StxLib.Pox5Builder {
builder.fee({ fee: '180' });
builder.nonce(0);
builder.fromPubKey(testData.TX_SENDER.pub);
builder.numberSignatures(1);
return builder;
}

function payloadOf(tx: BaseTransaction): StxLib.StacksContractPayload {
return tx.toJson().payload as StxLib.StacksContractPayload;
}

function uintArg(cv: { type: number; value?: unknown }, expected: string): void {
cv.type.should.equal(ClarityType.UInt);
String(cv.value).should.equal(expected);
}

it('builds a stake transaction carrying the tstxsignet chain ID', async () => {
const builder = configure(signetFactory.getPox5Builder());
builder.stake({
signerManager,
amountUstx: '100000000',
numCycles: 1,
startBurnHt: 66849,
});

const tx = await builder.build();
tx.toBroadcastFormat().slice(0, 10).should.equal(TSTXSIGNET_TX_PREFIX);

const payload = payloadOf(tx);
payload.contractAddress.should.equal(POX5_BOOT_ADDRESS);
payload.contractName.should.equal('pox-5');
payload.functionName.should.equal('stake');
payload.functionArgs.length.should.equal(5);
payload.functionArgs[0].type.should.equal(ClarityType.PrincipalContract);
cvToString(payload.functionArgs[0]).should.equal(signerManager);
uintArg(payload.functionArgs[1], '100000000');

uintArg(payload.functionArgs[2], '1');

uintArg(payload.functionArgs[3], '66849');

payload.functionArgs[4].type.should.equal(ClarityType.OptionalNone);
});

it('builds a stake-update transaction carrying the tstxsignet chain ID', async () => {
const builder = configure(signetFactory.getPox5Builder());
builder.stakeUpdate({
signerManager,
oldSignerManager,
cyclesToExtend: 1,
amountIncrease: '50000',
});

const tx = await builder.build();
tx.toBroadcastFormat().slice(0, 10).should.equal(TSTXSIGNET_TX_PREFIX);

const payload = payloadOf(tx);
payload.functionName.should.equal('stake-update');
payload.functionArgs.length.should.equal(5);
cvToString(payload.functionArgs[0]).should.equal(signerManager);
cvToString(payload.functionArgs[1]).should.equal(oldSignerManager);
uintArg(payload.functionArgs[2], '1');
uintArg(payload.functionArgs[3], '50000');
payload.functionArgs[4].type.should.equal(ClarityType.OptionalNone);
});

it('builds an unstake transaction carrying the tstxsignet chain ID', async () => {
const builder = configure(signetFactory.getPox5Builder());
builder.unstake(oldSignerManager);

const tx = await builder.build();
tx.toBroadcastFormat().slice(0, 10).should.equal(TSTXSIGNET_TX_PREFIX);

const payload = payloadOf(tx);
payload.functionName.should.equal('unstake');
payload.functionArgs.length.should.equal(1);
cvToString(payload.functionArgs[0]).should.equal(oldSignerManager);
});

it('builds register-for-bond transactions (L1 lockup and sBTC) carrying the tstxsignet chain ID', async () => {
const btcBuilder = configure(signetFactory.getPox5Builder());
btcBuilder.registerForBond({
bondIndex: 210,
signerManager,
amountUstx: '1005000',
lockup: { kind: 'btc', unlockBytes: '00', outputs: [validLockupOutput()] },
});
const btcTx = await btcBuilder.build();
btcTx.toBroadcastFormat().slice(0, 10).should.equal(TSTXSIGNET_TX_PREFIX);

const btcPayload = payloadOf(btcTx);
btcPayload.functionName.should.equal('register-for-bond');
btcPayload.functionArgs[3].type.should.equal(ClarityType.ResponseOk);

const sbtcBuilder = configure(signetFactory.getPox5Builder());
sbtcBuilder.registerForBond({
bondIndex: 210,
signerManager,
amountUstx: '1005000',
lockup: { kind: 'sbtc', sbtcSats: 10000 },
});
const sbtcTx = await sbtcBuilder.build();
sbtcTx.toBroadcastFormat().slice(0, 10).should.equal(TSTXSIGNET_TX_PREFIX);

const sbtcPayload = payloadOf(sbtcTx);
sbtcPayload.functionArgs[3].type.should.equal(ClarityType.ResponseErr);
});

it('rebuilds every staking flow byte-identically from raw hex', async () => {
const stakeBuilder = configure(signetFactory.getPox5Builder());
stakeBuilder.stake({
signerManager,
amountUstx: '100000000',
numCycles: 1,
startBurnHt: 66849,
});
const stakeTx = await stakeBuilder.build();

const stakeRebuilt = signetFactory.from(stakeTx.toBroadcastFormat());
stakeRebuilt.fromPubKey(testData.TX_SENDER.pub);
(await stakeRebuilt.build()).toBroadcastFormat().should.equal(stakeTx.toBroadcastFormat());

const unstakeBuilder = configure(signetFactory.getPox5Builder());
unstakeBuilder.unstake(oldSignerManager);
const unstakeTx = await unstakeBuilder.build();

const unstakeRebuilt = signetFactory.from(unstakeTx.toBroadcastFormat());
unstakeRebuilt.fromPubKey(testData.TX_SENDER.pub);
(await unstakeRebuilt.build()).toBroadcastFormat().should.equal(unstakeTx.toBroadcastFormat());

const registerBuilder = configure(signetFactory.getPox5Builder());
registerBuilder.registerForBond({
bondIndex: 210,
signerManager,
amountUstx: '1005000',
lockup: { kind: 'sbtc', sbtcSats: 10000 },
});
const registerTx = await registerBuilder.build();

const registerRebuilt = signetFactory.from(registerTx.toBroadcastFormat());
registerRebuilt.fromPubKey(testData.TX_SENDER.pub);
(await registerRebuilt.build()).toBroadcastFormat().should.equal(registerTx.toBroadcastFormat());
});

it('routes parsed stake transactions to the PoX-5 builder on tstxsignet', async () => {
const builder = configure(signetFactory.getPox5Builder());
builder.stake({
signerManager,
amountUstx: '100000000',
numCycles: 1,
startBurnHt: 66849,
});
const tx = await builder.build();
const payload = (tx as StxLib.Transaction).stxTransaction.payload as unknown as ContractCallPayload;

builder.should.be.an.instanceOf(StxLib.Pox5Builder);
should.equal(StxLib.Pox5Builder.isValidContractCall(coins.get('tstxsignet'), payload), true);
should.equal(
StxLib.Pox5Builder.isValidContractCall(coins.get('tstxsignet'), {
...payload,
contractAddress: createAddress(testData.ACCOUNT_1.address),
}),
false
);

const routed = signetFactory.from(tx.toBroadcastFormat());
routed.should.be.an.instanceOf(StxLib.Pox5Builder);
});

it('exposes fee, nonce and sender on the built stake transaction', async () => {
const builder = configure(signetFactory.getPox5Builder());
builder.stake({
signerManager,
amountUstx: '100000000',
numCycles: 1,
startBurnHt: 66849,
});

const txJson = (await builder.build()).toJson();
txJson.fee.should.equal('180');
txJson.nonce.should.equal(0);
txJson.id.should.match(/^[0-9a-f]{64}$/);
txJson.from.should.equal(testData.TX_SENDER.address);
});

it('rejects a signer-manager principal without the address.contract-name shape', () => {
const builder = configure(signetFactory.getPox5Builder());
assert.throws(
() =>
builder.stake({
signerManager: 'no-dot',
amountUstx: '100000000',
numCycles: 1,
startBurnHt: 66849,
}),
/address.contract-name format/
);
});
});
});
42 changes: 39 additions & 3 deletions modules/sdk-coin-stx/test/unit/tstxsignet.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,64 @@
import assert from 'assert';

import { BitGoAPI } from '@bitgo/sdk-api';
import { Wallet } from '@bitgo/sdk-core';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { TransactionVersion } from '@stacks/transactions';
import should from 'should';

import { Tstxsignet } from '../../src';
import { StxLib, Tstxsignet } from '../../src';

describe('tstxsignet', function () {
let bitgo: TestBitGoAPI;
let basecoin: Tstxsignet;

before(function () {
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
bitgo.initializeTestVars();
bitgo.safeRegister('tstxsignet', Tstxsignet.createInstance);
basecoin = bitgo.coin('tstxsignet') as Tstxsignet;
});

it('uses the statics tstxsignet coin', function () {
const basecoin = bitgo.coin('tstxsignet');
assert.strictEqual(basecoin.getChain(), 'tstxsignet');
assert.strictEqual(basecoin.getFullName(), 'Testnet Stacks (Signet)');
assert.strictEqual(basecoin.getFamily(), 'stx');
});

it('resolves the staking-testnet public node url', function () {
const basecoin = bitgo.coin('tstxsignet') as Tstxsignet;
assert.strictEqual(basecoin.getPublicNodeUrl(), 'https://api.staking-testnet.hiro.so');
});

describe('wallet creation', function () {
it('generates a wallet keypair from the coin', function () {
const keyPair = basecoin.generateKeyPair();
const { pub, prv } = keyPair;
if (pub === undefined || prv === undefined) {
throw new Error('wallet keypair is missing key material');
}
pub.should.match(/^xpub/);
prv.should.match(/^xprv/);
});

it('derives a valid testnet wallet address from the generated key', function () {
const keyPair = basecoin.generateKeyPair();
if (keyPair.pub === undefined) {
throw new Error('wallet keypair is missing the extended public key');
}
const address = new StxLib.KeyPair({ pub: keyPair.pub }).getSTXAddress(false, TransactionVersion.Testnet);
address.startsWith('ST').should.be.true();
basecoin.isValidAddress(address).should.be.true();
});

it('creates a wallet bound to the tstxsignet coin', function () {
// mirrors the wallet data the API returns for a created tstxsignet wallet
const wallet = new Wallet(bitgo, basecoin, { coin: 'tstxsignet' } as never);
should.equal(wallet.coin(), 'tstxsignet');
});

it('validates testnet wallet addresses', function () {
basecoin.isValidAddress('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').should.be.true();
basecoin.isValidAddress('not-an-address').should.be.false();
});
});
});
Loading