mirror of
https://github.com/luxwallet/sdk.git
synced 2026-10-30 19:32:37 +00:00
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.
39 lines
1.6 KiB
TypeScript
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/);
|
|
});
|
|
});
|