Skip to main content
Module

x/clarinet/index.ts

Clarinet is a simple, modern and opinionated runtime for testing, integrating and deploying Clarity smart contracts.
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
export class Tx { type: number; sender: string; contractCall?: TxContractCall; transferStx?: TxTransfer; deployContract?: TxDeployContract;
constructor(type: number, sender: string) { this.type = type; this.sender = sender; }
static transferSTX(amount: number, recipient: string, sender: string) { let tx = new Tx(1, sender); tx.transferStx = { recipient, amount, }; return tx; }
static contractCall( contract: string, method: string, args: Array<string>, sender: string, ) { let tx = new Tx(2, sender); tx.contractCall = { contract, method, args, }; return tx; }
static deployContract(name: string, code: string, sender: string) { let tx = new Tx(3, sender); tx.deployContract = { name, code, }; return tx; }}
export interface TxContractCall { contract: string; method: string; args: Array<string>;}
export interface TxDeployContract { code: string; name: string;}
export interface TxTransfer { amount: number; recipient: string;}
export interface TxReceipt { result: string; events: Array<any>;}
export interface Block { height: number; receipts: Array<TxReceipt>;}
export interface Account { address: string; balance: number; name: string; mnemonic: string; derivation: string;}
export interface Chain { sessionId: number;}
export interface ReadOnlyFn { session_id: number; result: string; events: Array<any>;}
export interface EmptyBlock { session_id: number; block_height: number;}
export interface AssetsMaps { session_id: number, assets: { [name: string]: { [owner: string]: number } }}
export class Chain { sessionId: number; blockHeight: number = 1;
constructor(sessionId: number) { this.sessionId = sessionId; }
mineBlock(transactions: Array<Tx>): Block { let result = JSON.parse((Deno as any).core.opSync("mine_block", { sessionId: this.sessionId, transactions: transactions, })); this.blockHeight = result.block_height; let block: Block = { height: result.block_height, receipts: result.receipts, }; return block; }
mineEmptyBlock(count: number): EmptyBlock { let result = JSON.parse((Deno as any).core.opSync("mine_empty_blocks", { sessionId: this.sessionId, count: count, })); this.blockHeight = result.block_height; let emptyBlock: EmptyBlock = { session_id: result.session_id, block_height: result.block_height } return emptyBlock; }
mineEmptyBlockUntil(targetBlockHeight: number): EmptyBlock { let count = targetBlockHeight - this.blockHeight; if (count < 0) { throw new Error(`Chain tip cannot be moved from ${this.blockHeight} to ${targetBlockHeight}`); } return this.mineEmptyBlock(count); }
callReadOnlyFn( contract: string, method: string, args: Array<any>, sender: string, ): ReadOnlyFn { let result = JSON.parse((Deno as any).core.opSync("call_read_only_fn", { sessionId: this.sessionId, contract: contract, method: method, args: args, sender: sender, })); let readOnlyFn: ReadOnlyFn = { session_id: result.session_id, result: result.result, events: result.events } return readOnlyFn; }
getAssetsMaps(): AssetsMaps { let result = JSON.parse((Deno as any).core.opSync("get_assets_maps", { sessionId: this.sessionId, })); let assetsMaps: AssetsMaps = { session_id: result.session_id, assets: result.assets } return assetsMaps; }}
type BeforeHookFunction = ( chain: Chain, accounts: Map<string, Account>,) => void | Promise<void>;
type TestFunction = ( chain: Chain, accounts: Map<string, Account>, contracts: Map<string, Contract>,) => void | Promise<void>;type PreSetupFunction = () => Array<Tx>;
interface UnitTestOptions { name: string; only?: true; ignore? :true; beforeContractsDeployment?: BeforeHookFunction; fn: TestFunction;}
export interface Contract { contract_id: string; source: string; contract_interface: any}
export interface StacksNode { url: string;}
type ScriptFunction = ( accounts: Map<string, Account>, contracts: Map<string, Contract>, node: StacksNode,) => void | Promise<void>;
interface ScriptOptions { fn: ScriptFunction;}
export class Clarinet { static test(options: UnitTestOptions) { Deno.test({ name: options.name, only: options.only, ignore: options.ignore, async fn() { (Deno as any).core.ops();
let includesPreDeploymentSteps = options.beforeContractsDeployment !== undefined;
let result = JSON.parse((Deno as any).core.opSync("start_setup_chain", { name: options.name, includesPreDeploymentSteps: includesPreDeploymentSteps, }));
if (options.beforeContractsDeployment !== undefined) { let chain = new Chain(result["session_id"]); let accounts: Map<string, Account> = new Map(); for (let account of result["accounts"]) { accounts.set(account.name, account); } await options.beforeContractsDeployment(chain, accounts);
result = JSON.parse((Deno as any).core.opSync("complete_setup_chain", { sessionId: chain.sessionId, })); }
let chain = new Chain(result["session_id"]); let accounts: Map<string, Account> = new Map(); for (let account of result["accounts"]) { accounts.set(account.name, account); } let contracts: Map<string, any> = new Map(); for (let contract of result["contracts"]) { contracts.set(contract.contract_id, contract); } await options.fn(chain, accounts, contracts); }, }); }
static run(options: ScriptOptions) { Deno.test({ name: "running script", async fn() { (Deno as any).core.ops(); let result = JSON.parse((Deno as any).core.opSync("setup_chain", { name: "running script", transactions: [], })); let accounts: Map<string, Account> = new Map(); for (let account of result["accounts"]) { accounts.set(account.name, account); } let contracts: Map<string, any> = new Map(); for (let contract of result["contracts"]) { contracts.set(contract.contract_id, contract); } let stacks_node: StacksNode = { url: result["stacks_node_url"] }; await options.fn(accounts, contracts, stacks_node); }, }); }
}
export namespace types { const byteToHex: any = []; for (let n = 0; n <= 0xff; ++n) { const hexOctet = n.toString(16).padStart(2, "0"); byteToHex.push(hexOctet); }
function serializeTuple(input: Object) { let items: Array<string> = []; for (var [key, value] of Object.entries(input)) { if (typeof value === "object") { items.push(`${key}: { ${serializeTuple(value)} }`); } else if (Array.isArray(value)) { // todo(ludo): not supported, should panic } else { items.push(`${key}: ${value}`); } } return items.join(", "); }
function isObject(obj: any) { return typeof obj === "object" && !Array.isArray(obj); }
export function ok(val: string) { return `(ok ${val})`; }
export function err(val: string) { return `(err ${val})`; }
export function some(val: string) { return `(some ${val})`; }
export function none() { return `none`; }
export function bool(val: boolean) { return `${val}`; }
export function int(val: number|bigint) { return `${val}`; }
export function uint(val: number|bigint) { return `u${val}`; }
export function ascii(val: string) { return `"${val}"`; }
export function utf8(val: string) { return `u"${val}"`; }
export function buff(val: ArrayBuffer | string) { const buff = typeof val == "string" ? new TextEncoder().encode(val) : new Uint8Array(val);
const hexOctets = new Array(buff.length);
for (let i = 0; i < buff.length; ++i) { hexOctets[i] = byteToHex[buff[i]]; }
return `0x${hexOctets.join("")}`; }
export function list(val: Array<any>) { return `(list ${val.join(" ")})`; }
export function principal(val: string) { return `'${val}`; }
export function tuple(val: Object) { return `{ ${serializeTuple(val)} }`; }}
declare global { interface String { expectOk(): String; expectErr(): String; expectSome(): String; expectNone(): void; expectBool(value: boolean): boolean; expectUint(value: number|bigint): bigint; expectInt(value: number|bigint): bigint; expectBuff(value: ArrayBuffer): ArrayBuffer; expectAscii(value: String): String; expectUtf8(value: String): String; expectPrincipal(value: String): String; expectList(): Array<String>; expectTuple(): Record<string, string>; }
interface Array<T> { expectSTXTransferEvent( amount: Number|bigint, sender: String, recipient: String, ): Object; expectFungibleTokenTransferEvent( amount: Number|bigint, sender: String, recipient: String, assetId: String, ): Object; expectFungibleTokenMintEvent( amount: Number|bigint, recipient: String, assetId: String, ): Object; expectFungibleTokenBurnEvent( amount: Number|bigint, sender: String, assetId: String, ): Object; expectPrintEvent( contract_identifier: string, value: string ): Object; expectNonFungibleTokenTransferEvent( tokenId: String, sender: String, recipient: String, assetAddress: String, assetId: String ): Object; expectNonFungibleTokenMintEvent( tokenId: String, recipient: String, assetAddress: String, assetId: String ): Object; expectNonFungibleTokenBurnEvent( tokenId: String, sender: String, assetAddress: String, assetId: String ): Object; // expectEvent(sel: (e: Object) => Object): Object; }}
function consume(src: String, expectation: String, wrapped: boolean) { let dst = (" " + src).slice(1); let size = expectation.length; if (!wrapped && src !== expectation) { throw new Error( `Expected ${green(expectation.toString())}, got ${red(src.toString())}`, ); } if (wrapped) { size += 2; } if (dst.length < size) { throw new Error( `Expected ${green(expectation.toString())}, got ${red(src.toString())}`, ); } if (wrapped) { dst = dst.substring(1, dst.length - 1); } let res = dst.slice(0, expectation.length); if (res !== expectation) { throw new Error( `Expected ${green(expectation.toString())}, got ${red(src.toString())}`, ); } let leftPad = 0; if (dst.charAt(expectation.length) === " ") { leftPad = 1; } let remainder = dst.substring(expectation.length + leftPad); return remainder;}
String.prototype.expectOk = function () { return consume(this, "ok", true);};
String.prototype.expectErr = function () { return consume(this, "err", true);};
String.prototype.expectSome = function () { return consume(this, "some", true);};
String.prototype.expectNone = function () { return consume(this, "none", false);};
String.prototype.expectBool = function (value: boolean) { try { consume(this, `${value}`, false); } catch (error) { throw error; } return value;};
String.prototype.expectUint = function (value: number|bigint):bigint { try { consume(this, `u${value}`, false); } catch (error) { throw error; } return BigInt(value);};
String.prototype.expectInt = function (value: number|bigint):bigint { try { consume(this, `${value}`, false); } catch (error) { throw error; } return BigInt(value);};
String.prototype.expectBuff = function (value: ArrayBuffer) { let buffer = types.buff(value); if (this !== buffer) { throw new Error(`Expected ${green(buffer)}, got ${red(this.toString())}`); } return value;};
String.prototype.expectAscii = function (value: string) { try { consume(this, `"${value}"`, false); } catch (error) { throw error; } return value;};
String.prototype.expectUtf8 = function (value: string) { try { consume(this, `u"${value}"`, false); } catch (error) { throw error; } return value;};
String.prototype.expectPrincipal = function (value: string) { try { consume(this, `${value}`, false); } catch (error) { throw error; } return value;};
String.prototype.expectList = function () { if (this.charAt(0) !== "[" || this.charAt(this.length - 1) !== "]") { throw new Error( `Expected ${green("(list ...)")}, got ${red(this.toString())}`, ); }
let stack = []; let elements = []; let start = 1; for (var i = 0; i < this.length; i++) { if (this.charAt(i) === "," && stack.length == 1) { elements.push(this.substring(start, i)); start = i + 2; } if (["(", "[", "{"].includes(this.charAt(i))) { stack.push(this.charAt(i)); } if (this.charAt(i) === ")" && stack[stack.length - 1] === "(") { stack.pop(); } if (this.charAt(i) === "}" && stack[stack.length - 1] === "{") { stack.pop(); } if (this.charAt(i) === "]" && stack[stack.length - 1] === "[") { stack.pop(); } } let remainder = this.substring(start, this.length - 1); if (remainder.length > 0) { elements.push(remainder); } return elements;};
String.prototype.expectTuple = function () { if (this.charAt(0) !== "{" || this.charAt(this.length - 1) !== "}") { throw new Error( `Expected ${green("(tuple ...)")}, got ${red(this.toString())}`, ); }
let start = 1; let stack = []; let elements = []; for (var i = 0; i < this.length; i++) { if (this.charAt(i) === "," && stack.length == 1) { elements.push(this.substring(start, i)); start = i + 2; } if (["(", "[", "{"].includes(this.charAt(i))) { stack.push(this.charAt(i)); } if (this.charAt(i) === ")" && stack[stack.length - 1] === "(") { stack.pop(); } if (this.charAt(i) === "}" && stack[stack.length - 1] === "{") { stack.pop(); } if (this.charAt(i) === "]" && stack[stack.length - 1] === "[") { stack.pop(); } } let remainder = this.substring(start, this.length - 1); if (remainder.length > 0) { elements.push(remainder); }
let tuple: Record<string, string> = {}; for (let element of elements) { for (var i = 0; i < element.length; i++) { if (element.charAt(i) === ":") { let key: string = element.substring(0, i); let value: string = element.substring(i + 2, element.length); tuple[key] = value; break; } } }
return tuple;};
Array.prototype.expectSTXTransferEvent = function ( amount: Number|bigint, sender: String, recipient: String,) { for (let event of this) { try { let e: any = {}; e["amount"] = event.stx_transfer_event.amount.expectInt(amount); e["sender"] = event.stx_transfer_event.sender.expectPrincipal(sender); e["recipient"] = event.stx_transfer_event.recipient.expectPrincipal( recipient, ); return e; } catch (error) { continue; } } throw new Error(`Unable to retrieve expected STXTransferEvent`);};
Array.prototype.expectFungibleTokenTransferEvent = function ( amount: Number, sender: String, recipient: String, assetId: String,) { for (let event of this) { try { let e: any = {}; e["amount"] = event.ft_transfer_event.amount.expectInt(amount); e["sender"] = event.ft_transfer_event.sender.expectPrincipal(sender); e["recipient"] = event.ft_transfer_event.recipient.expectPrincipal( recipient, ); if (event.ft_transfer_event.asset_identifier.endsWith(assetId)) { e["assetId"] = event.ft_transfer_event.asset_identifier; } else { continue; } return e; } catch (error) { continue; } } throw new Error( `Unable to retrieve expected FungibleTokenTransferEvent(${amount}, ${sender}, ${recipient}, ${assetId})\n${ JSON.stringify(this) }`, );};
Array.prototype.expectFungibleTokenMintEvent = function ( amount: Number|bigint, recipient: String, assetId: String,) { for (let event of this) { try { let e: any = {}; e["amount"] = event.ft_mint_event.amount.expectInt(amount); e["recipient"] = event.ft_mint_event.recipient.expectPrincipal(recipient); if (event.ft_mint_event.asset_identifier.endsWith(assetId)) { e["assetId"] = event.ft_mint_event.asset_identifier; } else { continue; } return e; } catch (error) { continue; } } throw new Error(`Unable to retrieve expected FungibleTokenMintEvent`);};
Array.prototype.expectFungibleTokenBurnEvent = function ( amount: Number|bigint, sender: String, assetId: String,) { for (let event of this) { try { let e: any = {}; e["amount"] = event.ft_burn_event.amount.expectInt(amount); e["sender"] = event.ft_burn_event.sender.expectPrincipal(sender); if (event.ft_burn_event.asset_identifier.endsWith(assetId)) { e["assetId"] = event.ft_burn_event.asset_identifier; } else { continue; } return e; } catch (error) { continue; } } throw new Error(`Unable to retrieve expected FungibleTokenBurnEvent`);};
Array.prototype.expectPrintEvent = function ( contract_identifier: string, value: string) { for (let event of this) { try { let e: any = {}; e["contract_identifier"] = event.contract_event.contract_identifier.expectPrincipal( contract_identifier );
if (event.contract_event.topic.endsWith("print")) { e["topic"] = event.contract_event.topic; } else { continue; }
if (event.contract_event.value.endsWith(value)) { e["value"] = event.contract_event.value; } else { continue; } return e; } catch (error) { continue; } } throw new Error(`Unable to retrieve expected PrintEvent`);};// Array.prototype.expectEvent = function(sel: (e: Object) => Object) {// for (let event of this) {// try {// sel(event);// return event as Object;// } catch (error) {// continue;// }// }// throw new Error(`Unable to retrieve expected PrintEvent`);// }Array.prototype.expectNonFungibleTokenTransferEvent = function( tokenId: String, sender: String, recipient: String, assetAddress: String, assetId: String ) { for (let event of this) { try { let e: any = {}; if(event.nft_transfer_event.value === tokenId) { e["tokenId"] = event.nft_transfer_event.value; } else { continue; } e["sender"] = event.nft_transfer_event.sender.expectPrincipal(sender); e["recipient"] = event.nft_transfer_event.recipient.expectPrincipal(recipient); if (event.nft_transfer_event.asset_identifier === `${assetAddress}::${assetId}`) { e["assetId"] = event.nft_transfer_event.asset_identifier; } else { continue; } return e; } catch (error) { continue; } } throw new Error(`Unable to retrieve expected NonFungibleTokenTransferEvent`);}
Array.prototype.expectNonFungibleTokenMintEvent = function( tokenId: String, recipient: String, assetAddress: String, assetId: String ) { for (let event of this) { try { let e: any = {}; if(event.nft_mint_event.value === tokenId) { e["tokenId"] = event.nft_mint_event.value; } else { continue; } e["recipient"] = event.nft_mint_event.recipient.expectPrincipal(recipient); if (event.nft_mint_event.asset_identifier === `${assetAddress}::${assetId}`) { e["assetId"] = event.nft_mint_event.asset_identifier; } else { continue; } return e; } catch (error) { continue; } } throw new Error(`Unable to retrieve expected NonFungibleTokenMintEvent`);}
Array.prototype.expectNonFungibleTokenBurnEvent = function( tokenId: String, sender: String, assetAddress: String, assetId: String ) { for (let event of this) { try { let e: any = {}; if(event.nft_burn_event.value === tokenId) { e["tokenId"] = event.nft_burn_event.value; } else { continue; } e["sender"] = event.nft_burn_event.sender.expectPrincipal(sender); if (event.nft_burn_event.asset_identifier === `${assetAddress}::${assetId}`) { e["assetId"] = event.nft_burn_event.asset_identifier; } else { continue; } return e; } catch (error) { continue; } } throw new Error(`Unable to retrieve expected NonFungibleTokenBurnEvent`);}
const noColor = globalThis.Deno?.noColor ?? true;
interface Code { open: string; close: string; regexp: RegExp;}
let enabled = !noColor;
function code(open: number[], close: number): Code { return { open: `\x1b[${open.join(";")}m`, close: `\x1b[${close}m`, regexp: new RegExp(`\\x1b\\[${close}m`, "g"), };}
function run(str: string, code: Code): string { return enabled ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` : str;}
export function red(str: string): string { return run(str, code([31], 39));}
export function green(str: string): string { return run(str, code([32], 39));}