mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
feat: testnet configuration set, updated deposit workflow with mpc workflow, not teleport
This commit is contained in:
@@ -0,0 +1,175 @@
|
|||||||
|
# Phase 1.3 sign-off — end-to-end testnet bridge
|
||||||
|
|
||||||
|
Goal: flip `REQUIREMENTS.md` §7, row 1.3 from 🟡 to ✅. The acceptance bar
|
||||||
|
is one signed bridge transaction from Sepolia → Lux Testnet that the m-chain
|
||||||
|
MPC quorum completes.
|
||||||
|
|
||||||
|
This is a one-shot human-in-the-loop verification. It's not part of CI —
|
||||||
|
running this requires a real wallet + faucet funds, so it lives outside the
|
||||||
|
automated test suite.
|
||||||
|
|
||||||
|
## Code state (verified before this doc was written)
|
||||||
|
|
||||||
|
These are pre-conditions the SDK + tenant satisfy as of the current branch:
|
||||||
|
|
||||||
|
- `useNetworks.ts` sends `?version=${cfg.env}` to `/api/networks`, so the
|
||||||
|
server returns the testnet registry instead of mainnet defaults.
|
||||||
|
- `wagmi-config.ts` ships testnet viem chains
|
||||||
|
(`sepolia`, `arbitrumSepolia`, `baseSepolia`, `optimismSepolia`,
|
||||||
|
`polygonAmoy`, `holesky`, `bscTestnet`) when `cfg.env === 'testnet' || 'devnet'`,
|
||||||
|
so the wagmi connector can `switchChain(11155111)` without rejecting.
|
||||||
|
- `app/bridge/src/bridge.config.ts` picks `[11155111, 84532, 17000]` as the
|
||||||
|
fallback wallet allow-list and `defaultChainId = 11155111` when
|
||||||
|
`BRIDGE_ENV=testnet`.
|
||||||
|
- Visual smoke: `app/server/result-img/testnet-{landing,chains}.png` show the
|
||||||
|
TESTNET header chip and 10 testnet chains in the picker.
|
||||||
|
- Backend deps verified live: `bridge-api.lux.network/api/networks?version=testnet`
|
||||||
|
returns 10 active testnet networks; `/api/quote` accepts
|
||||||
|
`ETHEREUM_SEPOLIA → LUX_TESTNET`; `mpc.lux.network` returns 200; Lux
|
||||||
|
testnet C-Chain RPC at `api.lux-test.network/ext/bc/C/rpc` returns
|
||||||
|
chainId `0x17870` (96368); Sepolia public RPC returns chainId `0xaa36a7`
|
||||||
|
(11155111).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
| Need | How to get it |
|
||||||
|
|---|---|
|
||||||
|
| EIP-6963 wallet (MetaMask, Rabby, Brave) | Browser extension |
|
||||||
|
| Sepolia ETH (≥ 0.05 recommended) | https://www.alchemy.com/faucets/ethereum-sepolia or https://sepoliafaucet.com (any modern faucet — usually requires GitHub or Alchemy login) |
|
||||||
|
| Recipient address on Lux Testnet | Lux Testnet is EVM chain 96368; reuse your MetaMask address — same hex string works as the destination |
|
||||||
|
| (Optional) Lux Testnet RPC added to MetaMask | RPC `https://api.lux-test.network/ext/bc/C/rpc`, chainId `96368`, symbol `LUX`, explorer `https://explore.lux-test.network/` — lets you confirm the destination balance after settlement |
|
||||||
|
|
||||||
|
## Run the app
|
||||||
|
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_BRIDGE_ENV=testnet pnpm -C app/bridge dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Vite listens on `http://localhost:3001` (falls back to `:3002` if busy).
|
||||||
|
The dev proxy forwards `/api/*` to `bridge-api.lux.network` with the
|
||||||
|
production `Origin` header injected — CORS allow-list passes without
|
||||||
|
deploying anything.
|
||||||
|
|
||||||
|
If you want a `BRIDGE_*` runtime path instead of the build-time
|
||||||
|
`VITE_BRIDGE_*` path, build the docker image and run it locally with the
|
||||||
|
operator runbook in `DEPLOY.md` §3 "Smoke-test the image locally", but pass
|
||||||
|
`-e BRIDGE_ENV=testnet`.
|
||||||
|
|
||||||
|
## Walkthrough
|
||||||
|
|
||||||
|
Each step has a checkbox that must be true to proceed. Stop at the first
|
||||||
|
failure and capture the symptom — silent fall-through to mainnet defaults
|
||||||
|
is the failure mode this doc is designed to catch.
|
||||||
|
|
||||||
|
### 1. Render check
|
||||||
|
|
||||||
|
Open `http://localhost:3001/` in a fresh incognito window.
|
||||||
|
|
||||||
|
- [ ] Header reads `LUX Bridge` + a `TESTNET` chip + `MPC · CGGMP21` chip.
|
||||||
|
If the chip says `MAINNET`, the env var didn't reach `import.meta.env`
|
||||||
|
— restart Vite with the prefix.
|
||||||
|
- [ ] Devtools → Network → first request to `/api/networks` carries
|
||||||
|
`?version=testnet`. If the query string is absent the SDK regressed —
|
||||||
|
see `pkg/bridge/src/app/hooks/useNetworks.ts`.
|
||||||
|
- [ ] Footer says `Network: testnet`.
|
||||||
|
|
||||||
|
### 2. Chain registry
|
||||||
|
|
||||||
|
Click the **FROM** chain selector.
|
||||||
|
|
||||||
|
- [ ] Dropdown shows at least: `Ethereum Sepolia`, `Base Sepolia`,
|
||||||
|
`Holesky Testnet`, `Lux Testnet`, `Zoo Testnet`, `BSC Testnet`,
|
||||||
|
`Bitcoin Testnet`, `Solana Devnet`, `Ton Testnet`, `XRP Testnet`.
|
||||||
|
- [ ] Dropdown does **not** show `Ethereum`, `Base`, `Polygon`, `Arbitrum
|
||||||
|
One`, `Optimism`, or any other mainnet entry. If any mainnet row
|
||||||
|
appears, the `is_testnet` boundary in `network-mapper.ts` regressed.
|
||||||
|
|
||||||
|
Pick **FROM = Ethereum Sepolia, ETH**. Pick **TO = Lux Testnet, LUX**.
|
||||||
|
|
||||||
|
### 3. Quote
|
||||||
|
|
||||||
|
Enter `0.01` in the **You send** amount field.
|
||||||
|
|
||||||
|
- [ ] A receive amount renders within ~1 s in the **You receive** row.
|
||||||
|
- [ ] Network tab shows `GET /api/quote?source_network=ETHEREUM_SEPOLIA&source_token=ETH&destination_network=LUX_TESTNET&destination_token=LUX&amount=0.01&refuel=0`.
|
||||||
|
- [ ] No `Failed to fetch` errors in the console. (If you see one, check
|
||||||
|
that the dev proxy is still up and the upstream backend is healthy
|
||||||
|
at https://bridge-api.lux.network/api/networks?version=testnet.)
|
||||||
|
|
||||||
|
### 4. Wallet
|
||||||
|
|
||||||
|
Click **Connect Wallet**. The picker (`WalletConnect.tsx` portal modal)
|
||||||
|
should open.
|
||||||
|
|
||||||
|
- [ ] An "Installed" group is shown if you have a browser-extension wallet —
|
||||||
|
MetaMask (or whichever) is listed with a "Detected" badge.
|
||||||
|
- [ ] A "Popular" group lists Coinbase Wallet (and WalletConnect if
|
||||||
|
`VITE_WC_PROJECT_ID` is set).
|
||||||
|
|
||||||
|
Click MetaMask (or your extension).
|
||||||
|
|
||||||
|
- [ ] Wallet popup asks to connect.
|
||||||
|
- [ ] After approval, the picker closes; the header **Connect Wallet** button
|
||||||
|
is replaced by your truncated address.
|
||||||
|
- [ ] If MetaMask is on a different chain it should prompt to switch to
|
||||||
|
**Sepolia (chainId 11155111)**. Approve.
|
||||||
|
|
||||||
|
### 5. Submit + sign + settle
|
||||||
|
|
||||||
|
Type your own Sepolia address into the **Destination address** field (or
|
||||||
|
toggle whichever "use connected wallet" affordance the UI exposes).
|
||||||
|
|
||||||
|
Click **Bridge** (or whatever the active CTA reads — it should not say
|
||||||
|
"Source and destination must differ" any longer).
|
||||||
|
|
||||||
|
- [ ] MetaMask opens a transaction popup for the Sepolia deposit. The
|
||||||
|
`to:` is the bridge deposit address surfaced by the server; the
|
||||||
|
value is `0.01 ETH`.
|
||||||
|
- [ ] Sign the transaction. A tx hash appears in the **Transfers** card
|
||||||
|
and is clickable to https://sepolia.etherscan.io/tx/<hash>.
|
||||||
|
- [ ] The transfer status advances through:
|
||||||
|
`user_transfer_pending → bridge_transfer_pending_signing → bridge_transfer_pending_broadcasting → completed`.
|
||||||
|
(Approximate phase names — the UI may render them as
|
||||||
|
`Depositing → Signing → Broadcasting → Settled` or similar.)
|
||||||
|
- [ ] The transfer card shows MPC session info:
|
||||||
|
`sessionId`, a non-empty `status`, and eventually a `signature` hex.
|
||||||
|
This is wired through `pkg/bridge/src/app/lib/mpc-session.ts`
|
||||||
|
against `https://mpc.lux.network` (`BRIDGE_MPC_PUBLIC_URL`).
|
||||||
|
- [ ] When the phase reaches `completed`, the destination address holds
|
||||||
|
the corresponding LUX on Lux Testnet — confirm with
|
||||||
|
`curl -s -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0xYOUR_ADDR","latest"]}' \
|
||||||
|
https://api.lux-test.network/ext/bc/C/rpc`.
|
||||||
|
|
||||||
|
### 6. Treasury / fee path (optional)
|
||||||
|
|
||||||
|
If `BRIDGE_MPC_PRIVATE_URL` is configured in your env, also verify the
|
||||||
|
private-cluster fee-collection signature fires on the same transfer.
|
||||||
|
For most external sign-offs leave this off — Phase 1.3 cares about the
|
||||||
|
public m-chain quorum only.
|
||||||
|
|
||||||
|
## Sign-off
|
||||||
|
|
||||||
|
If every box in §1–§5 is ticked:
|
||||||
|
|
||||||
|
1. Update `REQUIREMENTS.md` §7 row 1.3 status from `🟡` to `✅`. Add a
|
||||||
|
short evidence line under the row: testnet date, Sepolia tx hash,
|
||||||
|
Lux Testnet recipient address, MPC session id.
|
||||||
|
2. Commit with the screenshots from `app/server/result-img/testnet-*.png`
|
||||||
|
attached if you captured fresh ones during the run.
|
||||||
|
3. Phase 1.3 is now closed; Phase 1.4 (`bridge.lux.network` deploy) can
|
||||||
|
proceed without this gate.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Likely cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| Header chip says `MAINNET` | `VITE_BRIDGE_ENV` not present at Vite startup, or `window.__ENV.BRIDGE_ENV` got templated wrong | Restart `pnpm dev` with the prefix; check `/__ENV.js` in container mode |
|
||||||
|
| 22 mainnet chains in the picker | `?version` query param missing from `/api/networks` request | `useNetworks.ts` regressed — check the URL in devtools |
|
||||||
|
| `chain not configured` from wagmi on connect | `wagmi-config.ts` regressed to mainnet-only viem chain set | Re-add testnet imports + branch on `cfg.env` |
|
||||||
|
| Quote returns but submit fails with `unknown destination network` | Server expects `LUX_TESTNET`, not `LUX_MAINNET` — possibly a chain-id-to-internal-name mapping issue | Inspect the POST body; the `internalName` comes from the API row, not the bridge-api.ts dead-code map (see `chainIdToInternalName` — currently unused but mis-encoded) |
|
||||||
|
| MPC session stays at `pending` forever | `mpc.lux.network` unreachable, or `BRIDGE_MPC_PUBLIC_URL` set to something else | `curl -I https://mpc.lux.network/` — should be HTTP 200 |
|
||||||
|
| Sepolia deposit tx never confirms | Sepolia is congested / your gas was too low | Speed up via MetaMask; bridge picks up on confirmation, not on broadcast |
|
||||||
|
| Final balance never arrives on Lux Testnet | Bridge backend stuck mid-pipeline | `curl https://bridge-api.lux.network/api/swaps/<id>` for the server's view of the swap state |
|
||||||
@@ -84,12 +84,21 @@ const fallbackMpcPublicUrl = 'https://mpc.lux.network'
|
|||||||
const fallbackMpcProtocol: NonNullable<BridgeMPCConfig['protocol']> = 'cggmp21'
|
const fallbackMpcProtocol: NonNullable<BridgeMPCConfig['protocol']> = 'cggmp21'
|
||||||
|
|
||||||
// Curated EVM allow-list for the Lux tenant. The SDK's full supported set
|
// Curated EVM allow-list for the Lux tenant. The SDK's full supported set
|
||||||
// (mainnet, arbitrum, base, polygon, optimism) is intentionally narrowed
|
// (mainnet, arbitrum, base, polygon, optimism, bsc for mainnet; sepolia,
|
||||||
// here so the user wallet only sees chains the bridge backend actually
|
// arbitrumSepolia, baseSepolia, optimismSepolia, polygonAmoy, holesky,
|
||||||
// settles to. Non-EVM destinations (Lux native, Solana) are handled by the
|
// bscTestnet for testnet) is intentionally narrowed here so the user wallet
|
||||||
// MPC threshold layer and don't appear in the wagmi connector.
|
// only sees chains the bridge backend actually settles to. Non-EVM
|
||||||
const fallbackSupportedChainIds = [1, 42161, 8453, 137, 10]
|
// destinations (Lux native, Solana, BTC, TON, XRP) are handled by the MPC
|
||||||
const fallbackDefaultChainId = 1
|
// threshold layer and don't appear in the wagmi connector.
|
||||||
|
//
|
||||||
|
// Env-aware: BRIDGE_ENV=testnet → Sepolia / Base Sepolia / Holesky;
|
||||||
|
// BRIDGE_ENV=mainnet (default) → the production EVM allow-list.
|
||||||
|
const envSlug = env('BRIDGE_ENV', 'mainnet') ?? 'mainnet'
|
||||||
|
const isTestnetEnv = envSlug === 'testnet' || envSlug === 'devnet'
|
||||||
|
const fallbackSupportedChainIds = isTestnetEnv
|
||||||
|
? [11155111, 84532, 17000] // Sepolia, Base Sepolia, Holesky — the testnet pairs the bridge backend currently quotes.
|
||||||
|
: [1, 42161, 8453, 137, 10]
|
||||||
|
const fallbackDefaultChainId = isTestnetEnv ? 11155111 : 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a comma-separated list of numeric chain IDs from env, dropping
|
* Parse a comma-separated list of numeric chain IDs from env, dropping
|
||||||
@@ -157,7 +166,7 @@ const devApiHost =
|
|||||||
|
|
||||||
export const bridgeConfig: BridgeConfig = {
|
export const bridgeConfig: BridgeConfig = {
|
||||||
apiHost: devApiHost ?? env('BRIDGE_API_HOST', fallbackApiHost)!,
|
apiHost: devApiHost ?? env('BRIDGE_API_HOST', fallbackApiHost)!,
|
||||||
env: env('BRIDGE_ENV', 'mainnet')!,
|
env: envSlug,
|
||||||
clientId: env('BRIDGE_CLIENT_ID'),
|
clientId: env('BRIDGE_CLIENT_ID'),
|
||||||
iamOrg: env('BRIDGE_IAM_ORG', 'lux'),
|
iamOrg: env('BRIDGE_IAM_ORG', 'lux'),
|
||||||
brand: {
|
brand: {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
@@ -0,0 +1,170 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"description": "Canonical bridge deposit addresses for every supported chain. EVM chains use static teleporter + vault contracts; non-EVM chains use per-swap MPC-derived addresses returned by POST /api/swaps and have no static address to publish here.",
|
||||||
|
"generated_at": "2026-05-25",
|
||||||
|
"sources": {
|
||||||
|
"evm_contracts": "app/server/src/domain/contracts/teleport.ts",
|
||||||
|
"non_evm_address_types": "app/server/src/domain/mpc-wallet.ts",
|
||||||
|
"wallet_factory": "app/server/src/domain/mpc-wallet.ts::createMPCWalletForDeposit"
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"For EVM chains, the `teleporter` is the inbound contract users deposit to and the `vault` is the funds-holding contract.",
|
||||||
|
"Empty-string entries mean the chain is configured server-side but no deposit contract is currently deployed (Gnosis, Fantom, Aurora).",
|
||||||
|
"For non-EVM chains, a fresh address is derived from the MPC keys at swap-creation time. The static_address field is therefore null; address_type names the signature scheme."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"evm": {
|
||||||
|
"mainnet": [
|
||||||
|
{
|
||||||
|
"chain": "LUX_MAINNET",
|
||||||
|
"chain_id": 96369,
|
||||||
|
"teleporter": "0x5B562e80A56b600d729371eB14fE3B83298D0642",
|
||||||
|
"vault": "0x08c0f48517C6d94Dd18aB5b132CA4A84FB77108e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "ZOO_MAINNET",
|
||||||
|
"chain_id": 200200,
|
||||||
|
"teleporter": "0x5B562e80A56b600d729371eB14fE3B83298D0642",
|
||||||
|
"vault": "0x08c0f48517C6d94Dd18aB5b132CA4A84FB77108e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "ETHEREUM_MAINNET",
|
||||||
|
"chain_id": 1,
|
||||||
|
"teleporter": "0xebD1Ee9BCAaeE50085077651c1a2dD452fc6b72e",
|
||||||
|
"vault": "0xcf963Fe4E4cE126047147661e6e06e171f366506"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "POLYGON_MAINNET",
|
||||||
|
"chain_id": 137,
|
||||||
|
"teleporter": "0xE09C9b6Ed2BADAa97AB00652dF75da05adc6dAeF",
|
||||||
|
"vault": "0x217feE2a1a6A31Dda68433270531F56C91EC8D2B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "OPTIMISM_MAINNET",
|
||||||
|
"chain_id": 10,
|
||||||
|
"teleporter": "0xbdCE894aEd7d30BA0C0D0B51604ee9d225fc8b95",
|
||||||
|
"vault": "0x37d9fB96722ebDDbC8000386564945864675099B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "ARBITRUM_MAINNET",
|
||||||
|
"chain_id": 42161,
|
||||||
|
"teleporter": "0xA60429080752484044e529012aA46e1D691f50Ab",
|
||||||
|
"vault": "0xE6e3E18F86d5C35ec1E24c0be8672c8AA9989258"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "CELO_MAINNET",
|
||||||
|
"chain_id": 42220,
|
||||||
|
"teleporter": "0xbdCE894aEd7d30BA0C0D0B51604ee9d225fc8b95",
|
||||||
|
"vault": "0x37d9fB96722ebDDbC8000386564945864675099B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "BASE_MAINNET",
|
||||||
|
"chain_id": 8453,
|
||||||
|
"teleporter": "0x37d9fB96722ebDDbC8000386564945864675099B",
|
||||||
|
"vault": "0x3226bb1d3055685EFC1b0E49718B909a1c6Ce18d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "BSC_MAINNET",
|
||||||
|
"chain_id": 56,
|
||||||
|
"teleporter": "0xebD1Ee9BCAaeE50085077651c1a2dD452fc6b72e",
|
||||||
|
"vault": "0xcf963Fe4E4cE126047147661e6e06e171f366506"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "AVAX_MAINNET",
|
||||||
|
"chain_id": 43114,
|
||||||
|
"teleporter": "0x3226bb1d3055685EFC1b0E49718B909a1c6Ce18d",
|
||||||
|
"vault": "0x6149D8C44Ec9a4a683d14E494a73B1b11ac331A8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "ZORA_MAINNET",
|
||||||
|
"chain_id": 7777777,
|
||||||
|
"teleporter": "0xbdCE894aEd7d30BA0C0D0B51604ee9d225fc8b95",
|
||||||
|
"vault": "0x37d9fB96722ebDDbC8000386564945864675099B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "BLAST_MAINNET",
|
||||||
|
"chain_id": 81457,
|
||||||
|
"teleporter": "0xbdCE894aEd7d30BA0C0D0B51604ee9d225fc8b95",
|
||||||
|
"vault": "0x37d9fB96722ebDDbC8000386564945864675099B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "LINEA_MAINNET",
|
||||||
|
"chain_id": 59144,
|
||||||
|
"teleporter": "0x1e48d32a4f5e9f08db9ae4959163300faf8a6c8e",
|
||||||
|
"vault": "0x3226bb1d3055685EFC1b0E49718B909a1c6Ce18d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "GNOSIS_MAINNET",
|
||||||
|
"chain_id": 100,
|
||||||
|
"teleporter": "",
|
||||||
|
"vault": "",
|
||||||
|
"note": "Configured server-side but no deposit contract deployed yet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "FANTOM_MAINNET",
|
||||||
|
"chain_id": 250,
|
||||||
|
"teleporter": "",
|
||||||
|
"vault": "",
|
||||||
|
"note": "Configured server-side but no deposit contract deployed yet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "AURORA_MAINNET",
|
||||||
|
"chain_id": 1313161554,
|
||||||
|
"teleporter": "",
|
||||||
|
"vault": "",
|
||||||
|
"note": "Configured server-side but no deposit contract deployed yet."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"testnet": [
|
||||||
|
{
|
||||||
|
"chain": "ETHEREUM_SEPOLIA",
|
||||||
|
"chain_id": 11155111,
|
||||||
|
"teleporter": "0xb34e8a93A56De724c432fD41052E715657Fb0B1D",
|
||||||
|
"vault": "0x63139CFe43cEe881332a2264851940108333e8D0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "HOLESKY_TESTNET",
|
||||||
|
"chain_id": 17000,
|
||||||
|
"teleporter": "0x2951A9386df11a4EA8ae5A823B94DC257dEb35Cb",
|
||||||
|
"vault": "0xebD1Ee9BCAaeE50085077651c1a2dD452fc6b72e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "BASE_SEPOLIA",
|
||||||
|
"chain_id": 84532,
|
||||||
|
"teleporter": "0xE139056397091c15F860E94b20cCc169520b086A",
|
||||||
|
"vault": "0x2c4908f1B607A4448272ea29A054bf9AA08A1D76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "BSC_TESTNET",
|
||||||
|
"chain_id": 97,
|
||||||
|
"teleporter": "0xD293337c79A951C0e93aEb6a5E255097d65b671f",
|
||||||
|
"vault": "0xe0feC703840364714b97272973B8945FD5eB5600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "LUX_TESTNET",
|
||||||
|
"chain_id": 96368,
|
||||||
|
"teleporter": "0x5B562e80A56b600d729371eB14fE3B83298D0642",
|
||||||
|
"vault": "0x08c0f48517C6d94Dd18aB5b132CA4A84FB77108e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chain": "ZOO_TESTNET",
|
||||||
|
"chain_id": 200201,
|
||||||
|
"teleporter": "0x5B562e80A56b600d729371eB14fE3B83298D0642",
|
||||||
|
"vault": "0x08c0f48517C6d94Dd18aB5b132CA4A84FB77108e"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"non_evm": [
|
||||||
|
{ "chain": "BITCOIN_MAINNET", "address_type": "btc", "static_address": null },
|
||||||
|
{ "chain": "BITCOIN_TESTNET", "address_type": "btc", "static_address": null },
|
||||||
|
{ "chain": "SOLANA_MAINNET", "address_type": "sol", "static_address": null },
|
||||||
|
{ "chain": "SOLANA_DEVNET", "address_type": "sol", "static_address": null },
|
||||||
|
{ "chain": "SOLANA_TESTNET", "address_type": "sol", "static_address": null },
|
||||||
|
{ "chain": "TON_MAINNET", "address_type": "ton", "static_address": null },
|
||||||
|
{ "chain": "TON_TESTNET", "address_type": "ton", "static_address": null },
|
||||||
|
{ "chain": "XRP_MAINNET", "address_type": "xrp", "static_address": null },
|
||||||
|
{ "chain": "XRP_TESTNET", "address_type": "xrp", "static_address": null },
|
||||||
|
{ "chain": "POLKADOT_MAINNET", "address_type": "dot", "static_address": null },
|
||||||
|
{ "chain": "CARDANO_MAINNET", "address_type": "sol", "static_address": null, "note": "placeholder — needs Ed25519 finalisation in mpc-wallet.ts" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||||
|
// See the file LICENSE for licensing terms.
|
||||||
|
//
|
||||||
|
// useNetworks fetches `${apiHost}/api/networks?version=…` and transforms the
|
||||||
|
// response into the SDK's flat chains/assets shape. The version query param
|
||||||
|
// is load-bearing — without it the bridge backend defaults to mainnetSettings
|
||||||
|
// even when the SDK is configured for env=testnet, which masks the entire
|
||||||
|
// testnet network registry. These tests pin that behaviour.
|
||||||
|
|
||||||
|
import { renderHook, waitFor } from '@testing-library/react'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { setConfig } from '../config'
|
||||||
|
import { useNetworks } from '../app/hooks/useNetworks'
|
||||||
|
import { makeTestWrapper } from './test-providers'
|
||||||
|
|
||||||
|
const originalFetch = global.fetch
|
||||||
|
|
||||||
|
interface ApiNetworkJson {
|
||||||
|
display_name: string
|
||||||
|
internal_name: string
|
||||||
|
logo: string | null
|
||||||
|
native_currency: string
|
||||||
|
is_testnet: boolean
|
||||||
|
is_featured: boolean
|
||||||
|
average_completion_time: string
|
||||||
|
chain_id: string | null
|
||||||
|
type: string
|
||||||
|
status: string
|
||||||
|
transaction_explorer_template: string
|
||||||
|
account_explorer_template: string
|
||||||
|
currencies: Array<{
|
||||||
|
name: string
|
||||||
|
asset: string
|
||||||
|
logo: string | null
|
||||||
|
contract_address: string | null
|
||||||
|
decimals: number
|
||||||
|
status: string
|
||||||
|
is_deposit_enabled: boolean
|
||||||
|
is_withdrawal_enabled: boolean
|
||||||
|
is_refuel_enabled: boolean
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
const sepolia: ApiNetworkJson = {
|
||||||
|
display_name: 'Ethereum Sepolia',
|
||||||
|
internal_name: 'ETHEREUM_SEPOLIA',
|
||||||
|
logo: null,
|
||||||
|
native_currency: 'ETH',
|
||||||
|
is_testnet: true,
|
||||||
|
is_featured: true,
|
||||||
|
average_completion_time: '00:03:00',
|
||||||
|
chain_id: '11155111',
|
||||||
|
type: 'evm',
|
||||||
|
status: 'active',
|
||||||
|
transaction_explorer_template: 'https://sepolia.etherscan.io/tx/{0}',
|
||||||
|
account_explorer_template: 'https://sepolia.etherscan.io/address/{0}',
|
||||||
|
currencies: [
|
||||||
|
{
|
||||||
|
name: 'ETH',
|
||||||
|
asset: 'ETH',
|
||||||
|
logo: null,
|
||||||
|
contract_address: null,
|
||||||
|
decimals: 18,
|
||||||
|
status: 'active',
|
||||||
|
is_deposit_enabled: true,
|
||||||
|
is_withdrawal_enabled: true,
|
||||||
|
is_refuel_enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const luxTestnet: ApiNetworkJson = {
|
||||||
|
display_name: 'Lux Testnet',
|
||||||
|
internal_name: 'LUX_TESTNET',
|
||||||
|
logo: null,
|
||||||
|
native_currency: 'LUX',
|
||||||
|
is_testnet: true,
|
||||||
|
is_featured: true,
|
||||||
|
average_completion_time: '00:03:00',
|
||||||
|
chain_id: '96368',
|
||||||
|
type: 'evm',
|
||||||
|
status: 'active',
|
||||||
|
transaction_explorer_template: 'https://explore.lux-test.network/tx/{0}',
|
||||||
|
account_explorer_template: 'https://explore.lux-test.network/address/{0}',
|
||||||
|
currencies: [
|
||||||
|
{
|
||||||
|
name: 'LUX',
|
||||||
|
asset: 'LUX',
|
||||||
|
logo: null,
|
||||||
|
contract_address: null,
|
||||||
|
decimals: 18,
|
||||||
|
status: 'active',
|
||||||
|
is_deposit_enabled: true,
|
||||||
|
is_withdrawal_enabled: true,
|
||||||
|
is_refuel_enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const ethereumMainnet: ApiNetworkJson = {
|
||||||
|
display_name: 'Ethereum',
|
||||||
|
internal_name: 'ETHEREUM_MAINNET',
|
||||||
|
logo: null,
|
||||||
|
native_currency: 'ETH',
|
||||||
|
is_testnet: false,
|
||||||
|
is_featured: true,
|
||||||
|
average_completion_time: '00:03:00',
|
||||||
|
chain_id: '1',
|
||||||
|
type: 'evm',
|
||||||
|
status: 'active',
|
||||||
|
transaction_explorer_template: 'https://etherscan.io/tx/{0}',
|
||||||
|
account_explorer_template: 'https://etherscan.io/address/{0}',
|
||||||
|
currencies: [
|
||||||
|
{
|
||||||
|
name: 'ETH',
|
||||||
|
asset: 'ETH',
|
||||||
|
logo: null,
|
||||||
|
contract_address: null,
|
||||||
|
decimals: 18,
|
||||||
|
status: 'active',
|
||||||
|
is_deposit_enabled: true,
|
||||||
|
is_withdrawal_enabled: true,
|
||||||
|
is_refuel_enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockNetworksResponse(rows: ApiNetworkJson[]): ReturnType<typeof vi.fn> {
|
||||||
|
const fetchSpy = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
json: () => Promise.resolve({ data: rows }),
|
||||||
|
text: () => Promise.resolve(JSON.stringify({ data: rows })),
|
||||||
|
})
|
||||||
|
global.fetch = fetchSpy as unknown as typeof fetch
|
||||||
|
return fetchSpy
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useNetworks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setConfig({ apiHost: 'https://api.example.test', env: 'mainnet' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requests version=mainnet for env=mainnet', async () => {
|
||||||
|
const fetchSpy = mockNetworksResponse([ethereumMainnet])
|
||||||
|
const { result } = renderHook(() => useNetworks(), {
|
||||||
|
wrapper: makeTestWrapper(),
|
||||||
|
})
|
||||||
|
await waitFor(() => expect(result.current.isLoading).toBe(false))
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
||||||
|
const [calledUrl] = fetchSpy.mock.calls[0] as [string]
|
||||||
|
expect(calledUrl).toBe(
|
||||||
|
'https://api.example.test/api/networks?version=mainnet',
|
||||||
|
)
|
||||||
|
expect(result.current.chains.map((c) => c.internalName)).toContain(
|
||||||
|
'ETHEREUM_MAINNET',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requests version=testnet for env=testnet and surfaces testnet chains', async () => {
|
||||||
|
setConfig({ apiHost: 'https://api.example.test', env: 'testnet' })
|
||||||
|
const fetchSpy = mockNetworksResponse([sepolia, luxTestnet])
|
||||||
|
const { result } = renderHook(() => useNetworks(), {
|
||||||
|
wrapper: makeTestWrapper(),
|
||||||
|
})
|
||||||
|
await waitFor(() => expect(result.current.isLoading).toBe(false))
|
||||||
|
const [calledUrl] = fetchSpy.mock.calls[0] as [string]
|
||||||
|
expect(calledUrl).toBe(
|
||||||
|
'https://api.example.test/api/networks?version=testnet',
|
||||||
|
)
|
||||||
|
const internalNames = result.current.chains.map((c) => c.internalName)
|
||||||
|
expect(internalNames).toEqual(
|
||||||
|
expect.arrayContaining(['ETHEREUM_SEPOLIA', 'LUX_TESTNET']),
|
||||||
|
)
|
||||||
|
// Mainnet rows must not bleed through the env filter.
|
||||||
|
expect(internalNames).not.toContain('ETHEREUM_MAINNET')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to bundled DEFAULT_CHAINS when the API yields zero rows', async () => {
|
||||||
|
setConfig({ apiHost: 'https://api.example.test', env: 'testnet' })
|
||||||
|
// Server hands us only mainnet rows — they should be filtered out, and
|
||||||
|
// we should fall back to DEFAULT_CHAINS rather than render an empty picker.
|
||||||
|
mockNetworksResponse([ethereumMainnet])
|
||||||
|
const { result } = renderHook(() => useNetworks(), {
|
||||||
|
wrapper: makeTestWrapper(),
|
||||||
|
})
|
||||||
|
await waitFor(() => expect(result.current.isLoading).toBe(false))
|
||||||
|
expect(result.current.chains.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requests version=devnet for env=devnet', async () => {
|
||||||
|
setConfig({ apiHost: 'https://api.example.test', env: 'devnet' })
|
||||||
|
const fetchSpy = mockNetworksResponse([])
|
||||||
|
renderHook(() => useNetworks(), { wrapper: makeTestWrapper() })
|
||||||
|
await waitFor(() => expect(fetchSpy).toHaveBeenCalled())
|
||||||
|
const [calledUrl] = fetchSpy.mock.calls[0] as [string]
|
||||||
|
expect(calledUrl).toBe(
|
||||||
|
'https://api.example.test/api/networks?version=devnet',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -120,7 +120,10 @@ describe('useTransfers', () => {
|
|||||||
source_network: 'LUX_MAINNET',
|
source_network: 'LUX_MAINNET',
|
||||||
destination_network: 'ETHEREUM_MAINNET',
|
destination_network: 'ETHEREUM_MAINNET',
|
||||||
destination_address: '0xabc1234567890abcdef1234567890abcdef12345',
|
destination_address: '0xabc1234567890abcdef1234567890abcdef12345',
|
||||||
use_teleporter: true, // LUX leg → teleporter
|
// Pure MPC flow: every source gets an MPC-derived deposit address;
|
||||||
|
// the teleporter-contract dispatch is off the happy path.
|
||||||
|
use_deposit_address: true,
|
||||||
|
use_teleporter: false,
|
||||||
app_name: 'TestBridge',
|
app_name: 'TestBridge',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,22 @@ describe('buildWagmiConfig', () => {
|
|||||||
expect(ids).toContain(8453)
|
expect(ids).toContain(8453)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('exposes the testnet EVM set when env=testnet', () => {
|
||||||
|
const config = buildWagmiConfig({ ...base, env: 'testnet' })
|
||||||
|
const ids = config.chains.map((c) => c.id)
|
||||||
|
// Sepolia, Base Sepolia, Holesky must all be present; mainnet chains must not.
|
||||||
|
expect(ids).toEqual(expect.arrayContaining([11155111, 84532, 17000]))
|
||||||
|
expect(ids).not.toContain(1)
|
||||||
|
expect(ids).not.toContain(8453)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats env=devnet the same as env=testnet for chain set selection', () => {
|
||||||
|
const config = buildWagmiConfig({ ...base, env: 'devnet' })
|
||||||
|
const ids = config.chains.map((c) => c.id)
|
||||||
|
expect(ids).toContain(11155111)
|
||||||
|
expect(ids).not.toContain(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('narrows chains to supportedChainIds when provided', () => {
|
it('narrows chains to supportedChainIds when provided', () => {
|
||||||
const config = buildWagmiConfig({
|
const config = buildWagmiConfig({
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -252,14 +252,15 @@ export const SwapForm: FC<SwapFormProps> = ({ swap, wallet, transfers }) => {
|
|||||||
if (cfg.mpc?.utila) cosigners.push('Utila')
|
if (cfg.mpc?.utila) cosigners.push('Utila')
|
||||||
if (cfg.mpc?.fireblocks) cosigners.push('Fireblocks')
|
if (cfg.mpc?.fireblocks) cosigners.push('Fireblocks')
|
||||||
|
|
||||||
// Source chain family decides the flow:
|
// Every source goes through the MPC deposit-address flow: the server
|
||||||
// - EVM / Lux: wagmi connector signs the deposit tx; wallet required.
|
// mints a fresh MPC-derived address via createMPCWalletForDeposit() and
|
||||||
// - Anything else (BTC, Solana, TON, XRP, Cardano, Polkadot): the
|
// the user pays it from any wallet they own (MetaMask for EVM/Lux,
|
||||||
// server issues a deposit address and the user pays from any wallet
|
// native BTC wallet for Bitcoin, Solana wallet for SOL, etc.). There is
|
||||||
// they own. No wagmi integration needed for those chains.
|
// no wagmi writeContract path on the source side — the SDK never holds
|
||||||
|
// a private key and never calls a teleporter contract directly.
|
||||||
const sourceIsEvm =
|
const sourceIsEvm =
|
||||||
swap.fromChain.family === 'evm' || swap.fromChain.family === 'lux'
|
swap.fromChain.family === 'evm' || swap.fromChain.family === 'lux'
|
||||||
const needsDepositAddressFlow = !sourceIsEvm
|
const needsDepositAddressFlow = true
|
||||||
|
|
||||||
// Destination address resolution. We always need an address to deliver
|
// Destination address resolution. We always need an address to deliver
|
||||||
// bridged funds to. EVM destinations default to the connected wallet's
|
// bridged funds to. EVM destinations default to the connected wallet's
|
||||||
@@ -285,9 +286,6 @@ export const SwapForm: FC<SwapFormProps> = ({ swap, wallet, transfers }) => {
|
|||||||
if (!swap.quote) return 'Enter an amount'
|
if (!swap.quote) return 'Enter an amount'
|
||||||
if (sourceIsEvm && !wallet.address) return 'Connect wallet to bridge'
|
if (sourceIsEvm && !wallet.address) return 'Connect wallet to bridge'
|
||||||
if (!destOk) return `Enter ${swap.toChain.name} destination address`
|
if (!destOk) return `Enter ${swap.toChain.name} destination address`
|
||||||
if (needsDepositAddressFlow) {
|
|
||||||
return `Generate ${swap.fromAsset.symbol} deposit address`
|
|
||||||
}
|
|
||||||
return `Bridge ${swap.fromAsset.symbol} → ${swap.toAsset.symbol}`
|
return `Bridge ${swap.fromAsset.symbol} → ${swap.toAsset.symbol}`
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -398,13 +396,12 @@ export const SwapForm: FC<SwapFormProps> = ({ swap, wallet, transfers }) => {
|
|||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
aria-label="Destination address"
|
aria-label="Destination address"
|
||||||
/>
|
/>
|
||||||
{needsDepositAddressFlow ? (
|
<span style={destHint}>
|
||||||
<span style={destHint}>
|
The bridge will issue an MPC-secured deposit address — send your{' '}
|
||||||
{swap.fromChain.name} is non-EVM — the bridge will issue a
|
{swap.fromAsset.symbol} from any {swap.fromChain.name} wallet to
|
||||||
deposit address for you to send {swap.fromAsset.symbol} from any
|
that address and the {swap.toAsset.symbol} will arrive at your
|
||||||
wallet you own.
|
destination after the threshold quorum signs.
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={refuelRow}>
|
<div style={refuelRow}>
|
||||||
|
|||||||
@@ -46,9 +46,15 @@ export function useNetworks(): NetworksState {
|
|||||||
const cfg = getConfig()
|
const cfg = getConfig()
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['bridge-networks', cfg.apiHost],
|
queryKey: ['bridge-networks', cfg.apiHost, cfg.env],
|
||||||
queryFn: async ({ signal }): Promise<ApiNetwork[]> => {
|
queryFn: async ({ signal }): Promise<ApiNetwork[]> => {
|
||||||
const url = new URL('/api/networks', cfg.apiHost)
|
const url = new URL('/api/networks', cfg.apiHost)
|
||||||
|
// app/server (routes/networks.ts) selects between mainnetSettings,
|
||||||
|
// testnetSettings, devnetSettings based on ?version. Without this
|
||||||
|
// query param the server defaults to mainnet, so a testnet env build
|
||||||
|
// would silently fetch the mainnet registry and then filter to 0
|
||||||
|
// rows. Pin the param to cfg.env so the boundary stays explicit.
|
||||||
|
url.searchParams.set('version', cfg.env)
|
||||||
const resp = await fetch(url.toString(), { signal })
|
const resp = await fetch(url.toString(), { signal })
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw new Error(`networks fetch failed: HTTP ${resp.status}`)
|
throw new Error(`networks fetch failed: HTTP ${resp.status}`)
|
||||||
|
|||||||
@@ -372,13 +372,14 @@ export function useTransfers(): TransferState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-EVM source chains MUST use deposit-address mode (we have no
|
// Every source uses the MPC-derived deposit address. The server's
|
||||||
// wagmi connector for Bitcoin/Solana/TON/XRP/Cardano/Polkadot). The
|
// createMPCWalletForDeposit() returns a chain-appropriate address
|
||||||
// caller can also force it on for EVM sources by passing the flag
|
// (ETH for EVM, BTC for Bitcoin, SOL for Solana, etc.) via a single
|
||||||
// explicitly — useful for paste-the-address flows from cold wallets.
|
// MPC keygen, so we don't need a wagmi writeContract path or
|
||||||
const useDepositAddress =
|
// teleporter-contract dispatch on the client. Callers can still pass
|
||||||
input.useDepositAddress ??
|
// `useDepositAddress: false` to opt back into the legacy teleporter
|
||||||
(fromChain.family !== 'evm' && fromChain.family !== 'lux')
|
// flow on EVM sources, but the SDK default is the MPC flow.
|
||||||
|
const useDepositAddress = input.useDepositAddress ?? true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const swap = await createSwap(
|
const swap = await createSwap(
|
||||||
@@ -395,7 +396,10 @@ export function useTransfers(): TransferState {
|
|||||||
...(account.address ? { sender: account.address } : {}),
|
...(account.address ? { sender: account.address } : {}),
|
||||||
refuel: input.refuel ?? false,
|
refuel: input.refuel ?? false,
|
||||||
useDepositAddress,
|
useDepositAddress,
|
||||||
useTeleporter: fromChain.family === 'lux' || toChain.family === 'lux',
|
// MPC pipeline handles cross-chain delivery end-to-end —
|
||||||
|
// teleport-processor.ts (background loop watching teleporter
|
||||||
|
// contracts) is no longer on our happy path.
|
||||||
|
useTeleporter: false,
|
||||||
appName: input.appName ?? cfg.brand?.name ?? '@luxfi/bridge',
|
appName: input.appName ?? cfg.brand?.name ?? '@luxfi/bridge',
|
||||||
...(cosigners.length > 0 ? { cosigners } : {}),
|
...(cosigners.length > 0 ? { cosigners } : {}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,10 +18,18 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
arbitrum,
|
arbitrum,
|
||||||
|
arbitrumSepolia,
|
||||||
base,
|
base,
|
||||||
|
baseSepolia,
|
||||||
|
bsc,
|
||||||
|
bscTestnet,
|
||||||
|
holesky,
|
||||||
mainnet,
|
mainnet,
|
||||||
optimism,
|
optimism,
|
||||||
|
optimismSepolia,
|
||||||
polygon,
|
polygon,
|
||||||
|
polygonAmoy,
|
||||||
|
sepolia,
|
||||||
} from 'viem/chains'
|
} from 'viem/chains'
|
||||||
import type { Chain as ViemChain } from 'viem/chains'
|
import type { Chain as ViemChain } from 'viem/chains'
|
||||||
import { createConfig, http, type Config, type CreateConnectorFn } from 'wagmi'
|
import { createConfig, http, type Config, type CreateConnectorFn } from 'wagmi'
|
||||||
@@ -30,18 +38,48 @@ import { coinbaseWallet, injected, walletConnect } from 'wagmi/connectors'
|
|||||||
import type { BridgeConfig } from '../../types'
|
import type { BridgeConfig } from '../../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EVM chains the bridge can talk to out of the box.
|
* EVM chains the bridge can talk to out of the box, partitioned by env.
|
||||||
*
|
*
|
||||||
* Tenants narrow this via `BridgeConfig.wallet.supportedChainIds`; chains
|
* `mainnet` env builds get production chains; `testnet` env builds get the
|
||||||
* outside the allow-list are dropped from the wagmi config. Non-EVM chains
|
* testnet/devnet equivalents. The bridge backend's `?version=testnet` filter
|
||||||
* (Lux native, Solana, Bitcoin) don't appear here — they're signed via the
|
* gates the chain registry server-side; wagmi must also be configured for
|
||||||
* MPC threshold layer, not the user wallet.
|
* those chains or `switchChain(11155111)` rejects with "chain not configured."
|
||||||
|
*
|
||||||
|
* Tenants narrow either set via `BridgeConfig.wallet.supportedChainIds`;
|
||||||
|
* chains outside the allow-list are dropped from the wagmi config. Non-EVM
|
||||||
|
* chains (Lux native, Solana, Bitcoin) don't appear here — they're signed
|
||||||
|
* via the MPC threshold layer, not the user wallet.
|
||||||
*/
|
*/
|
||||||
const ALL_EVM_CHAINS: ViemChain[] = [mainnet, arbitrum, base, polygon, optimism]
|
const MAINNET_EVM_CHAINS: ViemChain[] = [mainnet, arbitrum, base, polygon, optimism, bsc]
|
||||||
|
const TESTNET_EVM_CHAINS: ViemChain[] = [
|
||||||
|
sepolia,
|
||||||
|
arbitrumSepolia,
|
||||||
|
baseSepolia,
|
||||||
|
optimismSepolia,
|
||||||
|
polygonAmoy,
|
||||||
|
holesky,
|
||||||
|
bscTestnet,
|
||||||
|
]
|
||||||
|
|
||||||
/** Lookup viem Chain by numeric chainId. */
|
/**
|
||||||
|
* EVM chains for a given env. `testnet` and `devnet` both use the testnet
|
||||||
|
* set (devnet currently rides Sepolia / Holesky for EVM legs too).
|
||||||
|
*/
|
||||||
|
function chainsForEnv(env: string): ViemChain[] {
|
||||||
|
return env === 'testnet' || env === 'devnet'
|
||||||
|
? TESTNET_EVM_CHAINS
|
||||||
|
: MAINNET_EVM_CHAINS
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Back-compat export — defaults to the mainnet set. */
|
||||||
|
const ALL_EVM_CHAINS: ViemChain[] = MAINNET_EVM_CHAINS
|
||||||
|
|
||||||
|
/** Lookup viem Chain by numeric chainId across both mainnet + testnet sets. */
|
||||||
export function viemChainById(chainId: number): ViemChain | undefined {
|
export function viemChainById(chainId: number): ViemChain | undefined {
|
||||||
return ALL_EVM_CHAINS.find((c) => c.id === chainId)
|
return (
|
||||||
|
MAINNET_EVM_CHAINS.find((c) => c.id === chainId) ??
|
||||||
|
TESTNET_EVM_CHAINS.find((c) => c.id === chainId)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,16 +92,30 @@ export function viemChainById(chainId: number): ViemChain | undefined {
|
|||||||
export function buildWagmiConfig(cfg: BridgeConfig): Config {
|
export function buildWagmiConfig(cfg: BridgeConfig): Config {
|
||||||
const wallet = cfg.wallet
|
const wallet = cfg.wallet
|
||||||
const supported = wallet?.supportedChainIds
|
const supported = wallet?.supportedChainIds
|
||||||
|
const envChains = chainsForEnv(cfg.env)
|
||||||
|
|
||||||
// Filter to the tenant's allow-list when present; otherwise expose all.
|
// Filter to the tenant's allow-list when present; otherwise expose all
|
||||||
const chains = supported && supported.length > 0
|
// chains appropriate for the current env. The allow-list may legitimately
|
||||||
? ALL_EVM_CHAINS.filter((c) => supported.includes(c.id))
|
// span both sets (a tenant could allow [1, 11155111] to switch between
|
||||||
: ALL_EVM_CHAINS
|
// mainnet+sepolia for cross-env testing), so we filter the union.
|
||||||
|
//
|
||||||
|
// Crucially: always materialize a fresh array. The hoist step below uses
|
||||||
|
// splice(), which would otherwise mutate the shared MAINNET_/TESTNET_EVM_CHAINS
|
||||||
|
// constants and silently corrupt subsequent buildWagmiConfig() calls in
|
||||||
|
// the same process (this hit the wagmi-config.test.ts suite when default-
|
||||||
|
// chain hoisting reordered the shared array).
|
||||||
|
const chains: ViemChain[] = supported && supported.length > 0
|
||||||
|
? [...MAINNET_EVM_CHAINS, ...TESTNET_EVM_CHAINS].filter((c) =>
|
||||||
|
supported.includes(c.id),
|
||||||
|
)
|
||||||
|
: [...envChains]
|
||||||
|
|
||||||
if (chains.length === 0) {
|
if (chains.length === 0) {
|
||||||
// Defensive: if the tenant's allow-list excludes every supported chain,
|
// Defensive: if the tenant's allow-list excludes every supported chain,
|
||||||
// fall back to mainnet so the wagmi createConfig() doesn't reject.
|
// fall back to env-appropriate chains[0] so wagmi createConfig() doesn't
|
||||||
chains.push(mainnet)
|
// reject. For testnet env that's sepolia; for mainnet env that's mainnet.
|
||||||
|
const fallback = envChains[0] ?? mainnet
|
||||||
|
chains.push(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order chains so wallet.defaultChainId (when provided + allowed) is first.
|
// Order chains so wallet.defaultChainId (when provided + allowed) is first.
|
||||||
|
|||||||
Reference in New Issue
Block a user