apow-cli 0.1.3 → 0.2.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 +4 -0
- package/README.md +9 -3
- package/dist/bridge/debridge.js +128 -0
- package/dist/bridge/solana.js +129 -0
- package/dist/bridge/squid.js +128 -0
- package/dist/fund.js +313 -0
- package/dist/index.js +13 -0
- package/dist/miner.js +9 -11
- package/dist/mint.js +23 -45
- package/dist/smhl.js +5 -5
- package/package.json +3 -1
- package/skill.md +93 -16
package/.env.example
CHANGED
|
@@ -19,6 +19,10 @@ LLM_API_KEY=
|
|
|
19
19
|
# Ollama (only if LLM_PROVIDER=ollama)
|
|
20
20
|
# OLLAMA_URL=http://127.0.0.1:11434
|
|
21
21
|
|
|
22
|
+
# Solana bridging (only needed for `apow fund --solana`)
|
|
23
|
+
# SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
|
|
24
|
+
# SQUID_INTEGRATOR_ID=
|
|
25
|
+
|
|
22
26
|
# Contract addresses (defaults built-in, override only if needed)
|
|
23
27
|
# MINING_AGENT_ADDRESS=0xB7caD3ca5F2BD8aEC2Eb67d6E8D448099B3bC03D
|
|
24
28
|
# AGENT_COIN_ADDRESS=0x12577CF0D8a07363224D6909c54C056A183e13b3
|
package/README.md
CHANGED
|
@@ -34,8 +34,9 @@ LLM_MODEL=gpt-4o-mini
|
|
|
34
34
|
LLM_API_KEY=<your key>
|
|
35
35
|
EOF
|
|
36
36
|
|
|
37
|
-
# 3.
|
|
38
|
-
|
|
37
|
+
# 3. Fund the wallet (bridge from Solana or send ETH on Base)
|
|
38
|
+
npx apow-cli fund --solana # bridge SOL → ETH on Base
|
|
39
|
+
# Or ask your user to send ≥0.005 ETH on Base directly
|
|
39
40
|
|
|
40
41
|
# 4. Mint a mining rig NFT (solves SMHL challenge via LLM)
|
|
41
42
|
npx apow-cli mint
|
|
@@ -66,7 +67,8 @@ If you prefer to do it yourself:
|
|
|
66
67
|
# 1. Interactive setup — wallet, RPC, LLM config
|
|
67
68
|
npx apow-cli setup
|
|
68
69
|
|
|
69
|
-
# 2. Fund your wallet
|
|
70
|
+
# 2. Fund your wallet (bridge from Solana or send ETH directly)
|
|
71
|
+
npx apow-cli fund
|
|
70
72
|
|
|
71
73
|
# 3. Mint a mining rig NFT
|
|
72
74
|
npx apow-cli mint
|
|
@@ -80,6 +82,7 @@ npx apow-cli mine
|
|
|
80
82
|
| Command | Description |
|
|
81
83
|
|---------|-------------|
|
|
82
84
|
| `apow setup` | Interactive setup wizard — configure wallet, RPC, and LLM |
|
|
85
|
+
| `apow fund` | Fund your wallet — bridge SOL → ETH on Base, or show deposit address |
|
|
83
86
|
| `apow wallet new` | Generate a new mining wallet |
|
|
84
87
|
| `apow wallet show` | Show configured wallet address |
|
|
85
88
|
| `apow wallet export` | Export your wallet's private key |
|
|
@@ -98,6 +101,9 @@ RPC_URL=https://mainnet.base.org
|
|
|
98
101
|
LLM_PROVIDER=openai # openai | anthropic | gemini | ollama | claude-code | codex
|
|
99
102
|
LLM_MODEL=gpt-4o-mini
|
|
100
103
|
LLM_API_KEY=sk-...
|
|
104
|
+
# Solana bridging (only for `apow fund --solana`)
|
|
105
|
+
# SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
|
|
106
|
+
# SQUID_INTEGRATOR_ID= # free, get at squidrouter.com (deposit address flow only)
|
|
101
107
|
# Contract addresses (defaults built-in, override only if needed)
|
|
102
108
|
# MINING_AGENT_ADDRESS=0xB7caD3ca5F2BD8aEC2Eb67d6E8D448099B3bC03D
|
|
103
109
|
# AGENT_COIN_ADDRESS=0x12577CF0D8a07363224D6909c54C056A183e13b3
|
|
@@ -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
|
+
}
|
package/dist/fund.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Fund command — bridge SOL → ETH on Base so Solana users can start mining.
|
|
3
|
+
// Three paths:
|
|
4
|
+
// Option A: Direct Solana signing via deBridge DLN (~20s)
|
|
5
|
+
// Option B: Deposit address + QR via Squid Router (~1-3 min)
|
|
6
|
+
// Option C: Manual send (just show Base address)
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.runFundFlow = runFundFlow;
|
|
42
|
+
const viem_1 = require("viem");
|
|
43
|
+
const wallet_1 = require("./wallet");
|
|
44
|
+
const ui = __importStar(require("./ui"));
|
|
45
|
+
async function fetchPrices() {
|
|
46
|
+
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=solana,ethereum&vs_currencies=usd");
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
throw new Error("Failed to fetch prices from CoinGecko");
|
|
49
|
+
const data = (await res.json());
|
|
50
|
+
const ethUsd = data.ethereum.usd;
|
|
51
|
+
const solUsd = data.solana.usd;
|
|
52
|
+
return { solPerEth: ethUsd / solUsd, ethPriceUsd: ethUsd, solPriceUsd: solUsd };
|
|
53
|
+
}
|
|
54
|
+
/** SOL needed for target ETH, with 10% buffer for bridge fees + slippage. */
|
|
55
|
+
function solNeededForEth(targetEth, solPerEth) {
|
|
56
|
+
return targetEth * solPerEth * 1.1;
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// QR code helper
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
async function showQrCode(text) {
|
|
62
|
+
try {
|
|
63
|
+
const mod = await Promise.resolve().then(() => __importStar(require("qrcode-terminal")));
|
|
64
|
+
const qrcode = mod.default || mod;
|
|
65
|
+
await new Promise((resolve) => {
|
|
66
|
+
qrcode.generate(text, { small: true }, (qr) => {
|
|
67
|
+
for (const line of qr.split("\n")) {
|
|
68
|
+
if (line)
|
|
69
|
+
console.log(` ${line}`);
|
|
70
|
+
}
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// QR is nice-to-have, not critical
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Option A — Direct signing via deBridge
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
async function runDirectBridge(solanaKeyInput, baseAddress, targetEth) {
|
|
83
|
+
const solanaSpinner = ui.spinner("Checking Solana balance...");
|
|
84
|
+
const solana = await Promise.resolve().then(() => __importStar(require("./bridge/solana")));
|
|
85
|
+
const debridge = await Promise.resolve().then(() => __importStar(require("./bridge/debridge")));
|
|
86
|
+
let kp;
|
|
87
|
+
try {
|
|
88
|
+
kp = await solana.parseSolanaKey(solanaKeyInput);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
solanaSpinner.fail("Invalid Solana key");
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
const balance = await solana.getSolanaBalance(kp.publicKey);
|
|
95
|
+
solanaSpinner.stop(`Solana balance: ${balance.toFixed(4)} SOL`);
|
|
96
|
+
// Prices
|
|
97
|
+
const priceSpinner = ui.spinner("Fetching prices...");
|
|
98
|
+
const prices = await fetchPrices();
|
|
99
|
+
const solAmount = solNeededForEth(targetEth, prices.solPerEth);
|
|
100
|
+
priceSpinner.stop(`SOL/ETH rate: ${prices.solPerEth.toFixed(1)} SOL = 1 ETH`);
|
|
101
|
+
if (balance < solAmount) {
|
|
102
|
+
ui.error(`Insufficient SOL. Need ~${solAmount.toFixed(4)} SOL, have ${balance.toFixed(4)} SOL.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log("");
|
|
106
|
+
ui.table([
|
|
107
|
+
["Bridging", `${solAmount.toFixed(4)} SOL → ~${targetEth.toFixed(4)} ETH on Base`],
|
|
108
|
+
["Via", "deBridge DLN (~20 seconds)"],
|
|
109
|
+
[
|
|
110
|
+
"From",
|
|
111
|
+
`${kp.publicKey.slice(0, 4)}...${kp.publicKey.slice(-4)}`,
|
|
112
|
+
],
|
|
113
|
+
["To", `${baseAddress.slice(0, 6)}...${baseAddress.slice(-4)}`],
|
|
114
|
+
]);
|
|
115
|
+
console.log("");
|
|
116
|
+
const proceed = await ui.confirm("Confirm bridge?");
|
|
117
|
+
if (!proceed) {
|
|
118
|
+
console.log(" Cancelled.");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const bridgeSpinner = ui.spinner("Signing bridge transaction...");
|
|
122
|
+
const result = await debridge.bridgeViaDeBridge(kp.keypair, baseAddress, solAmount);
|
|
123
|
+
bridgeSpinner.stop(`Submitted! Order: ${result.orderId.slice(0, 12)}...`);
|
|
124
|
+
const pollSpinner = ui.spinner("Waiting for bridge fulfillment... (~20s)");
|
|
125
|
+
const fulfillment = await debridge.pollOrderStatus(result.orderId, (status) => pollSpinner.update(`Bridge status: ${status}`));
|
|
126
|
+
const received = fulfillment.ethReceived || `~${targetEth.toFixed(4)}`;
|
|
127
|
+
pollSpinner.stop(`Bridge complete! ${received} ETH arrived on Base`);
|
|
128
|
+
console.log("");
|
|
129
|
+
console.log(` ${ui.green("Your wallet is funded!")} Next: ${ui.cyan("apow mint")}`);
|
|
130
|
+
console.log("");
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Options B+C — Deposit address via Squid
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
async function runDepositBridge(baseAddress, targetEth) {
|
|
136
|
+
const priceSpinner = ui.spinner("Fetching prices...");
|
|
137
|
+
const prices = await fetchPrices();
|
|
138
|
+
const solAmount = solNeededForEth(targetEth, prices.solPerEth);
|
|
139
|
+
priceSpinner.stop(`SOL/ETH rate: ${prices.solPerEth.toFixed(1)} SOL = 1 ETH`);
|
|
140
|
+
const addrSpinner = ui.spinner("Generating deposit address...");
|
|
141
|
+
const squid = await Promise.resolve().then(() => __importStar(require("./bridge/squid")));
|
|
142
|
+
const solana = await Promise.resolve().then(() => __importStar(require("./bridge/solana")));
|
|
143
|
+
let deposit;
|
|
144
|
+
try {
|
|
145
|
+
deposit = await squid.getDepositAddress(baseAddress, solAmount);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
addrSpinner.fail("Failed to get deposit address");
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
addrSpinner.stop("Deposit address ready");
|
|
152
|
+
// Display deposit info
|
|
153
|
+
console.log("");
|
|
154
|
+
console.log(` ${ui.bold("Send SOL to this address:")}`);
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(` ${ui.cyan(deposit.depositAddress)}`);
|
|
157
|
+
console.log("");
|
|
158
|
+
await showQrCode(deposit.depositAddress);
|
|
159
|
+
console.log("");
|
|
160
|
+
ui.table([
|
|
161
|
+
[
|
|
162
|
+
"Amount",
|
|
163
|
+
`~${solAmount.toFixed(4)} SOL (~$${(solAmount * prices.solPriceUsd).toFixed(2)})`,
|
|
164
|
+
],
|
|
165
|
+
["You'll receive", `~${deposit.expectedReceive} ETH on Base`],
|
|
166
|
+
["Bridge", "Squid Router (Chainflip)"],
|
|
167
|
+
["Time", "~1-3 minutes"],
|
|
168
|
+
]);
|
|
169
|
+
console.log("");
|
|
170
|
+
if (deposit.expiresAt) {
|
|
171
|
+
ui.warn(`Deposit address expires: ${deposit.expiresAt}`);
|
|
172
|
+
console.log("");
|
|
173
|
+
}
|
|
174
|
+
// Poll for SOL deposit
|
|
175
|
+
const depositSpinner = ui.spinner("Waiting for SOL deposit... (Ctrl+C to cancel)");
|
|
176
|
+
const initialBalance = await solana.getAddressBalance(deposit.depositAddress);
|
|
177
|
+
let depositDetected = false;
|
|
178
|
+
const depositDeadline = Date.now() + 600_000; // 10 min
|
|
179
|
+
while (!depositDetected && Date.now() < depositDeadline) {
|
|
180
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
181
|
+
try {
|
|
182
|
+
const currentBalance = await solana.getAddressBalance(deposit.depositAddress);
|
|
183
|
+
if (currentBalance > initialBalance + 0.001) {
|
|
184
|
+
depositDetected = true;
|
|
185
|
+
depositSpinner.stop(`Deposit received! ${(currentBalance - initialBalance).toFixed(4)} SOL`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Transient RPC error — keep polling
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!depositDetected) {
|
|
193
|
+
depositSpinner.fail("No deposit detected after 10 minutes");
|
|
194
|
+
ui.hint("If you sent SOL, check: https://explorer.squidrouter.com");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Poll for bridge completion
|
|
198
|
+
const bridgeSpinner = ui.spinner("Bridging SOL → ETH on Base... (~1-3 min)");
|
|
199
|
+
const result = await squid.pollBridgeStatus(deposit.requestId, (status) => bridgeSpinner.update(`Bridge status: ${status}`));
|
|
200
|
+
const received = result.ethReceived || deposit.expectedReceive;
|
|
201
|
+
bridgeSpinner.stop(`Bridge complete! ${received} ETH arrived`);
|
|
202
|
+
console.log("");
|
|
203
|
+
console.log(` ${ui.green("Your wallet is funded!")} Next: ${ui.cyan("apow mint")}`);
|
|
204
|
+
console.log("");
|
|
205
|
+
}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Option C — Manual Base address
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
async function runManualFund(baseAddress) {
|
|
210
|
+
console.log("");
|
|
211
|
+
console.log(` ${ui.bold("Send ETH on Base to this address:")}`);
|
|
212
|
+
console.log("");
|
|
213
|
+
console.log(` ${ui.cyan(baseAddress)}`);
|
|
214
|
+
console.log("");
|
|
215
|
+
await showQrCode(baseAddress);
|
|
216
|
+
console.log("");
|
|
217
|
+
console.log(` ${ui.dim("Send from any wallet — Coinbase, MetaMask, Phantom, etc.")}`);
|
|
218
|
+
console.log(` ${ui.dim("Need ~0.005 ETH to start mining.")}`);
|
|
219
|
+
console.log(` ${ui.dim("After sending, run:")} ${ui.cyan("apow mint")}`);
|
|
220
|
+
console.log("");
|
|
221
|
+
}
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Main entry point
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
async function runFundFlow(options) {
|
|
226
|
+
if (!wallet_1.account) {
|
|
227
|
+
ui.error("No wallet configured. Run `apow setup` first.");
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
const baseAddress = wallet_1.account.address;
|
|
231
|
+
const balance = await (0, wallet_1.getEthBalance)();
|
|
232
|
+
const ethBalance = Number((0, viem_1.formatEther)(balance));
|
|
233
|
+
console.log("");
|
|
234
|
+
ui.banner(["Fund Your Mining Wallet"]);
|
|
235
|
+
console.log("");
|
|
236
|
+
ui.table([
|
|
237
|
+
[
|
|
238
|
+
"Your Base wallet",
|
|
239
|
+
`${baseAddress.slice(0, 6)}...${baseAddress.slice(-4)}`,
|
|
240
|
+
],
|
|
241
|
+
[
|
|
242
|
+
"Balance",
|
|
243
|
+
`${ethBalance.toFixed(6)} ETH${ethBalance < 0.005 ? " (need ~0.005 ETH to start)" : ""}`,
|
|
244
|
+
],
|
|
245
|
+
]);
|
|
246
|
+
console.log("");
|
|
247
|
+
// Parse target ETH
|
|
248
|
+
const targetEth = options.amount ? parseFloat(options.amount) : 0.005;
|
|
249
|
+
if (isNaN(targetEth) || targetEth <= 0) {
|
|
250
|
+
ui.error("Invalid amount. Specify ETH target (e.g., --amount 0.005).");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// --key flag: direct bridge immediately
|
|
254
|
+
if (options.key) {
|
|
255
|
+
await runDirectBridge(options.key, baseAddress, targetEth);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// --solana flag: ask about key, then bridge
|
|
259
|
+
if (options.solana) {
|
|
260
|
+
const hasKey = await ui.confirm("Do you have your Solana private key?");
|
|
261
|
+
if (hasKey) {
|
|
262
|
+
const key = await ui.promptSecret("Solana private key (base58)");
|
|
263
|
+
if (!key) {
|
|
264
|
+
ui.error("No key provided.");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
await runDirectBridge(key, baseAddress, targetEth);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
await runDepositBridge(baseAddress, targetEth);
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Interactive menu
|
|
275
|
+
console.log(" How do you want to fund?");
|
|
276
|
+
console.log(` ${ui.cyan("1.")} Bridge from Solana (SOL → ETH on Base)`);
|
|
277
|
+
console.log(` ${ui.cyan("2.")} Send ETH on Base directly (from another wallet)`);
|
|
278
|
+
console.log(` ${ui.cyan("3.")} Copy address and fund manually`);
|
|
279
|
+
console.log("");
|
|
280
|
+
const choice = await ui.prompt("Choice", "1");
|
|
281
|
+
switch (choice) {
|
|
282
|
+
case "1": {
|
|
283
|
+
const hasKey = await ui.confirm("Do you have your Solana private key?");
|
|
284
|
+
if (hasKey) {
|
|
285
|
+
const key = await ui.promptSecret("Solana private key (base58)");
|
|
286
|
+
if (!key) {
|
|
287
|
+
ui.error("No key provided.");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
await runDirectBridge(key, baseAddress, targetEth);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
await runDepositBridge(baseAddress, targetEth);
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case "2": {
|
|
298
|
+
console.log("");
|
|
299
|
+
console.log(` ${ui.bold("Send ETH on Base to:")}`);
|
|
300
|
+
console.log(` ${ui.cyan(baseAddress)}`);
|
|
301
|
+
console.log("");
|
|
302
|
+
console.log(` ${ui.dim("Need ~0.005 ETH to start mining.")}`);
|
|
303
|
+
console.log(` ${ui.dim("After sending, run:")} ${ui.cyan("apow mint")}`);
|
|
304
|
+
console.log("");
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case "3":
|
|
308
|
+
default: {
|
|
309
|
+
await runManualFund(baseAddress);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ const MiningAgent_json_1 = __importDefault(require("./abi/MiningAgent.json"));
|
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const detect_1 = require("./detect");
|
|
47
47
|
const explorer_1 = require("./explorer");
|
|
48
|
+
const fund_1 = require("./fund");
|
|
48
49
|
const mint_1 = require("./mint");
|
|
49
50
|
const miner_1 = require("./miner");
|
|
50
51
|
const preflight_1 = require("./preflight");
|
|
@@ -271,6 +272,18 @@ async function main() {
|
|
|
271
272
|
.action(async () => {
|
|
272
273
|
await setupWizard();
|
|
273
274
|
});
|
|
275
|
+
program
|
|
276
|
+
.command("fund")
|
|
277
|
+
.description("Fund your wallet — bridge SOL → ETH on Base, or show deposit address")
|
|
278
|
+
.option("--solana", "Bridge from Solana")
|
|
279
|
+
.option("--key <base58>", "Solana private key for direct signing")
|
|
280
|
+
.option("--amount <eth>", "Target ETH amount (default: 0.005)")
|
|
281
|
+
.hook("preAction", async () => {
|
|
282
|
+
await (0, preflight_1.runPreflight)("readonly");
|
|
283
|
+
})
|
|
284
|
+
.action(async (opts) => {
|
|
285
|
+
await (0, fund_1.runFundFlow)(opts);
|
|
286
|
+
});
|
|
274
287
|
program
|
|
275
288
|
.command("mint")
|
|
276
289
|
.description("Mint a new miner NFT")
|
package/dist/miner.js
CHANGED
|
@@ -265,21 +265,19 @@ async function startMining(tokenId) {
|
|
|
265
265
|
throw new Error("Mine transaction reverted on-chain");
|
|
266
266
|
}
|
|
267
267
|
txSpinner.stop("Submitting transaction... confirmed");
|
|
268
|
-
// Fetch post-mine
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
abi: agentCoinAbi,
|
|
273
|
-
functionName: "tokenMineCount",
|
|
274
|
-
args: [tokenId],
|
|
275
|
-
}),
|
|
276
|
-
wallet_1.publicClient.readContract({
|
|
268
|
+
// Fetch post-mine earnings with retry (public RPC may lag)
|
|
269
|
+
let earnings = runningTotal;
|
|
270
|
+
for (let retry = 0; retry < 5; retry++) {
|
|
271
|
+
earnings = (await wallet_1.publicClient.readContract({
|
|
277
272
|
address: config_1.config.agentCoinAddress,
|
|
278
273
|
abi: agentCoinAbi,
|
|
279
274
|
functionName: "tokenEarnings",
|
|
280
275
|
args: [tokenId],
|
|
281
|
-
})
|
|
282
|
-
|
|
276
|
+
}));
|
|
277
|
+
if (earnings > runningTotal)
|
|
278
|
+
break;
|
|
279
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
280
|
+
}
|
|
283
281
|
const delta = earnings - runningTotal;
|
|
284
282
|
runningTotal = earnings;
|
|
285
283
|
console.log(` ${ui.green("+")} ${(0, viem_1.formatEther)(delta)} AGENT | Total: ${(0, viem_1.formatEther)(earnings)} AGENT | Tx: ${ui.dim((0, explorer_1.txUrl)(txHash))}`);
|
package/dist/mint.js
CHANGED
|
@@ -72,33 +72,6 @@ function deriveChallengeFromSeed(seed) {
|
|
|
72
72
|
totalLength,
|
|
73
73
|
]);
|
|
74
74
|
}
|
|
75
|
-
async function findMintedTokenId(startTokenId, endTokenIdExclusive, owner, blockNumber) {
|
|
76
|
-
for (let tokenId = startTokenId; tokenId < endTokenIdExclusive; tokenId += 1n) {
|
|
77
|
-
try {
|
|
78
|
-
const [tokenOwner, mintBlock] = await Promise.all([
|
|
79
|
-
wallet_1.publicClient.readContract({
|
|
80
|
-
address: config_1.config.miningAgentAddress,
|
|
81
|
-
abi: miningAgentAbi,
|
|
82
|
-
functionName: "ownerOf",
|
|
83
|
-
args: [tokenId],
|
|
84
|
-
}),
|
|
85
|
-
wallet_1.publicClient.readContract({
|
|
86
|
-
address: config_1.config.miningAgentAddress,
|
|
87
|
-
abi: miningAgentAbi,
|
|
88
|
-
functionName: "mintBlock",
|
|
89
|
-
args: [tokenId],
|
|
90
|
-
}),
|
|
91
|
-
]);
|
|
92
|
-
if (tokenOwner.toLowerCase() === owner.toLowerCase() && mintBlock === blockNumber) {
|
|
93
|
-
return tokenId;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Ignore missing token ids while scanning the minted window.
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
throw new Error("Unable to determine minted token ID from post-mint contract state.");
|
|
101
|
-
}
|
|
102
75
|
async function runMintFlow() {
|
|
103
76
|
const { account, walletClient } = (0, wallet_1.requireWallet)();
|
|
104
77
|
console.log("");
|
|
@@ -184,14 +157,21 @@ async function runMintFlow() {
|
|
|
184
157
|
throw new Error("Challenge request reverted on-chain");
|
|
185
158
|
}
|
|
186
159
|
challengeSpinner.stop("Requesting challenge... done");
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
160
|
+
// Read challenge seed with retry (public RPC may lag behind tx confirmation)
|
|
161
|
+
let challengeSeed = ZERO_SEED;
|
|
162
|
+
for (let retry = 0; retry < 5; retry++) {
|
|
163
|
+
challengeSeed = (await wallet_1.publicClient.readContract({
|
|
164
|
+
address: config_1.config.miningAgentAddress,
|
|
165
|
+
abi: miningAgentAbi,
|
|
166
|
+
functionName: "challengeSeeds",
|
|
167
|
+
args: [account.address],
|
|
168
|
+
}));
|
|
169
|
+
if (challengeSeed.toLowerCase() !== ZERO_SEED.toLowerCase())
|
|
170
|
+
break;
|
|
171
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
172
|
+
}
|
|
193
173
|
if (challengeSeed.toLowerCase() === ZERO_SEED.toLowerCase()) {
|
|
194
|
-
throw new Error("Challenge seed
|
|
174
|
+
throw new Error("Challenge seed not found after 5 retries. The RPC may be lagging — try again.");
|
|
195
175
|
}
|
|
196
176
|
// Solve SMHL
|
|
197
177
|
const challenge = deriveChallengeFromSeed(challengeSeed);
|
|
@@ -200,11 +180,6 @@ async function runMintFlow() {
|
|
|
200
180
|
smhlSpinner.update(`Solving SMHL... attempt ${attempt}/5`);
|
|
201
181
|
});
|
|
202
182
|
smhlSpinner.stop("Solving SMHL... done");
|
|
203
|
-
const nextTokenIdBefore = (await wallet_1.publicClient.readContract({
|
|
204
|
-
address: config_1.config.miningAgentAddress,
|
|
205
|
-
abi: miningAgentAbi,
|
|
206
|
-
functionName: "nextTokenId",
|
|
207
|
-
}));
|
|
208
183
|
// Mint
|
|
209
184
|
const mintSpinner = ui.spinner("Minting...");
|
|
210
185
|
const mintTx = await walletClient.writeContract({
|
|
@@ -221,12 +196,15 @@ async function runMintFlow() {
|
|
|
221
196
|
throw new Error("Mint transaction reverted on-chain");
|
|
222
197
|
}
|
|
223
198
|
mintSpinner.stop("Minting... confirmed");
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
199
|
+
// Parse token ID from Transfer event in receipt (avoids stale RPC reads)
|
|
200
|
+
const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
201
|
+
const mintLog = receipt.logs.find((log) => log.address.toLowerCase() === config_1.config.miningAgentAddress.toLowerCase() &&
|
|
202
|
+
log.topics[0] === TRANSFER_TOPIC &&
|
|
203
|
+
log.topics[1] === "0x0000000000000000000000000000000000000000000000000000000000000000");
|
|
204
|
+
if (!mintLog || !mintLog.topics[3]) {
|
|
205
|
+
throw new Error("Mint tx confirmed but Transfer event not found in logs. Check tx on Basescan.");
|
|
206
|
+
}
|
|
207
|
+
const tokenId = BigInt(mintLog.topics[3]);
|
|
230
208
|
const [rarityRaw, hashpowerRaw] = await Promise.all([
|
|
231
209
|
wallet_1.publicClient.readContract({
|
|
232
210
|
address: config_1.config.miningAgentAddress,
|
package/dist/smhl.js
CHANGED
|
@@ -59,8 +59,8 @@ function validateSmhlSolution(solution, challenge) {
|
|
|
59
59
|
return issues;
|
|
60
60
|
}
|
|
61
61
|
const len = Buffer.byteLength(solution, "utf8");
|
|
62
|
-
if (Math.abs(len - challenge.totalLength) >
|
|
63
|
-
issues.push(`length ${len} not within ±
|
|
62
|
+
if (Math.abs(len - challenge.totalLength) > 4) {
|
|
63
|
+
issues.push(`length ${len} not within ±4 of ${challenge.totalLength}`);
|
|
64
64
|
}
|
|
65
65
|
if (!/^[\x20-\x7E]+$/.test(solution)) {
|
|
66
66
|
issues.push("solution must use printable ASCII only");
|
|
@@ -82,8 +82,8 @@ function validateSmhlSolution(solution, challenge) {
|
|
|
82
82
|
*/
|
|
83
83
|
function adjustSolution(raw, challenge) {
|
|
84
84
|
const requiredChar = String.fromCharCode(challenge.charValue);
|
|
85
|
-
const minLen = challenge.totalLength -
|
|
86
|
-
const maxLen = challenge.totalLength +
|
|
85
|
+
const minLen = challenge.totalLength - 4;
|
|
86
|
+
const maxLen = challenge.totalLength + 4;
|
|
87
87
|
const maxWords = challenge.wordCount + 2;
|
|
88
88
|
// Clean: lowercase, letters and spaces only, collapse whitespace
|
|
89
89
|
let words = raw
|
|
@@ -234,7 +234,7 @@ async function requestGeminiSolution(prompt) {
|
|
|
234
234
|
parts: [{ text: "You generate short lowercase word sequences that match exact constraints. Return only the words separated by spaces. Nothing else." }],
|
|
235
235
|
},
|
|
236
236
|
contents: [{ parts: [{ text: prompt }] }],
|
|
237
|
-
generationConfig: { temperature: 0.7 },
|
|
237
|
+
generationConfig: { temperature: 0.7, thinkingConfig: { thinkingBudget: 0 } },
|
|
238
238
|
}),
|
|
239
239
|
});
|
|
240
240
|
if (response.status === 429) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apow-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Mine AGENT tokens on Base L2 with AI-powered proof of work",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"apow",
|
|
@@ -39,9 +39,11 @@
|
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@solana/web3.js": "^1.98.0",
|
|
42
43
|
"commander": "^14.0.0",
|
|
43
44
|
"dotenv": "^17.2.3",
|
|
44
45
|
"openai": "^6.6.0",
|
|
46
|
+
"qrcode-terminal": "^0.12.0",
|
|
45
47
|
"viem": "^2.38.2"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
package/skill.md
CHANGED
|
@@ -30,6 +30,7 @@ with ETH on Base. Everything else you handle yourself.
|
|
|
30
30
|
**Via npm (recommended):**
|
|
31
31
|
```bash
|
|
32
32
|
npx apow-cli setup # interactive wizard — wallet, RPC, LLM config
|
|
33
|
+
npx apow-cli fund # fund wallet — bridge from Solana or show deposit address
|
|
33
34
|
npx apow-cli mint # mint a mining rig NFT
|
|
34
35
|
npx apow-cli mine # start mining (auto-detects your best rig)
|
|
35
36
|
```
|
|
@@ -65,8 +66,10 @@ MINING_AGENT_ADDRESS=0xB7caD3ca5F2BD8aEC2Eb67d6E8D448099B3bC03D
|
|
|
65
66
|
AGENT_COIN_ADDRESS=0x12577CF0D8a07363224D6909c54C056A183e13b3
|
|
66
67
|
EOF
|
|
67
68
|
|
|
68
|
-
# 3.
|
|
69
|
-
#
|
|
69
|
+
# 3. Fund the wallet (bridge from Solana or send ETH on Base)
|
|
70
|
+
npx apow-cli fund --solana # interactive bridge SOL → ETH on Base
|
|
71
|
+
# Or: npx apow-cli fund --solana --key <base58> # direct signing (~20s)
|
|
72
|
+
# Or: ask your user to send ≥0.005 ETH on Base directly
|
|
70
73
|
|
|
71
74
|
# 4. Mint + mine (fully autonomous from here)
|
|
72
75
|
npx apow-cli mint
|
|
@@ -138,9 +141,34 @@ This prompts for confirmation, then displays your address and private key. It al
|
|
|
138
141
|
Your mining wallet needs ETH on Base for gas and the mint fee.
|
|
139
142
|
**Minimum:** 0.005 ETH (~$15) covers minting + several mining cycles.
|
|
140
143
|
|
|
141
|
-
|
|
144
|
+
### Built-in Bridge: `apow fund` (Recommended)
|
|
142
145
|
|
|
143
|
-
|
|
146
|
+
The CLI has a built-in cross-chain bridge for Solana users:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npx apow-cli fund # Interactive: choose funding method
|
|
150
|
+
npx apow-cli fund --solana # Deposit address + QR code
|
|
151
|
+
npx apow-cli fund --solana --key <base58> # Direct Solana signing (~20s)
|
|
152
|
+
npx apow-cli fund --solana --amount 0.01 # Custom ETH target
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Three funding paths:**
|
|
156
|
+
|
|
157
|
+
| Path | How it works | Speed | Requires Solana key? |
|
|
158
|
+
|------|-------------|-------|---------------------|
|
|
159
|
+
| Direct signing | deBridge DLN signs + submits via your Solana keypair | ~20 seconds | Yes (`--key`) |
|
|
160
|
+
| Deposit address | Squid Router generates a Solana address; send from any wallet | ~1-3 minutes | No |
|
|
161
|
+
| Manual | Shows your Base address + QR code | Varies | No |
|
|
162
|
+
|
|
163
|
+
**Direct signing (`--key`):** Provide your base58 Solana secret key. The CLI calls deBridge DLN to create a bridge order, signs the Solana transaction locally, submits it, and polls until ETH arrives on Base. No API key needed.
|
|
164
|
+
|
|
165
|
+
**Deposit address (`--solana` without `--key`):** Requires `SQUID_INTEGRATOR_ID` in `.env` (free, apply at [squidrouter.com](https://app.squidrouter.com/)). Generates a one-time Solana deposit address with a QR code. Send SOL from any wallet (Phantom, Backpack, etc.) and the bridge handles the rest.
|
|
166
|
+
|
|
167
|
+
### Manual Funding Options
|
|
168
|
+
|
|
169
|
+
If you prefer not to use the built-in bridge:
|
|
170
|
+
|
|
171
|
+
#### From Solana (Phantom Wallet)
|
|
144
172
|
Phantom natively supports Base. Tell your user:
|
|
145
173
|
1. Open Phantom → tap the **Swap** icon
|
|
146
174
|
2. Set **From:** SOL (Solana) → **To:** ETH (Base)
|
|
@@ -149,19 +177,17 @@ Phantom natively supports Base. Tell your user:
|
|
|
149
177
|
5. Once ETH arrives on Base, tap **Send** → paste the mining wallet address
|
|
150
178
|
6. Confirm the transfer
|
|
151
179
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
### From an Exchange (Coinbase, Binance, etc.)
|
|
180
|
+
#### From an Exchange (Coinbase, Binance, etc.)
|
|
155
181
|
1. Buy ETH on Base (Coinbase supports Base withdrawals natively)
|
|
156
182
|
2. Withdraw to the mining wallet address
|
|
157
183
|
3. Select **Base** as the network — do NOT send on Ethereum mainnet
|
|
158
184
|
|
|
159
|
-
|
|
185
|
+
#### From Ethereum Mainnet
|
|
160
186
|
Bridge ETH to Base via [bridge.base.org](https://bridge.base.org):
|
|
161
187
|
1. Connect source wallet → enter mining wallet address as recipient
|
|
162
188
|
2. Bridge ≥0.005 ETH → arrives on Base in ~10 minutes
|
|
163
189
|
|
|
164
|
-
|
|
190
|
+
#### From Another Base Wallet
|
|
165
191
|
Send ETH directly to the mining wallet address on Base.
|
|
166
192
|
|
|
167
193
|
### Verifying Funds
|
|
@@ -236,6 +262,8 @@ CHAIN=base
|
|
|
236
262
|
| `LLM_MODEL` | No | `gpt-4o-mini` | Model identifier passed to the provider |
|
|
237
263
|
| `RPC_URL` | No | `https://mainnet.base.org` | Base JSON-RPC endpoint |
|
|
238
264
|
| `CHAIN` | No | `base` | Network selector; auto-detects `baseSepolia` if RPC URL contains "sepolia" |
|
|
265
|
+
| `SOLANA_RPC_URL` | No | `https://api.mainnet-beta.solana.com` | Solana RPC endpoint (only for `apow fund --solana`) |
|
|
266
|
+
| `SQUID_INTEGRATOR_ID` | No | -- | Squid Router integrator ID for deposit address flow (free at [squidrouter.com](https://app.squidrouter.com/)) |
|
|
239
267
|
|
|
240
268
|
### LLM Provider Recommendations
|
|
241
269
|
|
|
@@ -253,10 +281,54 @@ CHAIN=base
|
|
|
253
281
|
|
|
254
282
|
### RPC Recommendations
|
|
255
283
|
|
|
256
|
-
The default `https://mainnet.base.org` is rate
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
284
|
+
The default `https://mainnet.base.org` is a public RPC with aggressive rate limits. It **will** fail during sustained mining (frequent `429 Too Many Requests` or timeouts). You need a dedicated RPC endpoint. All providers below offer a **free tier** that is more than sufficient for mining.
|
|
285
|
+
|
|
286
|
+
#### Option 1: Alchemy (Recommended)
|
|
287
|
+
|
|
288
|
+
1. Go to [alchemy.com](https://www.alchemy.com/) and sign up (free, no credit card)
|
|
289
|
+
2. Click **Create new app** → Name: `apow-miner` → Chain: **Base** → Network: **Base Mainnet**
|
|
290
|
+
3. On the app dashboard, copy the **HTTPS** URL. It looks like:
|
|
291
|
+
```
|
|
292
|
+
https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY
|
|
293
|
+
```
|
|
294
|
+
4. Set in your `.env`:
|
|
295
|
+
```
|
|
296
|
+
RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Free tier:** 300M compute units/month (~millions of RPC calls). More than enough for mining.
|
|
300
|
+
|
|
301
|
+
#### Option 2: QuickNode
|
|
302
|
+
|
|
303
|
+
1. Go to [quicknode.com](https://www.quicknode.com/) and sign up (free, no credit card)
|
|
304
|
+
2. Click **Create Endpoint** → Chain: **Base** → Network: **Mainnet**
|
|
305
|
+
3. Copy the **HTTP Provider** URL. It looks like:
|
|
306
|
+
```
|
|
307
|
+
https://something-something.base-mainnet.quiknode.pro/YOUR_TOKEN/
|
|
308
|
+
```
|
|
309
|
+
4. Set in your `.env`:
|
|
310
|
+
```
|
|
311
|
+
RPC_URL=https://something-something.base-mainnet.quiknode.pro/YOUR_TOKEN/
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Free tier:** 10M API credits/month. Sufficient for a few miners.
|
|
315
|
+
|
|
316
|
+
#### Option 3: Other Free RPCs
|
|
317
|
+
|
|
318
|
+
| Provider | Free Tier | URL Pattern |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| [Infura](https://infura.io/) | 100K req/day | `https://base-mainnet.infura.io/v3/KEY` |
|
|
321
|
+
| [Ankr](https://www.ankr.com/) | 30 req/s | `https://rpc.ankr.com/base` (no key needed) |
|
|
322
|
+
| [Blast](https://blastapi.io/) | 40 req/s | `https://base-mainnet.blastapi.io/KEY` |
|
|
323
|
+
|
|
324
|
+
#### Troubleshooting RPC Issues
|
|
325
|
+
|
|
326
|
+
| Symptom | Cause | Fix |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `429 Too Many Requests` | Public RPC rate limit | Switch to a dedicated RPC (Alchemy/QuickNode) |
|
|
329
|
+
| `Timed out waiting for next block (60s)` | RPC not responding | Check endpoint URL; try a different provider |
|
|
330
|
+
| `fetch failed` / `ECONNREFUSED` | RPC URL is wrong or down | Verify URL; test with `curl YOUR_RPC_URL -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'` |
|
|
331
|
+
| Stale data / missed mines | RPC caching or slow sync | Alchemy and QuickNode are fastest; avoid free community RPCs |
|
|
260
332
|
|
|
261
333
|
---
|
|
262
334
|
|
|
@@ -565,12 +637,17 @@ The CLI contains no analytics, no error reporting, and no phone-home behavior of
|
|
|
565
637
|
- No error reporting services (no Sentry, no Bugsnag)
|
|
566
638
|
- No tracking pixels, no usage metrics, no telemetry endpoints
|
|
567
639
|
|
|
568
|
-
The CLI makes
|
|
640
|
+
The CLI makes only these network calls:
|
|
569
641
|
|
|
570
642
|
1. **Blockchain RPC** (to user-configured RPC URL, default: `mainnet.base.org`) -- standard `eth_call`, `eth_sendRawTransaction`, etc.
|
|
571
643
|
2. **LLM API** (to user-configured provider) -- sends only word-puzzle prompts for SMHL solving, never wallet data
|
|
644
|
+
3. **Bridge APIs** (only when using `apow fund --solana`):
|
|
645
|
+
- **CoinGecko** (`api.coingecko.com`) -- SOL/ETH price quotes
|
|
646
|
+
- **deBridge DLN** (`dln.debridge.finance`) -- bridge order creation and status (direct signing flow)
|
|
647
|
+
- **Squid Router** (`v2.api.squidrouter.com`) -- deposit address generation (deposit address flow)
|
|
648
|
+
- **Solana RPC** (`api.mainnet-beta.solana.com` or custom) -- balance checks and tx submission
|
|
572
649
|
|
|
573
|
-
|
|
650
|
+
No private keys are transmitted to bridge providers. deBridge returns a serialized Solana transaction that is signed locally. Squid generates a deposit address -- the user sends SOL themselves.
|
|
574
651
|
|
|
575
652
|
### LLM Calls Are Data-Isolated
|
|
576
653
|
|
|
@@ -601,7 +678,7 @@ The SMHL solver sends only generic word-generation prompts to the LLM (e.g., "Wr
|
|
|
601
678
|
cd apow-cli && npm install && npm run build
|
|
602
679
|
node dist/index.js setup
|
|
603
680
|
```
|
|
604
|
-
5. **Review dependencies.** The dependency tree is minimal and standard: `viem` (Ethereum library), `commander` (CLI framework), `dotenv` (env loading), `
|
|
681
|
+
5. **Review dependencies.** The dependency tree is minimal and standard: `viem` (Ethereum library), `commander` (CLI framework), `dotenv` (env loading), `@solana/web3.js` (Solana signing, lazy-loaded only for bridging), `qrcode-terminal` (QR codes for fund command), and an LLM client. No exotic or suspicious packages.
|
|
605
682
|
|
|
606
683
|
### How to Verify These Claims Yourself
|
|
607
684
|
|