@pioneer-platform/pioneer-sdk 0.0.82 → 4.13.30

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/src/index.ts ADDED
@@ -0,0 +1,2250 @@
1
+ import { KeepKeySdk } from '@keepkey/keepkey-sdk';
2
+ import { caipToNetworkId, networkIdToCaip } from '@pioneer-platform/pioneer-caip';
3
+ import { Pioneer } from '@pioneer-platform/pioneer-client';
4
+ import { addressNListToBIP32, getPaths } from '@pioneer-platform/pioneer-coins';
5
+ import { assetData } from '@pioneer-platform/pioneer-discovery';
6
+ import { Events } from '@pioneer-platform/pioneer-events';
7
+ import { EventEmitter } from 'events';
8
+
9
+ import { getCharts } from './charts/index.js';
10
+ //internal
11
+ import { getPubkey } from './getPubkey.js';
12
+ import { optimizedGetPubkeys } from './kkapi-batch-client.js';
13
+ import { OfflineClient } from './offline-client.js';
14
+ import { TransactionManager } from './TransactionManager.js';
15
+ import { createUnsignedTendermintTx } from './txbuilder/createUnsignedTendermintTx.js';
16
+ import { createUnsignedStakingTx, type StakingTxParams } from './txbuilder/createUnsignedStakingTx.js';
17
+ import { getFees, estimateTransactionFee, type NormalizedFeeRates, type FeeEstimate } from './fees/index.js';
18
+ import { detectKkApiAvailability } from './utils/kkapi-detection.js';
19
+ import { formatTime } from './utils/format-time.js';
20
+ import { buildDashboardFromBalances } from './utils/build-dashboard.js';
21
+ import { PubkeyManager, getPubkeyKey, deduplicatePubkeys, validatePubkey } from './utils/pubkey-helpers.js';
22
+
23
+ const TAG = ' | Pioneer-sdk | ';
24
+
25
+
26
+ export interface PioneerSDKConfig {
27
+ appName: string;
28
+ appIcon: string;
29
+ blockchains: any;
30
+ nodes?: any;
31
+ username: string;
32
+ queryKey: string;
33
+ spec: string;
34
+ wss: string;
35
+ paths: any;
36
+ pubkeys?: any;
37
+ balances?: any;
38
+ keepkeyApiKey?: string;
39
+ ethplorerApiKey?: string;
40
+ covalentApiKey?: string;
41
+ utxoApiKey?: string;
42
+ walletConnectProjectId?: string;
43
+ offlineFirst?: boolean;
44
+ vaultUrl?: string;
45
+ forceLocalhost?: boolean;
46
+ }
47
+
48
+
49
+ export class SDK {
50
+ public status: string;
51
+ public username: string;
52
+ public queryKey: string;
53
+ public wss: string;
54
+ public spec: any;
55
+ public ethplorerApiKey: string | undefined;
56
+ public covalentApiKey: string | undefined;
57
+ public utxoApiKey: string | undefined;
58
+ public walletConnectProjectId: string | undefined;
59
+ public contextType: string;
60
+ public context: string;
61
+ public assetContext: any;
62
+ public blockchainContext: any;
63
+ public pubkeyContext: any;
64
+ public outboundAssetContext: any;
65
+ public outboundBlockchainContext: any;
66
+ public outboundPubkeyContext: any;
67
+ public buildDashboardFromBalances: any;
68
+ // public swapKit: any | null;
69
+ public pioneer: any;
70
+ public charts: any[];
71
+ public paths: any[];
72
+ public pubkeys: {
73
+ networks: string[];
74
+ pubkey: string;
75
+ pathMaster: string;
76
+ address?: string;
77
+ master?: string;
78
+ }[] = [];
79
+ private pubkeyManager: PubkeyManager = new PubkeyManager();
80
+ public wallets: any[];
81
+ public balances: any[];
82
+ public nodes: any[];
83
+ public assets: any[];
84
+ public assetsMap: any;
85
+ public dashboard: any;
86
+ public nfts: any[];
87
+ public events: any;
88
+ public pairWallet: (options: any) => Promise<any>;
89
+ public setContext: (context: string) => Promise<{ success: boolean }>;
90
+ public setContextType: (contextType: string) => Promise<{ success: boolean }>;
91
+ public refresh: () => Promise<any>;
92
+ public setAssetContext: (asset?: any) => Promise<any>;
93
+ public setOutboundAssetContext: (asset?: any) => Promise<any>;
94
+ public keepkeyApiKey: string | undefined;
95
+ public isPioneer: string | null;
96
+ public keepkeyEndpoint: { isAvailable: boolean; baseUrl: string; basePath: string } | null;
97
+ public forceLocalhost: boolean;
98
+ // public loadPubkeyCache: (pubkeys: any) => Promise<void>;
99
+ public getPubkeys: (wallets?: string[]) => Promise<any[]>;
100
+ public getBalances: (filter?: any) => Promise<any[]>;
101
+ public blockchains: any[];
102
+ public clearWalletState: () => Promise<boolean>;
103
+ public setBlockchains: (blockchains: any) => Promise<void>;
104
+ public appName: string;
105
+ public appIcon: any;
106
+ public init: (walletsVerbose: any, setup: any) => Promise<any>;
107
+ // public initOffline: () => Promise<any>;
108
+ // public backgroundSync: () => Promise<void>;
109
+ public getUnifiedPortfolio: () => Promise<any>;
110
+ public offlineClient: OfflineClient | null;
111
+ // public verifyWallet: () => Promise<void>;
112
+ public convertVaultPubkeysToPioneerFormat: (vaultPubkeys: any[]) => any[];
113
+ // public deriveNetworksFromPath: (path: string) => string[];
114
+ // public getAddress: (options: {
115
+ // networkId?: string;
116
+ // showDevice?: boolean;
117
+ // path?: any;
118
+ // }) => Promise<string>;
119
+ public app: {
120
+ getAddress: (options: {
121
+ networkId?: string;
122
+ showDevice?: boolean;
123
+ path?: any;
124
+ }) => Promise<string>;
125
+ };
126
+ public addAsset: (caip: string, data?: any) => Promise<any>;
127
+ public getAssets: (filter?: string) => Promise<any>;
128
+ public getBalance: (networkId: string) => Promise<any>;
129
+ public getFees: (networkId: string) => Promise<NormalizedFeeRates>;
130
+ public estimateTransactionFee: (feeRate: string, unit: string, networkType: string, txSize?: number) => FeeEstimate;
131
+ public getCharts: () => Promise<any>;
132
+ public keepKeySdk: any;
133
+ private getGasAssets: () => Promise<any>;
134
+ private transactions: any;
135
+ private transfer: (sendPayload: any) => Promise<any>;
136
+ private sync: () => Promise<boolean>;
137
+ private swap: (swapPayload: any, waitOnConfirm?: boolean) => Promise<any>;
138
+ public followTransaction: (
139
+ caip: string,
140
+ txid: string,
141
+ ) => Promise<{
142
+ detectedTime: string | null;
143
+ requiredConfirmations: any;
144
+ timeFromDetectionToConfirm: string | null;
145
+ txid: string;
146
+ confirmTime: string | null;
147
+ caip: string;
148
+ broadcastTime: string;
149
+ timeToDetect: string | null;
150
+ timeToConfirm: string | null;
151
+ }>;
152
+ public broadcastTx: (caip: string, signedTx: any) => Promise<any>;
153
+ public signTx: (unsignedTx: any) => Promise<any>;
154
+ public buildTx: (sendPayload: any) => Promise<any>;
155
+ public buildDelegateTx: (caip: string, params: StakingTxParams) => Promise<any>;
156
+ public buildUndelegateTx: (caip: string, params: StakingTxParams) => Promise<any>;
157
+ public buildClaimRewardsTx: (caip: string, params: StakingTxParams) => Promise<any>;
158
+ public buildClaimAllRewardsTx: (caip: string, params: StakingTxParams) => Promise<any>;
159
+ public estimateMax: (sendPayload: any) => Promise<void>;
160
+ public syncMarket: () => Promise<boolean>;
161
+ public getBalancesForNetworks: (networkIds: string[]) => Promise<any[]>;
162
+ // private search: (query: string, config: any) => Promise<void>;
163
+ // public networkPercentages: { networkId: string; percentage: string | number }[] = [];
164
+ // public assetQuery: { caip: string; pubkey: string }[] = [];
165
+ public setPubkeyContext: (pubkey?: any) => Promise<boolean>;
166
+ private getPubkeyKey: (pubkey: any) => string;
167
+ private deduplicatePubkeys: (pubkeys: any[]) => any[];
168
+ private addPubkey: (pubkey: any) => boolean;
169
+ private setPubkeys: (newPubkeys: any[]) => void;
170
+ constructor(spec: string, config: PioneerSDKConfig) {
171
+ this.status = 'preInit';
172
+ this.appName = config.appName || 'unknown app';
173
+ this.appIcon = config.appIcon || 'https://pioneers.dev/coins/keepkey.png';
174
+ this.spec = spec || config.spec || 'https://pioneers.dev/spec/swagger';
175
+ this.wss = config.wss || 'wss://pioneers.dev';
176
+ this.assets = assetData;
177
+ this.assetsMap = new Map();
178
+ this.username = config.username;
179
+ this.queryKey = config.queryKey;
180
+ this.keepkeyApiKey = config.keepkeyApiKey;
181
+ this.keepkeyEndpoint = null;
182
+ this.forceLocalhost = config.forceLocalhost || false;
183
+ this.paths = config.paths || [];
184
+
185
+ // Deduplicate blockchains to prevent duplicate dashboard calculations
186
+ this.blockchains = config.blockchains ? [...new Set(config.blockchains)] : [];
187
+ if (config.blockchains && config.blockchains.length !== this.blockchains.length) {
188
+ }
189
+
190
+ // Initialize pubkeys with deduplication if provided in config
191
+ if (config.pubkeys && config.pubkeys.length > 0) {
192
+ this.pubkeyManager.setPubkeys(config.pubkeys);
193
+ this.pubkeys = this.pubkeyManager.getPubkeys();
194
+ } else {
195
+ this.pubkeys = [];
196
+ this.pubkeyManager.clear();
197
+ }
198
+
199
+ this.balances = config.balances || [];
200
+ this.nodes = config.nodes || [];
201
+ this.charts = ['covalent', 'zapper'];
202
+ this.nfts = [];
203
+ this.isPioneer = null;
204
+ this.pioneer = null;
205
+ this.context = '';
206
+ this.pubkeyContext = null;
207
+ this.assetContext = null;
208
+ this.blockchainContext = null;
209
+ this.outboundAssetContext = null;
210
+ this.outboundBlockchainContext = null;
211
+ this.outboundPubkeyContext = null;
212
+ this.wallets = [];
213
+ this.events = new EventEmitter();
214
+ this.transactions = null;
215
+ this.ethplorerApiKey = config.ethplorerApiKey;
216
+ this.covalentApiKey = config.covalentApiKey;
217
+ this.utxoApiKey = config.utxoApiKey;
218
+ this.walletConnectProjectId = config.walletConnectProjectId;
219
+ this.contextType = '';
220
+
221
+ // Initialize offline client if offline-first mode is enabled
222
+ this.offlineClient = config.offlineFirst
223
+ ? new OfflineClient({
224
+ vaultUrl: config.vaultUrl || 'kkapi://',
225
+ timeout: 1000, // 1 second timeout for fast checks
226
+ fallbackToRemote: true,
227
+ })
228
+ : null;
229
+
230
+ this.pairWallet = async (options: any) => {
231
+ // Implementation will be added later
232
+ return Promise.resolve({});
233
+ };
234
+
235
+ // Helper method to generate unique key for a pubkey
236
+ this.getPubkeyKey = getPubkeyKey;
237
+
238
+ // Helper method to deduplicate pubkeys array
239
+ this.deduplicatePubkeys = deduplicatePubkeys;
240
+
241
+ // Helper method to validate and add a single pubkey
242
+ this.addPubkey = (pubkey: any): boolean => {
243
+ const result = this.pubkeyManager.addPubkey(pubkey);
244
+ if (result) {
245
+ this.pubkeys = this.pubkeyManager.getPubkeys();
246
+ }
247
+ return result;
248
+ };
249
+
250
+ // Helper method to set pubkeys array with deduplication
251
+ this.setPubkeys = (newPubkeys: any[]): void => {
252
+ this.pubkeyManager.setPubkeys(newPubkeys);
253
+ this.pubkeys = this.pubkeyManager.getPubkeys();
254
+ };
255
+
256
+ // Fast portfolio loading from kkapi:// cache
257
+ this.getUnifiedPortfolio = async function () {
258
+ const tag = `${TAG} | getUnifiedPortfolio | `;
259
+ try {
260
+ const startTime = performance.now();
261
+
262
+ // Check if kkapi is available and use the detected endpoint
263
+ try {
264
+ // Use the detected endpoint instead of hardcoded kkapi://
265
+ const baseUrl = this.keepkeyEndpoint?.baseUrl || 'kkapi://';
266
+ const portfolioUrl = `${baseUrl}/api/portfolio`;
267
+
268
+ const portfolioResponse = await fetch(portfolioUrl, {
269
+ method: 'GET',
270
+ signal: AbortSignal.timeout(2000), // 2 second timeout
271
+ });
272
+
273
+ if (!portfolioResponse.ok) {
274
+ console.warn(tag, 'Portfolio endpoint returned', portfolioResponse.status);
275
+ return null;
276
+ }
277
+
278
+ const portfolioData = await portfolioResponse.json();
279
+ const loadTime = performance.now() - startTime;
280
+
281
+ if (!portfolioData.success) {
282
+ console.warn(tag, 'Portfolio API returned success=false');
283
+ return null;
284
+ }
285
+
286
+ if (portfolioData.totalValueUsd === 0 || !portfolioData.totalValueUsd) {
287
+ console.warn(tag, 'Portfolio value is $0.00 - may need device connection or sync');
288
+ return null;
289
+ }
290
+
291
+ // Get device-specific balances if we have devices
292
+ let allBalances = [];
293
+ if (portfolioData.balances) allBalances = portfolioData.balances;
294
+
295
+ // Update SDK state if we have balances
296
+ if (allBalances.length > 0) {
297
+ this.balances = allBalances;
298
+ this.events.emit('SET_BALANCES', this.balances);
299
+ }
300
+
301
+ // Update pubkeys from cache
302
+ if (portfolioData.pubkeys && portfolioData.pubkeys.length > 0) {
303
+ // Convert vault pubkey format to pioneer-sdk format
304
+ // TODO: Implement convertVaultPubkeysToPioneerFormat method
305
+ const convertedPubkeys = portfolioData.pubkeys;
306
+ // Use setPubkeys to ensure deduplication
307
+ this.pubkeyManager.setPubkeys(convertedPubkeys);
308
+ this.pubkeys = this.pubkeyManager.getPubkeys();
309
+ this.events.emit('SET_PUBKEYS', this.pubkeys);
310
+ }
311
+
312
+ // Update wallets from devices
313
+ if (portfolioData.devices && portfolioData.devices.length > 0) {
314
+ this.wallets = portfolioData.devices.map((device: any) => ({
315
+ type: 'keepkey',
316
+ deviceId: device.deviceId,
317
+ label: device.label || `KeepKey ${device.shortId}`,
318
+ shortId: device.shortId,
319
+ totalValueUsd: device.totalValueUsd || 0,
320
+ }));
321
+ this.events.emit('SET_WALLETS', this.wallets);
322
+ }
323
+
324
+ // Validate cache data before using it
325
+ const isCacheDataValid = (portfolioData: any): boolean => {
326
+ // Check if networks data is reasonable (should be < 50 networks, not thousands)
327
+ if (!portfolioData.networks || !Array.isArray(portfolioData.networks)) {
328
+ console.warn('[CACHE VALIDATION] Networks is not an array');
329
+ return false;
330
+ }
331
+
332
+ if (portfolioData.networks.length > 50) {
333
+ console.error(
334
+ `[CACHE VALIDATION] CORRUPTED: ${portfolioData.networks.length} networks (should be < 50)`,
335
+ );
336
+ return false;
337
+ }
338
+
339
+ // Check if at least some networks have required fields
340
+ const validNetworks = portfolioData.networks.filter(
341
+ (n: any) => n.networkId && n.totalValueUsd !== undefined && n.gasAssetSymbol,
342
+ );
343
+
344
+ if (validNetworks.length === 0 && portfolioData.networks.length > 0) {
345
+ console.error('[CACHE VALIDATION] CORRUPTED: No networks have required fields');
346
+ return false;
347
+ }
348
+
349
+ console.log(
350
+ `[CACHE VALIDATION] Found ${portfolioData.networks.length} networks, ${validNetworks.length} valid`,
351
+ );
352
+ return true;
353
+ };
354
+
355
+ // Only use cache data if it's valid
356
+ if (isCacheDataValid(portfolioData)) {
357
+ const dashboardData = {
358
+ totalValueUsd: portfolioData.totalValueUsd,
359
+ pairedDevices: portfolioData.pairedDevices,
360
+ devices: portfolioData.devices || [],
361
+ networks: portfolioData.networks || [],
362
+ assets: portfolioData.assets || [],
363
+ statistics: portfolioData.statistics || {},
364
+ cached: portfolioData.cached,
365
+ lastUpdated: portfolioData.lastUpdated,
366
+ cacheAge: portfolioData.lastUpdated
367
+ ? Math.floor((Date.now() - portfolioData.lastUpdated) / 1000)
368
+ : 0,
369
+ networkPercentages:
370
+ portfolioData.networks?.map((network: any) => ({
371
+ networkId: network.network_id || network.networkId,
372
+ percentage: network.percentage || 0,
373
+ })) || [],
374
+ };
375
+
376
+ this.dashboard = dashboardData;
377
+ this.events.emit('SET_DASHBOARD', this.dashboard);
378
+ } else {
379
+ console.warn(
380
+ '[CACHE VALIDATION] ❌ Cache data corrupted, building dashboard from cached balances',
381
+ );
382
+ // Build dashboard from cached balances without hitting Pioneer APIs
383
+ const dashboardData = buildDashboardFromBalances(this.balances, this.blockchains, this.assetsMap);
384
+ this.dashboard = dashboardData;
385
+ this.events.emit('SET_DASHBOARD', this.dashboard);
386
+ }
387
+
388
+ return {
389
+ balances: allBalances,
390
+ dashboard: this.dashboard, // Use the dashboard that was set (or undefined if cache was invalid)
391
+ cached: portfolioData.cached,
392
+ loadTimeMs: loadTime,
393
+ totalValueUsd: portfolioData.totalValueUsd,
394
+ };
395
+ } catch (fetchError: any) {
396
+ if (fetchError.name === 'AbortError') {
397
+ console.log(
398
+ tag,
399
+ 'Unified portfolio request timed out (this is normal if vault not running)',
400
+ );
401
+ } else {
402
+ console.log(tag, 'Failed to fetch unified portfolio:', fetchError.message);
403
+ }
404
+ return null;
405
+ }
406
+ } catch (e) {
407
+ console.error(tag, 'Error:', e);
408
+ return null;
409
+ }
410
+ };
411
+
412
+ this.init = async function (walletsVerbose: any, setup: any) {
413
+ const tag = `${TAG} | init | `;
414
+ try {
415
+ if (!this.username) throw Error('username required!');
416
+ if (!this.queryKey) throw Error('queryKey required!');
417
+ if (!this.wss) throw Error('wss required!');
418
+ if (!this.wallets) throw Error('wallets required!');
419
+ if (!this.paths) throw Error('wallets required!');
420
+ const initStartTime = performance.now();
421
+
422
+ // Option to skip sync (for apps that will manually call getPubkeys/getBalances)
423
+ const skipSync = setup?.skipSync || false;
424
+
425
+ // Initialize Pioneer Client
426
+
427
+ // CRITICAL FIX: Ensure Pioneer client has proper HTTP headers for browser requests
428
+ const pioneerConfig = {
429
+ ...config,
430
+ };
431
+
432
+ const PioneerClient = new Pioneer(this.spec, pioneerConfig);
433
+ this.pioneer = await PioneerClient.init();
434
+ if (!this.pioneer) throw Error('Failed to init pioneer server!');
435
+
436
+ // Add paths for blockchains
437
+ this.paths.concat(getPaths(this.blockchains));
438
+
439
+ // Get gas assets (needed for asset map)
440
+ await this.getGasAssets();
441
+
442
+ // Detect KeepKey endpoint
443
+ this.keepkeyEndpoint = await detectKkApiAvailability(this.forceLocalhost);
444
+ const keepkeyEndpoint = this.keepkeyEndpoint;
445
+
446
+ // Initialize KeepKey SDK if available
447
+ try {
448
+ const configKeepKey = {
449
+ apiKey: this.keepkeyApiKey || 'keepkey-api-key',
450
+ pairingInfo: {
451
+ name: 'KeepKey SDK Demo App',
452
+ imageUrl: 'https://pioneers.dev/coins/keepkey.png',
453
+ basePath: keepkeyEndpoint.basePath,
454
+ url: keepkeyEndpoint.baseUrl,
455
+ },
456
+ };
457
+
458
+ console.log('🔑 [INIT] Initializing KeepKey SDK...');
459
+ const keepKeySdk = await KeepKeySdk.create(configKeepKey);
460
+ const features = await keepKeySdk.system.info.getFeatures();
461
+
462
+ this.keepkeyApiKey = configKeepKey.apiKey;
463
+ this.keepKeySdk = keepKeySdk;
464
+ this.context = 'keepkey:' + features.label + '.json';
465
+ } catch (e) {
466
+ console.error('⚠️ [INIT] KeepKey SDK initialization failed:', e);
467
+ }
468
+
469
+ // Initialize WebSocket events
470
+ let configWss = {
471
+ username: this.username,
472
+ queryKey: this.queryKey,
473
+ wss: this.wss,
474
+ };
475
+
476
+ let clientEvents = new Events(configWss);
477
+ await clientEvents.init();
478
+ await clientEvents.setUsername(this.username);
479
+
480
+ clientEvents.events.on('message', (request) => {
481
+ this.events.emit('message', request);
482
+ });
483
+
484
+ this.events.emit('SET_STATUS', 'init');
485
+
486
+ // Fast Portfolio Pattern: Try unified portfolio first, then sync if needed
487
+ if (this.keepKeySdk && !skipSync) {
488
+ console.log('⚡ [FAST PORTFOLIO] Attempting fast load...');
489
+ const fastStart = performance.now();
490
+
491
+ try {
492
+ const unifiedResult = await this.getUnifiedPortfolio();
493
+ console.log('unifiedResult: ', unifiedResult);
494
+
495
+ if (unifiedResult && unifiedResult.cached && unifiedResult.totalValueUsd > 0) {
496
+ console.log(
497
+ `✅ [FAST PORTFOLIO] Loaded in ${(performance.now() - fastStart).toFixed(0)}ms`,
498
+ );
499
+ console.log(
500
+ `💰 [PORTFOLIO] $${unifiedResult.totalValueUsd.toFixed(2)} USD (${
501
+ unifiedResult.balances.length
502
+ } assets)`,
503
+ );
504
+
505
+ // Skip background sync when cache is valid - we already have the data!
506
+ console.log('✅ [FAST PORTFOLIO] Cache valid - skipping sync');
507
+ this.events.emit('SYNC_COMPLETE');
508
+ } else {
509
+ console.log('⚠️ [FAST PORTFOLIO] Unavailable, using full sync...');
510
+ throw Error('Failing fast TEST');
511
+ const syncStart = performance.now();
512
+ // await this.sync();
513
+ // console.log(
514
+ // '✅ [SYNC] Full sync completed in',
515
+ // (performance.now() - syncStart).toFixed(0),
516
+ // 'ms',
517
+ // );
518
+ }
519
+ } catch (fastError) {
520
+ console.warn('⚠️ [FAST PORTFOLIO] Failed, using full sync');
521
+ const syncStart = performance.now();
522
+ await this.sync();
523
+ console.log(
524
+ '✅ [SYNC] Full sync completed in',
525
+ (performance.now() - syncStart).toFixed(0),
526
+ 'ms',
527
+ );
528
+ }
529
+ } else if (skipSync) {
530
+ console.log('⏭️ [INIT] Skipping sync (skipSync=true)');
531
+ }
532
+
533
+ return this.pioneer;
534
+ } catch (e) {
535
+ console.error(tag, 'e: ', e);
536
+ throw e;
537
+ }
538
+ };
539
+ // Build dashboard from cached balances (no Pioneer API calls)
540
+ this.buildDashboardFromBalances = () => {
541
+ return buildDashboardFromBalances(this.balances, this.blockchains, this.assetsMap);
542
+ };
543
+
544
+ this.syncMarket = async function () {
545
+ const tag = `${TAG} | syncMarket | `;
546
+ try {
547
+ // Log balances with invalid CAIPs for debugging
548
+ const invalidBalances = this.balances.filter(b =>
549
+ !b || !b.caip || typeof b.caip !== 'string' || !b.caip.includes(':')
550
+ );
551
+ if (invalidBalances.length > 0) {
552
+ console.warn(tag, `Found ${invalidBalances.length} balances with invalid CAIPs:`,
553
+ invalidBalances.map(b => ({
554
+ caip: b?.caip,
555
+ type: typeof b?.caip,
556
+ symbol: b?.symbol,
557
+ balance: b?.balance
558
+ }))
559
+ );
560
+ }
561
+
562
+ // Extract all CAIP identifiers from balances, filtering out invalid entries
563
+ let allCaips = this.balances
564
+ .filter(b => b && b.caip && typeof b.caip === 'string' && b.caip.trim().length > 0)
565
+ .map((b) => b.caip);
566
+
567
+ // Remove duplicates
568
+ allCaips = [...new Set(allCaips)];
569
+
570
+ // CRITICAL: Double-check all elements are valid strings after Set deduplication
571
+ // Filter out any non-string or empty values that might have slipped through
572
+ allCaips = allCaips.filter(caip =>
573
+ caip &&
574
+ typeof caip === 'string' &&
575
+ caip.trim().length > 0 &&
576
+ caip.includes(':') // CAIP format always has a colon
577
+ );
578
+
579
+ // Fetch market prices for all CAIPs
580
+ console.log('GetMarketInfo: payload: ', allCaips);
581
+ console.log('GetMarketInfo: payload type: ', typeof allCaips);
582
+ console.log('GetMarketInfo: payload length: ', allCaips.length);
583
+
584
+ // Additional validation log to catch issues
585
+ const invalidEntries = allCaips.filter(caip => typeof caip !== 'string');
586
+ if (invalidEntries.length > 0) {
587
+ console.error(tag, 'CRITICAL: Invalid entries detected in allCaips:', invalidEntries);
588
+ throw new Error('Invalid CAIP entries detected - aborting market sync');
589
+ }
590
+
591
+ if (allCaips && allCaips.length > 0) {
592
+ try {
593
+ let allPrices = await this.pioneer.GetMarketInfo(allCaips);
594
+ console.log('GetMarketInfo: response: ', allPrices);
595
+
596
+ // Create a map of CAIP to price for easier lookup
597
+ const priceMap = {};
598
+ if (allPrices && allPrices.data) {
599
+ for (let i = 0; i < allCaips.length && i < allPrices.data.length; i++) {
600
+ priceMap[allCaips[i]] = allPrices.data[i];
601
+ }
602
+ }
603
+
604
+ // Update each balance with the corresponding price and value
605
+ for (let balance of this.balances) {
606
+ if (balance && balance.caip && priceMap[balance.caip] !== undefined) {
607
+ balance.price = priceMap[balance.caip];
608
+ balance.priceUsd = priceMap[balance.caip]; // Also set priceUsd for compatibility
609
+ balance.valueUsd = balance.price * (balance.balance || 0);
610
+ }
611
+ }
612
+ } catch (apiError) {
613
+ console.error(tag, 'API error fetching market info:', apiError);
614
+ // Don't throw - just log and continue without prices
615
+ console.warn(tag, 'Continuing without market prices');
616
+ }
617
+ }
618
+ return true;
619
+ } catch (e) {
620
+ console.error(tag, 'e:', e);
621
+ throw e;
622
+ }
623
+ };
624
+ this.sync = async function () {
625
+ const tag = `${TAG} | sync | `;
626
+ try {
627
+ // Helper to check network match with EVM wildcard support (works for both paths and pubkeys)
628
+ const matchesNetwork = (item: any, networkId: string) => {
629
+ if (!item.networks || !Array.isArray(item.networks)) return false;
630
+ if (item.networks.includes(networkId)) return true;
631
+ if (networkId.startsWith('eip155:') && item.networks.includes('eip155:*')) return true;
632
+ return false;
633
+ };
634
+
635
+ //at least 1 path per chain
636
+ await this.getPubkeys();
637
+ for (let i = 0; i < this.blockchains.length; i++) {
638
+ let networkId = this.blockchains[i];
639
+ if (networkId.indexOf('eip155:') >= 0) networkId = 'eip155:*';
640
+
641
+ let paths = this.paths.filter((path) => matchesNetwork(path, networkId));
642
+ if (paths.length === 0) {
643
+ //get paths for chain
644
+ let paths = getPaths([networkId]);
645
+ if (!paths || paths.length === 0) throw Error('Unable to find paths for: ' + networkId);
646
+ //add to paths
647
+ this.paths = this.paths.concat(paths);
648
+ }
649
+ }
650
+
651
+ for (let i = 0; i < this.blockchains.length; i++) {
652
+ let networkId = this.blockchains[i];
653
+ if (networkId.indexOf('eip155:') >= 0) networkId = 'eip155:*';
654
+ const pathsForChain = this.paths.filter((path) => matchesNetwork(path, networkId));
655
+ if (!pathsForChain || pathsForChain.length === 0)
656
+ throw Error('No paths found for blockchain: ' + networkId);
657
+
658
+ for (let j = 0; j < pathsForChain.length; j++) {
659
+ const path = pathsForChain[j];
660
+ let pathBip32 = addressNListToBIP32(path.addressNListMaster);
661
+ let pubkey = this.pubkeys.find((pubkey) => pubkey.pathMaster === pathBip32);
662
+ if (!pubkey) {
663
+ const pubkey = await getPubkey(
664
+ this.blockchains[i],
665
+ path,
666
+ this.keepKeySdk,
667
+ this.context,
668
+ );
669
+ if (!pubkey) throw Error('Unable to get pubkey for network+ ' + networkId);
670
+ // Use addPubkey method for proper duplicate checking
671
+ this.addPubkey(pubkey);
672
+ }
673
+ }
674
+ }
675
+ await this.getBalances();
676
+
677
+ //we should be fully synced so lets make the dashboard
678
+ const dashboardData: {
679
+ networks: {
680
+ networkId: string;
681
+ totalValueUsd: number;
682
+ gasAssetCaip: string | null;
683
+ gasAssetSymbol: string | null;
684
+ icon: string | null;
685
+ color: string | null;
686
+ totalNativeBalance: string;
687
+ }[];
688
+ totalValueUsd: number;
689
+ networkPercentages: { networkId: string; percentage: number }[];
690
+ } = {
691
+ networks: [],
692
+ totalValueUsd: 0,
693
+ networkPercentages: [],
694
+ };
695
+
696
+ let totalPortfolioValue = 0;
697
+ const networksTemp: {
698
+ networkId: string;
699
+ totalValueUsd: number;
700
+ gasAssetCaip: string | null;
701
+ gasAssetSymbol: string | null;
702
+ icon: string | null;
703
+ color: string | null;
704
+ totalNativeBalance: string;
705
+ }[] = [];
706
+
707
+ // Deduplicate blockchains before calculation to prevent double-counting
708
+ const uniqueBlockchains = [...new Set(this.blockchains)];
709
+ console.log(tag, 'uniqueBlockchains: ', uniqueBlockchains);
710
+
711
+ // Calculate totals for each blockchain
712
+ for (const blockchain of uniqueBlockchains) {
713
+ const filteredBalances = this.balances.filter((b) => {
714
+ const networkId = caipToNetworkId(b.caip);
715
+ return (
716
+ networkId === blockchain ||
717
+ (blockchain === 'eip155:*' && networkId.startsWith('eip155:'))
718
+ );
719
+ });
720
+
721
+ console.log(tag, `Filtering for blockchain: ${blockchain}`);
722
+ console.log(tag, `Found ${filteredBalances.length} balances before deduplication`);
723
+
724
+ // Log each balance to see what's different
725
+ filteredBalances.forEach((balance, idx) => {
726
+ console.log(tag, `Balance[${idx}]:`, {
727
+ caip: balance.caip,
728
+ pubkey: balance.pubkey,
729
+ balance: balance.balance,
730
+ valueUsd: balance.valueUsd,
731
+ });
732
+ });
733
+
734
+ // Deduplicate balances based on caip + pubkey combination
735
+ const balanceMap = new Map();
736
+
737
+ // Special handling for Bitcoin to work around API bug
738
+ const isBitcoin = blockchain.includes('bip122:000000000019d6689c085ae165831e93');
739
+ if (isBitcoin) {
740
+ console.log(tag, 'Bitcoin network detected - checking for duplicate balances');
741
+ // Group Bitcoin balances by value to detect duplicates
742
+ const bitcoinByValue = new Map();
743
+ filteredBalances.forEach((balance) => {
744
+ const valueKey = `${balance.balance}_${balance.valueUsd}`;
745
+ if (!bitcoinByValue.has(valueKey)) {
746
+ bitcoinByValue.set(valueKey, []);
747
+ }
748
+ bitcoinByValue.get(valueKey).push(balance);
749
+ });
750
+
751
+ // Check if all three address types have the same non-zero balance (API bug)
752
+ for (const [valueKey, balances] of bitcoinByValue.entries()) {
753
+ if (balances.length === 3 && parseFloat(balances[0].valueUsd || '0') > 0) {
754
+ console.log(
755
+ tag,
756
+ 'BITCOIN API BUG DETECTED: All 3 address types have same balance, keeping only xpub',
757
+ );
758
+ // Keep only the xpub (or first one if no xpub)
759
+ const xpubBalance =
760
+ balances.find((b) => b.pubkey?.startsWith('xpub')) || balances[0];
761
+ const key = `${xpubBalance.caip}_${xpubBalance.pubkey || 'default'}`;
762
+ balanceMap.set(key, xpubBalance);
763
+ } else {
764
+ // Add all balances normally
765
+ balances.forEach((balance) => {
766
+ const key = `${balance.caip}_${balance.pubkey || 'default'}`;
767
+ balanceMap.set(key, balance);
768
+ });
769
+ }
770
+ }
771
+ } else {
772
+ // Standard deduplication for non-Bitcoin networks
773
+ filteredBalances.forEach((balance) => {
774
+ const key = `${balance.caip}_${balance.pubkey || 'default'}`;
775
+ // Only keep the first occurrence or the one with higher value
776
+ if (
777
+ !balanceMap.has(key) ||
778
+ parseFloat(balance.valueUsd || '0') >
779
+ parseFloat(balanceMap.get(key).valueUsd || '0')
780
+ ) {
781
+ balanceMap.set(key, balance);
782
+ }
783
+ });
784
+ }
785
+
786
+ const networkBalances = Array.from(balanceMap.values());
787
+
788
+ console.log(tag, 'networkBalances (deduplicated): ', networkBalances);
789
+ console.log(tag, 'networkBalances count: ', networkBalances.length);
790
+
791
+ // Ensure we're working with numbers for calculations
792
+ const networkTotal = networkBalances.reduce((sum, balance, idx) => {
793
+ const valueUsd =
794
+ typeof balance.valueUsd === 'string'
795
+ ? parseFloat(balance.valueUsd)
796
+ : balance.valueUsd || 0;
797
+
798
+ console.log(
799
+ tag,
800
+ `[${idx}] valueUsd:`,
801
+ balance.valueUsd,
802
+ '→ parsed:',
803
+ valueUsd,
804
+ '| running sum:',
805
+ sum + valueUsd,
806
+ );
807
+
808
+ if (blockchain.includes('bip122:000000000019d6689c085ae165831e93')) {
809
+ console.log(
810
+ tag,
811
+ `[BITCOIN DEBUG ${idx}] pubkey:`,
812
+ balance.pubkey?.substring(0, 10) + '...',
813
+ '| balance:',
814
+ balance.balance,
815
+ '| valueUsd:',
816
+ balance.valueUsd,
817
+ '→ parsed:',
818
+ valueUsd,
819
+ );
820
+ }
821
+
822
+ return sum + valueUsd;
823
+ }, 0);
824
+
825
+ console.log('Final networkTotal:', networkTotal);
826
+
827
+ // Get native asset for this blockchain
828
+ const nativeAssetCaip = networkIdToCaip(blockchain);
829
+ const gasAsset = networkBalances.find((b) => b.caip === nativeAssetCaip);
830
+
831
+ // Calculate total native balance (sum of all balances for the native asset)
832
+ const totalNativeBalance = networkBalances
833
+ .filter((b) => b.caip === nativeAssetCaip)
834
+ .reduce((sum, balance) => {
835
+ const balanceNum =
836
+ typeof balance.balance === 'string'
837
+ ? parseFloat(balance.balance)
838
+ : balance.balance || 0;
839
+ return sum + balanceNum;
840
+ }, 0)
841
+ .toString();
842
+
843
+ networksTemp.push({
844
+ networkId: blockchain,
845
+ totalValueUsd: networkTotal,
846
+ gasAssetCaip: nativeAssetCaip || null,
847
+ gasAssetSymbol: gasAsset?.symbol || null,
848
+ icon: gasAsset?.icon || null,
849
+ color: gasAsset?.color || null,
850
+ totalNativeBalance,
851
+ });
852
+
853
+ totalPortfolioValue += networkTotal;
854
+ }
855
+
856
+ // Sort networks by USD value and assign to dashboard
857
+ dashboardData.networks = networksTemp.sort((a, b) => b.totalValueUsd - a.totalValueUsd);
858
+ dashboardData.totalValueUsd = totalPortfolioValue;
859
+
860
+ // Calculate network percentages for pie chart
861
+ dashboardData.networkPercentages = dashboardData.networks
862
+ .map((network) => ({
863
+ networkId: network.networkId,
864
+ percentage:
865
+ totalPortfolioValue > 0
866
+ ? Number(((network.totalValueUsd / totalPortfolioValue) * 100).toFixed(2))
867
+ : 0,
868
+ }))
869
+ .filter((entry) => entry.percentage > 0); // Remove zero percentages
870
+
871
+ /* console.log('Bitcoin balances:', btcBalances.map(b => ({
872
+ pubkey: b.pubkey,
873
+ balance: b.balance,
874
+ valueUsd: b.valueUsd
875
+ }))); */
876
+
877
+ this.dashboard = dashboardData;
878
+
879
+ return true;
880
+ } catch (e) {
881
+ console.error(tag, 'Error in sync:', e);
882
+ throw e;
883
+ }
884
+ };
885
+ this.estimateMax = async function (sendPayload: any) {
886
+ try {
887
+ sendPayload.isMax = true;
888
+ let unsignedTx = await this.buildTx(sendPayload);
889
+ } catch (e) {
890
+ console.error(e);
891
+ throw e;
892
+ }
893
+ };
894
+ this.buildTx = async function (sendPayload: any) {
895
+ let tag = TAG + ' | buildTx | ';
896
+ try {
897
+ const transactionDependencies = {
898
+ context: this.context,
899
+ assetContext: this.assetContext,
900
+ balances: this.balances,
901
+ pioneer: this.pioneer,
902
+ pubkeys: this.pubkeys,
903
+ nodes: this.nodes,
904
+ keepKeySdk: this.keepKeySdk,
905
+ };
906
+ let txManager = new TransactionManager(transactionDependencies, this.events);
907
+ let unsignedTx = await txManager.transfer(sendPayload);
908
+ console.log(tag, 'unsignedTx: ', unsignedTx);
909
+ return unsignedTx;
910
+ } catch (e) {
911
+ console.error(e);
912
+ throw e;
913
+ }
914
+ };
915
+ this.buildDelegateTx = async function (caip: string, params: StakingTxParams) {
916
+ let tag = TAG + ' | buildDelegateTx | ';
917
+ try {
918
+ const delegateParams = {
919
+ ...params,
920
+ type: 'delegate' as const,
921
+ };
922
+ let unsignedTx = await createUnsignedStakingTx(
923
+ caip,
924
+ delegateParams,
925
+ this.pubkeys,
926
+ this.pioneer,
927
+ this.keepKeySdk,
928
+ );
929
+ console.log(tag, 'unsignedTx: ', unsignedTx);
930
+ return unsignedTx;
931
+ } catch (e) {
932
+ console.error(e);
933
+ throw e;
934
+ }
935
+ };
936
+ this.buildUndelegateTx = async function (caip: string, params: StakingTxParams) {
937
+ let tag = TAG + ' | buildUndelegateTx | ';
938
+ try {
939
+ const undelegateParams = {
940
+ ...params,
941
+ type: 'undelegate' as const,
942
+ };
943
+ let unsignedTx = await createUnsignedStakingTx(
944
+ caip,
945
+ undelegateParams,
946
+ this.pubkeys,
947
+ this.pioneer,
948
+ this.keepKeySdk,
949
+ );
950
+ console.log(tag, 'unsignedTx: ', unsignedTx);
951
+ return unsignedTx;
952
+ } catch (e) {
953
+ console.error(e);
954
+ throw e;
955
+ }
956
+ };
957
+ this.buildClaimRewardsTx = async function (caip: string, params: StakingTxParams) {
958
+ let tag = TAG + ' | buildClaimRewardsTx | ';
959
+ try {
960
+ const claimParams = {
961
+ ...params,
962
+ type: 'claim_rewards' as const,
963
+ };
964
+ let unsignedTx = await createUnsignedStakingTx(
965
+ caip,
966
+ claimParams,
967
+ this.pubkeys,
968
+ this.pioneer,
969
+ this.keepKeySdk,
970
+ );
971
+ console.log(tag, 'unsignedTx: ', unsignedTx);
972
+ return unsignedTx;
973
+ } catch (e) {
974
+ console.error(e);
975
+ throw e;
976
+ }
977
+ };
978
+ this.buildClaimAllRewardsTx = async function (caip: string, params: StakingTxParams) {
979
+ let tag = TAG + ' | buildClaimAllRewardsTx | ';
980
+ try {
981
+ const claimAllParams = {
982
+ ...params,
983
+ type: 'claim_all_rewards' as const,
984
+ };
985
+ let unsignedTx = await createUnsignedStakingTx(
986
+ caip,
987
+ claimAllParams,
988
+ this.pubkeys,
989
+ this.pioneer,
990
+ this.keepKeySdk,
991
+ );
992
+ //console.log(tag, 'unsignedTx: ', unsignedTx);
993
+ return unsignedTx;
994
+ } catch (e) {
995
+ console.error(e);
996
+ throw e;
997
+ }
998
+ };
999
+ this.signTx = async function (unsignedTx: any) {
1000
+ let tag = TAG + ' | signTx | ';
1001
+ try {
1002
+ const transactionDependencies = {
1003
+ context: this.context,
1004
+ assetContext: this.assetContext,
1005
+ balances: this.balances,
1006
+ pioneer: this.pioneer,
1007
+ pubkeys: this.pubkeys,
1008
+ nodes: this.nodes,
1009
+ keepKeySdk: this.keepKeySdk,
1010
+ };
1011
+ let txManager = new TransactionManager(transactionDependencies, this.events);
1012
+ let signedTx = await txManager.sign(unsignedTx);
1013
+ return signedTx;
1014
+ } catch (e) {
1015
+ console.error(e);
1016
+ throw e;
1017
+ }
1018
+ };
1019
+ this.broadcastTx = async function (caip: string, signedTx: any) {
1020
+ let tag = TAG + ' | broadcastTx | ';
1021
+ try {
1022
+ const transactionDependencies = {
1023
+ context: this.context,
1024
+ assetContext: this.assetContext,
1025
+ balances: this.balances,
1026
+ pioneer: this.pioneer,
1027
+ pubkeys: this.pubkeys,
1028
+ nodes: this.nodes,
1029
+ keepKeySdk: this.keepKeySdk,
1030
+ };
1031
+ let txManager = new TransactionManager(transactionDependencies, this.events);
1032
+ let payload = {
1033
+ networkId: caipToNetworkId(caip),
1034
+ serialized: signedTx,
1035
+ };
1036
+ let txid = await txManager.broadcast(payload);
1037
+ return txid;
1038
+ } catch (e) {
1039
+ console.error(e);
1040
+ throw e;
1041
+ }
1042
+ };
1043
+ this.swap = async function (swapPayload) {
1044
+ let tag = `${TAG} | swap | `;
1045
+ try {
1046
+ if (!swapPayload) throw Error('swapPayload required!');
1047
+ if (!swapPayload.caipIn) throw Error('caipIn required!');
1048
+ if (!swapPayload.caipOut) throw Error('caipOut required!');
1049
+ if (!swapPayload.isMax && !swapPayload.amount)
1050
+ throw Error('amount required! Set either amount or isMax: true');
1051
+
1052
+ //Set contexts
1053
+
1054
+ await this.setAssetContext({ caip: swapPayload.caipIn });
1055
+ await this.setOutboundAssetContext({ caip: swapPayload.caipOut });
1056
+
1057
+ if (!this.assetContext || !this.assetContext.networkId)
1058
+ throw Error('Invalid networkId for assetContext');
1059
+ if (!this.outboundAssetContext || !this.outboundAssetContext.networkId)
1060
+ throw Error('Invalid networkId for outboundAssetContext');
1061
+ if (!this.outboundAssetContext || !this.outboundAssetContext.address)
1062
+ throw Error('Invalid outboundAssetContext missing address');
1063
+
1064
+ //get quote
1065
+ // Quote fetching logic
1066
+ // Helper function to check if pubkey matches network (handles EVM wildcard)
1067
+ const matchesNetwork = (pubkey: any, networkId: string) => {
1068
+ if (!pubkey.networks || !Array.isArray(pubkey.networks)) return false;
1069
+ // Exact match
1070
+ if (pubkey.networks.includes(networkId)) return true;
1071
+ // For EVM chains, check if pubkey has eip155:* wildcard
1072
+ if (networkId.startsWith('eip155:') && pubkey.networks.includes('eip155:*')) return true;
1073
+ return false;
1074
+ };
1075
+
1076
+ const pubkeys = this.pubkeys.filter((e: any) =>
1077
+ matchesNetwork(e, this.assetContext.networkId),
1078
+ );
1079
+ let senderAddress = pubkeys[0]?.address || pubkeys[0]?.master || pubkeys[0]?.pubkey;
1080
+ if (!senderAddress) throw new Error('senderAddress not found! wallet not connected');
1081
+ if (senderAddress.includes('bitcoincash:')) {
1082
+ senderAddress = senderAddress.replace('bitcoincash:', '');
1083
+ }
1084
+
1085
+ const pubkeysOut = this.pubkeys.filter((e: any) =>
1086
+ matchesNetwork(e, this.outboundAssetContext.networkId),
1087
+ );
1088
+
1089
+ // Handle both regular addresses and xpubs for recipient
1090
+ let recipientAddress;
1091
+
1092
+ // First priority: use actual address if available
1093
+ recipientAddress = pubkeysOut[0]?.address || pubkeysOut[0]?.master || pubkeysOut[0]?.pubkey;
1094
+
1095
+ if (!recipientAddress) throw Error('Failed to Find recepient address');
1096
+ if (recipientAddress.includes('bitcoincash:')) {
1097
+ recipientAddress = recipientAddress.replace('bitcoincash:', '');
1098
+ }
1099
+
1100
+ // Handle max amount if isMax flag is set (consistent with transfer function pattern)
1101
+ let inputAmount;
1102
+ if (swapPayload.isMax) {
1103
+ // Find ALL balances for the input asset (important for UTXO chains with multiple xpubs)
1104
+ const inputBalances = this.balances.filter(
1105
+ (balance: any) => balance.caip === swapPayload.caipIn,
1106
+ );
1107
+
1108
+ if (!inputBalances || inputBalances.length === 0) {
1109
+ throw new Error(`Cannot use max amount: no balance found for ${swapPayload.caipIn}`);
1110
+ }
1111
+
1112
+ // Aggregate all balances for this asset (handles multiple xpubs for BTC, etc.)
1113
+ let totalBalance = 0;
1114
+ console.log(
1115
+ tag,
1116
+ `Found ${inputBalances.length} balance entries for ${swapPayload.caipIn}`,
1117
+ );
1118
+
1119
+ for (const balanceEntry of inputBalances) {
1120
+ const balance = parseFloat(balanceEntry.balance) || 0;
1121
+ totalBalance += balance;
1122
+ console.log(tag, ` - ${balanceEntry.pubkey || balanceEntry.identifier}: ${balance}`);
1123
+ }
1124
+
1125
+ // CRITICAL: Update the assetContext with the aggregated balance
1126
+ // This ensures the quote gets the correct total balance, not just one xpub
1127
+ this.assetContext.balance = totalBalance.toString();
1128
+ this.assetContext.valueUsd = (
1129
+ totalBalance * parseFloat(this.assetContext.priceUsd || '0')
1130
+ ).toFixed(2);
1131
+ console.log(tag, `Updated assetContext balance to aggregated total: ${totalBalance}`);
1132
+
1133
+ // Fee reserves by network (conservative estimates)
1134
+ // These match the pattern used in transfer functions
1135
+ const feeReserves: any = {
1136
+ 'bip122:000000000019d6689c085ae165831e93/slip44:0': 0.00005, // BTC
1137
+ 'eip155:1/slip44:60': 0.001, // ETH
1138
+ 'cosmos:thorchain-mainnet-v1/slip44:931': 0.02, // RUNE
1139
+ 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3': 1, // DOGE
1140
+ 'bip122:000007d91d1254d60e2dd1ae58038307/slip44:5': 0.001, // DASH
1141
+ 'bip122:000000000000000000651ef99cb9fcbe/slip44:145': 0.0005, // BCH
1142
+ };
1143
+
1144
+ const reserve = feeReserves[swapPayload.caipIn] || 0.0001;
1145
+ inputAmount = Math.max(0, totalBalance - reserve);
1146
+
1147
+ console.log(
1148
+ tag,
1149
+ `Using max amount for swap: ${inputAmount} (total balance: ${totalBalance}, reserve: ${reserve})`,
1150
+ );
1151
+ } else {
1152
+ // Convert amount to number for type safety
1153
+ inputAmount =
1154
+ typeof swapPayload.amount === 'string'
1155
+ ? parseFloat(swapPayload.amount)
1156
+ : swapPayload.amount;
1157
+
1158
+ // Validate the amount is a valid number
1159
+ if (isNaN(inputAmount) || inputAmount <= 0) {
1160
+ throw new Error(`Invalid amount provided: ${swapPayload.amount}`);
1161
+ }
1162
+ }
1163
+
1164
+ let quote = {
1165
+ affiliate: '0x658DE0443259a1027caA976ef9a42E6982037A03',
1166
+ sellAsset: this.assetContext,
1167
+ sellAmount: inputAmount.toPrecision(8),
1168
+ buyAsset: this.outboundAssetContext,
1169
+ recipientAddress, // Fill this based on your logic
1170
+ senderAddress, // Fill this based on your logic
1171
+ slippage: '3',
1172
+ };
1173
+
1174
+ let result: any;
1175
+ try {
1176
+ result = await this.pioneer.Quote(quote);
1177
+ result = result.data;
1178
+ } catch (e) {
1179
+ console.error(tag, 'Failed to get quote: ', e);
1180
+ }
1181
+ if (result.length === 0)
1182
+ throw Error(
1183
+ 'No quotes available! path: ' + quote.sellAsset.caip + ' -> ' + quote.buyAsset.caip,
1184
+ );
1185
+ //TODO let user handle selecting quote?
1186
+ let selected = result[0];
1187
+ let txs = selected.quote.txs;
1188
+ if (!txs) throw Error('invalid quote!');
1189
+ for (let i = 0; i < txs.length; i++) {
1190
+ let tx = txs[i];
1191
+ const transactionDependencies = {
1192
+ context: this.context,
1193
+ assetContext: this.assetContext,
1194
+ balances: this.balances,
1195
+ pioneer: this.pioneer,
1196
+ pubkeys: this.pubkeys,
1197
+ nodes: this.nodes,
1198
+ keepKeySdk: this.keepKeySdk,
1199
+ };
1200
+
1201
+ let txManager = new TransactionManager(transactionDependencies, this.events);
1202
+
1203
+ let caip = swapPayload.caipIn;
1204
+
1205
+ let unsignedTx;
1206
+ if (tx.type === 'deposit') {
1207
+ //build deposit tx
1208
+ unsignedTx = await createUnsignedTendermintTx(
1209
+ caip,
1210
+ tx.type,
1211
+ tx.txParams.amount,
1212
+ tx.txParams.memo,
1213
+ this.pubkeys,
1214
+ this.pioneer,
1215
+ this.keepKeySdk,
1216
+ false,
1217
+ undefined,
1218
+ );
1219
+ } else {
1220
+ if (!tx.txParams.memo) throw Error('memo required on swaps!');
1221
+ const sendPayload: any = {
1222
+ caip,
1223
+ to: tx.txParams.recipientAddress,
1224
+ amount: tx.txParams.amount,
1225
+ feeLevel: 5,
1226
+ memo: tx.txParams.memo,
1227
+ //Options
1228
+ };
1229
+
1230
+ //if isMax
1231
+ if (swapPayload.isMax) sendPayload.isMax = true;
1232
+
1233
+ unsignedTx = await txManager.transfer(sendPayload);
1234
+ }
1235
+
1236
+ return unsignedTx;
1237
+ }
1238
+ } catch (e) {
1239
+ console.error(tag, 'Error: ', e);
1240
+ throw e;
1241
+ }
1242
+ };
1243
+ this.transfer = async function (sendPayload) {
1244
+ let tag = `${TAG} | transfer | `;
1245
+ try {
1246
+ if (!sendPayload) throw Error('sendPayload required!');
1247
+ if (!sendPayload.caip) throw Error('caip required!');
1248
+ if (!sendPayload.to) throw Error('to required!');
1249
+ if (!sendPayload.isMax) sendPayload.isMax = false;
1250
+ let { caip } = sendPayload;
1251
+
1252
+ const transactionDependencies = {
1253
+ context: this.context,
1254
+ assetContext: this.assetContext,
1255
+ balances: this.balances,
1256
+ pioneer: this.pioneer,
1257
+ pubkeys: this.pubkeys,
1258
+ nodes: this.nodes,
1259
+ keepKeySdk: this.keepKeySdk,
1260
+ isMax: sendPayload.isMax,
1261
+ };
1262
+ let txManager = new TransactionManager(transactionDependencies, this.events);
1263
+ let unsignedTx = await txManager.transfer(sendPayload);
1264
+
1265
+ // Sign the transaction
1266
+ let signedTx = await txManager.sign({ caip, unsignedTx });
1267
+ if (!signedTx) throw Error('Failed to sign transaction!');
1268
+ // Broadcast the transaction
1269
+ let payload = {
1270
+ networkId: caipToNetworkId(caip),
1271
+ serialized: signedTx,
1272
+ };
1273
+ let txid = await txManager.broadcast(payload);
1274
+ return { txid, events: this.events };
1275
+ } catch (error: unknown) {
1276
+ if (error instanceof Error) {
1277
+ console.error(tag, 'An error occurred during the transfer process:', error.message);
1278
+ } else {
1279
+ console.error(tag, 'An unknown error occurred during the transfer process');
1280
+ }
1281
+ throw error;
1282
+ }
1283
+ };
1284
+ this.followTransaction = async function (caip: string, txid: string) {
1285
+ let tag = ' | followTransaction | ';
1286
+ try {
1287
+ const finalConfirmationBlocksByCaip = {
1288
+ dogecoin: 3,
1289
+ bitcoin: 6,
1290
+ };
1291
+
1292
+ const requiredConfirmations = finalConfirmationBlocksByCaip[caip] || 1;
1293
+ let isConfirmed = false;
1294
+ const broadcastTime = Date.now();
1295
+ let detectedTime: number | null = null;
1296
+ let confirmTime: number | null = null;
1297
+
1298
+ while (!isConfirmed) {
1299
+ try {
1300
+ const response = await this.pioneer.LookupTx({
1301
+ networkId: caipToNetworkId(caip),
1302
+ txid,
1303
+ });
1304
+
1305
+ if (response?.data?.data) {
1306
+ const txInfo = response.data.data;
1307
+
1308
+ if (txInfo.txid && detectedTime === null) {
1309
+ detectedTime = Date.now();
1310
+ /* Old debug code commented out
1311
+ //console.log(
1312
+ tag,
1313
+ `Time from broadcast to detection: ${formatTime(detectedTime - broadcastTime)}`,
1314
+ );
1315
+ */
1316
+ }
1317
+
1318
+ if (txInfo.confirmations >= requiredConfirmations) {
1319
+ isConfirmed = true;
1320
+ confirmTime = Date.now();
1321
+
1322
+ if (detectedTime !== null && confirmTime !== null) {
1323
+ /* Old debug code commented out
1324
+ //console.log(
1325
+ tag,
1326
+ `Time from detection to confirmation: ${formatTime(
1327
+ confirmTime - detectedTime,
1328
+ )}`,
1329
+ );
1330
+ */
1331
+ }
1332
+ }
1333
+ }
1334
+ } catch (error) {
1335
+ console.error(tag, 'Error:', error);
1336
+ }
1337
+
1338
+ if (!isConfirmed) {
1339
+ await new Promise((resolve) => setTimeout(resolve, 8000));
1340
+ }
1341
+ }
1342
+
1343
+ return {
1344
+ caip,
1345
+ txid,
1346
+ broadcastTime: new Date(broadcastTime).toISOString(),
1347
+ detectedTime: detectedTime ? new Date(detectedTime).toISOString() : null,
1348
+ confirmTime: confirmTime ? new Date(confirmTime).toISOString() : null,
1349
+ timeToDetect: detectedTime ? formatTime(detectedTime - broadcastTime) : null,
1350
+ timeToConfirm: confirmTime ? formatTime(confirmTime - broadcastTime) : null,
1351
+ timeFromDetectionToConfirm:
1352
+ detectedTime && confirmTime ? formatTime(confirmTime - detectedTime) : null,
1353
+ requiredConfirmations,
1354
+ };
1355
+ } catch (error) {
1356
+ console.error(tag, 'Error:', error);
1357
+ throw new Error('Failed to follow transaction');
1358
+ }
1359
+ };
1360
+ this.setBlockchains = async function (blockchains: any) {
1361
+ const tag = `${TAG} | setBlockchains | `;
1362
+ try {
1363
+ if (!blockchains) throw Error('blockchains required!');
1364
+
1365
+ // Deduplicate blockchains array to prevent duplicate calculations
1366
+ const uniqueBlockchains = [...new Set(blockchains)];
1367
+ if (blockchains.length !== uniqueBlockchains.length) {
1368
+ console.warn(
1369
+ tag,
1370
+ `Removed ${blockchains.length - uniqueBlockchains.length} duplicate blockchains`,
1371
+ );
1372
+ }
1373
+
1374
+ this.blockchains = uniqueBlockchains;
1375
+ this.events.emit('SET_BLOCKCHAINS', this.blockchains);
1376
+ } catch (e) {
1377
+ console.error('Failed to load balances! e: ', e);
1378
+ throw e;
1379
+ }
1380
+ };
1381
+ this.addAsset = async function (caip: string, data: any) {
1382
+ let tag = TAG + ' | addAsset | ';
1383
+ try {
1384
+ let success = false;
1385
+ if (!caip) throw new Error('caip required!');
1386
+
1387
+ let dataLocal = assetData[caip];
1388
+ //get assetData from discover
1389
+ if (!dataLocal) {
1390
+ if (!data.networkId) throw new Error('networkId required! can not build asset');
1391
+ // if (!data.chart) throw new Error('chart required! can not build asset');
1392
+ // console.error(tag, '*** DISCOVERY *** ', data);
1393
+ // console.error(tag, 'Failed to build asset for caip: ', caip);
1394
+ //build asset
1395
+ let asset: any = {};
1396
+ asset.source = data.chart;
1397
+ asset.caip = caip;
1398
+ asset.networkId = data.networkId;
1399
+ //Zapper chart
1400
+ if (data.token && data.token.symbol) asset.symbol = data.token.symbol;
1401
+ if (data.token && data.token.name) asset.name = data.token.name;
1402
+ if (data.token && data.token.decimals) asset.decimals = data.token.decimals;
1403
+
1404
+ //common
1405
+ asset.raw = JSON.stringify(data);
1406
+ //verify
1407
+ if (!asset.symbol) throw new Error('symbol required! can not build asset');
1408
+ if (!asset.name) throw new Error('name required! can not build asset');
1409
+ if (!asset.decimals) throw new Error('decimals required! can not build asset');
1410
+
1411
+ //post to pioneer-discovery
1412
+ // let resultSubmit = await this.pioneer.Discovery({asset})
1413
+
1414
+ //set locally into assetMap
1415
+ // this.assetsMap.set(caip, asset);
1416
+ success = true;
1417
+ } else {
1418
+ this.assetsMap.set(caip, dataLocal);
1419
+ success = true;
1420
+ }
1421
+
1422
+ return success;
1423
+ } catch (e) {
1424
+ console.error('Failed to load balances! e: ', e);
1425
+ throw e;
1426
+ }
1427
+ };
1428
+ this.clearWalletState = async function () {
1429
+ const tag = `${TAG} | clearWalletState | `;
1430
+ try {
1431
+ this.context = null;
1432
+ // this.contextType = WalletOption.KEEPKEY;
1433
+ this.paths = [];
1434
+ this.blockchains = [];
1435
+ this.pubkeys = [];
1436
+ this.pubkeySet.clear(); // Clear the tracking set as well
1437
+ console.log(tag, 'Cleared wallet state including pubkeys and tracking set');
1438
+ return true;
1439
+ } catch (e) {
1440
+ console.error(tag, 'e: ', e);
1441
+ throw e;
1442
+ }
1443
+ };
1444
+ this.getAssets = async function () {
1445
+ /*
1446
+ Get Asset Rules
1447
+
1448
+ asset MUST have a balance if a token to be tracked
1449
+ asset MUST have a pubkey to be tracked
1450
+ */
1451
+ return this.getGasAssets();
1452
+ };
1453
+ this.getGasAssets = async function () {
1454
+ const tag = `${TAG} | getGasAssets | `;
1455
+ try {
1456
+ //get configured blockchains
1457
+ for (let i = 0; i < this.blockchains.length; i++) {
1458
+ let networkId = this.blockchains[i];
1459
+ let caip = networkIdToCaip(networkId);
1460
+ //lookup in pioneerBlob
1461
+ let asset = await assetData[caip.toLowerCase()];
1462
+ if (asset) {
1463
+ asset.caip = caip.toLowerCase();
1464
+ asset.networkId = networkId;
1465
+ this.assetsMap.set(caip, asset);
1466
+ } else {
1467
+ //Discovery
1468
+ //TODO push to Discovery api
1469
+ throw Error('GAS Asset MISSING from assetData ' + caip);
1470
+ }
1471
+ }
1472
+
1473
+ //add gas assets to map
1474
+
1475
+ // Add missing MAYA token manually until it's added to assetData
1476
+ const mayaTokenCaip = 'cosmos:mayachain-mainnet-v1/denom:maya';
1477
+ if (!this.assetsMap.has(mayaTokenCaip)) {
1478
+ const mayaToken = {
1479
+ caip: mayaTokenCaip,
1480
+ networkId: 'cosmos:mayachain-mainnet-v1',
1481
+ chainId: 'mayachain-mainnet-v1',
1482
+ symbol: 'MAYA',
1483
+ name: 'Maya Token',
1484
+ precision: 4,
1485
+ decimals: 4,
1486
+ color: '#00D4AA',
1487
+ icon: 'https://pioneers.dev/coins/maya.png',
1488
+ explorer: 'https://explorer.mayachain.info',
1489
+ explorerAddressLink: 'https://explorer.mayachain.info/address/{{address}}',
1490
+ explorerTxLink: 'https://explorer.mayachain.info/tx/{{txid}}',
1491
+ type: 'token',
1492
+ isToken: true,
1493
+ denom: 'maya',
1494
+ };
1495
+ this.assetsMap.set(mayaTokenCaip, mayaToken);
1496
+ console.log(tag, 'Added MAYA token to assetsMap');
1497
+ }
1498
+
1499
+ return this.assetsMap;
1500
+ } catch (e) {
1501
+ console.error(e);
1502
+ throw e;
1503
+ }
1504
+ };
1505
+ this.getPubkeys = async function () {
1506
+ const tag = `${TAG} | getPubkeys | `;
1507
+ try {
1508
+ if (this.paths.length === 0) throw new Error('No paths found!');
1509
+
1510
+ // Use optimized batch fetching with individual fallback
1511
+ const pubkeys = await optimizedGetPubkeys(
1512
+ this.blockchains,
1513
+ this.paths,
1514
+ this.keepKeySdk,
1515
+ this.context,
1516
+ getPubkey, // Pass the original getPubkey function for fallback
1517
+ );
1518
+
1519
+ // Merge newly fetched pubkeys with existing ones using deduplication
1520
+ const beforeCount = this.pubkeys.length;
1521
+ const allPubkeys = [...this.pubkeys, ...pubkeys];
1522
+ const dedupedPubkeys = this.deduplicatePubkeys(allPubkeys);
1523
+
1524
+ // Use setPubkeys to properly update both array and set
1525
+ this.setPubkeys(dedupedPubkeys);
1526
+
1527
+ const duplicatesRemoved = allPubkeys.length - this.pubkeys.length;
1528
+ if (duplicatesRemoved > 0) {
1529
+ }
1530
+
1531
+ // Emit event to notify that pubkeys have been set
1532
+ this.events.emit('SET_PUBKEYS', this.pubkeys);
1533
+
1534
+ return pubkeys;
1535
+ } catch (error) {
1536
+ console.error('Error in getPubkeys:', error);
1537
+ console.error(tag, 'Error in getPubkeys:', error);
1538
+ throw error;
1539
+ }
1540
+ };
1541
+ this.getBalancesForNetworks = async function (networkIds: string[]) {
1542
+ const tag = `${TAG} | getBalancesForNetworks | `;
1543
+ try {
1544
+ // Add defensive check for pioneer initialization
1545
+ if (!this.pioneer) {
1546
+ console.error(
1547
+ tag,
1548
+ 'ERROR: Pioneer client not initialized! this.pioneer is:',
1549
+ this.pioneer,
1550
+ );
1551
+ throw new Error('Pioneer client not initialized. Call init() first.');
1552
+ }
1553
+
1554
+ const assetQuery: { caip: string; pubkey: string }[] = [];
1555
+
1556
+ for (const networkId of networkIds) {
1557
+ let adjustedNetworkId = networkId;
1558
+
1559
+ if (adjustedNetworkId.includes('eip155:')) {
1560
+ adjustedNetworkId = 'eip155:*';
1561
+ }
1562
+
1563
+ const isEip155 = adjustedNetworkId.includes('eip155');
1564
+ const pubkeys = this.pubkeys.filter(
1565
+ (pubkey) =>
1566
+ pubkey.networks &&
1567
+ Array.isArray(pubkey.networks) &&
1568
+ pubkey.networks.some((network) => {
1569
+ if (isEip155) return network.startsWith('eip155:');
1570
+ return network === adjustedNetworkId;
1571
+ }),
1572
+ );
1573
+
1574
+ const caipNative = await networkIdToCaip(networkId);
1575
+ for (const pubkey of pubkeys) {
1576
+ assetQuery.push({ caip: caipNative, pubkey: pubkey.pubkey });
1577
+ }
1578
+ }
1579
+
1580
+ console.time('GetPortfolioBalances Response Time');
1581
+
1582
+ try {
1583
+ let marketInfo = await this.pioneer.GetPortfolioBalances(assetQuery);
1584
+ console.timeEnd('GetPortfolioBalances Response Time');
1585
+
1586
+ // DEFENSIVE: Validate response structure to prevent "balances.filter is not a function"
1587
+ if (!marketInfo || !marketInfo.data) {
1588
+ console.error(tag, 'Invalid response structure:', marketInfo);
1589
+ throw new Error('GetPortfolioBalances returned invalid response: missing data field');
1590
+ }
1591
+
1592
+ if (!Array.isArray(marketInfo.data)) {
1593
+ console.error(
1594
+ tag,
1595
+ 'GetPortfolioBalances returned non-array data:',
1596
+ typeof marketInfo.data,
1597
+ marketInfo.data
1598
+ );
1599
+ throw new Error(
1600
+ `GetPortfolioBalances returned invalid data type: expected array, got ${typeof marketInfo.data}`
1601
+ );
1602
+ }
1603
+
1604
+ let balances = marketInfo.data;
1605
+
1606
+ const bitcoinBalances = balances.filter(
1607
+ (b: any) => b.caip === 'bip122:000000000019d6689c085ae165831e93/slip44:0',
1608
+ );
1609
+ if (bitcoinBalances.length > 0) {
1610
+ }
1611
+
1612
+ // Enrich balances with asset info
1613
+ for (let balance of balances) {
1614
+ const assetInfo = this.assetsMap.get(balance.caip);
1615
+ if (!assetInfo) continue;
1616
+
1617
+ Object.assign(balance, assetInfo, {
1618
+ networkId: caipToNetworkId(balance.caip),
1619
+ icon: assetInfo.icon || 'https://pioneers.dev/coins/etherum.png',
1620
+ identifier: `${balance.caip}:${balance.pubkey}`,
1621
+ });
1622
+ }
1623
+ console.log(tag, 'balances: ', balances);
1624
+
1625
+ this.balances = balances;
1626
+ this.events.emit('SET_BALANCES', this.balances);
1627
+ return this.balances;
1628
+ } catch (apiError: any) {
1629
+ console.error(tag, 'GetPortfolioBalances API call failed:', apiError);
1630
+ throw new Error(
1631
+ `GetPortfolioBalances API call failed: ${apiError?.message || 'Unknown error'}`,
1632
+ );
1633
+ }
1634
+ } catch (e) {
1635
+ console.error(tag, 'Error: ', e);
1636
+ throw e;
1637
+ }
1638
+ };
1639
+ this.getBalances = async function () {
1640
+ const tag = `${TAG} | getBalances | `;
1641
+ try {
1642
+ // Simply call the shared function with all blockchains
1643
+ return await this.getBalancesForNetworks(this.blockchains);
1644
+ } catch (e) {
1645
+ console.error(tag, 'Error in getBalances: ', e);
1646
+ throw e;
1647
+ }
1648
+ };
1649
+ this.getBalance = async function (networkId: string) {
1650
+ const tag = `${TAG} | getBalance | `;
1651
+ try {
1652
+ // If we need to handle special logic like eip155: inside getBalance,
1653
+ // we can do it here or just rely on getBalancesForNetworks to handle it.
1654
+ // For example:
1655
+ // if (networkId.includes('eip155:')) {
1656
+ // networkId = 'eip155:*';
1657
+ // }
1658
+
1659
+ // Call the shared function with a single-network array
1660
+ const results = await this.getBalancesForNetworks([networkId]);
1661
+
1662
+ // If needed, you can filter only those that match the specific network
1663
+ // (especially if you used wildcard eip155:*)
1664
+ const filtered = results.filter(
1665
+ async (b) => b.networkId === (await networkIdToCaip(networkId)),
1666
+ );
1667
+ return filtered;
1668
+ } catch (e) {
1669
+ console.error(tag, 'Error: ', e);
1670
+ throw e;
1671
+ }
1672
+ };
1673
+
1674
+ /**
1675
+ * Get normalized fee rates for a specific network
1676
+ * This method handles all fee complexity and returns a clean, consistent format
1677
+ */
1678
+ this.getFees = async function (networkId: string): Promise<NormalizedFeeRates> {
1679
+ const tag = `${TAG} | getFees | `;
1680
+ try {
1681
+ if (!this.pioneer) {
1682
+ throw new Error('Pioneer client not initialized. Call init() first.');
1683
+ }
1684
+
1685
+ // Use the fee management module to get normalized fees
1686
+ return await getFees(this.pioneer, networkId);
1687
+ } catch (e) {
1688
+ console.error(tag, 'Error getting fees: ', e);
1689
+ throw e;
1690
+ }
1691
+ };
1692
+
1693
+ /**
1694
+ * Estimate transaction fee based on fee rate and transaction parameters
1695
+ * This is a utility method that doesn't require network access
1696
+ */
1697
+ this.estimateTransactionFee = function (
1698
+ feeRate: string,
1699
+ unit: string,
1700
+ networkType: string,
1701
+ txSize?: number
1702
+ ): FeeEstimate {
1703
+ return estimateTransactionFee(feeRate, unit, networkType, txSize);
1704
+ };
1705
+ this.getCharts = async function () {
1706
+ const tag = `${TAG} | getCharts | `;
1707
+ try {
1708
+ console.log(tag, 'Fetching charts');
1709
+
1710
+ // Fetch balances from the `getCharts` function
1711
+ const newBalances = await getCharts(
1712
+ this.blockchains,
1713
+ this.pioneer,
1714
+ this.pubkeys,
1715
+ this.context,
1716
+ );
1717
+ console.log(tag, 'newBalances: ', newBalances);
1718
+
1719
+ // Deduplicate balances using a Map with `identifier` as the key
1720
+ const uniqueBalances = new Map(
1721
+ [...this.balances, ...newBalances].map((balance: any) => [
1722
+ balance.identifier,
1723
+ {
1724
+ ...balance,
1725
+ type: balance.type || 'balance',
1726
+ },
1727
+ ]),
1728
+ );
1729
+ console.log(tag, 'uniqueBalances: ', uniqueBalances);
1730
+
1731
+ // Convert Map back to array and set this.balances
1732
+ this.balances = Array.from(uniqueBalances.values());
1733
+ console.log(tag, 'Updated this.balances: ', this.balances);
1734
+
1735
+ return this.balances;
1736
+ } catch (e) {
1737
+ console.error(tag, 'Error in getCharts:', e);
1738
+ throw e;
1739
+ }
1740
+ };
1741
+ this.setContext = async (context: string): Promise<{ success: boolean }> => {
1742
+ const tag = `${TAG} | setContext | `;
1743
+ try {
1744
+ if (!context) throw Error('context required!');
1745
+ this.context = context;
1746
+ this.events.emit('SET_CONTEXT', context);
1747
+ return { success: true };
1748
+ } catch (e) {
1749
+ console.error(tag, 'e: ', e);
1750
+ return { success: false };
1751
+ }
1752
+ };
1753
+ this.setContextType = async (contextType: string): Promise<{ success: boolean }> => {
1754
+ const tag = `${TAG} | setContextType | `;
1755
+ try {
1756
+ if (!contextType) throw Error('contextType required!');
1757
+ this.contextType = contextType;
1758
+ this.events.emit('SET_CONTEXT_TYPE', contextType);
1759
+ return { success: true };
1760
+ } catch (e) {
1761
+ console.error(tag, 'e: ', e);
1762
+ return { success: false };
1763
+ }
1764
+ };
1765
+ this.refresh = async (): Promise<any> => {
1766
+ const tag = `${TAG} | refresh | `;
1767
+ try {
1768
+ await this.sync();
1769
+ return this.balances;
1770
+ } catch (e) {
1771
+ console.error(tag, 'e: ', e);
1772
+ throw e;
1773
+ }
1774
+ };
1775
+ this.setAssetContext = async function (asset?: any): Promise<any> {
1776
+ const tag = `${TAG} | setAssetContext | `;
1777
+ try {
1778
+ // Accept null
1779
+ if (!asset) {
1780
+ this.assetContext = null;
1781
+ return;
1782
+ }
1783
+
1784
+ if (!asset.caip) throw Error('Invalid Asset! missing caip!');
1785
+ if (!asset.networkId) asset.networkId = caipToNetworkId(asset.caip);
1786
+
1787
+ // CRITICAL VALIDATION: Check if we have an address/xpub for this network
1788
+ if (!this.pubkeys || this.pubkeys.length === 0) {
1789
+ const errorMsg = `Cannot set asset context for ${asset.caip} - no pubkeys loaded. Please initialize wallet first.`;
1790
+ console.error(tag, errorMsg);
1791
+ throw new Error(errorMsg);
1792
+ }
1793
+
1794
+ // For EVM chains, check for wildcard eip155:* in addition to exact match
1795
+ const pubkeysForNetwork = this.pubkeys.filter((e: any) => {
1796
+ if (!e.networks || !Array.isArray(e.networks)) return false;
1797
+
1798
+ // Exact match
1799
+ if (e.networks.includes(asset.networkId)) return true;
1800
+
1801
+ // For EVM chains, check if pubkey has eip155:* wildcard
1802
+ if (asset.networkId.startsWith('eip155:') && e.networks.includes('eip155:*')) {
1803
+ return true;
1804
+ }
1805
+
1806
+ return false;
1807
+ });
1808
+
1809
+ if (pubkeysForNetwork.length === 0) {
1810
+ const errorMsg = `Cannot set asset context for ${asset.caip} - no address/xpub found for network ${asset.networkId}`;
1811
+ console.error(tag, errorMsg);
1812
+ console.error(tag, 'Available networks in pubkeys:', [
1813
+ ...new Set(this.pubkeys.flatMap((p: any) => p.networks || [])),
1814
+ ]);
1815
+ throw new Error(errorMsg);
1816
+ }
1817
+
1818
+ // For UTXO chains, verify we have xpub
1819
+ const isUtxoChain = asset.networkId.startsWith('bip122:');
1820
+ if (isUtxoChain) {
1821
+ const xpubFound = pubkeysForNetwork.some((p: any) => p.type === 'xpub' && p.pubkey);
1822
+ if (!xpubFound) {
1823
+ const errorMsg = `Cannot set asset context for UTXO chain ${asset.caip} - xpub required but not found`;
1824
+ console.error(tag, errorMsg);
1825
+ throw new Error(errorMsg);
1826
+ }
1827
+ }
1828
+
1829
+ // Verify we have a valid address or pubkey
1830
+ const hasValidAddress = pubkeysForNetwork.some(
1831
+ (p: any) => p.address || p.master || p.pubkey,
1832
+ );
1833
+ if (!hasValidAddress) {
1834
+ const errorMsg = `Cannot set asset context for ${asset.caip} - no valid address found in pubkeys`;
1835
+ console.error(tag, errorMsg);
1836
+ throw new Error(errorMsg);
1837
+ }
1838
+
1839
+ console.log(
1840
+ tag,
1841
+ `✅ Validated: Found ${pubkeysForNetwork.length} addresses for ${asset.networkId}`,
1842
+ );
1843
+
1844
+ // ALWAYS fetch fresh market price for the asset
1845
+ let freshPriceUsd = 0;
1846
+ try {
1847
+ // Validate CAIP before calling API
1848
+ if (!asset.caip || typeof asset.caip !== 'string' || !asset.caip.includes(':')) {
1849
+ console.warn(tag, 'Invalid or missing CAIP, skipping market price fetch:', asset.caip);
1850
+ } else {
1851
+ console.log(tag, 'Fetching fresh market price for:', asset.caip);
1852
+ const marketData = await this.pioneer.GetMarketInfo([asset.caip]);
1853
+ console.log(tag, 'Market data response:', marketData);
1854
+
1855
+ if (marketData && marketData.data && marketData.data.length > 0) {
1856
+ freshPriceUsd = marketData.data[0];
1857
+ console.log(tag, '✅ Fresh market price:', freshPriceUsd);
1858
+ } else {
1859
+ console.warn(tag, 'No market data returned for:', asset.caip);
1860
+ }
1861
+ }
1862
+ } catch (marketError) {
1863
+ console.error(tag, 'Error fetching market price:', marketError);
1864
+ // Continue without fresh price, will try to use cached data
1865
+ }
1866
+
1867
+ // Try to find the asset in the local assetsMap
1868
+ let assetInfo = this.assetsMap.get(asset.caip.toLowerCase());
1869
+ console.log(tag, 'assetInfo: ', assetInfo);
1870
+
1871
+ //check discovery
1872
+ let assetInfoDiscovery = assetData[asset.caip];
1873
+ console.log(tag, 'assetInfoDiscovery: ', assetInfoDiscovery);
1874
+ if (assetInfoDiscovery) assetInfo = assetInfoDiscovery;
1875
+
1876
+ // If the asset is not found, create a placeholder object
1877
+ if (!assetInfo) {
1878
+ console.log(tag, 'Building placeholder asset!');
1879
+ // Create a placeholder asset if it's not found in Pioneer or locally
1880
+ assetInfo = {
1881
+ caip: asset.caip.toLowerCase(),
1882
+ networkId: asset.networkId,
1883
+ symbol: asset.symbol || 'UNKNOWN',
1884
+ name: asset.name || 'Unknown Asset',
1885
+ icon: asset.icon || 'https://pioneers.dev/coins/ethereum.png',
1886
+ };
1887
+ }
1888
+
1889
+ // Look for price and balance information in balances
1890
+ // CRITICAL: For UTXO chains, we need to aggregate ALL balances across all xpubs
1891
+ const matchingBalances = this.balances.filter((b) => b.caip === asset.caip);
1892
+
1893
+ if (matchingBalances.length > 0) {
1894
+ // Use price from first balance entry (all should have same price)
1895
+ // Check for both priceUsd and price properties (different sources may use different names)
1896
+ let priceValue = matchingBalances[0].priceUsd || matchingBalances[0].price;
1897
+
1898
+ // If no price but we have valueUsd and balance, calculate the price
1899
+ if ((!priceValue || priceValue === 0) && matchingBalances[0].valueUsd && matchingBalances[0].balance) {
1900
+ const balance = parseFloat(matchingBalances[0].balance);
1901
+ const valueUsd = parseFloat(matchingBalances[0].valueUsd);
1902
+ if (balance > 0 && valueUsd > 0) {
1903
+ priceValue = valueUsd / balance;
1904
+ console.log(tag, 'calculated priceUsd from valueUsd/balance:', priceValue);
1905
+ }
1906
+ }
1907
+
1908
+ if (priceValue && priceValue > 0) {
1909
+ console.log(tag, 'detected priceUsd from balance:', priceValue);
1910
+ assetInfo.priceUsd = priceValue;
1911
+ }
1912
+ }
1913
+
1914
+ // Override with fresh price if we got one from the API
1915
+ if (freshPriceUsd && freshPriceUsd > 0) {
1916
+ assetInfo.priceUsd = freshPriceUsd;
1917
+ console.log(tag, '✅ Using fresh market price:', freshPriceUsd);
1918
+
1919
+ // Aggregate all balances for this asset (critical for UTXO chains with multiple xpubs)
1920
+ let totalBalance = 0;
1921
+ let totalValueUsd = 0;
1922
+
1923
+ console.log(tag, `Found ${matchingBalances.length} balance entries for ${asset.caip}`);
1924
+ for (const balanceEntry of matchingBalances) {
1925
+ const balance = parseFloat(balanceEntry.balance) || 0;
1926
+ const valueUsd = parseFloat(balanceEntry.valueUsd) || 0;
1927
+ totalBalance += balance;
1928
+ totalValueUsd += valueUsd;
1929
+ console.log(tag, ` Balance entry: ${balance} (${valueUsd} USD)`);
1930
+ }
1931
+
1932
+ assetInfo.balance = totalBalance.toString();
1933
+ assetInfo.valueUsd = totalValueUsd.toFixed(2);
1934
+ console.log(tag, `Aggregated balance: ${totalBalance} (${totalValueUsd.toFixed(2)} USD)`);
1935
+ }
1936
+
1937
+ // Filter balances and pubkeys for this asset
1938
+ const assetBalances = this.balances.filter((b) => b.caip === asset.caip);
1939
+ const assetPubkeys = this.pubkeys.filter(
1940
+ (p) =>
1941
+ (p.networks &&
1942
+ Array.isArray(p.networks) &&
1943
+ p.networks.includes(caipToNetworkId(asset.caip))) ||
1944
+ (caipToNetworkId(asset.caip).includes('eip155') &&
1945
+ p.networks &&
1946
+ Array.isArray(p.networks) &&
1947
+ p.networks.some((n) => n.startsWith('eip155'))),
1948
+ );
1949
+
1950
+ // Combine the user-provided asset with any additional info we have
1951
+ // IMPORTANT: Don't let a 0 priceUsd from input override a valid price from balance
1952
+ const finalAssetContext = {
1953
+ ...assetInfo,
1954
+ ...asset,
1955
+ pubkeys: assetPubkeys,
1956
+ balances: assetBalances,
1957
+ };
1958
+
1959
+ // If input has priceUsd of 0 but we found a valid price from balance, use the balance price
1960
+ if ((!asset.priceUsd || asset.priceUsd === 0) && assetInfo.priceUsd && assetInfo.priceUsd > 0) {
1961
+ finalAssetContext.priceUsd = assetInfo.priceUsd;
1962
+ }
1963
+
1964
+ // Update all matching balances with the fresh price
1965
+ if (freshPriceUsd && freshPriceUsd > 0) {
1966
+ for (const balance of assetBalances) {
1967
+ balance.price = freshPriceUsd;
1968
+ balance.priceUsd = freshPriceUsd;
1969
+ // Recalculate valueUsd with fresh price
1970
+ const balanceAmount = parseFloat(balance.balance || 0);
1971
+ balance.valueUsd = (balanceAmount * freshPriceUsd).toString();
1972
+ }
1973
+ console.log(tag, 'Updated all balances with fresh price data');
1974
+ }
1975
+
1976
+ this.assetContext = finalAssetContext;
1977
+
1978
+ // For tokens, we need to also set the native gas balance and symbol
1979
+ if (
1980
+ asset.isToken ||
1981
+ asset.type === 'token' ||
1982
+ assetInfo.isToken ||
1983
+ assetInfo.type === 'token'
1984
+ ) {
1985
+ // Get the native asset for this network
1986
+ const networkId = asset.networkId || assetInfo.networkId;
1987
+
1988
+ // Determine the native gas symbol based on the network
1989
+ let nativeSymbol = 'GAS'; // default fallback
1990
+ let nativeCaip = '';
1991
+
1992
+ //TODO removeme
1993
+ if (networkId.includes('mayachain')) {
1994
+ nativeSymbol = 'CACAO';
1995
+ nativeCaip = 'cosmos:mayachain-mainnet-v1/slip44:931';
1996
+ } else if (networkId.includes('thorchain')) {
1997
+ nativeSymbol = 'RUNE';
1998
+ nativeCaip = 'cosmos:thorchain-mainnet-v1/slip44:931';
1999
+ } else if (networkId.includes('cosmoshub')) {
2000
+ nativeSymbol = 'ATOM';
2001
+ nativeCaip = 'cosmos:cosmoshub-4/slip44:118';
2002
+ } else if (networkId.includes('osmosis')) {
2003
+ nativeSymbol = 'OSMO';
2004
+ nativeCaip = 'cosmos:osmosis-1/slip44:118';
2005
+ } else if (networkId.includes('eip155:1')) {
2006
+ nativeSymbol = 'ETH';
2007
+ nativeCaip = 'eip155:1/slip44:60';
2008
+ } else if (networkId.includes('eip155:137')) {
2009
+ nativeSymbol = 'MATIC';
2010
+ nativeCaip = 'eip155:137/slip44:60';
2011
+ } else if (networkId.includes('eip155:56')) {
2012
+ nativeSymbol = 'BNB';
2013
+ nativeCaip = 'eip155:56/slip44:60';
2014
+ } else if (networkId.includes('eip155:43114')) {
2015
+ nativeSymbol = 'AVAX';
2016
+ nativeCaip = 'eip155:43114/slip44:60';
2017
+ }
2018
+
2019
+ // Set the native symbol
2020
+ this.assetContext.nativeSymbol = nativeSymbol;
2021
+
2022
+ // Try to find the native balance
2023
+ if (nativeCaip) {
2024
+ const nativeBalance = this.balances.find((b) => b.caip === nativeCaip);
2025
+ if (nativeBalance) {
2026
+ this.assetContext.nativeBalance = nativeBalance.balance || '0';
2027
+ } else {
2028
+ this.assetContext.nativeBalance = '0';
2029
+ }
2030
+ }
2031
+ }
2032
+
2033
+ // Set blockchain context based on asset
2034
+ if (asset.caip) {
2035
+ this.blockchainContext = caipToNetworkId(asset.caip);
2036
+ } else if (asset.networkId) {
2037
+ this.blockchainContext = asset.networkId;
2038
+ }
2039
+
2040
+ // Auto-set pubkey context for this asset's network
2041
+ // Use the first matching pubkey from assetPubkeys we already filtered
2042
+ if (assetPubkeys && assetPubkeys.length > 0) {
2043
+ this.pubkeyContext = assetPubkeys[0];
2044
+ // Also set it on keepKeySdk so tx builders can access it
2045
+ if (this.keepKeySdk) {
2046
+ this.keepKeySdk.pubkeyContext = assetPubkeys[0];
2047
+ }
2048
+ console.log(
2049
+ tag,
2050
+ 'Auto-set pubkey context for network:',
2051
+ this.pubkeyContext.address || this.pubkeyContext.pubkey,
2052
+ );
2053
+ }
2054
+
2055
+ this.events.emit('SET_ASSET_CONTEXT', this.assetContext);
2056
+ return this.assetContext;
2057
+ } catch (e) {
2058
+ console.error(tag, 'e: ', e);
2059
+ throw e;
2060
+ }
2061
+ };
2062
+ this.setPubkeyContext = async function (pubkey?: any) {
2063
+ let tag = `${TAG} | setPubkeyContext | `;
2064
+ try {
2065
+ if (!pubkey) throw Error('pubkey is required');
2066
+ if (!pubkey.pubkey && !pubkey.address)
2067
+ throw Error('invalid pubkey: missing pubkey or address');
2068
+
2069
+ // Validate pubkey exists in our pubkeys array
2070
+ const exists = this.pubkeys.some(
2071
+ (pk: any) =>
2072
+ pk.pubkey === pubkey.pubkey ||
2073
+ pk.address === pubkey.address ||
2074
+ pk.pubkey === pubkey.address,
2075
+ );
2076
+
2077
+ if (!exists) {
2078
+ console.warn(tag, 'Pubkey not found in current pubkeys array');
2079
+ }
2080
+
2081
+ /*
2082
+ Pubkey context is what FROM address we use in a tx
2083
+ Example
2084
+ ethereum account 0/1/2
2085
+ */
2086
+ this.pubkeyContext = pubkey;
2087
+ // Also set it on keepKeySdk so tx builders can access it
2088
+ if (this.keepKeySdk) {
2089
+ this.keepKeySdk.pubkeyContext = pubkey;
2090
+ }
2091
+ console.log(
2092
+ tag,
2093
+ 'Pubkey context set to:',
2094
+ pubkey.address || pubkey.pubkey,
2095
+ 'note:',
2096
+ pubkey.note,
2097
+ );
2098
+
2099
+ return true;
2100
+ } catch (e) {
2101
+ console.error(tag, 'e: ', e);
2102
+ throw e;
2103
+ }
2104
+ };
2105
+ this.setOutboundAssetContext = async function (asset?: any): Promise<any> {
2106
+ const tag = `${TAG} | setOutputAssetContext | `;
2107
+ try {
2108
+ console.log(tag, '0. asset: ', asset);
2109
+ // Accept null
2110
+ if (!asset) {
2111
+ this.outboundAssetContext = null;
2112
+ return;
2113
+ }
2114
+
2115
+ console.log(tag, '1 asset: ', asset);
2116
+
2117
+ if (!asset.caip) throw Error('Invalid Asset! missing caip!');
2118
+ if (!asset.networkId) asset.networkId = caipToNetworkId(asset.caip);
2119
+
2120
+ console.log(tag, 'networkId: ', asset.networkId);
2121
+ console.log(tag, 'this.pubkeys: ', this.pubkeys);
2122
+ //get a pubkey for network (handle EVM wildcard)
2123
+ const pubkey = this.pubkeys.find((p) => {
2124
+ if (!p.networks || !Array.isArray(p.networks)) return false;
2125
+ // Exact match
2126
+ if (p.networks.includes(asset.networkId)) return true;
2127
+ // For EVM chains, check if pubkey has eip155:* wildcard
2128
+ if (asset.networkId.startsWith('eip155:') && p.networks.includes('eip155:*')) return true;
2129
+ return false;
2130
+ });
2131
+ if (!pubkey) throw Error('Invalid network! missing pubkey for network! ' + asset.networkId);
2132
+
2133
+ // ALWAYS fetch fresh market price for the asset
2134
+ let freshPriceUsd = 0;
2135
+ try {
2136
+ // Validate CAIP before calling API
2137
+ if (!asset.caip || typeof asset.caip !== 'string' || !asset.caip.includes(':')) {
2138
+ console.warn(tag, 'Invalid or missing CAIP, skipping market price fetch:', asset.caip);
2139
+ } else {
2140
+ console.log(tag, 'Fetching fresh market price for:', asset.caip);
2141
+ const marketData = await this.pioneer.GetMarketInfo([asset.caip]);
2142
+ console.log(tag, 'Market data response:', marketData);
2143
+
2144
+ if (marketData && marketData.data && marketData.data.length > 0) {
2145
+ freshPriceUsd = marketData.data[0];
2146
+ console.log(tag, '✅ Fresh market price:', freshPriceUsd);
2147
+ } else {
2148
+ console.warn(tag, 'No market data returned for:', asset.caip);
2149
+ }
2150
+ }
2151
+ } catch (marketError) {
2152
+ console.error(tag, 'Error fetching market price:', marketError);
2153
+ // Continue without fresh price, will try to use cached data
2154
+ }
2155
+
2156
+ // Try to find the asset in the local assetsMap
2157
+ let assetInfo = this.assetsMap.get(asset.caip.toLowerCase());
2158
+ console.log(tag, 'assetInfo: ', assetInfo);
2159
+
2160
+ // If the asset is not found, create a placeholder object
2161
+ if (!assetInfo) {
2162
+ // Create a placeholder asset if it's not found in Pioneer or locally
2163
+ assetInfo = {
2164
+ caip: asset.caip.toLowerCase(),
2165
+ networkId: asset.networkId,
2166
+ symbol: asset.symbol || 'UNKNOWN',
2167
+ name: asset.name || 'Unknown Asset',
2168
+ icon: asset.icon || 'https://pioneers.dev/coins/ethereum.png',
2169
+ };
2170
+ }
2171
+
2172
+ // Look for price and balance information in balances
2173
+ // CRITICAL: For UTXO chains, we need to aggregate ALL balances across all xpubs
2174
+ const matchingBalances = this.balances.filter((b) => b.caip === asset.caip);
2175
+
2176
+ if (matchingBalances.length > 0) {
2177
+ // Use price from first balance entry (all should have same price)
2178
+ // Check for both priceUsd and price properties (different sources may use different names)
2179
+ let priceValue = matchingBalances[0].priceUsd || matchingBalances[0].price;
2180
+
2181
+ // If no price but we have valueUsd and balance, calculate the price
2182
+ if ((!priceValue || priceValue === 0) && matchingBalances[0].valueUsd && matchingBalances[0].balance) {
2183
+ const balance = parseFloat(matchingBalances[0].balance);
2184
+ const valueUsd = parseFloat(matchingBalances[0].valueUsd);
2185
+ if (balance > 0 && valueUsd > 0) {
2186
+ priceValue = valueUsd / balance;
2187
+ console.log(tag, 'calculated priceUsd from valueUsd/balance:', priceValue);
2188
+ }
2189
+ }
2190
+
2191
+ if (priceValue && priceValue > 0) {
2192
+ console.log(tag, 'detected priceUsd from balance:', priceValue);
2193
+ assetInfo.priceUsd = priceValue;
2194
+ }
2195
+ }
2196
+
2197
+ // Override with fresh price if we got one from the API
2198
+ if (freshPriceUsd && freshPriceUsd > 0) {
2199
+ assetInfo.priceUsd = freshPriceUsd;
2200
+ console.log(tag, '✅ Using fresh market price:', freshPriceUsd);
2201
+
2202
+ // Aggregate all balances for this asset (critical for UTXO chains with multiple xpubs)
2203
+ let totalBalance = 0;
2204
+ let totalValueUsd = 0;
2205
+
2206
+ console.log(tag, `Found ${matchingBalances.length} balance entries for ${asset.caip}`);
2207
+ for (const balanceEntry of matchingBalances) {
2208
+ const balance = parseFloat(balanceEntry.balance) || 0;
2209
+ const valueUsd = parseFloat(balanceEntry.valueUsd) || 0;
2210
+ totalBalance += balance;
2211
+ totalValueUsd += valueUsd;
2212
+ console.log(tag, ` Balance entry: ${balance} (${valueUsd} USD)`);
2213
+ }
2214
+
2215
+ assetInfo.balance = totalBalance.toString();
2216
+ assetInfo.valueUsd = totalValueUsd.toFixed(2);
2217
+ console.log(tag, `Aggregated balance: ${totalBalance} (${totalValueUsd.toFixed(2)} USD)`);
2218
+ }
2219
+
2220
+ console.log(tag, 'CHECKPOINT 1');
2221
+
2222
+ // Combine the user-provided asset with any additional info we have
2223
+ this.outboundAssetContext = { ...assetInfo, ...asset, ...pubkey };
2224
+
2225
+ console.log(tag, 'CHECKPOINT 3');
2226
+ console.log(tag, 'outboundAssetContext: assetInfo: ', assetInfo);
2227
+
2228
+ // Set outbound blockchain context based on asset
2229
+ if (asset.caip) {
2230
+ this.outboundBlockchainContext = caipToNetworkId(asset.caip);
2231
+ } else if (asset.networkId) {
2232
+ this.outboundBlockchainContext = asset.networkId;
2233
+ }
2234
+
2235
+ console.log(tag, 'CHECKPOINT 4');
2236
+
2237
+ this.events.emit('SET_OUTBOUND_ASSET_CONTEXT', this.outboundAssetContext);
2238
+ return this.outboundAssetContext;
2239
+ } catch (e) {
2240
+ console.error(tag, 'e: ', e);
2241
+ throw e;
2242
+ }
2243
+ };
2244
+ }
2245
+ }
2246
+
2247
+ // Export fee-related types for consumers
2248
+ export type { NormalizedFeeRates, FeeLevel, FeeEstimate } from './fees/index.js';
2249
+
2250
+ export default SDK;