mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
701 lines
22 KiB
Go
701 lines
22 KiB
Go
// Tests for the EVM tx assembler.
|
|
//
|
|
// Test vectors below come from the canonical Ethereum reference
|
|
// implementations (go-ethereum, ethers.js) — cross-checked against
|
|
// independent calculators for the RLP / keccak / EIP-155 paths.
|
|
|
|
package txassembler
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"math/big"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/luxfi/bridge/internal/tokens"
|
|
)
|
|
|
|
// =============================================================================
|
|
// RLP — primitive vectors
|
|
// =============================================================================
|
|
|
|
// Vectors lifted from the Ethereum Yellow Paper §B.1 examples.
|
|
func TestRLP_StringPrimitives(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in []byte
|
|
want string // hex
|
|
}{
|
|
{"empty", []byte{}, "80"},
|
|
{"single low byte", []byte{0x00}, "00"},
|
|
{"single byte 0x7f", []byte{0x7f}, "7f"},
|
|
{"single byte 0x80 → length prefix", []byte{0x80}, "8180"},
|
|
{"hello world", []byte("hello"), "8568656c6c6f"},
|
|
// 55 chars exactly → 0x80+0x37 = 0xb7 prefix
|
|
{"55 bytes", make55(), "b7" + hexrep("a", 55)},
|
|
// 56 chars → long-string encoding: 0xb8 (one length byte) + 0x38 (=56)
|
|
{"56 bytes", make56(), "b838" + hexrep("a", 56)},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := hex.EncodeToString(encodeBytes(tc.in))
|
|
if got != tc.want {
|
|
t.Errorf("got %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func make55() []byte { return bytes.Repeat([]byte{'a'}, 55) }
|
|
func make56() []byte { return bytes.Repeat([]byte{'a'}, 56) }
|
|
func hexrep(s string, n int) string {
|
|
out := ""
|
|
if s == "a" {
|
|
out = "61"
|
|
}
|
|
r := ""
|
|
for i := 0; i < n; i++ {
|
|
r += out
|
|
}
|
|
return r
|
|
}
|
|
|
|
// =============================================================================
|
|
// encodeUint — canonical big-int encoding
|
|
// =============================================================================
|
|
|
|
func TestEncodeUint_Canonical(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
n *big.Int
|
|
want string
|
|
}{
|
|
{"zero", big.NewInt(0), "80"},
|
|
{"one", big.NewInt(1), "01"},
|
|
{"127", big.NewInt(127), "7f"},
|
|
{"128", big.NewInt(128), "8180"},
|
|
{"255", big.NewInt(255), "81ff"},
|
|
{"256", big.NewInt(256), "820100"},
|
|
{"1024", big.NewInt(1024), "820400"},
|
|
{"nil → empty", nil, "80"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := hex.EncodeToString(encodeUint(tc.n))
|
|
if got != tc.want {
|
|
t.Errorf("got %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncodeUint_LargeValues(t *testing.T) {
|
|
// 1 ether = 10^18 wei = 0x0de0b6b3a7640000 (8 bytes)
|
|
oneEther := new(big.Int)
|
|
oneEther.SetString("1000000000000000000", 10)
|
|
got := hex.EncodeToString(encodeUint(oneEther))
|
|
want := "880de0b6b3a7640000"
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// keccak256 — known vectors
|
|
// =============================================================================
|
|
|
|
func TestKeccak256_KnownVectors(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
// keccak256("") = empty preimage
|
|
{"empty",
|
|
"",
|
|
"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"},
|
|
// keccak256("hello")
|
|
{"hello",
|
|
"hello",
|
|
"1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
h := keccak256([]byte(tc.in))
|
|
got := hex.EncodeToString(h[:])
|
|
if got != tc.want {
|
|
t.Errorf("got %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// FunctionSelector — Solidity ABI
|
|
// =============================================================================
|
|
|
|
func TestFunctionSelector_KnownSignatures(t *testing.T) {
|
|
// ERC-20 `transfer(address,uint256)` selector is the canonical 0xa9059cbb.
|
|
got := FunctionSelector("transfer(address,uint256)")
|
|
want := [4]byte{0xa9, 0x05, 0x9c, 0xbb}
|
|
if got != want {
|
|
t.Errorf("transfer selector: got %x, want %x", got, want)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// parseAddress
|
|
// =============================================================================
|
|
|
|
func TestParseAddress(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
wantHex string
|
|
wantErr bool
|
|
}{
|
|
{"0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
"a28fae14eb42e7a5c36ad2d774a2b7eb293c4473", false},
|
|
// No 0x prefix
|
|
{"a28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
"a28fae14eb42e7a5c36ad2d774a2b7eb293c4473", false},
|
|
{"0xtooshort", "", true},
|
|
{"0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ", "", true}, // bad hex
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.in, func(t *testing.T) {
|
|
got, err := parseAddress(tc.in)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hex.EncodeToString(got) != tc.wantHex {
|
|
t.Errorf("got %x, want %s", got, tc.wantHex)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// floatToWei
|
|
// =============================================================================
|
|
|
|
func TestFloatToWei(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
amount float64
|
|
decimals int
|
|
want string // decimal
|
|
}{
|
|
{"1 ether", 1.0, 18, "1000000000000000000"},
|
|
{"0.1 ether", 0.1, 18, "100000000000000000"},
|
|
{"0.001 ether", 0.001, 18, "1000000000000000"},
|
|
{"0 ether", 0.0, 18, "0"},
|
|
{"6-decimal USDC, 100 units", 100, 6, "100000000"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := floatToWei(tc.amount, tc.decimals)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.String() != tc.want {
|
|
t.Errorf("got %s, want %s", got.String(), tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFloatToWei_RejectsNegative(t *testing.T) {
|
|
if _, err := floatToWei(-1, 18); err == nil {
|
|
t.Error("expected error for negative amount")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// PreSign / Finalize — end-to-end with a known result
|
|
// =============================================================================
|
|
|
|
// Canonical EIP-155 example from EIP-155 itself:
|
|
//
|
|
// nonce=9, gasPrice=20gwei, gas=21000,
|
|
// to=0x3535353535353535353535353535353535353535, value=1ether,
|
|
// data=empty, chainID=1, r=0, s=0
|
|
//
|
|
// signing hash should be:
|
|
//
|
|
// 0xdaf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53
|
|
//
|
|
// The signed-tx hex (with the specific r,s,v from the EIP-155 example)
|
|
// is also documented. We test PreSign against the sighash.
|
|
func TestPreSign_EIP155Vector(t *testing.T) {
|
|
// Build a Provider that returns the EIP-155 example nonce + gasPrice.
|
|
// Note: StaticProvider strips the 0x prefix when looking up so we
|
|
// store the key without it.
|
|
p := &StaticProvider{
|
|
Nonces: map[string]uint64{
|
|
"TESTNET|3535353535353535353535353535353535353535": 9,
|
|
},
|
|
GasPrice: map[string]*big.Int{
|
|
"TESTNET": big.NewInt(20_000_000_000), // 20 gwei
|
|
},
|
|
}
|
|
a := New(p)
|
|
a.SetNetwork("TESTNET", PerNetwork{
|
|
ChainID: big.NewInt(1),
|
|
DefaultGasLimit: 21000,
|
|
NativeDecimals: 18,
|
|
})
|
|
|
|
// EIP-155 example sender is implicit; we hand the same address as
|
|
// "sender" so the provider returns nonce 9. The destination is the
|
|
// same 0x35... address per the EIP example.
|
|
intent := SwapIntent{
|
|
DestinationNetwork: "TESTNET",
|
|
DestinationAddress: "0x3535353535353535353535353535353535353535",
|
|
Amount: 1.0,
|
|
SenderAddress: "0x3535353535353535353535353535353535353535",
|
|
}
|
|
unsigned, err := a.PreSign(context.Background(), intent)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gotSighash := hex.EncodeToString(unsigned.SigHash[:])
|
|
wantSighash := "daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53"
|
|
if gotSighash != wantSighash {
|
|
t.Errorf("sighash:\n got %s\n want %s", gotSighash, wantSighash)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// PreSign — modes + RPC config
|
|
// =============================================================================
|
|
|
|
func TestPreSign_PureTransferMode(t *testing.T) {
|
|
p := &StaticProvider{
|
|
GasPrice: map[string]*big.Int{"LUX_TESTNET": big.NewInt(25_000_000_000)},
|
|
}
|
|
a := New(p)
|
|
a.SetNetwork("LUX_TESTNET", PerNetwork{
|
|
ChainID: big.NewInt(96368),
|
|
NativeDecimals: 18,
|
|
// BridgeContract empty → pure transfer mode.
|
|
})
|
|
|
|
unsigned, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "LUX_TESTNET",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 0.1,
|
|
SenderAddress: "0xMPC1111111111111111111111111111111111111111",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Transfer mode: `data` is empty.
|
|
if len(unsigned.Data) != 0 {
|
|
t.Errorf("transfer mode should have empty data, got %x", unsigned.Data)
|
|
}
|
|
// `to` is the destination address, not a contract.
|
|
if hex.EncodeToString(unsigned.To) != "a28fae14eb42e7a5c36ad2d774a2b7eb293c4473" {
|
|
t.Errorf("to mismatch: %x", unsigned.To)
|
|
}
|
|
// Value is 0.1 ether = 10^17 wei.
|
|
if unsigned.Value.String() != "100000000000000000" {
|
|
t.Errorf("value = %s, want 100000000000000000", unsigned.Value.String())
|
|
}
|
|
// Default gas limit for empty-data transfers is 21000.
|
|
if unsigned.GasLimit != 21000 {
|
|
t.Errorf("gas limit = %d, want 21000", unsigned.GasLimit)
|
|
}
|
|
}
|
|
|
|
func TestPreSign_BridgeContractMode(t *testing.T) {
|
|
p := &StaticProvider{
|
|
GasPrice: map[string]*big.Int{"LUX_TESTNET": big.NewInt(1)},
|
|
}
|
|
a := New(p)
|
|
bridge := "0x5B562e80A56b600d729371eB14fE3B83298D0642"
|
|
a.SetNetwork("LUX_TESTNET", PerNetwork{
|
|
ChainID: big.NewInt(96368),
|
|
BridgeContract: bridge,
|
|
ReleaseSelector: FunctionSelector("release(address,uint256)"),
|
|
NativeDecimals: 18,
|
|
})
|
|
unsigned, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "LUX_TESTNET",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 0.1,
|
|
SenderAddress: "0xMPC1111111111111111111111111111111111111111",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// to = bridge contract
|
|
if hex.EncodeToString(unsigned.To) != "5b562e80a56b600d729371eb14fe3b83298d0642" {
|
|
t.Errorf("to should be the bridge contract, got %x", unsigned.To)
|
|
}
|
|
// data = selector(4) || addr-padded(32) || amount(32) = 68 bytes
|
|
if len(unsigned.Data) != 68 {
|
|
t.Errorf("data length = %d, want 68", len(unsigned.Data))
|
|
}
|
|
// First 4 bytes = release selector
|
|
gotSel := unsigned.Data[:4]
|
|
wantSel := FunctionSelector("release(address,uint256)")
|
|
if hex.EncodeToString(gotSel) != hex.EncodeToString(wantSel[:]) {
|
|
t.Errorf("selector mismatch: got %x, want %x", gotSel, wantSel)
|
|
}
|
|
// Value is 0 (funds move via contract)
|
|
if unsigned.Value.Sign() != 0 {
|
|
t.Errorf("contract-mode value must be 0, got %s", unsigned.Value.String())
|
|
}
|
|
}
|
|
|
|
func TestPreSign_UnknownNetwork(t *testing.T) {
|
|
a := New(&StaticProvider{})
|
|
_, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "MARS_MAINNET",
|
|
Amount: 1,
|
|
SenderAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "no config for network") {
|
|
t.Errorf("expected 'no config' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPreSign_BadAddress(t *testing.T) {
|
|
a := New(&StaticProvider{GasPrice: map[string]*big.Int{"LUX_TESTNET": big.NewInt(1)}})
|
|
a.SetNetwork("LUX_TESTNET", PerNetwork{ChainID: big.NewInt(96368)})
|
|
_, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "LUX_TESTNET",
|
|
Amount: 1,
|
|
SenderAddress: "0xa28f",
|
|
DestinationAddress: "0xnotanaddr",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "DestinationAddress") {
|
|
t.Errorf("expected DestinationAddress error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// ERC-20 destination mode
|
|
// =============================================================================
|
|
|
|
func TestPreSign_ERC20Mode(t *testing.T) {
|
|
p := &StaticProvider{
|
|
GasPrice: map[string]*big.Int{"ETHEREUM_SEPOLIA": big.NewInt(20_000_000_000)},
|
|
}
|
|
a := New(p)
|
|
a.SetNetwork("ETHEREUM_SEPOLIA", txAsmNetwork(11155111))
|
|
|
|
// Register USDC on Sepolia: 6 decimals, real contract address.
|
|
reg := tokens.NewRegistry()
|
|
usdcContract := "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
|
|
_ = reg.Register(tokens.Info{
|
|
Network: "ETHEREUM_SEPOLIA",
|
|
Asset: "USDC",
|
|
Contract: usdcContract,
|
|
Decimals: 6,
|
|
})
|
|
a.Tokens = reg
|
|
|
|
unsigned, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "ETHEREUM_SEPOLIA",
|
|
DestinationAsset: "USDC",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 100, // 100 USDC
|
|
SenderAddress: "0x3535353535353535353535353535353535353535",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// to = USDC contract (not the recipient address)
|
|
wantTo := strings.TrimPrefix(strings.ToLower(usdcContract), "0x")
|
|
if hex.EncodeToString(unsigned.To) != wantTo {
|
|
t.Errorf("to = %x, want %s (USDC contract)", unsigned.To, wantTo)
|
|
}
|
|
// value = 0 (ERC-20 transfers carry no ETH value)
|
|
if unsigned.Value.Sign() != 0 {
|
|
t.Errorf("ERC-20 tx value must be 0, got %s", unsigned.Value.String())
|
|
}
|
|
// data = selector(4) || address-word(32) || amount-word(32) = 68 bytes
|
|
if len(unsigned.Data) != 68 {
|
|
t.Fatalf("data length = %d, want 68", len(unsigned.Data))
|
|
}
|
|
// First 4 bytes are the canonical transfer(address,uint256) selector.
|
|
if hex.EncodeToString(unsigned.Data[:4]) != "a9059cbb" {
|
|
t.Errorf("selector = %x, want a9059cbb (transfer)", unsigned.Data[:4])
|
|
}
|
|
// Amount word: 100 USDC = 100 * 10^6 = 100_000_000 base units = 0x5F5E100.
|
|
// Last 32 bytes of data = the amount word. Strip leading zeros.
|
|
amountWord := unsigned.Data[36:68]
|
|
amount := new(big.Int).SetBytes(amountWord)
|
|
want := big.NewInt(100_000_000)
|
|
if amount.Cmp(want) != 0 {
|
|
t.Errorf("amount in calldata = %s, want %s (100 USDC at 6 decimals)", amount, want)
|
|
}
|
|
// Recipient word: last 20 bytes of bytes 4..36 = the recipient.
|
|
recipientWord := unsigned.Data[4:36]
|
|
if !strings.EqualFold(hex.EncodeToString(recipientWord[12:]), "a28fae14eb42e7a5c36ad2d774a2b7eb293c4473") {
|
|
t.Errorf("recipient in calldata: %x", recipientWord)
|
|
}
|
|
}
|
|
|
|
func TestPreSign_ERC20_DecimalsMatter(t *testing.T) {
|
|
// BSC USDC uses 18 decimals (not 6 like mainnet/sepolia). Make
|
|
// sure the amount scaling picks up the right decimals.
|
|
p := &StaticProvider{
|
|
GasPrice: map[string]*big.Int{"BSC_MAINNET": big.NewInt(5_000_000_000)},
|
|
}
|
|
a := New(p)
|
|
a.SetNetwork("BSC_MAINNET", txAsmNetwork(56))
|
|
reg := tokens.NewRegistry()
|
|
_ = reg.Register(tokens.Info{
|
|
Network: "BSC_MAINNET",
|
|
Asset: "USDC",
|
|
Contract: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
Decimals: 18, // BSC convention
|
|
})
|
|
a.Tokens = reg
|
|
|
|
unsigned, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "BSC_MAINNET",
|
|
DestinationAsset: "USDC",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 1, // 1 USDC at 18 decimals = 10^18 base units
|
|
SenderAddress: "0x3535353535353535353535353535353535353535",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
amountWord := unsigned.Data[36:68]
|
|
amount := new(big.Int).SetBytes(amountWord)
|
|
want, _ := new(big.Int).SetString("1000000000000000000", 10) // 10^18
|
|
if amount.Cmp(want) != 0 {
|
|
t.Errorf("amount = %s, want %s (1 USDC at BSC's 18 decimals)", amount, want)
|
|
}
|
|
}
|
|
|
|
func TestPreSign_NativeWithRegistry_StillUsesNativePath(t *testing.T) {
|
|
// ETH registered with empty contract → IsNative() → pure transfer.
|
|
p := &StaticProvider{GasPrice: map[string]*big.Int{"LUX_TESTNET": big.NewInt(25_000_000_000)}}
|
|
a := New(p)
|
|
a.SetNetwork("LUX_TESTNET", txAsmNetwork(96368))
|
|
reg := tokens.NewRegistry()
|
|
_ = reg.Register(tokens.Info{Network: "LUX_TESTNET", Asset: "LUX", Decimals: 18})
|
|
a.Tokens = reg
|
|
|
|
unsigned, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "LUX_TESTNET",
|
|
DestinationAsset: "LUX",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 0.5,
|
|
SenderAddress: "0x3535353535353535353535353535353535353535",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(unsigned.Data) != 0 {
|
|
t.Errorf("native path should have empty data, got %x", unsigned.Data)
|
|
}
|
|
if unsigned.Value.String() != "500000000000000000" {
|
|
t.Errorf("value = %s, want 0.5 ether (5*10^17 wei)", unsigned.Value.String())
|
|
}
|
|
}
|
|
|
|
func TestPreSign_ERC20MissingContract_RejectsClean(t *testing.T) {
|
|
// Registry has the asset entry but with a bad contract string.
|
|
p := &StaticProvider{GasPrice: map[string]*big.Int{"X": big.NewInt(1)}}
|
|
a := New(p)
|
|
a.SetNetwork("X", txAsmNetwork(1))
|
|
reg := tokens.NewRegistry()
|
|
_ = reg.Register(tokens.Info{
|
|
Network: "X",
|
|
Asset: "BADTOKEN",
|
|
Contract: "0xshort",
|
|
Decimals: 6,
|
|
})
|
|
a.Tokens = reg
|
|
_, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "X",
|
|
DestinationAsset: "BADTOKEN",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 1,
|
|
SenderAddress: "0x3535353535353535353535353535353535353535",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "ERC-20 contract") {
|
|
t.Errorf("expected ERC-20 contract error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPreSign_UnknownAsset_FallsBackToNative(t *testing.T) {
|
|
// DestinationAsset is "UNOBTAINIUM" — not in registry → falls back
|
|
// to native pure-transfer mode (sanest default, preserves
|
|
// backward compat for tests that don't seed a registry).
|
|
p := &StaticProvider{GasPrice: map[string]*big.Int{"LUX_TESTNET": big.NewInt(1)}}
|
|
a := New(p)
|
|
a.SetNetwork("LUX_TESTNET", txAsmNetwork(96368))
|
|
a.Tokens = tokens.NewRegistry() // empty registry
|
|
unsigned, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "LUX_TESTNET",
|
|
DestinationAsset: "UNOBTAINIUM",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 0.001,
|
|
SenderAddress: "0x3535353535353535353535353535353535353535",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(unsigned.Data) != 0 {
|
|
t.Errorf("unknown asset should fall back to native path; got data=%x", unsigned.Data)
|
|
}
|
|
}
|
|
|
|
// txAsmNetwork is a small helper to build a default PerNetwork for tests.
|
|
func txAsmNetwork(chainID int64) PerNetwork {
|
|
return PerNetwork{
|
|
ChainID: big.NewInt(chainID),
|
|
DefaultGasLimit: 100_000,
|
|
NativeDecimals: 18,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Finalize — combine unsigned with sig + ParseRSV
|
|
// =============================================================================
|
|
|
|
func TestFinalize_ProducesValidRLP(t *testing.T) {
|
|
p := &StaticProvider{
|
|
GasPrice: map[string]*big.Int{"LUX_TESTNET": big.NewInt(20_000_000_000)},
|
|
}
|
|
a := New(p)
|
|
a.SetNetwork("LUX_TESTNET", PerNetwork{
|
|
ChainID: big.NewInt(96368),
|
|
DefaultGasLimit: 21000,
|
|
NativeDecimals: 18,
|
|
})
|
|
unsigned, err := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "LUX_TESTNET",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 0.1,
|
|
SenderAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Synthetic r, s, recoveryID — we only check the WIRE is well-formed.
|
|
r := new(big.Int).SetInt64(0xdeadbeef)
|
|
s := new(big.Int).SetInt64(0xcafe0001)
|
|
rawHex, err := a.Finalize(unsigned, r, s, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(rawHex, "0x") {
|
|
t.Errorf("expected 0x prefix, got %q", rawHex)
|
|
}
|
|
// Sanity-check it decodes as hex.
|
|
if _, err := hex.DecodeString(strings.TrimPrefix(rawHex, "0x")); err != nil {
|
|
t.Errorf("not valid hex: %v", err)
|
|
}
|
|
// v must encode 96368*2 + 35 + 0 = 192771 = 0x2f0c3.
|
|
// Decoded RLP would show v = 0x830002f0c3 (3 bytes prefix) — checking the
|
|
// raw bytes is fragile; trust the EIP-155 vector test above for sighash
|
|
// correctness and round-trip a synthetic value here.
|
|
}
|
|
|
|
func TestFinalize_RecoveryIDRange(t *testing.T) {
|
|
a := New(&StaticProvider{GasPrice: map[string]*big.Int{"LUX": big.NewInt(1)}})
|
|
a.SetNetwork("LUX", PerNetwork{ChainID: big.NewInt(1), DefaultGasLimit: 21000})
|
|
unsigned, _ := a.PreSign(context.Background(), SwapIntent{
|
|
DestinationNetwork: "LUX",
|
|
DestinationAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
SenderAddress: "0xa28fAE14eB42e7A5C36Ad2D774a2b7Eb293c4473",
|
|
Amount: 0.001,
|
|
})
|
|
if _, err := a.Finalize(unsigned, big.NewInt(1), big.NewInt(2), 2); err == nil {
|
|
t.Error("expected recovery id > 1 to error")
|
|
}
|
|
}
|
|
|
|
func TestParseRSV(t *testing.T) {
|
|
// Build a 65-byte signature: r = 0x01...01 (32), s = 0x02...02 (32), v = 0x01.
|
|
r := strings.Repeat("01", 32)
|
|
s := strings.Repeat("02", 32)
|
|
v := "01"
|
|
sigHex := "0x" + r + s + v
|
|
|
|
gotR, gotS, gotV, err := ParseRSV(sigHex)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantR, _ := new(big.Int).SetString(r, 16)
|
|
wantS, _ := new(big.Int).SetString(s, 16)
|
|
if gotR.Cmp(wantR) != 0 {
|
|
t.Errorf("r mismatch")
|
|
}
|
|
if gotS.Cmp(wantS) != 0 {
|
|
t.Errorf("s mismatch")
|
|
}
|
|
if gotV != 1 {
|
|
t.Errorf("v = %d, want 1", gotV)
|
|
}
|
|
}
|
|
|
|
func TestParseRSV_LegacyV27_28(t *testing.T) {
|
|
// Some signers emit v=27 instead of 0, v=28 instead of 1.
|
|
sigHex28 := "0x" + strings.Repeat("01", 32) + strings.Repeat("02", 32) + "1c" // 0x1c = 28
|
|
_, _, v, err := ParseRSV(sigHex28)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v != 1 {
|
|
t.Errorf("v=28 should normalize to 1, got %d", v)
|
|
}
|
|
sigHex27 := "0x" + strings.Repeat("01", 32) + strings.Repeat("02", 32) + "1b"
|
|
_, _, v, err = ParseRSV(sigHex27)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v != 0 {
|
|
t.Errorf("v=27 should normalize to 0, got %d", v)
|
|
}
|
|
}
|
|
|
|
func TestParseRSV_BadLength(t *testing.T) {
|
|
if _, _, _, err := ParseRSV("0xabc"); err == nil {
|
|
t.Error("expected length error")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// StaticProvider helpers
|
|
// =============================================================================
|
|
|
|
func TestStaticProvider_NonceAutoIncrements(t *testing.T) {
|
|
p := &StaticProvider{}
|
|
for i := 0; i < 5; i++ {
|
|
got, err := p.PendingNonce(context.Background(), "X", "0xAddr")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != uint64(i) {
|
|
t.Errorf("call %d: got %d, want %d", i, got, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStaticProvider_GasPriceUnknownErrors(t *testing.T) {
|
|
p := &StaticProvider{}
|
|
if _, err := p.SuggestGasPrice(context.Background(), "X"); err == nil {
|
|
t.Error("expected error for unconfigured network")
|
|
}
|
|
}
|