From 59dd95847289ac909ee06c8366bb239713a39bc8 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Sat, 18 Nov 2023 18:11:52 +0300 Subject: [PATCH 1/3] add example to cookbook --- docs/develop/dapps/cookbook.md | 52 +++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/docs/develop/dapps/cookbook.md b/docs/develop/dapps/cookbook.md index dc74967857..56bbfe5a05 100644 --- a/docs/develop/dapps/cookbook.md +++ b/docs/develop/dapps/cookbook.md @@ -785,4 +785,54 @@ console.log(text); -This example will help you understand how you can work with such cells using recursion. \ No newline at end of file +This example will help you understand how you can work with such cells using recursion. + +### How to parse transactions of an account? + +The list of transactions on an account can be fetched through `getTransactions` API method. It returns an array of `Transaction` objects, with each item having lots of attributes. However, the fields that are the most commonly used are: + - Sender, Body and Value of the message that initiated this transaction + - Transaction's hash and logical time (LT) + +Below is an example on how you can fetch 5 most recent transactions on any blockchain account and print out these fields in a loop. + + + + +```js +import { Address, TonClient, fromNano } from '@ton/ton'; + +async function main() { + const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' }); + + const transactions = await client.getTransactions( + Address.parse('EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN'), // address that you want to fetch transactions from + { + limit: 5, + } + ); + + transactions.forEach((tx) => { + const inMsg = tx.inMessage; + + console.log(`inMsg Body: ${inMsg?.body}`); + if (inMsg?.info.type == 'internal') { + // we only process internal messages here because they are used the most + // for external messages some of the fields are empty, but the main structure is similar + console.log(`inMsg Sender: ${inMsg?.info.src}`); + console.log(`inMsg Value: ${fromNano(inMsg?.info.value.coins)}`); + } + console.log(`Hash: ${tx.hash().toString('hex')}`); + console.log(`LT: ${tx.lt}`); + console.log(); + }); +} + +main().finally(() => console.log('Exiting...')); +``` + + + + +Note that this example covers only the simplest case, where it is enough to fetch the transactions on a single account. If you want to go deeper and handle more complex chains of transactions and messages, you should take `tx.outMessages` field into an account. It contains the list of the output messages sent by smart-contract in the result of this transaction. To understand the whole logic better, you can read these articles: + * [Internal messages](/develop/smart-contracts/guidelines/internal-messages) + * [Message Delivery Guarantees](/develop/smart-contracts/guidelines/message-delivery-guarantees) \ No newline at end of file From 45b07dc93f618ec583f1cd89cc67377f163a8e40 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Sat, 25 Nov 2023 10:33:02 +0300 Subject: [PATCH 2/3] update cookbook example --- docs/develop/dapps/cookbook.md | 142 ++++++++++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 12 deletions(-) diff --git a/docs/develop/dapps/cookbook.md b/docs/develop/dapps/cookbook.md index 56bbfe5a05..8454957167 100644 --- a/docs/develop/dapps/cookbook.md +++ b/docs/develop/dapps/cookbook.md @@ -787,22 +787,27 @@ console.log(text); This example will help you understand how you can work with such cells using recursion. -### How to parse transactions of an account? +### How to parse transactions of an account (Transfers, Jettons, NFTs)? The list of transactions on an account can be fetched through `getTransactions` API method. It returns an array of `Transaction` objects, with each item having lots of attributes. However, the fields that are the most commonly used are: - Sender, Body and Value of the message that initiated this transaction - Transaction's hash and logical time (LT) -Below is an example on how you can fetch 5 most recent transactions on any blockchain account and print out these fields in a loop. +_Sender_ and _Body_ fields may be used to determine the type of message (regular transfer, jetton transfer, nft transfer etc). + +Below is an example on how you can fetch 5 most recent transactions on any blockchain account, parse them depending on the type and print out in a loop. ```js -import { Address, TonClient, fromNano } from '@ton/ton'; +import { Address, TonClient, beginCell, fromNano } from '@ton/ton'; async function main() { - const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' }); + const client = new TonClient({ + endpoint: 'https://toncenter.com/api/v2/jsonRPC', + apiKey: '1b312c91c3b691255130350a49ac5a0742454725f910756aff94dfe44858388e', + }); const transactions = await client.getTransactions( Address.parse('EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN'), // address that you want to fetch transactions from @@ -811,20 +816,133 @@ async function main() { } ); - transactions.forEach((tx) => { + for (const tx of transactions) { const inMsg = tx.inMessage; - console.log(`inMsg Body: ${inMsg?.body}`); if (inMsg?.info.type == 'internal') { // we only process internal messages here because they are used the most // for external messages some of the fields are empty, but the main structure is similar - console.log(`inMsg Sender: ${inMsg?.info.src}`); - console.log(`inMsg Value: ${fromNano(inMsg?.info.value.coins)}`); + const sender = inMsg?.info.src; + const value = inMsg?.info.value.coins; + + const originalBody = inMsg?.body.beginParse(); + let body = originalBody.clone(); + if (body.remainingBits < 32) { + // if body doesn't have opcode: it's a simple message without comment + console.log(`Simple transfer from ${sender} with value ${fromNano(value)} TON`); + } else { + const op = body.loadUint(32); + if (op == 0) { + // if opcode is 0: it's a simple message with comment + const comment = body.loadStringTail(); + console.log( + `Simple transfer from ${sender} with value ${fromNano(value)} TON and comment: "${comment}"` + ); + } else if (op == 0x7362d09c) { + // if opcode is 0x7362d09c: it's a Jetton transfer notification + + body.skip(64); // skip query_id + const jettonAmount = body.loadCoins(); + const jettonSender = body.loadAddress(); + const originalForwardPayload = body.loadBit() ? body.loadRef().beginParse() : body; + let forwardPayload = originalForwardPayload.clone(); + + // IMPORTANT: we have to verify the source of this message because it can be faked + const runStack = (await client.runMethod(sender, 'get_wallet_data')).stack; + runStack.skip(2); + const jettonMaster = runStack.readAddress(); + const jettonSenderJettonWallet = ( + await client.runMethod(jettonMaster, 'get_wallet_address', [ + { type: 'slice', cell: beginCell().storeAddress(jettonSender).endCell() }, + ]) + ).stack.readAddress(); + if (!jettonSenderJettonWallet.equals(sender)) { + console.log(`FAKE Jetton transfer`); + continue; + } + + if (forwardPayload.remainingBits < 32) { + // if forward payload doesn't have opcode: it's a simple Jetton transfer + console.log(`Jetton transfer from ${jettonSender} with value ${fromNano(jettonAmount)} Jetton`); + } else { + const forwardOp = forwardPayload.loadUint(32); + if (forwardOp == 0) { + // if forward payload opcode is 0: it's a simple Jetton transfer with comment + const comment = forwardPayload.loadStringTail(); + console.log( + `Jetton transfer from ${jettonSender} with value ${fromNano( + jettonAmount + )} Jetton and comment: "${comment}"` + ); + } else { + // if forward payload opcode is something else: it's some message with arbitrary structure + // you may parse it manually if you know other opcodes or just print it as hex + console.log( + `Jetton transfer with unknown payload structure from ${jettonSender} with value ${fromNano( + jettonAmount + )} Jetton and payload: ${originalForwardPayload}` + ); + } + + console.log(`Jetton Master: ${jettonMaster}`); + } + } else if (op == 0x05138d91) { + // if opcode is 0x05138d91: it's a NFT transfer notification + + body.skip(64); // skip query_id + const prevOwner = body.loadAddress(); + const originalForwardPayload = body.loadBit() ? body.loadRef().beginParse() : body; + let forwardPayload = originalForwardPayload.clone(); + + // IMPORTANT: we have to verify the source of this message because it can be faked + const runStack = (await client.runMethod(sender, 'get_nft_data')).stack; + runStack.skip(1); + const index = runStack.readBigNumber(); + const collection = runStack.readAddress(); + const itemAddress = ( + await client.runMethod(collection, 'get_nft_address_by_index', [{ type: 'int', value: index }]) + ).stack.readAddress(); + + if (!itemAddress.equals(sender)) { + console.log(`FAKE NFT Transfer`); + continue; + } + + if (forwardPayload.remainingBits < 32) { + // if forward payload doesn't have opcode: it's a simple NFT transfer + console.log(`NFT transfer from ${prevOwner}`); + } else { + const forwardOp = forwardPayload.loadUint(32); + if (forwardOp == 0) { + // if forward payload opcode is 0: it's a simple NFT transfer with comment + const comment = forwardPayload.loadStringTail(); + console.log(`NFT transfer from ${prevOwner} with comment: "${comment}"`); + } else { + // if forward payload opcode is something else: it's some message with arbitrary structure + // you may parse it manually if you know other opcodes or just print it as hex + console.log( + `NFT transfer with unknown payload structure from ${prevOwner} and payload: ${originalForwardPayload}` + ); + } + } + + console.log(`NFT Item: ${itemAddress}`); + console.log(`NFT Collection: ${collection}`); + } else { + // if opcode is something else: it's some message with arbitrary structure + // you may parse it manually if you know other opcodes or just print it as hex + console.log( + `Message with unknown structure from ${sender} with value ${fromNano( + value + )} TON and body: ${originalBody}` + ); + } + } } - console.log(`Hash: ${tx.hash().toString('hex')}`); - console.log(`LT: ${tx.lt}`); + console.log(`Transaction Hash: ${tx.hash().toString('hex')}`); + console.log(`Transaction LT: ${tx.lt}`); console.log(); - }); + } } main().finally(() => console.log('Exiting...')); @@ -833,6 +951,6 @@ main().finally(() => console.log('Exiting...')); -Note that this example covers only the simplest case, where it is enough to fetch the transactions on a single account. If you want to go deeper and handle more complex chains of transactions and messages, you should take `tx.outMessages` field into an account. It contains the list of the output messages sent by smart-contract in the result of this transaction. To understand the whole logic better, you can read these articles: +Note that this example covers only the simplest case with incoming messages, where it is enough to fetch the transactions on a single account. If you want to go deeper and handle more complex chains of transactions and messages, you should take `tx.outMessages` field into an account. It contains the list of the output messages sent by smart-contract in the result of this transaction. To understand the whole logic better, you can read these articles: * [Internal messages](/develop/smart-contracts/guidelines/internal-messages) * [Message Delivery Guarantees](/develop/smart-contracts/guidelines/message-delivery-guarantees) \ No newline at end of file From 0ec6bb39debc0a14f1ef62b6a66b6eafe3261bcc Mon Sep 17 00:00:00 2001 From: Gusarich Date: Tue, 5 Dec 2023 01:03:51 +0300 Subject: [PATCH 3/3] update --- docs/develop/dapps/cookbook.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/develop/dapps/cookbook.md b/docs/develop/dapps/cookbook.md index 8454957167..9df3ea95c7 100644 --- a/docs/develop/dapps/cookbook.md +++ b/docs/develop/dapps/cookbook.md @@ -809,12 +809,11 @@ async function main() { apiKey: '1b312c91c3b691255130350a49ac5a0742454725f910756aff94dfe44858388e', }); - const transactions = await client.getTransactions( - Address.parse('EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN'), // address that you want to fetch transactions from - { - limit: 5, - } - ); + const myAddress = Address.parse('EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN'); // address that you want to fetch transactions from + + const transactions = await client.getTransactions(myAddress, { + limit: 5, + }); for (const tx of transactions) { const inMsg = tx.inMessage; @@ -843,7 +842,7 @@ async function main() { body.skip(64); // skip query_id const jettonAmount = body.loadCoins(); - const jettonSender = body.loadAddress(); + const jettonSender = body.loadAddressAny(); const originalForwardPayload = body.loadBit() ? body.loadRef().beginParse() : body; let forwardPayload = originalForwardPayload.clone(); @@ -851,12 +850,13 @@ async function main() { const runStack = (await client.runMethod(sender, 'get_wallet_data')).stack; runStack.skip(2); const jettonMaster = runStack.readAddress(); - const jettonSenderJettonWallet = ( + const jettonWallet = ( await client.runMethod(jettonMaster, 'get_wallet_address', [ - { type: 'slice', cell: beginCell().storeAddress(jettonSender).endCell() }, + { type: 'slice', cell: beginCell().storeAddress(myAddress).endCell() }, ]) ).stack.readAddress(); - if (!jettonSenderJettonWallet.equals(sender)) { + if (!jettonWallet.equals(sender)) { + // if sender is not our real JettonWallet: this message was faked console.log(`FAKE Jetton transfer`); continue; }