From 8984d1c5dd2a3d35760b0790cdba1a42529ea1f3 Mon Sep 17 00:00:00 2001 From: Vladislav <32094306+vzhovnitsky@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:27:54 +0300 Subject: [PATCH 01/14] add: adding fees calculation (#23) * wip: adding fees computation utils * wip: adding fees test * wip: adding test comparisons * wip: adding test comparisons --- src/utils/fees.spec.ts | 105 +++++++++++++++++++++++++++++++++ src/utils/fees.ts | 130 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 src/utils/fees.spec.ts create mode 100644 src/utils/fees.ts diff --git a/src/utils/fees.spec.ts b/src/utils/fees.spec.ts new file mode 100644 index 0000000..a4ada45 --- /dev/null +++ b/src/utils/fees.spec.ts @@ -0,0 +1,105 @@ +import { computeStorageFees, computeGasPrices, computeExternalMessageFees, computeMessageForwardFees } from './fees'; +import { Cell, storeMessage, storeMessageRelaxed, external, comment, internal, Address, SendMode, fromNano, toNano } from 'ton-core'; +import { WalletContractV4 } from '../wallets/WalletContractV4'; + +describe('estimateFees', () => { + it('should estimate fees correctly', () => { + const config = { + storage: [{ utime_since: 0, bit_price_ps: BigInt(1), cell_price_ps: BigInt(500), mc_bit_price_ps: BigInt(1000), mc_cell_price_ps: BigInt(500000) }], + workchain: { + gas: { flatLimit: BigInt(100), flatGasPrice: BigInt(100000), price: BigInt(65536000) }, + message: { lumpPrice: BigInt(1000000), bitPrice: BigInt(65536000), cellPrice: BigInt(6553600000), firstFrac: 21845 } + }, + }; + + const storageStats = [{ + lastPaid: 1696792239, duePayment: null, + used: { bits: 6888, cells: 14, publicCells: 0 } + }] + + const gasUsageByOutMsgs: { [key: number]: number } = { 1: 3308, 2: 3950, 3: 4592, 4: 5234 }; + + const contract = WalletContractV4.create({ workchain: 0, publicKey: Buffer.from('MUP3GpbKCQu64L4PIU0QprZxmSUygHcaYKuo2tZYA1c=', 'base64') }); + + const body = comment('Test message fees estimation'); + const testAddress = Address.parse('EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N'); + + // Create transfer + let intMessage = internal({ + to: testAddress, + value: 1400000000n, + bounce: true, + body, + }); + + let transfer = contract.createTransfer({ + seqno: 14, + secretKey: Buffer.alloc(64), + sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY, + messages: [intMessage] + }); + + const externalMessage = external({ + to: contract.address, + body: transfer, + init: null + }); + + let inMsg = new Cell().asBuilder(); + storeMessage(externalMessage)(inMsg); + + let outMsg = new Cell().asBuilder(); + storeMessageRelaxed(intMessage)(outMsg); + + // Storage fees + let storageFees = BigInt(0); + for (let storageStat of storageStats) { + if (storageStat) { + const computed = computeStorageFees({ + lastPaid: storageStat.lastPaid, + masterchain: false, + now: 1697445678, // Mon Oct 16 2023 11:42:56 GMT+0300 + special: false, + storagePrices: config.storage, + storageStat: { + bits: storageStat.used.bits, + cells: storageStat.used.cells, + publicCells: storageStat.used.publicCells + } + }); + storageFees = storageFees + computed; + } + } + + expect(fromNano(storageFees)).toBe('0.000138473'); + + // Calculate import fees + let importFees = computeExternalMessageFees(config.workchain.message as any, inMsg.endCell()); + + expect(fromNano(importFees)).toBe('0.001772'); + + // Any transaction use this amount of gas + const gasUsed = gasUsageByOutMsgs[1]; + let gasFees = computeGasPrices( + BigInt(gasUsed), + { flatLimit: config.workchain.gas.flatLimit, flatPrice: config.workchain.gas.flatGasPrice, price: config.workchain.gas.price } + ); + + expect(fromNano(gasFees)).toBe('0.003308'); + + // Total + let total = BigInt(0); + total += storageFees; + total += importFees; + total += gasFees; + + // Forward fees + let fwdFees = computeMessageForwardFees(config.workchain.message as any, outMsg.endCell()); + + expect(fromNano(fwdFees.fees)).toBe('0.000333328'); + + total += fwdFees.fees; + + expect(fromNano(total)).toBe('0.005551801'); + }); +}); \ No newline at end of file diff --git a/src/utils/fees.ts b/src/utils/fees.ts new file mode 100644 index 0000000..31f0f81 --- /dev/null +++ b/src/utils/fees.ts @@ -0,0 +1,130 @@ +import { Builder, Cell, loadMessageRelaxed, storeStateInit } from 'ton-core'; +import { MsgPrices, StoragePrices } from '../config/ConfigParser'; + +// +// Source: https://github.com/ton-foundation/ton/blob/ae5c0720143e231c32c3d2034cfe4e533a16d969/crypto/block/transaction.cpp#L425 +// + +export function computeStorageFees(data: { + now: number + lastPaid: number + storagePrices: StoragePrices[] + storageStat: { cells: number, bits: number, publicCells: number } + special: boolean + masterchain: boolean +}) { + const { + lastPaid, + now, + storagePrices, + storageStat, + special, + masterchain + } = data; + if (now <= lastPaid || storagePrices.length === 0 || now < storagePrices[0].utime_since || special) { + return BigInt(0); + } + let upto = Math.max(lastPaid, storagePrices[0].utime_since); + let total = BigInt(0); + for (let i = 0; i < storagePrices.length && upto < now; i++) { + let valid_until = (i < storagePrices.length - 1 ? Math.min(now, storagePrices[i + 1].utime_since) : now); + let payment = BigInt(0); + if (upto < valid_until) { + let delta = valid_until - upto; + payment += (BigInt(storageStat.cells) * (masterchain ? storagePrices[i].mc_cell_price_ps : storagePrices[i].cell_price_ps)); + payment += (BigInt(storageStat.bits) * (masterchain ? storagePrices[i].mc_bit_price_ps : storagePrices[i].bit_price_ps)); + payment = payment * BigInt(delta); + } + upto = valid_until; + total += payment; + } + + return shr16ceil(total); +} + +// +// Source: https://github.com/ton-foundation/ton/blob/ae5c0720143e231c32c3d2034cfe4e533a16d969/crypto/block/transaction.cpp#L1218 +// + +export function computeFwdFees(msgPrices: MsgPrices, cells: bigint, bits: bigint) { + return msgPrices.lumpPrice + (shr16ceil(msgPrices.bitPrice * bits + (msgPrices.cellPrice * cells))); +} + +// +// Source: https://github.com/ton-foundation/ton/blob/ae5c0720143e231c32c3d2034cfe4e533a16d969/crypto/block/transaction.cpp#L761 +// + +export function computeGasPrices(gasUsed: bigint, prices: { flatLimit: bigint, flatPrice: bigint, price: bigint }) { + if (gasUsed <= prices.flatLimit) { + return prices.flatPrice; + } else { + // td::rshift(gas_price256 * (gas_used - cfg.flat_gas_limit), 16, 1) + cfg.flat_gas_price + return prices.flatPrice + ((prices.price * (gasUsed - prices.flatLimit)) >> 16n); + } +} + +// +// Source: https://github.com/ton-foundation/ton/blob/ae5c0720143e231c32c3d2034cfe4e533a16d969/crypto/block/transaction.cpp#L530 +// + +export function computeExternalMessageFees(msgPrices: MsgPrices, cell: Cell) { + + // Collect stats + let storageStats = collectCellStats(cell); + storageStats.bits -= cell.bits.length; + storageStats.cells -= 1; + + return computeFwdFees(msgPrices, BigInt(storageStats.cells), BigInt(storageStats.bits)); +} + +export function computeMessageForwardFees(msgPrices: MsgPrices, cell: Cell) { + let msg = loadMessageRelaxed(cell.beginParse()); + let storageStats: { bits: number, cells: number } = { bits: 0, cells: 0 }; + + // Init + if (msg.init) { + const rawBuilder = new Cell().asBuilder(); + storeStateInit(msg.init)(rawBuilder); + const raw = rawBuilder.endCell(); + + let c = collectCellStats(raw); + c.bits -= raw.bits.length; + c.cells -= 1; + storageStats.bits += c.bits; + storageStats.cells += c.cells; + } + + // Body + let bc = collectCellStats(msg.body); + bc.bits -= msg.body.bits.length; + bc.cells -= 1; + storageStats.bits += bc.bits; + storageStats.cells += bc.cells; + + // NOTE: Extra currencies are ignored for now + + let fees = computeFwdFees(msgPrices, BigInt(storageStats.cells), BigInt(storageStats.bits)); + let res = (fees * BigInt(msgPrices.firstFrac)) >> 16n; + let remaining = fees - res; + return { fees: res, remaining }; +} + +function collectCellStats(cell: Cell): { bits: number, cells: number } { + let bits = cell.bits.length; + let cells = 1; + for (let ref of cell.refs) { + let r = collectCellStats(ref); + cells += r.cells; + bits += r.bits; + } + return { bits, cells }; +} + +function shr16ceil(src: bigint) { + let rem = src % 65536n; + let res = src >> 16n; + if (rem !== 0n) { + res += 1n; + } + return res; +} \ No newline at end of file From d117d5fa5509a674e36ed5a2dabcdc5cabfb04ba Mon Sep 17 00:00:00 2001 From: Sergey Andreev Date: Mon, 23 Oct 2023 15:05:32 +0200 Subject: [PATCH 02/14] feat: wallet v5 added (#19) * feat: wallet v5 added * fix: Wallet v5 code replaced with code that uses library. Tests fixes * fix: external actions utils refactored --- src/wallets/WalletContractV5.spec.ts | 179 ++++++++++++++ src/wallets/WalletContractV5.ts | 258 +++++++++++++++++++ src/wallets/WalletV5Utils.spec.ts | 261 ++++++++++++++++++++ src/wallets/WalletV5Utils.ts | 179 ++++++++++++++ src/wallets/signing/createWalletTransfer.ts | 48 +++- 5 files changed, 922 insertions(+), 3 deletions(-) create mode 100644 src/wallets/WalletContractV5.spec.ts create mode 100644 src/wallets/WalletContractV5.ts create mode 100644 src/wallets/WalletV5Utils.spec.ts create mode 100644 src/wallets/WalletV5Utils.ts diff --git a/src/wallets/WalletContractV5.spec.ts b/src/wallets/WalletContractV5.spec.ts new file mode 100644 index 0000000..92df56b --- /dev/null +++ b/src/wallets/WalletContractV5.spec.ts @@ -0,0 +1,179 @@ +/** + * Copyright (c) Whales Corp. + * All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { randomTestKey } from "../utils/randomTestKey"; +import { Address, internal, OpenedContract } from "ton-core"; +import { WalletContractV5 } from "./WalletContractV5"; +import { KeyPair } from "ton-crypto"; +import { createTestClient } from "../utils/createTestClient"; +import { TonClient } from "../client/TonClient"; + +const getExtensionsArray = async (wallet: OpenedContract) => { + try { + return await wallet.getExtensionsArray(); + } catch (e) { + // Handle toncenter bug. Toncenter incorrectly returns 'list' in the stack in case of empty extensions dict + if (e && typeof e === 'object' && 'message' in e && e.message === 'Unsupported stack item type: list') { + return []; + } + throw e; + } +} + +describe('WalletContractV5', () => { + let client: TonClient; + let walletKey: KeyPair; + let wallet: OpenedContract; + + beforeEach(() => { + client = createTestClient(); + walletKey = randomTestKey('v5-treasure'); + wallet = client.open(WalletContractV5.create({ walletId: { networkGlobalId: -3 }, publicKey: walletKey.publicKey })); + + }) + + it('should has balance and correct address', async () => { + const balance = await wallet.getBalance(); + + expect(wallet.address.equals(Address.parse('EQDv2B0jPmJZ1j-ne3Ko64eGqfYZRHGQbfSE5pUWVvUdQmDH'))).toBeTruthy(); + expect(balance > 0n).toBe(true); + }); + + it('should perform single transfer', async () => { + const seqno = await wallet.getSeqno(); + const transfer = wallet.createTransfer({ + seqno, + secretKey: walletKey.secretKey, + messages: [internal({ + to: 'EQDQ0PRYSWmW-v6LVHNYq5Uelpr5f7Ct7awG7Lao2HImrCzn', + value: '0.01', + body: 'Hello world single transfer!' + })] + }); + + await wallet.send(transfer); + }); + + it('should perform double transfer', async () => { + const seqno = await wallet.getSeqno(); + const transfer = wallet.createTransfer({ + seqno, + secretKey: walletKey.secretKey, + messages: [internal({ + to: 'EQDQ0PRYSWmW-v6LVHNYq5Uelpr5f7Ct7awG7Lao2HImrCzn', + value: '0.01', + body: 'Hello world to extension' + }), internal({ + to: 'EQAtHiE_vEyAogU1rHcz3uzp64h-yqeFJ2S2ChkKNwygLMk3', + value: '0.02', + body: 'Hello world to relayer' + })] + }); + + await wallet.send(transfer); + }); + + it('should add extension', async () => { + const extensionKey = randomTestKey('v5-treasure-extension'); + const extensionContract = client.open(WalletContractV5.create({ walletId: { workChain: 0, networkGlobalId: -3 }, publicKey: extensionKey.publicKey })); + + + const seqno = await wallet.getSeqno(); + const extensions = await getExtensionsArray(wallet); + + const extensionAlreadyAdded = extensions.some(address => address.equals(extensionContract.address)); + + if (!extensionAlreadyAdded) { + await wallet.sendAddExtension({ + seqno, + secretKey: walletKey.secretKey, + extensionAddress: extensionContract.address + }); + + const waitUntilExtensionAdded = async (attempt = 0): Promise => { + if (attempt >= 10) { + throw new Error('Extension was not added in 10 blocks'); + } + const extensions = await getExtensionsArray(wallet); + const extensionAdded = extensions.some(address => address.equals(extensionContract.address)); + if (extensionAdded) { + return; + } + + await new Promise(r => setTimeout(r, 1500)); + return waitUntilExtensionAdded(attempt + 1); + } + + await waitUntilExtensionAdded(); + } + + const extensionsSeqno = await extensionContract.getSeqno(); + await extensionContract.sendTransfer({ + seqno: extensionsSeqno, + secretKey: extensionKey.secretKey, + messages: [internal({ + to: wallet.address, + value: '0.1', + body: wallet.createTransfer({ + seqno: seqno + 1, + messages: [internal({ + to: 'kQD6oPnzaaAMRW24R8F0_nlSsJQni0cGHntR027eT9_sgtwt', + value: '0.03', + body: 'Hello world from plugin' + })] + }) + })] + }); + }, 60000); + + it('should remove extension', async () => { + const extensionKey = randomTestKey('v5-treasure-extension'); + const extensionContract = client.open(WalletContractV5.create({ walletId: { workChain: 0, networkGlobalId: -3 }, publicKey: extensionKey.publicKey })); + + + const seqno = await wallet.getSeqno(); + const extensions = await getExtensionsArray(wallet); + + const extensionAlreadyAdded = extensions.some(address => address.equals(extensionContract.address)); + + if (extensionAlreadyAdded) { + await wallet.sendRemoveExtension({ + seqno, + secretKey: walletKey.secretKey, + extensionAddress: extensionContract.address + }); + } + }); + + it('should send internal transfer via relayer', async () => { + const relaerKey = randomTestKey('v5-treasure-relayer'); + const relayerContract = client.open(WalletContractV5.create({ walletId: { workChain: 0, networkGlobalId: -3 }, publicKey: relaerKey.publicKey })); + + + const seqno = await wallet.getSeqno(); + + const relayerSeqno = await relayerContract.getSeqno(); + await relayerContract.sendTransfer({ + seqno: relayerSeqno, + secretKey: relaerKey.secretKey, + messages: [internal({ + to: wallet.address, + value: '0.1', + body: wallet.createTransfer({ + seqno: seqno, + secretKey: walletKey.secretKey, + messages: [internal({ + to: 'kQD6oPnzaaAMRW24R8F0_nlSsJQni0cGHntR027eT9_sgtwt', + value: '0.04', + body: 'Hello world from relayer' + })] + }) + })] + }); + }); +}); diff --git a/src/wallets/WalletContractV5.ts b/src/wallets/WalletContractV5.ts new file mode 100644 index 0000000..3771f25 --- /dev/null +++ b/src/wallets/WalletContractV5.ts @@ -0,0 +1,258 @@ +/** + * Copyright (c) Whales Corp. + * All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + Address, + beginCell, + Cell, + Contract, + contractAddress, + ContractProvider, Dictionary, + internal, + MessageRelaxed, + OutAction, + Sender, + SendMode +} from "ton-core"; +import { Maybe } from "../utils/maybe"; +import { createWalletTransferV5 } from "./signing/createWalletTransfer"; +import {OutActionExtended, storeWalletId, WalletId} from "./WalletV5Utils"; + + + +export type Wallet5BasicSendArgs = { + seqno: number; + sendMode?: Maybe; + timeout?: Maybe; +} + +export type SingedAuthWallet5SendArgs = Wallet5BasicSendArgs & { + secretKey: Buffer; +} + +export type ExtensionAuthWallet5SendArgs = Wallet5BasicSendArgs & { } + +export type Wallet5SendArgs = + | SingedAuthWallet5SendArgs + | ExtensionAuthWallet5SendArgs + + +export class WalletContractV5 implements Contract { + + static opCodes = { + auth_extension: 0x6578746e, + auth_signed: 0x7369676e + } + + static create(args: { + walletId?: Partial, + publicKey: Buffer + }) { + const walletId = { + networkGlobalId: args.walletId?.networkGlobalId ?? -239, + workChain: args?.walletId?.workChain ?? 0, + subwalletNumber: args?.walletId?.subwalletNumber ?? 0, + walletVersion: args?.walletId?.walletVersion ?? 'v5' + } + return new WalletContractV5(walletId, args.publicKey); + } + + readonly address: Address; + readonly init: { data: Cell, code: Cell }; + + private constructor( + readonly walletId: WalletId, + readonly publicKey: Buffer + ) { + this.walletId = walletId; + + // Build initial code and data + let code = Cell.fromBoc(Buffer.from('te6cckEBAQEAIwAIQgLND3fEdsoVqej99mmdJbaOAOcmH9K3vkNG64R7FPAsl9kimVw=', 'base64'))[0]; + let data = beginCell() + .storeUint(0, 32) // Seqno + .store(storeWalletId(this.walletId)) + .storeBuffer(this.publicKey) + .storeBit(0) // Empty plugins dict + .endCell(); + this.init = { code, data }; + this.address = contractAddress(this.walletId.workChain, { code, data }); + } + + /** + * Get Wallet Balance + */ + async getBalance(provider: ContractProvider) { + let state = await provider.getState(); + return state.balance; + } + + /** + * Get Wallet Seqno + */ + async getSeqno(provider: ContractProvider) { + let state = await provider.getState(); + if (state.state.type === 'active') { + let res = await provider.get('seqno', []); + return res.stack.readNumber(); + } else { + return 0; + } + } + + /** + * Get Wallet Extensions + */ + async getExtensions(provider: ContractProvider) { + let state = await provider.getState(); + if (state.state.type === 'active') { + const result = await provider.get('get_extensions', []); + return result.stack.readCellOpt(); + } else { + return null; + } + } + + /** + * Get Wallet Extensions + */ + async getExtensionsArray(provider: ContractProvider) { + const extensions = await this.getExtensions(provider); + if (!extensions) { + return []; + } + + const dict: Dictionary = Dictionary.loadDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigInt(8), + extensions + ); + + return dict.keys().map(key => { + const wc = dict.get(key)!; + const addressHex = key ^ (wc + 1n); + return Address.parseRaw(`${wc}:${addressHex.toString(16)}`); + }) + } + + /** + * Send signed transfer + */ + async send(provider: ContractProvider, message: Cell) { + await provider.external(message); + } + + /** + * Sign and send transfer + */ + async sendTransfer(provider: ContractProvider, args: Wallet5SendArgs & { messages: MessageRelaxed[] }) { + const transfer = this.createTransfer(args); + await this.send(provider, transfer); + } + + /** + * Sign and send add extension request + */ + async sendAddExtension(provider: ContractProvider, args: Wallet5SendArgs & { extensionAddress: Address }) { + const request = this.createAddExtension(args); + await this.send(provider, request); + } + + /** + * Sign and send remove extension request + */ + async sendRemoveExtension(provider: ContractProvider, args: Wallet5SendArgs & { extensionAddress: Address, }) { + const request = this.createRemoveExtension(args); + await this.send(provider, request); + } + + /** + * Sign and send request + */ + async sendRequest(provider: ContractProvider, args: Wallet5SendArgs & { actions: (OutAction | OutActionExtended)[], }) { + const request = this.createRequest(args); + await this.send(provider, request); + } + + /** + * Create signed transfer + */ + createTransfer(args: Wallet5SendArgs & { messages: MessageRelaxed[] }) { + const { messages, ...rest } = args; + + const sendMode = args.sendMode ?? SendMode.PAY_GAS_SEPARATELY; + const actions: OutAction[] = messages.map(message => ({ type: 'sendMsg', mode: sendMode, outMsg: message})); + + return this.createRequest({ + ...rest, + actions + }) + } + + /** + * Create signed add extension request + */ + createAddExtension(args: Wallet5SendArgs & { extensionAddress: Address, }) { + const { extensionAddress, ...rest } = args; + return this.createRequest({ + actions: [{ + type: 'addExtension', + address: extensionAddress + }], + ...rest + }) + } + + /** + * Create signed remove extension request + */ + createRemoveExtension(args: Wallet5SendArgs & { extensionAddress: Address, }) { + const { extensionAddress, ...rest } = args; + return this.createRequest({ + actions: [{ + type: 'removeExtension', + address: extensionAddress + }], + ...rest + }) + } + + /** + * Create signed request + */ + createRequest(args: Wallet5SendArgs & { actions: (OutAction | OutActionExtended)[], }) { + return createWalletTransferV5({ + ...args, + sendMode: args.sendMode ?? SendMode.PAY_GAS_SEPARATELY, + walletId: storeWalletId(this.walletId) + }) + } + + /** + * Create sender + */ + sender(provider: ContractProvider, secretKey: Buffer): Sender { + return { + send: async (args) => { + let seqno = await this.getSeqno(provider); + let transfer = this.createTransfer({ + seqno, + secretKey, + sendMode: args.sendMode, + messages: [internal({ + to: args.to, + value: args.value, + init: args.init, + body: args.body, + bounce: args.bounce + })] + }); + await this.send(provider, transfer); + } + }; + } +} diff --git a/src/wallets/WalletV5Utils.spec.ts b/src/wallets/WalletV5Utils.spec.ts new file mode 100644 index 0000000..2e5be6b --- /dev/null +++ b/src/wallets/WalletV5Utils.spec.ts @@ -0,0 +1,261 @@ +import { + beginCell, + SendMode, + storeMessageRelaxed, + storeOutAction, + Address, + OutAction, + storeOutList, + MessageRelaxed +} from "ton-core"; +import { + loadOutListExtended, + loadWalletId, + OutActionExtended, + storeOutActionExtended, + storeOutListExtended, + storeWalletId, + WalletId +} from "./WalletV5Utils"; + +const mockMessageRelaxed1: MessageRelaxed = { + info: { + type: 'external-out', + createdLt: 0n, + createdAt: 0, + dest: null, + src: null + }, + body: beginCell().storeUint(0,8).endCell(), + init: null +} +const mockData = beginCell().storeUint(123, 32).endCell(); +const mockAddress = Address.parseRaw('0:' + '1'.repeat(64)) + +describe('Wallet V5 utils', () => { + const outActionSetDataTag = 0x1ff8ea0b; + const outActionAddExtensionTag = 0x1c40db9f; + const outActionRemoveExtensionTag = 0x5eaef4a4; + const outActionSendMsgTag = 0x0ec3c86d; + + it('Should serialise set data action', () => { + const action = storeOutActionExtended({ + type: 'setData', + newData: mockData + }) ; + + const actual = beginCell().store(action).endCell(); + + const expected = beginCell() + .storeUint(outActionSetDataTag, 32) + .storeRef(mockData) + .endCell(); + + expect(expected.equals(actual)).toBeTruthy(); + }); + + it('Should serialise add extension action', () => { + const action = storeOutActionExtended({ + type: 'addExtension', + address: mockAddress + }) ; + + const actual = beginCell().store(action).endCell(); + + const expected = beginCell() + .storeUint(outActionAddExtensionTag, 32) + .storeAddress(mockAddress) + .endCell(); + + expect(expected.equals(actual)).toBeTruthy(); + }); + + it('Should serialise remove extension action', () => { + const action = storeOutActionExtended({ + type: 'removeExtension', + address: mockAddress + }) ; + + const actual = beginCell().store(action).endCell(); + + const expected = beginCell() + .storeUint(outActionRemoveExtensionTag, 32) + .storeAddress(mockAddress) + .endCell(); + + expect(expected.equals(actual)).toBeTruthy(); + }); + + it('Should serialise wallet id', () => { + const walletId: WalletId = { + walletVersion: 'v5', + networkGlobalId: -239, + workChain: 0, + subwalletNumber: 0 + } + + const actual = beginCell().store(storeWalletId(walletId)).endCell(); + + const expected = beginCell() + .storeInt(walletId.networkGlobalId, 32) + .storeInt(walletId.workChain, 8) + .storeUint(0, 8) + .storeUint(walletId.subwalletNumber, 32) + .endCell(); + + expect(expected.equals(actual)).toBeTruthy(); + }); + + it('Should deserialise wallet id', () => { + const expected: WalletId = { + walletVersion: 'v5', + networkGlobalId: -239, + workChain: 0, + subwalletNumber: 0 + } + + const actual = loadWalletId(beginCell() + .storeInt(expected.networkGlobalId, 32) + .storeInt(expected.workChain, 8) + .storeUint(0, 8) + .storeUint(expected.subwalletNumber, 32) + .endCell().beginParse()); + + + expect(expected).toEqual(actual); + }); + + it('Should serialise wallet id', () => { + const walletId: WalletId = { + walletVersion: 'v5', + networkGlobalId: -3, + workChain: -1, + subwalletNumber: 1234 + } + + const actual = beginCell().store(storeWalletId(walletId)).endCell(); + + const expected = beginCell() + .storeInt(walletId.networkGlobalId, 32) + .storeInt(walletId.workChain, 8) + .storeUint(0, 8) + .storeUint(walletId.subwalletNumber, 32) + .endCell(); + + expect(expected.equals(actual)).toBeTruthy(); + }); + + it('Should deserialise wallet id', () => { + const expected: WalletId = { + walletVersion: 'v5', + networkGlobalId: -239, + workChain: -1, + subwalletNumber: 1 + } + + const actual = loadWalletId(beginCell() + .storeInt(expected.networkGlobalId, 32) + .storeInt(expected.workChain, 8) + .storeUint(0, 8) + .storeUint(expected.subwalletNumber, 32) + .endCell().beginParse()); + + + expect(expected).toEqual(actual); + }); + + it('Should serialize extended out list', () => { + const sendMode1 = SendMode.PAY_GAS_SEPARATELY; + + const actions: (OutActionExtended | OutAction)[] = [ + { + type: 'addExtension', + address: mockAddress + }, + { + type: 'sendMsg', + mode: sendMode1, + outMsg: mockMessageRelaxed1 + } + ] + + const actual = beginCell().store(storeOutListExtended(actions)).endCell(); + + const expected = + beginCell() + .storeUint(1, 1) + .store(storeOutActionExtended(actions[0] as OutActionExtended)) + .storeRef( + beginCell() + .storeUint(0, 1) + .storeRef( + beginCell() + .storeRef(beginCell().endCell()) + .storeUint(outActionSendMsgTag, 32) + .storeUint(sendMode1, 8) + .storeRef(beginCell().store(storeMessageRelaxed(mockMessageRelaxed1)).endCell()) + .endCell() + ) + .endCell() + ) + .endCell() + + + + expect(actual.equals(expected)).toBeTruthy(); + }); + + it('Should deserialize extended out list', () => { + const sendMode1 = SendMode.PAY_GAS_SEPARATELY; + + const expected: (OutActionExtended | OutAction)[] = [ + { + type: 'addExtension', + address: mockAddress + }, + { + type: 'sendMsg', + mode: sendMode1, + outMsg: mockMessageRelaxed1 + } + ] + + const serialized = + beginCell() + .storeUint(1, 1) + .store(storeOutActionExtended(expected[0] as OutActionExtended)) + .storeRef( + beginCell() + .storeUint(0, 1) + .storeRef( + beginCell() + .storeRef(beginCell().endCell()) + .storeUint(outActionSendMsgTag, 32) + .storeUint(sendMode1, 8) + .storeRef(beginCell().store(storeMessageRelaxed(mockMessageRelaxed1)).endCell()) + .endCell() + ) + .endCell() + ) + .endCell() + + const actual = loadOutListExtended(serialized.beginParse()) + + expect(expected.length).toEqual(actual.length); + expected.forEach((item1, index) => { + const item2 = actual[index]; + expect(item1.type).toEqual(item2.type); + + if (item1.type === 'sendMsg' && item2.type === 'sendMsg') { + expect(item1.mode).toEqual(item2.mode); + expect(item1.outMsg.body.equals(item2.outMsg.body)).toBeTruthy(); + expect(item1.outMsg.info).toEqual(item2.outMsg.info); + expect(item1.outMsg.init).toEqual(item2.outMsg.init); + } + + if (item1.type === 'addExtension' && item2.type === 'addExtension') { + expect(item1.address.equals(item2.address)).toBeTruthy(); + } + }) + }); +}) diff --git a/src/wallets/WalletV5Utils.ts b/src/wallets/WalletV5Utils.ts new file mode 100644 index 0000000..e1a5d66 --- /dev/null +++ b/src/wallets/WalletV5Utils.ts @@ -0,0 +1,179 @@ +import { + Address, + beginCell, + BitReader, BitString, + Builder, + Cell, + loadOutList, + OutAction, + Slice, + storeOutList +} from 'ton-core'; + +export interface OutActionSetData { + type: 'setData'; + newData: Cell; +} + +export interface OutActionAddExtension { + type: 'addExtension'; + address: Address; +} + +export interface OutActionRemoveExtension { + type: 'removeExtension'; + address: Address; +} + +export type OutActionExtended = OutActionSetData | OutActionAddExtension | OutActionRemoveExtension; + +const outActionSetDataTag = 0x1ff8ea0b; +function storeOutActionSetData(action: OutActionSetData) { + return (builder: Builder) => { + builder.storeUint(outActionSetDataTag, 32).storeRef(action.newData) + } +} + +const outActionAddExtensionTag = 0x1c40db9f; +function storeOutActionAddExtension(action: OutActionAddExtension) { + return (builder: Builder) => { + builder.storeUint(outActionAddExtensionTag, 32).storeAddress(action.address) + } +} + +const outActionRemoveExtensionTag = 0x5eaef4a4; +function storeOutActionRemoveExtension(action: OutActionRemoveExtension) { + return (builder: Builder) => { + builder.storeUint(outActionRemoveExtensionTag, 32).storeAddress(action.address) + } +} + +export function storeOutActionExtended(action: OutActionExtended) { + if (action.type === 'setData') { + return storeOutActionSetData(action); + } + + if (action.type === 'addExtension') { + return storeOutActionAddExtension(action); + } + + return storeOutActionRemoveExtension(action); +} + +export function loadOutActionExtended(slice: Slice): OutActionExtended { + const tag = slice.loadUint(32); + + switch (tag) { + case outActionSetDataTag: + return { + type: 'setData', + newData: slice.loadRef() + } + case outActionAddExtensionTag: + return { + type: 'addExtension', + address: slice.loadAddress() + } + case outActionRemoveExtensionTag: + return { + type: 'removeExtension', + address: slice.loadAddress() + } + default: + throw new Error(`Unknown extended out action tag 0x${tag.toString(16)}`); + } +} + +export function isOutActionExtended(action: OutAction | OutActionExtended): action is OutActionExtended { + return ( + action.type === 'setData' || action.type === 'addExtension' || action.type === 'removeExtension' + ); +} + +export function storeOutListExtended(actions: (OutActionExtended | OutAction)[]) { + const [action, ...rest] = actions; + + if (!action || !isOutActionExtended(action)) { + if (actions.some(isOutActionExtended)) { + throw new Error("Can't serialize actions list: all extended actions must be placed before out actions"); + } + + return (builder: Builder) => { + builder + .storeUint(0, 1) + .storeRef(beginCell().store(storeOutList(actions as OutAction[])).endCell()) + } + } + + return (builder: Builder) => { + builder.storeUint(1, 1) + .store(storeOutActionExtended(action)) + .storeRef(beginCell().store(storeOutListExtended(rest)).endCell()) + } +} + +export function loadOutListExtended(slice: Slice): (OutActionExtended | OutAction)[] { + const actions: (OutActionExtended | OutAction)[] = []; + + while (slice.loadUint(1)) { + const action = loadOutActionExtended(slice); + actions.push(action); + + slice = slice.loadRef().beginParse(); + } + + return actions.concat(loadOutList(slice.loadRef().beginParse())); +} + +export interface WalletId { + readonly walletVersion: 'v5'; + + /** + * -239 is mainnet, -3 is testnet + */ + readonly networkGlobalId: number; + + readonly workChain: number; + + readonly subwalletNumber: number; +} + +const walletVersionsSerialisation: Record = { + v5: 0 +}; +export function loadWalletId(value: bigint | Buffer | Slice): WalletId { + const bitReader = new BitReader( + new BitString( + typeof value === 'bigint' ? + Buffer.from(value.toString(16), 'hex') : + value instanceof Slice ? value.loadBuffer(10) : value, + 0, + 80 + ) + ); + const networkGlobalId = bitReader.loadInt(32); + const workChain = bitReader.loadInt(8); + const walletVersionRaw = bitReader.loadUint(8); + const subwalletNumber = bitReader.loadUint(32); + + const walletVersion = Object.entries(walletVersionsSerialisation).find( + ([_, value]) => value === walletVersionRaw + )?.[0] as WalletId['walletVersion'] | undefined; + + if (walletVersion === undefined) { + throw new Error( + `Can't deserialize walletId: unknown wallet version ${walletVersionRaw}` + ); + } + + return { networkGlobalId, workChain, walletVersion, subwalletNumber } +} + +export function storeWalletId(walletId: WalletId) { + return (builder: Builder) => { + builder.storeInt(walletId.networkGlobalId, 32); + builder.storeInt(walletId.workChain, 8); + builder.storeUint(walletVersionsSerialisation[walletId.walletVersion], 8); + builder.storeUint(walletId.subwalletNumber, 32); + } +} diff --git a/src/wallets/signing/createWalletTransfer.ts b/src/wallets/signing/createWalletTransfer.ts index 2d89122..4c6f3ee 100644 --- a/src/wallets/signing/createWalletTransfer.ts +++ b/src/wallets/signing/createWalletTransfer.ts @@ -1,14 +1,22 @@ /** - * Copyright (c) Whales Corp. + * Copyright (c) Whales Corp. * All Rights Reserved. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -import { beginCell, MessageRelaxed, storeMessageRelaxed } from "ton-core"; +import { beginCell, Builder, MessageRelaxed, OutAction, storeMessageRelaxed } from "ton-core"; import { sign } from "ton-crypto"; import { Maybe } from "../../utils/maybe"; +import { + Wallet5SendArgs, + WalletContractV5 +} from "../WalletContractV5"; +import { + OutActionExtended, + storeOutListExtended +} from "../WalletV5Utils"; export function createWalletTransferV1(args: { seqno: number, sendMode: number, message: Maybe, secretKey: Buffer }) { @@ -148,4 +156,38 @@ export function createWalletTransferV4(args: { .endCell(); return body; -} \ No newline at end of file +} + +export function createWalletTransferV5(args: Wallet5SendArgs & { actions: (OutAction | OutActionExtended)[], walletId: (builder: Builder) => void }) { + // Check number of actions + if (args.actions.length > 255) { + throw Error("Maximum number of OutActions in a single request is 255"); + } + + if (!('secretKey' in args) || !args.secretKey) { + return beginCell() + .storeUint(WalletContractV5.opCodes.auth_extension, 32) + .store(storeOutListExtended(args.actions)) + .endCell(); + } + + const message = beginCell().store(args.walletId); + if (args.seqno === 0) { + for (let i = 0; i < 32; i++) { + message.storeBit(1); + } + } else { + message.storeUint(args.timeout || Math.floor(Date.now() / 1e3) + 60, 32); // Default timeout: 60 seconds + } + + message.storeUint(args.seqno, 32).store(storeOutListExtended(args.actions)); + + // Sign message + const signature = sign(message.endCell().hash(), args.secretKey); + + return beginCell() + .storeUint(WalletContractV5.opCodes.auth_signed, 32) + .storeBuffer(signature) + .storeBuilder(message) + .endCell(); +} From ff51bf97be92bf2436e2b70da8e5df21c6089041 Mon Sep 17 00:00:00 2001 From: Noodles Date: Mon, 23 Oct 2023 21:08:01 +0800 Subject: [PATCH 03/14] parseStack add `tuple` type support (#12) --- src/client/TonClient.ts | 54 +++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/client/TonClient.ts b/src/client/TonClient.ts index 6e07f4a..40e99d0 100644 --- a/src/client/TonClient.ts +++ b/src/client/TonClient.ts @@ -296,28 +296,46 @@ export class TonClient { } function parseStack(src: any[]) { - let stack: TupleItem[] = []; - for (let s of src) { - if (s[0] === 'num') { - let val = s[1] as string; - if (val.startsWith('-')) { - stack.push({ type: 'int', value: -BigInt(val.slice(1)) }); + const _parseStack = (src: []) => { + let stack: TupleItem[] = []; + for (let s of src) { + if (s[0] === 'num') { + let val = s[1] as string; + if (val.startsWith('-')) { + stack.push({ type: 'int', value: -BigInt(val.slice(1)) }); + } else { + stack.push({ type: 'int', value: BigInt(val) }); + } + } else if (s[0] === 'null') { + stack.push({ type: 'null' }); + } else if (s[0] === 'cell') { + stack.push({ type: 'cell', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); + } else if (s[0] === 'slice') { + stack.push({ type: 'slice', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); + } else if (s[0] === 'builder') { + stack.push({ type: 'builder', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); + } else if (s[0] === 'tuple') { + stack.push({ + type: 'tuple', + items: _parseStack(s[1].elements.map((e) => { + switch (e["@type"]) { + case "tvm.stackEntryNumber": + return ["num", e.number.number]; + case "tvm.stackEntryCell": + return ["cell", e.cell]; + default: + throw Error("Unsupported item type: " + e["@type"]); + } + })) + }) } else { - stack.push({ type: 'int', value: BigInt(val) }); + throw Error('Unsupported stack item type: ' + s[0]) } - } else if (s[0] === 'null') { - stack.push({ type: 'null' }); - } else if (s[0] === 'cell') { - stack.push({ type: 'cell', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); - } else if (s[0] === 'slice') { - stack.push({ type: 'slice', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); - } else if (s[0] === 'builder') { - stack.push({ type: 'builder', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); - } else { - throw Error('Unsupported stack item type: ' + s[0]) } + return stack; } - return new TupleReader(stack); + + return new TupleReader(_parseStack(stack)); } function createProvider(client: TonClient, address: Address, init: { code: Cell | null, data: Cell | null } | null): ContractProvider { From 0b3b99f52b5f6aa5eec499a7a18304e7ee3affac Mon Sep 17 00:00:00 2001 From: Vladislav <32094306+vzhovnitsky@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:45:36 +0300 Subject: [PATCH 04/14] feat: adding new TonClient4.getAccountTransactionsParsed support for api-v4 (#22) * feat: adding new getAccountTransactionsParsed support for api-v4 * chore: count to params --------- Co-authored-by: Dan Volkov --- src/client/TonClient4.spec.ts | 15 ++- src/client/TonClient4.ts | 169 ++++++++++++++++++++++++++++++++-- 2 files changed, 173 insertions(+), 11 deletions(-) diff --git a/src/client/TonClient4.spec.ts b/src/client/TonClient4.spec.ts index b2b4585..b1f238c 100644 --- a/src/client/TonClient4.spec.ts +++ b/src/client/TonClient4.spec.ts @@ -1,14 +1,14 @@ -import { Address, beginCell } from 'ton-core'; -import { TonClient } from './TonClient'; +import { Address } from 'ton-core'; import { TonClient4 } from './TonClient4'; +import { backoff } from '../utils/time'; let describeConditional = process.env.TEST_CLIENTS ? describe : describe.skip; -describeConditional('TonClient', () => { + describeConditional('TonClient', () => { let client = new TonClient4({ endpoint: 'https://mainnet-v4.tonhubapi.com', }); - const testAddress = Address.parse('EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N'); + const testAddress = Address.parse('EQBicYUqh1j9Lnqv9ZhECm0XNPaB7_HcwoBb3AJnYYfqB38_'); let seqno!: number; beforeAll(async () => { @@ -27,6 +27,13 @@ describeConditional('TonClient', () => { console.log(account, accountLite); }); + it('should get account parsed transactions', async () => { + let accountLite = await backoff(async () => await client.getAccountLite(seqno, testAddress), true); + let parsedTransactions = await backoff(async () => await client.getAccountTransactionsParsed(testAddress, BigInt(accountLite.account.last!.lt), Buffer.from(accountLite.account.last!.hash, 'base64'), 10), true); + + console.log(parsedTransactions.transactions.length); + }, 60_000); + it('should get config', async () => { let config = await client.getConfig(seqno); console.log(config); diff --git a/src/client/TonClient4.ts b/src/client/TonClient4.ts index 04b36a5..ad9c62d 100644 --- a/src/client/TonClient4.ts +++ b/src/client/TonClient4.ts @@ -180,6 +180,34 @@ export class TonClient4 { return tx; } + /** + * Load parsed account transactions + * @param address address + * @param lt last transaction lt + * @param hash last transaction hash + * @param count number of transactions to load + * @returns parsed transactions + */ + async getAccountTransactionsParsed(address: Address, lt: bigint, hash: Buffer, count: number = 20) { + let res = await axios.get( + this.#endpoint + '/account/' + address.toString({ urlSafe: true }) + '/tx/parsed/' + lt.toString(10) + '/' + toUrlSafe(hash.toString('base64')), + { + adapter: this.#adapter, + timeout: this.#timeout, + params: { + count + } + } + ); + let parsedTransactionsRes = parsedTransactionsCodec.safeParse(res.data); + + if (!parsedTransactionsRes.success) { + throw Error('Mailformed response'); + } + + return parsedTransactionsRes.data as ParsedTransactions; + } + /** * Get network config * @param seqno block sequence number @@ -560,13 +588,140 @@ const sendCodec = z.object({ status: z.number() }); +const blocksCodec = z.array(z.object({ + workchain: z.number(), + seqno: z.number(), + shard: z.string(), + rootHash: z.string(), + fileHash: z.string() +})); + const transactionsCodec = z.object({ - blocks: z.array(z.object({ - workchain: z.number(), - seqno: z.number(), - shard: z.string(), - rootHash: z.string(), - fileHash: z.string() - })), + blocks: blocksCodec, boc: z.string() }); + +const parsedAddressExternalCodec = z.object({ + bits: z.number(), + data: z.string() +}); + +const parsedMessageInfoCodec = z.union([ + z.object({ + type: z.literal('internal'), + value: z.string(), + dest: z.string(), + src: z.string(), + bounced: z.boolean(), + bounce: z.boolean(), + ihrDisabled: z.boolean(), + createdAt: z.number(), + createdLt: z.string(), + fwdFee: z.string(), + ihrFee: z.string() + }), + z.object({ + type: z.literal('external-in'), + dest: z.string(), + src: z.union([parsedAddressExternalCodec, z.null()]), + importFee: z.string() + }), + z.object({ + type: z.literal('external-out'), + dest: z.union([parsedAddressExternalCodec, z.null()]) + }) +]); + +const parsedStateInitCodec = z.object({ + splitDepth: z.union([z.number(), z.null()]), + code: z.union([z.string(), z.null()]), + data: z.union([z.string(), z.null()]), + special: z.union([z.object({ tick: z.boolean(), tock: z.boolean() }), z.null()]) +}); + +const parsedMessageCodec = z.object({ + body: z.string(), + info: parsedMessageInfoCodec, + init: z.union([parsedStateInitCodec, z.null()]) +}); + +const accountStatusCodec = z.union([z.literal('uninitialized'), z.literal('frozen'), z.literal('active'), z.literal('non-existing')]); + +const txBodyCodec = z.union([ + z.object({ type: z.literal('comment'), comment: z.string() }), + z.object({ type: z.literal('payload'), cell: z.string() }), +]); + +const parsedOperationItemCodec = z.union([ + z.object({ kind: z.literal('ton'), amount: z.string() }), + z.object({ kind: z.literal('token'), amount: z.string() }) +]); + +const supportedMessageTypeCodec = z.union([ + z.literal('jetton::excesses'), + z.literal('jetton::transfer'), + z.literal('jetton::transfer_notification'), + z.literal('deposit'), + z.literal('deposit::ok'), + z.literal('withdraw'), + z.literal('withdraw::all'), + z.literal('withdraw::delayed'), + z.literal('withdraw::ok'), + z.literal('airdrop') +]); + +const opCodec = z.object({ + type: supportedMessageTypeCodec, + options: z.optional(z.record(z.string())) +}); + +const parsedOperationCodec = z.object({ + address: z.string(), + comment: z.optional(z.string()), + items: z.array(parsedOperationItemCodec), + op: z.optional(opCodec) +}); + +const parsedTransactionCodec = z.object({ + address: z.string(), + lt: z.string(), + hash: z.string(), + prevTransaction: z.object({ + lt: z.string(), + hash: z.string() + }), + time: z.number(), + outMessagesCount: z.number(), + oldStatus: accountStatusCodec, + newStatus: accountStatusCodec, + fees: z.string(), + update: z.object({ + oldHash: z.string(), + newHash: z.string() + }), + inMessage: z.union([parsedMessageCodec, z.null()]), + outMessages: z.array(parsedMessageCodec), + parsed: z.object({ + seqno: z.union([z.number(), z.null()]), + body: z.union([txBodyCodec, z.null()]), + status: z.union([z.literal('success'), z.literal('failed'), z.literal('pending')]), + dest: z.union([z.string(), z.null()]), + kind: z.union([z.literal('out'), z.literal('in')]), + amount: z.string(), + resolvedAddress: z.string(), + bounced: z.boolean(), + mentioned: z.array(z.string()) + }), + operation: parsedOperationCodec +}); + +const parsedTransactionsCodec = z.object({ + blocks: blocksCodec, + transactions: z.array(parsedTransactionCodec) +}); + +export type ParsedTransaction = z.infer; +export type ParsedTransactions = { + blocks: z.infer, + transactions: ParsedTransaction[] +}; \ No newline at end of file From 641e458d40b8beff3c725fa5fa9030a254e8c010 Mon Sep 17 00:00:00 2001 From: Inal Kardanov Date: Mon, 23 Oct 2023 17:45:48 +0300 Subject: [PATCH 05/14] Update README.md with deprecation information (#20) --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 198e943..6cd8e25 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # TON js client +## 🚨 Repository Deprecated and Moved! 🚨 + +**This repository has been deprecated and is no longer actively maintained.** We have moved our project to a new repository, which you can find here: [ton-org/ton](https://github.com/ton-org/ton). The new NPM package is available here: [@ton/ton](https://www.npmjs.com/package/@ton/ton) + +Please make sure to update your bookmarks and star the new repository to stay up-to-date with the latest developments and updates. This repository will be archived and eventually removed. + +**Thank you for your continued support!** +___________ + [![Version npm](https://img.shields.io/npm/v/ton.svg?logo=npm)](https://www.npmjs.com/package/ton) Cross-platform client for TON blockchain. From eb4d39cd43cfbebab77b75010e17b45a2a1cdf0e Mon Sep 17 00:00:00 2001 From: Daeren Date: Mon, 23 Oct 2023 17:47:44 +0300 Subject: [PATCH 06/14] Zod, safeParse, Trx - The 'message' field is missing (#21) The 'message' field is missing - responsible for the message attached to the transaction. As a result, the 'getTransactions' method will return information without this field. --- src/client/api/HttpApi.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/api/HttpApi.ts b/src/client/api/HttpApi.ts index 486d487..b639507 100644 --- a/src/client/api/HttpApi.ts +++ b/src/client/api/HttpApi.ts @@ -84,7 +84,8 @@ const message = z.object({ ihr_fee: z.string(), created_lt: z.string(), body_hash: z.string(), - msg_data: messageData + msg_data: messageData, + message: z.string() }); const transaction = z.object({ @@ -362,4 +363,4 @@ function serializeStack(src: TupleItem[]) { } } return stack; -} \ No newline at end of file +} From 4b8192109c4530b848afa9599f06b33d8f07c93a Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Mon, 23 Oct 2023 19:33:34 +0400 Subject: [PATCH 07/14] wip: parseStackItem improvements --- src/client/TonClient.ts | 84 ++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/client/TonClient.ts b/src/client/TonClient.ts index 40e99d0..8509571 100644 --- a/src/client/TonClient.ts +++ b/src/client/TonClient.ts @@ -8,7 +8,7 @@ import { HttpApi } from "./api/HttpApi"; import { AxiosAdapter } from 'axios'; -import { Address, beginCell, Cell, comment, Contract, ContractProvider, ContractState, external, loadTransaction, Message, openContract, storeMessage, toNano, Transaction, TupleItem, TupleReader } from 'ton-core'; +import { Address, beginCell, Cell, comment, Contract, ContractProvider, ContractState, external, loadTransaction, Message, openContract, storeMessage, toNano, Transaction, Tuple, TupleItem, TupleReader } from 'ton-core'; import { Maybe } from "../utils/maybe"; export type TonClientParameters = { @@ -295,47 +295,53 @@ export class TonClient { } } -function parseStack(src: any[]) { - const _parseStack = (src: []) => { - let stack: TupleItem[] = []; - for (let s of src) { - if (s[0] === 'num') { - let val = s[1] as string; - if (val.startsWith('-')) { - stack.push({ type: 'int', value: -BigInt(val.slice(1)) }); - } else { - stack.push({ type: 'int', value: BigInt(val) }); - } - } else if (s[0] === 'null') { - stack.push({ type: 'null' }); - } else if (s[0] === 'cell') { - stack.push({ type: 'cell', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); - } else if (s[0] === 'slice') { - stack.push({ type: 'slice', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); - } else if (s[0] === 'builder') { - stack.push({ type: 'builder', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }); - } else if (s[0] === 'tuple') { - stack.push({ - type: 'tuple', - items: _parseStack(s[1].elements.map((e) => { - switch (e["@type"]) { - case "tvm.stackEntryNumber": - return ["num", e.number.number]; - case "tvm.stackEntryCell": - return ["cell", e.cell]; - default: - throw Error("Unsupported item type: " + e["@type"]); - } - })) - }) - } else { - throw Error('Unsupported stack item type: ' + s[0]) - } +function parseStackEntry(s: any): TupleItem { + switch (s["@type"]) { + case "tvm.stackEntryNumber": + return { type: 'int', value: BigInt(s.number.number) }; + case "tvm.stackEntryCell": + return { type: 'cell', cell: Cell.fromBase64(s.cell) }; + case 'tvm.stackEntryTuple': + return { type: 'tuple', items: s.tuple.elements.map(parseStackEntry) }; + default: + throw Error("Unsupported item type: " + s["@type"]); + } +} + +function parseStackItem(s: any): TupleItem { + if (s[0] === 'num') { + let val = s[1] as string; + if (val.startsWith('-')) { + return { type: 'int', value: -BigInt(val.slice(1)) }; + } else { + return { type: 'int', value: BigInt(val) }; } - return stack; + } else if (s[0] === 'null') { + return { type: 'null' }; + } else if (s[0] === 'cell') { + return { type: 'cell', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }; + } else if (s[0] === 'slice') { + return { type: 'slice', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }; + } else if (s[0] === 'builder') { + return { type: 'builder', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }; + } else if (s[0] === 'tuple' || s[0] === 'list') { + return { + type: s[0], + items: s[1].elements.map(parseStackEntry) + }; + } else { + throw Error('Unsupported stack item type: ' + s[0]) + } +} + +function parseStack(src: any[]) { + let stack: TupleItem[] = []; + + for (let s of src) { + stack.push(parseStackItem(s)); } - return new TupleReader(_parseStack(stack)); + return new TupleReader(stack); } function createProvider(client: TonClient, address: Address, init: { code: Cell | null, data: Cell | null } | null): ContractProvider { From 74843c44db1d0e88759bf02179e22baa03ba5c62 Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Mon, 23 Oct 2023 19:44:38 +0400 Subject: [PATCH 08/14] fix: toncenter missbehavior --- src/client/TonClient.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/TonClient.ts b/src/client/TonClient.ts index 8509571..4b868d8 100644 --- a/src/client/TonClient.ts +++ b/src/client/TonClient.ts @@ -325,6 +325,10 @@ function parseStackItem(s: any): TupleItem { } else if (s[0] === 'builder') { return { type: 'builder', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] }; } else if (s[0] === 'tuple' || s[0] === 'list') { + // toncenter.com missbehaviour + if (s[1].elements.length === 0) { + return { type: 'null' }; + } return { type: s[0], items: s[1].elements.map(parseStackEntry) From 950b38a87f014d79ec4d134178f979326c86693a Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Tue, 24 Oct 2023 14:46:29 +0400 Subject: [PATCH 09/14] chore: merge ton-org/ton --- CHANGELOG.md | 9 +++++ README.md | 8 +++-- src/client/TonClient4.ts | 2 +- src/multisig/MultisigWallet.spec.ts | 51 +++++++++++++++++++++++++++++ src/multisig/MultisigWallet.ts | 25 ++++++++++++++ 5 files changed, 92 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce4e63..5191397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [13.7.0] - 2023-09-18 + +## Added +- `sendOrderWithoutSecretKey` method to `MultisigWallet` + +## Fixed +- Uri encode get method name for `TonClient4` + + ## [13.6.1] - 2023-08-24 ## Fixed diff --git a/README.md b/README.md index 6cd8e25..66d8f01 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TON js client +# TON JS Client ## 🚨 Repository Deprecated and Moved! 🚨 @@ -25,7 +25,7 @@ Cross-platform client for TON blockchain. yarn add ton ton-crypto ton-core buffer ``` -#### Browser polifil +#### Browser polyfill ```js // Add before using library @@ -78,6 +78,10 @@ let transfer = await contract.createTransfer({ [Documentation](https://ton-community.github.io/ton/) +## Acknowledgements + +This library is developed by the [Whales Corp.](https://tonwhales.com/) and maintained by [Dan Volkov](https://github.com/dvlkv). + ## License MIT diff --git a/src/client/TonClient4.ts b/src/client/TonClient4.ts index ad9c62d..f5be461 100644 --- a/src/client/TonClient4.ts +++ b/src/client/TonClient4.ts @@ -237,7 +237,7 @@ export class TonClient4 { */ async runMethod(seqno: number, address: Address, name: string, args?: TupleItem[]) { let tail = args && args.length > 0 ? '/' + toUrlSafe(serializeTuple(args).toBoc({ idx: false, crc32: false }).toString('base64')) : ''; - let url = this.#endpoint + '/block/' + seqno + '/' + address.toString({ urlSafe: true }) + '/run/' + name + tail; + let url = this.#endpoint + '/block/' + seqno + '/' + address.toString({ urlSafe: true }) + '/run/' + encodeURIComponent(name) + tail; let res = await axios.get(url, { adapter: this.#adapter, timeout: this.#timeout }); let runMethod = runMethodCodec.safeParse(res.data); if (!runMethod.success) { diff --git a/src/multisig/MultisigWallet.spec.ts b/src/multisig/MultisigWallet.spec.ts index e861f8f..da17d66 100644 --- a/src/multisig/MultisigWallet.spec.ts +++ b/src/multisig/MultisigWallet.spec.ts @@ -260,6 +260,57 @@ describe('MultisigWallet', () => { } }); + it('should accept orders sent by `sendWithoutSecretKey` method', async () => { + let multisig = new MultisigWallet(publicKeys, 0, 123, 2); + let provider = createProvider(multisig); + await multisig.deployInternal(treasure, 10000000000n); + await system.run(); + + let orderBuilder = new MultisigOrderBuilder(123); + orderBuilder.addMessage( + createInternalMessage( + true, + testAddress('address1'), + 1000000000n, + Cell.EMPTY + ), + 3 + ); + orderBuilder.addMessage( + createInternalMessage( + true, + testAddress('address2'), + 0n, + beginCell().storeUint(3, 123).endCell() + ), + 3 + ); + orderBuilder.addMessage( + createInternalMessage( + true, + testAddress('address1'), + 2000000000n, + Cell.EMPTY + ), + 3 + ); + + const order = orderBuilder.build(); + + const signature = sign(order.toCell(3).hash(), secretKeys[3]); + await multisig.sendOrderWithoutSecretKey( + orderBuilder.build(), + signature, + 3, + provider + ); + let txs = await system.run(); + expect(txs).toHaveLength(1); + if (txs[0].description.type == 'generic') { + expect(txs[0].description.aborted).toBeFalsy; + } + }); + it('should throw in sendOrder if there is no provider', async () => { let multisig = new MultisigWallet(publicKeys, 0, 123, 2); let provider = createProvider(multisig); diff --git a/src/multisig/MultisigWallet.ts b/src/multisig/MultisigWallet.ts index 95b12e3..c7de001 100644 --- a/src/multisig/MultisigWallet.ts +++ b/src/multisig/MultisigWallet.ts @@ -162,6 +162,31 @@ export class MultisigWallet { await provider.external(cell); } + public async sendOrderWithoutSecretKey( + order: MultisigOrder, + signature: Buffer, + ownerId: number, + provider?: ContractProvider + ) { + if (!provider && !this.provider) { + throw Error( + 'you must specify provider if there is no such property in MultisigWallet instance' + ); + } + if (!provider) { + provider = this.provider!; + } + + let cell = order.toCell(ownerId); + + cell = beginCell() + .storeBuffer(signature) + .storeSlice(cell.asSlice()) + .endCell(); + + await provider.external(cell); + } + public getOwnerIdByPubkey(publicKey: Buffer) { for (const [key, value] of this.owners) { if (value.subarray(0, 32).equals(publicKey)) { From e25a05fbf4fedf471a9765c257b25f07ae3ba00c Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Tue, 24 Oct 2023 15:48:43 +0400 Subject: [PATCH 10/14] chore: changelog & fixes --- CHANGELOG.md | 10 +++++++--- package.json | 4 ++-- src/multisig/MultisigWallet.spec.ts | 2 +- src/wallets/WalletContractV5.spec.ts | 2 +- yarn.lock | 12 ++++++------ 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5191397..7a7adc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [13.7.0] - 2023-09-18 +## [Unreleased] ## Added -- `sendOrderWithoutSecretKey` method to `MultisigWallet` +- `TonClient4.getAccountTransactionsParsed` method (thanks @vzhovnitsky) +- `MultisigWallet` contract (thanks @Gusarich) +- `WalletV5` contract (thanks @siandreev) +- blockchain fees estimation via `computeStorageFees`/`computeExternalMessageFees`/`computeGasPrices`/`computeMessageForwardFees` (thanks @vzhovnitsky) ## Fixed -- Uri encode get method name for `TonClient4` +- Uri encode get method name for `TonClient4` (thanks @krigga) +- Improved `parseStackItem` due to toncenter.com bug ## [13.6.1] - 2023-08-24 diff --git a/package.json b/package.json index 22f2739..e28661c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "karma-webpack": "^5.0.0", "prando": "^6.0.1", "release-it": "^15.5.1", - "ton-core": "^0.52.0", + "ton-core": "^0.53.0", "ton-crypto": "3.2.0", "ton-emulator": "^2.1.1", "ts-jest": "^27.0.5", @@ -47,7 +47,7 @@ "zod": "^3.21.4" }, "peerDependencies": { - "ton-core": ">=0.51.0", + "ton-core": ">=0.53.0", "ton-crypto": ">=3.2.0" }, "publishConfig": { diff --git a/src/multisig/MultisigWallet.spec.ts b/src/multisig/MultisigWallet.spec.ts index da17d66..4634ad3 100644 --- a/src/multisig/MultisigWallet.spec.ts +++ b/src/multisig/MultisigWallet.spec.ts @@ -8,7 +8,7 @@ import { ContractProvider, MessageRelaxed, } from 'ton-core'; -import { getSecureRandomBytes, keyPairFromSeed } from 'ton-crypto'; +import { getSecureRandomBytes, keyPairFromSeed, sign } from 'ton-crypto'; import { testAddress, ContractSystem, Treasure } from 'ton-emulator'; import { MultisigWallet } from './MultisigWallet'; import { MultisigOrderBuilder } from './MultisigOrderBuilder'; diff --git a/src/wallets/WalletContractV5.spec.ts b/src/wallets/WalletContractV5.spec.ts index 92df56b..f67f078 100644 --- a/src/wallets/WalletContractV5.spec.ts +++ b/src/wallets/WalletContractV5.spec.ts @@ -18,7 +18,7 @@ const getExtensionsArray = async (wallet: OpenedContract) => { return await wallet.getExtensionsArray(); } catch (e) { // Handle toncenter bug. Toncenter incorrectly returns 'list' in the stack in case of empty extensions dict - if (e && typeof e === 'object' && 'message' in e && e.message === 'Unsupported stack item type: list') { + if (e instanceof Error && e.message === 'Unsupported stack item type: list') { return []; } throw e; diff --git a/yarn.lock b/yarn.lock index 2025ce1..3e8950c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8146,14 +8146,14 @@ __metadata: languageName: node linkType: hard -"ton-core@npm:^0.52.0": - version: 0.52.0 - resolution: "ton-core@npm:0.52.0" +"ton-core@npm:^0.53.0": + version: 0.53.0 + resolution: "ton-core@npm:0.53.0" dependencies: symbol.inspect: 1.0.1 peerDependencies: ton-crypto: ">=3.2.0" - checksum: dcb50ff6e2f7cb4c42a561319573ab07f7ff296229a7dec1210bcae6980e2a141a368136d9dacc45b93e6a5f24c4f820838a1aa724a69676458461373a3a587a + checksum: a0f6bc3034fda8521e1c628c1bedd5869166be29e4a1267268dd99e885ba25d5d7b67c88970b23bbb332d936ff1fe0d82f776ddb24649f938c047173b81a8629 languageName: node linkType: hard @@ -8213,7 +8213,7 @@ __metadata: release-it: ^15.5.1 symbol.inspect: 1.0.1 teslabot: ^1.3.0 - ton-core: ^0.52.0 + ton-core: ^0.53.0 ton-crypto: 3.2.0 ton-emulator: ^2.1.1 ts-jest: ^27.0.5 @@ -8224,7 +8224,7 @@ __metadata: webpack: ^5.51.2 zod: ^3.21.4 peerDependencies: - ton-core: ">=0.51.0" + ton-core: ">=0.53.0" ton-crypto: ">=3.2.0" languageName: unknown linkType: soft From 748080e881ed4df5ab84a530a24aa2248460b268 Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Tue, 24 Oct 2023 15:50:36 +0400 Subject: [PATCH 11/14] Release 13.7.0 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a7adc0..c4c5c54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [13.7.0] - 2023-10-24 ## Added - `TonClient4.getAccountTransactionsParsed` method (thanks @vzhovnitsky) diff --git a/package.json b/package.json index e28661c..693dd74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ton", - "version": "13.6.1", + "version": "13.7.0", "repository": "https://github.com/ton-core/ton.git", "author": "Whales Corp. ", "license": "MIT", From a62488f84fb8bb1608f6a0a1708937caf807ba28 Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Tue, 24 Oct 2023 15:52:24 +0400 Subject: [PATCH 12/14] chore: add exports for fees utils --- src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 90c38db..ebdfffc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,4 +71,10 @@ export { GasLimitsPrices, StoragePrices, MsgPrices, WorkchainDescriptor, configParseValidatorSet, configParseWorkchainDescriptor, parseBridge, parseProposalSetup, parseValidatorSet, parseVotingSetup, parseFullConfig, - loadConfigParamById, loadConfigParamsAsSlice } from './config/ConfigParser' \ No newline at end of file + loadConfigParamById, loadConfigParamsAsSlice } from './config/ConfigParser' + +// +// Fees +// + +export { computeExternalMessageFees, computeFwdFees, computeGasPrices, computeMessageForwardFees, computeStorageFees } from './utils/fees'; \ No newline at end of file From 9912067d967f6d67f477b0b1c15d8317c6f85d13 Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Tue, 24 Oct 2023 15:53:09 +0400 Subject: [PATCH 13/14] chore: changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c5c54..c777754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## Fixed +- exports for fees estimation utils + ## [13.7.0] - 2023-10-24 ## Added From bd3f556617919ebf07a60212ba7ab43635ab939d Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Tue, 24 Oct 2023 15:54:07 +0400 Subject: [PATCH 14/14] Release 13.8.0 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c777754..ba59d7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [13.8.0] - 2023-10-24 ## Fixed - exports for fees estimation utils diff --git a/package.json b/package.json index 693dd74..000c2d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ton", - "version": "13.7.0", + "version": "13.8.0", "repository": "https://github.com/ton-core/ton.git", "author": "Whales Corp. ", "license": "MIT",