apow-cli 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -1,7 +1,8 @@
1
1
  # Wallet
2
2
  PRIVATE_KEY=
3
3
 
4
- # RPC
4
+ # RPC — The public default works but is unreliable for sustained mining.
5
+ # We strongly recommend a free Alchemy endpoint: https://www.alchemy.com/ (no credit card)
5
6
  RPC_URL=https://mainnet.base.org
6
7
 
7
8
  # LLM Provider (openai / anthropic / gemini / ollama / claude-code / codex)
@@ -19,6 +20,10 @@ LLM_API_KEY=
19
20
  # Ollama (only if LLM_PROVIDER=ollama)
20
21
  # OLLAMA_URL=http://127.0.0.1:11434
21
22
 
23
+ # Solana bridging (only needed for `apow fund --solana`)
24
+ # SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
25
+ # SQUID_INTEGRATOR_ID=
26
+
22
27
  # Contract addresses (defaults built-in, override only if needed)
23
28
  # MINING_AGENT_ADDRESS=0xB7caD3ca5F2BD8aEC2Eb67d6E8D448099B3bC03D
24
29
  # AGENT_COIN_ADDRESS=0x12577CF0D8a07363224D6909c54C056A183e13b3
package/README.md CHANGED
@@ -16,6 +16,16 @@ Or run directly:
16
16
  npx apow-cli
17
17
  ```
18
18
 
19
+ ## Important: RPC Endpoint
20
+
21
+ > **The default public RPC (`mainnet.base.org`) is unreliable for mining.** It rate-limits aggressively and will cause frequent transaction failures. **We strongly recommend getting a free Alchemy endpoint** before you start:
22
+ >
23
+ > 1. Sign up at [alchemy.com](https://www.alchemy.com/) (free, no credit card)
24
+ > 2. Create an app → Chain: **Base** → Network: **Base Mainnet**
25
+ > 3. Copy the HTTPS URL → set `RPC_URL` in your `.env`
26
+ >
27
+ > Alchemy's free tier gives you 300M compute units/month — more than enough for mining. See [RPC Recommendations](skill.md#rpc-recommendations) for other free options.
28
+
19
29
  ## For AI Agents
20
30
 
21
31
  Any AI agent can go from zero to actively mining in 5 steps — no human interaction except funding the wallet.
@@ -28,14 +38,15 @@ npx apow-cli wallet new
28
38
  # 2. Write .env (no interactive prompts)
29
39
  cat > .env << 'EOF'
30
40
  PRIVATE_KEY=0x<from step 1>
31
- RPC_URL=https://mainnet.base.org
41
+ RPC_URL=https://mainnet.base.org # UNRELIABLE — get a free Alchemy URL (see above)
32
42
  LLM_PROVIDER=openai
33
43
  LLM_MODEL=gpt-4o-mini
34
44
  LLM_API_KEY=<your key>
35
45
  EOF
36
46
 
37
- # 3. Ask your user to fund the wallet with ≥0.005 ETH on Base
38
- # This is the ONLY step that requires human action
47
+ # 3. Fund the wallet (bridge from Solana or send ETH on Base)
48
+ npx apow-cli fund --solana # bridge SOL ETH on Base
49
+ # Or ask your user to send ≥0.005 ETH on Base directly
39
50
 
40
51
  # 4. Mint a mining rig NFT (solves SMHL challenge via LLM)
41
52
  npx apow-cli mint
@@ -66,7 +77,8 @@ If you prefer to do it yourself:
66
77
  # 1. Interactive setup — wallet, RPC, LLM config
67
78
  npx apow-cli setup
68
79
 
69
- # 2. Fund your wallet with ETH on Base (≥0.005 ETH)
80
+ # 2. Fund your wallet (bridge from Solana or send ETH directly)
81
+ npx apow-cli fund
70
82
 
71
83
  # 3. Mint a mining rig NFT
72
84
  npx apow-cli mint
@@ -80,6 +92,7 @@ npx apow-cli mine
80
92
  | Command | Description |
81
93
  |---------|-------------|
82
94
  | `apow setup` | Interactive setup wizard — configure wallet, RPC, and LLM |
95
+ | `apow fund` | Fund your wallet — bridge SOL → ETH on Base, or show deposit address |
83
96
  | `apow wallet new` | Generate a new mining wallet |
84
97
  | `apow wallet show` | Show configured wallet address |
85
98
  | `apow wallet export` | Export your wallet's private key |
@@ -94,10 +107,13 @@ Create a `.env` file or use `apow setup`:
94
107
 
95
108
  ```bash
96
109
  PRIVATE_KEY=0x... # Your wallet private key
97
- RPC_URL=https://mainnet.base.org
110
+ RPC_URL=https://mainnet.base.org # UNRELIABLE — strongly recommend a free Alchemy URL instead (see above)
98
111
  LLM_PROVIDER=openai # openai | anthropic | gemini | ollama | claude-code | codex
99
112
  LLM_MODEL=gpt-4o-mini
100
113
  LLM_API_KEY=sk-...
114
+ # Solana bridging (only for `apow fund --solana`)
115
+ # SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
116
+ # SQUID_INTEGRATOR_ID= # free, get at squidrouter.com (deposit address flow only)
101
117
  # Contract addresses (defaults built-in, override only if needed)
102
118
  # MINING_AGENT_ADDRESS=0xB7caD3ca5F2BD8aEC2Eb67d6E8D448099B3bC03D
103
119
  # AGENT_COIN_ADDRESS=0x12577CF0D8a07363224D6909c54C056A183e13b3
@@ -182,5 +182,22 @@
182
182
  "type": "uint256"
183
183
  }
184
184
  ]
185
+ },
186
+ {
187
+ "type": "function",
188
+ "name": "tokenURI",
189
+ "stateMutability": "view",
190
+ "inputs": [
191
+ {
192
+ "name": "tokenId",
193
+ "type": "uint256"
194
+ }
195
+ ],
196
+ "outputs": [
197
+ {
198
+ "name": "",
199
+ "type": "string"
200
+ }
201
+ ]
185
202
  }
186
203
  ]
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ // deBridge DLN bridge — direct signing flow (Option A).
3
+ // SOL → native ETH on Base in ~20 seconds via DLN market makers.
4
+ // No API key needed.
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.bridgeViaDeBridge = bridgeViaDeBridge;
40
+ exports.pollOrderStatus = pollOrderStatus;
41
+ const solana = __importStar(require("./solana"));
42
+ const DLN_API = "https://dln.debridge.finance/v1.0";
43
+ const SOLANA_CHAIN_ID = "7565164";
44
+ const BASE_CHAIN_ID = "8453";
45
+ const NATIVE_SOL = "11111111111111111111111111111111"; // System Program = native SOL
46
+ const NATIVE_ETH = "0x0000000000000000000000000000000000000000";
47
+ /**
48
+ * Create and submit a deBridge DLN order: SOL → ETH on Base.
49
+ * Returns after the Solana tx is confirmed. Call pollOrderStatus() to wait for fulfillment.
50
+ */
51
+ async function bridgeViaDeBridge(solanaKeypair, baseAddress, solAmount) {
52
+ const startTime = Date.now();
53
+ const lamports = Math.floor(solAmount * 1e9);
54
+ const srcPublicKey = solanaKeypair.publicKey.toBase58();
55
+ // Step 1: Get serialized bridge transaction from DLN
56
+ const params = new URLSearchParams({
57
+ srcChainId: SOLANA_CHAIN_ID,
58
+ srcChainTokenIn: NATIVE_SOL,
59
+ srcChainTokenInAmount: lamports.toString(),
60
+ dstChainId: BASE_CHAIN_ID,
61
+ dstChainTokenOut: NATIVE_ETH,
62
+ dstChainTokenOutRecipient: baseAddress,
63
+ senderAddress: srcPublicKey,
64
+ srcChainOrderAuthorityAddress: srcPublicKey,
65
+ dstChainOrderAuthorityAddress: baseAddress,
66
+ });
67
+ const response = await fetch(`${DLN_API}/dln/order/create-tx?${params}`);
68
+ if (!response.ok) {
69
+ const body = await response.text();
70
+ throw new Error(`deBridge API error (${response.status}): ${body}`);
71
+ }
72
+ const data = await response.json();
73
+ if (data.errorCode || data.error) {
74
+ throw new Error(`deBridge error: ${data.error || data.message || JSON.stringify(data)}`);
75
+ }
76
+ const orderId = data.orderId;
77
+ const txData = data.tx?.data;
78
+ if (!txData) {
79
+ throw new Error("deBridge API returned no transaction data");
80
+ }
81
+ // Step 2: Sign and submit on Solana
82
+ const txSignature = await solana.signAndSendTransaction(txData, solanaKeypair);
83
+ return {
84
+ orderId,
85
+ txSignature,
86
+ status: "submitted",
87
+ timeMs: Date.now() - startTime,
88
+ };
89
+ }
90
+ /**
91
+ * Poll deBridge order status until fulfilled, cancelled, or timeout.
92
+ * Default timeout: 5 minutes.
93
+ */
94
+ async function pollOrderStatus(orderId, onUpdate, timeoutMs = 300_000) {
95
+ const deadline = Date.now() + timeoutMs;
96
+ while (Date.now() < deadline) {
97
+ try {
98
+ const response = await fetch(`${DLN_API}/dln/order/${orderId}/status`);
99
+ if (response.ok) {
100
+ const data = await response.json();
101
+ const status = data.status || data.orderStatus || "unknown";
102
+ if (onUpdate)
103
+ onUpdate(status);
104
+ if (status === "Fulfilled" ||
105
+ status === "ClaimedUnlock" ||
106
+ status === "SentUnlock") {
107
+ return {
108
+ status: "fulfilled",
109
+ ethReceived: data.fulfilledDstAmount
110
+ ? (Number(data.fulfilledDstAmount) / 1e18).toFixed(6)
111
+ : undefined,
112
+ };
113
+ }
114
+ if (status === "Cancelled" || status === "CancelledByMaker") {
115
+ throw new Error(`Bridge order was cancelled: ${status}`);
116
+ }
117
+ }
118
+ }
119
+ catch (err) {
120
+ if (err instanceof Error && err.message.includes("cancelled"))
121
+ throw err;
122
+ // Transient fetch error — keep polling
123
+ }
124
+ await new Promise((r) => setTimeout(r, 5000));
125
+ }
126
+ throw new Error("Bridge order timed out after 5 minutes. Check deBridge explorer for order: " +
127
+ orderId);
128
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ // Solana wallet utilities — key parsing, balance checks, transaction signing.
3
+ // Uses dynamic import() for @solana/web3.js to avoid bloating startup.
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.getSolanaRpcUrl = getSolanaRpcUrl;
39
+ exports.parseSolanaKey = parseSolanaKey;
40
+ exports.getSolanaBalance = getSolanaBalance;
41
+ exports.getAddressBalance = getAddressBalance;
42
+ exports.signAndSendTransaction = signAndSendTransaction;
43
+ const DEFAULT_SOLANA_RPC = "https://api.mainnet-beta.solana.com";
44
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
45
+ function base58Decode(str) {
46
+ const bytes = [];
47
+ for (const char of str) {
48
+ const idx = BASE58_ALPHABET.indexOf(char);
49
+ if (idx === -1)
50
+ throw new Error(`Invalid base58 character: ${char}`);
51
+ let carry = idx;
52
+ for (let j = 0; j < bytes.length; j++) {
53
+ carry += bytes[j] * 58;
54
+ bytes[j] = carry & 0xff;
55
+ carry >>= 8;
56
+ }
57
+ while (carry > 0) {
58
+ bytes.push(carry & 0xff);
59
+ carry >>= 8;
60
+ }
61
+ }
62
+ // Preserve leading zeros
63
+ for (const char of str) {
64
+ if (char !== "1")
65
+ break;
66
+ bytes.push(0);
67
+ }
68
+ return new Uint8Array(bytes.reverse());
69
+ }
70
+ function getSolanaRpcUrl() {
71
+ return process.env.SOLANA_RPC_URL || DEFAULT_SOLANA_RPC;
72
+ }
73
+ /** Parse a base58-encoded Solana secret key (64 bytes) or seed (32 bytes). */
74
+ async function parseSolanaKey(input) {
75
+ const { Keypair } = await Promise.resolve().then(() => __importStar(require("@solana/web3.js")));
76
+ const trimmed = input.trim();
77
+ // Try JSON array format first (Solana CLI keygen output)
78
+ if (trimmed.startsWith("[")) {
79
+ try {
80
+ const arr = JSON.parse(trimmed);
81
+ const keypair = Keypair.fromSecretKey(new Uint8Array(arr));
82
+ return { keypair, publicKey: keypair.publicKey.toBase58() };
83
+ }
84
+ catch {
85
+ throw new Error("Invalid Solana key: looks like JSON but could not parse.");
86
+ }
87
+ }
88
+ // Base58 secret key (Phantom, Backpack export format)
89
+ const decoded = base58Decode(trimmed);
90
+ if (decoded.length === 64) {
91
+ const keypair = Keypair.fromSecretKey(decoded);
92
+ return { keypair, publicKey: keypair.publicKey.toBase58() };
93
+ }
94
+ if (decoded.length === 32) {
95
+ // 32-byte seed
96
+ const keypair = Keypair.fromSeed(decoded);
97
+ return { keypair, publicKey: keypair.publicKey.toBase58() };
98
+ }
99
+ throw new Error(`Invalid Solana key: expected 64 bytes (secret key) or 32 bytes (seed), got ${decoded.length}. Provide the full base58-encoded secret key from your wallet.`);
100
+ }
101
+ /** Get SOL balance for a public key (in SOL, not lamports). */
102
+ async function getSolanaBalance(publicKeyBase58) {
103
+ const { Connection, PublicKey } = await Promise.resolve().then(() => __importStar(require("@solana/web3.js")));
104
+ const connection = new Connection(getSolanaRpcUrl(), "confirmed");
105
+ const lamports = await connection.getBalance(new PublicKey(publicKeyBase58));
106
+ return lamports / 1e9;
107
+ }
108
+ /** Get SOL balance for any address (used to detect deposits). */
109
+ async function getAddressBalance(address) {
110
+ const { Connection, PublicKey } = await Promise.resolve().then(() => __importStar(require("@solana/web3.js")));
111
+ const connection = new Connection(getSolanaRpcUrl(), "confirmed");
112
+ const lamports = await connection.getBalance(new PublicKey(address));
113
+ return lamports / 1e9;
114
+ }
115
+ /** Deserialize, sign, and submit a base64-encoded Solana transaction. */
116
+ async function signAndSendTransaction(serializedTxBase64, keypair) {
117
+ const { Connection, VersionedTransaction } = await Promise.resolve().then(() => __importStar(require("@solana/web3.js")));
118
+ const connection = new Connection(getSolanaRpcUrl(), "confirmed");
119
+ const txBuffer = Buffer.from(serializedTxBase64, "base64");
120
+ const tx = VersionedTransaction.deserialize(txBuffer);
121
+ tx.sign([keypair]);
122
+ const signature = await connection.sendTransaction(tx, {
123
+ skipPreflight: false,
124
+ preflightCommitment: "confirmed",
125
+ });
126
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
127
+ await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, "confirmed");
128
+ return signature;
129
+ }
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ // Squid Router bridge — deposit address flow (Options B+C).
3
+ // SOL → ETH on Base via Chainflip multi-hop (~1-3 minutes).
4
+ // Requires SQUID_INTEGRATOR_ID (free, apply at squidrouter.com).
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getDepositAddress = getDepositAddress;
7
+ exports.pollBridgeStatus = pollBridgeStatus;
8
+ const SQUID_API = "https://v2.api.squidrouter.com/v2";
9
+ const SOLANA_CHAIN_ID = "solana";
10
+ const BASE_CHAIN_ID = "8453";
11
+ const NATIVE_SOL_ADDRESS = "So11111111111111111111111111111111111111112"; // Wrapped SOL mint
12
+ const NATIVE_ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
13
+ function getIntegratorId() {
14
+ const id = process.env.SQUID_INTEGRATOR_ID;
15
+ if (!id) {
16
+ throw new Error("SQUID_INTEGRATOR_ID is required for the deposit address flow.\n" +
17
+ "Get one free at https://app.squidrouter.com/\n" +
18
+ "Or use direct signing instead: apow fund --solana --key <base58>");
19
+ }
20
+ return id;
21
+ }
22
+ /**
23
+ * Get a Squid deposit address for SOL → ETH on Base bridging.
24
+ * User sends SOL to this address from any wallet; Squid handles the rest.
25
+ */
26
+ async function getDepositAddress(baseAddress, solAmount) {
27
+ const integratorId = getIntegratorId();
28
+ const lamports = Math.floor(solAmount * 1e9).toString();
29
+ // Step 1: Get route quote
30
+ const routeResponse = await fetch(`${SQUID_API}/route`, {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ "x-integrator-id": integratorId,
35
+ },
36
+ body: JSON.stringify({
37
+ fromChain: SOLANA_CHAIN_ID,
38
+ toChain: BASE_CHAIN_ID,
39
+ fromToken: NATIVE_SOL_ADDRESS,
40
+ toToken: NATIVE_ETH_ADDRESS,
41
+ fromAmount: lamports,
42
+ toAddress: baseAddress,
43
+ quoteOnly: false,
44
+ enableBoost: true,
45
+ prefer: ["CHAINFLIP_DEPOSIT_ADDRESS"],
46
+ }),
47
+ });
48
+ if (!routeResponse.ok) {
49
+ const body = await routeResponse.text();
50
+ throw new Error(`Squid route API error (${routeResponse.status}): ${body}`);
51
+ }
52
+ const routeData = (await routeResponse.json());
53
+ if (routeData.error) {
54
+ throw new Error(`Squid route error: ${routeData.error.message || JSON.stringify(routeData.error)}`);
55
+ }
56
+ // Step 2: Request deposit address from route
57
+ const depositResponse = await fetch(`${SQUID_API}/deposit-address`, {
58
+ method: "POST",
59
+ headers: {
60
+ "Content-Type": "application/json",
61
+ "x-integrator-id": integratorId,
62
+ },
63
+ body: JSON.stringify({
64
+ routeData: routeData.route,
65
+ }),
66
+ });
67
+ if (!depositResponse.ok) {
68
+ const body = await depositResponse.text();
69
+ throw new Error(`Squid deposit-address API error (${depositResponse.status}): ${body}`);
70
+ }
71
+ const depositData = (await depositResponse.json());
72
+ const estimatedReceive = routeData.route?.estimate?.toAmount
73
+ ? (Number(routeData.route.estimate.toAmount) / 1e18).toFixed(6)
74
+ : "unknown";
75
+ return {
76
+ depositAddress: depositData.depositAddress,
77
+ requestId: depositData.requestId || routeData.requestId,
78
+ expectedReceive: estimatedReceive,
79
+ expiresAt: depositData.expiresAt,
80
+ };
81
+ }
82
+ /**
83
+ * Poll Squid bridge status until complete, failed, or timeout.
84
+ * Default timeout: 10 minutes (Chainflip can be slow).
85
+ */
86
+ async function pollBridgeStatus(requestId, onUpdate, timeoutMs = 600_000) {
87
+ const integratorId = getIntegratorId();
88
+ const deadline = Date.now() + timeoutMs;
89
+ while (Date.now() < deadline) {
90
+ try {
91
+ const params = new URLSearchParams({
92
+ requestId,
93
+ bridgeType: "chainflipmultihop",
94
+ });
95
+ const response = await fetch(`${SQUID_API}/status?${params}`, {
96
+ headers: { "x-integrator-id": integratorId },
97
+ });
98
+ if (response.ok) {
99
+ const data = (await response.json());
100
+ const status = data.squidTransactionStatus || data.status || "unknown";
101
+ if (onUpdate)
102
+ onUpdate(status);
103
+ if (status === "success" ||
104
+ status === "completed" ||
105
+ status === "destination_executed") {
106
+ return {
107
+ status: "fulfilled",
108
+ ethReceived: data.toChain?.amount
109
+ ? (Number(data.toChain.amount) / 1e18).toFixed(6)
110
+ : undefined,
111
+ };
112
+ }
113
+ if (status === "failed" || status === "refunded") {
114
+ throw new Error(`Bridge failed with status: ${status}`);
115
+ }
116
+ }
117
+ }
118
+ catch (err) {
119
+ if (err instanceof Error &&
120
+ (err.message.includes("failed") || err.message.includes("refunded"))) {
121
+ throw err;
122
+ }
123
+ // Transient fetch error — keep polling
124
+ }
125
+ await new Promise((r) => setTimeout(r, 5000));
126
+ }
127
+ throw new Error("Bridge timed out after 10 minutes. Check Squid explorer: https://axelarscan.io/");
128
+ }