TON钱包签名、私钥导入与发送交易

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

全部评论(1)
seek CTO
点赞
点赞
回复