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
6 changes: 6 additions & 0 deletions modules/sdk-coin-stx/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const FUNCTION_NAME_SENDMANY = 'send-many';
export const CONTRACT_NAME_SENDMANY = 'send-many-memo';
export const CONTRACT_NAME_STAKING = 'pox-4';
// TODO: remove pox-4 after the Stacks Epoch 4.0 (SIP-045) hard fork activates
export const VALID_STAKING_CONTRACT_NAMES = ['pox-4', 'pox-5'];
export const FUNCTION_NAME_TRANSFER = 'transfer';
export const CONTRACT_NAME_SBTC_WITHDRAWAL = 'sbtc-withdrawal';
export const FUNCTION_NAME_INITIATE_WITHDRAWAL = 'initiate-withdrawal-request';
Expand All @@ -14,6 +16,10 @@ export const VALID_CONTRACT_FUNCTION_NAMES = [
'send-many',
'transfer',
'initiate-withdrawal-request',
// pox-5 (SIP-045) staking functions
'stake',
'stake-update',
'unstake',
];

export const DEFAULT_SEED_SIZE_BYTES = 64;
Expand Down
6 changes: 3 additions & 3 deletions modules/sdk-coin-stx/src/lib/contractBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Transaction } from './transaction';
import { isValidAddress } from './utils';
import { ClarityValueJson } from './iface';
import { Utils } from '.';
import { CONTRACT_NAME_SENDMANY, CONTRACT_NAME_STAKING } from './constants';
import { CONTRACT_NAME_SENDMANY, VALID_STAKING_CONTRACT_NAMES } from './constants';
import { AbstractContractBuilder } from './abstractContractBuilder';

export class ContractBuilder extends AbstractContractBuilder {
Expand Down Expand Up @@ -60,8 +60,8 @@ export class ContractBuilder extends AbstractContractBuilder {
if (name.length === 0) {
throw new InvalidParameterValueError('Invalid name');
}
if (name !== CONTRACT_NAME_STAKING && name !== CONTRACT_NAME_SENDMANY) {
throw new InvalidParameterValueError('Only pox-4 and send-many-memo contracts supported');
if (!VALID_STAKING_CONTRACT_NAMES.includes(name) && name !== CONTRACT_NAME_SENDMANY) {
throw new InvalidParameterValueError('Only pox-4, pox-5 and send-many-memo contracts supported');
}
this._contractName = name;
return this;
Expand Down
22 changes: 22 additions & 0 deletions modules/sdk-coin-stx/test/unit/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ export const SIGNED_CONTRACT_CALL =
export const MULTI_SIG_CONTRACT_CALL =
'808000000004012fe507c09dbb23c3b7e5d166c81fc4b87692510b000000000000000000000000000000b400000003020169a032112373b481c48ecb12b8847519f89720d4297a7bf5cac43b9daa8d4ad23dd53657089dad55c94e8be5762e97052683f49e1f457b2e9252f27e975603230200128d0ba2b9584339359ff42c1d6f35ce79a7073152b3c90879bc5162eb8df73b02eada3ee322873a5fa736479e6c536109681482d9f22f1e1a89608090b8666800038e3c4529395611be9abf6fa3b6987e81d402385e3d605a073f42f407565a4a3d0002030200000000021a000000000000000000000000000000000000000005706f782d3409737461636b2d737478000000010a000000000000000000000000000000007b';

// pox-5 (SIP-045) contract calls
export const MAINNET_CONTRACT_ADDRESS = 'SP000000000000000000002Q6VF78';
export const POX_5_CONTRACT_NAME = 'pox-5';
export const POX_5_STAKE_FUNCTION_NAME = 'stake';
export const POX_5_STAKE_UPDATE_FUNCTION_NAME = 'stake-update';
export const POX_5_UNSTAKE_FUNCTION_NAME = 'unstake';
export const SIGNER_MANAGER_PRINCIPAL_TESTNET = 'STDE7Y8HV3RX8VBM2TZVWJTS7ZA1XB0SSC3NEVH0.signer-manager';
export const OLD_SIGNER_MANAGER_PRINCIPAL_TESTNET = 'STDE7Y8HV3RX8VBM2TZVWJTS7ZA1XB0SSC3NEVH0.old-signer-manager';
export const SIGNER_MANAGER_PRINCIPAL_MAINNET = 'SPS4HSXAD1WSD3943WZ52MPSY9WPK56SDG54HTAR.signer-manager';
export const UNSIGNED_POX_5_STAKE_CONTRACT_CALL =
'80800000000400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030200000000021a000000000000000000000000000000000000000005706f782d35057374616b6500000005061a1ae3f911d8f1d46d7416bfbe4b593fd41eac19cb0e7369676e65722d6d616e616765720100000000000000000000000017d784000100000000000000000000000000000002010000000000000000000000000000ce400a020000000f7369676e65722d63616c6c64617461';
export const SIGNED_POX_5_STAKE_CONTRACT_CALL =
'80800000000400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b400018648913451ce62199b1188b91b267fd1a9ca4e7bd9bb1d25a1dee405b25b630a453b3e60e2b30165d1b3b6707e2e2b0a5df3df472a76d705db9731af4685f729030200000000021a000000000000000000000000000000000000000005706f782d35057374616b6500000005061a1ae3f911d8f1d46d7416bfbe4b593fd41eac19cb0e7369676e65722d6d616e616765720100000000000000000000000017d784000100000000000000000000000000000002010000000000000000000000000000ce400a020000000f7369676e65722d63616c6c64617461';
export const SIGNED_POX_5_STAKE_NO_CALLDATA_CONTRACT_CALL =
'80800000000400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b400013a3c939a420aae5c42ce92f115d196fcc995362aa46631fb4b23b15b90f599932c44baedfc095e990c51718ae81e99e8c453af3d9eb360818bbce0e5293d842b030200000000021a000000000000000000000000000000000000000005706f782d35057374616b6500000005061a1ae3f911d8f1d46d7416bfbe4b593fd41eac19cb0e7369676e65722d6d616e616765720100000000000000000000000017d784000100000000000000000000000000000002010000000000000000000000000000ce4009';
export const SIGNED_POX_5_STAKE_UPDATE_CONTRACT_CALL =
'80800000000400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b400003dfbc924d1702a2d35c26025a4906260c7a15adb089c5531f7f87023848516721e148d39220d8c77e8def2e897ae5eac8cc324c8d56b50e03e474148cef4a5dc030200000000021a000000000000000000000000000000000000000005706f782d350c7374616b652d75706461746500000005061a1ae3f911d8f1d46d7416bfbe4b593fd41eac19cb0e7369676e65722d6d616e61676572061a1ae3f911d8f1d46d7416bfbe4b593fd41eac19cb126f6c642d7369676e65722d6d616e6167657201000000000000000000000000000000040100000000000000000000000005f5e10009';
export const SIGNED_POX_5_UNSTAKE_CONTRACT_CALL =
'80800000000400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b40000dd6441ab1510efa7b6f1d6dc3e0872695f9b8a815882372456cf5267d3a7c3b931b5a43f884982e3f02561671dc75238676df058608e89d35fa76726623b89fd030200000000021a000000000000000000000000000000000000000005706f782d3507756e7374616b6500000001061a1ae3f911d8f1d46d7416bfbe4b593fd41eac19cb126f6c642d7369676e65722d6d616e61676572';
export const UNSIGNED_POX_5_STAKE_MAINNET_CONTRACT_CALL =
'00000000010400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000302000000000216000000000000000000000000000000000000000005706f782d35057374616b650000000506163248e7aa6879968d241f3e5152d9f2796994d96c0e7369676e65722d6d616e616765720100000000000000000000000017d784000100000000000000000000000000000002010000000000000000000000000000ce400a020000000f7369676e65722d63616c6c64617461';

// contract call with memo nonce 45n,
export const SEND_MANY_CONTRACT_ADDRESS_WITH_MEMO = 'ST3F1X4QGV2SM8XD96X45M6RTQXKA1PZJZZCQAB4B';
export const SEND_MANY_CONTRACT_NAME_WITH_MEMO = 'send-many-memo';
Expand Down
221 changes: 218 additions & 3 deletions modules/sdk-coin-stx/test/unit/transactionBuilder/contractBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import should from 'should';
import BigNum from 'bn.js';
import { StacksTestnet, StacksMainnet } from '@stacks/network';
import { TransactionType } from '@bitgo/sdk-core';
import { bufferCV, noneCV, someCV, standardPrincipalCV, tupleCV, uintCV, intCV } from '@stacks/transactions';
import {
bufferCV,
contractPrincipalCV,
noneCV,
someCV,
standardPrincipalCV,
tupleCV,
uintCV,
intCV,
} from '@stacks/transactions';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { BitGoAPI } from '@bitgo/sdk-api';
import { coins } from '@bitgo/statics';
Expand Down Expand Up @@ -291,6 +300,191 @@ describe('Stacks: Contract Builder', function () {
tx.inputs[0].value.should.equal('0');
});

describe('pox-5 contract calls', () => {
/* Contract call in clarity POX-5 (SIP-045)
(define-public (stake (signer-manager <signer-manager-trait>)
(amount-ustx uint)
(num-cycles uint)
(start-burn-ht uint)
(signer-calldata (optional (buff 500)))))
*/
const pox5StakeArgs = [
{ type: 'principal', val: testData.SIGNER_MANAGER_PRINCIPAL_TESTNET },
{ type: 'uint128', val: '400000000' },
{ type: 'uint128', val: '2' },
{ type: 'uint128', val: '52800' },
{ type: 'optional', val: { type: 'buffer', val: Buffer.from('signer-calldata') } },
];

const initPox5TxBuilder = (f = factory) => {
const txBuilder = f.getContractBuilder();
txBuilder.fee({ fee: '180' });
txBuilder.nonce(0);
txBuilder.contractName(testData.POX_5_CONTRACT_NAME);
return txBuilder;
};

it('an unsigned pox-5 stake contract call transaction', async () => {
const builder = initPox5TxBuilder();
builder.contractAddress(testData.CONTRACT_ADDRESS);
builder.functionName(testData.POX_5_STAKE_FUNCTION_NAME);
builder.functionArgs(pox5StakeArgs);
builder.fromPubKey(testData.TX_SENDER.pub);
builder.numberSignatures(1);
const tx = await builder.build();

const txJson = tx.toJson();
should.deepEqual(txJson.payload.contractAddress, testData.CONTRACT_ADDRESS);
should.deepEqual(txJson.payload.contractName, testData.POX_5_CONTRACT_NAME);
should.deepEqual(txJson.payload.functionName, testData.POX_5_STAKE_FUNCTION_NAME);
should.deepEqual(txJson.nonce, 0);
should.deepEqual(txJson.fee.toString(), '180');
should.deepEqual(tx.toBroadcastFormat(), testData.UNSIGNED_POX_5_STAKE_CONTRACT_CALL);

tx.type.should.equal(TransactionType.ContractCall);
tx.outputs.length.should.equal(1);
tx.outputs[0].address.should.equal(testData.CONTRACT_ADDRESS);
tx.outputs[0].value.should.equal('0');
tx.inputs.length.should.equal(1);
tx.inputs[0].address.should.equal(testData.TX_SENDER.address);
tx.inputs[0].value.should.equal('0');
});

it('a signed pox-5 stake contract call transaction', async () => {
const builder = initPox5TxBuilder();
builder.contractAddress(testData.CONTRACT_ADDRESS);
builder.functionName(testData.POX_5_STAKE_FUNCTION_NAME);
builder.functionArgs(pox5StakeArgs);
builder.sign({ key: testData.TX_SENDER.prv });
const tx = await builder.build();

const txJson = tx.toJson();
should.deepEqual(txJson.payload.contractName, testData.POX_5_CONTRACT_NAME);
should.deepEqual(txJson.payload.functionName, testData.POX_5_STAKE_FUNCTION_NAME);
should.deepEqual(tx.toBroadcastFormat(), testData.SIGNED_POX_5_STAKE_CONTRACT_CALL);
tx.type.should.equal(TransactionType.ContractCall);
});

it('a signed pox-5 stake contract call transaction without signer-calldata', async () => {
const builder = initPox5TxBuilder();
builder.contractAddress(testData.CONTRACT_ADDRESS);
builder.functionName(testData.POX_5_STAKE_FUNCTION_NAME);
builder.functionArgs([
{ type: 'principal', val: testData.SIGNER_MANAGER_PRINCIPAL_TESTNET },
{ type: 'uint128', val: '400000000' },
{ type: 'uint128', val: '2' },
{ type: 'uint128', val: '52800' },
{ type: 'optional' },
]);
builder.sign({ key: testData.TX_SENDER.prv });
const tx = await builder.build();

const txJson = tx.toJson();
should.deepEqual(txJson.payload.functionArgs[4], stringifyCv(noneCV()));
should.deepEqual(tx.toBroadcastFormat(), testData.SIGNED_POX_5_STAKE_NO_CALLDATA_CONTRACT_CALL);
});

it('a signed serialized pox-5 stake contract call transaction (round trip)', async () => {
const builder = factory.from(testData.SIGNED_POX_5_STAKE_CONTRACT_CALL);
const tx = await builder.build();
const txJson = tx.toJson();
should.deepEqual(txJson.payload.contractAddress, testData.CONTRACT_ADDRESS);
should.deepEqual(txJson.payload.contractName, testData.POX_5_CONTRACT_NAME);
should.deepEqual(txJson.payload.functionName, testData.POX_5_STAKE_FUNCTION_NAME);
should.deepEqual(txJson.nonce, 0);
should.deepEqual(txJson.fee.toString(), '180');
should.deepEqual(txJson.payload.functionArgs, [
stringifyCv(contractPrincipalCV('STDE7Y8HV3RX8VBM2TZVWJTS7ZA1XB0SSC3NEVH0', 'signer-manager')),
stringifyCv(uintCV('400000000')),
stringifyCv(uintCV('2')),
stringifyCv(uintCV('52800')),
stringifyCv(someCV(bufferCV(Buffer.from('signer-calldata')))),
]);
should.deepEqual(tx.toBroadcastFormat(), testData.SIGNED_POX_5_STAKE_CONTRACT_CALL);
tx.type.should.equal(TransactionType.ContractCall);
tx.outputs.length.should.equal(1);
tx.outputs[0].address.should.equal(testData.CONTRACT_ADDRESS);
tx.outputs[0].value.should.equal('0');
tx.inputs.length.should.equal(1);
tx.inputs[0].address.should.equal(testData.TX_SENDER.address);
tx.inputs[0].value.should.equal('0');
});

it('a signed pox-5 stake-update contract call transaction (round trip)', async () => {
/* Contract call in clarity POX-5 (SIP-045)
(define-public (stake-update (signer-manager <signer-manager-trait>)
(old-signer-manager <signer-manager-trait>)
(cycles-to-extend uint)
(amount-increase uint)
(signer-calldata (optional (buff 500)))))
*/
const builder = initPox5TxBuilder();
builder.contractAddress(testData.CONTRACT_ADDRESS);
builder.functionName(testData.POX_5_STAKE_UPDATE_FUNCTION_NAME);
builder.functionArgs([
{ type: 'principal', val: testData.SIGNER_MANAGER_PRINCIPAL_TESTNET },
{ type: 'principal', val: testData.OLD_SIGNER_MANAGER_PRINCIPAL_TESTNET },
{ type: 'uint128', val: '4' },
{ type: 'uint128', val: '100000000' },
{ type: 'optional' },
]);
builder.sign({ key: testData.TX_SENDER.prv });
const tx = await builder.build();
should.deepEqual(tx.toBroadcastFormat(), testData.SIGNED_POX_5_STAKE_UPDATE_CONTRACT_CALL);

const rebuilt = await factory.from(tx.toBroadcastFormat()).build();
const txJson = rebuilt.toJson();
should.deepEqual(txJson.payload.contractName, testData.POX_5_CONTRACT_NAME);
should.deepEqual(txJson.payload.functionName, testData.POX_5_STAKE_UPDATE_FUNCTION_NAME);
should.deepEqual(txJson.payload.functionArgs.length, 5);
should.deepEqual(rebuilt.toBroadcastFormat(), tx.toBroadcastFormat());
});

it('a signed pox-5 unstake contract call transaction (round trip)', async () => {
/* Contract call in clarity POX-5 (SIP-045)
(define-public (unstake (old-signer-manager <signer-manager-trait>)))
*/
const builder = initPox5TxBuilder();
builder.contractAddress(testData.CONTRACT_ADDRESS);
builder.functionName(testData.POX_5_UNSTAKE_FUNCTION_NAME);
builder.functionArgs([{ type: 'principal', val: testData.OLD_SIGNER_MANAGER_PRINCIPAL_TESTNET }]);
builder.sign({ key: testData.TX_SENDER.prv });
const tx = await builder.build();
should.deepEqual(tx.toBroadcastFormat(), testData.SIGNED_POX_5_UNSTAKE_CONTRACT_CALL);

const rebuilt = await factory.from(tx.toBroadcastFormat()).build();
const txJson = rebuilt.toJson();
should.deepEqual(txJson.payload.contractName, testData.POX_5_CONTRACT_NAME);
should.deepEqual(txJson.payload.functionName, testData.POX_5_UNSTAKE_FUNCTION_NAME);
should.deepEqual(txJson.payload.functionArgs, [
stringifyCv(contractPrincipalCV('STDE7Y8HV3RX8VBM2TZVWJTS7ZA1XB0SSC3NEVH0', 'old-signer-manager')),
]);
should.deepEqual(rebuilt.toBroadcastFormat(), tx.toBroadcastFormat());
});

it('an unsigned pox-5 stake contract call transaction on mainnet', async () => {
const builder = initPox5TxBuilder(factoryProd);
builder.contractAddress(testData.MAINNET_CONTRACT_ADDRESS);
builder.functionName(testData.POX_5_STAKE_FUNCTION_NAME);
builder.functionArgs([
{ type: 'principal', val: testData.SIGNER_MANAGER_PRINCIPAL_MAINNET },
{ type: 'uint128', val: '400000000' },
{ type: 'uint128', val: '2' },
{ type: 'uint128', val: '52800' },
{ type: 'optional', val: { type: 'buffer', val: Buffer.from('signer-calldata') } },
]);
builder.fromPubKey(testData.TX_SENDER.pub);
builder.numberSignatures(1);
const tx = await builder.build();

const txJson = tx.toJson();
should.deepEqual(txJson.payload.contractAddress, testData.MAINNET_CONTRACT_ADDRESS);
should.deepEqual(txJson.payload.contractName, testData.POX_5_CONTRACT_NAME);
should.deepEqual(txJson.payload.functionName, testData.POX_5_STAKE_FUNCTION_NAME);
should.deepEqual(tx.toBroadcastFormat(), testData.UNSIGNED_POX_5_STAKE_MAINNET_CONTRACT_CALL);
});
});

it('a multisig transfer transaction', async () => {
const builder = initTxBuilder();
builder.functionArgs([{ type: 'optional', val: { type: 'int128', val: '123' } }]);
Expand Down Expand Up @@ -363,6 +557,14 @@ describe('Stacks: Contract Builder', function () {
]);
});

it('contract principal (pox-5 signer-manager trait argument)', () => {
const builder = initTxBuilder();
builder.functionArgs([{ type: 'principal', val: testData.SIGNER_MANAGER_PRINCIPAL_TESTNET }]);
should.deepEqual((builder as any)._functionArgs, [
contractPrincipalCV('STDE7Y8HV3RX8VBM2TZVWJTS7ZA1XB0SSC3NEVH0', 'signer-manager'),
]);
});

it('Buffer as string', () => {
const builder = initTxBuilder();
builder.functionArgs([
Expand Down Expand Up @@ -408,11 +610,24 @@ describe('Stacks: Contract Builder', function () {
});
it('a contract call with an invalid contract name pox-2', () => {
const builder = initTxBuilder();
assert.throws(() => builder.contractName('pox-2'), /Only pox-4 and send-many-memo contracts supported/);
assert.throws(
() => builder.contractName('pox-2'),
/Only pox-4, pox-5 and send-many-memo contracts supported/
);
});
it('a contract call with an invalid contract name pox-3', () => {
const builder = initTxBuilder();
assert.throws(() => builder.contractName('pox-3'), /Only pox-4 and send-many-memo contracts supported/);
assert.throws(
() => builder.contractName('pox-3'),
/Only pox-4, pox-5 and send-many-memo contracts supported/
);
});
it('a contract call with an invalid contract name pox-6', () => {
const builder = initTxBuilder();
assert.throws(
() => builder.contractName('pox-6'),
/Only pox-4, pox-5 and send-many-memo contracts supported/
);
});
it('a contract call with an invalid contract function name', () => {
const builder = initTxBuilder();
Expand Down
Loading
Loading