diff --git a/docs.json b/docs.json index 402e5072..940a1641 100644 --- a/docs.json +++ b/docs.json @@ -167,6 +167,7 @@ "group": "RPC providers", "pages": [ "ecosystem/rpc/overview", + "ecosystem/rpc/external-normalization", "ecosystem/rpc/toncenter" ] }, diff --git a/ecosystem/rpc/external-normalization.mdx b/ecosystem/rpc/external-normalization.mdx new file mode 100644 index 00000000..58b36450 --- /dev/null +++ b/ecosystem/rpc/external-normalization.mdx @@ -0,0 +1,274 @@ +--- +title: "External message normalisation" +--- + +import { Aside } from "/snippets/aside.jsx"; + + + + +Normalization is a standardization process that converts different representations into a consistent format. While messages across interfaces follow the TL-B scheme, structural differences in implementation sometimes lead to collisions. + +To address this, the ecosystem defines a standard that ensures consistent hash calculation. The normalization rules are specified in detail in [TEP-467](https://github.com/ton-blockchain/TEPs/pull/467). + +**The problem normalization solves:** + +Functionally identical messages may differ in how they represent the `src`, `import_fee`, and `init` fields. These variations result in different hashes for messages with equivalent content, which complicates transaction tracking and deduplication. + +**How normalization works:** + +The normalized hash is computed by applying the following standardization rules to an external-in message: + +1. **Source Address (`src`)**: set to `addr_none$00` +2. **Import Fee (`import_fee`)**: set to `0` +3. **InitState (`init`)**: set to an empty value +4. **Body**: always stored as a reference + +# Transaction lookup using external message from TON Connect + +This guide shows how to find the transaction associated with an `external-in` message on the TON blockchain. + +## Message normalization + +In TON, messages may contain fields like `init`, `src` and `importFee`. These fields should be removed or zeroed out before calculating the message hash, as described in [TEP-467](https://github.com/ton-blockchain/TEPs/blob/8b3beda2d8611c90ec02a18bec946f5e33a80091/text/0467-normalized-message-hash.md). + +Use the function below to generate a **normalized message hash**: + +```ts +/** + * Generates a normalized hash of an "external-in" message for comparison. + * + * This function ensures consistent hashing of external-in messages by following [TEP-467](https://github.com/ton-blockchain/TEPs/blob/8b3beda2d8611c90ec02a18bec946f5e33a80091/text/0467-normalized-message-hash.md): + * + * @param {Message} message - The message to be normalized and hashed. Must be of type `"external-in"`. + * @returns {Buffer} The hash of the normalized message. + * @throws {Error} if the message type is not `"external-in"`. + */ +export function getNormalizedExtMessageHash(message: Message) { + if (message.info.type !== 'external-in') { + throw new Error(`Message must be "external-in", got ${message.info.type}`); + } + + const info = { + ...message.info, + src: undefined, + importFee: 0n + }; + + const normalizedMessage = { + ...message, + init: null, + info: info, + }; + + return beginCell() + .store(storeMessage(normalizedMessage, { forceRef: true })) + .endCell() + .hash(); +} +``` + +## Retrying API calls + +Sometimes API requests may fail due to rate limits or network issues. Use `retry` function presented below to deal with api failures: + +```ts +export async function retry(fn: () => Promise, options: { retries: number; delay: number }): Promise { + let lastError: Error | undefined; + for (let i = 0; i < options.retries; i++) { + try { + return await fn(); + } catch (e) { + if (e instanceof Error) { + lastError = e; + } + await new Promise((resolve) => setTimeout(resolve, options.delay)); + } + } + throw lastError; +} +``` + +## Find the transaction by incoming message + +The `getTransactionByInMessage` function searches the account’s transaction history for a match by normalized external message hash: + +```ts +/** + * Tries to find transaction by ExternalInMessage + */ +async function getTransactionByInMessage( + inMessageBoc: string, + client: TonClient, +): Promise { + // Step 1. Convert Base64 boc to Message if input is a string + const inMessage = loadMessage(Cell.fromBase64(inMessageBoc).beginParse()); + + // Step 2. Ensure the message is an external-in message + if (inMessage.info.type !== 'external-in') { + throw new Error(`Message must be "external-in", got ${inMessage.info.type}`); + } + const account = inMessage.info.dest; + + // Step 3. Compute the normalized hash of the input message + const targetInMessageHash = getNormalizedExtMessageHash(inMessage); + + let lt: string | undefined = undefined; + let hash: string | undefined = undefined; + + // Step 4. Paginate through transaction history of account + while (true) { + const transactions = await retry( + () => + client.getTransactions(account, { + hash, + lt, + limit: 10, + archival: true, + }), + { delay: 1000, retries: 3 }, + ); + + if (transactions.length === 0) { + // No more transactions found - message may not be processed yet + return undefined; + } + + // Step 5. Search for a transaction whose input message matches the normalized hash + for (const transaction of transactions) { + if (transaction.inMessage?.info.type !== 'external-in') { + continue; + } + + const inMessageHash = getNormalizedExtMessageHash(transaction.inMessage); + if (inMessageHash.equals(targetInMessageHash)) { + return transaction; + } + } + + const last = transactions.at(-1)!; + lt = last.lt.toString(); + hash = last.hash().toString('base64'); + } +} +``` + +If found, it returns a `Transaction` object. Otherwise, it returns `undefined`. + +### Example + +```ts +import { TonClient } from '@ton/ton'; + +const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' }); + +const tx = await getTransactionByInMessage( + 'te6ccgEBAQEA...your-base64-message...', + client +); + +if (tx) { + console.log('Found transaction:', tx); +} else { + console.log('Transaction not found'); +} +``` + +## Waiting for transaction confirmation + +If you’ve just sent a message, it may take a few seconds before it appears on-chain. +The function `waitForTransaction` to poll the blockchain and wait for the corresponding transaction should be used in this case: + +```ts +/** + * Waits for a transaction to appear on-chain by incoming external message. + * + * Useful when the message has just been sent. + */ +async function waitForTransaction( + inMessageBoc: string, + client: TonClient, + retries: number = 10, + timeout: number = 1000, +): Promise { + const inMessage = loadMessage(Cell.fromBase64(inMessageBoc).beginParse()); + + if (inMessage.info.type !== 'external-in') { + throw new Error(`Message must be "external-in", got ${inMessage.info.type}`); + } + const account = inMessage.info.dest; + + const targetInMessageHash = getNormalizedExtMessageHash(inMessage); + + let attempt = 0; + while (attempt < retries) { + console.log(`Waiting for transaction to appear in network. Attempt: ${attempt}`); + + const transactions = await retry( + () => + client.getTransactions(account, { + limit: 10, + archival: true, + }), + { delay: 1000, retries: 3 }, + ); + + for (const transaction of transactions) { + if (transaction.inMessage?.info.type !== 'external-in') { + continue; + } + + const inMessageHash = getNormalizedExtMessageHash(transaction.inMessage); + if (inMessageHash.equals(targetInMessageHash)) { + return transaction; + } + } + + await new Promise((resolve) => setTimeout(resolve, timeout)); + } + + // Transaction was not found - message may not be processed + return undefined; +} +``` + +### Example + +```typescript +import { TonClient } from '@ton/ton'; + +const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' }); + +const [tonConnectUI, setOptions] = useTonConnectUI(); + +// Obtain ExternalInMessage boc +const { boc } = await tonConnectUI.sendTransaction({ + messages: [ + { + address: "UQBSzBN6cnxDwDjn_IQXqgU8OJXUMcol9pxyL-yLkpKzYpKR", + amount: "20000000" + } + ] +}); + +const tx = await waitForTransaction( + boc, + client, + 10, // retries + 1000, // timeout before each retry +); + +if (tx) { + console.log('Found transaction:', tx); +} else { + console.log('Transaction not found'); +} +``` + +## See also + +* [TEP-467: Normalized Message Hash](https://github.com/ton-blockchain/TEPs/blob/8b3beda2d8611c90ec02a18bec946f5e33a80091/text/0467-normalized-message-hash.md) +* [Messages and transactions](/ton/transaction) +* [TON Connect: Sending messages](/ecosystem/ton-connect/index) \ No newline at end of file diff --git a/techniques/using-libraries.mdx b/techniques/using-libraries.mdx index 00f4f07a..593b79e3 100644 --- a/techniques/using-libraries.mdx +++ b/techniques/using-libraries.mdx @@ -2,7 +2,7 @@ title: "Using libraries" --- -import { Image, ImageControls } from '/snippets/image.jsx'; +import { Image } from '/snippets/image.jsx'; It is recommended to read [Library cells](/ton/cells/library-cells) first. diff --git a/ton/cells/bag-of-cells.mdx b/ton/cells/bag-of-cells.mdx new file mode 100644 index 00000000..e69de29b