mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
bridge: reach Hanzo, Pars and Osage, and hold the chain ids in one table
The bridge settled to Lux and Zoo but not to the other three chains Lux runs, so the set it offered was smaller than the set the exchange lists. Each is an EVM L1 whose custody address is the same secp256k1 key at the same address it already derives, so a crossing needs no new key material. A network reaches a user through five tables that are edited separately — address family, deposit endpoint, broadcast endpoint, assembler endpoint, asset row — and a chain in four of them fails after the user has already sent funds. estate_test reads all five and the served config, so the next chain either arrives everywhere or fails here. The assembler's chain ids move out of main into evm.go: an EIP-155 id is what goes into the signature, and a wrong one is valid somewhere else. Native symbols are the ones each chain's own registry reports — Hanzo's coin is AI. Zoo was serving live but missing from the checked-in config. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// The estate's own L1s, and the tables that have to agree about them.
|
||||
//
|
||||
// A network reaches a user through five independent tables: the address
|
||||
// family that derives its custody address, the endpoint the deposit watcher
|
||||
// reads, the endpoint the broadcaster writes, the endpoint the assembler
|
||||
// builds against, and the asset row that gives the native coin its decimals.
|
||||
// Each is edited separately, so a chain added to four of them is a chain the
|
||||
// picker offers and the swap then fails on — and it fails late, after the
|
||||
// user has already sent funds to a deposit address.
|
||||
//
|
||||
// This asserts the five agree, for the chains Lux itself runs. It reads the
|
||||
// tables rather than restating them: a sixth table added later is not covered
|
||||
// here, but nothing here goes stale when an endpoint changes.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/bridge/internal/broadcast"
|
||||
"github.com/luxfi/bridge/internal/depositcheck"
|
||||
"github.com/luxfi/bridge/internal/mchain"
|
||||
"github.com/luxfi/bridge/internal/tokens"
|
||||
"github.com/luxfi/bridge/internal/txassembler"
|
||||
)
|
||||
|
||||
// The EVM L1s Lux runs. Chain ids and native symbols are the ones the estate
|
||||
// registry reports at api-explore.lux.network/v1/explorer/admin/chains, and
|
||||
// each endpoint was asked eth_chainId and answered the id written beside it.
|
||||
//
|
||||
// These are the chains the bridge must reach for it to be a superset of what
|
||||
// the exchange lists, which is the whole point of holding them in one place.
|
||||
var estate = []struct {
|
||||
network string
|
||||
chainID int64
|
||||
asset string
|
||||
endpoint string
|
||||
}{
|
||||
{"LUX_MAINNET", 96369, "LUX", "https://api.lux.network/v1/bc/C/rpc"},
|
||||
{"ZOO_MAINNET", 200200, "ZOO", "https://api.zoo.network/v1/bc/C/rpc"},
|
||||
{"HANZO_MAINNET", 36963, "AI", "https://api.hanzo.network/v1/bc/C/rpc"},
|
||||
{"PARS_MAINNET", 494949, "PARS", "https://api.pars.network/v1/bc/C/rpc"},
|
||||
{"OSAGE_MAINNET", 1872, "OSG", "https://api.osage.network/v1/bc/C/rpc"},
|
||||
}
|
||||
|
||||
// Custody on an EVM chain is the same secp256k1 key at the same address, so
|
||||
// every one of these derives an eth address. A chain missing from the table
|
||||
// falls through to a default; asserting it explicitly is what keeps a later
|
||||
// non-EVM addition from inheriting that default silently.
|
||||
func TestEstateDerivesEthAddresses(t *testing.T) {
|
||||
for _, c := range estate {
|
||||
if got := mchain.AddressTypeFor(c.network); got != mchain.AddressTypeETH {
|
||||
t.Errorf("%s derives %v, want eth — custody address would be malformed", c.network, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The deposit watcher reads one table, the broadcaster writes against another
|
||||
// and the assembler builds against a third. They are separate because a
|
||||
// destination may need a different provider than a source, but for a chain
|
||||
// Lux runs there is one node and all three name it.
|
||||
func TestEstateEndpointsAgree(t *testing.T) {
|
||||
assembler := txassembler.DefaultEndpoints()
|
||||
for _, c := range estate {
|
||||
for _, table := range []struct {
|
||||
name string
|
||||
got string
|
||||
}{
|
||||
{"depositcheck", depositcheck.RPCURLFor(c.network)},
|
||||
{"broadcast", broadcast.RPCURLFor(c.network)},
|
||||
{"txassembler", assembler[c.network]},
|
||||
} {
|
||||
if table.got == "" {
|
||||
t.Errorf("%s missing from %s — a swap on it cannot be %s", c.network, table.name, map[string]string{
|
||||
"depositcheck": "seen",
|
||||
"broadcast": "sent",
|
||||
"txassembler": "built",
|
||||
}[table.name])
|
||||
continue
|
||||
}
|
||||
if table.got != c.endpoint {
|
||||
t.Errorf("%s in %s reads %s, want %s", c.network, table.name, table.got, c.endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Without an asset row the native coin has no decimals, and an amount is
|
||||
// scaled by whatever the caller assumed.
|
||||
func TestEstateNativeAssetsRegistered(t *testing.T) {
|
||||
reg := tokens.DefaultRegistry()
|
||||
for _, c := range estate {
|
||||
tok, ok := reg.Lookup(c.network, c.asset)
|
||||
if !ok {
|
||||
t.Errorf("%s has no %s row — the native coin has no decimals", c.network, c.asset)
|
||||
continue
|
||||
}
|
||||
if tok.Decimals != 18 {
|
||||
t.Errorf("%s %s has %d decimals, want 18", c.network, c.asset, tok.Decimals)
|
||||
}
|
||||
if tok.Contract != "" {
|
||||
t.Errorf("%s %s carries contract %s — the native coin is not an ERC-20", c.network, c.asset, tok.Contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The shipped config is what the deployment mounts, so a chain wired through
|
||||
// the tables above but absent from it reaches nobody. This reads the file the
|
||||
// container gets and asks the handler the SPA asks, which is the only pairing
|
||||
// that shows a chain the whole way from declaration to wire.
|
||||
func TestEstateReachesTheWire(t *testing.T) {
|
||||
cfg, err := LoadConfig("networks.mainnet.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("shipped mainnet config does not load: %v", err)
|
||||
}
|
||||
app := newRigForConfig(t, cfg)
|
||||
status, body := fireRequest(t, app, http.MethodGet, "/api/networks", nil)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, body)
|
||||
}
|
||||
|
||||
served := map[string]map[string]any{}
|
||||
for _, row := range decodeNetworks(t, body) {
|
||||
name, _ := row["internal_name"].(string)
|
||||
served[name] = row
|
||||
}
|
||||
|
||||
for _, c := range estate {
|
||||
row, ok := served[c.network]
|
||||
if !ok {
|
||||
t.Errorf("%s is wired but the shipped config never offers it", c.network)
|
||||
continue
|
||||
}
|
||||
if got, _ := row["chain_id"].(string); got != itoa(c.chainID) {
|
||||
t.Errorf("%s served as chain %q, want %q", c.network, got, itoa(c.chainID))
|
||||
}
|
||||
if got, _ := row["native_currency"].(string); got != c.asset {
|
||||
t.Errorf("%s served with native %q, want %q", c.network, got, c.asset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func itoa(n int64) string { return big.NewInt(n).String() }
|
||||
|
||||
// The assembler signs with the chain id in the domain, so a wrong one here is
|
||||
// a signature the destination rejects — or, worse, one another chain accepts.
|
||||
func TestEstateChainIDsAreConfigured(t *testing.T) {
|
||||
asm := txassembler.New(nil)
|
||||
configureEVM(asm)
|
||||
for _, c := range estate {
|
||||
net, ok := asm.Networks[c.network]
|
||||
if !ok {
|
||||
t.Errorf("%s not configured on the assembler", c.network)
|
||||
continue
|
||||
}
|
||||
if net.ChainID.Cmp(big.NewInt(c.chainID)) != 0 {
|
||||
t.Errorf("%s configured as chain %s, want %d", c.network, net.ChainID, c.chainID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// The EVM chains the assembler builds transactions for, and their ids.
|
||||
//
|
||||
// This is the chain id that goes into the EIP-155 signature. A wrong one is
|
||||
// not a failed transaction — it is a signature valid on some other chain, so
|
||||
// the table is data rather than a dozen calls in main, where an id is easy to
|
||||
// read past and impossible to test.
|
||||
//
|
||||
// Every entry is eighteen-decimal because every EVM native coin is. An L2 or
|
||||
// an L1 that is not gets its own row here rather than a special case at the
|
||||
// call site.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/luxfi/bridge/internal/txassembler"
|
||||
)
|
||||
|
||||
// evmChainIDs maps a network's internal_name to its EIP-155 chain id.
|
||||
//
|
||||
// The Lux-run chains — Lux, Zoo, Hanzo, Pars, Osage — carry the ids their own
|
||||
// nodes report; each endpoint in internal/txassembler, internal/broadcast and
|
||||
// internal/depositcheck was asked eth_chainId and answered the id here. The
|
||||
// rest are the public networks the bridge settles to.
|
||||
var evmChainIDs = map[string]int64{
|
||||
// Lux and the chains it runs. Sovereign L1s, each its own primary
|
||||
// network, so an id here is the whole identity of the chain.
|
||||
"LUX_MAINNET": 96369,
|
||||
"LUX_TESTNET": 96368,
|
||||
"ZOO_MAINNET": 200200,
|
||||
"ZOO_TESTNET": 200201,
|
||||
"HANZO_MAINNET": 36963,
|
||||
"PARS_MAINNET": 494949,
|
||||
"OSAGE_MAINNET": 1872,
|
||||
|
||||
// Public networks.
|
||||
"ETHEREUM_MAINNET": 1,
|
||||
"ETHEREUM_SEPOLIA": 11155111,
|
||||
"HOLESKY_TESTNET": 17000,
|
||||
"BASE_MAINNET": 8453,
|
||||
"BASE_SEPOLIA": 84532,
|
||||
"POLYGON_MAINNET": 137,
|
||||
"BSC_MAINNET": 56,
|
||||
"BSC_TESTNET": 97,
|
||||
}
|
||||
|
||||
// configureEVM registers every EVM network on the assembler.
|
||||
func configureEVM(asm *txassembler.Assembler) {
|
||||
for network, id := range evmChainIDs {
|
||||
asm.SetNetwork(network, txassembler.PerNetwork{
|
||||
ChainID: big.NewInt(id),
|
||||
NativeDecimals: 18,
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-14
@@ -524,23 +524,10 @@ func main() {
|
||||
// override is configured. Operators can shadow specific networks
|
||||
// via --source-rpc-overrides (same flag the deposit watcher uses).
|
||||
//
|
||||
// Chain IDs sourced from app/server/src/domain/contracts/teleport.ts
|
||||
// and the LP-134 chain registry.
|
||||
asmProvider := txassembler.NewRPCProvider(overrides, 8*time.Second)
|
||||
asm := txassembler.New(asmProvider)
|
||||
asm.Tokens = tokenRegistry
|
||||
asm.SetNetwork("ETHEREUM_SEPOLIA", txassembler.PerNetwork{ChainID: big.NewInt(11155111), NativeDecimals: 18})
|
||||
asm.SetNetwork("ETHEREUM_MAINNET", txassembler.PerNetwork{ChainID: big.NewInt(1), NativeDecimals: 18})
|
||||
asm.SetNetwork("BASE_SEPOLIA", txassembler.PerNetwork{ChainID: big.NewInt(84532), NativeDecimals: 18})
|
||||
asm.SetNetwork("BASE_MAINNET", txassembler.PerNetwork{ChainID: big.NewInt(8453), NativeDecimals: 18})
|
||||
asm.SetNetwork("LUX_TESTNET", txassembler.PerNetwork{ChainID: big.NewInt(96368), NativeDecimals: 18})
|
||||
asm.SetNetwork("LUX_MAINNET", txassembler.PerNetwork{ChainID: big.NewInt(96369), NativeDecimals: 18})
|
||||
asm.SetNetwork("ZOO_TESTNET", txassembler.PerNetwork{ChainID: big.NewInt(200201), NativeDecimals: 18})
|
||||
asm.SetNetwork("ZOO_MAINNET", txassembler.PerNetwork{ChainID: big.NewInt(200200), NativeDecimals: 18})
|
||||
asm.SetNetwork("BSC_TESTNET", txassembler.PerNetwork{ChainID: big.NewInt(97), NativeDecimals: 18})
|
||||
asm.SetNetwork("BSC_MAINNET", txassembler.PerNetwork{ChainID: big.NewInt(56), NativeDecimals: 18})
|
||||
asm.SetNetwork("POLYGON_MAINNET", txassembler.PerNetwork{ChainID: big.NewInt(137), NativeDecimals: 18})
|
||||
asm.SetNetwork("HOLESKY_TESTNET", txassembler.PerNetwork{ChainID: big.NewInt(17000), NativeDecimals: 18})
|
||||
configureEVM(asm)
|
||||
|
||||
// XRP-side of the assembler. Without an XRPProvider the
|
||||
// PreSignXRP path refuses to build — the signing driver short-
|
||||
|
||||
@@ -35,6 +35,46 @@ networks:
|
||||
txExplorerTpl: https://explore.lux.network/tx/{tx}
|
||||
addrExplorerTpl: https://explore.lux.network/address/{addr}
|
||||
|
||||
# The other chains Lux runs. Each is a sovereign L1 on its own primary
|
||||
# network, so its EVM chain id is the whole identity of the chain. Custody
|
||||
# is the same secp256k1 key at the same address it derives on Lux — a
|
||||
# crossing here needs no new key material, only liquidity on the far side.
|
||||
- internalName: ZOO_MAINNET
|
||||
displayName: Zoo
|
||||
nativeCurrency: ZOO
|
||||
chainId: "200200"
|
||||
type: evm
|
||||
avgCompletion: 1m
|
||||
txExplorerTpl: https://explore-zoo.lux.network/tx/{tx}
|
||||
addrExplorerTpl: https://explore-zoo.lux.network/address/{addr}
|
||||
|
||||
- internalName: HANZO_MAINNET
|
||||
displayName: Hanzo
|
||||
nativeCurrency: AI
|
||||
chainId: "36963"
|
||||
type: evm
|
||||
avgCompletion: 1m
|
||||
txExplorerTpl: https://explore-hanzo.lux.network/tx/{tx}
|
||||
addrExplorerTpl: https://explore-hanzo.lux.network/address/{addr}
|
||||
|
||||
- internalName: PARS_MAINNET
|
||||
displayName: Pars
|
||||
nativeCurrency: PARS
|
||||
chainId: "494949"
|
||||
type: evm
|
||||
avgCompletion: 1m
|
||||
txExplorerTpl: https://explore-pars.lux.network/tx/{tx}
|
||||
addrExplorerTpl: https://explore-pars.lux.network/address/{addr}
|
||||
|
||||
- internalName: OSAGE_MAINNET
|
||||
displayName: Osage
|
||||
nativeCurrency: OSG
|
||||
chainId: "1872"
|
||||
type: evm
|
||||
avgCompletion: 1m
|
||||
txExplorerTpl: https://explore-osage.lux.network/tx/{tx}
|
||||
addrExplorerTpl: https://explore-osage.lux.network/address/{addr}
|
||||
|
||||
- internalName: ETHEREUM_MAINNET
|
||||
displayName: Ethereum
|
||||
nativeCurrency: ETH
|
||||
@@ -139,11 +179,28 @@ networks:
|
||||
addrExplorerTpl: https://livenet.xrpl.org/accounts/{addr}
|
||||
|
||||
tokens:
|
||||
# LUX
|
||||
# The chains Lux runs — native coin only. Liquid tokens get added per
|
||||
# chain when the contract list firms up.
|
||||
- asset: LUX
|
||||
name: Lux
|
||||
decimals: 18
|
||||
network: LUX_MAINNET
|
||||
- asset: ZOO
|
||||
name: Zoo
|
||||
decimals: 18
|
||||
network: ZOO_MAINNET
|
||||
- asset: AI
|
||||
name: Hanzo
|
||||
decimals: 18
|
||||
network: HANZO_MAINNET
|
||||
- asset: PARS
|
||||
name: Pars
|
||||
decimals: 18
|
||||
network: PARS_MAINNET
|
||||
- asset: OSG
|
||||
name: Osage
|
||||
decimals: 18
|
||||
network: OSAGE_MAINNET
|
||||
|
||||
# Ethereum mainnet — ETH + 3 major stablecoins
|
||||
- asset: ETH
|
||||
|
||||
@@ -67,6 +67,9 @@ var rpcURLs = map[string]string{
|
||||
"LUX_MAINNET": "https://api.lux.network/v1/bc/C/rpc",
|
||||
"LUX_TESTNET": "https://api.lux-test.network/v1/bc/C/rpc",
|
||||
"ZOO_MAINNET": "https://api.zoo.network/v1/bc/C/rpc",
|
||||
"HANZO_MAINNET": "https://api.hanzo.network/v1/bc/C/rpc",
|
||||
"PARS_MAINNET": "https://api.pars.network/v1/bc/C/rpc",
|
||||
"OSAGE_MAINNET": "https://api.osage.network/v1/bc/C/rpc",
|
||||
"ZOO_TESTNET": "https://api.zoo-test.network/v1/bc/C/rpc",
|
||||
"BSC_MAINNET": "https://bsc-dataseed.binance.org",
|
||||
"BSC_TESTNET": "https://data-seed-prebsc-1-s1.binance.org:8545",
|
||||
|
||||
@@ -68,6 +68,9 @@ var rpcURLs = map[string]string{
|
||||
"LUX_MAINNET": "https://api.lux.network/v1/bc/C/rpc",
|
||||
"LUX_TESTNET": "https://api.lux-test.network/v1/bc/C/rpc",
|
||||
"ZOO_MAINNET": "https://api.zoo.network/v1/bc/C/rpc",
|
||||
"HANZO_MAINNET": "https://api.hanzo.network/v1/bc/C/rpc",
|
||||
"PARS_MAINNET": "https://api.pars.network/v1/bc/C/rpc",
|
||||
"OSAGE_MAINNET": "https://api.osage.network/v1/bc/C/rpc",
|
||||
"ZOO_TESTNET": "https://api.zoo-test.network/v1/bc/C/rpc",
|
||||
"BSC_MAINNET": "https://bsc-dataseed.binance.org",
|
||||
"BSC_TESTNET": "https://data-seed-prebsc-1-s1.binance.org:8545",
|
||||
|
||||
@@ -104,6 +104,9 @@ var networkAddressType = map[string]AddressType{
|
||||
"ZOO_MAINNET": AddressTypeETH,
|
||||
"ZOO_TESTNET": AddressTypeETH,
|
||||
"ZOO_DEVNET": AddressTypeETH,
|
||||
"HANZO_MAINNET": AddressTypeETH,
|
||||
"PARS_MAINNET": AddressTypeETH,
|
||||
"OSAGE_MAINNET": AddressTypeETH,
|
||||
"BSC_MAINNET": AddressTypeETH,
|
||||
"BSC_TESTNET": AddressTypeETH,
|
||||
"POLYGON_MAINNET": AddressTypeETH,
|
||||
|
||||
@@ -166,6 +166,12 @@ var defaultTokens = []Info{
|
||||
{Network: "ZOO_MAINNET", Asset: "ZOO", Decimals: 18},
|
||||
{Network: "ZOO_TESTNET", Asset: "ZOO", Decimals: 18},
|
||||
|
||||
// Hanzo, Pars and Osage — native only. Symbols are the ones each
|
||||
// chain's own registry reports; Hanzo's coin is AI, not HANZO.
|
||||
{Network: "HANZO_MAINNET", Asset: "AI", Decimals: 18},
|
||||
{Network: "PARS_MAINNET", Asset: "PARS", Decimals: 18},
|
||||
{Network: "OSAGE_MAINNET", Asset: "OSG", Decimals: 18},
|
||||
|
||||
// Base — Circle's canonical USDC addresses
|
||||
{Network: "BASE_MAINNET", Asset: "ETH", Decimals: 18},
|
||||
{Network: "BASE_MAINNET", Asset: "USDC", Contract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", Decimals: 6},
|
||||
|
||||
@@ -46,6 +46,9 @@ var defaultEndpoints = map[string]string{
|
||||
"LUX_MAINNET": "https://api.lux.network/v1/bc/C/rpc",
|
||||
"LUX_TESTNET": "https://api.lux-test.network/v1/bc/C/rpc",
|
||||
"ZOO_MAINNET": "https://api.zoo.network/v1/bc/C/rpc",
|
||||
"HANZO_MAINNET": "https://api.hanzo.network/v1/bc/C/rpc",
|
||||
"PARS_MAINNET": "https://api.pars.network/v1/bc/C/rpc",
|
||||
"OSAGE_MAINNET": "https://api.osage.network/v1/bc/C/rpc",
|
||||
"ZOO_TESTNET": "https://api.zoo-test.network/v1/bc/C/rpc",
|
||||
"BSC_MAINNET": "https://bsc-dataseed.binance.org",
|
||||
"BSC_TESTNET": "https://data-seed-prebsc-1-s1.binance.org:8545",
|
||||
|
||||
Reference in New Issue
Block a user