From 8d645210eca2f33377fefa75df1115313b45761f Mon Sep 17 00:00:00 2001 From: Aliaksandr Bahdanau Date: Wed, 15 May 2024 18:03:33 +0300 Subject: [PATCH] Implement ton public api reference --- src/blockchain/Blockchain.ts | 207 +++++++++++++++++++ src/blockchain/BlockchainContractProvider.ts | 31 +++ src/blockchain/BlockchainSender.ts | 6 +- src/blockchain/BlockchainStorage.ts | 29 +++ src/treasury/Treasury.ts | 20 ++ src/utils/message.ts | 5 +- src/utils/prettyLogTransaction.ts | 15 +- src/utils/printTransactionFees.ts | 13 ++ 8 files changed, 323 insertions(+), 3 deletions(-) diff --git a/src/blockchain/Blockchain.ts b/src/blockchain/Blockchain.ts index d1ea41e..bb7a4dc 100644 --- a/src/blockchain/Blockchain.ts +++ b/src/blockchain/Blockchain.ts @@ -59,6 +59,12 @@ export type BlockchainTransaction = Transaction & { externals: ExternalOut[], } +/** + * @type SendMessageResult Represents the result of sending a message. + * @property {BlockchainTransaction[]} transactions Array of blockchain transactions. + * @property {Event[]} events Array of blockchain events. + * @property {ExternalOut[]} externals - Array of external messages. + */ export type SendMessageResult = { transactions: BlockchainTransaction[], events: Event[], @@ -69,6 +75,10 @@ type ExtendsContractProvider = T extends ContractProvider ? true : (T extends export const SANDBOX_CONTRACT_SYMBOL = Symbol('SandboxContract') +/** + * @type SandboxContract Represents a sandbox contract. + * @template F Type parameter representing the original contract object. + */ export type SandboxContract = { [P in keyof F]: P extends `get${string}` ? (F[P] extends (x: infer CP, ...args: infer P) => infer R ? (ExtendsContractProvider extends true ? (...args: P) => R : never) : never) @@ -79,6 +89,11 @@ export type SandboxContract = { : F[P]); } +/** + * Provide way to check if contract is in sandbox environment. + * @param contract Any open contract + * @throws Error if contract not a sandbox contract + */ export function toSandboxContract(contract: OpenedContract): SandboxContract { if ((contract as any)[SANDBOX_CONTRACT_SYMBOL] === true) { return contract as any @@ -97,6 +112,13 @@ export type PendingMessage = (({ parentTransaction?: BlockchainTransaction, } +/** + * @type TreasuryParams Parameters for configuring a treasury contract. + * @property {number} workchain The workchain ID of the treasury. + * @property {boolean} predeploy If set the treasury will be deployed on the moment of creation. + * @property {bigint} balance Initial balance of the treasury. If omitted {@link TREASURY_INIT_BALANCE_TONS} is used. + * @property {boolean} resetBalanceIfZero If set and treasury balance is zero on moment of calling method it reset balance to {@link balance}. + */ export type TreasuryParams = Partial<{ workchain: number, predeploy: boolean, @@ -148,6 +170,14 @@ export class Blockchain { readonly executor: Executor + /** + * Saves snapshot of current blockchain. + * ```ts + * const snapshot = blockchain.snapshot(); + * // some operations + * blockchain.loadFrom(snapshot); // restores blockchain state + * ``` + */ snapshot(): BlockchainSnapshot { return { contracts: this.storage.knownContracts().map(s => s.snapshot()), @@ -160,6 +190,12 @@ export class Blockchain { } } + /** + * Restores blockchain state from snapshot. + * Usage provided in {@link snapshot}. + * + * @param snapshot Snapshot of blockchain + */ async loadFrom(snapshot: BlockchainSnapshot) { this.storage.clearKnownContracts() for (const contract of snapshot.contracts) { @@ -175,14 +211,24 @@ export class Blockchain { this.nextCreateWalletIndex = snapshot.nextCreateWalletIndex } + /** + * @returns Current time in blockchain + */ get now() { return this.currentTime } + /** + * Updates Current time in blockchain. + * @param now UNIX time to set + */ set now(now: number | undefined) { this.currentTime = now } + /** + * @returns Current logical time in blockchain + */ get lt() { return this.currentLt } @@ -193,19 +239,59 @@ export class Blockchain { this.storage = opts.storage } + /** + * @returns Config used in blockchain. + */ get config(): Cell { return Cell.fromBase64(this.networkConfig) } + /** + * @returns Config used in blockchain in base64 format. + */ get configBase64(): string { return this.networkConfig } + + /** + * Pushes message to message queue and executes it. Every transaction simply increases lt by {@link LT_ALIGN}. + * ```ts + * const result = await blockchain.sendMessage(internal({ + * from: sender.address, + * to: address, + * value: toNano('1'), + * body: beginCell().storeUint(0, 32).endCell(), + * })); + * ``` + * + * @param message Message to sent + * @param params Optional params + * @returns Result of queue processing + */ async sendMessage(message: Message | Cell, params?: MessageParams): Promise { await this.pushMessage(message) return await this.runQueue(params) } + /** + * Pushes message to message queue and executes it step by step. + * ```ts + * const message = internal({ + * from: sender.address, + * to: address, + * value: toNano('1'), + * body: beginCell().storeUint(0, 32).endCell(), + * }, { randomSeed: crypto.randomBytes(32) }); + * for await (const tx of await blockchain.sendMessageIter(message)) { + * // process transaction + * } + * ``` + * + * @param message Message to sent + * @param params Optional params + * @returns Async iterable of {@link BlockchainTransaction} + */ async sendMessageIter(message: Message | Cell, params?: MessageParams): Promise & AsyncIterable> { params = { now: this.now, @@ -217,6 +303,17 @@ export class Blockchain { return await this.txIter(true, params) } + /** + * Runs tick or tock transaction. + * ```ts + * let res = await blockchain.runTickTock(address, 'tock'); + * ``` + * + * @param on Address or addresses to run tick-tock + * @param which Type of transaction (tick or tock) + * @param [params] Params to run tick tock transaction + * @returns Result of tick-tock transaction + */ async runTickTock(on: Address | Address[], which: TickOrTock, params?: MessageParams): Promise { for (const addr of (Array.isArray(on) ? on : [on])) { await this.pushTickTock(addr, which) @@ -224,6 +321,21 @@ export class Blockchain { return await this.runQueue(params) } + /** + * Runs get method on contract. + * ```ts + * const { stackReader } = await blockchain.runGetMethod(address, 'get_now', [], { + * now: 2, + * }); + * const now = res.stackReader.readNumber(); + * ``` + * + * @param address Address or addresses to run get method + * @param method MethodId or method name to run + * @param stack Method params + * @param [params] Params to run get method + * @returns Result of get method + */ async runGetMethod(address: Address, method: number | string, stack: TupleItem[] = [], params?: GetMethodParams) { return await (await this.getContract(address)).get(method, stack, { now: this.now, @@ -354,6 +466,16 @@ export class Blockchain { }) } + /** + * Creates new provider for contract address. + * ```ts + * const provider = this.provider(address, init); + * const txs = await provider.getTransactions(...); + * ``` + * + * @param address Address to create provider + * @param init Initial state of contract + */ provider(address: Address, init?: StateInit | null): ContractProvider { return new BlockchainContractProvider({ getContract: (addr) => this.getContract(addr), @@ -364,6 +486,15 @@ export class Blockchain { }, address, init) } + /** + * Creates sender for address. + * ```ts + * const sender = this.sender(address); + * await contract.send(sender, ...); + * ``` + * + * @param address Address to create sender + */ sender(address: Address): Sender { return new BlockchainSender({ pushMessage: (msg) => this.pushMessage(msg), @@ -374,6 +505,16 @@ export class Blockchain { return `${workchain}:${seed}` } + /** + * Creates treasury wallet contract. This wallet is used as alternative to wallet-v4 smart contract. + * ```ts + * const sender = this.sender(address); + * await contract.send(sender, ...); + * ``` + * + * @param {string} seed Initial seed for treasury. If the same seed is used to create a treasury, then these treasuries will be identical + * @param [params] Params for treasury creation. See {@link TreasuryParams} for more information. + */ async treasury(seed: string, params?: TreasuryParams) { const subwalletId = testSubwalletId(seed) const wallet = this.openContract(TreasuryContract.create(params?.workchain ?? 0, subwalletId)) @@ -394,6 +535,15 @@ export class Blockchain { return wallet } + /** + * Bulk variant of {@link treasury}. + * ```ts + * const [wallet1, wallet2, wallet3] = await blockchain.createWallets(3); + * ``` + * @param n Number of wallets to create + * @param params Params for treasury creation. See {@link TreasuryParams} for more information. + * @returns Array of opened treasury contracts + */ async createWallets(n: number, params?: TreasuryParams) { const wallets: SandboxContract[] = [] for (let i = 0; i < n; i++) { @@ -403,6 +553,14 @@ export class Blockchain { return wallets } + /** + * Opens contract. Returns proxy that substitutes the blockchain Provider in methods starting with get and set. + * ```ts + * const contract = blockchain.openContract(new Contract(address)); + * ``` + * + * @param contract Contract to open. + */ openContract(contract: T) { let address: Address; let init: StateInit | undefined = undefined; @@ -468,6 +626,11 @@ export class Blockchain { return promise } + + /** + * Retrieves contract from {@link BlockchainStorage}. + * @param address Address of contract to get + */ async getContract(address: Address) { try { const contract = await this.startFetchingContract(address) @@ -479,10 +642,17 @@ export class Blockchain { } } + /** + * @returns {LogsVerbosity} level + */ get verbosity() { return this.logsVerbosity } + /** + * Updates logs verbosity level. + * @param {LogsVerbosity} value + */ set verbosity(value: LogsVerbosity) { this.logsVerbosity = value } @@ -501,14 +671,51 @@ export class Blockchain { contract.account = account } + + /** + * Retrieves global libs cell + */ get libs() { return this.globalLibs } + /** + * Update global blockchain libs. + * ```ts + * const code = await compile('Contract'); + * + * const libsDict = Dictionary.empty(Dictionary.Keys.Buffer(32), Dictionary.Values.Cell()); + * libsDict.set(code.hash(), code); + * + * blockchain.libs = beginCell().storeDictDirect(libsDict).endCell(); + * ``` + * + * @param value Config used in blockchain. If omitted {@link defaultConfig} used. + */ set libs(value: Cell | undefined) { this.globalLibs = value } + /** + * Creates instance of sandbox blockchain. + * ```ts + * const blockchain = await Blockchain.create({ config: 'slim' }); + * ``` + * + * Remote storage example: + * ```ts + * let client = new TonClient4({ + * endpoint: 'https://mainnet-v4.tonhubapi.com' + * }) + * + * let blockchain = await Blockchain.create({ + * storage: new RemoteBlockchainStorage(wrapTonClient4ForRemote(client), 34892000) + * }); + * ``` + * + * @param [opts.config] Config used in blockchain. If omitted {@link defaultConfig} used. + * @param [opts.storage] Contracts storage used for blockchain. If omitted {@link LocalBlockchainStorage} used. + */ static async create(opts?: { config?: BlockchainConfig, storage?: BlockchainStorage }) { return new Blockchain({ executor: await Executor.create(), diff --git a/src/blockchain/BlockchainContractProvider.ts b/src/blockchain/BlockchainContractProvider.ts index cf26c87..c078c25 100644 --- a/src/blockchain/BlockchainContractProvider.ts +++ b/src/blockchain/BlockchainContractProvider.ts @@ -50,6 +50,9 @@ export interface SandboxContractProvider extends ContractProvider { tickTock(which: TickOrTock): Promise } +/** + * Provider used in contracts to send messages or invoke getters. For additional information see {@link Blockchain.provider} + */ export class BlockchainContractProvider implements SandboxContractProvider { constructor( private readonly blockchain: { @@ -63,9 +66,13 @@ export class BlockchainContractProvider implements SandboxContractProvider { private readonly init?: StateInit | null, ) {} + /** + * Opens contract. For additional information see {@link Blockchain.open} + */ open(contract: T): OpenedContract { return this.blockchain.openContract(contract); } + async getState(): Promise { const contract = await this.blockchain.getContract(this.address) return { @@ -77,6 +84,12 @@ export class BlockchainContractProvider implements SandboxContractProvider { state: convertState(contract.accountState), } } + + /** + * Invokes get method. + * @param name Name of get method + * @param args Args to invoke get method. + */ async get(name: string, args: TupleItem[]): Promise { const result = await this.blockchain.runGetMethod(this.address, name, args) const ret = { @@ -88,9 +101,19 @@ export class BlockchainContractProvider implements SandboxContractProvider { delete (ret as any).stackReader return ret } + + /** + * Throws error in every call + * @throws {Error} + */ getTransactions(address: Address, lt: bigint, hash: Buffer, limit?: number | undefined): Promise { throw new Error("`getTransactions` is not implemented in `BlockchainContractProvider`, do not use it in the tests") } + + /** + * Pushes external-in message to message queue and executes it. + * @param message Message to push + */ async external(message: Cell) { const init = ((await this.getState()).state.type !== 'active' && this.init) ? this.init : undefined @@ -104,6 +127,10 @@ export class BlockchainContractProvider implements SandboxContractProvider { body: message, }) } + + /** + * Pushes internal message to message queue and executes it. + */ async internal(via: Sender, args: { value: string | bigint; bounce?: boolean | null; sendMode?: SendMode; body?: string | Cell | null; }) { const init = ((await this.getState()).state.type !== 'active' && this.init) ? this.init : undefined @@ -122,6 +149,10 @@ export class BlockchainContractProvider implements SandboxContractProvider { body, }) } + + /** + * Pushes tick-tock message to message queue and executes it. + */ async tickTock(which: TickOrTock) { await this.blockchain.pushTickTock(this.address, which) } diff --git a/src/blockchain/BlockchainSender.ts b/src/blockchain/BlockchainSender.ts index 56636b3..145b06a 100644 --- a/src/blockchain/BlockchainSender.ts +++ b/src/blockchain/BlockchainSender.ts @@ -1,5 +1,9 @@ import { Address, Cell, Message, Sender, SenderArguments } from "@ton/core"; + +/** + * Sender for sandbox blockchain. For additional information see {@link Blockchain.sender} + */ export class BlockchainSender implements Sender { constructor( private readonly blockchain: { @@ -26,4 +30,4 @@ export class BlockchainSender implements Sender { body: args.body ?? new Cell() }) } -} \ No newline at end of file +} diff --git a/src/blockchain/BlockchainStorage.ts b/src/blockchain/BlockchainStorage.ts index 264a622..d85bdd4 100644 --- a/src/blockchain/BlockchainStorage.ts +++ b/src/blockchain/BlockchainStorage.ts @@ -2,6 +2,10 @@ import {AccountState, Address, Cell} from "@ton/core"; import {SmartContract} from "./SmartContract"; import {Blockchain} from "./Blockchain"; + +/** + * @interface BlockchainStorage Provides information about contracts by blockchain + */ export interface BlockchainStorage { getContract(blockchain: Blockchain, address: Address): Promise knownContracts(): SmartContract[] @@ -61,6 +65,19 @@ function convertTonClient4State(state: { } } +/** + * Wraps ton client for remote storage. + * + * ```ts + * let client = new TonClient4({ + * endpoint: 'https://mainnet-v4.tonhubapi.com' + * }) + * + * let remoteStorageClient = wrapTonClient4ForRemote(client); + * ``` + * + * @param client TonClient4 to wrap + */ export function wrapTonClient4ForRemote(client: { getLastBlock(): Promise<{ last: { @@ -108,6 +125,18 @@ export function wrapTonClient4ForRemote(client: { } } +/** + * @class {RemoteBlockchainStorage} Remote blockchain storage implementation. + * ```ts + * let client = new TonClient4({ + * endpoint: 'https://mainnet-v4.tonhubapi.com' + * }) + * + * let blockchain = await Blockchain.create({ + * storage: new RemoteBlockchainStorage(wrapTonClient4ForRemote(client), 34892000) + * }); + * ``` + */ export class RemoteBlockchainStorage implements BlockchainStorage { private contracts: Map = new Map() private client: RemoteBlockchainStorageClient diff --git a/src/treasury/Treasury.ts b/src/treasury/Treasury.ts index 46fd41f..0be5830 100644 --- a/src/treasury/Treasury.ts +++ b/src/treasury/Treasury.ts @@ -43,6 +43,9 @@ function senderArgsToMessageRelaxed(args: SenderArguments): MessageRelaxed { }) } +/** + * @class TreasuryContract is Wallet v4 alternative. For additional information see {@link Blockchain.treasury} + */ export class TreasuryContract implements Contract { static readonly code = Cell.fromBase64('te6cckEBBAEARQABFP8A9KQT9LzyyAsBAgEgAwIAWvLT/+1E0NP/0RK68qL0BNH4AH+OFiGAEPR4b6UgmALTB9QwAfsAkTLiAbPmWwAE0jD+omUe') @@ -63,6 +66,11 @@ export class TreasuryContract implements Contract { this.subwalletId = subwalletId; } + /** + * Send bulk messages using one external message. + * @param messages Messages to send + * @param sendMode Send mode of every message + */ async sendMessages(provider: ContractProvider, messages: MessageRelaxed[], sendMode?: SendMode) { let transfer = this.createTransfer({ sendMode: sendMode, @@ -71,10 +79,16 @@ export class TreasuryContract implements Contract { await provider.external(transfer) } + /** + * Sends message by arguments specified. + */ async send(provider: ContractProvider, args: SenderArguments) { await this.sendMessages(provider, [senderArgsToMessageRelaxed(args)], args.sendMode ?? undefined) } + /** + * @returns Sender + */ getSender(provider: ContractProvider): Treasury { return { address: this.address, @@ -88,10 +102,16 @@ export class TreasuryContract implements Contract { }; } + /** + * @returns wallet balance in nanoTONs + */ async getBalance(provider: ContractProvider): Promise { return (await provider.getState()).balance } + /** + * Creates transfer cell for {@link sendMessages}. + */ createTransfer(args: { messages: MessageRelaxed[] sendMode?: SendMode, diff --git a/src/utils/message.ts b/src/utils/message.ts index 9e496f2..4eef342 100644 --- a/src/utils/message.ts +++ b/src/utils/message.ts @@ -1,5 +1,8 @@ import { Address, Cell, Message, StateInit } from "@ton/core"; +/** + * Creates {@link Message} from params. + */ export function internal(params: { from: Address to: Address @@ -31,4 +34,4 @@ export function internal(params: { body: params.body ?? new Cell(), init: params.stateInit, } -} \ No newline at end of file +} diff --git a/src/utils/prettyLogTransaction.ts b/src/utils/prettyLogTransaction.ts index fe2fc08..5b035ee 100644 --- a/src/utils/prettyLogTransaction.ts +++ b/src/utils/prettyLogTransaction.ts @@ -1,5 +1,9 @@ import {Transaction, fromNano} from "@ton/core"; +/** + * @param tx Transaction to create log string + * @returns Transaction log string + */ export function prettyLogTransaction(tx: Transaction) { let res = `${tx.inMessage?.info.src!} ➡️ ${tx.inMessage?.info.dest}\n` @@ -14,6 +18,15 @@ export function prettyLogTransaction(tx: Transaction) { return res } +/** + * Log transaction using `console.log`. Logs base on result of {@link prettyLogTransaction}. + * Example output: + * ``` + * null ➡️ EQBGhqLAZseEqRXz4ByFPTGV7SVMlI4hrbs-Sps_Xzx01x8G + * ➡️ 0.05 💎 EQC2VluVfpj2FoHNMAiDMpcMzwvjLZxxTG8ecq477RE3NvVt + * ``` + * @param txs Transactions to log + */ export function prettyLogTransactions(txs: Transaction[]) { let out = '' @@ -22,4 +35,4 @@ export function prettyLogTransactions(txs: Transaction[]) { } console.log(out) -} \ No newline at end of file +} diff --git a/src/utils/printTransactionFees.ts b/src/utils/printTransactionFees.ts index 17b8c12..f7ecd0b 100644 --- a/src/utils/printTransactionFees.ts +++ b/src/utils/printTransactionFees.ts @@ -34,6 +34,19 @@ function formatCoins(value: bigint | undefined, precision = 6): string { return formatCoinsPure(value, precision) + ' TON'; } +/** + * Prints transaction fees. + * Example output: + * ``` + * ┌─────────┬─────────────┬────────────────┬────────────────┬────────────────┬────────────────┬───────────────┬────────────┬────────────────┬──────────┬────────────┐ + * │ (index) │ op │ valueIn │ valueOut │ totalFees │ inForwardFee │ outForwardFee │ outActions │ computeFee │ exitCode │ actionCode │ + * ├─────────┼─────────────┼────────────────┼────────────────┼────────────────┼────────────────┼───────────────┼────────────┼────────────────┼──────────┼────────────┤ + * │ 0 │ 'N/A' │ 'N/A' │ '1000 TON' │ '0.004007 TON' │ 'N/A' │ '0.001 TON' │ 1 │ '0.001937 TON' │ 0 │ 0 │ + * │ 1 │ '0x45ab564' │ '1000 TON' │ '998.8485 TON' │ '1.051473 TON' │ '0.000667 TON' │ '0.255 TON' │ 255 │ '0.966474 TON' │ 0 │ 0 │ + * │ 2 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │ + * ``` + * @param transactions List of transaction to print fees + */ export function printTransactionFees(transactions: Transaction[]) { console.table( transactions