|
| 1 | +# Implementation Plan: @bitgo/wasm-ton |
| 2 | + |
| 3 | +## Reference Package |
| 4 | +`wasm-solana` (general structure) + `wasm-dot` (for comparison) |
| 5 | + |
| 6 | +## Rust Crates |
| 7 | +```toml |
| 8 | +tlb = { version = "0.7", default-features = false, features = ["sha2"] } |
| 9 | +tlbits = "0.7" |
| 10 | +tlb-ton = { version = "0.7", default-features = false, features = ["sha2"] } |
| 11 | +ton-contracts = { version = "0.7", features = ["wallet"] } |
| 12 | +``` |
| 13 | + |
| 14 | +## Prior Art |
| 15 | +Experiment code at `~/BitGoWasm/.cursor/ton-wasm-experiments/` has working Rust code for all transaction types. Signable payloads verified byte-identical with BitGoJS. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Phase 1: Scaffold + Address Creation |
| 20 | + |
| 21 | +### Package scaffold |
| 22 | +Copy from `wasm-solana` and adapt: |
| 23 | +- `Cargo.toml` — name=wasm-ton, add toner crates, remove solana crates |
| 24 | +- `Makefile` — copy from wasm-solana, change package name |
| 25 | +- `package.json` — name=@bitgo/wasm-ton, `"private": true`, version=0.1.0 |
| 26 | +- `tsconfig.json`, `tsconfig.cjs.json` — copy as-is |
| 27 | +- `.gitignore` — copy as-is |
| 28 | + |
| 29 | +### Rust source files |
| 30 | +- `src/lib.rs` — module declarations, re-exports |
| 31 | +- `src/error.rs` — `WasmTonError` enum (thiserror) |
| 32 | +- `src/address.rs` — Core address logic: |
| 33 | + - `encode(pubkey: &[u8; 32], bounceable: bool, wallet_id: u32) -> String` using toner's `MsgAddress` + `WalletV4R2` |
| 34 | + - `decode(address: &str) -> AddressInfo { workchain, hash, bounceable }` |
| 35 | + - `validate(address: &str) -> bool` |
| 36 | + - Address derivation: pubkey → v4r2 state init → cell hash → MsgAddress |
| 37 | +- `src/wasm/mod.rs` — re-export WASM types |
| 38 | +- `src/wasm/address.rs` — `AddressNamespace` with `#[wasm_bindgen]`: |
| 39 | + - `encode(pubkey: &[u8], bounceable: bool) -> String` |
| 40 | + - `decode(address: &str) -> JsValue` (AddressInfo) |
| 41 | + - `validate(address: &str) -> bool` |
| 42 | + - `is_bounceable(address: &str) -> bool` |
| 43 | + - `set_bounceable(address: &str, bounceable: bool) -> String` |
| 44 | + |
| 45 | +### TypeScript wrappers |
| 46 | +- `js/address.ts` — `Address` namespace: `encode()`, `decode()`, `validate()`, `isBounceable()`, `setBounceable()` |
| 47 | +- `js/index.ts` — barrel exports |
| 48 | + |
| 49 | +### Tests |
| 50 | +- Address round-trip: encode → decode → re-encode |
| 51 | +- Bounceable/non-bounceable conversion |
| 52 | +- Known addresses from BitGoJS fixtures |
| 53 | +- Invalid address rejection |
| 54 | + |
| 55 | +### Verification |
| 56 | +- `npm run build` passes |
| 57 | +- `npm test` passes |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Phase 2: Transaction Parsing + Signing |
| 62 | + |
| 63 | +### Core Rust |
| 64 | +- `src/transaction.rs` — `TonTransaction` struct: |
| 65 | + - `from_boc(boc_base64: &str) -> Result<Self>` — deserialize BOC to typed message |
| 66 | + - Internal fields: `ext_msg: Message`, `sign_body: WalletV4R2SignBody`, `signature: [u8; 64]` |
| 67 | + - `signable_payload(&self) -> [u8; 32]` — SHA-256 hash of sign body Cell |
| 68 | + - `add_signature(&mut self, pubkey: &[u8; 32], signature: &[u8; 64])` — place sig |
| 69 | + - `to_broadcast_format(&self) -> String` — serialize to base64 BOC |
| 70 | + - `id(&self) -> String` — base64 cell hash with `/`→`_`, `+`→`-` |
| 71 | + - `to_bytes(&self) -> Vec<u8>` — raw BOC bytes |
| 72 | + - Public key extraction from StateInit (when seqno=0) |
| 73 | + |
| 74 | +- `src/parser.rs` — `parse_transaction(tx: &TonTransaction) -> ParsedTransaction`: |
| 75 | + - Standalone function (NOT a method on TonTransaction) |
| 76 | + - Returns `ParsedTransaction` with: sender, recipient, amount, seqno, expire_time, wallet_id, bounceable, memo, tx_type |
| 77 | + - Opcode dispatch for tx_type detection: |
| 78 | + - `0x00000000` → text comment (check "Deposit"/"Withdraw" for vesting) |
| 79 | + - `0x0f8a7ea5` → Jetton transfer (use toner's `JettonTransfer`) |
| 80 | + - `0x00001000` → SingleNominatorWithdraw |
| 81 | + - `2077040623` → TonWhalesDeposit |
| 82 | + - `3665837821` → TonWhalesWithdraw |
| 83 | + - empty body → plain transfer |
| 84 | + |
| 85 | +- `src/types.rs` — shared types: |
| 86 | + - `TonTransactionType` enum: Send, SendToken, SingleNominatorWithdraw, TonWhalesDeposit, TonWhalesWithdrawal, TonWhalesVestingDeposit, TonWhalesVestingWithdrawal |
| 87 | + - `ParsedTransaction` struct |
| 88 | + - `ParsedPayload` enum (per-opcode data) |
| 89 | + |
| 90 | +- `src/staking.rs` — custom CellSerialize/CellDeserialize impls: |
| 91 | + - `NominatorWithdraw { query_id: u64, amount: BigUint }` |
| 92 | + - `WhalesDeposit { query_id: u64 }` |
| 93 | + - `WhalesWithdraw { query_id: u64, unstake_amount: BigUint }` |
| 94 | + |
| 95 | +### WASM bindings |
| 96 | +- `src/wasm/transaction.rs` — `WasmTransaction`: |
| 97 | + - `from_boc(boc: &str) -> WasmTransaction` |
| 98 | + - `signable_payload(&self) -> Vec<u8>` |
| 99 | + - `add_signature(&mut self, pubkey: &[u8], signature: &[u8])` |
| 100 | + - `to_broadcast_format(&self) -> String` |
| 101 | + - `id(&self) -> String` |
| 102 | + |
| 103 | +- `src/wasm/parser.rs` — `ParserNamespace`: |
| 104 | + - `parse_transaction(boc: &str) -> JsValue` (ParsedTransaction via serde_wasm_bindgen) |
| 105 | + |
| 106 | +- `src/wasm/try_into_js_value.rs` — BigInt conversion for u64 amounts |
| 107 | + |
| 108 | +### TypeScript wrappers |
| 109 | +- `js/transaction.ts` — `Transaction` class wrapping WasmTransaction |
| 110 | +- `js/parser.ts` — `parseTransaction()` function + `ParsedTransaction` type |
| 111 | + |
| 112 | +### Tests |
| 113 | +- Round-trip: parse fixture BOC → verify fields → add signature → serialize → re-parse |
| 114 | +- Use real fixtures from `~/BitGoJS/modules/sdk-coin-ton/test/resources/ton.ts` |
| 115 | +- All 7 transaction types |
| 116 | +- Signable payload matches BitGoJS output (verified in experiments) |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## Phase 3: Transaction Building (Intents) |
| 121 | + |
| 122 | +### Core Rust |
| 123 | +- `src/builder/mod.rs` — module declarations |
| 124 | +- `src/builder/types.rs` — intent types: |
| 125 | + ```rust |
| 126 | + #[derive(Deserialize)] |
| 127 | + #[serde(tag = "intentType")] |
| 128 | + pub enum TonIntent { |
| 129 | + Payment(PaymentIntent), |
| 130 | + JettonTransfer(JettonTransferIntent), |
| 131 | + SingleNominatorWithdraw(NominatorWithdrawIntent), |
| 132 | + TonWhalesDeposit(WhalesDepositIntent), |
| 133 | + TonWhalesWithdrawal(WhalesWithdrawalIntent), |
| 134 | + TonWhalesVestingDeposit(VestingDepositIntent), |
| 135 | + TonWhalesVestingWithdrawal(VestingWithdrawalIntent), |
| 136 | + } |
| 137 | + ``` |
| 138 | + Each intent struct has the params from wallet-platform research. |
| 139 | + |
| 140 | +- `src/builder/context.rs` — build context: |
| 141 | + ```rust |
| 142 | + pub struct TonBuildContext { |
| 143 | + pub sender: String, |
| 144 | + pub public_key: String, // hex |
| 145 | + pub seqno: u32, |
| 146 | + pub expire_time: u32, |
| 147 | + pub wallet_id: Option<u32>, // default 698983191 |
| 148 | + pub bounceable: Option<bool>, |
| 149 | + } |
| 150 | + ``` |
| 151 | + |
| 152 | +- `src/builder/build.rs` — `build_transaction(intent: &TonIntent, context: &TonBuildContext) -> Result<TonTransaction>`: |
| 153 | + - Shared: `V4R2::create_sign_body(wallet_id, expire_at, seqno, actions)` + `SendMsgAction` + `Message::transfer` |
| 154 | + - Payment: `TextComment(memo)` body (or empty) |
| 155 | + - JettonTransfer: `JettonTransfer` body (toner built-in) |
| 156 | + - SingleNominatorWithdraw: custom `NominatorWithdraw` body |
| 157 | + - TonWhalesDeposit: custom `WhalesDeposit` body |
| 158 | + - TonWhalesWithdrawal: custom `WhalesWithdraw` body |
| 159 | + - VestingDeposit: `TextComment("Deposit")` + wallet_id=268 |
| 160 | + - VestingWithdrawal: `TextComment("Withdraw")` + wallet_id=268 |
| 161 | + - StateInit inclusion when seqno=0 |
| 162 | + |
| 163 | +### WASM bindings |
| 164 | +- `src/wasm/builder.rs` — `BuilderNamespace`: |
| 165 | + - `build_transaction(intent: JsValue, context: JsValue) -> WasmTransaction` |
| 166 | + |
| 167 | +### TypeScript wrappers |
| 168 | +- `js/builder.ts` — `buildTransaction(intent, context)` + intent type interfaces |
| 169 | + |
| 170 | +### Tests per intent |
| 171 | +- Build → serialize → parse → verify decoded fields match intent params |
| 172 | +- Build with seqno=0 → verify StateInit present |
| 173 | +- Compare signable payloads with experiment results where available |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## Phase 4: Scratch Validation (Testnet) |
| 178 | + |
| 179 | +- Create `scratch/ton-wallet.ts` |
| 180 | +- Commands: setup, balance, send, jetton-send, stake (whales), unstake |
| 181 | +- Testnet: TON testnet |
| 182 | +- Signing: tweetnacl (Ed25519) |
| 183 | +- Validate all intents broadcast successfully |
0 commit comments