TON 离线签名
脚手架创建项目
env CXXFLAGS="-std=c++17" pnpm create ts-app@latest sol1
- 报错1: Error: not found: python2
- 报错2: npm error gyp verb extracted file from tarball include/node/v8-callbacks.h
- 报错3: /v8-internal.h:492:38: error: ‘remove_cv_t’ is not a member of ‘std’
服务器环境是Ubuntu20.04,报错1安装python2解决,报错2因为是node版本20太高了降低到16解决,报错3通过export CXXFLAGS="-std=c++17"解决
以下是 nvm 从 20 切换到 16 版本的实战演示(dapplink目前大部分项目用16 合约项目用的可能18,20比较新)
w@w:~$ nvm current
v20.15.1
w@w:~$ which node
/home/w/.nvm/versions/node/v20.15.1/bin/node
w@w:~$ nvm alias default 16
default -> 16 (-> v16.20.2)
依然报错,鉴定完毕 ts-app 这个脚手架不能用,还是自己写个 ts 项目的脚手架(含单元测试) vscode 需要安装 jest 插件运行测试
scaffold.sh:
#!/bin/bash
set -exu
dir=$1
mkdir $dir
cd $dir
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init # create tsconfig.json
mkdir src
#touch src/index.ts
echo "export const sum = (a: number, b: number): number => {
return a + b;
};" > src/index.ts
npm install jest ts-jest @types/jest --save-dev
npx ts-jest config:init # create jest.config.js
mkdir tests
echo "
import { sum } from '../src';
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
" > tests/example.test.ts
jq '.scripts += {"build": "tsc", "test": "jest"}' package.json > tmp.json && mv tmp.json package.json
echo "
node_modules
package-lock.json
" > .gitignore
git init .
sol开发依赖库
npm install "@solana/web3.js" "@solana/spl-token" "ed25519-hd-key" "bs58"
(ton/sol 钱包库写在了同一个项目里)
定义所需接口
为什么 solana web3.js 源码中完全不用ts的抽象类而是用interface? 听上去抽象类都是已经过时要被淘汰的东西,用起来不灵活
- 支持多继承: C++和Python 但要防止菱形继承
- 不支持多继承用interface: TypeScript,Java,Ruby
算了先不写interface了
ed25516-hd-key 版本报错
import { derivePath, getPublicKey } from "ed25519-hd-key"
export function createTonAddress(seedHex: string, addressIndex: number) {
const key = derivePath(`m/44'/607'/0'/0'/${addressIndex}`, seedHex)
// getPublicKey: (privateKey: Buffer, withZeroByte?: boolean) => Buffer
const publicKey = getPublicKey(new Uint8Array(key), false) // .toString("hex")
}
src/ton.ts:15:36 - error TS2345: Argument of type 'Uint8Array' is not assignable to parameter of type 'Buffer'.
Type 'Uint8Array' is missing the following properties from type 'Buffer': write, toJSON, equals, compare, and 66 more.
15 const publicKey = getPublicKey(new Uint8Array(key), false) // .toString("hex")
~~~~~~~~~~~~~~~~~~~
src/ton.ts:15:51 - error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'Keys' is not assignable to parameter of type 'ArrayBufferLike'.
15 const publicKey = getPublicKey(new Uint8Array(key), false) // .toString("hex")
~~~
"ed25519-hd-key": "^1.3.0"
版本改成 "ed25519-hd-key": "^1.2.0"
(好吧 tonweb 依赖 1.3 不能改)
其实只需要改 const publicKey = getPublicKey(key.key, false)
地址打印成 promise
奇葩的是 tonweb 库 pubkey->addr 的函数 getAddress(): Promise<Address>
居然是个 promise 要 await 明明就是个CPU密集型任务
没办法jest测试用例和createTonAddress要改成异步函数,改了之后还是打印出promise像是我的函数没有被执行/生成地址的函数里面的log都没打印, 原来是vscode改了ts代码不会自动编译,手动npm run build之后再执行就能正确打印出地址了
准备 TON 钱包
TON 的钱包软件大多都是24词,用的比较多的是tonkeeper 不过仅支持助记词导入,不像 ok 钱包同时支持助记词和私钥导入
导入私钥到ok钱包
由于ton钱包私钥格式大多是 ed25516 32byte私钥 拼接上 32byte公钥 的 hexStr
所以导入到ok的时候要截取前32byte
助记词->地址
export async function createTonAddress(seedHex: string, addressIndex: number): Promise<string> {
console.log("createTonAddress")
const path = `m/44'/607'/0'/0'/${addressIndex}'`
console.info(`path=${path}`)
// const key = derivePath("m/44'/607'/0'/" + addressIndex + "'", seedHex)
const key = derivePath(path, seedHex)
// getPublicKey: (privateKey: Buffer, withZeroByte?: boolean) => Buffer
const publicKey = getPublicKey(key.key, false) // .toString("hex")
return await pubkeyToAddr(publicKey)
// return {
// privateKey: (),
// publicKey: (),
// address: walletAddress.toString()
// }
}
test('get ton addr', async () => {
// expect(sum(1, 2)).toBe(3);
// generateMnemonic() 默认是 12 个英文词
// generateMnemonic(12) 会报错提示有些参数没传,传参数要么都传入
// TON 的钱包软件大多都是24词
// const m = generateMnemonic(256)
const m = bip39.generateMnemonic(256, null, bip39.wordlists.english)
console.info(`m=${m}`)
expect(m.split(' ').length).toBe(24)
const seed = mnemonicToSeedSync(m)
expect(seed.length).toBe(64)
const seedHex = seed.toString('hex');
const addr = await createTonAddress(seedHex, 0)
console.info(addr)
// assert(verifyAddr(addr))
});
运行 jest 测试用例的办法:
node node_modules/jest/bin/jest.js tests/ton.test.ts -t 'get ton addr'
如果 jest 之前 npm install -g 过,可以简写成
jest tests/ton.test.ts -t 'get ton addr'
js生成离线签名
const keyPair = tonweb.utils.nacl.sign.keyPair.fromSecretKey(TonWeb.utils.hexToBytes(privateKey))
const { publicKey, secretKey } = keyPair;
console.info(`publicKey=${TonWeb.utils.bytesToHex(publicKey)}`)
const WalletClass = tonweb.wallet.all.v3R2;
const wallet = new WalletClass(tonweb.provider, {
publicKey: publicKey,
});
// const seqno = await wallet.methods.seqno().call() as number;
// console.info(`seqno = ${seqno}`)
// https://toncenter.com/api/v2/getWalletInformation?address=EQAUAHcUab66DpOV2GaT_QDuSagpMdIn0x6aMmO3_fPVMyD8
const seqno = 7
assert(seqno !== null)
const memo = "8701657"
const toAddress = 'EQD5vcDeRhwaLgAvralVC7sJXI-fc2aNcMUXqcx-BQ-OWnOZ';
const amount = TonWeb.utils.toNano('0.001'); // 0.001 TON in nanoton
// TODO 检查 amount2 是否为整数
const transfer = wallet.methods.transfer({
secretKey: secretKey,
toAddress: toAddress,
amount: amount,
seqno: seqno,
payload: memo, // Optional payload
sendMode: 3, // Default send mode
});
const txdata = await transfer.getQuery()
const boc = await txdata.toBoc()
// TON 的交易哈希是事先算出来的
const txhash = await txdata.hash()
const offlineSignedTx = {
"boc": TonWeb.utils.bytesToBase64(boc),
"txhash": TonWeb.utils.bytesToBase64(txhash)
}
console.info("offlineSignedTx", offlineSignedTx)
输出示例
offlineSignedTx {
boc: 'te6ccsEBAgEAswAAcwHfiAAmZVAVnov4puH1FtQjhNdQ45YuUrxt4yikM1AaXglncAf/0TbyklCBGDfr0m5tfPDGmogBlcPyzA1ckGkDSVjEaVUJHlvRU0L3sCivIj5zdHVY6LQqcj6O/AF+seA4NNhRTU0YuzSk2kgAAAA4HAEAfGIAfN7gbyMODRcAF9bUqoXdhK5Hz7mzRrhii9TmPwKHxy0YehIAAAAAAAAAAAAAAAAAAAAAAAA4NzAxNjU36Dh8zw==',
txhash: '+Su+6VQK1NAl6YIUjj2I2LI8j9x/Tx7A+ZJTQCkNnBU='
}
tonweb库报错多太难用
但是用tonweb库发送交易到rpc节点就各种报错,我尝试下别的C++/Rust/go的库去发送交易
编译TON C++源码
/usr/include/c++/9/exception:102:8: note: declared here
102 | bool uncaught_exception() _GLIBCXX_USE_NOEXCEPT __attribute__ ((__pure__));
| ^~~~~~~~~~~~~~~~~~
In file included from /root/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/tonlib-sys-2024.6.1/ton/tdutils/td/utils/logging.cpp:26:
根据 TON 文档,ubuntu 上编译需要使用 llvm clang-16
[env]
# ton C++ source require clang 16
CC = { value = "/usr/bin/clang-16", force = true }
CXX = { value = "/usr/bin/clang++-16", force = true }
[target.x86_64-unknown-linux-gnu]
linker="clang"
rustflags = ["-Clink-arg=-fuse-ld=/usr/bin/ld.lld-16"]
ld.lld-16: error: undefined symbol: secp256k1_context_create
>>> referenced by secp256k1.cpp
>>> secp256k1.cpp.o:(td::ecrecover(unsigned char const*, unsigned char const*, unsigned char*)) in archive /root/wallet-rust-sdk/target/debug/deps/libtonlib_sys-0da826f5fa314f52.rlib
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Go发送交易示例
参考了代码 https://github.com/xssnick/tonutils-go/blob/master/example/wallet/main.go
我作出改动如下:
diff --git a/example/wallet/main.go b/example/wallet/main.go
index d7e8464..3524788 100644
--- a/example/wallet/main.go
+++ b/example/wallet/main.go
@@ -3,8 +3,8 @@ package main
import (
"context"
"encoding/base64"
+ "encoding/hex"
"log"
- "strings"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/liteclient"
@@ -17,7 +17,7 @@ func main() {
client := liteclient.NewConnectionPool()
// get config
- cfg, err := liteclient.GetConfigFromUrl(context.Background(), "https://ton.org/testnet-global.config.json")
+ cfg, err := liteclient.GetConfigFromUrl(context.Background(), "https://ton.org/global-config.json")
if err != nil {
log.Fatalln("get config err: ", err.Error())
return
@@ -38,9 +38,17 @@ func main() {
ctx := client.StickyContext(context.Background())
- w, err := wallet.FromSeed(api, words, wallet.V4R2)
+ // UQAUAHcUab66DpOV2GaT_QDuSagpMdIn0x6aMmO3_fPVM305
+ hexStr := "YOUR_64_byte_PRIVATE_KEY"
+ bytes, err := hex.DecodeString(hexStr)
+ if err != nil {
+ panic(err)
+ }
+
+ w, err := wallet.FromPrivateKey(api, bytes, wallet.V4R2)
+ // w, err := wallet.FromSeed(api, words, wallet.V4R2)
if err != nil {
log.Fatalln("FromSeed err:", err.Error())
return
@@ -61,9 +69,9 @@ func main() {
log.Fatalln("GetBalance err:", err.Error())
return
}
-
- if balance.Nano().Uint64() >= 3000000 {
- addr := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N")
+ log.Println("balance:", balance.String())
+ if balance.Nano().Uint64() >= 1000000 {
+ addr := address.MustParseAddr("EQD5vcDeRhwaLgAvralVC7sJXI-fc2aNcMUXqcx-BQ-OWnOZ")
log.Println("sending transaction and waiting for confirmation...")
@@ -72,7 +80,7 @@ func main() {
// If bounce is true, money will be returned in case of not initialized destination wallet or smart-contract error
bounce := false
- transfer, err := w.BuildTransfer(addr, tlb.MustFromTON("0.003"), bounce, "Hello from tonutils-go!")
+ transfer, err := w.BuildTransfer(addr, tlb.MustFromTON("0.001"), bounce, "8701657")
if err != nil {
log.Fatalln("Transfer err:", err.Error())
return
https://tonviewer.com/transaction/1363544af7848a3628ce3ab041c84ec7585971332c20915a4a6af2543ec61966
ok交易所的memo
okx 上面充值 ton 充值成功一次之后,memo 会变,我发现这个有趣的现象,看来ok交易所数据库里面 uid->memo 的映射会变的,或者说方便okx自己对账,每次充币ton到交易所的memo都是唯一的,就比课上讲的交易所memo数据库要复杂点。系统设计上应该是,用户点击充值ton,ok的ton数据库表主键id自增生成一条记录,关联到用户的uid,当用户充值成功后,数据库这这个memo_id的状态标记成已完成,用户下次进入充值页面需要重新获取memo_id。这样对于量化交易机器人来说变复杂了,每次memo都不同
可能我同时开两个充币页面,他们数据库被重入了,我转账的memo实际是 8701657, 但现在充币页面说让我用 8701656 可能是我同时刷新两个页面太快了,给我在数据库创建了 8701656 和 8701657 两个 memo