@pioneer-platform/pioneer-sdk 4.20.3 → 4.20.4

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