@kaleidorg/wallet-engine 1.0.0-beta.30 → 1.0.0-beta.32

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.
Files changed (49) hide show
  1. package/dist/adapters/ArkadeAdapter.d.ts +78 -15
  2. package/dist/adapters/ArkadeAdapter.d.ts.map +1 -1
  3. package/dist/adapters/ArkadeAdapter.js +652 -162
  4. package/dist/adapters/ArkadeAdapter.js.map +1 -1
  5. package/dist/adapters/native.d.ts +1 -0
  6. package/dist/adapters/native.d.ts.map +1 -1
  7. package/dist/adapters/native.js +1 -0
  8. package/dist/adapters/native.js.map +1 -1
  9. package/dist/adapters/wdk/RgbLibWasmAdapter.d.ts +14 -0
  10. package/dist/adapters/wdk/RgbLibWasmAdapter.d.ts.map +1 -1
  11. package/dist/adapters/wdk/RgbLibWasmAdapter.js +30 -0
  12. package/dist/adapters/wdk/RgbLibWasmAdapter.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +5 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/arkade-client-manager.d.ts +64 -24
  18. package/dist/lib/arkade-client-manager.d.ts.map +1 -1
  19. package/dist/lib/arkade-client-manager.js +240 -65
  20. package/dist/lib/arkade-client-manager.js.map +1 -1
  21. package/dist/lib/arkade-converters.d.ts +39 -0
  22. package/dist/lib/arkade-converters.d.ts.map +1 -0
  23. package/dist/lib/arkade-converters.js +148 -0
  24. package/dist/lib/arkade-converters.js.map +1 -0
  25. package/dist/lib/arkade-helpers.d.ts +110 -0
  26. package/dist/lib/arkade-helpers.d.ts.map +1 -0
  27. package/dist/lib/arkade-helpers.js +227 -0
  28. package/dist/lib/arkade-helpers.js.map +1 -0
  29. package/dist/lib/arkade-swaps-client-manager.d.ts +55 -0
  30. package/dist/lib/arkade-swaps-client-manager.d.ts.map +1 -0
  31. package/dist/lib/arkade-swaps-client-manager.js +127 -0
  32. package/dist/lib/arkade-swaps-client-manager.js.map +1 -0
  33. package/dist/lib/arkade-vtxo-lifecycle.d.ts +116 -0
  34. package/dist/lib/arkade-vtxo-lifecycle.d.ts.map +1 -0
  35. package/dist/lib/arkade-vtxo-lifecycle.js +184 -0
  36. package/dist/lib/arkade-vtxo-lifecycle.js.map +1 -0
  37. package/dist/lib/ln-message-sign.d.ts +20 -0
  38. package/dist/lib/ln-message-sign.d.ts.map +1 -0
  39. package/dist/lib/ln-message-sign.js +90 -0
  40. package/dist/lib/ln-message-sign.js.map +1 -0
  41. package/dist/lib/log.d.ts +15 -0
  42. package/dist/lib/log.d.ts.map +1 -0
  43. package/dist/lib/log.js +16 -0
  44. package/dist/lib/log.js.map +1 -0
  45. package/dist/lib/zbase32.d.ts +3 -0
  46. package/dist/lib/zbase32.d.ts.map +1 -0
  47. package/dist/lib/zbase32.js +64 -0
  48. package/dist/lib/zbase32.js.map +1 -0
  49. package/package.json +8 -3
@@ -1,316 +1,806 @@
1
1
  /**
2
2
  * Arkade Protocol Adapter
3
- * Implements IProtocolAdapter using @arkade-os/sdk.
4
- * Ported from rate-extension, adapted for React Native with Expo providers.
3
+ * Implements IProtocolAdapter using @arkade-os/sdk v0.4.x.
4
+ *
5
+ * SDK API facts for v0.4.x:
6
+ * - `wallet.getBalance()` includes `assets: { assetId, amount }[]`
7
+ * - `wallet.assetManager.getAssetDetails(assetId)` resolves supply + metadata
8
+ * - `wallet.send({ address, assets: [...] })` sends Arkade-native assets
9
+ * - `wallet.sendBitcoin({ address, amount })` still sends BTC
10
+ * - `wallet.getTransactionHistory()` → ArkTransaction[]
11
+ * where ArkTransaction = { key, type: TxType, amount: number, settled: boolean, createdAt: number }
12
+ * - `TxType.TxSent = "SENT"`, `TxType.TxReceived = "RECEIVED"`
13
+ * - `WalletBalance.boarding.total` (number), `.settled`, `.preconfirmed`, `.available`, `.recoverable`, `.total`
5
14
  */
6
- import { arkadeClientManager } from '../lib/arkade-client-manager.js';
7
- import { ProtocolError, ConnectionError, } from '../types/base.js';
8
- import { PROTOCOL_OPERATIONS } from '../capabilities/operations.js';
15
+ import { mnemonicToSeedSync } from "@scure/bip39";
16
+ import { HDKey } from "@scure/bip32";
17
+ import { signLnMessage, verifyLnMessage } from "../lib/ln-message-sign.js";
18
+ import { log } from "../lib/log.js";
19
+ import { arkadeClientManager } from "../lib/arkade-client-manager.js";
20
+ import { arkadeSwapsClientManager } from "../lib/arkade-swaps-client-manager.js";
21
+ import { Ramps, isSpendable, } from "@arkade-os/sdk";
22
+ import { PROTOCOL_OPERATIONS } from "../capabilities/operations.js";
23
+ import { formatSats, formatUnits, getAssetMetadata, getAssetName, getAssetPrecision, getAssetTicker, normalizeVtxos, selectVtxosByExpiry, sortVtxosByExpiry, toNumber, toPositiveIntegerBigInt, toStringValue, } from "../lib/arkade-helpers.js";
24
+ import { convertArkTxToUnifiedAll } from "../lib/arkade-converters.js";
25
+ import { ProtocolError, ConnectionError, } from "../types/base.js";
26
+ /** Bare bolt11 prefixes (lnbc / lntb / lnbcrt / lnsb) — case-insensitive. */
27
+ function isLightningInvoice(value) {
28
+ if (!value)
29
+ return false;
30
+ const lower = value.trim().toLowerCase();
31
+ // Strip a `lightning:` URI prefix if present.
32
+ const body = lower.startsWith("lightning:") ? lower.slice("lightning:".length) : lower;
33
+ return /^ln(bc|tb|bcrt|sb)/.test(body);
34
+ }
35
+ function stripLightningPrefix(value) {
36
+ const trimmed = value.trim();
37
+ return trimmed.toLowerCase().startsWith("lightning:")
38
+ ? trimmed.slice("lightning:".length)
39
+ : trimmed;
40
+ }
41
+ function isArkadeAddress(value) {
42
+ return /^(ark1|tark1)/i.test(value.trim());
43
+ }
9
44
  export class ArkadeAdapter {
10
45
  constructor() {
11
- this.protocolName = 'ARKADE';
46
+ this.protocolName = "ARKADE";
47
+ this.supportedLayers = ["BTC_ARKADE", "BTC_L1", "ARKADE_ARKADE"];
48
+ this.version = "1.0.0";
12
49
  this.capabilities = PROTOCOL_OPERATIONS.ARKADE;
13
- this.supportedLayers = ['BTC_ARKADE', 'BTC_L1', 'ARKADE_ARKADE'];
14
- this.version = '1.0.0';
15
50
  this.config = null;
51
+ this.assetDetailsCache = new Map();
16
52
  }
17
- // ========================================================================
53
+ // =========================================================================
18
54
  // Connection Management
19
- // ========================================================================
55
+ // =========================================================================
20
56
  async connect(config) {
21
57
  const arkadeConfig = config;
22
58
  if (!arkadeConfig.mnemonic) {
23
- throw new ConnectionError('Mnemonic is required for Arkade wallet', 'ARKADE');
59
+ throw new ConnectionError("Wallet recovery secret is required for Arkade wallet", "ARKADE");
24
60
  }
25
61
  if (!arkadeConfig.arkServerUrl) {
26
- throw new ConnectionError('arkServerUrl is required for Arkade wallet', 'ARKADE');
62
+ throw new ConnectionError("arkServerUrl is required for Arkade wallet", "ARKADE");
27
63
  }
28
64
  try {
29
65
  await arkadeClientManager.initialize(arkadeConfig);
30
66
  this.config = arkadeConfig;
31
- console.log('[ArkadeAdapter] Connected to Arkade successfully');
67
+ this.assetDetailsCache.clear();
68
+ log.info("[ArkadeAdapter] Connected to Arkade successfully");
69
+ // Initialize the Boltz swap client in the background. Failures are
70
+ // non-fatal — swaps just stay unavailable until the next connect.
71
+ const wallet = arkadeClientManager.getWallet();
72
+ arkadeSwapsClientManager.initialize(wallet).catch((error) => {
73
+ log.warn("[ArkadeAdapter] Boltz swaps init failed (Lightning swaps unavailable):", error);
74
+ });
32
75
  }
33
76
  catch (error) {
34
77
  const msg = error instanceof Error ? error.message : String(error);
35
- throw new ConnectionError(`Failed to connect to Arkade: ${msg}`, 'ARKADE');
78
+ throw new ConnectionError(`Failed to connect to Arkade: ${msg}`, "ARKADE");
36
79
  }
37
80
  }
38
81
  async disconnect() {
82
+ // Dispose Boltz swaps client first (stops SwapManager monitoring) so it
83
+ // doesn't try to use the wallet after we tear it down.
84
+ await arkadeSwapsClientManager.dispose();
39
85
  await arkadeClientManager.disconnect();
40
86
  this.config = null;
41
- console.log('[ArkadeAdapter] Disconnected from Arkade');
87
+ this.assetDetailsCache.clear();
88
+ log.info("[ArkadeAdapter] Disconnected from Arkade");
42
89
  }
43
90
  isConnected() {
44
91
  return arkadeClientManager.isInitialized();
45
92
  }
46
93
  async getConnectionInfo() {
47
94
  if (!this.isConnected()) {
48
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
95
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
49
96
  }
50
97
  return {
51
- protocol: 'ARKADE',
98
+ protocol: "ARKADE",
52
99
  connected: true,
53
- network: this.config?.network ?? 'signet',
100
+ network: this.config?.network ?? "signet",
54
101
  syncStatus: { synced: true, progress: 100 },
55
102
  };
56
103
  }
57
- // ========================================================================
104
+ // =========================================================================
58
105
  // Asset Operations
59
- // ========================================================================
106
+ // =========================================================================
60
107
  async listAssets() {
61
108
  if (!this.isConnected()) {
62
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
109
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
63
110
  }
64
111
  try {
65
112
  const wallet = arkadeClientManager.getWallet();
66
- const balance = await wallet.getBalance();
67
- const totalSats = this.toNumber(balance?.total);
68
- const availableSats = this.toNumber(balance?.available);
69
- const preconfirmed = this.toNumber(balance?.preconfirmed);
70
- return [{
71
- id: 'BTC',
72
- name: 'Bitcoin (Arkade)',
73
- ticker: 'BTC',
74
- precision: 8,
75
- protocol: 'ARKADE',
76
- layer: 'BTC_ARKADE',
113
+ const rawBalance = await wallet.getBalance();
114
+ const balance = await this.getWalletBalanceSummary(wallet);
115
+ const totalSats = balance.total;
116
+ const preconfirmed = balance.preconfirmed;
117
+ const btcAsset = {
118
+ id: "BTC",
119
+ name: "Bitcoin (Arkade)",
120
+ ticker: "BTC",
121
+ precision: 8,
122
+ protocol: "ARKADE",
123
+ layer: "BTC_ARKADE",
124
+ balance: {
125
+ total: totalSats,
126
+ // preconfirmed VTXOs are spendable (isSpendable = !vtxo.isSpent in the SDK),
127
+ // so they count as available just like settled ones.
128
+ available: balance.available,
129
+ pending: 0,
130
+ locked: 0,
131
+ totalDisplay: formatSats(totalSats),
132
+ availableDisplay: formatSats(balance.available),
133
+ },
134
+ capabilities: {
135
+ canSend: true,
136
+ canReceive: true,
137
+ canSwap: false,
138
+ supportsLightning: false,
139
+ supportsOnchain: true,
140
+ },
141
+ metadata: {
142
+ boarding: balance.boardingTotal,
143
+ settled: balance.settled,
144
+ preconfirmed,
145
+ recoverable: balance.recoverable,
146
+ },
147
+ };
148
+ const rawAssets = Array.isArray(rawBalance?.assets) ? rawBalance.assets : [];
149
+ const arkadeAssets = await Promise.all(rawAssets
150
+ .filter((entry) => toStringValue(entry?.assetId) !== "" && toNumber(entry?.amount) > 0)
151
+ .map(async (entry) => {
152
+ const assetId = toStringValue(entry.assetId);
153
+ const amount = toNumber(entry.amount);
154
+ const details = await this.getCachedAssetDetails(wallet, assetId);
155
+ const metadata = getAssetMetadata(details);
156
+ const precision = getAssetPrecision(metadata);
157
+ const ticker = getAssetTicker(assetId, metadata);
158
+ const name = getAssetName(assetId, ticker, metadata);
159
+ const icon = typeof metadata?.icon === "string" ? metadata.icon : undefined;
160
+ const asset = {
161
+ id: assetId,
162
+ name,
163
+ ticker,
164
+ precision,
165
+ protocol: "ARKADE",
166
+ layer: "ARKADE_ARKADE",
77
167
  balance: {
78
- total: totalSats,
79
- available: availableSats,
80
- pending: preconfirmed,
168
+ total: amount,
169
+ available: amount,
170
+ pending: 0,
81
171
  locked: 0,
82
- totalDisplay: this.formatSats(totalSats),
83
- availableDisplay: this.formatSats(availableSats),
172
+ totalDisplay: formatUnits(amount, precision),
173
+ availableDisplay: formatUnits(amount, precision),
84
174
  },
175
+ icon,
85
176
  capabilities: {
86
177
  canSend: true,
87
178
  canReceive: true,
88
179
  canSwap: false,
89
180
  supportsLightning: false,
90
- supportsOnchain: true,
181
+ supportsOnchain: false,
91
182
  },
183
+ // Don't spread `details` — the Arkade SDK returns BigInt /
184
+ // Uint8Array fields (totalSupply, raw identifiers) that crash
185
+ // chrome.runtime.sendMessage with "Could not serialize message".
92
186
  metadata: {
93
- boarding: this.toNumber(balance?.boarding?.total),
94
- settled: this.toNumber(balance?.settled),
95
- preconfirmed,
96
- recoverable: this.toNumber(balance?.recoverable),
187
+ arkadeAssetId: assetId,
188
+ decimals: precision,
97
189
  },
98
- }];
190
+ };
191
+ return asset;
192
+ }));
193
+ return [btcAsset, ...arkadeAssets];
99
194
  }
100
195
  catch (error) {
101
196
  const msg = error instanceof Error ? error.message : String(error);
102
- throw new ProtocolError(`Failed to list assets: ${msg}`, 'ARKADE', 'LIST_ASSETS_ERROR');
197
+ throw new ProtocolError(`Failed to list assets: ${msg}`, "ARKADE", "LIST_ASSETS_ERROR");
103
198
  }
104
199
  }
105
200
  async getAsset(assetId) {
106
201
  const assets = await this.listAssets();
107
- const asset = assets.find(a => a.id === assetId || a.ticker === assetId);
202
+ const asset = assets.find((a) => a.id === assetId || a.ticker === assetId);
108
203
  if (!asset) {
109
- throw new ProtocolError(`Asset not found: ${assetId}`, 'ARKADE', 'ASSET_NOT_FOUND');
204
+ throw new ProtocolError(`Asset not found: ${assetId}`, "ARKADE", "ASSET_NOT_FOUND");
110
205
  }
111
206
  return asset;
112
207
  }
113
208
  async getAssetBalance(assetId) {
209
+ if (assetId === "BTC" || assetId.toLowerCase() === "btc") {
210
+ const asset = await this.getAsset("BTC");
211
+ return asset.balance;
212
+ }
114
213
  const asset = await this.getAsset(assetId);
115
214
  return asset.balance;
116
215
  }
117
- async refreshBalances() { }
118
- // ========================================================================
216
+ async refreshBalances() {
217
+ // Balances are fetched live on each call
218
+ }
219
+ // =========================================================================
119
220
  // Transaction Operations
120
- // ========================================================================
221
+ // =========================================================================
121
222
  async listTransactions(filter) {
122
223
  if (!this.isConnected()) {
123
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
224
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
124
225
  }
125
226
  try {
126
227
  const wallet = arkadeClientManager.getWallet();
127
228
  const history = await wallet.getTransactionHistory();
128
- return (history ?? [])
129
- .map((item) => this.convertArkTx(item))
130
- .filter((tx) => tx !== null)
131
- .filter(tx => {
229
+ const resolveDetails = (assetId) => this.getCachedAssetDetails(wallet, assetId);
230
+ const expanded = await Promise.all((history ?? []).map((item) => convertArkTxToUnifiedAll(item, resolveDetails)));
231
+ const validTxs = expanded.flat();
232
+ return validTxs
233
+ .filter((tx) => {
132
234
  if (!filter)
133
235
  return true;
236
+ if (filter.asset && tx.asset?.id !== filter.asset)
237
+ return false;
134
238
  if (filter.type && tx.type !== filter.type)
135
239
  return false;
136
240
  if (filter.status && tx.status !== filter.status)
137
241
  return false;
242
+ if (filter.fromTimestamp && tx.timestamp < filter.fromTimestamp)
243
+ return false;
244
+ if (filter.toTimestamp && tx.timestamp > filter.toTimestamp)
245
+ return false;
138
246
  return true;
139
247
  })
140
248
  .slice(filter?.offset ?? 0, filter?.limit ? (filter.offset ?? 0) + filter.limit : undefined);
141
249
  }
142
250
  catch (error) {
143
251
  const msg = error instanceof Error ? error.message : String(error);
144
- throw new ProtocolError(`Failed to list transactions: ${msg}`, 'ARKADE', 'LIST_TRANSACTIONS_ERROR');
252
+ throw new ProtocolError(`Failed to list transactions: ${msg}`, "ARKADE", "LIST_TRANSACTIONS_ERROR");
145
253
  }
146
254
  }
147
255
  async getTransaction(txId) {
148
256
  const txs = await this.listTransactions();
149
- const tx = txs.find(t => t.id === txId);
257
+ const tx = txs.find((t) => t.id === txId);
150
258
  if (!tx) {
151
- throw new ProtocolError(`Transaction not found: ${txId}`, 'ARKADE', 'TX_NOT_FOUND');
259
+ throw new ProtocolError(`Transaction not found: ${txId}`, "ARKADE", "TX_NOT_FOUND");
152
260
  }
153
261
  return tx;
154
262
  }
155
- // ========================================================================
263
+ // =========================================================================
156
264
  // Payment Operations
157
- // ========================================================================
265
+ // =========================================================================
158
266
  async createInvoice(request) {
159
267
  if (!this.isConnected()) {
160
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
268
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
269
+ }
270
+ try {
271
+ const wallet = arkadeClientManager.getWallet();
272
+ const address = await wallet.getAddress();
273
+ return {
274
+ invoice: address,
275
+ paymentHash: "",
276
+ amount: request.amount,
277
+ expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
278
+ description: request.description ?? "Arkade receiving address",
279
+ };
280
+ }
281
+ catch (error) {
282
+ const msg = error instanceof Error ? error.message : String(error);
283
+ throw new ProtocolError(`Failed to create invoice: ${msg}`, "ARKADE", "CREATE_INVOICE_ERROR");
284
+ }
285
+ }
286
+ /**
287
+ * Generate a Boltz reverse-swap Lightning invoice that, when paid, lands
288
+ * the funds in this Arkade wallet as a VTXO. Requires amount > 0 — Boltz
289
+ * can't issue an amountless invoice. The embedded `SwapManager` claims
290
+ * the VHTLC automatically once the LN payment settles.
291
+ */
292
+ async createArkadeLightningInvoice(request) {
293
+ if (!this.isConnected()) {
294
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
295
+ }
296
+ if (!request.amount || request.amount <= 0) {
297
+ throw new ProtocolError("Amount is required for Boltz Lightning invoices into Arkade", "ARKADE", "INVALID_AMOUNT");
298
+ }
299
+ if (!arkadeSwapsClientManager.isInitialized()) {
300
+ throw new ProtocolError("Lightning swaps are not ready yet. Try again in a moment.", "ARKADE", "SWAPS_NOT_READY");
301
+ }
302
+ try {
303
+ const swaps = arkadeSwapsClientManager.getClient();
304
+ const result = await swaps.createLightningInvoice({
305
+ amount: request.amount,
306
+ description: request.description,
307
+ });
308
+ return {
309
+ invoice: result.invoice,
310
+ paymentHash: result.paymentHash ?? "",
311
+ amount: request.amount,
312
+ expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
313
+ description: request.description ?? "Boltz reverse swap into Arkade",
314
+ };
315
+ }
316
+ catch (error) {
317
+ const msg = error instanceof Error ? error.message : String(error);
318
+ throw new ProtocolError(`Failed to create Boltz Lightning invoice: ${msg}`, "ARKADE", "CREATE_INVOICE_ERROR");
161
319
  }
162
- const wallet = arkadeClientManager.getWallet();
163
- const address = await wallet.getAddress();
164
- return {
165
- invoice: address,
166
- paymentHash: '',
167
- amount: request.amount,
168
- expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
169
- description: request.description ?? 'Arkade receiving address',
170
- };
171
320
  }
172
321
  async decodeInvoice(invoice) {
173
- return { paymentHash: '', expiresAt: 0, destination: invoice };
322
+ // Arkade uses addresses, not bolt11 invoices
323
+ return {
324
+ paymentHash: "",
325
+ expiresAt: 0,
326
+ destination: invoice,
327
+ };
174
328
  }
175
329
  async sendPayment(request) {
176
330
  if (!this.isConnected()) {
177
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
331
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
332
+ }
333
+ // Lightning invoice → Boltz submarine swap (Arkade → Lightning).
334
+ // The swap library extracts the amount from the invoice itself, so
335
+ // amountless invoices cannot be paid this way (Boltz rejects them
336
+ // with "0 is less than minimal of 333"). Reject early with a clear
337
+ // error rather than letting Boltz's cryptic message bubble up.
338
+ if (isLightningInvoice(request.invoice)) {
339
+ if (!arkadeSwapsClientManager.isInitialized()) {
340
+ throw new ProtocolError("Lightning swaps are not ready yet. Try again in a moment.", "ARKADE", "SWAPS_NOT_READY");
341
+ }
342
+ const invoiceBody = stripLightningPrefix(request.invoice);
343
+ try {
344
+ const swaps = arkadeSwapsClientManager.getClient();
345
+ const result = await swaps.sendLightningPayment({ invoice: invoiceBody });
346
+ return {
347
+ paymentHash: result.preimage ?? result.txid ?? "",
348
+ amount: result.amount ?? request.amount ?? 0,
349
+ fee: 0,
350
+ // Boltz submarine swap; the swap can still fail in the HODL/claim
351
+ // phase. Caller polls `getPaymentStatus` to reach a terminal state.
352
+ status: "pending",
353
+ timestamp: Date.now(),
354
+ };
355
+ }
356
+ catch (error) {
357
+ const msg = error instanceof Error ? error.message : String(error);
358
+ // Translate Boltz error strings to actionable messages.
359
+ if (/less than minimal/i.test(msg)) {
360
+ throw new ProtocolError("Arkade can't pay amountless Lightning invoices. Please use a different route or ask the recipient for an invoice with an amount.", "ARKADE", "INVALID_AMOUNT");
361
+ }
362
+ if (/vHTLC.*already exists/i.test(msg)) {
363
+ throw new ProtocolError("A swap for this invoice is already in progress. Wait for it to complete or refund before retrying.", "ARKADE", "SWAP_IN_PROGRESS");
364
+ }
365
+ throw new ProtocolError(`Failed to send Lightning payment via Boltz: ${msg}`, "ARKADE", "SEND_PAYMENT_ERROR");
366
+ }
178
367
  }
368
+ // Non-Lightning destinations: existing Ark / on-chain BTC path.
179
369
  if (!request.amount || request.amount <= 0) {
180
- throw new ProtocolError('Amount is required for Arkade payments', 'ARKADE', 'INVALID_AMOUNT');
370
+ throw new ProtocolError("Amount is required for Arkade payments", "ARKADE", "INVALID_AMOUNT");
181
371
  }
182
372
  try {
183
373
  const wallet = arkadeClientManager.getWallet();
374
+ const selectedVtxos = await this.selectSpendableBtcVtxos(wallet, request.amount);
184
375
  const txid = await wallet.sendBitcoin({
185
- address: request.invoice,
186
- amount: request.amount,
376
+ address: request.invoice, // Ark or on-chain address
377
+ amount: request.amount, // satoshis
378
+ ...(selectedVtxos ? { selectedVtxos } : {}),
187
379
  });
188
380
  return {
189
381
  paymentHash: txid,
190
382
  amount: request.amount,
191
383
  fee: 0,
192
- status: 'confirmed',
384
+ // Ark VTXO sends are immediately valid once sendBitcoin resolves.
385
+ // On-chain destinations still need confirmation, so callers should
386
+ // keep polling via getPaymentStatus.
387
+ status: (isArkadeAddress(request.invoice) ? "confirmed" : "pending"),
193
388
  timestamp: Date.now(),
194
389
  };
195
390
  }
196
391
  catch (error) {
197
392
  const msg = error instanceof Error ? error.message : String(error);
198
- throw new ProtocolError(`Failed to send payment: ${msg}`, 'ARKADE', 'SEND_PAYMENT_ERROR');
393
+ throw new ProtocolError(`Failed to send payment: ${msg}`, "ARKADE", "SEND_PAYMENT_ERROR");
199
394
  }
200
395
  }
396
+ /**
397
+ * Resolve a payment's terminal state from the SDK's transaction history.
398
+ * `paymentHash` is the txid returned by `sendBitcoin` / `sendLightningPayment`
399
+ * (Boltz returns a preimage as a fallback if there's no on-chain txid yet —
400
+ * in that case we don't have a history row and the payment stays pending).
401
+ */
201
402
  async getPaymentStatus(paymentHash) {
202
- return { paymentHash, status: 'pending' };
403
+ if (!this.isConnected() || !paymentHash) {
404
+ return { paymentHash, status: "pending" };
405
+ }
406
+ try {
407
+ const wallet = arkadeClientManager.getWallet();
408
+ const history = (await wallet.getTransactionHistory());
409
+ const match = history.find((entry) => {
410
+ const key = entry?.key;
411
+ const id = typeof key === "string" ? key : key?.txid;
412
+ return id === paymentHash;
413
+ });
414
+ if (!match) {
415
+ return { paymentHash, status: "pending" };
416
+ }
417
+ // Arkade's reference wallet treats SENT history rows as settled while
418
+ // leaving unsettled RECEIVED rows as preconfirmed/pending.
419
+ const isSent = match.type === "SENT";
420
+ return {
421
+ paymentHash,
422
+ status: (isSent || match.settled ? "confirmed" : "pending"),
423
+ amount: match.amount,
424
+ timestamp: Date.now(),
425
+ };
426
+ }
427
+ catch (error) {
428
+ log.warn("[ArkadeAdapter] getPaymentStatus history lookup failed:", error);
429
+ return { paymentHash, status: "pending" };
430
+ }
203
431
  }
204
- // ========================================================================
432
+ // =========================================================================
205
433
  // Address Operations
206
- // ========================================================================
434
+ // =========================================================================
207
435
  async getReceiveAddress(assetId) {
208
436
  if (!this.isConnected()) {
209
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
437
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
210
438
  }
211
- const wallet = arkadeClientManager.getWallet();
212
- if (assetId === 'onchain' || assetId === 'boarding') {
213
- const address = await wallet.getBoardingAddress();
214
- return { address, format: 'BTC_ADDRESS', asset: 'BTC' };
439
+ try {
440
+ const wallet = arkadeClientManager.getWallet();
441
+ // 'onchain' or 'boarding' → return on-chain boarding address
442
+ if (assetId === "onchain" || assetId === "boarding") {
443
+ const address = await wallet.getBoardingAddress();
444
+ return { address, format: "BTC_ADDRESS", asset: "BTC" };
445
+ }
446
+ // Default → Ark address (off-chain)
447
+ const address = await wallet.getAddress();
448
+ return {
449
+ address,
450
+ format: "ARKADE_ADDRESS",
451
+ asset: assetId && assetId !== "BTC" ? assetId : "BTC",
452
+ };
453
+ }
454
+ catch (error) {
455
+ const msg = error instanceof Error ? error.message : String(error);
456
+ throw new ProtocolError(`Failed to get receive address: ${msg}`, "ARKADE", "GET_ADDRESS_ERROR");
215
457
  }
216
- const address = await wallet.getAddress();
217
- return { address, format: 'ARKADE_ADDRESS', asset: 'BTC' };
218
458
  }
219
- // ========================================================================
459
+ // =========================================================================
220
460
  // Node & Balance Operations
221
- // ========================================================================
461
+ // =========================================================================
222
462
  async getNodeInfo() {
223
463
  if (!this.isConnected()) {
224
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
464
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
465
+ }
466
+ try {
467
+ const wallet = arkadeClientManager.getWallet();
468
+ const balance = await this.getWalletBalanceSummary(wallet);
469
+ const spendableSats = balance.available;
470
+ return {
471
+ channelsBalanceMsat: spendableSats * 1000,
472
+ maxPayableMsat: spendableSats * 1000,
473
+ onchainBalanceMsat: balance.boardingConfirmed * 1000,
474
+ pendingOnchainBalanceMsat: balance.boardingUnconfirmed * 1000,
475
+ maxReceivableMsat: 0,
476
+ inboundLiquidityMsats: 0,
477
+ connectedPeers: [],
478
+ utxos: 0,
479
+ };
480
+ }
481
+ catch (error) {
482
+ const msg = error instanceof Error ? error.message : String(error);
483
+ throw new ProtocolError(`Failed to get node info: ${msg}`, "ARKADE", "NODE_INFO_ERROR");
225
484
  }
226
- const wallet = arkadeClientManager.getWallet();
227
- const balance = await wallet.getBalance();
228
- const spendable = this.toNumber(balance?.available);
229
- return {
230
- channelsBalanceMsat: spendable * 1000,
231
- maxPayableMsat: spendable * 1000,
232
- onchainBalanceMsat: this.toNumber(balance?.boarding?.total) * 1000,
233
- pendingOnchainBalanceMsat: 0,
234
- maxReceivableMsat: 0,
235
- inboundLiquidityMsats: 0,
236
- connectedPeers: [],
237
- utxos: 0,
238
- };
239
485
  }
240
486
  async getBtcBalance() {
241
487
  if (!this.isConnected()) {
242
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
488
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
243
489
  }
244
- const wallet = arkadeClientManager.getWallet();
245
- const balance = await wallet.getBalance();
246
- const confirmed = this.toNumber(balance?.available);
247
- const total = this.toNumber(balance?.total);
248
- return { confirmed, unconfirmed: Math.max(total - confirmed, 0), total };
249
- }
250
- async listChannels() { return []; }
251
- async listPayments() { return { payments: await this.listTransactions() }; }
252
- async listTransfers(_options) { return { transfers: [] }; }
253
- // ========================================================================
254
- // Unsupported Operations
255
- // ========================================================================
256
- supportsSwaps() { return false; }
490
+ try {
491
+ const wallet = arkadeClientManager.getWallet();
492
+ const balance = await this.getWalletBalanceSummary(wallet);
493
+ // preconfirmed VTXOs are spendable (isSpendable = !vtxo.isSpent in the SDK),
494
+ // so include them in `confirmed` so the Withdraw UI sees the full spendable balance.
495
+ const confirmed = balance.available; // settled + preconfirmed
496
+ const total = balance.total;
497
+ const unconfirmed = Math.max(total - confirmed, 0);
498
+ return { confirmed, unconfirmed, total };
499
+ }
500
+ catch (error) {
501
+ const msg = error instanceof Error ? error.message : String(error);
502
+ throw new ProtocolError(`Failed to get BTC balance: ${msg}`, "ARKADE", "BALANCE_ERROR");
503
+ }
504
+ }
505
+ async listChannels() {
506
+ return [];
507
+ }
508
+ async listPayments() {
509
+ const txs = await this.listTransactions();
510
+ return { payments: txs };
511
+ }
512
+ /**
513
+ * Get all VTXOs, sorted by batchExpiry ascending (expiry-first).
514
+ * This ensures UI consumers see soon-to-expire VTXOs first, and any
515
+ * manual coin selection naturally picks the shortest-lived coins.
516
+ */
517
+ async getVtxos() {
518
+ if (!this.isConnected()) {
519
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
520
+ }
521
+ try {
522
+ const wallet = arkadeClientManager.getWallet();
523
+ const vtxos = await wallet.getVtxos();
524
+ const sorted = sortVtxosByExpiry(vtxos);
525
+ return normalizeVtxos(sorted).map((vtxo) => ({
526
+ txid: vtxo.txid,
527
+ vout: vtxo.vout,
528
+ value: vtxo.value,
529
+ state: vtxo.state,
530
+ batchTxid: vtxo.batchTxid,
531
+ batchExpiry: vtxo.batchExpiry,
532
+ createdAt: vtxo.createdAt,
533
+ assets: vtxo.assets,
534
+ }));
535
+ }
536
+ catch (error) {
537
+ const msg = error instanceof Error ? error.message : String(error);
538
+ throw new ProtocolError(`Failed to get VTXOs: ${msg}`, "ARKADE", "GET_VTXOS_ERROR");
539
+ }
540
+ }
541
+ async getBoardingUtxos() {
542
+ if (!this.isConnected()) {
543
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
544
+ }
545
+ try {
546
+ const wallet = arkadeClientManager.getWallet();
547
+ const utxos = await wallet.getBoardingUtxos();
548
+ return (utxos ?? []).map((u) => ({
549
+ txid: u.txid,
550
+ vout: u.vout,
551
+ value: u.value,
552
+ confirmed: u.status?.confirmed ?? false,
553
+ }));
554
+ }
555
+ catch (error) {
556
+ const msg = error instanceof Error ? error.message : String(error);
557
+ throw new ProtocolError(`Failed to get boarding UTXOs: ${msg}`, "ARKADE", "GET_BOARDING_UTXOS_ERROR");
558
+ }
559
+ }
560
+ /**
561
+ * Onboard — settle boarding UTXOs into VTXOs via a Commitment Transaction.
562
+ * Requires at least one confirmed boarding UTXO.
563
+ * Returns the commitment txid.
564
+ */
565
+ async onboard() {
566
+ if (!this.isConnected()) {
567
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
568
+ }
569
+ try {
570
+ const wallet = arkadeClientManager.getWallet();
571
+ // Get current fee info from server
572
+ const info = await wallet.arkProvider.getInfo();
573
+ const commitmentTxid = await new Ramps(wallet).onboard(info.fees);
574
+ return { txid: commitmentTxid };
575
+ }
576
+ catch (error) {
577
+ const msg = error instanceof Error ? error.message : String(error);
578
+ throw new ProtocolError(`Onboard failed: ${msg}`, "ARKADE", "ONBOARD_ERROR");
579
+ }
580
+ }
581
+ /**
582
+ * Offboard — collaborative exit: convert VTXOs back to an on-chain Bitcoin UTXO.
583
+ * @param address Bitcoin on-chain destination (bc1/tb1)
584
+ * @param amount Optional sats to offboard; undefined = exit all
585
+ */
586
+ async offboard(address, amount) {
587
+ if (!this.isConnected()) {
588
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
589
+ }
590
+ if (!address) {
591
+ throw new ProtocolError("Destination address required for offboard", "ARKADE", "INVALID_ADDRESS");
592
+ }
593
+ if (amount !== undefined && (!Number.isInteger(amount) || amount <= 0)) {
594
+ throw new ProtocolError(`Invalid offboard amount: ${amount} (must be a positive integer of sats)`, "ARKADE", "INVALID_AMOUNT");
595
+ }
596
+ try {
597
+ const wallet = arkadeClientManager.getWallet();
598
+ const info = await wallet.arkProvider.getInfo();
599
+ const exitTxid = await new Ramps(wallet).offboard(address, info.fees, amount !== undefined ? BigInt(amount) : undefined);
600
+ return { txid: exitTxid };
601
+ }
602
+ catch (error) {
603
+ const msg = error instanceof Error ? error.message : String(error);
604
+ throw new ProtocolError(`Offboard failed: ${msg}`, "ARKADE", "OFFBOARD_ERROR");
605
+ }
606
+ }
607
+ async listTransfers(_options) {
608
+ return { transfers: [] };
609
+ }
610
+ // =========================================================================
611
+ // Asset / On-chain Send
612
+ // =========================================================================
613
+ async sendAsset(params) {
614
+ if (!this.isConnected()) {
615
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
616
+ }
617
+ const request = (params ?? {});
618
+ const assetId = toStringValue(request.assetId);
619
+ const amount = toPositiveIntegerBigInt(request.amount);
620
+ const recipientId = toStringValue(request.recipientId);
621
+ if (!assetId) {
622
+ throw new ProtocolError("Asset ID is required", "ARKADE", "INVALID_ASSET");
623
+ }
624
+ if (!recipientId) {
625
+ throw new ProtocolError("Recipient address is required", "ARKADE", "INVALID_ADDRESS");
626
+ }
627
+ if (amount <= 0n) {
628
+ throw new ProtocolError("Amount must be greater than zero", "ARKADE", "INVALID_AMOUNT");
629
+ }
630
+ try {
631
+ const wallet = arkadeClientManager.getWallet();
632
+ const txid = await wallet.send({
633
+ address: recipientId,
634
+ assets: [{ assetId, amount }],
635
+ });
636
+ return { txid };
637
+ }
638
+ catch (error) {
639
+ const msg = error instanceof Error ? error.message : String(error);
640
+ throw new ProtocolError(`Failed to send Arkade asset: ${msg}`, "ARKADE", "SEND_ASSET_ERROR");
641
+ }
642
+ }
257
643
  async sendBtcOnchain(params) {
258
644
  if (!this.isConnected()) {
259
- throw new ProtocolError('Not connected', 'ARKADE', 'NOT_CONNECTED');
645
+ throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
646
+ }
647
+ try {
648
+ const wallet = arkadeClientManager.getWallet();
649
+ const selectedVtxos = await this.selectSpendableBtcVtxos(wallet, params.amount);
650
+ const txid = await wallet.sendBitcoin({
651
+ address: params.address,
652
+ amount: params.amount,
653
+ ...(selectedVtxos ? { selectedVtxos } : {}),
654
+ });
655
+ return { txid };
260
656
  }
261
- const wallet = arkadeClientManager.getWallet();
262
- const txid = await wallet.sendBitcoin({ address: params.address, amount: params.amount });
263
- return { txid };
657
+ catch (error) {
658
+ const msg = error instanceof Error ? error.message : String(error);
659
+ throw new ProtocolError(`Failed to send BTC: ${msg}`, "ARKADE", "SEND_BTC_ERROR");
660
+ }
661
+ }
662
+ // =========================================================================
663
+ // Swap Operations (Not supported)
664
+ // =========================================================================
665
+ supportsSwaps() {
666
+ return false;
667
+ }
668
+ async getSwapQuote(_request) {
669
+ throw new ProtocolError("Not supported", "ARKADE", "NOT_SUPPORTED");
670
+ }
671
+ async executeSwap(_quote) {
672
+ throw new ProtocolError("Not supported", "ARKADE", "NOT_SUPPORTED");
264
673
  }
265
- // ========================================================================
674
+ async getSwapStatus(_swapId) {
675
+ throw new ProtocolError("Not supported", "ARKADE", "NOT_SUPPORTED");
676
+ }
677
+ // =========================================================================
678
+ // Message Signing
679
+ // =========================================================================
680
+ async signMessage(message) {
681
+ if (!this.config?.mnemonic) {
682
+ throw new ProtocolError("Wallet mnemonic not available", "ARKADE", "NOT_CONNECTED");
683
+ }
684
+ const seed = mnemonicToSeedSync(this.config.mnemonic);
685
+ const node = HDKey.fromMasterSeed(seed).derive("m/138'/1");
686
+ if (!node.privateKey) {
687
+ throw new ProtocolError("Failed to derive message-signing key", "ARKADE", "KEY_DERIVATION_ERROR");
688
+ }
689
+ return signLnMessage(message, node.privateKey);
690
+ }
691
+ async verifyMessage(message, signature) {
692
+ return verifyLnMessage(message, signature);
693
+ }
694
+ // =========================================================================
266
695
  // Private Helpers
267
- // ========================================================================
268
- convertArkTx(tx) {
696
+ // =========================================================================
697
+ /**
698
+ * Pre-select spendable BTC VTXOs for a sendBitcoin call using the
699
+ * expiry-first policy. Returns `undefined` (not an empty array) when no
700
+ * override should be applied — that lets the SDK fall back to its own
701
+ * selection when the fetch fails, the spendable set can't cover the target,
702
+ * or the target is non-positive. Mixed-asset VTXOs are filtered out.
703
+ */
704
+ async selectSpendableBtcVtxos(wallet, targetSats) {
705
+ if (!Number.isFinite(targetSats) || targetSats <= 0)
706
+ return undefined;
269
707
  try {
270
- const isSend = tx.type === 'SENT';
271
- const amountSats = tx.amount ?? 0;
272
- const timestamp = typeof tx.createdAt === 'number' ? tx.createdAt
273
- : tx.createdAt instanceof Date ? tx.createdAt.getTime()
274
- : Date.now();
275
- const txId = tx.key?.arkTxid || tx.key?.commitmentTxid || tx.key?.boardingTxid || tx.txid || `ark-${timestamp}`;
276
- const btcAsset = {
277
- id: 'BTC', name: 'Bitcoin (Ark)', ticker: 'BTC', precision: 8,
278
- protocol: 'ARKADE', layer: 'ARKADE_ARKADE',
279
- balance: {
280
- total: amountSats, available: amountSats, pending: 0,
281
- totalDisplay: this.formatSats(amountSats),
282
- availableDisplay: this.formatSats(amountSats),
283
- },
284
- capabilities: { canSend: true, canReceive: true, canSwap: false, supportsLightning: false, supportsOnchain: true },
285
- };
286
- return {
287
- id: txId,
288
- type: isSend ? 'send' : 'receive',
289
- status: tx.settled || !isSend ? 'confirmed' : 'pending',
290
- timestamp, amount: amountSats,
291
- amountDisplay: this.formatSats(amountSats),
292
- fee: 0, feeDisplay: '0.00000000',
293
- asset: btcAsset,
294
- protocolData: { type: tx.type, settled: tx.settled, key: tx.key },
295
- };
708
+ const raw = await wallet.getVtxos();
709
+ const list = Array.isArray(raw)
710
+ ? raw
711
+ : Array.isArray(raw?.vtxos)
712
+ ? raw.vtxos
713
+ : [];
714
+ const spendableBtc = list.filter((vtxo) => {
715
+ if (!isSpendable(vtxo))
716
+ return false;
717
+ // Exclude VTXOs carrying assets — sendBitcoin would either reject
718
+ // them or accidentally burn the asset side. Pure BTC only.
719
+ const assets = vtxo.assets;
720
+ if (Array.isArray(assets) && assets.length > 0)
721
+ return false;
722
+ return true;
723
+ });
724
+ const selected = selectVtxosByExpiry(spendableBtc, targetSats);
725
+ return selected ?? undefined;
296
726
  }
297
- catch {
298
- return null;
727
+ catch (error) {
728
+ log.warn("[ArkadeAdapter] selectSpendableBtcVtxos failed; falling back to SDK default selection:", error);
729
+ return undefined;
299
730
  }
300
731
  }
301
- toNumber(value) {
302
- if (typeof value === 'number' && Number.isFinite(value))
303
- return value;
304
- if (typeof value === 'bigint')
305
- return Number(value);
306
- if (typeof value === 'string' && value.trim() !== '') {
307
- const parsed = Number(value);
308
- return Number.isFinite(parsed) ? parsed : 0;
732
+ async getWalletBalanceSummary(wallet) {
733
+ const balance = await wallet.getBalance();
734
+ const normalized = {
735
+ boardingConfirmed: toNumber(balance?.boarding?.confirmed),
736
+ boardingUnconfirmed: toNumber(balance?.boarding?.unconfirmed),
737
+ boardingTotal: toNumber(balance?.boarding?.total),
738
+ settled: toNumber(balance?.settled),
739
+ preconfirmed: toNumber(balance?.preconfirmed),
740
+ available: toNumber(balance?.available),
741
+ recoverable: toNumber(balance?.recoverable),
742
+ total: toNumber(balance?.total),
743
+ };
744
+ let normalizedVtxos = [];
745
+ try {
746
+ normalizedVtxos = normalizeVtxos(await wallet.getVtxos());
747
+ }
748
+ catch (error) {
749
+ log.warn("[ArkadeAdapter] Failed to derive balance from VTXOs, falling back to wallet.getBalance()", error);
309
750
  }
310
- return 0;
751
+ if (normalizedVtxos.length === 0) {
752
+ // Mirror the vtxo path: boarding UTXOs must be counted in total even
753
+ // when there are no VTXOs. The SDK's top-level balance.total omits the
754
+ // boarding portion, so we compute it the same way as the vtxo path below.
755
+ const available = normalized.settled + normalized.preconfirmed;
756
+ return {
757
+ ...normalized,
758
+ available,
759
+ total: normalized.boardingTotal + available + normalized.recoverable,
760
+ };
761
+ }
762
+ const vtxoSummary = normalizedVtxos.reduce((summary, vtxo) => {
763
+ if (vtxo.state === "swept") {
764
+ summary.recoverable += vtxo.value;
765
+ }
766
+ else if (vtxo.state === "preconfirmed") {
767
+ summary.preconfirmed += vtxo.value;
768
+ }
769
+ else {
770
+ summary.settled += vtxo.value;
771
+ }
772
+ return summary;
773
+ }, {
774
+ settled: 0,
775
+ preconfirmed: 0,
776
+ recoverable: 0,
777
+ });
778
+ const available = vtxoSummary.settled + vtxoSummary.preconfirmed;
779
+ const total = normalized.boardingTotal + available + vtxoSummary.recoverable;
780
+ return {
781
+ ...normalized,
782
+ settled: vtxoSummary.settled,
783
+ preconfirmed: vtxoSummary.preconfirmed,
784
+ available,
785
+ recoverable: vtxoSummary.recoverable,
786
+ total,
787
+ };
311
788
  }
312
- formatSats(sats) {
313
- return (sats / 1e8).toFixed(8);
789
+ async getCachedAssetDetails(wallet, assetId) {
790
+ if (this.assetDetailsCache.has(assetId)) {
791
+ return this.assetDetailsCache.get(assetId) ?? null;
792
+ }
793
+ try {
794
+ const details = await wallet.assetManager.getAssetDetails(assetId);
795
+ const normalized = details && typeof details === "object" ? details : null;
796
+ this.assetDetailsCache.set(assetId, normalized);
797
+ return normalized;
798
+ }
799
+ catch (error) {
800
+ log.warn("[ArkadeAdapter] Failed to fetch asset details for", assetId, error);
801
+ this.assetDetailsCache.set(assetId, null);
802
+ return null;
803
+ }
314
804
  }
315
805
  }
316
806
  //# sourceMappingURL=ArkadeAdapter.js.map