diff --git a/docs/develop/dapps/cookbook.md b/docs/develop/dapps/cookbook.md index dc74967857..9df3ea95c7 100644 --- a/docs/develop/dapps/cookbook.md +++ b/docs/develop/dapps/cookbook.md @@ -785,4 +785,172 @@ 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 (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) + +_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, beginCell, fromNano } from '@ton/ton'; + +async function main() { + const client = new TonClient({ + endpoint: 'https://toncenter.com/api/v2/jsonRPC', + apiKey: '1b312c91c3b691255130350a49ac5a0742454725f910756aff94dfe44858388e', + }); + + 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; + + 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 + 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.loadAddressAny(); + 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 jettonWallet = ( + await client.runMethod(jettonMaster, 'get_wallet_address', [ + { type: 'slice', cell: beginCell().storeAddress(myAddress).endCell() }, + ]) + ).stack.readAddress(); + if (!jettonWallet.equals(sender)) { + // if sender is not our real JettonWallet: this message was faked + 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(`Transaction Hash: ${tx.hash().toString('hex')}`); + console.log(`Transaction LT: ${tx.lt}`); + console.log(); + } +} + +main().finally(() => console.log('Exiting...')); +``` + + + + +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