@broxus/locklift-network
Version:
In-memory TVM-blockchain emulator for locklift
217 lines (191 loc) • 7.27 kB
text/typescript
import * as nt from "nekoton-wasm";
import { Address, FullContractState, LT_COLLATOR } from "everscale-inpage-provider";
import { Heap } from "heap-js";
import _ from "lodash";
import { EMPTY_STATE, GIVER_ADDRESS, GIVER_BOC, TEST_CODE_HASH, ZERO_ADDRESS } from "./constants";
import { BlockchainConfig } from "nekoton-wasm";
import { AccountFetcherCallback } from "../types";
const messageComparator = (a: nt.JsRawMessage, b: nt.JsRawMessage) => LT_COLLATOR.compare(a.lt || "0", b.lt || "0");
type ExecutorState = {
accounts: { [id: string]: FullContractState };
// txId -> tx
transactions: { [id: string]: nt.JsRawTransaction };
// txId -> trace
traces: { [id: string]: nt.EngineTraceInfo[] };
// msgHash -> tx_id
msgToTransaction: { [msgHash: string]: string };
// address -> tx_ids
addrToTransactions: { [addr: string]: string[] };
messageQueue: Heap<nt.JsRawMessage>;
};
interface LockliftTransport {
getBlockchainConfig(): Promise<BlockchainConfig>;
setExecutor(executor: LockliftExecutor): void;
}
export class LockliftExecutor {
private state: ExecutorState = {} as ExecutorState;
private snapshots: { [id: string]: ExecutorState } = {};
private nonce = 0;
private blockchainConfig: string | undefined;
private globalId: number | undefined;
private clock: nt.ClockWithOffset | undefined;
constructor(
private readonly transport: LockliftTransport,
private readonly accountFetcherCallback?: AccountFetcherCallback,
) {
this.createInitialBlockchainState();
transport.setExecutor(this);
}
private createInitialBlockchainState() {
this.state = {
accounts: {},
transactions: {},
msgToTransaction: {},
addrToTransactions: {},
traces: {},
messageQueue: new Heap<nt.JsRawMessage>(messageComparator),
};
// set this in order to pass standalone-client checks
this.state.accounts[ZERO_ADDRESS.toString()] = nt.parseFullAccountBoc(
nt.makeFullAccountBoc(GIVER_BOC),
) as nt.FullContractState;
this.state.accounts[ZERO_ADDRESS.toString()].codeHash = TEST_CODE_HASH;
// manually add giver account
this.state.accounts[GIVER_ADDRESS] = nt.parseFullAccountBoc(
nt.makeFullAccountBoc(GIVER_BOC),
) as nt.FullContractState;
}
async initialize() {
const config = await this.transport.getBlockchainConfig();
this.blockchainConfig = config.boc;
this.globalId = Number(config.globalId);
}
setClock(clock: nt.ClockWithOffset) {
if (this.clock !== undefined) throw new Error("Clock already set");
this.clock = clock;
}
_setAccount(address: Address | string, boc: string) {
this.state.accounts[address.toString()] = nt.parseFullAccountBoc(boc) as nt.FullContractState;
}
setAccount(address: Address | string, boc: string, type: "accountStuffBoc" | "fullAccountBoc") {
this.state.accounts[address.toString()] = nt.parseFullAccountBoc(
type === "accountStuffBoc" ? nt.makeFullAccountBoc(boc) : boc,
) as nt.FullContractState;
}
async getAccount(address: Address | string): Promise<FullContractState | undefined> {
return (
this.state.accounts[address.toString()] ||
this.accountFetcherCallback?.(address instanceof Address ? address : new Address(address))
.then(({ boc, type }) => {
if (!boc) throw new Error("Account not found");
this.setAccount(address, boc, type);
return this.state.accounts[address.toString()];
})
.catch(e => {
console.error(`Failed to fetch account ${address.toString()}: ${e.trace}`);
return undefined;
})
);
}
getAccounts(): { [id: string]: FullContractState } {
return this.state.accounts;
}
getTxTrace(txId: string): nt.EngineTraceInfo[] | undefined {
return this.state.traces[txId];
}
private saveTransaction(tx: nt.JsRawTransaction, trace: nt.EngineTraceInfo[]) {
this.state.transactions[tx.hash] = tx;
this.state.msgToTransaction[tx.inMessage.hash] = tx.hash;
this.state.addrToTransactions[tx.inMessage.dst as string] = [tx.hash].concat(
this.state.addrToTransactions[tx.inMessage.dst as string] || [],
);
this.state.traces[tx.hash] = trace;
}
getDstTransaction(msgHash: string): nt.JsRawTransaction | undefined {
return this.state.transactions[this.state.msgToTransaction[msgHash]];
}
getTransaction(id: string): nt.JsRawTransaction | undefined {
return this.state.transactions[id];
}
getTransactions(address: Address | string, fromLt: string, count: number): nt.JsRawTransaction[] {
const result: nt.JsRawTransaction[] = [];
for (const txId of this.state.addrToTransactions[address.toString()] || []) {
const rawTx = this.state.transactions[txId];
if (Number(rawTx.lt) > Number(fromLt)) continue;
result.push(rawTx);
if (result.length >= count) return result;
}
return result;
}
saveSnapshot(): number {
this.snapshots[this.nonce] = _.cloneDeep(this.state);
// postincrement!
return this.nonce++;
}
loadSnapshot(id: number) {
if (this.snapshots[id] === undefined) {
throw new Error(`Snapshot ${id} not found`);
}
this.state = this.snapshots[id];
}
clearSnapshots() {
this.snapshots = {};
}
resetBlockchainState() {
this.createInitialBlockchainState();
}
// process all msgs in queue
async processQueue() {
while (this.state.messageQueue.size() > 0) {
await this.processNextMsg();
}
}
// process msg with lowest lt in queue
async processNextMsg() {
const message = this.state.messageQueue.pop() as nt.JsRawMessage;
// everything is processed
if (!message) return;
const receiverAcc = await this.getAccount(message.dst as string);
let res: nt.TransactionExecutorExtendedOutput = nt.executeLocalExtended(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.blockchainConfig!,
receiverAcc ? nt.makeFullAccountBoc(receiverAcc.boc) : EMPTY_STATE,
message.boc,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Math.floor(this.clock!.nowMs / 1000),
false,
undefined,
undefined,
this.globalId,
false,
);
if ("account" in res && res.transaction.description.aborted) {
// run 1 more time with trace on
res = nt.executeLocalExtended(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.blockchainConfig!,
receiverAcc ? nt.makeFullAccountBoc(receiverAcc.boc) : EMPTY_STATE,
message.boc,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Math.floor(this.clock!.nowMs / 1000),
false,
undefined,
undefined,
this.globalId,
true,
);
}
if ("account" in res) {
this._setAccount(message.dst as string, res.account);
this.saveTransaction(res.transaction, res.trace);
res.transaction.outMessages.map((msg: nt.JsRawMessage) => {
if (msg.msgType === "ExtOut") return; // event
this.enqueueMsg(msg);
});
}
}
// push new message to queue
enqueueMsg(message: nt.JsRawMessage) {
this.state.messageQueue.push(message);
}
}