본문으로 건너뛰기

TON 요리책

제품 개발 중에는 TON의 다양한 컨트랙트와의 상호작용에 대한 여러 가지 질문이 자주 발생합니다.

이 문서는 모든 개발자의 모범 사례를 수집하고 모두와 공유하기 위해 만들어졌습니다.

컨트랙트 주소 작업하기

주소를 변환하고(사용자 친화적 <-> raw), 조립하고, 문자열에서 추출하는 방법은?

TON 주소는 블록체인에서 컨트랙트를 고유하게 식별하며, 워크체인과 원래 상태 해시를 나타냅니다. 두 가지 일반적인 형식이 사용됩니다: raw(":"로 구분된 워크체인과 HEX로 인코딩된 해시)와 사용자 친화적(특정 플래그가 있는 base64로 인코딩된) 형식입니다.

User-friendly: EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
Raw: 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

SDK에서 문자열로부터 주소 객체를 얻으려면 다음 코드를 사용할 수 있습니다:

import { Address } from "@ton/core";


const address1 = Address.parse('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
const address2 = Address.parse('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e');

// toStrings arguments: urlSafe, bounceable, testOnly
// defaults values: true, true, false

console.log(address1.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address1.toRawString()); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

console.log(address2.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address2.toRawString()); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

사용자 친화적 주소에는 어떤 플래그가 있나요?

바운스 가능/바운스 불가능테스트넷/모든 넷의 두 가지 플래그가 정의됩니다. 주소의 첫 글자를 보면 쉽게 감지할 수 있습니다. 첫 글자는 주소 인코딩의 처음 6비트를 나타내며, TEP-2에 따라 거기에 플래그가 위치하기 때문입니다:

주소 시작이진 형식바운스 가능테스트넷 전용
E...000100.01아니오
U...010100.01아니오아니오
k...100100.01
0...110100.01아니오

테스트넷 전용 플래그는 블록체인에서 전혀 표현되지 않습니다. 바운스 불가능 플래그는 전송의 목적지 주소로 사용될 때만 차이를 만듭니다: 이 경우, 보낸 메시지에 대해 바운스를 허용하지 않습니다. 블록체인의 주소는 이 플래그를 포함하지 않습니다.

또한 일부 라이브러리에서는 urlSafe라는 직렬화 매개변수를 볼 수 있습니다. base64 형식은 URL에 안전하지 않습니다. 즉, 일부 문자(즉, +/)가 링크로 주소를 전송할 때 문제를 일으킬 수 있습니다. urlSafe = true일 때는 모든 + 기호가 -로, 모든 / 기호가 _로 대체됩니다. 다음 코드를 사용하여 이러한 주소 형식을 얻을 수 있습니다:

import { Address } from "@ton/core";

const address = Address.parse('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');

// toStrings arguments: urlSafe, bounceable, testOnly
// defaults values: true, true, false

console.log(address.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHFэ
console.log(address.toString({urlSafe: false})) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF
console.log(address.toString({bounceable: false})) // UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
console.log(address.toString({testOnly: true})) // kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
console.log(address.toString({bounceable: false, testOnly: true})) // 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK

TON 주소의 유효성을 어떻게 확인하나요?


const TonWeb = require("tonweb")

TonWeb.utils.Address.isValid('...')

TON 생태계의 표준 지갑들

TON을 전송하는 방법? 다른 지갑에 텍스트 메시지를 보내는 방법?

메시지 보내기



컨트랙트 배포하기



대부분의 SDK는 다음과 같은 지갑에서 메시지를 보내는 과정을 제공합니다:

  • 올바른 버전의 지갑 래퍼(프로그램의 객체)를 만듭니다(대부분의 경우 v3r2; 지갑 버전 참조). 비밀 키와 워크체인을 사용합니다(보통 0으로, 이는 베이스체인을 나타냅니다).
  • 또한 블록체인 래퍼나 "클라이언트" - API나 라이트서버에 요청을 라우팅할 객체를 만듭니다.
  • 그런 다음 블록체인 래퍼에서 컨트랙트를 _열기_합니다. 이는 컨트랙트 객체가 더 이상 추상적이지 않고 TON 메인넷이나 테스트넷의 실제 계정을 나타낸다는 의미입니다.
  • 그 후에는 원하는 메시지를 만들고 보낼 수 있습니다. 고급 매뉴얼에 설명된 대로 요청당 최대 4개의 메시지를 보낼 수도 있습니다.
import { TonClient, WalletContractV4, internal } from "@ton/ton";
import { mnemonicNew, mnemonicToPrivateKey } from "@ton/crypto";

const client = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
apiKey: 'your-api-key', // Optional, but note that without api-key you need to send requests once per second, and with 0.25 seconds
});

// Convert mnemonics to private key
let mnemonics = "word1 word2 ...".split(" ");
let keyPair = await mnemonicToPrivateKey(mnemonics);

// Create wallet contract
let workchain = 0; // Usually you need a workchain 0
let wallet = WalletContractV4.create({ workchain, publicKey: keyPair.publicKey });
let contract = client.open(wallet);

// Create a transfer
let seqno: number = await contract.getSeqno();
await contract.sendTransfer({
seqno,
secretKey: keyPair.secretKey,
messages: [internal({
value: '1',
to: 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N',
body: 'Example transfer body',
})]
});

코멘트 작성하기: 스네이크 형식의 긴 문자열

때로는 긴 문자열(또는 다른 큰 정보)을 저장해야 할 필요가 있지만 셀은 최대 1023비트만 보유할 수 있습니다. 이 경우에는 스네이크 셀을 사용할 수 있습니다. 스네이크 셀은 다른 셀에 대한 참조를 포함하는 셀이며, 그 셀은 다시 다른 셀에 대한 참조를 포함하는 식입니다.

const TonWeb = require("tonweb");

function writeStringTail(str, cell) {
const bytes = Math.floor(cell.bits.getFreeBits() / 8); // 1 symbol = 8 bits
if(bytes < str.length) { // if we can't write all string
cell.bits.writeString(str.substring(0, bytes)); // write part of string
const newCell = writeStringTail(str.substring(bytes), new TonWeb.boc.Cell()); // create new cell
cell.refs.push(newCell); // add new cell to current cell's refs
} else {
cell.bits.writeString(str); // write all string
}

return cell;
}

function readStringTail(slice) {
const str = new TextDecoder('ascii').decode(slice.array); // decode uint8array to string
if (cell.refs.length > 0) {
return str + readStringTail(cell.refs[0].beginParse()); // read next cell
} else {
return str;
}
}

let cell = new TonWeb.boc.Cell();
const str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In euismod, ligula vel lobortis hendrerit, lectus sem efficitur enim, vel efficitur nibh dui a elit. Quisque augue nisi, vulputate vitae mauris sit amet, iaculis lobortis nisi. Aenean molestie ultrices massa eu fermentum. Cras rhoncus ipsum mauris, et egestas nibh interdum in. Maecenas ante ipsum, sodales eget suscipit at, placerat ut turpis. Nunc ac finibus dui. Donec sit amet leo id augue tempus aliquet. Vestibulum eu aliquam ex, sit amet suscipit odio. Vestibulum et arcu dui.";
cell = writeStringTail(str, cell);
const text = readStringTail(cell.beginParse());
console.log(text);

많은 SDK에는 이미 긴 문자열을 파싱하고 저장하는 기능이 있습니다. 다른 SDK에서는 재귀를 사용하거나 "tail calls"라고 알려진 트릭을 사용하여 이러한 셀을 다룰 수 있습니다.

코멘트 메시지의 앞에 32개의 0비트가 있다는 것을 잊지 마세요(opcode가 0이라고 할 수 있습니다)!

TEP-74 (Jettons 표준)

사용자의 Jetton 지갑 주소를 어떻게 계산하나요(오프체인)?

사용자의 jetton 지갑 주소를 계산하기 위해서는 실제로 사용자 주소를 가진 jetton 마스터 컨트랙트의 "get_wallet_address" get-메소드를 호출해야 합니다. 이 작업을 위해 JettonMaster의 getWalletAddress 메소드를 쉽게 사용하거나 직접 마스터 컨트랙트를 호출할 수 있습니다.

정보

@ton/tonJettonMaster는 많은 기능이 부족하지만 다행히도 _이 기능_은 있습니다.

const { Address, beginCell } = require("@ton/core")
const { TonClient, JettonMaster } = require("@ton/ton")

const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});

const jettonMasterAddress = Address.parse('...') // for example EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE
const userAddress = Address.parse('...')

const jettonMaster = client.open(JettonMaster.create(jettonMasterAddress))
console.log(await jettonMaster.getWalletAddress(userAddress))

사용자의 Jetton 지갑 주소를 어떻게 계산하나요(오프라인)?

지갑 주소를 얻기 위해 매번 GET 메소드를 호출하는 것은 많은 시간과 리소스가 필요할 수 있습니다. Jetton 지갑 코드와 그 저장소 구조를 미리 알고 있다면, 네트워크 요청 없이 지갑 주소를 얻을 수 있습니다.

Tonviewer를 사용하여 코드를 얻을 수 있습니다. 예를 들어 jUSDT를 예로 들어보겠습니다. Jetton Master 주소는 EQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwiuA입니다. 이 주소로 가서 Methods 탭을 열면 이미 get_jetton_data 메소드가 있는 것을 볼 수 있습니다. 이를 호출하면 Jetton 지갑 코드가 있는 셀의 hex 형식을 얻을 수 있습니다:

b5ee9c7201021301000385000114ff00f4a413f4bcf2c80b0102016202030202cb0405001ba0f605da89a1f401f481f481a9a30201ce06070201580a0b02f70831c02497c138007434c0c05c6c2544d7c0fc07783e903e900c7e800c5c75c87e800c7e800c1cea6d0000b4c7c076cf16cc8d0d0d09208403e29fa96ea68c1b088d978c4408fc06b809208405e351466ea6cc1b08978c840910c03c06f80dd6cda0841657c1ef2ea7c09c6c3cb4b01408eebcb8b1807c073817c160080900113e910c30003cb85360005c804ff833206e953080b1f833de206ef2d29ad0d30731d3ffd3fff404d307d430d0fa00fa00fa00fa00fa00fa00300008840ff2f00201580c0d020148111201f70174cfc0407e803e90087c007b51343e803e903e903534544da8548b31c17cb8b04ab0bffcb8b0950d109c150804d50500f214013e809633c58073c5b33248b232c044bd003d0032c032481c007e401d3232c084b281f2fff274013e903d010c7e800835d270803cb8b13220060072c15401f3c59c3e809dc072dae00e02f33b51343e803e903e90353442b4cfc0407e80145468017e903e9014d771c1551cdbdc150804d50500f214013e809633c58073c5b33248b232c044bd003d0032c0325c007e401d3232c084b281f2fff2741403f1c147ac7cb8b0c33e801472a84a6d8206685401e8062849a49b1578c34975c2c070c00870802c200f1000aa13ccc88210178d4519580a02cb1fcb3f5007fa0222cf165006cf1625fa025003cf16c95005cc2391729171e25007a813a008aa005004a017a014bcf2e2c501c98040fb004300c85004fa0258cf1601cf16ccc9ed5400725269a018a1c882107362d09c2902cb1fcb3f5007fa025004cf165007cf16c9c8801001cb0527cf165004fa027101cb6a13ccc971fb0050421300748e23c8801001cb055006cf165005fa027001cb6a8210d53276db580502cb1fcb3fc972fb00925b33e24003c85004fa0258cf1601cf16ccc9ed5400eb3b51343e803e903e9035344174cfc0407e800870803cb8b0be903d01007434e7f440745458a8549631c17cb8b049b0bffcb8b0b220841ef765f7960100b2c7f2cfc07e8088f3c58073c584f2e7f27220060072c148f3c59c3e809c4072dab33260103ec01004f214013e809633c58073c5b3327b55200087200835c87b51343e803e903e9035344134c7c06103c8608405e351466e80a0841ef765f7ae84ac7cbd34cfc04c3e800c04e81408f214013e809633c58073c5b3327b5520

이제 Jetton 지갑 코드, Jetton Master 주소, 그리고 보관소 구조를 알고 있으므로 수동으로 지갑 주소를 계산할 수 있습니다:

import { Address, Cell, beginCell, storeStateInit } from '@ton/core';

const JETTON_WALLET_CODE = Cell.fromBoc(Buffer.from('b5ee9c7201021301000385000114ff00f4a413f4bcf2c80b0102016202030202cb0405001ba0f605da89a1f401f481f481a9a30201ce06070201580a0b02f70831c02497c138007434c0c05c6c2544d7c0fc07783e903e900c7e800c5c75c87e800c7e800c1cea6d0000b4c7c076cf16cc8d0d0d09208403e29fa96ea68c1b088d978c4408fc06b809208405e351466ea6cc1b08978c840910c03c06f80dd6cda0841657c1ef2ea7c09c6c3cb4b01408eebcb8b1807c073817c160080900113e910c30003cb85360005c804ff833206e953080b1f833de206ef2d29ad0d30731d3ffd3fff404d307d430d0fa00fa00fa00fa00fa00fa00300008840ff2f00201580c0d020148111201f70174cfc0407e803e90087c007b51343e803e903e903534544da8548b31c17cb8b04ab0bffcb8b0950d109c150804d50500f214013e809633c58073c5b33248b232c044bd003d0032c032481c007e401d3232c084b281f2fff274013e903d010c7e800835d270803cb8b13220060072c15401f3c59c3e809dc072dae00e02f33b51343e803e903e90353442b4cfc0407e80145468017e903e9014d771c1551cdbdc150804d50500f214013e809633c58073c5b33248b232c044bd003d0032c0325c007e401d3232c084b281f2fff2741403f1c147ac7cb8b0c33e801472a84a6d8206685401e8062849a49b1578c34975c2c070c00870802c200f1000aa13ccc88210178d4519580a02cb1fcb3f5007fa0222cf165006cf1625fa025003cf16c95005cc2391729171e25007a813a008aa005004a017a014bcf2e2c501c98040fb004300c85004fa0258cf1601cf16ccc9ed5400725269a018a1c882107362d09c2902cb1fcb3f5007fa025004cf165007cf16c9c8801001cb0527cf165004fa027101cb6a13ccc971fb0050421300748e23c8801001cb055006cf165005fa027001cb6a8210d53276db580502cb1fcb3fc972fb00925b33e24003c85004fa0258cf1601cf16ccc9ed5400eb3b51343e803e903e9035344174cfc0407e800870803cb8b0be903d01007434e7f440745458a8549631c17cb8b049b0bffcb8b0b220841ef765f7960100b2c7f2cfc07e8088f3c58073c584f2e7f27220060072c148f3c59c3e809c4072dab33260103ec01004f214013e809633c58073c5b3327b55200087200835c87b51343e803e903e9035344134c7c06103c8608405e351466e80a0841ef765f7ae84ac7cbd34cfc04c3e800c04e81408f214013e809633c58073c5b3327b5520', 'hex'))[0];
const JETTON_MASTER_ADDRESS = Address.parse('EQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwiuA');
const USER_ADDRESS = Address.parse('UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA');

const jettonWalletStateInit = beginCell().store(storeStateInit({
code: JETTON_WALLET_CODE,
data: beginCell()
.storeCoins(0)
.storeAddress(USER_ADDRESS)
.storeAddress(JETTON_MASTER_ADDRESS)
.storeRef(JETTON_WALLET_CODE)
.endCell()
}))
.endCell();
const userJettonWalletAddress = new Address(0, jettonWalletStateInit.hash());

console.log('User Jetton Wallet address:', userJettonWalletAddress.toString());

대부분의 주요 토큰들은 TEP-74 표준의 표준 구현을 사용하기 때문에 다른 저장소 구조를 가지고 있지 않습니다. 예외는 중앙화된 스테이블코인을 위한 새로운 Jetton-with-governance 컨트랙트입니다. 이들의 차이점은 지갑 상태 필드의 존재와 보관소에서의 코드 셀의 부재입니다.

코멘트가 있는 jetton 전송을 위한 메시지를 어떻게 구성하나요?

토큰 전송을 위한 메시지를 구성하는 방법을 이해하기 위해서는 토큰 표준을 설명하는 TEP-74를 사용합니다.

Jettons 전송하기



경고

When displayed, token doesn't usually show count of indivisible units user has; rather, amount is divided by 10 ^ decimals. This value is commonly set to 9, and this allows us to use toNano function. If decimals were different, we would need to multiply by a different value (for instance, if decimals are 6, then we would end up transferring thousand times the amount we wanted).

물론 항상 불가분 단위로 계산할 수도 있습니다.

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const jettonWalletAddress = Address.parse('put your jetton wallet address');
const destinationAddress = Address.parse('put destination wallet address');

const forwardPayload = beginCell()
.storeUint(0, 32) // 0 opcode means we have a comment
.storeStringTail('Hello, TON!')
.endCell();

const messageBody = beginCell()
.storeUint(0x0f8a7ea5, 32) // opcode for jetton transfer
.storeUint(0, 64) // query id
.storeCoins(toNano(5)) // jetton amount, amount * 10^9
.storeAddress(destinationAddress)
.storeAddress(destinationAddress) // response destination
.storeBit(0) // no custom payload
.storeCoins(toNano('0.02')) // forward amount - if >0, will send notification message
.storeBit(1) // we store forwardPayload as a reference
.storeRef(forwardPayload)
.endCell();

const internalMessage = internal({
to: jettonWalletAddress,
value: toNano('0.1'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

forward_amount가 0이 아닌 경우, 이 섹션의 상단에 있는 도식에서 볼 수 있듯이 목적지 컨트랙트로 jetton 수신에 대한 알림이 전송됩니다. response_destination 주소가 null이 아닌 경우, 남은 toncoins(이를 "excesses"라고 함)가 그 주소로 보내집니다.

익스플로러는 일반 TON 전송과 마찬가지로 jetton 알림의 코멘트도 지원합니다. 형식은 32개의 0비트와 그 다음에 텍스트(가급적 UTF-8)입니다.

Jetton 전송에는 수수료와 나가는 메시지 뒤의 금액에 대한 신중한 고려가 필요합니다. 예를 들어, 0.2 TON으로 전송을 "호출"하면 0.1 TON을 전달하고 excess 반환 메시지로 0.1 TON을 받을 수 없습니다.

TEP-62 (NFT 표준)

NFT 컬렉션은 매우 다양합니다. 실제로 TON의 NFT 컨트랙트는 "적절한 get-메소드를 가지고 있고 유효한 메타데이터를 반환하는 컨트랙트"로 정의될 수 있습니다. 전송 작업은 표준화되어 있고 jetton의 것과 매우 유사하므로, 이것에 대해서는 자세히 다루지 않고 대신 만날 수 있는 대부분의 컬렉션이 제공하는 추가 기능들을 살펴보겠습니다!

경고

주의사항: NFT에 대한 아래의 모든 메소드는 TEP-62에 의해 작동이 강제되지 않습니다. 이들을 시도하기 전에 NFT나 컬렉션이 예상대로 그러한 메시지를 처리할 것인지 확인하세요. 이 경우 지갑 앱 에뮬레이션이 유용할 수 있습니다.

NFT 일괄 배포를 어떻게 사용하나요?

컬렉션을 위한 스마트 컨트랙트는 단일 트랜잭션에서 최대 250개의 NFT를 배포할 수 있습니다. 하지만 1 ton의 계산 수수료 한도 때문에 실제로는 이 최대값이 100-130개의 NFT 정도라는 점을 고려해야 합니다. 이를 달성하기 위해서는 새로운 NFT에 대한 정보를 딕셔너리에 저장해야 합니다.

NFT 일괄 발행하기

정보

NFT 표준 /ton-blockchain /token-contract에서 지정되지 않음



import { Address, Cell, Dictionary, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";
import { TonClient } from "@ton/ton";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const nftMinStorage = '0.05';
const client = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC' // for Testnet
});
const ownersAddress = [
Address.parse('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx'),
Address.parse('EQAUTbQiM522Y_XJ_T98QPhPhTmb4nV--VSPiha8kC6kRfPO'),
Address.parse('EQDWTH7VxFyk_34J1CM6wwEcjVeqRQceNwzPwGr30SsK43yo')
];
const nftsMeta = [
'0/meta.json',
'1/meta.json',
'2/meta.json'
];

const getMethodResult = await client.runMethod(collectionAddress, 'get_collection_data');
let nextItemIndex = getMethodResult.stack.readNumber();

먼저, 저장소 수수료를 위한 TON의 최소 금액이 0.05라고 가정해봅시다. 이는 NFT를 배포한 후 컬렉션의 스마트 컨트랙트가 이 만큼의 TON을 잔액으로 보낸다는 의미입니다. 다음으로 새로운 NFT의 소유자와 그들의 콘텐츠가 있는 배열을 얻습니다. 그 후에 GET 메소드 get_collection_data를 사용하여 next_item_index를 얻습니다.

	let counter = 0;
const nftDict = Dictionary.empty<number, Cell>();
for (let index = 0; index < 3; index++) {
const metaCell = beginCell()
.storeStringTail(nftsMeta[index])
.endCell();
const nftContent = beginCell()
.storeAddress(ownersAddress[index])
.storeRef(metaCell)
.endCell();
nftDict.set(nextItemIndex, nftContent);
nextItemIndex++;
counter++;
}

/*
We need to write our custom serialization and deserialization
functions to store data correctly in the dictionary since the
built-in functions in the library are not suitable for our case.
*/
const messageBody = beginCell()
.storeUint(2, 32)
.storeUint(0, 64)
.storeDict(nftDict, Dictionary.Keys.Uint(64), {
serialize: (src, builder) => {
builder.storeCoins(toNano(nftMinStorage));
builder.storeRef(src);
},
parse: (src) => {
return beginCell()
.storeCoins(src.loadCoins())
.storeRef(src.loadRef())
.endCell();
}
})
.endCell();

const totalValue = String(
(counter * parseFloat(nftMinStorage) + 0.015 * counter).toFixed(6)
);

const internalMessage = internal({
to: collectionAddress,
value: totalValue,
bounce: true,
body: messageBody
});
}

main().finally(() => console.log("Exiting..."));

다음으로는 총 트랜잭션 비용을 정확하게 계산해야 합니다. 0.015 값은 테스트를 통해 얻었지만, 각 시나리오마다 다를 수 있습니다. 이는 주로 NFT의 콘텐츠에 따라 달라지며, 콘텐츠 크기가 증가하면 전달 수수료(배송 수수료)가 더 높아집니다.

컬렉션 스마트 컨트랙트의 소유자를 어떻게 변경하나요?

컬렉션의 소유자를 변경하는 것은 매우 간단합니다. opcode = 3, 임의의 query_id, 그리고 새로운 소유자의 주소를 지정하기만 하면 됩니다:

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const newOwnerAddress = Address.parse('put new owner wallet address');

const messageBody = beginCell()
.storeUint(3, 32) // opcode for changing owner
.storeUint(0, 64) // query id
.storeAddress(newOwnerAddress)
.endCell();

const internalMessage = internal({
to: collectionAddress,
value: toNano('0.05'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

컬렉션 스마트 컨트랙트의 콘텐츠를 어떻게 변경하나요?

스마트 컨트랙트의 컬렉션 콘텐츠를 변경하려면 어떻게 저장되어 있는지 이해해야 합니다. 컬렉션은 모든 콘텐츠를 단일 셀에 저장하며, 그 안에는 두 개의 셀이 있습니다: 컬렉션 콘텐츠NFT 공통 콘텐츠. 첫 번째 셀은 컬렉션의 메타데이터를 포함하고, 두 번째 셀은 NFT 메타데이터를 위한 기본 URL을 포함합니다.

종종 컬렉션의 메타데이터는 0.json과 유사한 형식으로 저장되고 증가하며, 이 파일 앞의 주소는 동일하게 유지됩니다. NFT 공통 콘텐츠에 저장되어야 하는 것이 바로 이 주소입니다.

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const newCollectionMeta = 'put url fol collection meta';
const newNftCommonMeta = 'put common url for nft meta';
const royaltyAddress = Address.parse('put royalty address');

const collectionMetaCell = beginCell()
.storeUint(1, 8) // we have offchain metadata
.storeStringTail(newCollectionMeta)
.endCell();
const nftCommonMetaCell = beginCell()
.storeUint(1, 8) // we have offchain metadata
.storeStringTail(newNftCommonMeta)
.endCell();

const contentCell = beginCell()
.storeRef(collectionMetaCell)
.storeRef(nftCommonMetaCell)
.endCell();

const royaltyCell = beginCell()
.storeUint(5, 16) // factor
.storeUint(100, 16) // base
.storeAddress(royaltyAddress) // this address will receive 5% of each sale
.endCell();

const messageBody = beginCell()
.storeUint(4, 32) // opcode for changing content
.storeUint(0, 64) // query id
.storeRef(contentCell)
.storeRef(royaltyCell)
.endCell();

const internalMessage = internal({
to: collectionAddress,
value: toNano('0.05'),
bounce: true,
body: messageBody
});

const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

추가로, 로열티 정보를 메시지에 포함해야 합니다. 이들도 이 opcode를 사용하여 변경되기 때문입니다. 모든 값에 새로운 값을 지정할 필요가 없다는 것을 알아두는 것이 중요합니다. 예를 들어, NFT 공통 콘텐츠만 변경하려면 다른 모든 값을 이전과 동일하게 지정할 수 있습니다.

서드파티: 탈중앙화 거래소 (DEX)

DEX(DeDust)에 스왑 메시지를 어떻게 보내나요?

DEX는 작업을 위해 다른 프로토콜을 사용합니다. 이 예제에서는 DeDust와 상호작용할 것입니다.

DeDust에는 두 가지 교환 경로가 있습니다: jetton <-> jetton 또는 TON <-> jetton입니다. 각각 다른 구조를 가지고 있습니다. 스왑하려면 특정 vault에 jettons(또는 toncoin)을 보내고 특별한 페이로드를 제공해야 합니다. 다음은 jetton을 jetton으로 또는 jetton을 toncoin으로 스왑하기 위한 구조입니다:

swap#e3a0d482 _:SwapStep swap_params:^SwapParams = ForwardPayload;
step#_ pool_addr:MsgAddressInt params:SwapStepParams = SwapStep;
step_params#_ kind:SwapKind limit:Coins next:(Maybe ^SwapStep) = SwapStepParams;
swap_params#_ deadline:Timestamp recipient_addr:MsgAddressInt referral_addr:MsgAddress
fulfill_payload:(Maybe ^Cell) reject_payload:(Maybe ^Cell) = SwapParams;

이 구조는 jettons 전송 메시지(transfer#0f8a7ea5)의 forward_payload에 있어야 할 내용을 보여줍니다.

그리고 toncoin에서 jetton으로의 스왑 구조:

swap#ea06185d query_id:uint64 amount:Coins _:SwapStep swap_params:^SwapParams = InMsgBody;
step#_ pool_addr:MsgAddressInt params:SwapStepParams = SwapStep;
step_params#_ kind:SwapKind limit:Coins next:(Maybe ^SwapStep) = SwapStepParams;
swap_params#_ deadline:Timestamp recipient_addr:MsgAddressInt referral_addr:MsgAddress
fulfill_payload:(Maybe ^Cell) reject_payload:(Maybe ^Cell) = SwapParams;

이는 toncoin vault 로의 전송 본문을 위한 구조입니다.

먼저 스왑할 jettons의 vault 주소나 toncoin vault 주소를 알아야 합니다. 이는 Factory 컨트랙트의 get_vault_address get 메소드를 사용하여 할 수 있습니다. 인수로는 다음 구조에 따른 슬라이스를 전달해야 합니다:

native$0000 = Asset; // for ton
jetton$0001 workchain_id:int8 address:uint256 = Asset; // for jetton

또한 교환 자체를 위해서는 pool 주소가 필요합니다 - get 메소드 get_pool_address에서 얻습니다. 인수로는 위 구조에 따른 자산 슬라이스를 사용합니다. 응답으로 두 메소드 모두 요청된 vault / pool의 주소 슬라이스를 반환합니다.

이제 메시지를 만들 수 있습니다.

DEX는 작업을 위해 다른 프로토콜을 사용합니다. 우리는 주요 개념과 몇 가지 중요한 구성 요소를 파악해야 하며, 또한 스왑 프로세스를 올바르게 수행하기 위해 관련된 TL-B 스키마를 알아야 합니다. 이 튜토리얼에서는 TON에서 완전히 구현된 유명한 DEX 중 하나인 DeDust를 다룹니다. DeDust에서는 스왑 가능한 모든 자산 유형을 포함하는 추상 Asset 개념을 가지고 있습니다. 자산 유형에 대한 추상화는 자산 유형이 중요하지 않고 이 접근 방식에서는 추가 통화나 심지어 다른 체인의 자산도 쉽게 다룰 수 있기 때문에 스왑 프로세스를 단순화합니다.

다음은 DeDust가 Asset 개념을 위해 도입한 TL-B 스키마입니다.

native$0000 = Asset; // for ton

jetton$0001 workchain_id:int8 address:uint256 = Asset; // for any jetton,address refer to jetton master address

// Upcoming, not implemented yet.
extra_currency$0010 currency_id:int32 = Asset;

다음으로, DeDust는 Vault, Pool, Factory라는 세 가지 구성 요소를 도입했습니다. 이러한 구성 요소는 컨트랙트나 컨트랙트 그룹이며 스왑 프로세스의 일부를 담당합니다. factory는 다른 구성 요소의 주소(vault와 pool 같은)를 찾고 다른 구성 요소를 구축하는 역할을 합니다. Vault는 전송 메시지를 받고, 자산을 보유하고, 해당 pool에 "사용자 A가 100 X를 Y로 스왑하고 싶어한다"고 알리는 역할만 합니다.

반면에 Pool은 사전 정의된 공식에 따라 스왑 금액을 계산하고, 자산 Y를 담당하는 다른 Vault에 알리고, 계산된 금액을 사용자에게 지불하라고 지시하는 역할을 합니다. 스왑 금액의 계산은 수학적 공식을 기반으로 하며, 지금까지 우리는 두 가지 다른 pool을 가지고 있습니다. 하나는 일반적으로 사용되는 "Constant Product" 공식: x _ y = k를 기반으로 하는 Volatile이고, 다른 하나는 거의 동일한 가치를 가진 자산(예: USDT / USDC, TON / stTON)에 최적화된 Stable-Swap입니다. 이는 공식 x3 _ y + y3 * x = k를 사용합니다. 따라서 모든 스왑에는 해당하는 Vault가 필요하며, 이는 특정 자산 유형과 상호작용하기 위해 맞춤화된 특정 API만 구현하면 됩니다. DeDust는 세 가지 Vault 구현을 가지고 있습니다: 네이티브 코인(Toncoin)을 처리하는 Native Vault, jettons를 관리하는 Jetton Vault, 그리고 TON extra-currencies를 위해 설계된 Extra-Currency Vault(예정)입니다.

DeDust는 컨트랙트, 구성 요소, API와 작업하기 위한 특별한 SDK를 제공하며, 이는 typescript로 작성되었습니다. 이론은 충분합니다. 이제 하나의 jetton을 TON과 스왑하기 위한 환경을 설정해봅시다.

npm install --save @ton/core @ton/ton @ton/crypto

DeDust SDK도 가져와야 합니다.

npm install --save @dedust/sdk

이제 몇 가지 객체를 초기화해야 합니다.

import { Factory, MAINNET_FACTORY_ADDR } from "@dedust/sdk";
import { Address, TonClient4 } from "@ton/ton";

const tonClient = new TonClient4({
endpoint: "https://mainnet-v4.tonhubapi.com",
});
const factory = tonClient.open(Factory.createFromAddress(MAINNET_FACTORY_ADDR));
//The Factory contract is used to locate other contracts.

스왑 과정에는 몇 가지 단계가 있습니다. 예를 들어 TON을 Jetton으로 스왑하려면 먼저 해당하는 Vault와 Pool을 찾고 그들이 배포되었는지 확인해야 합니다. 우리의 예제인 TON과 SCALE의 경우 코드는 다음과 같습니다:

import { Asset, VaultNative } from "@dedust/sdk";

//Native vault is for TON
const tonVault = tonClient.open(await factory.getNativeVault());
//We use the factory to find our native coin (Toncoin) Vault.

다음 단계는 해당하는 Pool을 찾는 것입니다. 여기서는 (TON과 SCALE)입니다.

import { PoolType } from "@dedust/sdk";

const SCALE_ADDRESS = Address.parse(
"EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE",
);
// master address of SCALE jetton
const TON = Asset.native();
const SCALE = Asset.jetton(SCALE_ADDRESS);

const pool = tonClient.open(
await factory.getPool(PoolType.VOLATILE, [TON, SCALE]),
);

이제 이러한 컨트랙트들이 존재하는지 확인해야 합니다. 비활성 컨트랙트로 자금을 보내면 복구할 수 없는 손실이 발생할 수 있기 때문입니다.

import { ReadinessStatus } from "@dedust/sdk";

// Check if the pool exists:
if ((await pool.getReadinessStatus()) !== ReadinessStatus.READY) {
throw new Error("Pool (TON, SCALE) does not exist.");
}

// Check if the vault exits:
if ((await tonVault.getReadinessStatus()) !== ReadinessStatus.READY) {
throw new Error("Vault (TON) does not exist.");
}

그 후에 TON 금액으로 전송 메시지를 보낼 수 있습니다.

import { toNano } from "@ton/core";
import { mnemonicToPrivateKey } from "@ton/crypto";

if (!process.env.MNEMONIC) {
throw new Error("Environment variable MNEMONIC is required.");
}

const mnemonic = process.env.MNEMONIC.split(" ");

const keys = await mnemonicToPrivateKey(mnemonic);
const wallet = tonClient.open(
WalletContractV3R2.create({
workchain: 0,
publicKey: keys.publicKey,
}),
);

const sender = wallet.sender(keys.secretKey);

const amountIn = toNano("5"); // 5 TON

await tonVault.sendSwap(sender, {
poolAddress: pool.address,
amount: amountIn,
gasAmount: toNano("0.25"),
});

토큰 X를 Y로 스왑하는 과정도 동일합니다. 예를 들어, 우리는 X 토큰 금액을 vault X에 보내고, vault X는 우리의 자산을 받아서 보유하고, (X, Y)의 Pool에게 이 주소가 스왑을 요청한다고 알립니다. 이제 Pool은 계산을 기반으로 다른 Vault, 여기서는 Vault Y에게 알려서 스왑을 요청한 사용자에게 상응하는 Y를 전달합니다.

자산 간의 차이는 전송 방법에만 있습니다. 예를 들어, jettons의 경우 특정 forward_payload를 첨부하여 Vault로 전송 메시지를 보내지만, 네이티브 코인의 경우에는 해당 TON 금액을 첨부하여 Vault에 스왑 메시지를 보냅니다.

다음은 TON과 jetton의 스키마입니다:

swap#ea06185d query_id:uint64 amount:Coins _:SwapStep swap_params:^SwapParams = InMsgBody;

따라서 각 vault와 해당하는 Pool은 특정 스왑을 위해 설계되었으며 특정 자산에 맞춤화된 특별한 API를 가지고 있습니다.

이것이 TON을 jetton SCALE로 스왑하는 과정이었습니다. jetton을 jetton으로 스왑하는 과정도 동일하며, 유일한 차이점은 TL-B 스키마에 설명된 페이로드를 제공해야 한다는 것입니다.

swap#e3a0d482 _:SwapStep swap_params:^SwapParams = ForwardPayload;
//find Vault
const scaleVault = tonClient.open(await factory.getJettonVault(SCALE_ADDRESS));
//find jetton address
import { JettonRoot, JettonWallet } from '@dedust/sdk';

const scaleRoot = tonClient.open(JettonRoot.createFromAddress(SCALE_ADDRESS));
const scaleWallet = tonClient.open(await scaleRoot.getWallet(sender.address);

// Transfer jettons to the Vault (SCALE) with corresponding payload

const amountIn = toNano('50'); // 50 SCALE

await scaleWallet.sendTransfer(sender, toNano("0.3"), {
amount: amountIn,
destination: scaleVault.address,
responseAddress: sender.address, // return gas to user
forwardAmount: toNano("0.25"),
forwardPayload: VaultJetton.createSwapPayload({ poolAddress }),
});

메시지 처리 기본사항

계정의 트랜잭션을 어떻게 파싱하나요(전송, Jettons, NFTs)?

계정의 트랜잭션 목록은 getTransactions API 메소드를 통해 가져올 수 있습니다. 이는 Transaction 객체의 배열을 반환하며, 각 항목은 많은 속성을 가지고 있습니다. 하지만 가장 일반적으로 사용되는 필드는 다음과 같습니다:

  • 이 트랜잭션을 시작한 메시지의 발신자, 본문, 값
  • 트랜잭션의 해시와 논리적 시간(LT)

_발신자_와 본문 필드는 메시지의 유형(일반 전송, jetton 전송, nft 전송 등)을 결정하는 데 사용될 수 있습니다.

다음은 블록체인 계정의 최근 5개 트랜잭션을 가져와서 유형에 따라 파싱하고 루프에서 출력하는 방법의 예시입니다.

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...'));

이 예제는 단일 계정의 트랜잭션만 가져오면 충분한 가장 단순한 경우의 들어오는 메시지만 다룹니다. 더 깊이 들어가서 더 복잡한 트랜잭션과 메시지 체인을 처리하고 싶다면, tx.outMessages 필드를 고려해야 합니다. 이는 이 트랜잭션의 결과로 스마트 컨트랙트가 보낸 출력 메시지의 목록을 포함합니다. 전체 로직을 더 잘 이해하려면 다음 글을 읽어보세요:

이 주제는 결제 처리 글에서 더 깊이 다루어집니다.

특정 TON Connect 결과에 대한 트랜잭션을 어떻게 찾나요?

TON Connect 2는 블록체인에 보낸 셀만 반환하고 생성된 트랜잭션 해시는 반환하지 않습니다(외부 메시지가 손실되거나 시간 초과되면 그 트랜잭션이 발생하지 않을 수 있기 때문입니다). 하지만 제공된 BOC를 통해 우리 계정 내역에서 그 정확한 메시지를 검색할 수 있습니다.

검색을 더 쉽게 하기 위해 인덱서를 사용할 수 있습니다. 제공된 구현은 RPC에 연결된 TonClient용입니다.

블록체인 청취를 위한 시도에 retry 함수 준비:


export async function retry<T>(fn: () => Promise<T>, options: { retries: number, delay: number }): Promise<T> {
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;
}

boc의 메시지 본문과 동일한 특정 들어오는 외부 메시지가 있는 특정 계정의 특정 트랜잭션을 주장할 리스너 함수를 만듭니다:


import {Cell, Address, beginCell, storeMessage, TonClient} from "@ton/ton";

const res = tonConnectUI.send(msg); // exBoc in the result of sending message
const exBoc = res.boc;
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: 'INSERT YOUR API-KEY', // https://t.me/tonapibot
});

export async function getTxByBOC(exBoc: string): Promise<string> {

const myAddress = Address.parse('INSERT TON WALLET ADDRESS'); // Address to fetch transactions from

return retry(async () => {
const transactions = await client.getTransactions(myAddress, {
limit: 5,
});
for (const tx of transactions) {
const inMsg = tx.inMessage;
if (inMsg?.info.type === 'external-in') {

const inBOC = inMsg?.body;
if (typeof inBOC === 'undefined') {

reject(new Error('Invalid external'));
continue;
}
const extHash = Cell.fromBase64(exBoc).hash().toString('hex')
const inHash = beginCell().store(storeMessage(inMsg)).endCell().hash().toString('hex')

console.log(' hash BOC', extHash);
console.log('inMsg hash', inHash);
console.log('checking the tx', tx, tx.hash().toString('hex'));


// Assuming `inBOC.hash()` is synchronous and returns a hash object with a `toString` method
if (extHash === inHash) {
console.log('Tx match');
const txHash = tx.hash().toString('hex');
console.log(`Transaction Hash: ${txHash}`);
console.log(`Transaction LT: ${tx.lt}`);
return (txHash);
}
}
}
throw new Error('Transaction not found');
}, {retries: 30, delay: 1000});
}

txRes = getTxByBOC(exBOC);
console.log(txRes);

트랜잭션 또는 메시지 해시를 찾는 방법

정보

해시 정의에 주의하세요. 트랜잭션 해시와 메시지 해시는 서로 다른 것입니다.

트랜잭션 해시를 얻으려면 트랜잭션의 hash 메서드를 사용하세요. 외부 메시지 해시를 얻으려면 storeMessage 메서드를 사용하여 메시지 셀을 만든 다음 이 셀의 hash 메서드를 사용하세요.

import { storeMessage, TonClient } from '@ton/ton';
import { Address, beginCell } from '@ton/core';

const tonClient = new TonClient({ endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC' });

const transactions = await tonClient.getTransactions(Address.parse('[ADDRESS]'), { limit: 10 });
for (const transaction of transactions) {
// ful transaction hash
const transactionHash = transaction.hash();

const inMessage = transaction.inMessage;
if (inMessage?.info.type === 'external-in') {
const inMessageCell = beginCell().store(storeMessage(inMessage)).endCell();
// external-in message hash
const inMessageHash = inMessageCell.hash();
}

// also you can get hash of out messages if needed
for (const outMessage of transaction.outMessages.values()) {
const outMessageCell = beginCell().store(storeMessage(outMessage)).endCell();
const outMessageHash = outMessageCell.hash();
}
}

메시지를 생성할 때도 해시를 얻을 수 있습니다. 이는 이전 예제에서처럼 트랜잭션을 시작하기 위해 전송된 메시지의 해시와 동일합니다.

import { mnemonicNew, mnemonicToPrivateKey } from '@ton/crypto';
import { internal, TonClient, WalletContractV4 } from '@ton/ton';
import { toNano } from '@ton/core';

const tonClient = new TonClient({ endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC' });

const mnemonic = await mnemonicNew();
const keyPair = await mnemonicToPrivateKey(mnemonic);
const wallet = tonClient.open(WalletContractV4.create({ publicKey: keyPair.publicKey, workchain: 0 }));
const transfer = await wallet.createTransfer({
secretKey: keyPair.secretKey,
seqno: 0,
messages: [
internal({
to: wallet.address,
value: toNano(1)
})
]
});
const inMessageHash = transfer.hash();