@veridex/sdk 1.0.0-beta.1
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/CHANGELOG.md +73 -0
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/chains/aptos/index.d.mts +140 -0
- package/dist/chains/aptos/index.d.ts +140 -0
- package/dist/chains/aptos/index.js +563 -0
- package/dist/chains/aptos/index.js.map +1 -0
- package/dist/chains/aptos/index.mjs +536 -0
- package/dist/chains/aptos/index.mjs.map +1 -0
- package/dist/chains/evm/index.d.mts +5 -0
- package/dist/chains/evm/index.d.ts +5 -0
- package/dist/chains/evm/index.js +1233 -0
- package/dist/chains/evm/index.js.map +1 -0
- package/dist/chains/evm/index.mjs +1205 -0
- package/dist/chains/evm/index.mjs.map +1 -0
- package/dist/chains/solana/index.d.mts +116 -0
- package/dist/chains/solana/index.d.ts +116 -0
- package/dist/chains/solana/index.js +513 -0
- package/dist/chains/solana/index.js.map +1 -0
- package/dist/chains/solana/index.mjs +491 -0
- package/dist/chains/solana/index.mjs.map +1 -0
- package/dist/chains/starknet/index.d.mts +172 -0
- package/dist/chains/starknet/index.d.ts +172 -0
- package/dist/chains/starknet/index.js +534 -0
- package/dist/chains/starknet/index.js.map +1 -0
- package/dist/chains/starknet/index.mjs +507 -0
- package/dist/chains/starknet/index.mjs.map +1 -0
- package/dist/chains/sui/index.d.mts +182 -0
- package/dist/chains/sui/index.d.ts +182 -0
- package/dist/chains/sui/index.js +560 -0
- package/dist/chains/sui/index.js.map +1 -0
- package/dist/chains/sui/index.mjs +533 -0
- package/dist/chains/sui/index.mjs.map +1 -0
- package/dist/constants.d.mts +150 -0
- package/dist/constants.d.ts +150 -0
- package/dist/constants.js +430 -0
- package/dist/constants.js.map +1 -0
- package/dist/constants.mjs +392 -0
- package/dist/constants.mjs.map +1 -0
- package/dist/index-0NXfbk0z.d.ts +637 -0
- package/dist/index-D0dLVjTA.d.mts +637 -0
- package/dist/index.d.mts +3101 -0
- package/dist/index.d.ts +3101 -0
- package/dist/index.js +13186 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +13011 -0
- package/dist/index.mjs.map +1 -0
- package/dist/payload.d.mts +125 -0
- package/dist/payload.d.ts +125 -0
- package/dist/payload.js +315 -0
- package/dist/payload.js.map +1 -0
- package/dist/payload.mjs +269 -0
- package/dist/payload.mjs.map +1 -0
- package/dist/queries/index.d.mts +148 -0
- package/dist/queries/index.d.ts +148 -0
- package/dist/queries/index.js +1533 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/index.mjs +1508 -0
- package/dist/queries/index.mjs.map +1 -0
- package/dist/types-ChIsqCiw.d.mts +565 -0
- package/dist/types-ChIsqCiw.d.ts +565 -0
- package/dist/types-FJL7j6gQ.d.mts +172 -0
- package/dist/types-FJL7j6gQ.d.ts +172 -0
- package/dist/types.d.mts +407 -0
- package/dist/types.d.ts +407 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/types.mjs +1 -0
- package/dist/types.mjs.map +1 -0
- package/dist/utils.d.mts +81 -0
- package/dist/utils.d.ts +81 -0
- package/dist/utils.js +430 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +390 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/wormhole.d.mts +167 -0
- package/dist/wormhole.d.ts +167 -0
- package/dist/wormhole.js +468 -0
- package/dist/wormhole.js.map +1 -0
- package/dist/wormhole.mjs +422 -0
- package/dist/wormhole.mjs.map +1 -0
- package/package.json +151 -0
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
// src/queries/constants.ts
|
|
2
|
+
var WORMHOLE_QUERY_PROXY_URLS = {
|
|
3
|
+
mainnet: "https://query.wormhole.com/v1/query",
|
|
4
|
+
testnet: "https://testnet.query.wormhole.com/v1/query"
|
|
5
|
+
};
|
|
6
|
+
var WORMHOLE_QUERY_RATE_LIMIT_PER_SECOND = 6;
|
|
7
|
+
var WORMHOLE_QUERY_CHAIN_IDS = {
|
|
8
|
+
ETHEREUM: 2,
|
|
9
|
+
POLYGON: 5,
|
|
10
|
+
ARBITRUM: 23,
|
|
11
|
+
OPTIMISM: 24,
|
|
12
|
+
BASE: 30
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/queries/hubState.ts
|
|
16
|
+
import axios from "axios";
|
|
17
|
+
import { Buffer } from "buffer";
|
|
18
|
+
import { ethers } from "ethers";
|
|
19
|
+
import {
|
|
20
|
+
EthCallQueryRequest,
|
|
21
|
+
EthCallQueryResponse,
|
|
22
|
+
PerChainQueryRequest,
|
|
23
|
+
QueryRequest,
|
|
24
|
+
QueryResponse,
|
|
25
|
+
hexToUint8Array,
|
|
26
|
+
isValidHexString
|
|
27
|
+
} from "@wormhole-foundation/wormhole-query-sdk";
|
|
28
|
+
|
|
29
|
+
// src/constants.ts
|
|
30
|
+
var TESTNET_CHAINS = {
|
|
31
|
+
baseSepolia: {
|
|
32
|
+
name: "Base Sepolia",
|
|
33
|
+
chainId: 84532,
|
|
34
|
+
wormholeChainId: 10004,
|
|
35
|
+
rpcUrl: "https://sepolia.base.org",
|
|
36
|
+
// Public CORS-friendly RPC
|
|
37
|
+
explorerUrl: "https://sepolia.basescan.org",
|
|
38
|
+
isEvm: true,
|
|
39
|
+
contracts: {
|
|
40
|
+
hub: "0x66D87dE68327f48A099c5B9bE97020Feab9a7c82",
|
|
41
|
+
vaultFactory: "0x40D9B16094808Fa48e73598E31AB964Cf15b475f",
|
|
42
|
+
vaultImplementation: "0xcBEb49b0109E61c1C69C51D5D9483A3aD6D18258",
|
|
43
|
+
wormholeCoreBridge: "0x79A1027a6A159502049F10906D333EC57E95F083",
|
|
44
|
+
tokenBridge: "0x86F55A04690fd7815A3D802bD587e83eA888B239"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
optimismSepolia: {
|
|
48
|
+
name: "Optimism Sepolia",
|
|
49
|
+
chainId: 11155420,
|
|
50
|
+
wormholeChainId: 10005,
|
|
51
|
+
rpcUrl: "https://sepolia.optimism.io",
|
|
52
|
+
explorerUrl: "https://sepolia-optimism.etherscan.io",
|
|
53
|
+
isEvm: true,
|
|
54
|
+
contracts: {
|
|
55
|
+
vaultFactory: "0xAbB421166E648953CDBE93c0078a0A794c56Fb84",
|
|
56
|
+
vaultImplementation: "0xDCD7daEf1AC06f4a8392957cca4834F7a16c058D",
|
|
57
|
+
wormholeCoreBridge: "0x31377888146f3253211EFEf5c676D41ECe7D58Fe",
|
|
58
|
+
tokenBridge: "0x99737Ec4B815d816c49A385943baf0380e75c0Ac"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
arbitrumSepolia: {
|
|
62
|
+
name: "Arbitrum Sepolia",
|
|
63
|
+
chainId: 421614,
|
|
64
|
+
wormholeChainId: 10003,
|
|
65
|
+
rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc",
|
|
66
|
+
explorerUrl: "https://sepolia.arbiscan.io",
|
|
67
|
+
isEvm: true,
|
|
68
|
+
contracts: {
|
|
69
|
+
vaultFactory: "0xd36D3D5DB59d78f1E33813490F72DABC15C9B07c",
|
|
70
|
+
vaultImplementation: "0xB10ACf39eBF17fc33F722cBD955b7aeCB0611bc4",
|
|
71
|
+
wormholeCoreBridge: "0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35",
|
|
72
|
+
tokenBridge: "0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
seiTestnet: {
|
|
76
|
+
name: "Sei Atlantic-2",
|
|
77
|
+
chainId: 1328,
|
|
78
|
+
wormholeChainId: 40,
|
|
79
|
+
rpcUrl: "https://evm-rpc-testnet.sei-apis.com",
|
|
80
|
+
explorerUrl: "https://seitrace.com/?chain=atlantic-2",
|
|
81
|
+
isEvm: true,
|
|
82
|
+
contracts: {
|
|
83
|
+
vaultFactory: "0x07F608AFf6d63b68029488b726d895c4Bb593038",
|
|
84
|
+
vaultImplementation: "0xD66153fccFB6731fB6c4944FbD607ba86A76a1f6",
|
|
85
|
+
wormholeCoreBridge: "0x0000000000000000000000000000000000000000"
|
|
86
|
+
// Mock - not yet deployed
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
solanaDevnet: {
|
|
90
|
+
name: "Solana Devnet",
|
|
91
|
+
chainId: 0,
|
|
92
|
+
wormholeChainId: 1,
|
|
93
|
+
rpcUrl: "https://api.devnet.solana.com",
|
|
94
|
+
explorerUrl: "https://explorer.solana.com",
|
|
95
|
+
isEvm: false,
|
|
96
|
+
contracts: {
|
|
97
|
+
hub: "AnyXHsqq9c2BiW4WgBcj6Aye7Ua7a7L7iSuwpfJxECJM",
|
|
98
|
+
wormholeCoreBridge: "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5",
|
|
99
|
+
tokenBridge: "DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
aptosTestnet: {
|
|
103
|
+
name: "Aptos Testnet",
|
|
104
|
+
chainId: 0,
|
|
105
|
+
wormholeChainId: 22,
|
|
106
|
+
rpcUrl: "https://fullnode.testnet.aptoslabs.com/v1",
|
|
107
|
+
explorerUrl: "https://explorer.aptoslabs.com",
|
|
108
|
+
isEvm: false,
|
|
109
|
+
contracts: {
|
|
110
|
+
hub: "0x1a89da9e9f8f0bc90d8d492890bd55fb261c6277d2a95dfcac70c268d0c23dcc",
|
|
111
|
+
wormholeCoreBridge: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
|
|
112
|
+
tokenBridge: "0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
suiTestnet: {
|
|
116
|
+
name: "Sui Testnet",
|
|
117
|
+
chainId: 0,
|
|
118
|
+
wormholeChainId: 21,
|
|
119
|
+
rpcUrl: "https://fullnode.testnet.sui.io:443",
|
|
120
|
+
explorerUrl: "https://suiscan.xyz/testnet",
|
|
121
|
+
isEvm: false,
|
|
122
|
+
contracts: {
|
|
123
|
+
hub: "0x35e99fdbbc1cde7e093da6f9e758ba2c4a077904bd64caee2fa6db5e6c4e9e37",
|
|
124
|
+
wormholeCoreBridge: "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
starknetSepolia: {
|
|
128
|
+
name: "Starknet Sepolia",
|
|
129
|
+
chainId: 0,
|
|
130
|
+
// Native Starknet chain ID (SN_SEPOLIA = 0x534e5f5345504f4c4941)
|
|
131
|
+
wormholeChainId: 50001,
|
|
132
|
+
// Custom chain ID (50000+ reserved for non-Wormhole chains)
|
|
133
|
+
rpcUrl: "https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_7/tsOnfTBZDKMXcUA26OED-",
|
|
134
|
+
explorerUrl: "https://sepolia.starkscan.co",
|
|
135
|
+
isEvm: false,
|
|
136
|
+
contracts: {
|
|
137
|
+
// Starknet spoke contract
|
|
138
|
+
hub: "0x68adcc730ed6c355200d00f763825448497b9cdf7936ca121711e078c88e811",
|
|
139
|
+
// Custom bridge contract (NOT Wormhole)
|
|
140
|
+
wormholeCoreBridge: "0x2c458c1ae64556482b05cc2d3ee5b032ed114d68429dda2062c9849a5a725f8"
|
|
141
|
+
},
|
|
142
|
+
// Hub chain ID that Starknet bridge validates (Base Sepolia = 10004)
|
|
143
|
+
hubChainId: 10004
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var MAINNET_CHAINS = {
|
|
147
|
+
ethereum: {
|
|
148
|
+
name: "Ethereum",
|
|
149
|
+
chainId: 1,
|
|
150
|
+
wormholeChainId: 2,
|
|
151
|
+
rpcUrl: "https://eth.llamarpc.com",
|
|
152
|
+
explorerUrl: "https://etherscan.io",
|
|
153
|
+
isEvm: true,
|
|
154
|
+
contracts: {
|
|
155
|
+
wormholeCoreBridge: "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B",
|
|
156
|
+
tokenBridge: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
base: {
|
|
160
|
+
name: "Base",
|
|
161
|
+
chainId: 8453,
|
|
162
|
+
wormholeChainId: 30,
|
|
163
|
+
rpcUrl: "https://mainnet.base.org",
|
|
164
|
+
explorerUrl: "https://basescan.org",
|
|
165
|
+
isEvm: true,
|
|
166
|
+
contracts: {
|
|
167
|
+
wormholeCoreBridge: "0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6",
|
|
168
|
+
tokenBridge: "0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627"
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
optimism: {
|
|
172
|
+
name: "Optimism",
|
|
173
|
+
chainId: 10,
|
|
174
|
+
wormholeChainId: 24,
|
|
175
|
+
rpcUrl: "https://mainnet.optimism.io",
|
|
176
|
+
explorerUrl: "https://optimistic.etherscan.io",
|
|
177
|
+
isEvm: true,
|
|
178
|
+
contracts: {
|
|
179
|
+
wormholeCoreBridge: "0xEe91C335eab126dF5fDB3797EA9d6aD93aeC9722",
|
|
180
|
+
tokenBridge: "0x1D68124e65faFC907325e3EDbF8c4d84499DAa8b"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
arbitrum: {
|
|
184
|
+
name: "Arbitrum",
|
|
185
|
+
chainId: 42161,
|
|
186
|
+
wormholeChainId: 23,
|
|
187
|
+
rpcUrl: "https://arb1.arbitrum.io/rpc",
|
|
188
|
+
explorerUrl: "https://arbiscan.io",
|
|
189
|
+
isEvm: true,
|
|
190
|
+
contracts: {
|
|
191
|
+
wormholeCoreBridge: "0xa5f208e072434bC67592E4C49C1B991BA79BCA46",
|
|
192
|
+
tokenBridge: "0x0b2402144Bb366A632D14B83F244D2e0e21bD39c"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
polygon: {
|
|
196
|
+
name: "Polygon",
|
|
197
|
+
chainId: 137,
|
|
198
|
+
wormholeChainId: 5,
|
|
199
|
+
rpcUrl: "https://polygon-rpc.com",
|
|
200
|
+
explorerUrl: "https://polygonscan.com",
|
|
201
|
+
isEvm: true,
|
|
202
|
+
contracts: {
|
|
203
|
+
wormholeCoreBridge: "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7",
|
|
204
|
+
tokenBridge: "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE"
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
solana: {
|
|
208
|
+
name: "Solana",
|
|
209
|
+
chainId: 0,
|
|
210
|
+
wormholeChainId: 1,
|
|
211
|
+
rpcUrl: "https://api.mainnet-beta.solana.com",
|
|
212
|
+
explorerUrl: "https://explorer.solana.com",
|
|
213
|
+
isEvm: false,
|
|
214
|
+
contracts: {
|
|
215
|
+
wormholeCoreBridge: "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth",
|
|
216
|
+
tokenBridge: "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
aptos: {
|
|
220
|
+
name: "Aptos",
|
|
221
|
+
chainId: 0,
|
|
222
|
+
wormholeChainId: 22,
|
|
223
|
+
rpcUrl: "https://fullnode.mainnet.aptoslabs.com/v1",
|
|
224
|
+
explorerUrl: "https://explorer.aptoslabs.com",
|
|
225
|
+
isEvm: false,
|
|
226
|
+
contracts: {
|
|
227
|
+
wormholeCoreBridge: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
|
|
228
|
+
tokenBridge: "0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
sui: {
|
|
232
|
+
name: "Sui",
|
|
233
|
+
chainId: 0,
|
|
234
|
+
wormholeChainId: 21,
|
|
235
|
+
rpcUrl: "https://fullnode.mainnet.sui.io:443",
|
|
236
|
+
explorerUrl: "https://suiscan.xyz/mainnet",
|
|
237
|
+
isEvm: false,
|
|
238
|
+
contracts: {
|
|
239
|
+
wormholeCoreBridge: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/queries/hubState.ts
|
|
245
|
+
var QueryHubStateError = class extends Error {
|
|
246
|
+
code;
|
|
247
|
+
cause;
|
|
248
|
+
constructor(code, message, cause) {
|
|
249
|
+
super(message);
|
|
250
|
+
this.name = "QueryHubStateError";
|
|
251
|
+
this.code = code;
|
|
252
|
+
this.cause = cause;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
function resolveNetwork(options) {
|
|
256
|
+
if (options?.network) return options.network;
|
|
257
|
+
const envCandidates = [
|
|
258
|
+
globalThis?.process?.env?.NEXT_PUBLIC_VERIDEX_NETWORK,
|
|
259
|
+
globalThis?.process?.env?.VERIDEX_NETWORK,
|
|
260
|
+
globalThis?.process?.env?.NEXT_PUBLIC_WORMHOLE_NETWORK,
|
|
261
|
+
globalThis?.process?.env?.WORMHOLE_NETWORK
|
|
262
|
+
].filter(Boolean);
|
|
263
|
+
const env = envCandidates[0]?.toLowerCase();
|
|
264
|
+
if (env === "mainnet" || env === "testnet") return env;
|
|
265
|
+
return "testnet";
|
|
266
|
+
}
|
|
267
|
+
function concatBytes(parts) {
|
|
268
|
+
const total = parts.reduce((sum, p) => sum + p.length, 0);
|
|
269
|
+
const out = new Uint8Array(total);
|
|
270
|
+
let offset = 0;
|
|
271
|
+
for (const p of parts) {
|
|
272
|
+
out.set(p, offset);
|
|
273
|
+
offset += p.length;
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
function signaturesToProofBytes(signatures) {
|
|
278
|
+
const chunks = [];
|
|
279
|
+
for (const sig of signatures) {
|
|
280
|
+
if (typeof sig !== "string" || sig.length !== 132 || !/^[0-9a-fA-F]+$/.test(sig)) {
|
|
281
|
+
throw new QueryHubStateError(
|
|
282
|
+
"PROXY_RESPONSE_INVALID",
|
|
283
|
+
`Invalid guardian signature format (expected 132 hex chars): ${String(sig)}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
chunks.push(hexToUint8Array(`0x${sig}`));
|
|
287
|
+
}
|
|
288
|
+
return concatBytes(chunks);
|
|
289
|
+
}
|
|
290
|
+
function decodeQueryBytes(bytes) {
|
|
291
|
+
if (typeof bytes !== "string" || bytes.length === 0) {
|
|
292
|
+
throw new QueryHubStateError("PROXY_RESPONSE_INVALID", "Missing query response bytes");
|
|
293
|
+
}
|
|
294
|
+
if (isValidHexString(bytes)) {
|
|
295
|
+
return hexToUint8Array(bytes);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
if (typeof atob === "function") {
|
|
299
|
+
const raw = atob(bytes);
|
|
300
|
+
const arr = new Uint8Array(raw.length);
|
|
301
|
+
for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
|
|
302
|
+
return arr;
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
return new Uint8Array(Buffer.from(bytes, "base64"));
|
|
308
|
+
} catch (cause) {
|
|
309
|
+
throw new QueryHubStateError("PROXY_RESPONSE_INVALID", "Unrecognized query response bytes encoding", cause);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function sleep(ms) {
|
|
313
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
314
|
+
}
|
|
315
|
+
async function withExponentialBackoff(fn, maxAttempts) {
|
|
316
|
+
let attempt = 0;
|
|
317
|
+
let lastError;
|
|
318
|
+
while (attempt < maxAttempts) {
|
|
319
|
+
try {
|
|
320
|
+
return await fn();
|
|
321
|
+
} catch (err) {
|
|
322
|
+
lastError = err;
|
|
323
|
+
attempt += 1;
|
|
324
|
+
if (attempt >= maxAttempts) break;
|
|
325
|
+
const baseMs = 250;
|
|
326
|
+
const backoffMs = Math.min(5e3, baseMs * 2 ** (attempt - 1));
|
|
327
|
+
const jitterMs = Math.floor(Math.random() * 100);
|
|
328
|
+
await sleep(backoffMs + jitterMs);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
throw lastError;
|
|
332
|
+
}
|
|
333
|
+
function getHubConfig(network) {
|
|
334
|
+
if (network === "testnet") {
|
|
335
|
+
const baseSepolia = TESTNET_CHAINS.baseSepolia;
|
|
336
|
+
if (!baseSepolia?.contracts?.hub) {
|
|
337
|
+
throw new QueryHubStateError("MISSING_HUB_ADDRESS", "Missing Base Sepolia hub address in SDK constants");
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
wormholeChainId: baseSepolia.wormholeChainId,
|
|
341
|
+
hubAddress: baseSepolia.contracts.hub,
|
|
342
|
+
endpoint: WORMHOLE_QUERY_PROXY_URLS.testnet,
|
|
343
|
+
rpcUrl: baseSepolia.rpcUrl
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (network === "mainnet") {
|
|
347
|
+
const base = MAINNET_CHAINS.base;
|
|
348
|
+
const hubAddress = base?.contracts?.hub;
|
|
349
|
+
if (!hubAddress) {
|
|
350
|
+
throw new QueryHubStateError(
|
|
351
|
+
"MISSING_HUB_ADDRESS",
|
|
352
|
+
"Missing mainnet hub address in SDK constants (MAINNET_CHAINS.base.contracts.hub)"
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
wormholeChainId: base.wormholeChainId,
|
|
357
|
+
hubAddress,
|
|
358
|
+
endpoint: WORMHOLE_QUERY_PROXY_URLS.mainnet,
|
|
359
|
+
rpcUrl: base.rpcUrl
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
throw new QueryHubStateError("UNSUPPORTED_NETWORK", `Unsupported network: ${network}`);
|
|
363
|
+
}
|
|
364
|
+
function encodeHubCalls(hubAddress, userKeyHash) {
|
|
365
|
+
const nonceAbiCandidates = [
|
|
366
|
+
"function getUserNonce(bytes32 userKeyHash) view returns (uint256)",
|
|
367
|
+
"function userNonces(bytes32 userKeyHash) view returns (uint256)",
|
|
368
|
+
"function getNonceByHash(bytes32 userKeyHash) view returns (uint256)"
|
|
369
|
+
];
|
|
370
|
+
const registeredAbiCandidates = [
|
|
371
|
+
"function registeredKeys(bytes32 userKeyHash) view returns (bool)",
|
|
372
|
+
"function isKeyRegisteredByHash(bytes32 userKeyHash) view returns (bool)"
|
|
373
|
+
];
|
|
374
|
+
const actionHashAbiCandidates = [
|
|
375
|
+
"function getUserLastActionHash(bytes32 userKeyHash) view returns (bytes32)",
|
|
376
|
+
"function userLastActionHash(bytes32 userKeyHash) view returns (bytes32)"
|
|
377
|
+
];
|
|
378
|
+
const iface = new ethers.Interface([
|
|
379
|
+
...nonceAbiCandidates,
|
|
380
|
+
...registeredAbiCandidates,
|
|
381
|
+
...actionHashAbiCandidates
|
|
382
|
+
]);
|
|
383
|
+
const nonceFnNames = ["getUserNonce", "userNonces", "getNonceByHash"];
|
|
384
|
+
const regFnNames = ["registeredKeys", "isKeyRegisteredByHash"];
|
|
385
|
+
const actionHashFnNames = ["getUserLastActionHash", "userLastActionHash"];
|
|
386
|
+
return [
|
|
387
|
+
...nonceFnNames.map((fn) => ({
|
|
388
|
+
to: hubAddress,
|
|
389
|
+
data: iface.encodeFunctionData(fn, [userKeyHash])
|
|
390
|
+
})),
|
|
391
|
+
...regFnNames.map((fn) => ({
|
|
392
|
+
to: hubAddress,
|
|
393
|
+
data: iface.encodeFunctionData(fn, [userKeyHash])
|
|
394
|
+
})),
|
|
395
|
+
...actionHashFnNames.map((fn) => ({
|
|
396
|
+
to: hubAddress,
|
|
397
|
+
data: iface.encodeFunctionData(fn, [userKeyHash])
|
|
398
|
+
}))
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
function decodeFirstNonce(results) {
|
|
402
|
+
const candidates = [
|
|
403
|
+
"function getUserNonce(bytes32 userKeyHash) view returns (uint256)",
|
|
404
|
+
"function userNonces(bytes32 userKeyHash) view returns (uint256)",
|
|
405
|
+
"function getNonceByHash(bytes32 userKeyHash) view returns (uint256)"
|
|
406
|
+
];
|
|
407
|
+
const iface = new ethers.Interface(candidates);
|
|
408
|
+
const fnNames = ["getUserNonce", "userNonces", "getNonceByHash"];
|
|
409
|
+
for (let idx = 0; idx < fnNames.length; idx++) {
|
|
410
|
+
const fnName = fnNames[idx];
|
|
411
|
+
try {
|
|
412
|
+
const data = results[idx];
|
|
413
|
+
if (!data || data === "0x") continue;
|
|
414
|
+
const decoded = iface.decodeFunctionResult(fnName, data);
|
|
415
|
+
return decoded[0];
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
throw new QueryHubStateError("QUERY_RESPONSE_INVALID", "Unable to decode user nonce from query response");
|
|
420
|
+
}
|
|
421
|
+
function decodeFirstIsRegistered(results) {
|
|
422
|
+
const nonceCandidateCount = 3;
|
|
423
|
+
const candidates = [
|
|
424
|
+
"function registeredKeys(bytes32 userKeyHash) view returns (bool)",
|
|
425
|
+
"function isKeyRegisteredByHash(bytes32 userKeyHash) view returns (bool)"
|
|
426
|
+
];
|
|
427
|
+
const iface = new ethers.Interface(candidates);
|
|
428
|
+
const fnNames = ["registeredKeys", "isKeyRegisteredByHash"];
|
|
429
|
+
for (let i = 0; i < fnNames.length; i++) {
|
|
430
|
+
const fnName = fnNames[i];
|
|
431
|
+
try {
|
|
432
|
+
const data = results[nonceCandidateCount + i];
|
|
433
|
+
if (!data || data === "0x") continue;
|
|
434
|
+
const decoded = iface.decodeFunctionResult(fnName, data);
|
|
435
|
+
return Boolean(decoded[0]);
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
function decodeLastActionHash(results) {
|
|
442
|
+
const nonceCandidateCount = 3;
|
|
443
|
+
const registeredCandidateCount = 2;
|
|
444
|
+
const actionHashOffset = nonceCandidateCount + registeredCandidateCount;
|
|
445
|
+
const candidates = [
|
|
446
|
+
"function getUserLastActionHash(bytes32 userKeyHash) view returns (bytes32)",
|
|
447
|
+
"function userLastActionHash(bytes32 userKeyHash) view returns (bytes32)"
|
|
448
|
+
];
|
|
449
|
+
const iface = new ethers.Interface(candidates);
|
|
450
|
+
const fnNames = ["getUserLastActionHash", "userLastActionHash"];
|
|
451
|
+
for (let i = 0; i < fnNames.length; i++) {
|
|
452
|
+
const fnName = fnNames[i];
|
|
453
|
+
try {
|
|
454
|
+
const data = results[actionHashOffset + i];
|
|
455
|
+
if (!data || data === "0x") continue;
|
|
456
|
+
const decoded = iface.decodeFunctionResult(fnName, data);
|
|
457
|
+
return decoded[0];
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return ethers.ZeroHash;
|
|
462
|
+
}
|
|
463
|
+
async function queryHubState(userKeyHash, apiKey, options) {
|
|
464
|
+
if (typeof userKeyHash !== "string" || userKeyHash.length === 0) {
|
|
465
|
+
throw new QueryHubStateError("INVALID_ARGUMENT", "userKeyHash is required");
|
|
466
|
+
}
|
|
467
|
+
if (!isValidHexString(userKeyHash) || hexToUint8Array(userKeyHash).length !== 32) {
|
|
468
|
+
throw new QueryHubStateError("INVALID_ARGUMENT", "userKeyHash must be a 32-byte hex string");
|
|
469
|
+
}
|
|
470
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
471
|
+
throw new QueryHubStateError("INVALID_ARGUMENT", "apiKey is required");
|
|
472
|
+
}
|
|
473
|
+
const network = resolveNetwork(options);
|
|
474
|
+
const maxAgeSeconds = options?.maxAge ?? 60;
|
|
475
|
+
const maxAttempts = options?.maxAttempts ?? 4;
|
|
476
|
+
const { wormholeChainId, hubAddress, endpoint, rpcUrl } = getHubConfig(network);
|
|
477
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
478
|
+
const callData = encodeHubCalls(hubAddress, userKeyHash);
|
|
479
|
+
const doFetch = async () => {
|
|
480
|
+
try {
|
|
481
|
+
const latestBlock = await provider.getBlockNumber();
|
|
482
|
+
const blockTag = Math.max(0, latestBlock - 2);
|
|
483
|
+
const request = new QueryRequest(Date.now() & 4294967295, [
|
|
484
|
+
new PerChainQueryRequest(wormholeChainId, new EthCallQueryRequest(blockTag, callData))
|
|
485
|
+
]);
|
|
486
|
+
const requestHex = Buffer.from(request.serialize()).toString("hex");
|
|
487
|
+
const response = await axios.post(
|
|
488
|
+
endpoint,
|
|
489
|
+
{ bytes: requestHex },
|
|
490
|
+
{
|
|
491
|
+
headers: {
|
|
492
|
+
"X-API-Key": apiKey,
|
|
493
|
+
"Content-Type": "application/json"
|
|
494
|
+
},
|
|
495
|
+
timeout: 1e4
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
const data = response.data;
|
|
499
|
+
const signatures = data?.signatures;
|
|
500
|
+
const bytes = data?.bytes;
|
|
501
|
+
if (!Array.isArray(signatures) || typeof bytes !== "string") {
|
|
502
|
+
throw new QueryHubStateError("PROXY_RESPONSE_INVALID", "Query Proxy response missing signatures/bytes");
|
|
503
|
+
}
|
|
504
|
+
const proof = signaturesToProofBytes(signatures);
|
|
505
|
+
const queryBytes = decodeQueryBytes(bytes);
|
|
506
|
+
const parsed = QueryResponse.from(queryBytes);
|
|
507
|
+
const perChain = parsed.responses.find((r) => r.chainId === wormholeChainId);
|
|
508
|
+
if (!perChain) {
|
|
509
|
+
throw new QueryHubStateError("QUERY_RESPONSE_INVALID", "Missing per-chain response for hub chain");
|
|
510
|
+
}
|
|
511
|
+
const chainResp = EthCallQueryResponse.from(perChain.response.serialize());
|
|
512
|
+
const blockTime = Number(chainResp.blockTime);
|
|
513
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
514
|
+
if (nowSeconds - blockTime > maxAgeSeconds) {
|
|
515
|
+
throw new QueryHubStateError(
|
|
516
|
+
"ATTESTATION_STALE",
|
|
517
|
+
`Guardian attestation is stale (blockTime=${blockTime}, now=${nowSeconds}, maxAge=${maxAgeSeconds}s)`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const nonce = decodeFirstNonce(chainResp.results);
|
|
521
|
+
const isRegistered = decodeFirstIsRegistered(chainResp.results);
|
|
522
|
+
const lastActionHash = decodeLastActionHash(chainResp.results);
|
|
523
|
+
return {
|
|
524
|
+
nonce,
|
|
525
|
+
isRegistered,
|
|
526
|
+
blockTime,
|
|
527
|
+
proof,
|
|
528
|
+
lastActionHash
|
|
529
|
+
};
|
|
530
|
+
} catch (err) {
|
|
531
|
+
if (err instanceof QueryHubStateError) throw err;
|
|
532
|
+
if (axios.isAxiosError(err)) {
|
|
533
|
+
const ax = err;
|
|
534
|
+
const status = ax.response?.status;
|
|
535
|
+
const statusText = ax.response?.statusText;
|
|
536
|
+
const details = typeof ax.response?.data === "string" ? ax.response?.data : void 0;
|
|
537
|
+
throw new QueryHubStateError(
|
|
538
|
+
"PROXY_HTTP_ERROR",
|
|
539
|
+
`Query Proxy request failed${status ? ` (${status} ${statusText ?? ""})` : ""}${details ? `: ${details}` : ""}`,
|
|
540
|
+
err
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
throw new QueryHubStateError("PROXY_HTTP_ERROR", "Query Proxy request failed", err);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
return await withExponentialBackoff(doFetch, maxAttempts);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/queries/portfolio.ts
|
|
550
|
+
import axios2 from "axios";
|
|
551
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
552
|
+
import { ethers as ethers4 } from "ethers";
|
|
553
|
+
import { PublicKey } from "@solana/web3.js";
|
|
554
|
+
import {
|
|
555
|
+
EthCallQueryRequest as EthCallQueryRequest2,
|
|
556
|
+
EthCallQueryResponse as EthCallQueryResponse2,
|
|
557
|
+
PerChainQueryRequest as PerChainQueryRequest2,
|
|
558
|
+
QueryRequest as QueryRequest2,
|
|
559
|
+
QueryResponse as QueryResponse2,
|
|
560
|
+
SolanaAccountQueryRequest,
|
|
561
|
+
SolanaAccountQueryResponse,
|
|
562
|
+
hexToUint8Array as hexToUint8Array2,
|
|
563
|
+
isValidHexString as isValidHexString2
|
|
564
|
+
} from "@wormhole-foundation/wormhole-query-sdk";
|
|
565
|
+
|
|
566
|
+
// src/core/WalletManager.ts
|
|
567
|
+
import { ethers as ethers3 } from "ethers";
|
|
568
|
+
|
|
569
|
+
// src/utils.ts
|
|
570
|
+
import { ethers as ethers2 } from "ethers";
|
|
571
|
+
function computeKeyHash(publicKeyX, publicKeyY) {
|
|
572
|
+
return ethers2.keccak256(
|
|
573
|
+
ethers2.solidityPacked(["uint256", "uint256"], [publicKeyX, publicKeyY])
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/core/WalletManager.ts
|
|
578
|
+
var PROXY_BYTECODE_PREFIX = "0x3d602d80600a3d3981f3363d3d373d3d3d363d73";
|
|
579
|
+
var PROXY_BYTECODE_SUFFIX = "5af43d82803e903d91602b57fd5bf3";
|
|
580
|
+
var WalletManager = class {
|
|
581
|
+
config;
|
|
582
|
+
addressCache = /* @__PURE__ */ new Map();
|
|
583
|
+
constructor(config = {}) {
|
|
584
|
+
this.config = {
|
|
585
|
+
cacheAddresses: config.cacheAddresses ?? true,
|
|
586
|
+
persistToStorage: config.persistToStorage ?? false,
|
|
587
|
+
storageKey: config.storageKey ?? "veridex_wallet_addresses"
|
|
588
|
+
};
|
|
589
|
+
if (this.config.persistToStorage && typeof window !== "undefined") {
|
|
590
|
+
this.loadFromStorage();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// ========================================================================
|
|
594
|
+
// Address Computation
|
|
595
|
+
// ========================================================================
|
|
596
|
+
/**
|
|
597
|
+
* Compute the deterministic vault address for an EVM chain
|
|
598
|
+
*
|
|
599
|
+
* Uses CREATE2 with EIP-1167 minimal proxy pattern:
|
|
600
|
+
* - Salt = keccak256(factoryAddress, ownerKeyHash)
|
|
601
|
+
* - InitCode = EIP-1167 proxy bytecode with implementation address
|
|
602
|
+
*
|
|
603
|
+
* @param keyHash - The owner's key hash (keccak256 of public key coordinates)
|
|
604
|
+
* @param factoryAddress - The vault factory contract address
|
|
605
|
+
* @param implementationAddress - The vault implementation contract address
|
|
606
|
+
* @returns The deterministic vault address
|
|
607
|
+
*/
|
|
608
|
+
computeVaultAddress(keyHash, factoryAddress, implementationAddress) {
|
|
609
|
+
const salt = ethers3.keccak256(
|
|
610
|
+
ethers3.solidityPacked(
|
|
611
|
+
["address", "bytes32"],
|
|
612
|
+
[factoryAddress, keyHash]
|
|
613
|
+
)
|
|
614
|
+
);
|
|
615
|
+
const initCode = this.buildProxyInitCode(implementationAddress);
|
|
616
|
+
const initCodeHash = ethers3.keccak256(initCode);
|
|
617
|
+
const create2Data = ethers3.solidityPacked(
|
|
618
|
+
["bytes1", "address", "bytes32", "bytes32"],
|
|
619
|
+
["0xff", factoryAddress, salt, initCodeHash]
|
|
620
|
+
);
|
|
621
|
+
const hash = ethers3.keccak256(create2Data);
|
|
622
|
+
return ethers3.getAddress("0x" + hash.slice(26));
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Compute vault address from public key coordinates
|
|
626
|
+
*
|
|
627
|
+
* @param publicKeyX - P-256 public key X coordinate
|
|
628
|
+
* @param publicKeyY - P-256 public key Y coordinate
|
|
629
|
+
* @param factoryAddress - The vault factory contract address
|
|
630
|
+
* @param implementationAddress - The vault implementation contract address
|
|
631
|
+
* @returns The deterministic vault address
|
|
632
|
+
*/
|
|
633
|
+
computeVaultAddressFromPublicKey(publicKeyX, publicKeyY, factoryAddress, implementationAddress) {
|
|
634
|
+
const keyHash = computeKeyHash(publicKeyX, publicKeyY);
|
|
635
|
+
return this.computeVaultAddress(keyHash, factoryAddress, implementationAddress);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Build EIP-1167 minimal proxy initcode
|
|
639
|
+
*/
|
|
640
|
+
buildProxyInitCode(implementationAddress) {
|
|
641
|
+
const impl = implementationAddress.toLowerCase().replace("0x", "");
|
|
642
|
+
return PROXY_BYTECODE_PREFIX + impl + PROXY_BYTECODE_SUFFIX;
|
|
643
|
+
}
|
|
644
|
+
// ========================================================================
|
|
645
|
+
// Unified Identity
|
|
646
|
+
// ========================================================================
|
|
647
|
+
/**
|
|
648
|
+
* Get unified identity with addresses across all configured chains
|
|
649
|
+
*
|
|
650
|
+
* @param credential - The passkey credential
|
|
651
|
+
* @param chainConfigs - Map of chain configurations with factory/implementation addresses
|
|
652
|
+
* @returns Unified identity with addresses on each chain
|
|
653
|
+
*/
|
|
654
|
+
async getUnifiedIdentity(credential, chainConfigs) {
|
|
655
|
+
const addresses = [];
|
|
656
|
+
for (const [wormholeChainId, config] of chainConfigs) {
|
|
657
|
+
const address = await this.deriveAddressForChain(
|
|
658
|
+
credential,
|
|
659
|
+
wormholeChainId,
|
|
660
|
+
config
|
|
661
|
+
);
|
|
662
|
+
if (address) {
|
|
663
|
+
addresses.push(address);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const identity = {
|
|
667
|
+
keyHash: credential.keyHash,
|
|
668
|
+
publicKeyX: credential.publicKeyX,
|
|
669
|
+
publicKeyY: credential.publicKeyY,
|
|
670
|
+
credentialId: credential.credentialId,
|
|
671
|
+
addresses,
|
|
672
|
+
createdAt: Date.now(),
|
|
673
|
+
updatedAt: Date.now()
|
|
674
|
+
};
|
|
675
|
+
if (this.config.cacheAddresses) {
|
|
676
|
+
this.addressCache.set(credential.keyHash, addresses);
|
|
677
|
+
}
|
|
678
|
+
if (this.config.persistToStorage) {
|
|
679
|
+
this.saveToStorage(identity);
|
|
680
|
+
}
|
|
681
|
+
return identity;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Derive address for a specific chain
|
|
685
|
+
*/
|
|
686
|
+
async deriveAddressForChain(credential, wormholeChainId, config) {
|
|
687
|
+
if (config.isEvm) {
|
|
688
|
+
if (!config.factoryAddress || !config.implementationAddress) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const address = this.computeVaultAddress(
|
|
692
|
+
credential.keyHash,
|
|
693
|
+
config.factoryAddress,
|
|
694
|
+
config.implementationAddress
|
|
695
|
+
);
|
|
696
|
+
return {
|
|
697
|
+
wormholeChainId,
|
|
698
|
+
chainName: config.chainName,
|
|
699
|
+
address,
|
|
700
|
+
isEvm: true,
|
|
701
|
+
deployed: false
|
|
702
|
+
// Will be checked separately
|
|
703
|
+
};
|
|
704
|
+
} else {
|
|
705
|
+
return this.deriveNonEvmAddress(credential, wormholeChainId, config);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Derive address for non-EVM chains
|
|
710
|
+
*
|
|
711
|
+
* Each chain has its own address format:
|
|
712
|
+
* - Solana: Base58 encoded public key hash
|
|
713
|
+
* - Aptos: 32-byte hex address
|
|
714
|
+
* - Sui: 32-byte hex address with 0x prefix
|
|
715
|
+
*/
|
|
716
|
+
deriveNonEvmAddress(credential, wormholeChainId, _config) {
|
|
717
|
+
switch (wormholeChainId) {
|
|
718
|
+
case 1:
|
|
719
|
+
return {
|
|
720
|
+
wormholeChainId: 1,
|
|
721
|
+
chainName: "Solana",
|
|
722
|
+
address: credential.keyHash,
|
|
723
|
+
// PDA will be derived from this
|
|
724
|
+
isEvm: false,
|
|
725
|
+
derivationType: "pda",
|
|
726
|
+
deployed: false
|
|
727
|
+
};
|
|
728
|
+
case 22:
|
|
729
|
+
return {
|
|
730
|
+
wormholeChainId: 22,
|
|
731
|
+
chainName: "Aptos",
|
|
732
|
+
address: credential.keyHash,
|
|
733
|
+
isEvm: false,
|
|
734
|
+
derivationType: "resource_account",
|
|
735
|
+
deployed: false
|
|
736
|
+
};
|
|
737
|
+
case 21:
|
|
738
|
+
return {
|
|
739
|
+
wormholeChainId: 21,
|
|
740
|
+
chainName: "Sui",
|
|
741
|
+
address: credential.keyHash,
|
|
742
|
+
isEvm: false,
|
|
743
|
+
derivationType: "object",
|
|
744
|
+
deployed: false
|
|
745
|
+
};
|
|
746
|
+
default:
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// ========================================================================
|
|
751
|
+
// Address Lookup
|
|
752
|
+
// ========================================================================
|
|
753
|
+
/**
|
|
754
|
+
* Get cached address for a chain
|
|
755
|
+
*/
|
|
756
|
+
getAddressForChain(keyHash, wormholeChainId) {
|
|
757
|
+
const addresses = this.addressCache.get(keyHash);
|
|
758
|
+
return addresses?.find((a) => a.wormholeChainId === wormholeChainId);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Get all cached addresses for a key hash
|
|
762
|
+
*/
|
|
763
|
+
getAddresses(keyHash) {
|
|
764
|
+
return this.addressCache.get(keyHash) ?? [];
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Update deployment status for an address
|
|
768
|
+
*/
|
|
769
|
+
updateDeploymentStatus(keyHash, wormholeChainId, deployed, deploymentTxHash) {
|
|
770
|
+
const addresses = this.addressCache.get(keyHash);
|
|
771
|
+
if (!addresses) return;
|
|
772
|
+
const address = addresses.find((a) => a.wormholeChainId === wormholeChainId);
|
|
773
|
+
if (address) {
|
|
774
|
+
address.deployed = deployed;
|
|
775
|
+
address.deploymentTxHash = deploymentTxHash;
|
|
776
|
+
}
|
|
777
|
+
if (this.config.persistToStorage) {
|
|
778
|
+
this.saveAddressesToStorage(keyHash, addresses);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// ========================================================================
|
|
782
|
+
// Storage
|
|
783
|
+
// ========================================================================
|
|
784
|
+
/**
|
|
785
|
+
* Load addresses from localStorage
|
|
786
|
+
*/
|
|
787
|
+
loadFromStorage() {
|
|
788
|
+
if (typeof window === "undefined") return;
|
|
789
|
+
try {
|
|
790
|
+
const stored = localStorage.getItem(this.config.storageKey);
|
|
791
|
+
if (!stored) return;
|
|
792
|
+
const data = JSON.parse(stored);
|
|
793
|
+
for (const [keyHash, addresses] of Object.entries(data.addresses)) {
|
|
794
|
+
this.addressCache.set(keyHash, addresses);
|
|
795
|
+
}
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.warn("Failed to load wallet addresses from storage:", error);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Save identity to localStorage
|
|
802
|
+
*/
|
|
803
|
+
saveToStorage(identity) {
|
|
804
|
+
if (typeof window === "undefined") return;
|
|
805
|
+
try {
|
|
806
|
+
const stored = localStorage.getItem(this.config.storageKey) ?? "{}";
|
|
807
|
+
const data = JSON.parse(stored);
|
|
808
|
+
if (!data.addresses) {
|
|
809
|
+
data.addresses = {};
|
|
810
|
+
}
|
|
811
|
+
data.addresses[identity.keyHash] = identity.addresses;
|
|
812
|
+
data.identities = data.identities ?? {};
|
|
813
|
+
data.identities[identity.keyHash] = {
|
|
814
|
+
keyHash: identity.keyHash,
|
|
815
|
+
publicKeyX: identity.publicKeyX.toString(),
|
|
816
|
+
publicKeyY: identity.publicKeyY.toString(),
|
|
817
|
+
credentialId: identity.credentialId,
|
|
818
|
+
createdAt: identity.createdAt,
|
|
819
|
+
updatedAt: identity.updatedAt
|
|
820
|
+
};
|
|
821
|
+
localStorage.setItem(this.config.storageKey, JSON.stringify(data));
|
|
822
|
+
} catch (error) {
|
|
823
|
+
console.warn("Failed to save wallet addresses to storage:", error);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Save addresses to localStorage
|
|
828
|
+
*/
|
|
829
|
+
saveAddressesToStorage(keyHash, addresses) {
|
|
830
|
+
if (typeof window === "undefined") return;
|
|
831
|
+
try {
|
|
832
|
+
const stored = localStorage.getItem(this.config.storageKey) ?? "{}";
|
|
833
|
+
const data = JSON.parse(stored);
|
|
834
|
+
if (!data.addresses) {
|
|
835
|
+
data.addresses = {};
|
|
836
|
+
}
|
|
837
|
+
data.addresses[keyHash] = addresses;
|
|
838
|
+
localStorage.setItem(this.config.storageKey, JSON.stringify(data));
|
|
839
|
+
} catch (error) {
|
|
840
|
+
console.warn("Failed to save wallet addresses to storage:", error);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Clear all cached data
|
|
845
|
+
*/
|
|
846
|
+
clearCache() {
|
|
847
|
+
this.addressCache.clear();
|
|
848
|
+
if (this.config.persistToStorage && typeof window !== "undefined") {
|
|
849
|
+
localStorage.removeItem(this.config.storageKey);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Load identity from storage
|
|
854
|
+
*/
|
|
855
|
+
loadIdentityFromStorage(keyHash) {
|
|
856
|
+
if (typeof window === "undefined") return null;
|
|
857
|
+
try {
|
|
858
|
+
const stored = localStorage.getItem(this.config.storageKey);
|
|
859
|
+
if (!stored) return null;
|
|
860
|
+
const data = JSON.parse(stored);
|
|
861
|
+
const storedIdentity = data.identities?.[keyHash];
|
|
862
|
+
const addresses = data.addresses?.[keyHash];
|
|
863
|
+
if (!storedIdentity || !addresses) return null;
|
|
864
|
+
return {
|
|
865
|
+
keyHash: storedIdentity.keyHash,
|
|
866
|
+
publicKeyX: BigInt(storedIdentity.publicKeyX),
|
|
867
|
+
publicKeyY: BigInt(storedIdentity.publicKeyY),
|
|
868
|
+
credentialId: storedIdentity.credentialId,
|
|
869
|
+
addresses,
|
|
870
|
+
createdAt: storedIdentity.createdAt,
|
|
871
|
+
updatedAt: storedIdentity.updatedAt
|
|
872
|
+
};
|
|
873
|
+
} catch (error) {
|
|
874
|
+
console.warn("Failed to load identity from storage:", error);
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// src/constants/tokens.ts
|
|
881
|
+
var NATIVE_TOKEN_ADDRESS = "native";
|
|
882
|
+
var EVM_ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
883
|
+
var BASE_SEPOLIA_TOKENS = {
|
|
884
|
+
wormholeChainId: 10004,
|
|
885
|
+
chainName: "Base Sepolia",
|
|
886
|
+
nativeToken: {
|
|
887
|
+
symbol: "ETH",
|
|
888
|
+
name: "Ether",
|
|
889
|
+
address: NATIVE_TOKEN_ADDRESS,
|
|
890
|
+
decimals: 18,
|
|
891
|
+
isNative: true
|
|
892
|
+
},
|
|
893
|
+
tokens: [
|
|
894
|
+
{
|
|
895
|
+
symbol: "USDC",
|
|
896
|
+
name: "USD Coin (Test)",
|
|
897
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
898
|
+
// Circle test USDC
|
|
899
|
+
decimals: 6,
|
|
900
|
+
isNative: false
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
symbol: "WETH",
|
|
904
|
+
name: "Wrapped Ether",
|
|
905
|
+
address: "0x4200000000000000000000000000000000000006",
|
|
906
|
+
decimals: 18,
|
|
907
|
+
isNative: false
|
|
908
|
+
}
|
|
909
|
+
]
|
|
910
|
+
};
|
|
911
|
+
var OPTIMISM_SEPOLIA_TOKENS = {
|
|
912
|
+
wormholeChainId: 10005,
|
|
913
|
+
chainName: "Optimism Sepolia",
|
|
914
|
+
nativeToken: {
|
|
915
|
+
symbol: "ETH",
|
|
916
|
+
name: "Ether",
|
|
917
|
+
address: NATIVE_TOKEN_ADDRESS,
|
|
918
|
+
decimals: 18,
|
|
919
|
+
isNative: true
|
|
920
|
+
},
|
|
921
|
+
tokens: [
|
|
922
|
+
{
|
|
923
|
+
symbol: "USDC",
|
|
924
|
+
name: "USD Coin (Test)",
|
|
925
|
+
address: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
|
|
926
|
+
// Test USDC
|
|
927
|
+
decimals: 6,
|
|
928
|
+
isNative: false
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
symbol: "WETH",
|
|
932
|
+
name: "Wrapped Ether",
|
|
933
|
+
address: "0x4200000000000000000000000000000000000006",
|
|
934
|
+
decimals: 18,
|
|
935
|
+
isNative: false
|
|
936
|
+
}
|
|
937
|
+
]
|
|
938
|
+
};
|
|
939
|
+
var ARBITRUM_SEPOLIA_TOKENS = {
|
|
940
|
+
wormholeChainId: 10003,
|
|
941
|
+
chainName: "Arbitrum Sepolia",
|
|
942
|
+
nativeToken: {
|
|
943
|
+
symbol: "ETH",
|
|
944
|
+
name: "Ether",
|
|
945
|
+
address: NATIVE_TOKEN_ADDRESS,
|
|
946
|
+
decimals: 18,
|
|
947
|
+
isNative: true
|
|
948
|
+
},
|
|
949
|
+
tokens: [
|
|
950
|
+
{
|
|
951
|
+
symbol: "USDC",
|
|
952
|
+
name: "USD Coin (Test)",
|
|
953
|
+
address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
|
|
954
|
+
// Circle USDC Arbitrum Sepolia
|
|
955
|
+
decimals: 6,
|
|
956
|
+
isNative: false
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
symbol: "WETH",
|
|
960
|
+
name: "Wrapped Ether",
|
|
961
|
+
address: "0x980B62Da83eFf3D4576C647993b0c1D7faf17c73",
|
|
962
|
+
decimals: 18,
|
|
963
|
+
isNative: false
|
|
964
|
+
}
|
|
965
|
+
]
|
|
966
|
+
};
|
|
967
|
+
var TOKEN_REGISTRY = {
|
|
968
|
+
10004: BASE_SEPOLIA_TOKENS,
|
|
969
|
+
10005: OPTIMISM_SEPOLIA_TOKENS,
|
|
970
|
+
10003: ARBITRUM_SEPOLIA_TOKENS
|
|
971
|
+
};
|
|
972
|
+
function getTokenList(wormholeChainId) {
|
|
973
|
+
return TOKEN_REGISTRY[wormholeChainId] ?? null;
|
|
974
|
+
}
|
|
975
|
+
function getAllTokens(wormholeChainId) {
|
|
976
|
+
const list = getTokenList(wormholeChainId);
|
|
977
|
+
if (!list) return [];
|
|
978
|
+
return [list.nativeToken, ...list.tokens];
|
|
979
|
+
}
|
|
980
|
+
function isNativeToken(address) {
|
|
981
|
+
return address.toLowerCase() === NATIVE_TOKEN_ADDRESS || address === EVM_ZERO_ADDRESS;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/queries/portfolio.ts
|
|
985
|
+
var QueryPortfolioError = class extends Error {
|
|
986
|
+
code;
|
|
987
|
+
cause;
|
|
988
|
+
constructor(code, message, cause) {
|
|
989
|
+
super(message);
|
|
990
|
+
this.name = "QueryPortfolioError";
|
|
991
|
+
this.code = code;
|
|
992
|
+
this.cause = cause;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
var PORTFOLIO_CACHE = /* @__PURE__ */ new Map();
|
|
996
|
+
function resolveNetwork2(options) {
|
|
997
|
+
if (options?.network) return options.network;
|
|
998
|
+
const envCandidates = [
|
|
999
|
+
globalThis?.process?.env?.NEXT_PUBLIC_VERIDEX_NETWORK,
|
|
1000
|
+
globalThis?.process?.env?.VERIDEX_NETWORK,
|
|
1001
|
+
globalThis?.process?.env?.NEXT_PUBLIC_WORMHOLE_NETWORK,
|
|
1002
|
+
globalThis?.process?.env?.WORMHOLE_NETWORK
|
|
1003
|
+
].filter(Boolean);
|
|
1004
|
+
const env = envCandidates[0]?.toLowerCase();
|
|
1005
|
+
if (env === "mainnet" || env === "testnet") return env;
|
|
1006
|
+
return "testnet";
|
|
1007
|
+
}
|
|
1008
|
+
function concatBytes2(parts) {
|
|
1009
|
+
const total = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1010
|
+
const out = new Uint8Array(total);
|
|
1011
|
+
let offset = 0;
|
|
1012
|
+
for (const p of parts) {
|
|
1013
|
+
out.set(p, offset);
|
|
1014
|
+
offset += p.length;
|
|
1015
|
+
}
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
1018
|
+
function signaturesToProofBytes2(signatures) {
|
|
1019
|
+
const chunks = [];
|
|
1020
|
+
for (const sig of signatures) {
|
|
1021
|
+
if (typeof sig !== "string" || sig.length !== 132 || !/^[0-9a-fA-F]+$/.test(sig)) {
|
|
1022
|
+
throw new QueryPortfolioError(
|
|
1023
|
+
"PROXY_RESPONSE_INVALID",
|
|
1024
|
+
`Invalid guardian signature format (expected 132 hex chars): ${String(sig)}`
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
chunks.push(hexToUint8Array2(`0x${sig}`));
|
|
1028
|
+
}
|
|
1029
|
+
return concatBytes2(chunks);
|
|
1030
|
+
}
|
|
1031
|
+
function decodeQueryBytes2(bytes) {
|
|
1032
|
+
if (typeof bytes !== "string" || bytes.length === 0) {
|
|
1033
|
+
throw new QueryPortfolioError("PROXY_RESPONSE_INVALID", "Missing query response bytes");
|
|
1034
|
+
}
|
|
1035
|
+
if (isValidHexString2(bytes)) {
|
|
1036
|
+
return hexToUint8Array2(bytes);
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
if (typeof atob === "function") {
|
|
1040
|
+
const raw = atob(bytes);
|
|
1041
|
+
const arr = new Uint8Array(raw.length);
|
|
1042
|
+
for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
|
|
1043
|
+
return arr;
|
|
1044
|
+
}
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1047
|
+
try {
|
|
1048
|
+
return new Uint8Array(Buffer2.from(bytes, "base64"));
|
|
1049
|
+
} catch (cause) {
|
|
1050
|
+
throw new QueryPortfolioError("PROXY_RESPONSE_INVALID", "Unrecognized query response bytes encoding", cause);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function sleep2(ms) {
|
|
1054
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1055
|
+
}
|
|
1056
|
+
var rateLimiter = {
|
|
1057
|
+
queue: [],
|
|
1058
|
+
lastRequest: 0,
|
|
1059
|
+
minInterval: 170,
|
|
1060
|
+
// ~6 req/s with buffer
|
|
1061
|
+
async acquire() {
|
|
1062
|
+
return new Promise((resolve) => {
|
|
1063
|
+
const tryAcquire = () => {
|
|
1064
|
+
const now = Date.now();
|
|
1065
|
+
const elapsed = now - rateLimiter.lastRequest;
|
|
1066
|
+
if (elapsed >= rateLimiter.minInterval) {
|
|
1067
|
+
rateLimiter.lastRequest = now;
|
|
1068
|
+
resolve();
|
|
1069
|
+
} else {
|
|
1070
|
+
setTimeout(tryAcquire, rateLimiter.minInterval - elapsed);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
tryAcquire();
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
async function withExponentialBackoff2(fn, maxAttempts) {
|
|
1078
|
+
let attempt = 0;
|
|
1079
|
+
let lastError;
|
|
1080
|
+
while (attempt < maxAttempts) {
|
|
1081
|
+
try {
|
|
1082
|
+
return await fn();
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
lastError = err;
|
|
1085
|
+
attempt += 1;
|
|
1086
|
+
if (attempt >= maxAttempts) break;
|
|
1087
|
+
const baseMs = 250;
|
|
1088
|
+
const backoffMs = Math.min(5e3, baseMs * 2 ** (attempt - 1));
|
|
1089
|
+
const jitterMs = Math.floor(Math.random() * 100);
|
|
1090
|
+
await sleep2(backoffMs + jitterMs);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
throw lastError;
|
|
1094
|
+
}
|
|
1095
|
+
function getChainConfigs(network) {
|
|
1096
|
+
return network === "testnet" ? TESTNET_CHAINS : MAINNET_CHAINS;
|
|
1097
|
+
}
|
|
1098
|
+
function getProxyEndpoint(network, options) {
|
|
1099
|
+
if (options?.endpoint) return options.endpoint;
|
|
1100
|
+
return network === "testnet" ? WORMHOLE_QUERY_PROXY_URLS.testnet : WORMHOLE_QUERY_PROXY_URLS.mainnet;
|
|
1101
|
+
}
|
|
1102
|
+
function getDefaultPortfolioChains(network) {
|
|
1103
|
+
return network === "testnet" ? [10004, 10005, 10003, 1] : [30, 24, 23, 1];
|
|
1104
|
+
}
|
|
1105
|
+
function normalizeHex32(hex) {
|
|
1106
|
+
if (typeof hex !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(hex)) {
|
|
1107
|
+
throw new QueryPortfolioError("INVALID_ARGUMENT", `Invalid userKeyHash (expected 0x + 32 bytes hex): ${hex}`);
|
|
1108
|
+
}
|
|
1109
|
+
return new Uint8Array(Buffer2.from(hex.slice(2), "hex"));
|
|
1110
|
+
}
|
|
1111
|
+
function deriveSolanaVaultAddress(programIdBase58, userKeyHash) {
|
|
1112
|
+
const programId = new PublicKey(programIdBase58);
|
|
1113
|
+
const keyHashBytes = normalizeHex32(userKeyHash);
|
|
1114
|
+
const [vaultPda] = PublicKey.findProgramAddressSync([Buffer2.from("vault"), Buffer2.from(keyHashBytes)], programId);
|
|
1115
|
+
return vaultPda.toBase58();
|
|
1116
|
+
}
|
|
1117
|
+
function makeCacheKey(userKeyHash, apiKey, options, chainIds) {
|
|
1118
|
+
const payload = {
|
|
1119
|
+
userKeyHash,
|
|
1120
|
+
network: resolveNetwork2(options),
|
|
1121
|
+
chainIds: [...chainIds].sort((a, b) => a - b),
|
|
1122
|
+
vaultAddresses: options?.vaultAddresses ?? null,
|
|
1123
|
+
evmTokenAddresses: options?.evmTokenAddresses ?? null,
|
|
1124
|
+
solanaAccounts: options?.solanaAccounts ?? null,
|
|
1125
|
+
endpoint: options?.endpoint ?? null
|
|
1126
|
+
};
|
|
1127
|
+
void apiKey;
|
|
1128
|
+
return JSON.stringify(payload);
|
|
1129
|
+
}
|
|
1130
|
+
function safeNumberFromBigint(b) {
|
|
1131
|
+
const n = Number(b);
|
|
1132
|
+
return Number.isFinite(n) ? n : 0;
|
|
1133
|
+
}
|
|
1134
|
+
function priceLookup(pricesUsd, symbol, assetId) {
|
|
1135
|
+
if (!pricesUsd) return void 0;
|
|
1136
|
+
if (symbol && pricesUsd[symbol] != null) return pricesUsd[symbol];
|
|
1137
|
+
if (assetId && pricesUsd[assetId] != null) return pricesUsd[assetId];
|
|
1138
|
+
return void 0;
|
|
1139
|
+
}
|
|
1140
|
+
function sumUsd(balances, pricesUsd) {
|
|
1141
|
+
let total = 0;
|
|
1142
|
+
let any = false;
|
|
1143
|
+
for (const b of balances) {
|
|
1144
|
+
const p = priceLookup(pricesUsd, b.symbol, b.assetId);
|
|
1145
|
+
if (p == null) continue;
|
|
1146
|
+
if (b.decimals == null) continue;
|
|
1147
|
+
const denom = 10 ** b.decimals;
|
|
1148
|
+
const amount = Number(b.amount) / denom;
|
|
1149
|
+
if (!Number.isFinite(amount)) continue;
|
|
1150
|
+
total += amount * p;
|
|
1151
|
+
any = true;
|
|
1152
|
+
}
|
|
1153
|
+
return any ? total : void 0;
|
|
1154
|
+
}
|
|
1155
|
+
async function getRecentBlockTag(rpcUrl) {
|
|
1156
|
+
const provider = new ethers4.JsonRpcProvider(rpcUrl);
|
|
1157
|
+
const latest = await provider.getBlockNumber();
|
|
1158
|
+
return Math.max(0, latest - 2);
|
|
1159
|
+
}
|
|
1160
|
+
function encodeErc20BalanceCalls(vaultAddress, tokenAddresses) {
|
|
1161
|
+
const iface = new ethers4.Interface(["function balanceOf(address owner) view returns (uint256)"]);
|
|
1162
|
+
return tokenAddresses.map((token) => ({
|
|
1163
|
+
to: token,
|
|
1164
|
+
data: iface.encodeFunctionData("balanceOf", [vaultAddress])
|
|
1165
|
+
}));
|
|
1166
|
+
}
|
|
1167
|
+
function decodeErc20Balances(results, tokenAddresses) {
|
|
1168
|
+
const iface = new ethers4.Interface(["function balanceOf(address owner) view returns (uint256)"]);
|
|
1169
|
+
const out = [];
|
|
1170
|
+
for (let i = 0; i < tokenAddresses.length; i++) {
|
|
1171
|
+
const data = results[i];
|
|
1172
|
+
if (!data || data === "0x") {
|
|
1173
|
+
out.push(0n);
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const decoded = iface.decodeFunctionResult("balanceOf", data);
|
|
1177
|
+
out.push(decoded[0]);
|
|
1178
|
+
}
|
|
1179
|
+
return out;
|
|
1180
|
+
}
|
|
1181
|
+
async function queryPortfolio(userKeyHash, apiKey, options) {
|
|
1182
|
+
const network = resolveNetwork2(options);
|
|
1183
|
+
const maxAgeSeconds = options?.maxAge ?? 60;
|
|
1184
|
+
const maxAttempts = options?.maxAttempts ?? 4;
|
|
1185
|
+
const cacheTtlMs = options?.cacheTtlMs ?? 3e4;
|
|
1186
|
+
const endpoint = getProxyEndpoint(network, options);
|
|
1187
|
+
const chainIds = options?.rpcUrls ? Object.keys(options.rpcUrls).map(Number) : void 0;
|
|
1188
|
+
void chainIds;
|
|
1189
|
+
const requestedChains = options?.vaultAddresses ? Object.keys(options.vaultAddresses).map(Number) : void 0;
|
|
1190
|
+
const defaultChains = getDefaultPortfolioChains(network);
|
|
1191
|
+
const wormholeChainIds = options?.evmTokenAddresses ? Array.from(/* @__PURE__ */ new Set([...defaultChains, ...Object.keys(options.evmTokenAddresses).map(Number)])) : defaultChains;
|
|
1192
|
+
const finalChainIds = requestedChains ? Array.from(/* @__PURE__ */ new Set([...wormholeChainIds, ...requestedChains])) : wormholeChainIds;
|
|
1193
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
1194
|
+
throw new QueryPortfolioError("INVALID_ARGUMENT", "Missing Query Proxy apiKey");
|
|
1195
|
+
}
|
|
1196
|
+
const cacheKey = makeCacheKey(userKeyHash, apiKey, options, finalChainIds);
|
|
1197
|
+
const cached = PORTFOLIO_CACHE.get(cacheKey);
|
|
1198
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
1199
|
+
return cached.value;
|
|
1200
|
+
}
|
|
1201
|
+
const chains = getChainConfigs(network);
|
|
1202
|
+
const walletManager = new WalletManager({ cacheAddresses: true, persistToStorage: false });
|
|
1203
|
+
const pricesUsd = {
|
|
1204
|
+
USDC: 1,
|
|
1205
|
+
...options?.pricesUsd ?? {}
|
|
1206
|
+
};
|
|
1207
|
+
const doFetch = async () => {
|
|
1208
|
+
try {
|
|
1209
|
+
const perChainRequests = [];
|
|
1210
|
+
const chainMeta = {};
|
|
1211
|
+
const evmChainIds = finalChainIds.filter((id) => {
|
|
1212
|
+
const cfg = Object.values(chains).find((c) => c.wormholeChainId === id);
|
|
1213
|
+
return cfg?.isEvm;
|
|
1214
|
+
});
|
|
1215
|
+
const blockTagsByChainId = /* @__PURE__ */ new Map();
|
|
1216
|
+
await Promise.all(
|
|
1217
|
+
evmChainIds.map(async (wormholeChainId) => {
|
|
1218
|
+
const cfg = Object.values(chains).find((c) => c.wormholeChainId === wormholeChainId);
|
|
1219
|
+
if (!cfg) return;
|
|
1220
|
+
const rpcUrl = options?.rpcUrls?.[wormholeChainId] ?? cfg.rpcUrl;
|
|
1221
|
+
const blockTag = await getRecentBlockTag(rpcUrl);
|
|
1222
|
+
blockTagsByChainId.set(wormholeChainId, blockTag);
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
for (const wormholeChainId of finalChainIds) {
|
|
1226
|
+
const cfg = Object.values(chains).find((c) => c.wormholeChainId === wormholeChainId);
|
|
1227
|
+
if (wormholeChainId === 22) {
|
|
1228
|
+
chainMeta[wormholeChainId] = { kind: "unsupported", chainName: cfg?.name };
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (!cfg) {
|
|
1232
|
+
chainMeta[wormholeChainId] = { kind: "unsupported" };
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (cfg.isEvm) {
|
|
1236
|
+
const vaultAddress = options?.vaultAddresses?.[wormholeChainId] ?? (cfg.contracts.vaultFactory && cfg.contracts.vaultImplementation ? walletManager.computeVaultAddress(userKeyHash, cfg.contracts.vaultFactory, cfg.contracts.vaultImplementation) : void 0);
|
|
1237
|
+
chainMeta[wormholeChainId] = { kind: "evm", vaultAddress, chainName: cfg.name };
|
|
1238
|
+
if (!vaultAddress) {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
const tokenAddresses = options?.evmTokenAddresses?.[wormholeChainId] ?? getAllTokens(wormholeChainId).filter((t) => !isNativeToken(t.address)).map((t) => t.address);
|
|
1242
|
+
if (!tokenAddresses.length) {
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
const blockTag = blockTagsByChainId.get(wormholeChainId);
|
|
1246
|
+
if (blockTag == null) {
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
const calls = encodeErc20BalanceCalls(vaultAddress, tokenAddresses);
|
|
1250
|
+
perChainRequests.push(new PerChainQueryRequest2(wormholeChainId, new EthCallQueryRequest2(blockTag, calls)));
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (wormholeChainId === 1) {
|
|
1254
|
+
const programId = options?.vaultAddresses?.[wormholeChainId] ?? cfg.contracts.hub;
|
|
1255
|
+
chainMeta[wormholeChainId] = { kind: "solana", chainName: cfg.name };
|
|
1256
|
+
if (!programId) {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
const vaultAddress = deriveSolanaVaultAddress(programId, userKeyHash);
|
|
1260
|
+
chainMeta[wormholeChainId] = { kind: "solana", chainName: cfg.name, vaultAddress };
|
|
1261
|
+
const accounts = [vaultAddress, ...options?.solanaAccounts ?? []];
|
|
1262
|
+
perChainRequests.push(
|
|
1263
|
+
new PerChainQueryRequest2(
|
|
1264
|
+
wormholeChainId,
|
|
1265
|
+
new SolanaAccountQueryRequest("finalized", accounts, void 0, 0n, 0n)
|
|
1266
|
+
)
|
|
1267
|
+
);
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
chainMeta[wormholeChainId] = { kind: "unsupported", chainName: cfg.name };
|
|
1271
|
+
}
|
|
1272
|
+
if (perChainRequests.length > 255) {
|
|
1273
|
+
throw new QueryPortfolioError(
|
|
1274
|
+
"INVALID_ARGUMENT",
|
|
1275
|
+
`Too many per-chain requests (${perChainRequests.length}); max is 255 per Query Proxy request`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (perChainRequests.length === 0) {
|
|
1279
|
+
const chainsOut2 = finalChainIds.map((wormholeChainId) => {
|
|
1280
|
+
const meta = chainMeta[wormholeChainId] ?? { kind: "unsupported" };
|
|
1281
|
+
return {
|
|
1282
|
+
wormholeChainId,
|
|
1283
|
+
chainName: meta.chainName,
|
|
1284
|
+
vaultAddress: meta.vaultAddress,
|
|
1285
|
+
balances: [],
|
|
1286
|
+
error: {
|
|
1287
|
+
code: "MISSING_VAULT",
|
|
1288
|
+
message: "Unable to build query: missing vault address or no tokens configured"
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
});
|
|
1292
|
+
return { proof: new Uint8Array(), totalUsd: 0, chains: chainsOut2 };
|
|
1293
|
+
}
|
|
1294
|
+
const request = new QueryRequest2(Date.now() & 4294967295, perChainRequests);
|
|
1295
|
+
const requestHex = Buffer2.from(request.serialize()).toString("hex");
|
|
1296
|
+
await rateLimiter.acquire();
|
|
1297
|
+
let response;
|
|
1298
|
+
try {
|
|
1299
|
+
console.log("[queryPortfolio] Sending request to:", endpoint);
|
|
1300
|
+
console.log("[queryPortfolio] Request bytes length:", requestHex.length);
|
|
1301
|
+
const defaultTimeout = network === "testnet" ? 15e3 : 1e4;
|
|
1302
|
+
const requestTimeout = options?.timeout ?? defaultTimeout;
|
|
1303
|
+
response = await axios2.post(
|
|
1304
|
+
endpoint,
|
|
1305
|
+
{ bytes: requestHex },
|
|
1306
|
+
{
|
|
1307
|
+
headers: {
|
|
1308
|
+
"X-API-Key": apiKey,
|
|
1309
|
+
"Content-Type": "application/json"
|
|
1310
|
+
},
|
|
1311
|
+
timeout: requestTimeout
|
|
1312
|
+
}
|
|
1313
|
+
);
|
|
1314
|
+
} catch (axiosErr) {
|
|
1315
|
+
const err = axiosErr;
|
|
1316
|
+
if (!err.response) {
|
|
1317
|
+
console.error("[queryPortfolio] Network error:", {
|
|
1318
|
+
message: err.message,
|
|
1319
|
+
code: err.code,
|
|
1320
|
+
name: err.name
|
|
1321
|
+
});
|
|
1322
|
+
throw new QueryPortfolioError(
|
|
1323
|
+
"NETWORK_ERROR",
|
|
1324
|
+
`Network error: ${err.message} (code: ${err.code})`
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
const status = err.response.status;
|
|
1328
|
+
const errData = err.response.data;
|
|
1329
|
+
console.error("[queryPortfolio] Wormhole Query Proxy error:", { status, data: errData });
|
|
1330
|
+
throw new QueryPortfolioError(
|
|
1331
|
+
"PROXY_RESPONSE_INVALID",
|
|
1332
|
+
`Query Proxy returned ${status}: ${JSON.stringify(errData)}`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
const data = response.data;
|
|
1336
|
+
const signatures = data?.signatures;
|
|
1337
|
+
const bytes = data?.bytes;
|
|
1338
|
+
if (!Array.isArray(signatures) || typeof bytes !== "string") {
|
|
1339
|
+
throw new QueryPortfolioError("PROXY_RESPONSE_INVALID", "Query Proxy response missing signatures/bytes");
|
|
1340
|
+
}
|
|
1341
|
+
const proof = signaturesToProofBytes2(signatures);
|
|
1342
|
+
const queryBytes = decodeQueryBytes2(bytes);
|
|
1343
|
+
const parsed = QueryResponse2.from(queryBytes);
|
|
1344
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
1345
|
+
const chainsOut = [];
|
|
1346
|
+
for (const wormholeChainId of finalChainIds) {
|
|
1347
|
+
const meta = chainMeta[wormholeChainId] ?? { kind: "unsupported" };
|
|
1348
|
+
if (meta.kind === "unsupported") {
|
|
1349
|
+
const msg = wormholeChainId === 22 ? "Aptos Queries are not supported by the Wormhole Query SDK yet" : "Chain is not supported by this portfolio query implementation";
|
|
1350
|
+
chainsOut.push({
|
|
1351
|
+
wormholeChainId,
|
|
1352
|
+
chainName: meta.chainName,
|
|
1353
|
+
balances: [],
|
|
1354
|
+
error: { code: "UNSUPPORTED_CHAIN", message: msg }
|
|
1355
|
+
});
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (!meta.vaultAddress) {
|
|
1359
|
+
chainsOut.push({
|
|
1360
|
+
wormholeChainId,
|
|
1361
|
+
chainName: meta.chainName,
|
|
1362
|
+
balances: [],
|
|
1363
|
+
error: { code: "MISSING_VAULT", message: "Unable to derive vault address for chain" }
|
|
1364
|
+
});
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
const perChain = parsed.responses.find((r) => r.chainId === wormholeChainId);
|
|
1368
|
+
if (!perChain) {
|
|
1369
|
+
chainsOut.push({
|
|
1370
|
+
wormholeChainId,
|
|
1371
|
+
chainName: meta.chainName,
|
|
1372
|
+
vaultAddress: meta.vaultAddress,
|
|
1373
|
+
balances: [],
|
|
1374
|
+
error: { code: "DECODE_ERROR", message: "Missing per-chain response from Query Proxy" }
|
|
1375
|
+
});
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
if (meta.kind === "evm") {
|
|
1380
|
+
const cfg = Object.values(chains).find((c) => c.wormholeChainId === wormholeChainId);
|
|
1381
|
+
const tokenAddresses = options?.evmTokenAddresses?.[wormholeChainId] ?? getAllTokens(wormholeChainId).filter((t) => !isNativeToken(t.address)).map((t) => t.address);
|
|
1382
|
+
if (!tokenAddresses.length) {
|
|
1383
|
+
chainsOut.push({
|
|
1384
|
+
wormholeChainId,
|
|
1385
|
+
chainName: meta.chainName,
|
|
1386
|
+
vaultAddress: meta.vaultAddress,
|
|
1387
|
+
balances: [],
|
|
1388
|
+
error: { code: "MISSING_TOKENS", message: "No ERC20 tokens configured for EVM portfolio query" }
|
|
1389
|
+
});
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
const chainResp2 = EthCallQueryResponse2.from(perChain.response.serialize());
|
|
1393
|
+
const blockTime2 = Number(chainResp2.blockTime);
|
|
1394
|
+
const age2 = nowSeconds - blockTime2;
|
|
1395
|
+
if (age2 > maxAgeSeconds) {
|
|
1396
|
+
chainsOut.push({
|
|
1397
|
+
wormholeChainId,
|
|
1398
|
+
chainName: meta.chainName,
|
|
1399
|
+
vaultAddress: meta.vaultAddress,
|
|
1400
|
+
blockTime: blockTime2,
|
|
1401
|
+
balances: [],
|
|
1402
|
+
error: { code: "ATTESTATION_STALE", message: `Attestation stale by ${age2}s (maxAge=${maxAgeSeconds}s)` }
|
|
1403
|
+
});
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const amounts = decodeErc20Balances(chainResp2.results, tokenAddresses);
|
|
1407
|
+
const tokenInfos = getAllTokens(wormholeChainId).filter((t) => !isNativeToken(t.address));
|
|
1408
|
+
const balances2 = tokenAddresses.map((addr, i) => {
|
|
1409
|
+
const info = tokenInfos.find((t) => t.address.toLowerCase() === addr.toLowerCase());
|
|
1410
|
+
const symbol = info?.symbol;
|
|
1411
|
+
const decimals = info?.decimals;
|
|
1412
|
+
const usdPrice = priceLookup(pricesUsd, symbol, addr);
|
|
1413
|
+
const usdValue = usdPrice != null && decimals != null ? Number(amounts[i]) / 10 ** decimals * usdPrice : void 0;
|
|
1414
|
+
return {
|
|
1415
|
+
assetId: addr,
|
|
1416
|
+
amount: amounts[i],
|
|
1417
|
+
symbol,
|
|
1418
|
+
decimals,
|
|
1419
|
+
usdValue: Number.isFinite(usdValue ?? NaN) ? usdValue : void 0
|
|
1420
|
+
};
|
|
1421
|
+
});
|
|
1422
|
+
void cfg;
|
|
1423
|
+
chainsOut.push({
|
|
1424
|
+
wormholeChainId,
|
|
1425
|
+
chainName: meta.chainName,
|
|
1426
|
+
vaultAddress: meta.vaultAddress,
|
|
1427
|
+
blockTime: blockTime2,
|
|
1428
|
+
balances: balances2
|
|
1429
|
+
});
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
const chainResp = SolanaAccountQueryResponse.from(perChain.response.serialize());
|
|
1433
|
+
const blockTime = safeNumberFromBigint(chainResp.blockTime);
|
|
1434
|
+
const age = nowSeconds - blockTime;
|
|
1435
|
+
if (age > maxAgeSeconds) {
|
|
1436
|
+
chainsOut.push({
|
|
1437
|
+
wormholeChainId,
|
|
1438
|
+
chainName: meta.chainName,
|
|
1439
|
+
vaultAddress: meta.vaultAddress,
|
|
1440
|
+
blockTime,
|
|
1441
|
+
balances: [],
|
|
1442
|
+
error: { code: "ATTESTATION_STALE", message: `Attestation stale by ${age}s (maxAge=${maxAgeSeconds}s)` }
|
|
1443
|
+
});
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
const vaultResult = chainResp.results[0];
|
|
1447
|
+
const lamports = vaultResult?.lamports ?? 0n;
|
|
1448
|
+
const balances = [
|
|
1449
|
+
{
|
|
1450
|
+
assetId: "SOL",
|
|
1451
|
+
amount: lamports,
|
|
1452
|
+
decimals: 9,
|
|
1453
|
+
symbol: "SOL",
|
|
1454
|
+
usdValue: priceLookup(pricesUsd, "SOL", "SOL") != null ? Number(lamports) / 1e9 * priceLookup(pricesUsd, "SOL", "SOL") : void 0
|
|
1455
|
+
}
|
|
1456
|
+
];
|
|
1457
|
+
chainsOut.push({
|
|
1458
|
+
wormholeChainId,
|
|
1459
|
+
chainName: meta.chainName,
|
|
1460
|
+
vaultAddress: meta.vaultAddress,
|
|
1461
|
+
blockTime,
|
|
1462
|
+
balances
|
|
1463
|
+
});
|
|
1464
|
+
} catch (cause) {
|
|
1465
|
+
chainsOut.push({
|
|
1466
|
+
wormholeChainId,
|
|
1467
|
+
chainName: meta.chainName,
|
|
1468
|
+
vaultAddress: meta.vaultAddress,
|
|
1469
|
+
balances: [],
|
|
1470
|
+
error: { code: "DECODE_ERROR", message: `Failed to decode chain response: ${String(cause?.message ?? cause)}` }
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
const allBalances = chainsOut.flatMap((c) => c.balances);
|
|
1475
|
+
const totalUsd = sumUsd(allBalances, pricesUsd);
|
|
1476
|
+
const result2 = { proof, totalUsd, chains: chainsOut };
|
|
1477
|
+
PORTFOLIO_CACHE.set(cacheKey, { expiresAt: Date.now() + cacheTtlMs, value: result2 });
|
|
1478
|
+
return result2;
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
if (err instanceof QueryPortfolioError) throw err;
|
|
1481
|
+
if (axios2.isAxiosError(err)) {
|
|
1482
|
+
const ax = err;
|
|
1483
|
+
const status = ax.response?.status;
|
|
1484
|
+
const statusText = ax.response?.statusText;
|
|
1485
|
+
const details = typeof ax.response?.data === "string" ? ax.response?.data : void 0;
|
|
1486
|
+
throw new QueryPortfolioError(
|
|
1487
|
+
"PROXY_HTTP_ERROR",
|
|
1488
|
+
`Query Proxy request failed${status ? ` (${status} ${statusText ?? ""})` : ""}${details ? `: ${details}` : ""}`,
|
|
1489
|
+
err
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
throw new QueryPortfolioError("PROXY_HTTP_ERROR", "Query Proxy request failed", err);
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
const result = await withExponentialBackoff2(doFetch, maxAttempts);
|
|
1496
|
+
PORTFOLIO_CACHE.set(cacheKey, { expiresAt: Date.now() + cacheTtlMs, value: result });
|
|
1497
|
+
return result;
|
|
1498
|
+
}
|
|
1499
|
+
export {
|
|
1500
|
+
QueryHubStateError,
|
|
1501
|
+
QueryPortfolioError,
|
|
1502
|
+
WORMHOLE_QUERY_CHAIN_IDS,
|
|
1503
|
+
WORMHOLE_QUERY_PROXY_URLS,
|
|
1504
|
+
WORMHOLE_QUERY_RATE_LIMIT_PER_SECOND,
|
|
1505
|
+
queryHubState,
|
|
1506
|
+
queryPortfolio
|
|
1507
|
+
};
|
|
1508
|
+
//# sourceMappingURL=index.mjs.map
|