Files
zeekay c1c743cb98 feat(tx): non-EVM unsigned-tx builders for every bridge chain
Implement transfer builders for the six external bridge chains so the
wallet can SEND/bridge beyond EVM. Each is one file over a permissive
offline lib; signing stays in @luxwallet/keyring (builders emit only the
canonical UNSIGNED encoding + the bytes-to-sign).

Ready (broadcastable from the intent alone):
- solana   @solana/web3.js (MIT)  — SystemProgram.transfer -> message bytes
- xrp      xrpl (ISC)             — Payment -> encodeForSigning blob
- ton      @ton/core (MIT)        — internal transfer message -> Cell/BOC + hash
- bitcoin  @scure/btc-signer (MIT)— P2WPKH/P2TR PSBT from UTXOs+outputs

Partial (real payload, caller supplies chain state the builder can't
derive offline — honestly marked "partial", not "ready"):
- polkadot @polkadot/types (Apache-2.0) — balances.transferKeepAlive
           signing payload; needs runtime metadata + era/nonce/versions
- cardano  @emurgo/cardano-serialization-lib (MIT) — tx body + blake2b
           body hash; needs UTXO set + fee/ttl. Async, lazy WASM load.

types.ts: per-chain Intent types; UnsignedTx gains optional `digest`
(bytes-to-sign when != serialized); local BuilderStatus superset adds
"partial". index.ts: BUILDER_STATUS table (5 ready / 2 partial / 4 todo).
stubs.ts: only Lux-native families (P/X/atomic/Z) remain "todo"; svm stub
removed (Solana real). Each builder has a vitest with real round-trip /
byte-equality assertions (Polkadot uses real Polkadot V14 metadata).

All deps MIT/Apache/ISC (verify-clean gate passes; LGPL rpc-websockets is
a transitive @solana/web3.js dep off the offline code path). Bump 0.2.0.

typecheck + 34 tests pass; full workspace green.
2026-06-24 15:28:35 -07:00

39 lines
1.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { Cell, loadMessageRelaxed, Address } from "@ton/core";
import { buildTonUnsignedTx } from "./ton.js";
const DEST = "EQD4FPq-PRDieyQKkizFTRtSDyucUIqrj0v_zXJmqaDp6_0t";
describe("@luxwallet/tx TON builder (real transfer message)", () => {
const intent = { to: DEST, amountNano: "1500000000", seqno: 5, comment: "gm" };
it("produces a message BOC + cell-hash digest (0x hex)", () => {
const tx = buildTonUnsignedTx(intent);
expect(tx.family).toBe("ton");
expect(tx.serialized).toMatch(/^0x[0-9a-f]+$/);
expect(tx.digest).toMatch(/^0x[0-9a-f]{64}$/); // 32-byte cell hash
});
it("round-trips: the BOC parses to the internal transfer message", () => {
const tx = buildTonUnsignedTx(intent);
const cell = Cell.fromBoc(Buffer.from(tx.serialized.slice(2), "hex"))[0]!;
const msg = loadMessageRelaxed(cell.beginParse());
expect(msg.info.type).toBe("internal");
if (msg.info.type !== "internal") throw new Error("expected internal message");
expect(msg.info.dest.toString()).toBe(Address.parse(DEST).toString());
expect(msg.info.value.coins).toBe(1500000000n);
expect(msg.info.bounce).toBe(true);
});
it("the digest equals the message cell hash", () => {
const tx = buildTonUnsignedTx(intent);
const cell = Cell.fromBoc(Buffer.from(tx.serialized.slice(2), "hex"))[0]!;
expect(`0x${Buffer.from(cell.hash()).toString("hex")}`).toBe(tx.digest);
});
it("rejects non-positive amounts", () => {
expect(() => buildTonUnsignedTx({ ...intent, amountNano: "0" })).toThrow(/> 0/);
});
});