Skip to content
Open
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
28 changes: 28 additions & 0 deletions modules/sdk-coin-stx/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
export const FUNCTION_NAME_SENDMANY = 'send-many';
export const CONTRACT_NAME_SENDMANY = 'send-many-memo';
export const CONTRACT_NAME_STAKING = 'pox-4';
export const CONTRACT_NAME_POX5 = 'pox-5';
export const CONTRACT_NAME_STAKING_POX5 = CONTRACT_NAME_POX5;
export const VALID_STAKING_CONTRACT_NAMES = [CONTRACT_NAME_STAKING, CONTRACT_NAME_POX5];
export const POX5_CONTRACT_ADDRESS_TESTNET = 'ST000000000000000000002AMW42H';
export const POX5_CONTRACT_ADDRESS_MAINNET = 'SP000000000000000000002Q6VF78';
export const FUNCTION_NAME_TRANSFER = 'transfer';
export const CONTRACT_NAME_SBTC_WITHDRAWAL = 'sbtc-withdrawal';
export const FUNCTION_NAME_INITIATE_WITHDRAWAL = 'initiate-withdrawal-request';

export const FUNCTION_NAME_STAKE = 'stake';
export const FUNCTION_NAME_STAKE_UPDATE = 'stake-update';
export const FUNCTION_NAME_UNSTAKE = 'unstake';
export const FUNCTION_NAME_REGISTER_FOR_BOND = 'register-for-bond';
export const FUNCTION_NAME_ANNOUNCE_L1_EARLY_EXIT = 'announce-l1-early-exit';
export const FUNCTION_NAME_UPDATE_BOND_REGISTRATION = 'update-bond-registration';
export const FUNCTION_NAME_CLAIM_REWARDS = 'claim-rewards';
export const FUNCTION_NAME_CLAIM_STAKER_REWARDS = 'claim-staker-rewards-for-signer';
export const FUNCTION_NAME_CALCULATE_REWARDS = 'calculate-rewards';

export const VALID_POX5_CONTRACT_FUNCTION_NAMES = [
FUNCTION_NAME_STAKE,
FUNCTION_NAME_STAKE_UPDATE,
FUNCTION_NAME_UNSTAKE,
FUNCTION_NAME_REGISTER_FOR_BOND,
FUNCTION_NAME_UPDATE_BOND_REGISTRATION,
FUNCTION_NAME_ANNOUNCE_L1_EARLY_EXIT,
FUNCTION_NAME_CLAIM_REWARDS,
FUNCTION_NAME_CLAIM_STAKER_REWARDS,
FUNCTION_NAME_CALCULATE_REWARDS,
];

export const VALID_CONTRACT_FUNCTION_NAMES = [
'stack-stx',
'delegate-stx',
Expand All @@ -14,6 +41,7 @@ export const VALID_CONTRACT_FUNCTION_NAMES = [
'send-many',
'transfer',
'initiate-withdrawal-request',
...VALID_POX5_CONTRACT_FUNCTION_NAMES,
];

export const DEFAULT_SEED_SIZE_BYTES = 64;
Expand Down
46 changes: 42 additions & 4 deletions modules/sdk-coin-stx/src/lib/contractBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import {
ClarityValue,
encodeClarityValue,
noneCV,
listCV,
responseErrorCV,
responseOkCV,
someCV,
contractPrincipalCV,
standardPrincipalCV,
tupleCV,
} from '@stacks/transactions';
import { InvalidParameterValueError } from '@bitgo/sdk-core';
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 +65,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 All @@ -77,7 +82,7 @@ export class ContractBuilder extends AbstractContractBuilder {
if (name.length === 0) {
throw new InvalidParameterValueError('Invalid name');
}
if (!Utils.isValidContractFunctionName(name)) {
if (!Utils.isValidContractFunctionName(name, this._contractName)) {
throw new InvalidParameterValueError(`${name} is not supported contract function name`);
}
this._functionName = name;
Expand All @@ -104,6 +109,22 @@ export class ContractBuilder extends AbstractContractBuilder {
} else {
return someCV(this.parseCv(arg.val));
}
case 'list':
if (arg.val instanceof Array) {
return listCV(arg.val.map((value) => this.parseCv(value)));
}
throw new InvalidParameterValueError('list requires Array val');
case 'response':
if (arg.val && typeof arg.val === 'object' && !Array.isArray(arg.val)) {
const response = arg.val as { type?: string; val?: ClarityValueJson };
if (response.type === 'ok' && response.val !== undefined) {
return responseOkCV(this.parseCv(response.val));
}
if (response.type === 'err' && response.val !== undefined) {
return responseErrorCV(this.parseCv(response.val));
}
}
throw new InvalidParameterValueError('response requires { type: ok|err, val }');
case 'tuple':
if (arg.val instanceof Array) {
const data = {};
Expand All @@ -113,6 +134,23 @@ export class ContractBuilder extends AbstractContractBuilder {
return tupleCV(data);
}
throw new InvalidParameterValueError('tuple require Array val');
case 'contractPrincipal':
case 'contract-principal': {
if (typeof arg.val !== 'string') {
throw new InvalidParameterValueError('contract principal requires string val');
}
const separator = arg.val.indexOf('.');
if (separator <= 0 || separator === arg.val.length - 1 || arg.val.indexOf('.', separator + 1) !== -1) {
throw new InvalidParameterValueError('contract principal must have address.contract-name format');
}
return contractPrincipalCV(arg.val.slice(0, separator), arg.val.slice(separator + 1));
}
case 'standardPrincipal':
case 'standard-principal':
if (typeof arg.val !== 'string') {
throw new InvalidParameterValueError('standard principal requires string val');
}
return standardPrincipalCV(arg.val);
case 'buffer':
if (arg.val instanceof Buffer) {
return bufferCV(arg.val);
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-coin-stx/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ export * from './transaction';
export * from './transactionBuilderFactory';
export * from './sbtcWithdrawBuilder';
export * from './btcAddressUtils';
export * from './pox5Builder';
export * from './constants';
export * from './iface';
export * as Utils from './utils';
253 changes: 253 additions & 0 deletions modules/sdk-coin-stx/src/lib/pox5Builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { BaseCoin as CoinConfig, StacksNetwork as BitgoStacksNetwork } from '@bitgo/statics';
import {
bufferCV,
contractPrincipalCV,
ContractCallPayload,
ClarityValue,
listCV,
noneCV,
responseErrorCV,
responseOkCV,
someCV,
standardPrincipalCV,
tupleCV,
uintCV,
} from '@stacks/transactions';
import { InvalidParameterValueError } from '@bitgo/sdk-core';
import { ContractBuilder } from './contractBuilder';
import {
CONTRACT_NAME_POX5,
FUNCTION_NAME_ANNOUNCE_L1_EARLY_EXIT,
FUNCTION_NAME_CALCULATE_REWARDS,
FUNCTION_NAME_CLAIM_REWARDS,
FUNCTION_NAME_CLAIM_STAKER_REWARDS,
FUNCTION_NAME_REGISTER_FOR_BOND,
FUNCTION_NAME_STAKE,
FUNCTION_NAME_STAKE_UPDATE,
FUNCTION_NAME_UNSTAKE,
FUNCTION_NAME_UPDATE_BOND_REGISTRATION,
} from './constants';

type Integer = bigint | number | string;
type ByteValue = Buffer | Uint8Array | string;

export interface Pox5LockupOutput {
height: number;
tx: ByteValue;
outputIndex: number;
header: ByteValue;
leafHashes: ByteValue[];
txCount: number;
txIndex: number;
amount: Integer;
unlockBurnHeight: number;
}

export type Pox5Lockup =
| {
kind: 'btc';
outputs: Pox5LockupOutput[];
unlockBytes: ByteValue;
}
| {
kind: 'sbtc';
sbtcSats: Integer;
};

export interface Pox5RegisterForBondParams {
bondIndex: Integer;
signerManager: string;
amountUstx: Integer;
lockup: Pox5Lockup;
signerCalldata?: ByteValue;
}

export interface Pox5UpdateBondRegistrationParams {
signerManager: string;
oldSignerManager: string;
signerCalldata?: ByteValue;
}

export interface Pox5AnnounceL1EarlyExitParams {
staker: string;
oldSignerManager: string;
}

export interface Pox5StakeParams {
signerManager: string;
amountUstx: Integer;
numCycles: Integer;
startBurnHt: Integer;
signerCalldata?: ByteValue;
}

export interface Pox5StakeUpdateParams {
signerManager: string;
oldSignerManager: string;
cyclesToExtend?: Integer;
amountIncrease?: Integer;
signerCalldata?: ByteValue;
}

export interface Pox5ClaimRewardsParams {
bondIndices: Integer[];
rewardCycle: Integer;
}

export interface Pox5ClaimStakerRewardsParams {
staker: string;
rewardCycle: Integer;
bondIndex?: Integer;
}

function byteBuffer(value: ByteValue, field: string): Buffer {
if (Buffer.isBuffer(value)) {
return value;
}
if (value instanceof Uint8Array) {
return Buffer.from(value);
}
const hex = value.startsWith('0x') ? value.slice(2) : value;
if (hex.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(hex)) {
throw new InvalidParameterValueError(`${field} must be an even-length hexadecimal string`);
}
return Buffer.from(hex, 'hex');
}

function contractPrincipal(value: string): ClarityValue {
const separator = value.indexOf('.');
if (separator <= 0 || separator === value.length - 1 || value.indexOf('.', separator + 1) !== -1) {
throw new InvalidParameterValueError(`${value} must have address.contract-name format`);
}
return contractPrincipalCV(value.slice(0, separator), value.slice(separator + 1));
}

function optionalBuffer(value: ByteValue | undefined): ClarityValue {
return value === undefined ? noneCV() : someCV(bufferCV(byteBuffer(value, 'signerCalldata')));
}

function lockupValue(lockup: Pox5Lockup): ClarityValue {
if (lockup.kind === 'sbtc') {
return responseErrorCV(uintCV(lockup.sbtcSats));
}
if (lockup.outputs.length === 0 || lockup.outputs.length > 10) {
throw new InvalidParameterValueError('btc lockup outputs must contain between 1 and 10 outputs');
}
for (const [index, output] of lockup.outputs.entries()) {
if (output.leafHashes.length > 14) {
throw new InvalidParameterValueError(`btc lockup output ${index} has more than 14 merkle siblings`);
}
}
return responseOkCV(
tupleCV({
outputs: listCV(
lockup.outputs.map((output) =>
tupleCV({
height: uintCV(output.height),
tx: bufferCV(byteBuffer(output.tx, 'tx')),
'output-index': uintCV(output.outputIndex),
header: bufferCV(byteBuffer(output.header, 'header')),
'leaf-hashes': listCV(output.leafHashes.map((hash) => bufferCV(byteBuffer(hash, 'leafHash')))),
'tx-count': uintCV(output.txCount),
'tx-index': uintCV(output.txIndex),
amount: uintCV(output.amount),
'unlock-burn-height': uintCV(output.unlockBurnHeight),
})
)
),
'staker-unlock-bytes': bufferCV(byteBuffer(lockup.unlockBytes, 'unlockBytes')),
})
);
}

export class Pox5Builder extends ContractBuilder {
constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
this._contractAddress = (coinConfig.network as BitgoStacksNetwork).stakingContractAddress;
this._contractName = CONTRACT_NAME_POX5;
}

public static isValidContractCall(payload: ContractCallPayload): boolean {
return payload.contractName.content === CONTRACT_NAME_POX5;
}

registerForBond(params: Pox5RegisterForBondParams): this {
this.functionName(FUNCTION_NAME_REGISTER_FOR_BOND);
this.functionArgs([
uintCV(params.bondIndex),
contractPrincipal(params.signerManager),
uintCV(params.amountUstx),
lockupValue(params.lockup),
optionalBuffer(params.signerCalldata),
]);
return this;
}

updateBondRegistration(params: Pox5UpdateBondRegistrationParams): this {
this.functionName(FUNCTION_NAME_UPDATE_BOND_REGISTRATION);
this.functionArgs([
contractPrincipal(params.signerManager),
contractPrincipal(params.oldSignerManager),
optionalBuffer(params.signerCalldata),
]);
return this;
}

announceL1EarlyExit(params: Pox5AnnounceL1EarlyExitParams): this {
this.functionName(FUNCTION_NAME_ANNOUNCE_L1_EARLY_EXIT);
this.functionArgs([standardPrincipalCV(params.staker), contractPrincipal(params.oldSignerManager)]);
return this;
}

stake(params: Pox5StakeParams): this {
this.functionName(FUNCTION_NAME_STAKE);
this.functionArgs([
contractPrincipal(params.signerManager),
uintCV(params.amountUstx),
uintCV(params.numCycles),
uintCV(params.startBurnHt),
optionalBuffer(params.signerCalldata),
]);
return this;
}

stakeUpdate(params: Pox5StakeUpdateParams): this {
this.functionName(FUNCTION_NAME_STAKE_UPDATE);
this.functionArgs([
contractPrincipal(params.signerManager),
contractPrincipal(params.oldSignerManager),
uintCV(params.cyclesToExtend ?? 0),
uintCV(params.amountIncrease ?? 0),
optionalBuffer(params.signerCalldata),
]);
return this;
}

unstake(oldSignerManager: string): this {
this.functionName(FUNCTION_NAME_UNSTAKE);
this.functionArgs([contractPrincipal(oldSignerManager)]);
return this;
}

calculateRewards(bondIndices: Integer[]): this {
this.functionName(FUNCTION_NAME_CALCULATE_REWARDS);
this.functionArgs([listCV(bondIndices.map((bondIndex) => uintCV(bondIndex)))]);
return this;
}

claimRewards(params: Pox5ClaimRewardsParams): this {
this.functionName(FUNCTION_NAME_CLAIM_REWARDS);
this.functionArgs([listCV(params.bondIndices.map((bondIndex) => uintCV(bondIndex))), uintCV(params.rewardCycle)]);
return this;
}

claimStakerRewardsForSigner(params: Pox5ClaimStakerRewardsParams): this {
this.functionName(FUNCTION_NAME_CLAIM_STAKER_REWARDS);
this.functionArgs([
standardPrincipalCV(params.staker),
uintCV(params.rewardCycle),
params.bondIndex === undefined ? noneCV() : someCV(uintCV(params.bondIndex)),
]);
return this;
}
}
Loading
Loading