feat: support multisig address

This commit is contained in:
stxer
2025-09-13 04:52:49 -05:00
parent 51994eab79
commit 4f61c4c4c8
12 changed files with 2664 additions and 2370 deletions

28
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,28 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.preferences.importModuleSpecifier": "shortest",
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
}
}

63
biome.json Normal file
View File

@@ -0,0 +1,63 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"files": {
"includes": [
"**",
"!**/coverage",
"!**/dist",
"!**/node_modules",
"!**/cache"
]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all",
"semicolons": "always"
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noBannedTypes": "off"
},
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error"
},
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": false
}
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "stxer", "name": "stxer",
"version": "0.4.2", "version": "0.4.3",
"license": "MIT", "license": "MIT",
"author": "Kyle Fang", "author": "Kyle Fang",
"repository": { "repository": {
@@ -17,7 +17,7 @@
"scripts": { "scripts": {
"analyze": "size-limit --why", "analyze": "size-limit --why",
"build": "dts build", "build": "dts build",
"lint": "dts lint", "lint": "dts lint && biome check .",
"prepare": "dts build", "prepare": "dts build",
"size": "size-limit", "size": "size-limit",
"start": "dts watch", "start": "dts watch",
@@ -27,7 +27,7 @@
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "dts lint" "pre-commit": "dts lint && biome check ."
} }
}, },
"prettier": { "prettier": {
@@ -53,22 +53,23 @@
} }
], ],
"devDependencies": { "devDependencies": {
"@size-limit/preset-small-lib": "^11.1.5", "@biomejs/biome": "^2.2.4",
"@tsconfig/recommended": "^1.0.7", "@size-limit/preset-small-lib": "^11.2.0",
"@types/node": "^22.13.5", "@tsconfig/recommended": "^1.0.10",
"@types/node": "^24.3.3",
"dts-cli": "^2.0.5", "dts-cli": "^2.0.5",
"husky": "^9.1.6", "husky": "^9.1.7",
"size-limit": "^11.1.5", "size-limit": "^11.2.0",
"tslib": "^2.7.0", "tslib": "^2.8.1",
"tsx": "^4.19.2", "tsx": "^4.20.5",
"typescript": "^5.6.2" "typescript": "^5.9.2"
}, },
"dependencies": { "dependencies": {
"@stacks/network": "^7.0.0", "@stacks/network": "^7.2.0",
"@stacks/stacks-blockchain-api-types": "^7.14.1", "@stacks/stacks-blockchain-api-types": "^7.14.1",
"@stacks/transactions": "^7.0.0", "@stacks/transactions": "^7.2.0",
"c32check": "^2.0.0", "c32check": "^2.0.0",
"clarity-abi": "^0.1.0", "clarity-abi": "^0.1.0",
"ts-clarity": "^0.1.0-pre.2" "ts-clarity": "^0.1.1"
} }
} }

4617
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

View File

@@ -59,7 +59,7 @@ function convertResults(
export async function batchRead( export async function batchRead(
reads: BatchReads, reads: BatchReads,
options: BatchApiOptions = {} options: BatchApiOptions = {},
): Promise<BatchReadsResult> { ): Promise<BatchReadsResult> {
const ibh = const ibh =
reads.index_block_hash == null reads.index_block_hash == null
@@ -77,7 +77,10 @@ export async function batchRead(
if (reads.variables != null) { if (reads.variables != null) {
for (const variable of reads.variables) { for (const variable of reads.variables) {
payload.vars.push([serializeCV(variable.contract), variable.variableName]); payload.vars.push([
serializeCV(variable.contract),
variable.variableName,
]);
} }
} }
@@ -96,7 +99,7 @@ export async function batchRead(
payload.readonly.push([ payload.readonly.push([
serializeCV(ro.contract), serializeCV(ro.contract),
ro.functionName, ro.functionName,
...ro.functionArgs.map(v => serializeCV(v)), ...ro.functionArgs.map((v) => serializeCV(v)),
]); ]);
} }
} }
@@ -132,4 +135,4 @@ export async function batchRead(
maps: convertResults(rs.maps), maps: convertResults(rs.maps),
readonly: convertResults(rs.readonly), readonly: convertResults(rs.readonly),
}; };
} }

View File

@@ -7,8 +7,8 @@
import { import {
type ClarityValue, type ClarityValue,
type OptionalCV,
contractPrincipalCV, contractPrincipalCV,
type OptionalCV,
} from '@stacks/transactions'; } from '@stacks/transactions';
import { type BatchReads, batchRead } from './BatchAPI'; import { type BatchReads, batchRead } from './BatchAPI';
@@ -175,4 +175,4 @@ export class BatchProcessor {
} }
} }
} }
} }

View File

@@ -10,15 +10,15 @@ import type {
TContractPrincipal, TContractPrincipal,
TPrincipal, TPrincipal,
} from 'clarity-abi'; } from 'clarity-abi';
import { decodeAbi, encodeAbi } from 'ts-clarity';
import type { import type {
InferReadonlyCallParameterType,
InferReadonlyCallResultType,
InferMapValueType, InferMapValueType,
InferReadMapParameterType, InferReadMapParameterType,
InferReadonlyCallParameterType,
InferReadonlyCallResultType,
InferReadVariableParameterType, InferReadVariableParameterType,
InferVariableType, InferVariableType,
} from 'ts-clarity'; } from 'ts-clarity';
import { decodeAbi, encodeAbi } from 'ts-clarity';
import { BatchProcessor } from './BatchProcessor'; import { BatchProcessor } from './BatchProcessor';
// Shared processor instance with default settings // Shared processor instance with default settings
@@ -60,14 +60,16 @@ export async function callReadonly<
const processor = params.batchProcessor ?? defaultProcessor; const processor = params.batchProcessor ?? defaultProcessor;
const [deployer, contractName] = params.contract.split('.', 2); const [deployer, contractName] = params.contract.split('.', 2);
const fn = String(params.functionName); const fn = String(params.functionName);
const functionDef = (params.abi as readonly ClarityAbiFunction[]).find( const functionDef = (params.abi as readonly ClarityAbiFunction[]).find(
(def) => def.name === params.functionName, (def) => def.name === params.functionName,
); );
if (!functionDef) { if (!functionDef) {
throw new Error(`failed to find function definition for ${params.functionName}`); throw new Error(
`failed to find function definition for ${params.functionName}`,
);
} }
const argsKV = (params as unknown as { args: Record<string, unknown> }).args; const argsKV = (params as unknown as { args: Record<string, unknown> }).args;
const args: ClarityValue[] = []; const args: ClarityValue[] = [];
for (const argDef of functionDef.args) { for (const argDef of functionDef.args) {
@@ -87,7 +89,9 @@ export async function callReadonly<
resolve: (result: ClarityValue | OptionalCV) => { resolve: (result: ClarityValue | OptionalCV) => {
try { try {
const decoded = decodeAbi(functionDef.outputs.type, result); const decoded = decodeAbi(functionDef.outputs.type, result);
resolve(decoded as InferReadonlyCallResultType<Functions, FunctionName>); resolve(
decoded as InferReadonlyCallResultType<Functions, FunctionName>,
);
} catch (error) { } catch (error) {
reject(error); reject(error);
} }
@@ -98,21 +102,23 @@ export async function callReadonly<
} }
export async function readMap< export async function readMap<
Maps extends readonly ClarityAbiMap[] | readonly unknown[] = readonly ClarityAbiMap[], Maps extends
| readonly ClarityAbiMap[]
| readonly unknown[] = readonly ClarityAbiMap[],
MapName extends string = string, MapName extends string = string,
>( >(
params: InferReadMapParameterType<Maps, MapName> & ReadMapRuntimeParameters, params: InferReadMapParameterType<Maps, MapName> & ReadMapRuntimeParameters,
): Promise<InferMapValueType<Maps, MapName> | null> { ): Promise<InferMapValueType<Maps, MapName> | null> {
const processor = params.batchProcessor ?? defaultProcessor; const processor = params.batchProcessor ?? defaultProcessor;
const [deployer, contractName] = params.contract.split('.', 2); const [deployer, contractName] = params.contract.split('.', 2);
const mapDef = (params.abi as readonly ClarityAbiMap[]).find( const mapDef = (params.abi as readonly ClarityAbiMap[]).find(
(m) => m.name === params.mapName, (m) => m.name === params.mapName,
); );
if (!mapDef) { if (!mapDef) {
throw new Error(`failed to find map definition for ${params.mapName}`); throw new Error(`failed to find map definition for ${params.mapName}`);
} }
const key: ClarityValue = encodeAbi(mapDef.key, params.key); const key: ClarityValue = encodeAbi(mapDef.key, params.key);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -134,7 +140,10 @@ export async function readMap<
if (result.type !== ClarityType.OptionalSome) { if (result.type !== ClarityType.OptionalSome) {
throw new Error(`unexpected map value: ${result}`); throw new Error(`unexpected map value: ${result}`);
} }
const someCV = result as { type: ClarityType.OptionalSome; value: ClarityValue }; const someCV = result as {
type: ClarityType.OptionalSome;
value: ClarityValue;
};
const decoded = decodeAbi(mapDef.value, someCV.value); const decoded = decodeAbi(mapDef.value, someCV.value);
resolve(decoded as InferMapValueType<Maps, MapName>); resolve(decoded as InferMapValueType<Maps, MapName>);
} catch (error) { } catch (error) {
@@ -147,7 +156,9 @@ export async function readMap<
} }
export async function readVariable< export async function readVariable<
Variables extends readonly ClarityAbiVariable[] | readonly unknown[] = readonly ClarityAbiVariable[], Variables extends
| readonly ClarityAbiVariable[]
| readonly unknown[] = readonly ClarityAbiVariable[],
VariableName extends string = string, VariableName extends string = string,
>( >(
params: InferReadVariableParameterType<Variables, VariableName> & params: InferReadVariableParameterType<Variables, VariableName> &
@@ -155,12 +166,14 @@ export async function readVariable<
): Promise<InferVariableType<Variables, VariableName>> { ): Promise<InferVariableType<Variables, VariableName>> {
const processor = params.batchProcessor ?? defaultProcessor; const processor = params.batchProcessor ?? defaultProcessor;
const [deployer, contractName] = params.contract.split('.', 2); const [deployer, contractName] = params.contract.split('.', 2);
const varDef = (params.abi as readonly ClarityAbiVariable[]).find( const varDef = (params.abi as readonly ClarityAbiVariable[]).find(
(def) => def.name === params.variableName, (def) => def.name === params.variableName,
); );
if (!varDef) { if (!varDef) {
throw new Error(`failed to find variable definition for ${params.variableName}`); throw new Error(
`failed to find variable definition for ${params.variableName}`,
);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -183,4 +196,4 @@ export async function readVariable<
reject, reject,
}); });
}); });
} }

View File

@@ -1,3 +1,3 @@
export * from './BatchAPI'; export * from './BatchAPI';
export * from './clarity-api';
export * from './simulation'; export * from './simulation';
export * from './clarity-api';

View File

@@ -1,12 +1,14 @@
import { uintCV } from '@stacks/transactions'; import { uintCV } from '@stacks/transactions';
import { SimulationBuilder } from '..'; import { SimulationBuilder } from '..';
const test = () => SimulationBuilder.new() const sender = 'SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER';
.withSender('SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER')
.inlineSimulation('1ab04a8d13d72a301b77b6af9d4f612b') const test = () =>
.addContractDeploy({ SimulationBuilder.new()
contract_name: 'test-simulation', .withSender(sender)
source_code: ` .addContractDeploy({
contract_name: 'test-simulation',
source_code: `
;; counter example ;; counter example
(define-data-var counter uint u0) (define-data-var counter uint u0)
@@ -23,24 +25,18 @@ const test = () => SimulationBuilder.new()
(define-read-only (get-counter) (define-read-only (get-counter)
(ok (var-get counter))) (ok (var-get counter)))
`, `,
}) })
.addEvalCode( .addEvalCode(`${sender}.test-simulation`, '(get-counter)')
'SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER.test-simulation', .addContractCall({
'(get-counter)' contract_id: `${sender}.test-simulation`,
) function_name: 'increment',
.addContractCall({ function_args: [uintCV(10)],
contract_id: 'SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER.test-simulation', })
function_name: 'increment', .addEvalCode(`${sender}.test-simulation`, '(get-counter)')
function_args: [uintCV(10)], .run();
})
.addEvalCode(
'SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER.test-simulation',
'(get-counter)'
)
.run()
if (require.main === module) { if (require.main === module) {
; (async () => { (async () => {
await test() await test();
})().catch(console.error); })().catch(console.error);
} }

View File

@@ -4,11 +4,11 @@ import {
tupleCV, tupleCV,
uintCV, uintCV,
} from '@stacks/transactions'; } from '@stacks/transactions';
import { SIP010TraitABI } from 'clarity-abi/abis' import { SIP010TraitABI } from 'clarity-abi/abis';
import { unwrapResponse } from 'ts-clarity';
import { batchRead } from '../BatchAPI'; import { batchRead } from '../BatchAPI';
import { BatchProcessor } from '../BatchProcessor'; import { BatchProcessor } from '../BatchProcessor';
import { callReadonly, readMap, readVariable } from '../clarity-api'; import { callReadonly, readMap, readVariable } from '../clarity-api';
import { unwrapResponse } from 'ts-clarity';
async function batchReadsExample() { async function batchReadsExample() {
const rs = await batchRead({ const rs = await batchRead({
@@ -18,21 +18,21 @@ async function batchReadsExample() {
{ {
contract: contractPrincipalCV( contract: contractPrincipalCV(
'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275', 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
'liquidity-token-v5kbe3oqvac' 'liquidity-token-v5kbe3oqvac',
), ),
variableName: 'balance-x', variableName: 'balance-x',
}, },
{ {
contract: contractPrincipalCV( contract: contractPrincipalCV(
'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275', 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
'liquidity-token-v5kbe3oqvac' 'liquidity-token-v5kbe3oqvac',
), ),
variableName: 'balance-y', variableName: 'balance-y',
}, },
{ {
contract: contractPrincipalCV( contract: contractPrincipalCV(
'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275', 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
'liquidity-token-v5kbe3oqvac' 'liquidity-token-v5kbe3oqvac',
), ),
variableName: 'something-not-exists', variableName: 'something-not-exists',
}, },
@@ -41,15 +41,15 @@ async function batchReadsExample() {
{ {
contract: contractPrincipalCV( contract: contractPrincipalCV(
'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM', 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM',
'amm-registry-v2-01' 'amm-registry-v2-01',
), ),
mapName: 'pools-data-map', mapName: 'pools-data-map',
mapKey: tupleCV({ mapKey: tupleCV({
'token-x': principalCV( 'token-x': principalCV(
'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-wstx-v2' 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-wstx-v2',
), ),
'token-y': principalCV( 'token-y': principalCV(
'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex' 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex',
), ),
factor: uintCV(1e8), factor: uintCV(1e8),
}), }),
@@ -57,7 +57,7 @@ async function batchReadsExample() {
{ {
contract: contractPrincipalCV( contract: contractPrincipalCV(
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1', 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1',
'univ2-core' 'univ2-core',
), ),
mapName: 'pools', mapName: 'pools',
mapKey: uintCV(1), mapKey: uintCV(1),
@@ -65,7 +65,7 @@ async function batchReadsExample() {
{ {
contract: contractPrincipalCV( contract: contractPrincipalCV(
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1', 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1',
'contract-not-exists' 'contract-not-exists',
), ),
mapName: 'pools', mapName: 'pools',
mapKey: uintCV(1), mapKey: uintCV(1),
@@ -81,7 +81,6 @@ async function batchQueueProcessorExample() {
batchDelayMs: 1000, batchDelayMs: 1000,
}); });
const promiseA = processor.read({ const promiseA = processor.read({
mode: 'variable', mode: 'variable',
contractAddress: 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275', contractAddress: 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
@@ -105,7 +104,7 @@ async function batchSip010Example() {
abi: SIP010TraitABI.functions, abi: SIP010TraitABI.functions,
functionName: 'get-total-supply', functionName: 'get-total-supply',
contract: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex', contract: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex',
}).then(unwrapResponse) }).then(unwrapResponse);
const balance = callReadonly({ const balance = callReadonly({
abi: SIP010TraitABI.functions, abi: SIP010TraitABI.functions,
functionName: 'get-balance', functionName: 'get-balance',
@@ -113,20 +112,18 @@ async function batchSip010Example() {
args: { args: {
who: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-vault-v2-01', who: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-vault-v2-01',
}, },
}).then(unwrapResponse) }).then(unwrapResponse);
const paused = readVariable({ const paused = readVariable({
abi: [{ name: 'paused', type: 'bool', access: 'variable' },], abi: [{ name: 'paused', type: 'bool', access: 'variable' }],
variableName: 'paused', variableName: 'paused',
contract: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-vault-v2-01', contract: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-vault-v2-01',
}); });
const approved = readMap({ const approved = readMap({
abi: [ abi: [{ key: 'principal', name: 'approved-tokens', value: 'bool' }],
{ key: 'principal', name: 'approved-tokens', value: 'bool' },
],
mapName: 'approved-tokens', mapName: 'approved-tokens',
key: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex', key: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex',
contract: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-vault-v2-01', contract: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-vault-v2-01',
}) });
const result = await Promise.all([supply, balance, paused, approved]); const result = await Promise.all([supply, balance, paused, approved]);
console.log(result); console.log(result);
} }

View File

@@ -1,27 +1,55 @@
import { type AccountDataResponse, getNodeInfo, richFetch } from 'ts-clarity';
import type { Block } from '@stacks/stacks-blockchain-api-types';
import { import {
AddressVersion,
STACKS_MAINNET, STACKS_MAINNET,
STACKS_TESTNET, STACKS_TESTNET,
type StacksNetworkName, type StacksNetworkName,
} from '@stacks/network'; } from '@stacks/network';
import type { Block } from '@stacks/stacks-blockchain-api-types';
import { import {
AddressHashMode,
bufferCV,
type ClarityValue, type ClarityValue,
ClarityVersion, ClarityVersion,
PostConditionMode,
type StacksTransactionWire,
bufferCV,
contractPrincipalCV, contractPrincipalCV,
deserializeTransaction, deserializeTransaction,
type MultiSigSpendingCondition,
makeUnsignedContractCall, makeUnsignedContractCall,
makeUnsignedContractDeploy, makeUnsignedContractDeploy,
makeUnsignedSTXTokenTransfer, makeUnsignedSTXTokenTransfer,
PostConditionMode,
type StacksTransactionWire,
serializeCVBytes, serializeCVBytes,
stringAsciiCV, stringAsciiCV,
tupleCV, tupleCV,
uintCV, uintCV,
} from '@stacks/transactions'; } from '@stacks/transactions';
import { c32addressDecode } from 'c32check'; import { c32addressDecode } from 'c32check';
import { type AccountDataResponse, getNodeInfo, richFetch } from 'ts-clarity';
function setSender(tx: StacksTransactionWire, sender: string) {
const [addressVersion, signer] = c32addressDecode(sender);
switch (addressVersion) {
case AddressVersion.MainnetSingleSig:
case AddressVersion.TestnetSingleSig:
tx.auth.spendingCondition.hashMode = AddressHashMode.P2PKH;
tx.auth.spendingCondition.signer = signer;
break;
case AddressVersion.MainnetMultiSig:
case AddressVersion.TestnetMultiSig: {
const sc = tx.auth.spendingCondition;
tx.auth.spendingCondition = {
hashMode: AddressHashMode.P2SH,
signer,
fields: [],
signaturesRequired: 0,
nonce: sc.nonce,
fee: sc.fee,
} as MultiSigSpendingCondition;
break;
}
}
return tx;
}
function runTx(tx: StacksTransactionWire) { function runTx(tx: StacksTransactionWire) {
// type 0: run transaction // type 0: run transaction
@@ -43,8 +71,8 @@ export function runEval({ contract_id, code }: SimulationEval) {
tupleCV({ tupleCV({
contract: contractPrincipalCV(contract_address, contract_name), contract: contractPrincipalCV(contract_address, contract_name),
code: stringAsciiCV(code), code: stringAsciiCV(code),
}) }),
) ),
), ),
}); });
} }
@@ -53,7 +81,7 @@ export async function runSimulation(
apiEndpoint: string, apiEndpoint: string,
block_hash: string, block_hash: string,
block_height: number, block_height: number,
txs: (StacksTransactionWire | SimulationEval)[] txs: (StacksTransactionWire | SimulationEval)[],
) { ) {
// Convert 'sim-v1' to Uint8Array // Convert 'sim-v1' to Uint8Array
const header = new TextEncoder().encode('sim-v1'); const header = new TextEncoder().encode('sim-v1');
@@ -73,7 +101,7 @@ export async function runSimulation(
throw new Error('Invalid block hash format'); throw new Error('Invalid block hash format');
} }
const hashBytes = new Uint8Array( const hashBytes = new Uint8Array(
matches.map((byte) => Number.parseInt(byte, 16)) matches.map((byte) => Number.parseInt(byte, 16)),
); );
// Convert transactions to bytes // Convert transactions to bytes
@@ -128,10 +156,12 @@ export class SimulationBuilder {
private constructor(options: SimulationBuilderOptions = {}) { private constructor(options: SimulationBuilderOptions = {}) {
this.network = options.network ?? 'mainnet'; this.network = options.network ?? 'mainnet';
const isTestnet = this.network === 'testnet'; const isTestnet = this.network === 'testnet';
this.apiEndpoint = options.apiEndpoint ?? this.apiEndpoint =
options.apiEndpoint ??
(isTestnet ? 'https://testnet-api.stxer.xyz' : 'https://api.stxer.xyz'); (isTestnet ? 'https://testnet-api.stxer.xyz' : 'https://api.stxer.xyz');
this.stacksNodeAPI = options.stacksNodeAPI ?? this.stacksNodeAPI =
options.stacksNodeAPI ??
(isTestnet ? 'https://api.testnet.hiro.so' : 'https://api.hiro.so'); (isTestnet ? 'https://api.testnet.hiro.so' : 'https://api.hiro.so');
} }
@@ -139,37 +169,37 @@ export class SimulationBuilder {
return new SimulationBuilder(options); return new SimulationBuilder(options);
} }
// biome-ignore lint/style/useNumberNamespace: <explanation> // biome-ignore lint/style/useNumberNamespace: ignore this
private block = NaN; private block = NaN;
private sender = ''; private sender = '';
private steps: ( private steps: (
| { | {
// inline simulation // inline simulation
simulationId: string; simulationId: string;
} }
| { | {
// contract call // contract call
contract_id: string; contract_id: string;
function_name: string; function_name: string;
function_args?: ClarityValue[]; function_args?: ClarityValue[];
sender: string; sender: string;
fee: number; fee: number;
} }
| { | {
// contract deploy // contract deploy
contract_name: string; contract_name: string;
source_code: string; source_code: string;
deployer: string; deployer: string;
fee: number; fee: number;
clarity_version: ClarityVersion; clarity_version: ClarityVersion;
} }
| { | {
// STX transfer // STX transfer
recipient: string; recipient: string;
amount: number; amount: number;
sender: string; sender: string;
fee: number; fee: number;
} }
| SimulationEval | SimulationEval
)[] = []; )[] = [];
@@ -184,7 +214,7 @@ export class SimulationBuilder {
public inlineSimulation(simulationId: string) { public inlineSimulation(simulationId: string) {
this.steps.push({ this.steps.push({
simulationId, simulationId,
}) });
return this; return this;
} }
public addSTXTransfer(params: { public addSTXTransfer(params: {
@@ -195,7 +225,7 @@ export class SimulationBuilder {
}) { }) {
if (params.sender == null && this.sender === '') { if (params.sender == null && this.sender === '') {
throw new Error( throw new Error(
'Please specify a sender with useSender or adding a sender paramenter' 'Please specify a sender with useSender or adding a sender paramenter',
); );
} }
this.steps.push({ this.steps.push({
@@ -214,7 +244,7 @@ export class SimulationBuilder {
}) { }) {
if (params.sender == null && this.sender === '') { if (params.sender == null && this.sender === '') {
throw new Error( throw new Error(
'Please specify a sender with useSender or adding a sender paramenter' 'Please specify a sender with useSender or adding a sender paramenter',
); );
} }
this.steps.push({ this.steps.push({
@@ -233,7 +263,7 @@ export class SimulationBuilder {
}) { }) {
if (params.deployer == null && this.sender === '') { if (params.deployer == null && this.sender === '') {
throw new Error( throw new Error(
'Please specify a deployer with useSender or adding a deployer paramenter' 'Please specify a deployer with useSender or adding a deployer paramenter',
); );
} }
this.steps.push({ this.steps.push({
@@ -274,7 +304,7 @@ export class SimulationBuilder {
this.block = stacks_tip_height; this.block = stacks_tip_height;
} }
const info: Block = await richFetch( const info: Block = await richFetch(
`${this.stacksNodeAPI}/extended/v1/block/by_height/${this.block}?unanchored=true` `${this.stacksNodeAPI}/extended/v1/block/by_height/${this.block}?unanchored=true`,
).then((r) => r.json()); ).then((r) => r.json());
if ( if (
info.height !== this.block || info.height !== this.block ||
@@ -282,7 +312,7 @@ export class SimulationBuilder {
!info.hash.startsWith('0x') !info.hash.startsWith('0x')
) { ) {
throw new Error( throw new Error(
`failed to get block info for block height ${this.block}` `failed to get block info for block height ${this.block}`,
); );
} }
return { return {
@@ -302,21 +332,22 @@ SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER
Feedbacks and feature requests are welcome. Feedbacks and feature requests are welcome.
To get in touch: contact@stxer.xyz To get in touch: contact@stxer.xyz
--------------------------------` --------------------------------`,
); );
const block = await this.getBlockInfo(); const block = await this.getBlockInfo();
console.log( console.log(
`Using block height ${block.block_height} hash 0x${block.block_hash} to run simulation.` `Using block height ${block.block_height} hash 0x${block.block_hash} to run simulation.`,
); );
const txs: (StacksTransactionWire | SimulationEval)[] = []; const txs: (StacksTransactionWire | SimulationEval)[] = [];
const nonce_by_address = new Map<string, number>(); const nonce_by_address = new Map<string, number>();
const nextNonce = async (sender: string) => { const nextNonce = async (sender: string) => {
const nonce = nonce_by_address.get(sender); const nonce = nonce_by_address.get(sender);
if (nonce == null) { if (nonce == null) {
const url = `${this.stacksNodeAPI const url = `${
}/v2/accounts/${sender}?proof=${false}&tip=${block.index_block_hash}`; this.stacksNodeAPI
}/v2/accounts/${sender}?proof=${false}&tip=${block.index_block_hash}`;
const account: AccountDataResponse = await richFetch(url).then((r) => const account: AccountDataResponse = await richFetch(url).then((r) =>
r.json() r.json(),
); );
nonce_by_address.set(sender, account.nonce + 1); nonce_by_address.set(sender, account.nonce + 1);
return account.nonce; return account.nonce;
@@ -336,7 +367,21 @@ To get in touch: contact@stxer.xyz
} }
for (const step of this.steps) { for (const step of this.steps) {
if ('simulationId' in step) { if ('simulationId' in step) {
const previousSimulation: {steps: ({tx: string} | {code: string, contract: string})[]} = await fetch(`https://api.stxer.xyz/simulations/${step.simulationId}/request`).then(x => x.json()) const previousSimulation: {
steps: ({ tx: string } | { code: string; contract: string })[];
} = await fetch(
`https://api.stxer.xyz/simulations/${step.simulationId}/request`,
).then(async (rs) => {
const body = await rs.text();
if (!body.startsWith('{')) {
throw new Error(
`failed to get simulation ${step.simulationId}: ${body}`,
);
}
return JSON.parse(body) as {
steps: ({ tx: string } | { code: string; contract: string })[];
};
});
for (const step of previousSimulation.steps) { for (const step of previousSimulation.steps) {
if ('tx' in step) { if ('tx' in step) {
txs.push(deserializeTransaction(step.tx)); txs.push(deserializeTransaction(step.tx));
@@ -361,7 +406,7 @@ To get in touch: contact@stxer.xyz
postConditionMode: PostConditionMode.Allow, postConditionMode: PostConditionMode.Allow,
fee: step.fee, fee: step.fee,
}); });
tx.auth.spendingCondition.signer = c32addressDecode(step.sender)[1]; setSender(tx, step.sender);
txs.push(tx); txs.push(tx);
} else if ('sender' in step && 'recipient' in step) { } else if ('sender' in step && 'recipient' in step) {
const nonce = await nextNonce(step.sender); const nonce = await nextNonce(step.sender);
@@ -373,7 +418,7 @@ To get in touch: contact@stxer.xyz
publicKey: '', publicKey: '',
fee: step.fee, fee: step.fee,
}); });
tx.auth.spendingCondition.signer = c32addressDecode(step.sender)[1]; setSender(tx, step.sender);
txs.push(tx); txs.push(tx);
} else if ('deployer' in step) { } else if ('deployer' in step) {
const nonce = await nextNonce(step.deployer); const nonce = await nextNonce(step.deployer);
@@ -386,28 +431,29 @@ To get in touch: contact@stxer.xyz
postConditionMode: PostConditionMode.Allow, postConditionMode: PostConditionMode.Allow,
fee: step.fee, fee: step.fee,
}); });
tx.auth.spendingCondition.signer = c32addressDecode(step.deployer)[1]; setSender(tx, step.deployer);
txs.push(tx); txs.push(tx);
} else if ('code' in step) { } else if ('code' in step) {
txs.push(step); txs.push(step);
} else { } else {
// biome-ignore lint/style/noUnusedTemplateLiteral: <explanation> console.log(`Invalid simulation step: ${step}`);
console.log(`Invalid simulation step:`, step);
} }
} }
const id = await runSimulation( const id = await runSimulation(
`${this.apiEndpoint}/simulations`, `${this.apiEndpoint}/simulations`,
block.block_hash, block.block_hash,
block.block_height, block.block_height,
txs txs,
); );
console.log( console.log(
`Simulation will be available at: https://stxer.xyz/simulations/${this.network}/${id}` `Simulation will be available at: https://stxer.xyz/simulations/${this.network}/${id}`,
); );
return id; return id;
} }
public pipe(transform: (builder: SimulationBuilder) => SimulationBuilder): SimulationBuilder { public pipe(
transform: (builder: SimulationBuilder) => SimulationBuilder,
): SimulationBuilder {
return transform(this); return transform(this);
} }
} }