-
Notifications
You must be signed in to change notification settings - Fork 72
Expand file tree
/
Copy pathresolve.js
More file actions
224 lines (203 loc) · 8.12 KB
/
Copy pathresolve.js
File metadata and controls
224 lines (203 loc) · 8.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/**
* Shared wallet resolution — used by all commands that operate on a wallet.
*/
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
import * as ows from "./keystore.js";
import { getConfigValue, getWalletOrigin } from "../config.js";
import { isSolana } from "../chain/registry.js";
import { printError } from "../common/output.js";
import { resolveWatchAddress } from "./watchlist.js";
import { WALLET_ORIGIN } from "../common/constants.js";
const ENS_TIMEOUT_MS = 10_000;
const ENS_RETRIES = 2;
const ENS_RPC_URLS = [
process.env.ETH_RPC_URL,
"https://eth.llamarpc.com",
"https://ethereum-rpc.publicnode.com",
].filter(Boolean);
function makeEnsClient(rpcUrl) {
return createPublicClient({ chain: mainnet, transport: http(rpcUrl) });
}
async function resolveEns(name) {
let lastErr;
for (let i = 0; i < ENS_RETRIES; i++) {
const rpcUrl = ENS_RPC_URLS[i % ENS_RPC_URLS.length];
const client = makeEnsClient(rpcUrl);
try {
const result = await Promise.race([
client.getEnsAddress({ name }),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`ENS resolution timed out for "${name}"`)), ENS_TIMEOUT_MS)
),
]);
return result;
} catch (err) {
lastErr = err;
}
}
throw lastErr;
}
export async function resolveAddress(input) {
if (/^0x[0-9a-fA-F]{40}$/.test(input)) return input;
// Check local wallets first — handles names like "test.zerion.eth"
try {
return ows.getEvmAddress(input);
} catch { /* not a local wallet — continue */ }
if (input.endsWith(".eth")) {
const resolved = await resolveEns(input);
if (!resolved) throw new Error(`Could not resolve ENS name: ${input}`);
return resolved;
}
// Solana public keys: 44-character base58
if (/^[1-9A-HJ-NP-Za-km-z]{43,44}$/.test(input)) return input;
throw new Error(`Invalid address: "${input}". Expected a 0x address, ENS name (.eth), or Solana address.`);
}
export function resolveWallet(flags, args = []) {
// If --watch is passed, resolve from watchlist
if (flags.watch) {
const address = resolveWatchAddress(flags.watch);
return { walletName: flags.watch, address, needsResolve: true };
}
// If --address is passed, use it directly (supports ENS names and raw addresses)
if (flags.address) {
return { walletName: flags.address, address: flags.address, needsResolve: true };
}
const walletName = flags.wallet || args[0] || getConfigValue("defaultWallet");
if (!walletName) {
printError("no_wallet", "No wallet specified", {
suggestion:
"Use --wallet <name>, --address <addr/ens>, or set default: zerion config set defaultWallet <name>",
});
process.exit(1);
}
// Determine chain to pick the right address type
const chain = flags.chain || flags["from-chain"] || getConfigValue("defaultChain") || "ethereum";
try {
let address;
if (isSolana(chain)) {
address = ows.getSolAddress(walletName);
if (!address) throw new Error("No Solana address");
} else {
address = ows.getEvmAddress(walletName);
}
return { walletName, address };
} catch (err) {
const code = err.message?.includes("not found") ? "wallet_not_found" : "ows_error";
printError(code, code === "wallet_not_found"
? `Wallet "${walletName}" not found`
: `Wallet error: ${err.message}`, {
suggestion: "List wallets with: zerion wallet list",
});
process.exit(1);
}
}
const SOL_ADDR_RE = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/;
const EVM_ADDR_RE = /^0x[0-9a-fA-F]{40}$/;
/**
* Resolve a destination address for cross-chain bridges/swaps. Picks the right
* account type for `targetChain` — Solana wallets need a Solana receiver, EVM
* chains need a 0x address.
*
* Inputs (in priority order):
* - `toAddressOrEns` — raw address (validated to match the chain) or ENS for EVM dests.
* - `toWalletName` — local wallet whose corresponding account is used.
* - falls back to default wallet (when source wallet has the required account on dest chain).
*
* Returns `{ address, source }` or throws with a helpful message.
*/
export async function resolveDestination({ toAddressOrEns, toWalletName, fallbackWallet, targetChain }) {
const wantsSolana = isSolana(targetChain);
if (toAddressOrEns) {
if (wantsSolana) {
if (!SOL_ADDR_RE.test(toAddressOrEns)) {
throw new Error(
`--to-address ${toAddressOrEns} is not a Solana address. ` +
`Solana destinations need a base58 Solana pubkey.`
);
}
return { address: toAddressOrEns, source: "address" };
}
if (EVM_ADDR_RE.test(toAddressOrEns) || toAddressOrEns.endsWith(".eth")) {
const resolved = await resolveAddress(toAddressOrEns);
return { address: resolved, source: "address" };
}
throw new Error(
`--to-address ${toAddressOrEns} is not a valid EVM address or ENS name for chain "${targetChain}".`
);
}
const lookupWallet = toWalletName || fallbackWallet;
if (!lookupWallet) {
throw new Error(
`Cross-chain destination required. Pass --to-wallet <name> or --to-address <addr/ens>.`
);
}
// Existence check — keep error wording consistent with other commands.
try {
ows.getWallet(lookupWallet);
} catch {
throw new Error(
`Destination wallet "${lookupWallet}" not found. List wallets with: zerion wallet list`
);
}
// Block auto-derived receivers when the wallet was imported via a single-curve
// private key. OWS still generates a random key for the other curve so the
// wallet exposes an address there, but that key is not recoverable from the
// imported secret — bridging funds to it locks them to this CLI vault. Pass
// --to-address <addr> to send to a wallet you actually control.
const origin = getWalletOrigin(lookupWallet);
if (wantsSolana && origin === WALLET_ORIGIN.EVM_KEY) {
throw new Error(
`Wallet "${lookupWallet}" was imported from an EVM private key, so its Solana ` +
`address is backed by a random key that is not recoverable from your imported secret. ` +
`Bridging to it would lock funds to this CLI vault.\n\n` +
`Pass --to-address <solana-pubkey> to send to a Solana wallet you control, ` +
`or --to-wallet <name> for a wallet imported via mnemonic or Solana private key.`
);
}
if (!wantsSolana && origin === WALLET_ORIGIN.SOL_KEY) {
throw new Error(
`Wallet "${lookupWallet}" was imported from a Solana private key, so its EVM ` +
`address is backed by a random key that is not recoverable from your imported secret. ` +
`Bridging to it would lock funds to this CLI vault.\n\n` +
`Pass --to-address <0x…/ENS> to send to an EVM wallet you control, ` +
`or --to-wallet <name> for a wallet imported via mnemonic or EVM private key.`
);
}
if (wantsSolana) {
const solAddress = ows.getSolAddress(lookupWallet);
if (!solAddress) {
throw new Error(
`Destination wallet "${lookupWallet}" has no Solana account. ` +
`Use --to-address <solana-pubkey> or pick a wallet with a Solana account.`
);
}
return { address: solAddress, source: "wallet", walletName: lookupWallet };
}
let evmAddress;
try {
evmAddress = ows.getEvmAddress(lookupWallet);
} catch {
throw new Error(
`Destination wallet "${lookupWallet}" has no EVM account for chain "${targetChain}". ` +
`Use --to-address <0x…> or pick an EVM wallet.`
);
}
return { address: evmAddress, source: "wallet", walletName: lookupWallet };
}
/**
* Resolve address from positional arg or --wallet/--address/--watch flags.
* Supports both `wallet portfolio <addr>` and `portfolio --wallet <name>`.
*/
export async function resolveAddressOrWallet(args, flags) {
if (args[0] && (args[0].startsWith("0x") || args[0].endsWith(".eth") || /^[1-9A-HJ-NP-Za-km-z]{43,44}$/.test(args[0]))) {
const address = await resolveAddress(args[0]);
return { walletName: args[0], address };
}
const resolved = resolveWallet(flags, args);
let address = resolved.address;
if (resolved.needsResolve) {
address = await resolveAddress(address);
}
return { walletName: resolved.walletName, address };
}