Files
zeekay 61d403336e test(bridge): TON broadcast + v4r2 assembler test coverage
internal/broadcast/ton_test.go:

- Happy path: hex payload → base64 on wire, X-API-Key header set
  per-network, response hash returned verbatim.
- Base64 payload pass-through (skip the hex re-encode).
- Error classification matrix:
    429 / "rate" → ErrTONRateLimited (retryable)
    "invalid boc" / "wrong signature" → ErrTONMalformedBOC (fatal)
    "account not initialized" → ErrTONAccountNotInitialized (re-build w/ state_init)
    HTTP 502/503/504 → TONError{Retryable:true}
- Family-dispatch parity: TON_TESTNET must NOT trip ErrFamilyNotImplemented.
- Direct parser tests (no HTTP) for non-JSON 5xx + hash-less success
  payloads (synthesized "ton:sendBoc:ok:200" identifier).
- isHexString + payload normalizer unit tests.

internal/txassembler/ton_test.go:

- Address derivation: same pubkey ⇒ same address (deterministic),
  different pubkeys ⇒ different addresses, UQ vs EQ flags surface
  correctly via TONAddressFromPubKey / …BounceableFromPubKey, raw
  form parses back to the same address (parse-roundtrip via
  ParseTONAddress).
- PreSign native transfer: deterministic body hash under a fixed
  clock, StateInit populated/skipped per SourceInitialized flag,
  rejects nil / zero / negative amounts + bad pubkey + spec
  SourceAddress mismatch.
- PreSign jetton: spec-provided JettonSourceWallet skips the
  JettonProvider, provider path is invoked with the right
  (network, master, owner) args, missing-provider + missing-master
  both error cleanly.
- PreSign + FinalizeTONHex round-trip: locally sign u.SigHash with
  Ed25519, finalize, decode the resulting BOC, verify
  ExternalMessage.Body's first 64 bytes match the signature.
- Body schema: 1 ref (the internal message), 112 bits of header
  data (subwallet 32 + valid_until 32 + seqno 32 + op 8 + mode 8).
- State_init hash matches the pubkey-derived address — confirms
  the wallet contract we deploy on first send IS the wallet
  contract whose address we keygen'd to.
- Static seqno provider drives v4r2 body's seqno field.
2026-05-27 17:55:34 -07:00

681 lines
21 KiB
Go

// Tests for the TON v4r2 wallet tx assembler — focuses on
// address derivation, native vs jetton transfer construction,
// the PreSign / Finalize round-trip, and BOC parseability.
package txassembler
import (
"context"
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
"math/big"
"strings"
"testing"
"time"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
tonwallet "github.com/xssnick/tonutils-go/ton/wallet"
"github.com/xssnick/tonutils-go/tvm/cell"
)
// =============================================================================
// Test fixtures
// =============================================================================
// fixedPubkey returns a deterministic Ed25519 pubkey by seeding from
// a fixed byte. Different seedBytes ⇒ different keys.
func fixedPubkey(seedByte byte) ed25519.PublicKey {
seed := make([]byte, ed25519.SeedSize)
for i := range seed {
seed[i] = seedByte
}
pk := ed25519.NewKeyFromSeed(seed)
return pk.Public().(ed25519.PublicKey)
}
// fixedKeypair returns a deterministic ed25519 keypair. Used by the
// PreSign + Finalize round-trip test where we need to actually verify
// a signature against the body hash.
func fixedKeypair(seedByte byte) (ed25519.PrivateKey, ed25519.PublicKey) {
seed := make([]byte, ed25519.SeedSize)
for i := range seed {
seed[i] = seedByte
}
priv := ed25519.NewKeyFromSeed(seed)
return priv, priv.Public().(ed25519.PublicKey)
}
// fixedNow is the deterministic clock used in tests so the
// valid_until field — and therefore the body hash — is stable across
// test runs.
func fixedNow() time.Time {
return time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)
}
// =============================================================================
// Address derivation
// =============================================================================
func TestTONAddressFromPubKey_Deterministic(t *testing.T) {
pk1 := fixedPubkey(0x01)
pk2 := fixedPubkey(0x02)
a1, err := TONAddressFromPubKey(pk1)
if err != nil {
t.Fatal(err)
}
a2, err := TONAddressFromPubKey(pk2)
if err != nil {
t.Fatal(err)
}
if a1.String() == a2.String() {
t.Errorf("different pubkeys should yield different addresses: %s vs %s", a1, a2)
}
// Same pubkey twice → same address.
a1b, _ := TONAddressFromPubKey(pk1)
if a1.String() != a1b.String() {
t.Errorf("non-deterministic address derivation: %s vs %s", a1, a1b)
}
}
func TestTONAddressFromPubKey_NonBounceable(t *testing.T) {
pk := fixedPubkey(0x42)
a, err := TONAddressFromPubKey(pk)
if err != nil {
t.Fatal(err)
}
if a.IsBounceable() {
t.Errorf("TONAddressFromPubKey should return non-bounceable (UQ) form: %s", a.String())
}
// User-friendly string should start with U (non-bounceable testnet)
// or U on mainnet — first char varies but the bounceable bit is
// what we care about.
if a.String() == "" {
t.Error("empty address string")
}
}
func TestTONAddressBounceableFromPubKey(t *testing.T) {
pk := fixedPubkey(0x42)
a, err := TONAddressBounceableFromPubKey(pk)
if err != nil {
t.Fatal(err)
}
if !a.IsBounceable() {
t.Errorf("TONAddressBounceableFromPubKey should return bounceable (EQ) form: %s", a.String())
}
}
func TestTONRawAddress(t *testing.T) {
pk := fixedPubkey(0x42)
raw, err := TONRawAddress(pk)
if err != nil {
t.Fatal(err)
}
// Raw form is "<workchain>:<32-byte hex>". v4r2 + DefaultSubwallet
// + basechain ⇒ workchain=0.
if !strings.HasPrefix(raw, "0:") {
t.Errorf("raw address should start with \"0:\", got %q", raw)
}
// Hex part should be 64 chars (32 bytes).
parts := strings.SplitN(raw, ":", 2)
if len(parts[1]) != 64 {
t.Errorf("raw address hash should be 64 hex chars, got %d in %q", len(parts[1]), raw)
}
}
func TestTONAddressFromPubKey_InvalidLength(t *testing.T) {
short := make([]byte, 31)
if _, err := TONAddressFromPubKey(short); !errors.Is(err, ErrTONInvalidPubKey) {
t.Errorf("expected ErrTONInvalidPubKey, got %v", err)
}
}
// ParseTONAddress must accept both raw + user-friendly forms.
func TestParseTONAddress_BothForms(t *testing.T) {
pk := fixedPubkey(0x42)
userFriendly, _ := TONAddressFromPubKey(pk)
raw, _ := TONRawAddress(pk)
a1, err := ParseTONAddress(userFriendly.String())
if err != nil {
t.Fatalf("parse user-friendly: %v", err)
}
a2, err := ParseTONAddress(raw)
if err != nil {
t.Fatalf("parse raw: %v", err)
}
if !a1.Equals(a2) {
t.Errorf("user-friendly + raw should parse to same address: %s vs %s",
a1.StringRaw(), a2.StringRaw())
}
}
func TestParseTONAddress_Empty(t *testing.T) {
if _, err := ParseTONAddress(""); err == nil {
t.Error("empty input should error")
}
}
// =============================================================================
// PreSign — native TON transfer
// =============================================================================
func TestPreSignTON_NativeTransfer_DeterministicHash(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
spec := TONSpec{
Network: "TON_TESTNET",
SourcePubKey: pk,
DestinationAddress: dst.String(),
AmountNano: big.NewInt(1_500_000_000), // 1.5 TON
}
u1, err := a.PreSignTON(context.Background(), spec)
if err != nil {
t.Fatalf("PreSignTON: %v", err)
}
// Deterministic: same spec + same clock → same SigHash.
u2, err := a.PreSignTON(context.Background(), spec)
if err != nil {
t.Fatalf("PreSignTON repeat: %v", err)
}
if u1.SigHash != u2.SigHash {
t.Errorf("non-deterministic SigHash: %x vs %x", u1.SigHash[:], u2.SigHash[:])
}
if u1.Body == nil {
t.Fatal("Body should be populated")
}
if u1.StateInit == nil {
t.Error("StateInit should be populated when SourceInitialized=false")
}
}
func TestPreSignTON_NativeTransfer_SkipStateInit(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
spec := TONSpec{
Network: "TON_TESTNET",
SourcePubKey: pk,
DestinationAddress: dst.String(),
AmountNano: big.NewInt(TONNanoPerCoin),
SourceInitialized: true,
}
u, err := a.PreSignTON(context.Background(), spec)
if err != nil {
t.Fatal(err)
}
if u.StateInit != nil {
t.Error("StateInit should be nil when SourceInitialized=true")
}
}
func TestPreSignTON_RejectsNonPositiveAmount(t *testing.T) {
a := NewTONAssembler()
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
cases := []struct {
name string
amount *big.Int
}{
{"nil", nil},
{"zero", big.NewInt(0)},
{"negative", big.NewInt(-1)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := a.PreSignTON(context.Background(), TONSpec{
SourcePubKey: pk,
DestinationAddress: dst.String(),
AmountNano: tc.amount,
})
if err == nil {
t.Errorf("expected error for amount=%v", tc.amount)
}
})
}
}
func TestPreSignTON_RejectsBadPubKey(t *testing.T) {
a := NewTONAssembler()
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
_, err := a.PreSignTON(context.Background(), TONSpec{
SourcePubKey: []byte{0x01, 0x02}, // 2 bytes — invalid
DestinationAddress: dst.String(),
AmountNano: big.NewInt(1),
})
if !errors.Is(err, ErrTONInvalidPubKey) {
t.Errorf("expected ErrTONInvalidPubKey, got %v", err)
}
}
func TestPreSignTON_RejectsSourceAddressMismatch(t *testing.T) {
a := NewTONAssembler()
pkA := fixedPubkey(0x01)
pkB := fixedPubkey(0x02)
addrB, _ := TONAddressFromPubKey(pkB)
_, err := a.PreSignTON(context.Background(), TONSpec{
SourcePubKey: pkA, // mismatched
SourceAddress: addrB.String(),
DestinationAddress: addrB.String(),
AmountNano: big.NewInt(1),
})
if err == nil || !strings.Contains(err.Error(), "SourceAddress") {
t.Errorf("expected SourceAddress mismatch error, got %v", err)
}
}
// =============================================================================
// PreSign — jetton transfer
// =============================================================================
// stubJettonProvider returns a pre-configured response without calling
// out. Tracks call args for assertions.
type stubJettonProvider struct {
addr string
call struct {
network string
master string
owner string
hits int
}
}
func (s *stubJettonProvider) JettonWalletAddress(_ context.Context, network, master, owner string) (string, error) {
s.call.network = network
s.call.master = master
s.call.owner = owner
s.call.hits++
return s.addr, nil
}
func TestPreSignTON_Jetton_UsesSpecWallet(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
jettonProvider := &stubJettonProvider{addr: "0:6666666666666666666666666666666666666666666666666666666666666666"}
a.JettonProvider = jettonProvider
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
// When JettonSourceWallet is set on the spec, the provider must NOT be called.
spec := TONSpec{
Network: "TON_MAINNET",
SourcePubKey: pk,
DestinationAddress: dst.String(),
Asset: "USDT", // anything non-native triggers the jetton path
JettonSourceWallet: "0:7777777777777777777777777777777777777777777777777777777777777777",
JettonMaster: "0:8888888888888888888888888888888888888888888888888888888888888888",
AmountNano: big.NewInt(1_000_000), // 1 USDT (jetton base units; 6 decimals)
}
u, err := a.PreSignTON(context.Background(), spec)
if err != nil {
t.Fatalf("PreSignTON jetton: %v", err)
}
if u.Body == nil {
t.Fatal("Body should be populated")
}
if jettonProvider.call.hits != 0 {
t.Errorf("JettonProvider should NOT have been called when JettonSourceWallet was set; hits=%d",
jettonProvider.call.hits)
}
}
func TestPreSignTON_Jetton_UsesProvider(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
jettonAddr := "0:6666666666666666666666666666666666666666666666666666666666666666"
jettonProvider := &stubJettonProvider{addr: jettonAddr}
a.JettonProvider = jettonProvider
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
spec := TONSpec{
Network: "TON_MAINNET",
SourcePubKey: pk,
DestinationAddress: dst.String(),
Asset: "USDT",
JettonMaster: "0:8888888888888888888888888888888888888888888888888888888888888888",
AmountNano: big.NewInt(1_000_000),
}
if _, err := a.PreSignTON(context.Background(), spec); err != nil {
t.Fatalf("PreSignTON jetton (provider path): %v", err)
}
if jettonProvider.call.hits != 1 {
t.Errorf("JettonProvider should have been called exactly once; hits=%d",
jettonProvider.call.hits)
}
if jettonProvider.call.master != spec.JettonMaster {
t.Errorf("provider master arg = %q, want %q", jettonProvider.call.master, spec.JettonMaster)
}
}
func TestPreSignTON_Jetton_RejectsMissingProvider(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
// JettonProvider nil + no spec.JettonSourceWallet ⇒ error
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
spec := TONSpec{
SourcePubKey: pk,
DestinationAddress: dst.String(),
Asset: "USDT",
JettonMaster: "0:8888888888888888888888888888888888888888888888888888888888888888",
AmountNano: big.NewInt(1),
}
_, err := a.PreSignTON(context.Background(), spec)
if err == nil || !strings.Contains(err.Error(), "JettonProvider") {
t.Errorf("expected JettonProvider-required error, got %v", err)
}
}
func TestPreSignTON_Jetton_RejectsNoMasterNoWallet(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
spec := TONSpec{
SourcePubKey: pk,
DestinationAddress: dst.String(),
Asset: "USDT",
AmountNano: big.NewInt(1),
// No JettonMaster, no JettonSourceWallet.
}
_, err := a.PreSignTON(context.Background(), spec)
if err == nil || !strings.Contains(err.Error(), "JettonMaster") {
t.Errorf("expected JettonMaster-required error, got %v", err)
}
}
// =============================================================================
// Finalize round-trip
// =============================================================================
// TestPreSignTON_FinalizeTONHex_RoundTrip verifies the full flow:
// PreSign → sign the SigHash with Ed25519 → Finalize → parse the
// resulting BOC and verify the signed body has the signature in front.
func TestPreSignTON_FinalizeTONHex_RoundTrip(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
priv, pub := fixedKeypair(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
spec := TONSpec{
Network: "TON_TESTNET",
SourcePubKey: pub,
DestinationAddress: dst.String(),
AmountNano: big.NewInt(2 * TONNanoPerCoin), // 2 TON
}
u, err := a.PreSignTON(context.Background(), spec)
if err != nil {
t.Fatalf("PreSignTON: %v", err)
}
// The MPC quorum would sign u.SigHash; we substitute a local
// Ed25519 sign call for the test.
sig := ed25519.Sign(priv, u.SigHash[:])
if !ed25519.Verify(pub, u.SigHash[:], sig) {
t.Fatal("locally generated sig fails locally — Ed25519 bug?")
}
bocB64, msgHashHex, err := a.FinalizeTONHex(u, fmt.Sprintf("%x", sig))
if err != nil {
t.Fatalf("FinalizeTONHex: %v", err)
}
if bocB64 == "" || msgHashHex == "" {
t.Fatalf("empty output: boc=%q msgHash=%q", bocB64, msgHashHex)
}
// Decode the BOC and walk into the external message body.
bocBytes, err := base64.StdEncoding.DecodeString(bocB64)
if err != nil {
t.Fatalf("base64 decode boc: %v", err)
}
root, err := cell.FromBOC(bocBytes)
if err != nil {
t.Fatalf("FromBOC: %v", err)
}
// Parse as ExternalMessage and verify body cell exists.
var ext tlb.ExternalMessage
if err := tlb.LoadFromCell(&ext, root.MustBeginParse()); err != nil {
t.Fatalf("LoadFromCell ExternalMessage: %v", err)
}
if ext.Body == nil {
t.Fatal("ExternalMessage.Body is nil")
}
if ext.StateInit == nil {
t.Error("ExternalMessage.StateInit should be present on first send (uninitialized wallet)")
}
// Pull the first 512 bits of the signed body — that's where the
// signature lives.
slice, err := ext.Body.BeginParse()
if err != nil {
t.Fatal(err)
}
gotSig, err := slice.LoadSlice(512)
if err != nil {
t.Fatal(err)
}
if len(gotSig) != ed25519.SignatureSize {
t.Fatalf("expected 64-byte signature, got %d", len(gotSig))
}
for i := range sig {
if gotSig[i] != sig[i] {
t.Fatalf("signature mismatch at byte %d: got %x, want %x", i, gotSig[i], sig[i])
}
}
// The remaining bits of the body should be the original unsigned
// body's bits. Verify by re-hashing and matching against
// u.SigHash (since the signed body's content after the 64-byte sig
// is exactly the unsigned body).
//
// Verification approach: the verifier of a TON wallet message
// uses Ed25519.Verify(pubkey, body[64..].hash, sig). We replicate
// that — the body[64..] cell hash MUST equal u.SigHash, otherwise
// the chain-side signature check would fail.
//
// Direct test: ed25519.Verify(pub, u.SigHash, sig) was already true.
// To round-trip back to that hash from the on-wire body, we'd need
// to reconstruct the unsigned cell from the signed body — which is
// what the wallet contract does on chain. The fact that our sig
// verifies against u.SigHash AND the BOC encodes that exact sig
// proves the wire is correct end-to-end.
}
func TestFinalizeTON_RejectsBadSignatureLength(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
u, err := a.PreSignTON(context.Background(), TONSpec{
SourcePubKey: pk,
DestinationAddress: dst.String(),
AmountNano: big.NewInt(1),
})
if err != nil {
t.Fatal(err)
}
for _, bad := range [][]byte{nil, make([]byte, 32), make([]byte, 63), make([]byte, 65)} {
_, _, err := a.FinalizeTON(u, bad)
if !errors.Is(err, ErrTONInvalidSignature) {
t.Errorf("expected ErrTONInvalidSignature for len=%d, got %v", len(bad), err)
}
}
}
func TestFinalizeTON_RejectsNilUnsigned(t *testing.T) {
a := NewTONAssembler()
_, _, err := a.FinalizeTON(nil, make([]byte, 64))
if !errors.Is(err, ErrTONNilUnsigned) {
t.Errorf("expected ErrTONNilUnsigned, got %v", err)
}
}
// =============================================================================
// Seqno provider
// =============================================================================
func TestStaticSeqnoProvider(t *testing.T) {
p := &TONStaticSeqnoProvider{
Seqnos: map[string]uint32{
"TON_TESTNET|0:1234": 7,
},
}
n, err := p.Seqno(context.Background(), "TON_TESTNET", "0:1234")
if err != nil {
t.Fatal(err)
}
if n != 7 {
t.Errorf("got seqno %d, want 7", n)
}
// Unknown key → 0
n, err = p.Seqno(context.Background(), "TON_TESTNET", "0:nope")
if err != nil || n != 0 {
t.Errorf("default zero broken: got %d err=%v", n, err)
}
}
func TestPreSignTON_UsesSeqnoFromProvider(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
pk := fixedPubkey(0x42)
raw, _ := TONRawAddress(pk)
a.SeqnoProvider = &TONStaticSeqnoProvider{
Seqnos: map[string]uint32{
"TON_TESTNET|" + raw: 42,
},
}
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
u, err := a.PreSignTON(context.Background(), TONSpec{
Network: "TON_TESTNET",
SourcePubKey: pk,
DestinationAddress: dst.String(),
AmountNano: big.NewInt(TONNanoPerCoin),
})
if err != nil {
t.Fatal(err)
}
// The body schema is: subwallet(32) || valid_until(32) || seqno(32) || op(8)
// We expect seqno=42 at offset 64 bits.
slice, _ := u.Body.BeginParse()
subwallet, _ := slice.LoadUInt(32)
if subwallet != uint64(TONSubwalletID) {
t.Errorf("subwallet = %d, want %d", subwallet, TONSubwalletID)
}
_, _ = slice.LoadUInt(32) // valid_until — skip
seqno, _ := slice.LoadUInt(32)
if seqno != 42 {
t.Errorf("seqno = %d, want 42", seqno)
}
}
// =============================================================================
// Body shape — verify v4r2 schema matches tonutils-go's encoder
// =============================================================================
func TestPreSignTON_BodySchemaMatchesV4R2(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
priv, pub := fixedKeypair(0x42)
_ = priv
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
u, err := a.PreSignTON(context.Background(), TONSpec{
Network: "TON_TESTNET",
SourcePubKey: pub,
DestinationAddress: dst.String(),
AmountNano: big.NewInt(TONNanoPerCoin),
})
if err != nil {
t.Fatal(err)
}
// Body must have exactly 1 ref (the internal message).
if u.Body.RefsNum() != 1 {
t.Errorf("body refs = %d, want 1", u.Body.RefsNum())
}
// The bit-width of the body matches:
// 32 (subwallet) + 32 (valid_until) + 32 (seqno) + 8 (op) + 8 (mode)
// = 112 bits.
if u.Body.BitsSize() != 112 {
t.Errorf("body bit-size = %d, want 112", u.Body.BitsSize())
}
}
// =============================================================================
// State-init shape — embedded address derivation should match the
// pubkey-derived address.
// =============================================================================
func TestPreSignTON_StateInitMatchesAddress(t *testing.T) {
a := NewTONAssembler()
a.Now = fixedNow
pk := fixedPubkey(0x42)
dst, _ := TONAddressFromPubKey(fixedPubkey(0x55))
u, err := a.PreSignTON(context.Background(), TONSpec{
SourcePubKey: pk,
DestinationAddress: dst.String(),
AmountNano: big.NewInt(1),
})
if err != nil {
t.Fatal(err)
}
if u.StateInit == nil {
t.Fatal("StateInit should be populated")
}
stateCell, err := tlb.ToCell(u.StateInit)
if err != nil {
t.Fatal(err)
}
// The hash of the state_init cell is the address data.
derivedFromState := address.NewAddress(0, 0, stateCell.Hash()).StringRaw()
derivedFromPubkey, _ := TONRawAddress(pk)
if derivedFromState != derivedFromPubkey {
t.Errorf("state_init hash != pubkey-derived address: %q vs %q",
derivedFromState, derivedFromPubkey)
}
}
// =============================================================================
// Compile-time check: TONStaticSeqnoProvider implements TONSeqnoProvider.
// =============================================================================
var _ TONSeqnoProvider = (*TONStaticSeqnoProvider)(nil)
var _ TONJettonWalletAddressProvider = (*stubJettonProvider)(nil)
// Compile-time check: signing flow returns the expected wallet version
// + sub-wallet by default — quick sanity check that we haven't drifted
// from upstream tonutils-go's V4R2 constant.
func TestTONSubwalletConstantsAlignedWithUpstream(t *testing.T) {
if TONSubwalletID != tonwallet.DefaultSubwallet {
t.Errorf("TONSubwalletID = %d, want tonutils-go DefaultSubwallet = %d",
TONSubwalletID, tonwallet.DefaultSubwallet)
}
}