지갑 스마트 컨트랙트 작업하기
👋 소개
스마트 컨트랙트 개발을 시작하기 전에 TON에서 지갑과 트랜잭션이 어떻게 작동하는지 배우는 것이 중요합니다. 이 지식은 개발자들이 특정 개발 작업을 구현하기 위해 지갑, 메시지, 스마트 컨트랙트 간의 상호작용을 이해하는 데 도움이 될 것입니다.
이 튜토리얼을 읽기 전에 지갑 컨트랙트의 유형 문서를 숙지하는 것이 권장됩니다.
이 섹션에서는 개발 워크플로우를 이해하기 위해 미리 구성된 함수를 사용하지 않고 작업을 생성하는 방법을 배울 것입니다. 이 튜토리얼 분석에 필요한 모든 참조는 참고 자료 장에 있습니다.
💡 전제 조건
이 튜토리얼은 JavaScript와 TypeScript 또는 Golang에 대한 기본 지식이 필요합니다. 또한 최소 3 TON을 보유하고 있어야 합니다(거래소 계정, 비수탁 지갑 또는 Telegram 봇 지갑을 사용하여 저장 가능). 이 튜토리얼을 이해하기 위해서는 cell, TON의 주소, 블록체인의 블록체인에 대한 기본적인 이해가 필요합니다.
TON 테스트넷으로 작업하면 종종 배포 오류, 트랜잭션 추적의 어려움, 불안정한 네트워크 기능이 발생합니다. 따라서 이러한 문제를 피하기 위해 TON 메인넷에서 대부분의 개발을 완료하는 것이 유익할 수 있으며, 이는 트랜잭션 수를 줄이고 수수료를 최소화하는 데 필요할 수 있습니다.
💿 소스 코드
이 튜토리얼에서 사용된 모든 코드 예제는 다음 GitHub 저장소에서 찾을 수 있습니다.
✍️ 시작하기 위해 필요한 것
- NodeJS가 설치되어 있는지 확인하세요.
- 특정 Ton 라이브러리가 필요합니다: @ton/ton 13.5.1+, @ton/core 0.49.2+ 및 @ton/crypto 3.2.0+.
선택사항: JS 대신 Go를 사용하려면 tonutils-go 라이브러리와 GoLand IDE를 설치하여 TON에서 개발을 수행해야 합니다. 이 라이브러리는 이 튜토리얼의 GO 버전에서 사용됩니다.
- JavaScript
- Golang
npm i --save @ton/ton @ton/core @ton/crypto
go get github.com/xssnick/tonutils-go
go get github.com/xssnick/tonutils-go/adnl
go get github.com/xssnick/tonutils-go/address
⚙ 환경 설정
TypeScript 프로젝트를 생성하려면 다음 순서대로 단계를 수행해야 합니다:
- 빈 폴더를 생성합니다(WalletsTutorial로 이름 지정).
- CLI를 사용하여 프로젝트 폴더를 엽니다.
- 다음 명령을 사용하여 프로젝트를 설정하세요:
npm init -y
npm install typescript @types/node ts-node nodemon --save-dev
npx tsc --init --rootDir src --outDir build \ --esModuleInterop --target es2020 --resolveJsonModule --lib es6 \ --module commonjs --allowJs true --noImplicitAny false --allowSyntheticDefaultImports true --strict false
다음 프로세스를 수행하는데 도움이 되도록 ts-node
를 사용하여 미리 컴파일하지 않고도 TypeScript 코드를 직접 실행합니다. nodemon
은 디렉토리에서 파일 변경이 감지될 때 노드 애플리케이션을 자동으로 재시작하는 데 사용됩니다.
"files": [
"\\",
"\\"
]
- 그런 다음 프로젝트 루트에 다음 내용으로
nodemon.json
설정을 생성합니다:
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "npx ts-node ./src/index.ts"
}
- 프로젝트 생성 시 추가되는 "test" 대신 이 스크립트를
package.json
에 추가합니다:
"start:dev": "npx nodemon"
- 프로젝트 루트에
src
폴더를 생성하고 이 폴더에index.ts
파일을 생성합니다. - 다음 코드를 추가해야 합니다:
async function main() {
console.log("Hello, TON!");
}
main().finally(() => console.log("Exiting..."));
- 터미널을 사용하여 코드를 실행합니다:
npm run start:dev
- 마지막으로 콘솔 출력이 나타납니다.
TON 커뮤니티는 모든 개발 프로세스(배포, 컨트랙트 작성, 테스트)를 자동화하기 위한 훌륭한 도구인 Blueprint를 만들었습니다. 하지만 이렇게 강력한 도구는 필요 하지 않으므로 위의 지침을 따르는 것이 좋습니다.
**선택사항: ** Golang을 사용할 때는 다음 지침을 따르세요:
- GoLand IDE를 설치합니다.
- 프로젝트 폴더와 다음 내용이 포함된
go.mod
파일을 생성합니다(현재 버전이 오래된 경우 Go 버전을 변경해야 할 수 있음):
module main
go 1.20
- 터미널에 다음 명령을 입력합니다:
go get github.com/xssnick/tonutils-go
- 다음 내용으로 프로젝트 루트에
main.go
파일을 생성합니다:
package main
import (
"log"
)
func main() {
log.Println("Hello, TON!")
}
go.mod
의 모듈 이름을main
으로 변경합니다.- 터미널에 출력이 표시될 때까지 위의 코드를 실행합니다.
GoLand가 무료가 아니므로 다른 IDE를 사용할 수 있지만 GoLand가 선호됩니다.
또한 각 새 섹션에서는 특정 코드 섹션에 필요한 임포트만 지정되며 새 임포트를 추가하고 이전 것과 결합해야 합니다.
🚀 시작하기
이 튜토리얼에서는 TON 블록체인에서 가장 많이 사용되는 지갑(버전 3과 4)에 대해 배우고 이러한 스마트 컨트랙트가 어떻게 작동하는지 알아볼 것입니다. 이를 통해 개발자들은 서로 다른 메시지 유형을 더 잘 이해하여 메시지 생성, 블록체인으로 전송, 지갑 배포가 더 쉬워지고, 궁극적으로 고부하 지갑과도 작업할 수 있게 됩니다.
우리의 주요 과제는 @ton/ton, @ton/core, @ton/crypto(ExternalMessage, InternalMessage, Signing 등)에 대한 다양한 객체와 함수를 사용하여 메시지를 구축하여 더 큰 규모에서 메시지가 어떻게 보이는지 이해하는 것입니다. 이 과정을 수행하기 위해 거래소, 비수탁 지갑, 대부분의 사용자가 이러한 특정 버전만 사용했기 때문에 두 가지 주요 지갑 버전(v3와 v4)을 사용할 것입니다.
There may be occasions in this tutorial when there is no explanation for particular details. In these cases, more details will be provided in later stages of this tutorial.
중요: 이 튜토리얼 전체에서 지갑 개발 프로세스를 더 잘 이해하기 위해 wallet v3 코드를 사용합니다. v3에는 r1과 r2 두 가지 하위 버전이 있다는 점에 유의해야 합니다. 현재는 두 번째 버전만 사용되고 있으며, 이는 이 문서에서 v3를 언급할 때 v3r2를 의미한다는 것을 의미합니다.
💎 TON 블록체인 지갑
TON 블록체인에서 작동하고 실행되는 모든 지갑은 실제로 스마트 컨트랙트입니다. TON에서 작동하는 모든 것이 스마트 컨트랙트인 것과 같은 방식입니다. 대부분의 블록체인과 마찬가지로 네트워크에 스마트 컨트랙트를 배포하고 다양한 용도로 사용자 정의할 수 있습니다. 이 기능 덕분에 전체 지갑 사용자 정의가 가능합니다. TON에서 지갑 스마트 컨트랙트는 플랫폼이 다른 유형의 스마트 컨트랙트와 통신하는 것을 돕습니다. 그러나 지갑 통신이 어떻게 이루어지는지 고려하는 것이 중요합니다.
지갑 통신
일반적으로 TON 블록체인에는 두 가지 메시지 유형이 있습니다: internal
과 external
. 외부 메시지는 외부 세계에서 블록체인으로 메시지를 보낼 수 있는 기능을 제공하므로 그러한 메시지를 수락하는 스마트 컨트랙트와 통신할 수 있습니다. 이 프로세스를 수행하는 책임이 있는 함수는 다음과 같습니다:
() recv_external(slice in_msg) impure {
;; some code
}
지갑에 대해 더 자세히 알아보기 전에, 지갑이 어떻게 외부 메시지를 수락하는지 살펴보겠습니다. TON에서 모든 지갑은 소유자의 public key
, seqno
, subwallet_id
를 보유합니다. 외부 메시지를 수신할 때 지갑은 get_data()
메서드를 사용하여 지갑의 저장소 부분에서 데이터를 검색합니다. 그런 다음 여러 확인 절차를 수행하고 메시지를 수락할지 여부를 결정합니다. 이 프로세스는 다음과 같이 수행됩니다:
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512); ;; get signature from the message body
var cs = in_msg;
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32)); ;; get rest values from the message body
throw_if(35, valid_until <= now()); ;; check the relevance of the message
var ds = get_data().begin_parse(); ;; get data from storage and convert it into a slice to be able to read values
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256)); ;; read values from storage
ds.end_parse(); ;; make sure we do not have anything in ds variable
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, subwallet_id == stored_subwallet);
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
accept_message();
💡 유용한 링크:
이제 더 자세히 살펴보겠습니다.
재생 보호 - Seqno
지갑 스마트 컨트랙트의 메시지 재생 보호는 메시지가 어떤 순서로 전송되는지 추적하는 메시지 seqno(Sequence Number)와 직접적으로 관련이 있습니다. 하나의 메시지가 지갑에서 반복되지 않는 것이 매우 중요한데, 이는 시스템의 무결성을 완전히 무너뜨리기 때문입니다. 지갑 내 스마트 컨트랙트 코드를 더 자세히 살펴보면, seqno
는 일반적으로 다음과 같이 처리됩니다:
throw_unless(33, msg_seqno == stored_seqno);
위의 코드 라인은 메시지에 들어오는 seqno
를 확인하고 스마트 컨트랙트에 저장된 seqno
와 확인합니다. 일치하지 않으면 컨트랙트는 33 exit code
로 오류를 반환합니다. 따라서 발신자가 잘못된 seqno를 전달하면 메시지 순서에서 실수를 했다는 의미이며, 컨트랙트는 이러한 경우를 막아냅니다.
외부 메시지는 누구나 보낼 수 있다는 점도 고려해야 합니다. 이는 누군가에게 1 TON을 보내면 다른 사람이 이 메시지를 반복할 수 있다는 것을 의미합니다. 하지만 seqno가 증가하면 이전 외부 메시지가 무효가 되어 아무도 반복할 수 없게 되므로 자금을 훔칠 가능성을 방지합니다.
서명
앞서 언급했듯이 지갑 스마트 컨트랙트는 외부 메시지를 수락합니다. 하지만 이러한 메시지는 외부 세계에서 오며 해당 데이터를 100% 신뢰할 수 없습니다. 따라서 각 지갑은 소유자의 공개 키를 저장합니다. 스마트 컨트랙트는 소유자가 개인 키로 서명한 외부 메시지를 받을 때 공개 키를 사용하여 메시지 서명의 정당성을 확인합니다. 이는 메시지가 실제로 컨트랙트 소유자로부터 온 것임을 검증합니다.
이 프로세스를 수행하기 위해 지갑은 먼저 들어오는 메시지에서 서명을 얻어 저장소에서 공개 키를 로드하고 다음 프로세스를 사용하여 서명을 검증해야 합니다:
var signature = in_msg~load_bits(512);
var ds = get_data().begin_parse();
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
그리고 모든 검증 프로세스가 올바르게 완료되면 스마트 컨트랙트는 메시지를 수락하고 처리합니다:
accept_message();
메시지가 외부 세계에서 오기 때문에 트랜잭션 수수료를 지불하는 데 필요한 Toncoin을 포함하지 않습니다. accept_message() 함수를 사용하여 TON을 보낼 때는 gas_credit(작성 시점에서 그 값은 10,000 gas 단위)이 적용되어 gas가 gas_credit 값을 초과하지 않는 경우 필요한 계산을 무료로 수행할 수 있습니다. accept_message() 함수가 사용된 후 사용된 모든 gas(TON으로)는 스마트 컨트랙트의 잔액에서 가져갑니다. 이 프로세스에 대해 여기에서 더 자세히 읽을 수 있습니다.
트랜잭션 만료
외부 메시지의 유효성을 확인하는 또 다른 단계는 valid_until
필드입니다. 변수 이름에서 알 수 있듯이, 이는 메시지가 유효한 UNIX 시간입니다. 이 검증 프로세스가 실패하면 컨트랙트는 트랜잭션 처리를 완료하고 35 exit code를 반환합니다:
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
throw_if(35, valid_until <= now());
이 알고리즘은 메시지가 더 이상 유효하지 않지만 알 수 없는 이유로 여전히 블록체인에 전송된 경우의 다양한 오류에 대한 취약성을 방지하는 데 도움이 됩니다.
지갑 v3와 지갑 v4의 차이점
지갑 v3와 지갑 v4의 유일한 차이점은 지갑 v4가 설치하고 삭제할 수 있는 플러그인
을 사용한다는 것입니다. 이러한 플러그인은 특정 시간에 지갑 스마트 컨트랙트에서 특정 수의 TON을 요청할 수 있는 특별한 스마트 컨트랙트입니다.
지갑 스마트 컨트랙트는 소유자의 참여 없이도 필요한 TON 금액을 응답으로 보냅니다. 이는 플러그인이 생성되는 구독 모델과 유사합니다. 이는 이 튜토리얼의 범위를 벗어나므로 이러한 세부사항은 배우지 않을 것입니다.
지갑이 스마트 컨트랙트와의 통신을 용이하게 하는 방법
앞서 논의했듯이, 지갑 스마트 컨트랙트는 외부 메시지를 수락하고, 검증하며, 모든 검사를 통과하면 수락합니다. 그런 다음 컨트랙트는 외부 메시지의 본문에서 메시지를 검색하는 루프를 시작한 다음 내부 메시지를 생성하고 다음과 같이 블록체인에 보냅니다:
cs~touch();
while (cs.slice_refs()) {
var mode = cs~load_uint(8); ;; load message mode
send_raw_message(cs~load_ref(), mode); ;; get each new internal message as a cell with the help of load_ref() and send it
}
TON에서 모든 스마트 컨트랙트는 스택 기반 TON Virtual Machine (TVM)에서 실행됩니다. ~ touch()는 더 적은 gas로 코드를 실행하도록 최적화하기 위해 변수 cs
를 스택의 맨 위에 배치합니다.
하나의 cell에 최대 4개의 참조를 저장할 수 있으므로, 외부 메시지당 최대 4개의 내부 메시지를 보낼 수 있습니다.
💡 유용한 링크:
📬 외부 및 내부 메시지
이 섹션에서는 internal
과 external
메시지에 대해 더 자세히 알아보고 미리 준비된 함수의 사용을 최소화하여 메시지를 생성하고 네트워크에 보낼 것입니다.
이 프로세스를 수행하기 위해서는 작업을 더 쉽게 하기 위해 준비된 지갑을 사용해야 합니다. 이를 위해:
- 지갑 앱을 설치하세요(예: 저자는 Tonkeeper를 사용)
- 지갑 앱을 v3r2 주소 버전으로 전환
- 지갑에 1 TON 입금
- 다른 주소로 메시지를 보내세요(자신에게, 같은 지갑으로 보낼 수 있음)
이렇게 하면 Tonkeeper 지갑 앱이 지갑 컨트랙트를 배포하고 다음 단계에서 사용할 수 있습니다.
이 글을 쓰는 시점에서 TON의 대부분의 지갑 앱은 기본적으로 지갑 v4 버전을 사용합니다. 이 튜토리얼에서는 플러그인이 필요하지 않으며 지갑 v3가 제공하는 기능을 사용할 것입니다. 사용 중에 Tonkeeper는 사용자가 원하는 지갑 버전을 선택할 수 있게 해줍니다. 따라서 지갑 버전 3(wallet v3)을 배포하는 것이 권장됩니다.
TL-B
앞서 언급했듯이, TON 블록체인의 모든 것은 cell로 구성된 스마트 컨트랙트입니다. 데이터를 적절하게 직렬화하고 역직렬화하기 위해서는 표준이 필요합니다. 직렬화와 역직렬화 프로세스를 수행하기 위해 TL-B
가 cell 내에서 다른 방식으로 다양한 데이터 유형을 설명하는 범용 도구로 생성되었습니다.
이 섹션에서는 block.tlb를 살펴볼 것입니다. 이 파일은 미래의 개발에서 매우 유용할 것입니다. 다양한 cell이 어떻게 조립되어야 하는지 설명하기 때문입니다. 우리의 경우 특히 내부 및 외부 메시지의 복잡성에 대해 자세히 설명합니다.
이 가이드 내에서 기본 정보가 제공됩니다. 자세한 내용은 TL-B 문서를 참조하여 TL-B에 대해 더 자세히 알아보세요.
CommonMsgInfo
초기에 각 메시지는 먼저 CommonMsgInfo
(TL-B) 또는 CommonMsgInfoRelaxed
(TL-B)를 저장해야 합니다. 이를 통해 메시지 유형, 메시지 시간, 수신자 주소, 기술적 플래그, 수수료와 관련된 기술적 세부사항을 정의할 수 있습니다.
block.tlb
파일을 읽어보면 세 가지 유형의 CommonMsgInfo를 확인할 수 있습니다: int_msg_info$0
, ext_in_msg_info$10
, ext_out_msg_info$11
. ext_out_msg_info
TL-B 구조의 특정 세부사항에 대해서는 자세히 다루지 않을 것입니다. 다만, 이는 스마트 컨트랙트가 외부 로그로 사용하기 위해 보낼 수 있는 외부 메시지 유형입니다. 이 형식의 예시는 Elector 컨트랙트를 살펴보세요.
TL-B를 살펴보면, ext_in_msg_info 유형과 함께 사용할 때는 CommonMsgInfo만 사용할 수 있음을 알 수 있습니다. 이는 src
, created_lt
, created_at
등과 같은 메시지 필드가 트랜잭션 처리 중에 검증자에 의해 다시 작성되기 때문입니다. 이 경우, 메시지의 src
필드가 가장 중요한데, 메시지를 보낼 때 발신자를 알 수 없고 검증 중에 검증자가 작성하기 때문입니다. 이렇게 하면 src
필드의 주소가 올바르며 조작할 수 없다는 것이 보장됩니다.
하지만 CommonMsgInfo
구조는 MsgAddress
사양만 지원하지만, 발신자의 주소는 일반적으로 알 수 없으며 addr_none
(두 개의 0비트 00
)을 작성해야 합니다. 이 경우 CommonMsgInfoRelaxed
구조가 사용되며, 이는 addr_none
주소를 지원합니다. ext_in_msg_info
(들어오는 외부 메시지에 사용됨)의 경우 발신자를 사용하지 않고 항상 MsgAddressExt 구조(addr_none$00
은 두 개의 0비트를 의미)를 사용하기 때문에 CommonMsgInfo
구조가 사용됩니다. 이는 데이터를 덮어쓸 필요가 없다는 것을 의미합니다.
$
기호 뒤의 숫자는 특정 구조를 식별하기 위해 읽기(역직렬화) 중에 이러한 구조의 시작 부분에 저장해야 하는 비트입니다.
내부 메시지 생성
내부 메시지는 컨트랙트 간에 메시지를 보내는 데 사용됩니다. NFT와 Jeton과 같은 다양한 컨트랙트 유형을 분석할 때 메시지를 보내는 컨트랙트 작성을 고려하면 다음과 같은 코드 라인이 자주 사용됩니다:
var msg = begin_cell()
.store_uint(0x18, 6) ;; or 0x10 for non-bounce
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
;; store something as a body
먼저 0x18
과 0x10
(x - 16진수)을 고려해보겠습니다. 이는 다음과 같은 방식으로 배치된 16진수입니다(6비트를 할당한다고 가정): 011000
과 010000
. 이는 위의 코드를 다음과 같이 다시 쓸 수 있다는 것을 의미합니다:
var msg = begin_cell()
.store_uint(0, 1) ;; this bit indicates that we send an internal message according to int_msg_info$0
.store_uint(1, 1) ;; IHR Disabled
.store_uint(1, 1) ;; or .store_uint(0, 1) for 0x10 | bounce
.store_uint(0, 1) ;; bounced
.store_uint(0, 2) ;; src -> two zero bits for addr_none
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
;; store something as a body
이제 각 옵션을 자세히 살펴보겠습니다:
옵션 | 설명 |
---|---|
IHR Disabled | 현재 이 옵션은 비활성화되어 있습니다(즉, 1을 저장). Instant Hypercube Routing이 완전히 구현되지 않았기 때문입니다. 또한 네트워크에 많은 수의 Shardchains가 라이브될 때 이것이 필요할 것입니다. IHR Disabled 옵션에 대해 자세히 알아보려면 tblkch.pdf (2장)를 참조하세요. |
Bounce | 메시지를 보내는 동안 스마트 컨트랙트 처리 중에 다양한 오류가 발생할 수 있습니다. TON을 잃지 않기 위해 Bounce 옵션을 1(true)로 설정해야 합니다. 이 경우 트랜잭션 처리 중에 컨트랙트 오류가 발생하면 메시지가 발신자에게 반환되고 수수료를 제외한 동일한 양의 TON이 수신됩니다. 비바운스 메시지에 대해 자세히 알아보려면 여기를 참조하세요. |
Bounced | Bounced 메시지는 스마트 컨트랙트로 트랜잭션을 처리하는 동안 오류가 발생해 발신자에게 반환되는 메시지입니다. 이 옵션은 수신된 메시지가 바운스된 것인지 여부를 알려줍니다. |
Src | Src는 발신자 주소입니다. 이 경우 addr_none 주소를 나타내기 위해 두 개의 0비트가 작성됩니다. |
다음 두 줄의 코드:
...
.store_slice(to_address)
.store_coins(amount)
...
- 수신자와 보낼 TON 수를 지정합니다.
마지막으로 남은 코드 라인을 살펴보겠습 니다:
...
.store_uint(0, 1) ;; Extra currency
.store_uint(0, 4) ;; IHR fee
.store_uint(0, 4) ;; Forwarding fee
.store_uint(0, 64) ;; Logical time of creation
.store_uint(0, 32) ;; UNIX time of creation
.store_uint(0, 1) ;; State Init
.store_uint(0, 1) ;; Message body
;; store something as a body
옵션 | 설명 |
---|---|
Extra currency | 이것은 기존 jeton의 네이티브 구현이며 현재 사용되지 않습니다. |
IHR fee | 앞서 언급했듯이 IHR은 현재 사용되지 않으므로 이 수수료는 항상 0입니다. 이에 대해 자세히 알아보려면 tblkch.pdf (3.1.8)를 참조하세요. |
Forwarding fee | 메시지 전달 수수료입니다. 자세한 내용은 수수료 문서에서 확인할 수 있습니다. |
Logical time of creation | 올바른 메시지 대기열을 생성 하는 데 사용되는 시간입니다. |
UNIX time of creation | UNIX에서 메시지가 생성된 시간입니다. |
State Init | 스마트 컨트랙트 배포를 위한 코드와 소스 데이터입니다. 비트가 0 으로 설정되어 있으면 State Init이 없다는 것을 의미합니다. 하지만 1 로 설정된 경우 State Init이 동일한 cell(0)에 저장되어 있는지 또는 참조(1)로 작성되었는지를 나타내는 다른 비트를 작성해야 합니다. |
Message body | 이 부분은 메시지 본문이 어떻게 저장되는지 정의합니다. 때로는 메시지 본문이 너무 커서 메시지 자체에 맞지 않습니다. 이 경우 참조로 저장해야 하며 비트는 1 로 설정되어 본문이 참조로 사용됨을 나타냅니다. 비트가 0 이면 본문은 메시지와 동일한 cell에 있습니다. |
State Init과 Message Body 비트를 제외한 위의 값들(src 포함)은 검증자에 의해 다시 작성됩니다.
값이 지정된 것보다 더 적은 비트에 맞는 경우 누락된 0이 값의 왼쪽에 추가됩니다. 예를 들어 0x18은 5비트에 맞습니다 -> 11000
. 하지만 6비트가 지정되었으므로 최종 결과는 011000
이 됩니다.
다음으로, 다른 지갑 v3로 Toncoin을 보낼 메시지를 준비하기 시작하겠습니다. 먼저, 사용자가 "Hello, TON!"이라는 텍스트와 함께 자신에게 0.5 TON을 보내고 싶다고 가정해 봅시다. 주석이 있는 메시지를 보내는 방법을 참조하세요.
- JavaScript
- Golang
import { beginCell } from '@ton/core';
let internalMessageBody = beginCell()
.storeUint(0, 32) // write 32 zero bits to indicate that a text comment will follow
.storeStringTail("Hello, TON!") // write our text comment
.endCell();
import (
"github.com/xssnick/tonutils-go/tvm/cell"
)
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32). // write 32 zero bits to indicate that a text comment will follow
MustStoreStringSnake("Hello, TON!"). // write our text comment
EndCell()
위에서 우리의 메시지 본문이 저장된 InternalMessageBody
를 만들었습니다. 텍스트가 단일 Cell(1023비트)에 맞지 않을 때는 다음 문서에 따라 데이터를 여러 cell로 분할해야 한다는 점에 유의하세요. 하지만 이 경우 고수준 라이브러리가 요구사항에 따라 cell을 생성하므로 이 단계에서는 걱정할 필요가 없습니다.
다음으로, 앞서 학습한 정보에 따라 InternalMessage
를 생성합니다:
- JavaScript
- Golang
import { toNano, Address } from '@ton/ton';
const walletAddress = Address.parse('put your wallet address');
let internalMessage = beginCell()
.storeUint(0, 1) // indicate that it is an internal message -> int_msg_info$0
.storeBit(1) // IHR Disabled
.storeBit(1) // bounce
.storeBit(0) // bounced
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress)
.storeCoins(toNano("0.2")) // amount
.storeBit(0) // Extra currency
.storeCoins(0) // IHR Fee
.storeCoins(0) // Forwarding Fee
.storeUint(0, 64) // Logical time of creation
.storeUint(0, 32) // UNIX time of creation
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(internalMessageBody) // Store Message Body as a reference
.endCell();
import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
)
walletAddress := address.MustParseAddr("put your address")
internalMessage := cell.BeginCell().
MustStoreUInt(0, 1). // indicate that it is an internal message -> int_msg_info$0
MustStoreBoolBit(true). // IHR Disabled
MustStoreBoolBit(true). // bounce
MustStoreBoolBit(false). // bounced
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress).
MustStoreCoins(tlb.MustFromTON("0.2").NanoTON().Uint64()). // amount
MustStoreBoolBit(false). // Extra currency
MustStoreCoins(0). // IHR Fee
MustStoreCoins(0). // Forwarding Fee
MustStoreUInt(0, 64). // Logical time of creation
MustStoreUInt(0, 32). // UNIX time of creation
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(internalMessageBody). // Store Message Body as a reference
EndCell()
메시지 생성
지갑 스마트 컨트랙트의 seqno
(시퀀스 넘버)를 검색해야 합니다. 이를 위해 우리 지갑의 Get 메서드 "seqno"를 실행하는 요청을 보내는 데 사용할 Client
를 생성합니다. 또한 메시지에 서명하기 위해 시드 문구(여기에서 지갑 생성 시 저장한)를 추가해야 합니다:
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
import { mnemonicToWalletKey } from '@ton/crypto';
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC", // you can replace it on https://testnet.toncenter.com/api/v2/jsonRPC for testnet
apiKey: "put your api key" // you can get an api key from @tonapibot bot in Telegram
});
const mnemonic = 'put your mnemonic'; // word1 word2 word3
let getMethodResult = await client.runMethod(walletAddress, "seqno"); // run "seqno" GET method from your wallet contract
let seqno = getMethodResult.stack.readNumber(); // get seqno from response
const mnemonicArray = mnemonic.split(' '); // get array from string
const keyPair = await mnemonicToWalletKey(mnemonicArray); // get Secret and Public keys from mnemonic
import (
"context"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"golang.org/x/crypto/pbkdf2"
"log"
"strings"
)
mnemonic := strings.Split("put your mnemonic", " ") // get our mnemonic as array
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection) // create client
block, err := client.CurrentMasterchainInfo(context.Background()) // get current block, we will need it in requests to LiteServer
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
getMethodResult, err := client.RunGetMethod(context.Background(), block, walletAddress, "seqno") // run "seqno" GET method from your wallet contract
if err != nil {
log.Fatalln("RunGetMethod err:", err.Error())
return
}
seqno := getMethodResult.MustInt(0) // get seqno from response
// The next three lines will extract the private key using the mnemonic phrase. We will not go into cryptographic details. With the tonutils-go library, this is all implemented, but we’re doing it again to get a full understanding.
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
privateKey := ed25519.NewKeyFromSeed(k)
따라서 seqno
, keys
및 internal message
가 있으며 이를 보내야 합니다. 이제 우리 지갑을 위한 메시지를 생성하고 튜토리얼 시작부분에서 사용된 순서대로 이 메시지에 데이터를 저장해야 합니다. 이는 다음과 같이 수행됩니다:
- JavaScript
- Golang
import { sign } from '@ton/crypto';
let toSign = beginCell()
.storeUint(698983191, 32) // subwallet_id | We consider this further
.storeUint(Math.floor(Date.now() / 1e3) + 60, 32) // Message expiration time, +60 = 1 minute
.storeUint(seqno, 32) // store seqno
.storeUint(3, 8) // store mode of our internal message
.storeRef(internalMessage); // store our internalMessage as a reference
let signature = sign(toSign.endCell().hash(), keyPair.secretKey); // get the hash of our message to wallet smart contract and sign it to get signature
let body = beginCell()
.storeBuffer(signature) // store signature
.storeBuilder(toSign) // store our message
.endCell();
import (
"time"
)
toSign := cell.BeginCell().
MustStoreUInt(698983191, 32). // subwallet_id | We consider this further
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32). // Message expiration time, +60 = 1 minute
MustStoreUInt(seqno.Uint64(), 32). // store seqno
MustStoreUInt(uint64(3), 8). // store mode of our internal message
MustStoreRef(internalMessage) // store our internalMessage as a reference
signature := ed25519.Sign(privateKey, toSign.EndCell().Hash()) // get the hash of our message to wallet smart contract and sign it to get signature
body := cell.BeginCell().
MustStoreSlice(signature, 512). // store signature
MustStoreBuilder(toSign). // store our message
EndCell()
여기서 toSign
정의에 .endCell()
을 사용하지 않았다는 점에 주목하세요. 이 경우 toSign 내용을 메시지 본문에 직접 전달해야 하기 때문입니다. cell을 작성해야 하는 경우라면 참조로 저장해야 했을 것입니다.
Wallet V3에 대해 아래에서 배웠던 기본 검증 프로세스 외에도, Wallet V4 스마트 컨트랙트는 간단한 번역인지 또는 플러그인과 관련된 메시지인지 결정하기 위해 opcode를 추출합니다. 이 버전과 일치시키려면 seqno(시퀀스 넘버)를 작성한 후와 트랜잭션 모드를 지정하기 전에 storeUint(0, 8).
(JS/TS), MustStoreUInt(0, 8).
(Golang) 함수를 추가해야 합니다.
외부 메시지 생성
외부 세계에서 블록체인으로 내부 메시지를 전달하려면 외부 메시지 내에서 보내야 합니다. 이전에 살펴본 바와 같이 계약에 외부 메시지를 보내는 것이 목표이므로 ext_in_msg_info$10
구조만 사용해야 합니다. 이제 우리의 지갑에 보낼 외부 메시지를 생성해보겠습니다:
- JavaScript
- Golang
let externalMessage = beginCell()
.storeUint(0b10, 2) // 0b10 -> 10 in binary
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress) // Destination address
.storeCoins(0) // Import Fee
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(body) // Store Message Body as a reference
.endCell();
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // 0b10 -> 10 in binary
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress). // Destination address
MustStoreCoins(0). // Import Fee
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(body). // Store Message Body as a reference
EndCell()
옵션 | 설명 |
---|---|
Src | 발신자 주소. 들어오는 외부 메시지는 발신자를 가질 수 없으므로 항상 2개의 0비트(addr_none TL-B)가 됩니다. |
Import Fee | 들어오는 외부 메시지 가져오기에 사용되는 수수료입니다. |
State Init | 내부 메시지와 달리 외부 메시지의 State Init은 외부 세계에서 컨트랙트를 배포하는 데 필요합니다. 내부 메시지와 함께 사용되는 State Init을 통해 한 컨트랙트가 다른 컨트랙트를 배포할 수 있습니다. |
Message Body | 처리를 위해 컨트랙트에 보내야 하는 메시지입니다. |
0b10(b - 이진수)는 이진 기록을 나타냅니다. 이 과정에서 두 개의 비트가 저장됩니다: 1
과 0
. 이렇게 하여 ext_in_msg_info$10
임을 지정합니다.
이제 우리 컨트랙트에 보낼 준비가 완료된 메시지가 있습니다. 이를 수행하려면 먼저 BOC
(Bag of Cells)로 직렬화한 다음 다음 코드를 사용하여 보내야 합니다:
- JavaScript
- Golang
console.log(externalMessage.toBoc().toString("base64"))
client.sendFile(externalMessage.toBoc());
import (
"encoding/base64"
"github.com/xssnick/tonutils-go/tl"
)
log.Println(base64.StdEncoding.EncodeToString(externalMessage.ToBOCWithFlags(false)))
var resp tl.Serializable
err = client.Client().QueryLiteserver(context.Background(), ton.SendMessage{Body: externalMessage.ToBOCWithFlags(false)}, &resp)
if err != nil {
log.Fatalln(err.Error())
return
}
💡 유용한 링크:
결과적으로 콘솔에 BOC 출력을 얻었고 메시지가 우리 지갑으로 보내졌습니다. base64로 인코딩된 문자열을 복사하면 toncenter를 사용하여 수동으로 메시지를 보내고 해시를 검색할 수 있습니다.
👛 지갑 배포
우리는 메시지 생성의 기본 사항을 배웠으며, 이제 이는 지갑을 배포하는 데 도움이 될 것입니다. 과거에는 지갑 앱을 통해 지갑을 배포했지만, 이번에는 수동으로 지갑을 배포해야 합니다.
이 섹션에서는 처음부터 지갑(wallet v3)을 생성하는 방법을 알아볼 것입니다. 지갑 스마트 컨트랙트 용 코드를 컴파일하고, 니모닉 문구를 생성하고, 지갑 주소를 받고, 외부 메시지와 State Init(상태 초기화)를 사용하여 지갑을 배포하는 방법을 배우게 됩니다.
니모닉 생성
지갑을 올바르게 생성하는 데 필요한 첫 번째 것은 private
와 public
키를 검색하는 것입니다. 이 작업을 수행하려면 니모닉 시드 문구를 생성한 다음 암호화 라이브러리를 사용하여 개인 키와 공개 키를 추출해야 합니다.
이는 다음과 같이 수행됩니다:
- JavaScript
- Golang
import { mnemonicToWalletKey, mnemonicNew } from '@ton/crypto';
// const mnemonicArray = 'put your mnemonic'.split(' ') // get our mnemonic as array
const mnemonicArray = await mnemonicNew(24); // 24 is the number of words in a seed phrase
const keyPair = await mnemonicToWalletKey(mnemonicArray); // extract private and public keys from mnemonic
console.log(mnemonicArray) // if we want, we can print our mnemonic
import (
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"log"
"github.com/xssnick/tonutils-go/ton/wallet"
"golang.org/x/crypto/pbkdf2"
"strings"
)
// mnemonic := strings.Split("put your mnemonic", " ") // get our mnemonic as array
mnemonic := wallet.NewSeed() // get new mnemonic
// The following three lines will extract the private key using the mnemonic phrase. We will not go into cryptographic details. It has all been implemented in the tonutils-go library, but it immediately returns the finished object of the wallet with the address and ready methods. So we’ll have to write the lines to get the key separately. Goland IDE will automatically import all required libraries (crypto, pbkdf2 and others).
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
// 32 is a key len
privateKey := ed25519.NewKeyFromSeed(k) // get private key
publicKey := privateKey.Public().(ed25519.PublicKey) // get public key from private key
log.Println(publicKey) // print publicKey so that at this stage the compiler does not complain that we do not use our variable
log.Println(mnemonic) // if we want, we can print our mnemonic
개인 키는 메시지에 서명하는 데 필요하고 공개 키는 지갑의 스마트 컨트랙트에 저장됩니다.
생성된 니모닉 시드 문구를 콘솔에 출력한 다음 저장하여 지갑의 코드를 실행할 때마다 동일한 키 쌍을 사용할 수 있도록 해야 합니다(이전 섹션에서 자세히 설명함).
서브월렛 ID
지갑이 스마트 컨트랙트라는 가장 주목할 만한 이점 중 하나는 단 하나의 개인 키를 사용하여 매우 많은 수의 지갑을 생성할 수 있다는 것입니다. 이는 TON 블록체인의 스마트 컨트랙트 주소가 stateInit
을 포함한 여러 요소를 사용하여 계산되기 때문입니다. stateInit에는 블록체인의 스마트 컨트랙트 저장소에 저장되는 code
와 초기 데이터
가 포함됩니다.
stateInit 내의 단 하나의 비트만 변경 해도 다른 주소가 생성될 수 있습니다. 그래서 subwallet_id
가 처음에 생성되었습니다. subwallet_id
는 컨트랙트 저장소에 저장되며 하나의 개인 키로 많은 다른 지갑(다른 subwallet ID를 가진)을 만드는 데 사용될 수 있습니다. 이 기능은 거래소와 같은 중앙집중식 서비스와 다양한 유형의 지갑을 통합할 때 매우 유용할 수 있습니다.
기본 subwallet_id 값은 코드 라인에 따라 698983191
입니다:
res.wallet_id = td::as<td::uint32>(res.config.zero_state_id.root_hash.as_slice().data());
설정 파일에서 제네시스 블록 정보(zero_state)를 검색할 수 있습니다. 이에 대한 복잡성과 세부사항을 이해할 필요는 없지만 subwallet_id
의 기본값이 698983191
이라는 것을 기억하는 것이 중요합니다.
각 지갑 컨트랙트는 다른 ID를 가진 지갑으로 보낸 요청이 있을 때를 피하기 위해 외부 메시지의 subwallet_id 필드를 확인합니다:
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
throw_unless(34, subwallet_id == stored_subwallet);
컨트랙트의 초기 데이터에 위 값을 추가해야 하므로 다음과 같이 변수를 저장해야 합니다:
- JavaScript
- Golang
const subWallet = 698983191;
var subWallet uint64 = 698983191
지갑 코드 컴파일
이제 private와 public 키, subwallet_id가 명확하게 정의되었으므로 지갑 코드를 컴파일해야 합니다. 이를 위해 공식 저장소에서 wallet v3 코드를 사용할 것입니다.
지갑 코드를 컴파일하려면 @ton-community/func-js 라이브러리를 사용해야 합니다.
이 라이브러리를 사용하면 FunC 코드를 컴파일하고 코드가 포함된 cell을 검색할 수 있습니다. 시작하려면 라이브러리를 설치하고 package.json
에 저장(--save)해야 합니다:
npm i --save @ton-community/func-js
코드를 컴파일하는 데는 자바스크립트만 사용할 것입니다. 코드 컴파일을 위한 라이브러리가 자바스크립트 기반이기 때문입니다. 하지만 컴파일이 완료되고 cell의 base64 출력이 있으면 이 컴파일된 코드를 Go와 같은 다른 언어에서 사용할 수 있습니다.
먼저 wallet_v3.fc
와 stdlib.fc
두 파일을 생성해야 합니다. 컴파일러는 stdlib.fc 라이브러리와 함께 작동합니다. asm
명령어와 일치하는 모든 필수적이고 기본적인 함수들이 이 라이브러리에서 생성되었습니다. stdlib.fc 파일은 여기에서 다운로드할 수 있습니다. wallet_v3.fc
파일에는 위의 코드를 복사해야 합니다.
이제 우리가 만들고 있는 프로젝트에 대해 다음과 같은 구조를 가지게 됩니다:
.
├── src/
│ ├── main.ts
│ ├── wallet_v3.fc
│ └── stdlib.fc
├── nodemon.json
├── package-lock.json
├── package.json
└── tsconfig.json
IDE 플러그인이 stdlib.fc
파일의 () set_seed(int) impure asm "SETRAND";
와 충돌하더라도 괜찮습니다.
아래의 함수들이 사용될 것임을 나타내기 위해 wallet_v3.fc
파일의 시작 부분에 다음 줄을 추가하는 것을 잊지 마세요:
#include "stdlib.fc";
이제 스마트 컨트랙트를 컴파일하고 npm run start:dev
를 사용하여 실행하는 코드를 작성해보겠습니다:
import { compileFunc } from '@ton-community/func-js';
import fs from 'fs'; // we use fs for reading content of files
import { Cell } from '@ton/core';
const result = await compileFunc({
targets: ['wallet_v3.fc'], // targets of your project
sources: {
"stdlib.fc": fs.readFileSync('./src/stdlib.fc', { encoding: 'utf-8' }),
"wallet_v3.fc": fs.readFileSync('./src/wallet_v3.fc', { encoding: 'utf-8' }),
}
});
if (result.status === 'error') {
console.error(result.message)
return;
}
const codeCell = Cell.fromBoc(Buffer.from(result.codeBoc, "base64"))[0]; // get buffer from base64 encoded BOC and get cell from this buffer
// now we have base64 encoded BOC with compiled code in result.codeBoc
console.log('Code BOC: ' + result.codeBoc);
console.log('\nHash: ' + codeCell.hash().toString('base64')); // get the hash of cell and convert in to base64 encoded string. We will need it further
터미널에 다음과 같은 출력이 나타날 것입니다:
Code BOC: te6ccgEBCAEAhgABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQCW8oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOgwAaTIyx/LH8v/ye1UAATQMAIBSAYHABe7Oc7UTQ0z8x1wv/gAEbjJftRNDXCx+A==
Hash: idlku00WfSC36ujyK2JVT92sMBEpCNRUXOGO4sJVBPA=
이것이 완료되면 base64로 인코딩된 출력을 사용하여 다른 라이브러리와 언어에서 지갑 코드가 포함된 동일한 cell을 검색할 수 있습니다:
- Golang
import (
"encoding/base64"
"github.com/xssnick/tonutils-go/tvm/cell"
)
base64BOC := "te6ccgEBCAEAhgABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQCW8oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOgwAaTIyx/LH8v/ye1UAATQMAIBSAYHABe7Oc7UTQ0z8x1wv/gAEbjJftRNDXCx+A==" // save our base64 encoded output from compiler to variable
codeCellBytes, _ := base64.StdEncoding.DecodeString(base64BOC) // decode base64 in order to get byte array
codeCell, err := cell.FromBOC(codeCellBytes) // get cell with code from byte array
if err != nil { // check if there are any error
panic(err)
}
log.Println("Hash:", base64.StdEncoding.EncodeToString(codeCell.Hash())) // get the hash of our cell, encode it to base64 because it has []byte type and output to the terminal
터미널에 다음과 같은 출력이 나타날 것입니다:
idlku00WfSC36ujyK2JVT92sMBEpCNRUXOGO4sJVBPA=
위 과정이 완료된 후 해시가 일치하므로 우리 cell에 올바른 코드를 사용하고 있다는 것이 확인되었습니다.
배포를 위한 State Init 생성하기
메시지를 빌드하기 전에 State Init이 무엇인지 이해하는 것이 중요합니다. 먼저 TL-B 스키마를 살펴보겠습니다:
옵션 | 설명 |
---|---|
split_depth | 이 옵션은 여러 샤드체인에 분할되어 위치할 수 있는 고부하 스마트 컨트랙트를 위한 것입니다. 이 작동 방식에 대한 자세한 정보는 tblkch.pdf (4.1.6)에서 찾을 수 있습니다. 지갑 스마트 컨트랙트 내에서만 사용되므로 0 비트만 저장됩니다. |
special | TicTok에 사용됩니다. 이러한 스마트 컨트랙트는 각 블록마다 자동으로 호출되며 일반 스마트 컨트랙트에는 필요하지 않습니다. 이에 대한 정보는 이 섹션 또는 tblkch.pdf (4.1.6)에서 찾을 수 있습니다. 이러한 기능이 필요하지 않으므로 이 사양 내에서 0 비트만 저장됩니다. |
code | 1 비트는 참조로써 스마트 컨트랙트 코드의 존재를 의미합니다. |
data | 1 비트는 참조로써 스마트 컨트랙트 데이터의 존재를 의미합니다. |
library | 마스터체인에서 작동하며 다른 스마트 컨트랙트가 사용할 수 있는 라이브러리입니다. 이는 지갑에 사용되지 않으므로 비트 는 0 으로 설정됩니다. 이에 대한 정보는 tblkch.pdf (1.8.4)에서 찾을 수 있습니다. |
다음으로 배포 직후 우리 컨트랙트의 저장소에 있을 초기 데이터
를 준비해 보겠습니다:
- JavaScript
- Golang
import { beginCell } from '@ton/core';
const dataCell = beginCell()
.storeUint(0, 32) // Seqno
.storeUint(698983191, 32) // Subwallet ID
.storeBuffer(keyPair.publicKey) // Public Key
.endCell();
dataCell := cell.BeginCell().
MustStoreUInt(0, 32). // Seqno
MustStoreUInt(698983191, 32). // Subwallet ID
MustStoreSlice(publicKey, 256). // Public Key
EndCell()
이 단계에서 컨트랙트의 code
와 초기 data
가 모두 준비되었습니다. 이 데이터로 우리는 지갑 주소를 생성할 수 있습니다. 지갑의 주소는 코드와 초기 데이터를 포함하는 State Init에 따라 달라집니다.
- JavaScript
- Golang
import { Address } from '@ton/core';
const stateInit = beginCell()
.storeBit(0) // No split_depth
.storeBit(0) // No special
.storeBit(1) // We have code
.storeRef(codeCell)
.storeBit(1) // We have data
.storeRef(dataCell)
.storeBit(0) // No library
.endCell();
const contractAddress = new Address(0, stateInit.hash()); // get the hash of stateInit to get the address of our smart contract in workchain with ID 0
console.log(`Contract address: ${contractAddress.toString()}`); // Output contract address to console
import (
"github.com/xssnick/tonutils-go/address"
)
stateInit := cell.BeginCell().
MustStoreBoolBit(false). // No split_depth
MustStoreBoolBit(false). // No special
MustStoreBoolBit(true). // We have code
MustStoreRef(codeCell).
MustStoreBoolBit(true). // We have data
MustStoreRef(dataCell).
MustStoreBoolBit(false). // No library
EndCell()
contractAddress := address.NewAddress(0, 0, stateInit.Hash()) // get the hash of stateInit to get the address of our smart contract in workchain with ID 0
log.Println("Contract address:", contractAddress.String()) // Output contract address to console
State Init을 사용하여 이제 메시지를 만들어 블록체인에 보낼 수 있습니다.
To carry out this process, a minimum wallet balance of 0.1 TON is required (the balance can be less, but this amount is guaranteed to be sufficient). To accomplish this, we’ll need to run the code mentioned earlier in the tutorial, obtain the correct wallet address, and send 0.1 TON to this address. Alternatively, you can send this sum manually via your wallet app before sending the deployment message itself.
외부 메시지를 통한 배포는 여기서는 주로 교육 목적으로 다룹니다. 실제로는 나중에 설명할 지갑을 통한 스마트 컨트랙트 배포가 훨씬 더 편리합니다.
이전 섹션에서 만든 것과 비슷한 메시지를 만들어 보겠습니다:
- JavaScript
- Golang
import { sign } from '@ton/crypto';
import { toNano } from '@ton/core';
const internalMessageBody = beginCell()
.storeUint(0, 32)
.storeStringTail("Hello, TON!")
.endCell();
const internalMessage = beginCell()
.storeUint(0x10, 6) // no bounce
.storeAddress(Address.parse("put your first wallet address from were you sent 0.1 TON"))
.storeCoins(toNano("0.03"))
.storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) // We store 1 that means we have body as a reference
.storeRef(internalMessageBody)
.endCell();
// message for our wallet
const toSign = beginCell()
.storeUint(subWallet, 32)
.storeUint(Math.floor(Date.now() / 1e3) + 60, 32)
.storeUint(0, 32) // We put seqno = 0, because after deploying wallet will store 0 as seqno
.storeUint(3, 8)
.storeRef(internalMessage);
const signature = sign(toSign.endCell().hash(), keyPair.secretKey);
const body = beginCell()
.storeBuffer(signature)
.storeBuilder(toSign)
.endCell();
import (
"github.com/xssnick/tonutils-go/tlb"
"time"
)
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32).
MustStoreStringSnake("Hello, TON!").
EndCell()
internalMessage := cell.BeginCell().
MustStoreUInt(0x10, 6). // no bounce
MustStoreAddr(address.MustParseAddr("put your first wallet address from were you sent 0.1 TON")).
MustStoreBigCoins(tlb.MustFromTON("0.03").NanoTON()).
MustStoreUInt(1, 1 + 4 + 4 + 64 + 32 + 1 + 1). // We store 1 that means we have body as a reference
MustStoreRef(internalMessageBody).
EndCell()
// message for our wallet
toSign := cell.BeginCell().
MustStoreUInt(subWallet, 32).
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32).
MustStoreUInt(0, 32). // We put seqno = 0, because after deploying wallet will store 0 as seqno
MustStoreUInt(3, 8).
MustStoreRef(internalMessage)
signature := ed25519.Sign(privateKey, toSign.EndCell().Hash())
body := cell.BeginCell().
MustStoreSlice(signature, 512).
MustStoreBuilder(toSign).
EndCell()
이제 올바른 State Init과 Message Body가 완성되었습니다.
외부 메시지 보내기
주요 차이점은 외부 메시지의 존재입니다. State Init은 올바른 컨트랙트 배포를 수행하기 위해 저장됩니다. 컨트랙트는 아직 자체 코드가 없기 때문에 내부 메시지를 처리할 수 없습니다. 따라서 "Hello, TON!" 코멘트가 포함된 메시지를 처리할 수 있도 록 성공적으로 배포된 후에 코드와 초기 데이터를 보냅니다:
- JavaScript
- Golang
const externalMessage = beginCell()
.storeUint(0b10, 2) // indicate that it is an incoming external message
.storeUint(0, 2) // src -> addr_none
.storeAddress(contractAddress)
.storeCoins(0) // Import fee
.storeBit(1) // We have State Init
.storeBit(1) // We store State Init as a reference
.storeRef(stateInit) // Store State Init as a reference
.storeBit(1) // We store Message Body as a reference
.storeRef(body) // Store Message Body as a reference
.endCell();
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // indicate that it is an incoming external message
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(contractAddress).
MustStoreCoins(0). // Import fee
MustStoreBoolBit(true). // We have State Init
MustStoreBoolBit(true). // We store State Init as a reference
MustStoreRef(stateInit). // Store State Init as a reference
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(body). // Store Message Body as a reference
EndCell()
마지막으로 우리 지갑을 배포하고 사용하기 위해 메시지를 블록체인에 보낼 수 있습니다.
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
apiKey: "put your api key" // you can get an api key from @tonapibot bot in Telegram
});
client.sendFile(externalMessage.toBoc());
import (
"context"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/tl"
"github.com/xssnick/tonutils-go/ton"
)
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection)
var resp tl.Serializable
err = client.Client().QueryLiteserver(context.Background(), ton.SendMessage{Body: externalMessage.ToBOCWithFlags(false)}, &resp)
if err != nil {
log.Fatalln(err.Error())
return
}
mode 3
으로 내부 메시지를 보냈다는 점에 주목하세요. 같은 지갑을 다시 배포해야 하는 경우, 스마트 컨트랙트를 제거할 수 있습니다. 이를 위해서는 128(스마트 컨트랙트의 전체 잔액 가져오기) + 32(스마트 컨트랙트 제거)를 더해 160
으로 mode를 설정하면 됩니다. 이렇게 하면 남은 TON 잔액을 회수하고 지갑을 다시 배포할 수 있습니다.
각각의 새로운 트랜잭션에서 seqno를 1씩 증가시켜야 한다는 점을 기억하세요.
💸 지갑 스마트 컨트랙트 작업하기
이 튜토리얼의 전반부를 완료했으므로 이제 지갑 스마트 컨트랙트가 어떻게 개발되고 사용되는지 훨씬 더 잘 알게 되었습니다. 우리는 그것들을 배포하고 제거하는 방법과 사전 구성된 라이브러리 함수에 의존하지 않고 메시지를 보내는 방법을 배웠습니다. 위에서 배운 내용을 더 적용하기 위해 다음 섹션에서는 더 복잡한 메시지를 만들고 보내는 데 초점을 맞추겠습니다.
여러 메시지 동 시에 보내기
이미 알고 있듯이, 하나의 셀은 최대 1023비트의 데이터와 다른 셀에 대한 최대 4개의 참조를 저장할 수 있습니다. 튜토리얼의 첫 번째 섹션에서 내부 메시지가 '전체' 루프로 링크로 전달되고 전송되는 방법을 자세히 설명했습니다. 이것은 외부 메시지 내에 최대 4개의 내부 메시지를 저장할 수 있다는 것을 의미합니다. 이를 통해 한 번에 4개의 메시지를 보낼 수 있습니다.
이를 위해서는 4개의 서로 다른 내부 메시지를 만들어야 합니다. 이것은 수동으로 하거나 loop
를 통해 할 수 있습니다. TON 금액의 배열, 코멘트의 배열, 메시지의 배열 이렇게 3개의 배열이 필요합니다. 메시지를 위해서는 또 다른 배열인 internalMessages를 준비해야 합니다.
- JavaScript
- Golang
import { Cell } from '@ton/core';
const internalMessagesAmount = ["0.01", "0.02", "0.03", "0.04"];
const internalMessagesComment = [
"Hello, TON! #1",
"Hello, TON! #2",
"", // Let's leave the third message without comment
"Hello, TON! #4"
]
const destinationAddresses = [
"Put any address that belongs to you",
"Put any address that belongs to you",
"Put any address that belongs to you",
"Put any address that belongs to you"
] // All 4 addresses can be the same
let internalMessages:Cell[] = []; // array for our internal messages
import (
"github.com/xssnick/tonutils-go/tvm/cell"
)
internalMessagesAmount := [4]string{"0.01", "0.02", "0.03", "0.04"}
internalMessagesComment := [4]string{
"Hello, TON! #1",
"Hello, TON! #2",
"", // Let's leave the third message without comment
"Hello, TON! #4",
}
destinationAddresses := [4]string{
"Put any address that belongs to you",
"Put any address that belongs to you",
"Put any address that belongs to you",
"Put any address that belongs to you",
} // All 4 addresses can be the same
var internalMessages [len(internalMessagesAmount)]*cell.Cell // array for our internal messages
모든 메시지의 전송 모드는 mode 3