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 +6 -1
- package/README.md +21 -5
- package/dist/abi/MiningAgent.json +17 -0
- package/dist/bridge/debridge.js +128 -0
- package/dist/bridge/solana.js +129 -0
- package/dist/bridge/squid.js +128 -0
- package/dist/dashboard-html.js +321 -0
- package/dist/dashboard.js +424 -0
- package/dist/fund.js +313 -0
- package/dist/index.js +204 -0
- package/dist/smhl.js +1 -1
- package/package.json +3 -1
- package/skill.md +106 -18
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.
|
|
38
|
-
|
|
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
|
|
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
|
+
}
|