feat: add batch api

This commit is contained in:
Kyle Fang
2024-12-24 05:27:45 +00:00
parent 9330748d79
commit 33b109b398
5 changed files with 674 additions and 390 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "stxer",
"version": "0.2.6",
"version": "0.3.0",
"license": "MIT",
"author": "Kyle Fang",
"main": "dist/index.js",
@@ -18,7 +18,8 @@
"size": "size-limit",
"start": "dts watch",
"test": "dts test",
"sample": "tsx src/sample/counter.ts"
"sample:counter": "tsx src/sample/counter.ts",
"sample:read": "tsx src/sample/read.ts"
},
"husky": {
"hooks": {

157
src/batch-api.ts Normal file
View File

@@ -0,0 +1,157 @@
import {
type ClarityValue,
type ContractPrincipalCV,
deserializeCV,
type OptionalCV,
serializeCV,
} from '@stacks/transactions';
export interface BatchReads {
variables?: {
contract: ContractPrincipalCV;
variableName: string;
}[];
maps?: {
contract: ContractPrincipalCV;
mapName: string;
mapKey: ClarityValue;
}[];
index_block_hash?: string;
}
export interface BatchReadsResult {
variables: (ClarityValue | Error)[];
maps: (OptionalCV | Error)[];
}
export interface BatchApiOptions {
stxerApi?: string;
}
const DEFAULT_STXER_API = 'https://api.stxer.xyz';
export async function batchRead(
reads: BatchReads,
options: BatchApiOptions = {}
): Promise<BatchReadsResult> {
const payload: string[][] = [];
if (reads.variables != null) {
for (const variable of reads.variables) {
payload.push([serializeCV(variable.contract), variable.variableName]);
}
}
if (reads.maps != null) {
for (const map of reads.maps) {
payload.push([
serializeCV(map.contract),
map.mapName,
serializeCV(map.mapKey),
]);
}
}
const ibh =
reads.index_block_hash == null
? null
: reads.index_block_hash.startsWith('0x')
? reads.index_block_hash.substring(2)
: reads.index_block_hash;
const url = `${options.stxerApi ?? DEFAULT_STXER_API}/sidecar/v2/batch-reads${
ibh == null ? '' : `?tip=${ibh}`
}`;
const data = await fetch(url, {
method: 'POST',
body: JSON.stringify(payload),
});
const text = await data.text();
if (!text.includes('Ok') && !text.includes('Err')) {
throw new Error(
`Requesting batch reads failed: ${text}, url: ${url}, payload: ${JSON.stringify(
payload
)}`
);
}
const results = JSON.parse(text) as [{ Ok: string } | { Err: string }];
const rs: BatchReadsResult = {
variables: [],
maps: [],
};
let variablesLength = 0;
if (reads.variables != null) {
variablesLength = reads.variables.length;
for (let i = 0; i < variablesLength; i++) {
const result = results[i];
if ('Ok' in result) {
rs.variables.push(deserializeCV(result.Ok));
} else {
rs.variables.push(new Error(result.Err));
}
}
}
if (reads.maps != null) {
for (let i = 0; i < reads.maps.length; i++) {
const result = results[i + variablesLength];
if ('Ok' in result) {
rs.maps.push(deserializeCV(result.Ok) as OptionalCV);
} else {
rs.maps.push(new Error(result.Err));
}
}
}
return rs;
}
export interface BatchReadonlyRequest {
readonly: {
contract: ContractPrincipalCV;
functionName: string;
functionArgs: ClarityValue[];
}[];
index_block_hash?: string;
}
export async function batchReadonly(
req: BatchReadonlyRequest,
options: BatchApiOptions = {}
): Promise<(ClarityValue | Error)[]> {
const payload = req.readonly.map((r) => [
serializeCV(r.contract),
r.functionName,
...r.functionArgs.map((arg) => serializeCV(arg)),
]);
const ibh =
req.index_block_hash == null
? null
: req.index_block_hash.startsWith('0x')
? req.index_block_hash.substring(2)
: req.index_block_hash;
const url = `${options.stxerApi ?? DEFAULT_STXER_API}/sidecar/v2/batch-readonly${
ibh == null ? '' : `?tip=${ibh}`
}`;
const data = await fetch(url, {
method: 'POST',
body: JSON.stringify(payload),
});
const text = await data.text();
if (!text.includes('Ok') && !text.includes('Err')) {
throw new Error(
`Requesting batch readonly failed: ${text}, url: ${url}, payload: ${JSON.stringify(
payload
)}`
);
}
const results = JSON.parse(text) as [{ Ok: string } | { Err: string }];
const rs: (ClarityValue | Error)[] = [];
for (const result of results) {
if ('Ok' in result) {
rs.push(deserializeCV(result.Ok));
} else {
rs.push(new Error(result.Err));
}
}
return rs;
}

View File

@@ -1,388 +1,2 @@
import { type AccountDataResponse, getNodeInfo, richFetch } from 'ts-clarity';
import type { Block } from '@stacks/stacks-blockchain-api-types';
import {
networkFrom,
STACKS_MAINNET,
STACKS_TESTNET,
type StacksNetworkName,
} from '@stacks/network';
import {
type ClarityValue,
ClarityVersion,
PostConditionMode,
type StacksTransactionWire,
bufferCV,
contractPrincipalCV,
makeUnsignedContractCall,
makeUnsignedContractDeploy,
makeUnsignedSTXTokenTransfer,
serializeCVBytes,
stringAsciiCV,
tupleCV,
uintCV,
} from '@stacks/transactions';
import { c32addressDecode } from 'c32check';
function runTx(tx: StacksTransactionWire) {
// type 0: run transaction
return tupleCV({ type: uintCV(0), data: bufferCV(tx.serializeBytes()) });
}
export interface SimulationEval {
contract_id: string;
code: string;
}
export function runEval({ contract_id, code }: SimulationEval) {
const [contract_address, contract_name] = contract_id.split('.');
// type 1: eval arbitrary code inside a contract
return tupleCV({
type: uintCV(1),
data: bufferCV(
serializeCVBytes(
tupleCV({
contract: contractPrincipalCV(contract_address, contract_name),
code: stringAsciiCV(code),
})
)
),
});
}
export async function runSimulation(
apiEndpoint: string,
block_hash: string,
block_height: number,
txs: (StacksTransactionWire | SimulationEval)[]
) {
// Convert 'sim-v1' to Uint8Array
const header = new TextEncoder().encode('sim-v1');
// Create 8 bytes for block height
const heightBytes = new Uint8Array(8);
// Convert block height to bytes
const view = new DataView(heightBytes.buffer);
view.setBigUint64(0, BigInt(block_height), false); // false for big-endian
// Convert block hash to bytes
const hashHex = block_hash.startsWith('0x')
? block_hash.substring(2)
: block_hash;
// Replace non-null assertion with null check
const matches = hashHex.match(/.{1,2}/g);
if (!matches) {
throw new Error('Invalid block hash format');
}
const hashBytes = new Uint8Array(
matches.map((byte) => Number.parseInt(byte, 16))
);
// Convert transactions to bytes
const txBytes = txs
.map((t) => ('contract_id' in t && 'code' in t ? runEval(t) : runTx(t)))
.map((t) => serializeCVBytes(t));
// Combine all byte arrays
const totalLength =
header.length +
heightBytes.length +
hashBytes.length +
txBytes.reduce((acc, curr) => acc + curr.length, 0);
const body = new Uint8Array(totalLength);
let offset = 0;
body.set(header, offset);
offset += header.length;
body.set(heightBytes, offset);
offset += heightBytes.length;
body.set(hashBytes, offset);
offset += hashBytes.length;
for (const tx of txBytes) {
body.set(tx, offset);
offset += tx.length;
}
const rs = await fetch(apiEndpoint, {
method: 'POST',
body,
}).then(async (rs) => {
const response = await rs.text();
if (!response.startsWith('{')) {
throw new Error(`failed to submit simulation: ${response}`);
}
return JSON.parse(response) as { id: string };
});
return rs.id;
}
interface SimulationBuilderOptions {
apiEndpoint?: string;
stacksNodeAPI?: string;
network?: StacksNetworkName | string;
}
export class SimulationBuilder {
private apiEndpoint: string;
private stacksNodeAPI: string;
private network: StacksNetworkName | string;
private constructor(options: SimulationBuilderOptions = {}) {
this.apiEndpoint = options.apiEndpoint ?? 'https://api.stxer.xyz';
this.stacksNodeAPI = options.stacksNodeAPI ?? 'https://api.hiro.so';
this.network = options.network ?? 'mainnet';
}
public static new(options?: SimulationBuilderOptions) {
return new SimulationBuilder(options);
}
// biome-ignore lint/style/useNumberNamespace: <explanation>
private block = NaN;
private sender = '';
private steps: (
| {
// contract call
contract_id: string;
function_name: string;
function_args?: ClarityValue[];
sender: string;
fee: number;
}
| {
// contract deploy
contract_name: string;
source_code: string;
deployer: string;
fee: number;
clarity_version: ClarityVersion;
}
| {
// STX transfer
recipient: string;
amount: number;
sender: string;
fee: number;
}
| SimulationEval
)[] = [];
public useBlockHeight(block: number) {
this.block = block;
return this;
}
public withSender(address: string) {
this.sender = address;
return this;
}
public addSTXTransfer(params: {
recipient: string;
amount: number;
sender?: string;
fee?: number;
}) {
if (params.sender == null && this.sender === '') {
throw new Error(
'Please specify a sender with useSender or adding a sender paramenter'
);
}
this.steps.push({
...params,
sender: params.sender ?? this.sender,
fee: params.fee ?? 0,
});
return this;
}
public addContractCall(params: {
contract_id: string;
function_name: string;
function_args?: ClarityValue[];
sender?: string;
fee?: number;
}) {
if (params.sender == null && this.sender === '') {
throw new Error(
'Please specify a sender with useSender or adding a sender paramenter'
);
}
this.steps.push({
...params,
sender: params.sender ?? this.sender,
fee: params.fee ?? 0,
});
return this;
}
public addContractDeploy(params: {
contract_name: string;
source_code: string;
deployer?: string;
fee?: number;
clarity_version?: ClarityVersion;
}) {
if (params.deployer == null && this.sender === '') {
throw new Error(
'Please specify a deployer with useSender or adding a deployer paramenter'
);
}
this.steps.push({
...params,
deployer: params.deployer ?? this.sender,
fee: params.fee ?? 0,
clarity_version: params.clarity_version ?? ClarityVersion.Clarity3,
});
return this;
}
public addEvalCode(inside_contract_id: string, code: string) {
this.steps.push({
contract_id: inside_contract_id,
code,
});
return this;
}
public addMapRead(contract_id: string, map: string, key: string) {
this.steps.push({
contract_id,
code: `(map-get ${map} ${key})`,
});
return this;
}
public addVarRead(contract_id: string, variable: string) {
this.steps.push({
contract_id,
code: `(var-get ${variable})`,
});
return this;
}
private async getBlockInfo() {
if (Number.isNaN(this.block)) {
const { stacks_tip_height } = await getNodeInfo({
stacksEndpoint: this.stacksNodeAPI,
});
this.block = stacks_tip_height;
}
const info: Block = await richFetch(
`${this.stacksNodeAPI}/extended/v1/block/by_height/${this.block}?unanchored=true`
).then((r) => r.json());
if (
info.height !== this.block ||
typeof info.hash !== 'string' ||
!info.hash.startsWith('0x')
) {
throw new Error(
`failed to get block info for block height ${this.block}`
);
}
return {
block_height: this.block,
block_hash: info.hash.substring(2),
index_block_hash: info.index_block_hash.substring(2),
};
}
public async run() {
console.log(
`--------------------------------
This product can never exist without your support!
We receive sponsorship funds with:
SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER
Feedbacks and feature requests are welcome.
To get in touch: contact@stxer.xyz
--------------------------------`
);
const block = await this.getBlockInfo();
console.log(
`Using block height ${block.block_height} hash 0x${block.block_hash} to run simulation.`
);
const txs: (StacksTransactionWire | SimulationEval)[] = [];
const nonce_by_address = new Map<string, number>();
const nextNonce = async (sender: string) => {
const nonce = nonce_by_address.get(sender);
if (nonce == null) {
const url = `${
this.stacksNodeAPI
}/v2/accounts/${sender}?proof=${false}&tip=${block.index_block_hash}`;
const account: AccountDataResponse = await richFetch(url).then((r) =>
r.json()
);
nonce_by_address.set(sender, account.nonce + 1);
return account.nonce;
}
nonce_by_address.set(sender, nonce + 1);
return nonce;
};
for (const step of this.steps) {
let network = this.network === 'mainnet' ? STACKS_MAINNET : STACKS_TESTNET;
if (this.stacksNodeAPI) {
network = {
...network,
client: {
...network.client,
baseUrl: this.stacksNodeAPI,
},
};
}
if ('sender' in step && 'function_name' in step) {
const nonce = await nextNonce(step.sender);
const [contractAddress, contractName] = step.contract_id.split('.');
const tx = await makeUnsignedContractCall({
contractAddress,
contractName,
functionName: step.function_name,
functionArgs: step.function_args ?? [],
nonce,
network,
publicKey: '',
postConditionMode: PostConditionMode.Allow,
fee: step.fee,
});
tx.auth.spendingCondition.signer = c32addressDecode(step.sender)[1];
txs.push(tx);
} else if ('sender' in step && 'recipient' in step) {
const nonce = await nextNonce(step.sender);
const tx = await makeUnsignedSTXTokenTransfer({
recipient: step.recipient,
amount: step.amount,
nonce,
network,
publicKey: '',
fee: step.fee,
});
tx.auth.spendingCondition.signer = c32addressDecode(step.sender)[1];
txs.push(tx);
} else if ('deployer' in step) {
const nonce = await nextNonce(step.deployer);
const tx = await makeUnsignedContractDeploy({
contractName: step.contract_name,
codeBody: step.source_code,
nonce,
network,
publicKey: '',
postConditionMode: PostConditionMode.Allow,
fee: step.fee,
});
tx.auth.spendingCondition.signer = c32addressDecode(step.deployer)[1];
txs.push(tx);
} else if ('code' in step) {
txs.push(step);
} else {
// biome-ignore lint/style/noUnusedTemplateLiteral: <explanation>
console.log(`Invalid simulation step:`, step);
}
}
const id = await runSimulation(
`${this.apiEndpoint}/simulations`,
block.block_hash,
block.block_height,
txs
);
console.log(
`Simulation will be available at: https://stxer.xyz/simulations/${this.network}/${id}`
);
return id;
}
public pipe(transform: (builder: SimulationBuilder) => SimulationBuilder): SimulationBuilder {
return transform(this);
}
}
export * from './batch-api';
export * from './simulation';

125
src/sample/read.ts Normal file
View File

@@ -0,0 +1,125 @@
import {
contractPrincipalCV,
principalCV,
stringAsciiCV,
tupleCV,
uintCV,
} from '@stacks/transactions';
import { batchRead, batchReadonly } from '../batch-api';
async function batchReadsExample() {
const rs = await batchRead({
// index_block_hash:
// 'ce04817b9c6d90814ff9c06228d3a07d64335b1d9b01a233456fc304e34f7c0e', // block 373499
variables: [
{
contract: contractPrincipalCV(
'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
'liquidity-token-v5kbe3oqvac'
),
variableName: 'balance-x',
},
{
contract: contractPrincipalCV(
'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
'liquidity-token-v5kbe3oqvac'
),
variableName: 'balance-y',
},
{
contract: contractPrincipalCV(
'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
'liquidity-token-v5kbe3oqvac'
),
variableName: 'something-not-exists',
},
],
maps: [
{
contract: contractPrincipalCV(
'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM',
'amm-registry-v2-01'
),
mapName: 'pools-data-map',
mapKey: tupleCV({
'token-x': principalCV(
'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-wstx-v2'
),
'token-y': principalCV(
'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex'
),
factor: uintCV(1e8),
}),
},
{
contract: contractPrincipalCV(
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1',
'univ2-core'
),
mapName: 'pools',
mapKey: uintCV(1),
},
{
contract: contractPrincipalCV(
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1',
'contract-not-exists'
),
mapName: 'pools',
mapKey: uintCV(1),
},
],
});
console.log(rs);
}
async function batchReadonlyExample() {
const rs = await batchReadonly({
index_block_hash:
'ce04817b9c6d90814ff9c06228d3a07d64335b1d9b01a233456fc304e34f7c0e',
readonly: [
{
contract: contractPrincipalCV(
'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4',
'sbtc-token'
),
functionName: 'get-total-supply',
functionArgs: [],
},
{
contract: contractPrincipalCV(
'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4',
'sbtc-token'
),
functionName: 'get-balance',
functionArgs: [
principalCV('SP1CT7J2RWBZD62QAX36A2PQ3HKH5NFDGVHB8J34V'),
],
},
{
contract: contractPrincipalCV(
'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4',
'sbtc-token'
),
functionName: 'function-not-exists',
functionArgs: [],
},
{
contract: contractPrincipalCV(
'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4',
'sbtc-token'
),
functionName: 'get-balance',
functionArgs: [stringAsciiCV('invalid-args')],
},
],
});
console.log(rs);
}
async function main() {
await batchReadsExample();
await batchReadonlyExample();
}
main().catch(console.error);

387
src/simulation.ts Normal file
View File

@@ -0,0 +1,387 @@
import { type AccountDataResponse, getNodeInfo, richFetch } from 'ts-clarity';
import type { Block } from '@stacks/stacks-blockchain-api-types';
import {
STACKS_MAINNET,
STACKS_TESTNET,
type StacksNetworkName,
} from '@stacks/network';
import {
type ClarityValue,
ClarityVersion,
PostConditionMode,
type StacksTransactionWire,
bufferCV,
contractPrincipalCV,
makeUnsignedContractCall,
makeUnsignedContractDeploy,
makeUnsignedSTXTokenTransfer,
serializeCVBytes,
stringAsciiCV,
tupleCV,
uintCV,
} from '@stacks/transactions';
import { c32addressDecode } from 'c32check';
function runTx(tx: StacksTransactionWire) {
// type 0: run transaction
return tupleCV({ type: uintCV(0), data: bufferCV(tx.serializeBytes()) });
}
export interface SimulationEval {
contract_id: string;
code: string;
}
export function runEval({ contract_id, code }: SimulationEval) {
const [contract_address, contract_name] = contract_id.split('.');
// type 1: eval arbitrary code inside a contract
return tupleCV({
type: uintCV(1),
data: bufferCV(
serializeCVBytes(
tupleCV({
contract: contractPrincipalCV(contract_address, contract_name),
code: stringAsciiCV(code),
})
)
),
});
}
export async function runSimulation(
apiEndpoint: string,
block_hash: string,
block_height: number,
txs: (StacksTransactionWire | SimulationEval)[]
) {
// Convert 'sim-v1' to Uint8Array
const header = new TextEncoder().encode('sim-v1');
// Create 8 bytes for block height
const heightBytes = new Uint8Array(8);
// Convert block height to bytes
const view = new DataView(heightBytes.buffer);
view.setBigUint64(0, BigInt(block_height), false); // false for big-endian
// Convert block hash to bytes
const hashHex = block_hash.startsWith('0x')
? block_hash.substring(2)
: block_hash;
// Replace non-null assertion with null check
const matches = hashHex.match(/.{1,2}/g);
if (!matches) {
throw new Error('Invalid block hash format');
}
const hashBytes = new Uint8Array(
matches.map((byte) => Number.parseInt(byte, 16))
);
// Convert transactions to bytes
const txBytes = txs
.map((t) => ('contract_id' in t && 'code' in t ? runEval(t) : runTx(t)))
.map((t) => serializeCVBytes(t));
// Combine all byte arrays
const totalLength =
header.length +
heightBytes.length +
hashBytes.length +
txBytes.reduce((acc, curr) => acc + curr.length, 0);
const body = new Uint8Array(totalLength);
let offset = 0;
body.set(header, offset);
offset += header.length;
body.set(heightBytes, offset);
offset += heightBytes.length;
body.set(hashBytes, offset);
offset += hashBytes.length;
for (const tx of txBytes) {
body.set(tx, offset);
offset += tx.length;
}
const rs = await fetch(apiEndpoint, {
method: 'POST',
body,
}).then(async (rs) => {
const response = await rs.text();
if (!response.startsWith('{')) {
throw new Error(`failed to submit simulation: ${response}`);
}
return JSON.parse(response) as { id: string };
});
return rs.id;
}
interface SimulationBuilderOptions {
apiEndpoint?: string;
stacksNodeAPI?: string;
network?: StacksNetworkName | string;
}
export class SimulationBuilder {
private apiEndpoint: string;
private stacksNodeAPI: string;
private network: StacksNetworkName | string;
private constructor(options: SimulationBuilderOptions = {}) {
this.apiEndpoint = options.apiEndpoint ?? 'https://api.stxer.xyz';
this.stacksNodeAPI = options.stacksNodeAPI ?? 'https://api.hiro.so';
this.network = options.network ?? 'mainnet';
}
public static new(options?: SimulationBuilderOptions) {
return new SimulationBuilder(options);
}
// biome-ignore lint/style/useNumberNamespace: <explanation>
private block = NaN;
private sender = '';
private steps: (
| {
// contract call
contract_id: string;
function_name: string;
function_args?: ClarityValue[];
sender: string;
fee: number;
}
| {
// contract deploy
contract_name: string;
source_code: string;
deployer: string;
fee: number;
clarity_version: ClarityVersion;
}
| {
// STX transfer
recipient: string;
amount: number;
sender: string;
fee: number;
}
| SimulationEval
)[] = [];
public useBlockHeight(block: number) {
this.block = block;
return this;
}
public withSender(address: string) {
this.sender = address;
return this;
}
public addSTXTransfer(params: {
recipient: string;
amount: number;
sender?: string;
fee?: number;
}) {
if (params.sender == null && this.sender === '') {
throw new Error(
'Please specify a sender with useSender or adding a sender paramenter'
);
}
this.steps.push({
...params,
sender: params.sender ?? this.sender,
fee: params.fee ?? 0,
});
return this;
}
public addContractCall(params: {
contract_id: string;
function_name: string;
function_args?: ClarityValue[];
sender?: string;
fee?: number;
}) {
if (params.sender == null && this.sender === '') {
throw new Error(
'Please specify a sender with useSender or adding a sender paramenter'
);
}
this.steps.push({
...params,
sender: params.sender ?? this.sender,
fee: params.fee ?? 0,
});
return this;
}
public addContractDeploy(params: {
contract_name: string;
source_code: string;
deployer?: string;
fee?: number;
clarity_version?: ClarityVersion;
}) {
if (params.deployer == null && this.sender === '') {
throw new Error(
'Please specify a deployer with useSender or adding a deployer paramenter'
);
}
this.steps.push({
...params,
deployer: params.deployer ?? this.sender,
fee: params.fee ?? 0,
clarity_version: params.clarity_version ?? ClarityVersion.Clarity3,
});
return this;
}
public addEvalCode(inside_contract_id: string, code: string) {
this.steps.push({
contract_id: inside_contract_id,
code,
});
return this;
}
public addMapRead(contract_id: string, map: string, key: string) {
this.steps.push({
contract_id,
code: `(map-get ${map} ${key})`,
});
return this;
}
public addVarRead(contract_id: string, variable: string) {
this.steps.push({
contract_id,
code: `(var-get ${variable})`,
});
return this;
}
private async getBlockInfo() {
if (Number.isNaN(this.block)) {
const { stacks_tip_height } = await getNodeInfo({
stacksEndpoint: this.stacksNodeAPI,
});
this.block = stacks_tip_height;
}
const info: Block = await richFetch(
`${this.stacksNodeAPI}/extended/v1/block/by_height/${this.block}?unanchored=true`
).then((r) => r.json());
if (
info.height !== this.block ||
typeof info.hash !== 'string' ||
!info.hash.startsWith('0x')
) {
throw new Error(
`failed to get block info for block height ${this.block}`
);
}
return {
block_height: this.block,
block_hash: info.hash.substring(2),
index_block_hash: info.index_block_hash.substring(2),
};
}
public async run() {
console.log(
`--------------------------------
This product can never exist without your support!
We receive sponsorship funds with:
SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER
Feedbacks and feature requests are welcome.
To get in touch: contact@stxer.xyz
--------------------------------`
);
const block = await this.getBlockInfo();
console.log(
`Using block height ${block.block_height} hash 0x${block.block_hash} to run simulation.`
);
const txs: (StacksTransactionWire | SimulationEval)[] = [];
const nonce_by_address = new Map<string, number>();
const nextNonce = async (sender: string) => {
const nonce = nonce_by_address.get(sender);
if (nonce == null) {
const url = `${
this.stacksNodeAPI
}/v2/accounts/${sender}?proof=${false}&tip=${block.index_block_hash}`;
const account: AccountDataResponse = await richFetch(url).then((r) =>
r.json()
);
nonce_by_address.set(sender, account.nonce + 1);
return account.nonce;
}
nonce_by_address.set(sender, nonce + 1);
return nonce;
};
for (const step of this.steps) {
let network = this.network === 'mainnet' ? STACKS_MAINNET : STACKS_TESTNET;
if (this.stacksNodeAPI) {
network = {
...network,
client: {
...network.client,
baseUrl: this.stacksNodeAPI,
},
};
}
if ('sender' in step && 'function_name' in step) {
const nonce = await nextNonce(step.sender);
const [contractAddress, contractName] = step.contract_id.split('.');
const tx = await makeUnsignedContractCall({
contractAddress,
contractName,
functionName: step.function_name,
functionArgs: step.function_args ?? [],
nonce,
network,
publicKey: '',
postConditionMode: PostConditionMode.Allow,
fee: step.fee,
});
tx.auth.spendingCondition.signer = c32addressDecode(step.sender)[1];
txs.push(tx);
} else if ('sender' in step && 'recipient' in step) {
const nonce = await nextNonce(step.sender);
const tx = await makeUnsignedSTXTokenTransfer({
recipient: step.recipient,
amount: step.amount,
nonce,
network,
publicKey: '',
fee: step.fee,
});
tx.auth.spendingCondition.signer = c32addressDecode(step.sender)[1];
txs.push(tx);
} else if ('deployer' in step) {
const nonce = await nextNonce(step.deployer);
const tx = await makeUnsignedContractDeploy({
contractName: step.contract_name,
codeBody: step.source_code,
nonce,
network,
publicKey: '',
postConditionMode: PostConditionMode.Allow,
fee: step.fee,
});
tx.auth.spendingCondition.signer = c32addressDecode(step.deployer)[1];
txs.push(tx);
} else if ('code' in step) {
txs.push(step);
} else {
// biome-ignore lint/style/noUnusedTemplateLiteral: <explanation>
console.log(`Invalid simulation step:`, step);
}
}
const id = await runSimulation(
`${this.apiEndpoint}/simulations`,
block.block_hash,
block.block_height,
txs
);
console.log(
`Simulation will be available at: https://stxer.xyz/simulations/${this.network}/${id}`
);
return id;
}
public pipe(transform: (builder: SimulationBuilder) => SimulationBuilder): SimulationBuilder {
return transform(this);
}
}