@pioneer-platform/pioneer-sdk 8.15.14 → 8.15.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import EventEmitter from 'events';
8
8
 
9
9
  import { getCharts } from './charts/index.js';
10
10
  //internal
11
+ import { logger } from './utils/logger.js';
11
12
  import { getPubkey } from './getPubkey.js';
12
13
  import { optimizedGetPubkeys } from './kkapi-batch-client.js';
13
14
  import { OfflineClient } from './offline-client.js';
@@ -21,43 +22,47 @@ import { detectKkApiAvailability } from './utils/kkapi-detection.js';
21
22
  import { formatTime } from './utils/format-time.js';
22
23
  import { buildDashboardFromBalances } from './utils/build-dashboard.js';
23
24
  import { syncMarket } from './utils/sync-market.js';
25
+ import {
26
+ getPubkeyKey,
27
+ deduplicatePubkeys,
28
+ validatePubkey,
29
+ findPubkeysForNetwork,
30
+ findPubkeyForNetwork,
31
+ validatePubkeysForNetwork,
32
+ filterPubkeysForAsset
33
+ } from './utils/pubkey-management.js';
34
+ import {
35
+ isCacheDataValid,
36
+ buildDashboardFromPortfolioData,
37
+ fetchMarketPrice,
38
+ extractPriceFromBalances,
39
+ aggregateBalances,
40
+ updateBalancesWithPrice
41
+ } from './utils/portfolio-helpers.js';
42
+ import {
43
+ createInitialSyncState,
44
+ createCacheSyncState,
45
+ createFreshSyncState,
46
+ type SyncState,
47
+ resolveAssetInfo
48
+ } from './utils/sync-state.js';
49
+ import { getMaxSendableAmount } from './utils/fee-reserves.js';
50
+ import {
51
+ matchesNetwork,
52
+ normalizeNetworkId,
53
+ validatePubkeysNetworks
54
+ } from './utils/network-helpers.js';
55
+ import {
56
+ buildAssetQuery,
57
+ logQueryDiagnostics,
58
+ enrichBalancesWithAssetInfo
59
+ } from './utils/portfolio-helpers.js';
60
+ import { ensurePathsForBlockchains } from './utils/path-discovery.js';
61
+ import { syncPubkeysForBlockchains } from './utils/pubkey-sync.js';
62
+ // import { ASSET_COLORS } from './constants/asset-colors.js'; // TODO: Create this file or remove usage
24
63
 
25
64
  const TAG = ' | Pioneer-sdk | ';
26
65
 
27
- // Color mappings for major assets (until pioneer-discovery includes colors)
28
- const ASSET_COLORS: Record<string, string> = {
29
- // Bitcoin
30
- 'bip122:000000000019d6689c085ae165831e93/slip44:0': '#FF9800',
31
- // Ethereum
32
- 'eip155:1/slip44:60': '#627EEA',
33
- // Polygon
34
- 'eip155:137/slip44:60': '#8247E5',
35
- // Base
36
- 'eip155:8453/slip44:60': '#0052FF',
37
- // BNB
38
- 'eip155:56/slip44:60': '#F3BA2F',
39
- // Litecoin
40
- 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2': '#BFBBBB',
41
- // Dogecoin
42
- 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3': '#C2A633',
43
- // Bitcoin Cash
44
- 'bip122:000000000000000000651ef99cb9fcbe/slip44:145': '#8DC351',
45
- // Dash
46
- 'bip122:000007d91d1254d60e2dd1ae58038307/slip44:5': '#008CE7',
47
- // DigiByte
48
- 'bip122:4da631f2ac1bed857bd968c67c913978/slip44:20': '#006AD2',
49
- // Cosmos
50
- 'cosmos:cosmoshub-4/slip44:118': '#2E3148',
51
- // Osmosis
52
- 'cosmos:osmosis-1/slip44:118': '#9B1FD7',
53
- // XRP
54
- 'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144': '#23292F',
55
- // Maya/CACAO
56
- 'cosmos:mayachain-mainnet-v1/slip44:931': '#00D4AA',
57
- // Thorchain/RUNE
58
- 'cosmos:thorchain-mainnet-v1/slip44:931': '#00CCFF',
59
- };
60
-
61
66
  export interface PioneerSDKConfig {
62
67
  appName: string;
63
68
  appIcon: string;
@@ -84,18 +89,6 @@ export interface PioneerSDKConfig {
84
89
  skipKeeperEndpoint?: boolean; // Skip vault endpoint detection
85
90
  }
86
91
 
87
- export interface SyncState {
88
- isSynced: boolean; // Has fresh data from full sync?
89
- isInitialSync: boolean; // First sync (no cache)?
90
- cacheAge: number; // Seconds since last cache update
91
- syncProgress: number; // 0-100 percentage of completion
92
- syncedChains: number; // Number of chains synced
93
- totalChains: number; // Total chains to sync
94
- lastSyncTime: number | null; // Timestamp of last successful sync
95
- syncSource: 'cache' | 'fresh' | 'none'; // Where data came from
96
- }
97
-
98
-
99
92
  export class SDK {
100
93
  public status: string;
101
94
  public username: string;
@@ -159,18 +152,8 @@ export class SDK {
159
152
  public appName: string;
160
153
  public appIcon: any;
161
154
  public init: (walletsVerbose: any, setup: any) => Promise<any>;
162
- // public initOffline: () => Promise<any>;
163
- // public backgroundSync: () => Promise<void>;
164
155
  public getUnifiedPortfolio: () => Promise<any>;
165
156
  public offlineClient: OfflineClient | null;
166
- // public verifyWallet: () => Promise<void>;
167
- public convertVaultPubkeysToPioneerFormat: (vaultPubkeys: any[]) => any[];
168
- // public deriveNetworksFromPath: (path: string) => string[];
169
- // public getAddress: (options: {
170
- // networkId?: string;
171
- // showDevice?: boolean;
172
- // path?: any;
173
- // }) => Promise<string>;
174
157
  public app: {
175
158
  getAddress: (options: {
176
159
  networkId?: string;
@@ -272,16 +255,7 @@ export class SDK {
272
255
  this.contextType = '';
273
256
 
274
257
  // Initialize sync state
275
- this.syncState = {
276
- isSynced: false,
277
- isInitialSync: true,
278
- cacheAge: 0,
279
- syncProgress: 0,
280
- syncedChains: 0,
281
- totalChains: this.blockchains.length,
282
- lastSyncTime: null,
283
- syncSource: 'none'
284
- };
258
+ this.syncState = createInitialSyncState(this.blockchains.length);
285
259
 
286
260
  // Initialize offline client if offline-first mode is enabled
287
261
  this.offlineClient = config.offlineFirst
@@ -314,40 +288,17 @@ export class SDK {
314
288
  });
315
289
  };
316
290
 
317
- // Helper method to generate unique key for a pubkey
318
- this.getPubkeyKey = (pubkey: any): string => {
319
- return `${pubkey.pubkey}_${pubkey.pathMaster}`;
320
- };
321
-
322
- // Helper method to deduplicate pubkeys array
323
- this.deduplicatePubkeys = (pubkeys: any[]): any[] => {
324
- const seen = new Set<string>();
325
- const deduped = pubkeys.filter((pubkey) => {
326
- const key = this.getPubkeyKey(pubkey);
327
- if (seen.has(key)) {
328
- return false;
329
- }
330
- seen.add(key);
331
- return true;
332
- });
333
- return deduped;
334
- };
291
+ // Pubkey helper methods using utilities
292
+ this.getPubkeyKey = getPubkeyKey;
293
+ this.deduplicatePubkeys = deduplicatePubkeys;
335
294
 
336
295
  // Helper method to validate and add a single pubkey
337
296
  this.addPubkey = (pubkey: any): boolean => {
338
- // Validate pubkey has required fields
339
- if (!pubkey.pubkey || !pubkey.pathMaster) {
340
- return false;
341
- }
342
-
343
- const key = this.getPubkeyKey(pubkey);
297
+ if (!validatePubkey(pubkey)) return false;
344
298
 
345
- // Check if already exists
346
- if (this.pubkeySet.has(key)) {
347
- return false;
348
- }
299
+ const key = getPubkeyKey(pubkey);
300
+ if (this.pubkeySet.has(key)) return false;
349
301
 
350
- // Add to both array and set
351
302
  this.pubkeys.push(pubkey);
352
303
  this.pubkeySet.add(key);
353
304
  return true;
@@ -378,20 +329,6 @@ export class SDK {
378
329
  this.pubkeys = [];
379
330
  this.pubkeySet.clear();
380
331
  }
381
-
382
- // View-only mode helper methods
383
- this.isViewOnlyMode = (): boolean => {
384
- return this.viewOnlyMode;
385
- };
386
-
387
- this.canSignTransactions = (): boolean => {
388
- return !this.viewOnlyMode && !!this.keepKeySdk;
389
- };
390
-
391
- this.isVaultAvailable = (): boolean => {
392
- return !!this.keepkeyEndpoint && this.keepkeyEndpoint.isAvailable;
393
- };
394
-
395
332
  // Fast portfolio loading from kkapi:// cache
396
333
  this.getUnifiedPortfolio = async function () {
397
334
  const tag = `${TAG} | getUnifiedPortfolio | `;
@@ -439,10 +376,8 @@ export class SDK {
439
376
 
440
377
  // Update pubkeys from cache
441
378
  if (portfolioData.pubkeys && portfolioData.pubkeys.length > 0) {
442
- // Convert vault pubkey format to pioneer-sdk format
443
- const convertedPubkeys = this.convertVaultPubkeysToPioneerFormat(portfolioData.pubkeys);
444
379
  // Use setPubkeys to ensure deduplication
445
- this.setPubkeys(convertedPubkeys);
380
+ this.setPubkeys(portfolioData.pubkeys);
446
381
  this.events.emit('SET_PUBKEYS', this.pubkeys);
447
382
  }
448
383
 
@@ -459,75 +394,12 @@ export class SDK {
459
394
  }
460
395
 
461
396
  // Validate cache data before using it
462
- const isCacheDataValid = (portfolioData: any): boolean => {
463
- // Check if networks data is reasonable (should be < 50 networks, not thousands)
464
- if (!portfolioData.networks || !Array.isArray(portfolioData.networks)) {
465
- console.warn('[CACHE VALIDATION] Networks is not an array');
466
- return false;
467
- }
468
-
469
- if (portfolioData.networks.length > 50) {
470
- console.error(
471
- `[CACHE VALIDATION] CORRUPTED: ${portfolioData.networks.length} networks (should be < 50)`,
472
- );
473
- return false;
474
- }
475
-
476
- // Check if at least some networks have required fields
477
- const validNetworks = portfolioData.networks.filter(
478
- (n: any) => n.networkId && n.totalValueUsd !== undefined && n.gasAssetSymbol,
479
- );
480
-
481
- if (validNetworks.length === 0 && portfolioData.networks.length > 0) {
482
- console.error('[CACHE VALIDATION] CORRUPTED: No networks have required fields');
483
- return false;
484
- }
485
-
486
- console.log(
487
- `[CACHE VALIDATION] Found ${portfolioData.networks.length} networks, ${validNetworks.length} valid`,
488
- );
489
- return true;
490
- };
491
-
492
- // Only use cache data if it's valid
493
397
  if (isCacheDataValid(portfolioData)) {
494
- const dashboardData = {
495
- totalValueUsd: portfolioData.totalValueUsd,
496
- pairedDevices: portfolioData.pairedDevices,
497
- devices: portfolioData.devices || [],
498
- networks: portfolioData.networks || [],
499
- assets: portfolioData.assets || [],
500
- statistics: portfolioData.statistics || {},
501
- cached: portfolioData.cached,
502
- lastUpdated: portfolioData.lastUpdated,
503
- cacheAge: portfolioData.lastUpdated
504
- ? Math.floor((Date.now() - portfolioData.lastUpdated) / 1000)
505
- : 0,
506
- networkPercentages:
507
- portfolioData.networks?.map((network: any) => ({
508
- networkId: network.network_id || network.networkId,
509
- percentage: network.percentage || 0,
510
- })) || [],
511
- };
512
-
513
- this.dashboard = dashboardData;
398
+ this.dashboard = buildDashboardFromPortfolioData(portfolioData);
514
399
  this.events.emit('SET_DASHBOARD', this.dashboard);
515
400
 
516
401
  // Update sync state - cache hit
517
- const cacheAge = portfolioData.lastUpdated
518
- ? Math.floor((Date.now() - portfolioData.lastUpdated) / 1000)
519
- : 0;
520
-
521
- this.syncState = {
522
- isSynced: true,
523
- isInitialSync: false,
524
- cacheAge,
525
- syncProgress: 100,
526
- syncedChains: this.blockchains.length,
527
- totalChains: this.blockchains.length,
528
- lastSyncTime: portfolioData.lastUpdated || Date.now(),
529
- syncSource: 'cache'
530
- };
402
+ this.syncState = createCacheSyncState(portfolioData.lastUpdated, this.blockchains.length);
531
403
  this.events.emit('SYNC_STATE_CHANGED', this.syncState);
532
404
  } else {
533
405
  console.warn(
@@ -784,249 +656,106 @@ export class SDK {
784
656
  this.buildDashboardFromBalances = function () {
785
657
  return buildDashboardFromBalances(this.balances, this.blockchains, this.assetsMap);
786
658
  };
787
- // ✅ PHASE 3: Infer asset type from CAIP structure (fallback helper)
788
- this.inferTypeFromCaip = function (caip: string): string {
789
- if (caip.includes('/slip44:')) return 'native';
790
- if (caip.includes('/erc20:') || caip.includes('/bep20:') || caip.includes('/spl:')) return 'token';
791
- if (caip.includes('/denom:')) return 'native'; // Cosmos denoms
792
- return 'unknown';
793
- };
794
659
  this.syncMarket = async function () {
795
660
  return syncMarket(this.balances, this.pioneer);
796
661
  };
797
662
  this.sync = async function () {
798
663
  const tag = `${TAG} | sync | `;
664
+ const log = logger;
665
+
799
666
  try {
800
- // Helper to check network match with EVM wildcard support (works for both paths and pubkeys)
801
- const matchesNetwork = (item: any, networkId: string) => {
802
- if (!item.networks || !Array.isArray(item.networks)) return false;
803
- if (item.networks.includes(networkId)) return true;
804
- if (networkId.startsWith('eip155:') && item.networks.includes('eip155:*')) return true;
805
- return false;
667
+ // Update sync state: starting
668
+ this.syncState = {
669
+ ...createInitialSyncState(this.blockchains.length),
670
+ syncProgress: 10
806
671
  };
672
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
807
673
 
808
- //at least 1 path per chain
674
+ // Step 1: Initial pubkey fetch
675
+ log.info(tag, 'Fetching initial pubkeys...');
809
676
  await this.getPubkeys();
810
- for (let i = 0; i < this.blockchains.length; i++) {
811
- let networkId = this.blockchains[i];
812
- if (networkId.indexOf('eip155:') >= 0) networkId = 'eip155:*';
813
-
814
- let paths = this.paths.filter((path) => matchesNetwork(path, networkId));
815
- if (paths.length === 0) {
816
- //get paths for chain
817
- let paths = getPaths([networkId]);
818
- if (!paths || paths.length === 0) throw Error('Unable to find paths for: ' + networkId);
819
- //add to paths
820
- this.paths = this.paths.concat(paths);
821
- }
822
- }
823
677
 
824
- for (let i = 0; i < this.blockchains.length; i++) {
825
- let networkId = this.blockchains[i];
826
- if (networkId.indexOf('eip155:') >= 0) networkId = 'eip155:*';
827
- const pathsForChain = this.paths.filter((path) => matchesNetwork(path, networkId));
828
- if (!pathsForChain || pathsForChain.length === 0)
829
- throw Error('No paths found for blockchain: ' + networkId);
830
-
831
- for (let j = 0; j < pathsForChain.length; j++) {
832
- const path = pathsForChain[j];
833
- let pathBip32 = addressNListToBIP32(path.addressNListMaster);
834
- let pubkey = this.pubkeys.find((pubkey) => pubkey.pathMaster === pathBip32);
835
- if (!pubkey) {
836
- const pubkey = await getPubkey(
837
- this.blockchains[i],
838
- path,
839
- this.keepKeySdk,
840
- this.context,
841
- );
842
- if (!pubkey) throw Error('Unable to get pubkey for network+ ' + networkId);
843
- // Use addPubkey method for proper duplicate checking
844
- this.addPubkey(pubkey);
845
- }
846
- }
847
- }
678
+ this.syncState.syncProgress = 20;
679
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
680
+
681
+ // Step 2: Ensure paths for all blockchains
682
+ log.info(tag, 'Discovering paths for blockchains...');
683
+ this.paths = await ensurePathsForBlockchains(
684
+ this.blockchains,
685
+ this.paths,
686
+ tag
687
+ );
688
+
689
+ this.syncState.syncProgress = 30;
690
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
691
+
692
+ // Step 3: Sync pubkeys for all paths
693
+ log.info(tag, 'Synchronizing pubkeys...');
694
+ await syncPubkeysForBlockchains(
695
+ this.blockchains,
696
+ this.paths,
697
+ this.pubkeys,
698
+ this.keepKeySdk,
699
+ this.context,
700
+ getPubkey,
701
+ (pubkey) => this.addPubkey(pubkey),
702
+ tag
703
+ );
704
+
705
+ this.syncState.syncProgress = 50;
706
+ this.syncState.syncedChains = this.blockchains.length;
707
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
708
+
709
+ // Step 4: Fetch balances
710
+ log.info(tag, 'Fetching balances...');
848
711
  await this.getBalances();
849
712
 
850
- // Load charts (portfolio + ERC20 tokens + staking positions)
851
- // This is CRITICAL for discovering user's token balances (USDT, USDC, etc.)
852
- console.log(tag, 'Loading charts (tokens + portfolio)...');
713
+ this.syncState.syncProgress = 70;
714
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
715
+
716
+ // Step 5: Load charts (critical for token discovery)
717
+ log.info(tag, 'Loading charts (tokens + portfolio)...');
853
718
  await this.getCharts();
854
- console.log(tag, `Charts loaded. Total balances: ${this.balances.length}`);
719
+ log.info(tag, `Charts loaded. Total balances: ${this.balances.length}`);
855
720
 
856
- // Sync market prices for all balances
857
- await this.syncMarket();
721
+ this.syncState.syncProgress = 85;
722
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
858
723
 
859
- //we should be fully synced so lets make the dashboard
860
- const dashboardData: {
861
- networks: {
862
- networkId: string;
863
- totalValueUsd: number;
864
- gasAssetCaip: string | null;
865
- gasAssetSymbol: string | null;
866
- icon: string | null;
867
- color: string | null;
868
- totalNativeBalance: string;
869
- }[];
870
- totalValueUsd: number;
871
- networkPercentages: { networkId: string; percentage: number }[];
872
- } = {
873
- networks: [],
874
- totalValueUsd: 0,
875
- networkPercentages: [],
876
- };
724
+ // Step 6: Sync market prices
725
+ log.info(tag, 'Syncing market prices...');
726
+ await this.syncMarket();
877
727
 
878
- let totalPortfolioValue = 0;
879
- const networksTemp: {
880
- networkId: string;
881
- totalValueUsd: number;
882
- gasAssetCaip: string | null;
883
- gasAssetSymbol: string | null;
884
- icon: string | null;
885
- color: string | null;
886
- totalNativeBalance: string;
887
- }[] = [];
888
-
889
- // Deduplicate blockchains before calculation to prevent double-counting
890
- const uniqueBlockchains = [...new Set(this.blockchains)];
891
-
892
- // Calculate totals for each blockchain
893
- for (const blockchain of uniqueBlockchains) {
894
- const filteredBalances = this.balances.filter((b) => {
895
- const networkId = caipToNetworkId(b.caip);
896
- return (
897
- networkId === blockchain ||
898
- (blockchain === 'eip155:*' && networkId.startsWith('eip155:'))
899
- );
900
- });
728
+ this.syncState.syncProgress = 95;
729
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
901
730
 
902
- // Deduplicate balances based on caip + pubkey combination
903
- const balanceMap = new Map();
904
-
905
- // Special handling for Bitcoin to work around API bug
906
- const isBitcoin = blockchain.includes('bip122:000000000019d6689c085ae165831e93');
907
- if (isBitcoin) {
908
- // Group Bitcoin balances by value to detect duplicates
909
- const bitcoinByValue = new Map();
910
- filteredBalances.forEach((balance) => {
911
- const valueKey = `${balance.balance}_${balance.valueUsd}`;
912
- if (!bitcoinByValue.has(valueKey)) {
913
- bitcoinByValue.set(valueKey, []);
914
- }
915
- bitcoinByValue.get(valueKey).push(balance);
916
- });
731
+ // Step 7: Build dashboard
732
+ log.info(tag, 'Building dashboard...');
733
+ this.dashboard = buildDashboardFromBalances(
734
+ this.balances,
735
+ [...new Set(this.blockchains)], // Deduplicate blockchains
736
+ this.assetsMap
737
+ );
917
738
 
918
- // Check if all three address types have the same non-zero balance (API bug)
919
- for (const [valueKey, balances] of bitcoinByValue.entries()) {
920
- if (balances.length === 3 && parseFloat(balances[0].valueUsd || '0') > 0) {
921
- // Keep only the xpub (or first one if no xpub)
922
- const xpubBalance =
923
- balances.find((b) => b.pubkey?.startsWith('xpub')) || balances[0];
924
- const key = `${xpubBalance.caip}_${xpubBalance.pubkey || 'default'}`;
925
- balanceMap.set(key, xpubBalance);
926
- } else {
927
- // Add all balances normally
928
- balances.forEach((balance) => {
929
- const key = `${balance.caip}_${balance.pubkey || 'default'}`;
930
- balanceMap.set(key, balance);
931
- });
932
- }
933
- }
934
- } else {
935
- // Standard deduplication for non-Bitcoin networks
936
- filteredBalances.forEach((balance) => {
937
- const key = `${balance.caip}_${balance.pubkey || 'default'}`;
938
- // Only keep the first occurrence or the one with higher value
939
- if (
940
- !balanceMap.has(key) ||
941
- parseFloat(balance.valueUsd || '0') >
942
- parseFloat(balanceMap.get(key).valueUsd || '0')
943
- ) {
944
- balanceMap.set(key, balance);
945
- }
946
- });
947
- }
739
+ // Step 8: Update sync state - complete
740
+ this.syncState = createFreshSyncState(this.blockchains.length);
741
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
742
+ this.events.emit('SYNC_COMPLETE', this.syncState);
948
743
 
949
- const networkBalances = Array.from(balanceMap.values());
950
-
951
- // Ensure we're working with numbers for calculations
952
- const networkTotal = networkBalances.reduce((sum, balance, idx) => {
953
- const valueUsd =
954
- typeof balance.valueUsd === 'string'
955
- ? parseFloat(balance.valueUsd)
956
- : balance.valueUsd || 0;
957
-
958
- return sum + valueUsd;
959
- }, 0);
960
-
961
- // Get native asset for this blockchain
962
- const nativeAssetCaip = networkIdToCaip(blockchain);
963
- const gasAsset = networkBalances.find((b) => b.caip === nativeAssetCaip);
964
-
965
- // Calculate total native balance (sum of all balances for the native asset)
966
- const totalNativeBalance = networkBalances
967
- .filter((b) => b.caip === nativeAssetCaip)
968
- .reduce((sum, balance) => {
969
- const balanceNum =
970
- typeof balance.balance === 'string'
971
- ? parseFloat(balance.balance)
972
- : balance.balance || 0;
973
- return sum + balanceNum;
974
- }, 0)
975
- .toString();
976
-
977
- networksTemp.push({
978
- networkId: blockchain,
979
- totalValueUsd: networkTotal,
980
- gasAssetCaip: nativeAssetCaip || null,
981
- gasAssetSymbol: gasAsset?.symbol || null,
982
- icon: gasAsset?.icon || null,
983
- color: gasAsset?.color || null,
984
- totalNativeBalance,
985
- });
744
+ log.info(tag, '✅ Sync complete!');
745
+ return true;
986
746
 
987
- totalPortfolioValue += networkTotal;
988
- }
747
+ } catch (e) {
748
+ log.error(tag, 'Sync failed:', e);
989
749
 
990
- // Sort networks by USD value and assign to dashboard
991
- dashboardData.networks = networksTemp.sort((a, b) => b.totalValueUsd - a.totalValueUsd);
992
- dashboardData.totalValueUsd = totalPortfolioValue;
993
-
994
- // Calculate network percentages for pie chart
995
- dashboardData.networkPercentages = dashboardData.networks
996
- .map((network) => ({
997
- networkId: network.networkId,
998
- percentage:
999
- totalPortfolioValue > 0
1000
- ? Number(((network.totalValueUsd / totalPortfolioValue) * 100).toFixed(2))
1001
- : 0,
1002
- }))
1003
- .filter((entry) => entry.percentage > 0); // Remove zero percentages
1004
-
1005
- /* console.log('Bitcoin balances:', btcBalances.map(b => ({
1006
- pubkey: b.pubkey,
1007
- balance: b.balance,
1008
- valueUsd: b.valueUsd
1009
- }))); */
1010
-
1011
- this.dashboard = dashboardData;
1012
-
1013
- // Update sync state - fresh sync complete
750
+ // Update sync state with error
1014
751
  this.syncState = {
1015
- isSynced: true,
1016
- isInitialSync: false,
1017
- cacheAge: 0,
1018
- syncProgress: 100,
1019
- syncedChains: this.blockchains.length,
1020
- totalChains: this.blockchains.length,
1021
- lastSyncTime: Date.now(),
1022
- syncSource: 'fresh'
752
+ ...this.syncState,
753
+ isSynced: false,
754
+ syncProgress: 0
1023
755
  };
1024
756
  this.events.emit('SYNC_STATE_CHANGED', this.syncState);
1025
- this.events.emit('SYNC_COMPLETE', this.syncState);
757
+ this.events.emit('SYNC_ERROR', e);
1026
758
 
1027
- return true;
1028
- } catch (e) {
1029
- console.error(tag, 'Error in sync:', e);
1030
759
  throw e;
1031
760
  }
1032
761
  };
@@ -1213,16 +942,6 @@ export class SDK {
1213
942
 
1214
943
  //get quote
1215
944
  // Quote fetching logic
1216
- // Helper function to check if pubkey matches network (handles EVM wildcard)
1217
- const matchesNetwork = (pubkey: any, networkId: string) => {
1218
- if (!pubkey.networks || !Array.isArray(pubkey.networks)) return false;
1219
- // Exact match
1220
- if (pubkey.networks.includes(networkId)) return true;
1221
- // For EVM chains, check if pubkey has eip155:* wildcard
1222
- if (networkId.startsWith('eip155:') && pubkey.networks.includes('eip155:*')) return true;
1223
- return false;
1224
- };
1225
-
1226
945
  const pubkeys = this.pubkeys.filter((e: any) =>
1227
946
  matchesNetwork(e, this.assetContext.networkId),
1228
947
  );
@@ -1280,23 +999,12 @@ export class SDK {
1280
999
  ).toFixed(2);
1281
1000
  console.log(tag, `Updated assetContext balance to aggregated total: ${totalBalance}`);
1282
1001
 
1283
- // Fee reserves by network (conservative estimates)
1284
- // These match the pattern used in transfer functions
1285
- const feeReserves: any = {
1286
- 'bip122:000000000019d6689c085ae165831e93/slip44:0': 0.00005, // BTC
1287
- 'eip155:1/slip44:60': 0.001, // ETH
1288
- 'cosmos:thorchain-mainnet-v1/slip44:931': 0.02, // RUNE
1289
- 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3': 1, // DOGE
1290
- 'bip122:000007d91d1254d60e2dd1ae58038307/slip44:5': 0.001, // DASH
1291
- 'bip122:000000000000000000651ef99cb9fcbe/slip44:145': 0.0005, // BCH
1292
- };
1293
-
1294
- const reserve = feeReserves[swapPayload.caipIn] || 0.0001;
1295
- inputAmount = Math.max(0, totalBalance - reserve);
1002
+ // Calculate max sendable amount using centralized fee reserve utility
1003
+ inputAmount = getMaxSendableAmount(totalBalance, swapPayload.caipIn);
1296
1004
 
1297
1005
  console.log(
1298
1006
  tag,
1299
- `Using max amount for swap: ${inputAmount} (total balance: ${totalBalance}, reserve: ${reserve})`,
1007
+ `Using max amount for swap: ${inputAmount} (total balance: ${totalBalance})`,
1300
1008
  );
1301
1009
  } else {
1302
1010
  // Convert amount to number for type safety
@@ -1780,31 +1488,8 @@ export class SDK {
1780
1488
  }
1781
1489
  }
1782
1490
 
1783
- //add gas assets to map
1784
-
1785
- // Add missing MAYA token manually until it's added to assetData
1786
- const mayaTokenCaip = 'cosmos:mayachain-mainnet-v1/denom:maya';
1787
- if (!this.assetsMap.has(mayaTokenCaip)) {
1788
- const mayaToken = {
1789
- caip: mayaTokenCaip,
1790
- networkId: 'cosmos:mayachain-mainnet-v1',
1791
- chainId: 'mayachain-mainnet-v1',
1792
- symbol: 'MAYA',
1793
- name: 'Maya Token',
1794
- precision: 4,
1795
- decimals: 4,
1796
- color: '#00D4AA',
1797
- icon: 'https://pioneers.dev/coins/maya.png',
1798
- explorer: 'https://explorer.mayachain.info',
1799
- explorerAddressLink: 'https://explorer.mayachain.info/address/{{address}}',
1800
- explorerTxLink: 'https://explorer.mayachain.info/tx/{{txid}}',
1801
- type: 'token',
1802
- isToken: true,
1803
- denom: 'maya',
1804
- };
1805
- this.assetsMap.set(mayaTokenCaip, mayaToken);
1806
- console.log(tag, 'Added MAYA token to assetsMap');
1807
- }
1491
+ // All gas assets and tokens are now loaded from assetData
1492
+ // MAYA token is included in generatedAssetData.json
1808
1493
 
1809
1494
  return this.assetsMap;
1810
1495
  } catch (e) {
@@ -1892,195 +1577,85 @@ export class SDK {
1892
1577
  this.getBalancesForNetworks = async function (networkIds: string[], forceRefresh?: boolean) {
1893
1578
  const tag = `${TAG} | getBalancesForNetworks | `;
1894
1579
  try {
1895
- // Add defensive check for pioneer initialization
1896
1580
  if (!this.pioneer) {
1897
- console.error(
1898
- tag,
1899
- 'ERROR: Pioneer client not initialized! this.pioneer is:',
1900
- this.pioneer,
1901
- );
1902
1581
  throw new Error('Pioneer client not initialized. Call init() first.');
1903
1582
  }
1904
1583
 
1905
- // Log force refresh request
1906
- if (forceRefresh) {
1907
- console.log(tag, '🔄 Force refresh requested - bypassing balance cache');
1908
- }
1909
-
1910
- // DIAGNOSTIC: Log input
1911
- console.log('🔍 [DIAGNOSTIC] Input networks:', networkIds);
1912
- console.log('🔍 [DIAGNOSTIC] Total pubkeys:', this.pubkeys.length);
1913
-
1914
- // DIAGNOSTIC: Check which pubkeys have networks field
1915
- const pubkeysWithNetworks = this.pubkeys.filter(p => p.networks && Array.isArray(p.networks));
1916
- const pubkeysWithoutNetworks = this.pubkeys.filter(p => !p.networks || !Array.isArray(p.networks));
1584
+ if (forceRefresh) console.log(tag, '🔄 Force refresh requested');
1917
1585
 
1918
- console.log('🔍 [DIAGNOSTIC] Pubkeys WITH networks:', pubkeysWithNetworks.length);
1919
- console.log('🔍 [DIAGNOSTIC] Pubkeys WITHOUT networks:', pubkeysWithoutNetworks.length);
1920
-
1921
- if (pubkeysWithoutNetworks.length > 0) {
1922
- console.warn('⚠️ [WARNING] Some pubkeys missing networks field:');
1923
- pubkeysWithoutNetworks.forEach(pk => {
1924
- console.warn(` - ${pk.note || pk.pubkey.slice(0, 10)}: networks=${pk.networks}`);
1925
- });
1926
- }
1586
+ // Validate pubkeys and log diagnostics
1587
+ console.log('🔍 [DIAGNOSTIC] Networks:', networkIds.length, 'Pubkeys:', this.pubkeys.length);
1588
+ const { valid, invalid } = validatePubkeysNetworks(this.pubkeys, '🔍 [DIAGNOSTIC]');
1589
+ console.log('🔍 [DIAGNOSTIC] Pubkeys:', { valid: valid.length, invalid: invalid.length });
1927
1590
 
1591
+ // Build asset query for all networks
1928
1592
  const assetQuery: { caip: string; pubkey: string }[] = [];
1929
-
1930
1593
  for (const networkId of networkIds) {
1931
- let adjustedNetworkId = networkId;
1932
-
1933
- if (adjustedNetworkId.includes('eip155:')) {
1934
- adjustedNetworkId = 'eip155:*';
1935
- }
1594
+ const pubkeys = findPubkeysForNetwork(this.pubkeys, networkId, this.paths, tag);
1595
+ const caip = await networkIdToCaip(networkId);
1596
+ assetQuery.push(...buildAssetQuery(pubkeys, caip));
1597
+ }
1936
1598
 
1937
- const isEip155 = adjustedNetworkId.includes('eip155');
1938
- let pubkeys = this.pubkeys.filter(
1939
- (pubkey) =>
1940
- pubkey.networks &&
1941
- Array.isArray(pubkey.networks) &&
1942
- pubkey.networks.some((network) => {
1943
- if (isEip155) return network.startsWith('eip155:');
1944
- return network === adjustedNetworkId;
1945
- }),
1946
- );
1599
+ logQueryDiagnostics(assetQuery, '🔍 [DIAGNOSTIC]');
1947
1600
 
1948
- // FIX 2: Fallback query for missing networks
1949
- if (pubkeys.length === 0) {
1950
- console.warn(tag, `⚠️ No pubkeys found for ${networkId} with networks field`);
1951
- console.warn(tag, 'Attempting fallback: finding pubkeys by path matching');
1601
+ // Fetch balances from API
1602
+ console.log(`⏱️ [PERF] Starting GetPortfolioBalances...`);
1603
+ const apiStart = performance.now();
1604
+ console.time('GetPortfolioBalances Response');
1952
1605
 
1953
- // Find paths for this network
1954
- const pathsForNetwork = this.paths.filter(p =>
1955
- p.networks?.includes(networkId) ||
1956
- (networkId.startsWith('eip155:') && p.networks?.includes('eip155:*'))
1957
- );
1958
-
1959
- // Find pubkeys matching those paths
1960
- for (const path of pathsForNetwork) {
1961
- const matchingPubkey = this.pubkeys.find(pk =>
1962
- JSON.stringify(pk.addressNList) === JSON.stringify(path.addressNList)
1963
- );
1606
+ const marketInfo = await this.pioneer.GetPortfolioBalances(
1607
+ { pubkeys: assetQuery },
1608
+ forceRefresh ? { forceRefresh: true } : undefined
1609
+ );
1964
1610
 
1965
- if (matchingPubkey) {
1966
- console.warn(tag, ` Found pubkey via path matching: ${matchingPubkey.note || matchingPubkey.pubkey.slice(0, 10)}`);
1967
- pubkeys.push(matchingPubkey);
1968
- }
1969
- }
1611
+ console.timeEnd('GetPortfolioBalances Response');
1612
+ console.log(`⏱️ [PERF] API completed in ${(performance.now() - apiStart).toFixed(0)}ms`);
1970
1613
 
1971
- if (pubkeys.length > 0) {
1972
- console.warn(tag, ` ✅ Fallback successful: Found ${pubkeys.length} pubkeys for ${networkId}`);
1973
- } else {
1974
- console.error(tag, ` ❌ Fallback failed: No pubkeys found for ${networkId}`);
1975
- }
1976
- }
1614
+ // Enrich balances with asset metadata
1615
+ const enrichStart = performance.now();
1616
+ console.log(`⏱️ [PERF] Enriching ${marketInfo.data?.length || 0} balances...`);
1977
1617
 
1978
- const caipNative = await networkIdToCaip(networkId);
1979
- for (const pubkey of pubkeys) {
1980
- assetQuery.push({ caip: caipNative, pubkey: pubkey.pubkey });
1981
- }
1982
- }
1618
+ const balances = enrichBalancesWithAssetInfo(
1619
+ marketInfo.data,
1620
+ this.assetsMap,
1621
+ caipToNetworkId
1622
+ );
1983
1623
 
1984
- // DIAGNOSTIC: Log assetQuery before API call
1985
- console.log('🔍 [DIAGNOSTIC] Built assetQuery with', assetQuery.length, 'entries');
1986
- console.log('🔍 [DIAGNOSTIC] Sample queries:', assetQuery.slice(0, 5));
1624
+ console.log(`⏱️ [PERF] Enrichment completed in ${(performance.now() - enrichStart).toFixed(0)}ms`);
1987
1625
 
1988
- // Group by CAIP to see which chains are included
1989
- const caipCounts = new Map<string, number>();
1990
- for (const query of assetQuery) {
1991
- caipCounts.set(query.caip, (caipCounts.get(query.caip) || 0) + 1);
1992
- }
1993
- console.log('🔍 [DIAGNOSTIC] Queries by chain:');
1994
- caipCounts.forEach((count, caip) => {
1995
- console.log(` - ${caip}: ${count} queries`);
1996
- });
1626
+ // Update state and emit events
1627
+ this.balances = balances;
1628
+ this.events.emit('SET_BALANCES', this.balances);
1997
1629
 
1998
- console.log(`⏱️ [PERF] Starting GetPortfolioBalances API call...`);
1999
- const apiCallStart = performance.now();
2000
- console.time('GetPortfolioBalances Response Time');
1630
+ // Build and emit dashboard
1631
+ const dashStart = performance.now();
1632
+ this.dashboard = this.buildDashboardFromBalances();
1633
+ this.events.emit('SET_DASHBOARD', this.dashboard);
1634
+ console.log(`⏱️ [PERF] Dashboard built in ${(performance.now() - dashStart).toFixed(0)}ms`);
1635
+ console.log(`📊 Dashboard: ${this.dashboard?.networks?.length || 0} networks, $${this.dashboard?.totalValueUsd?.toFixed(2) || '0.00'}`);
2001
1636
 
2002
- try {
2003
- // Wrap assetQuery in object with pubkeys field (API expects { pubkeys: [...] })
2004
- // Pass forceRefresh as query parameter if provided
2005
- let marketInfo = await this.pioneer.GetPortfolioBalances(
2006
- { pubkeys: assetQuery },
2007
- forceRefresh ? { forceRefresh: true } : undefined
2008
- );
2009
- const apiCallTime = performance.now() - apiCallStart;
2010
- console.timeEnd('GetPortfolioBalances Response Time');
2011
- console.log(`⏱️ [PERF] API call completed in ${apiCallTime.toFixed(0)}ms`);
2012
-
2013
- const enrichStart = performance.now();
2014
- let balances = marketInfo.data;
2015
- console.log(`⏱️ [PERF] Received ${balances?.length || 0} balances from server`);
2016
-
2017
- // Enrich balances with asset info
2018
- console.log(`⏱️ [PERF] Starting balance enrichment...`);
2019
- for (let balance of balances) {
2020
- const assetInfo = this.assetsMap.get(balance.caip.toLowerCase()) || this.assetsMap.get(balance.caip);
2021
-
2022
- // ✅ PHASE 3: Add fallback for missing asset metadata
2023
- if (!assetInfo) {
2024
- console.warn(`⚠️ [ENRICHMENT] Asset metadata missing for ${balance.caip}, using fallback enrichment`);
2025
-
2026
- // Fallback: Use API-provided values or infer from CAIP
2027
- const inferredType = this.inferTypeFromCaip(balance.caip);
2028
- Object.assign(balance, {
2029
- type: balance.type || inferredType,
2030
- isNative: balance.isNative ?? (inferredType === 'native'),
2031
- networkId: caipToNetworkId(balance.caip),
2032
- icon: 'https://pioneers.dev/coins/unknown.png',
2033
- identifier: `${balance.caip}:${balance.pubkey}`,
2034
- updated: Date.now(),
2035
- });
2036
- continue;
2037
- }
1637
+ console.log(`⏱️ [PERF] Total: ${(performance.now() - apiStart).toFixed(0)}ms`);
1638
+ return this.balances;
2038
1639
 
2039
- // Get color from mapping (fallback to assetInfo.color if it exists)
2040
- const color = ASSET_COLORS[balance.caip] || assetInfo.color;
2041
-
2042
- Object.assign(balance, assetInfo, {
2043
- type: balance.type || assetInfo.type, // ✅ PHASE 3: Prefer API type, fallback to assetInfo
2044
- isNative: balance.isNative ?? assetInfo.isNative, // ✅ PHASE 3: Prefer API isNative
2045
- networkId: caipToNetworkId(balance.caip),
2046
- icon: assetInfo.icon || 'https://pioneers.dev/coins/etherum.png',
2047
- identifier: `${balance.caip}:${balance.pubkey}`,
2048
- updated: Date.now(), // Add timestamp for worker validation
2049
- color, // Add color from mapping
2050
- });
2051
- }
2052
- const enrichTime = performance.now() - enrichStart;
2053
- console.log(`⏱️ [PERF] Enrichment completed in ${enrichTime.toFixed(0)}ms`);
2054
-
2055
- this.balances = balances;
2056
- this.events.emit('SET_BALANCES', this.balances);
2057
-
2058
- // Build dashboard from balances
2059
- console.log(`⏱️ [PERF] Building dashboard from ${balances.length} balances...`);
2060
- const dashboardStart = performance.now();
2061
- const dashboardData = this.buildDashboardFromBalances();
2062
- this.dashboard = dashboardData;
2063
- this.events.emit('SET_DASHBOARD', this.dashboard);
2064
- console.log(`⏱️ [PERF] Dashboard built in ${(performance.now() - dashboardStart).toFixed(0)}ms`);
2065
- console.log(`📊 Dashboard created: ${this.dashboard?.networks?.length || 0} networks, $${this.dashboard?.totalValueUsd?.toFixed(2) || '0.00'} total`);
2066
-
2067
- console.log(`⏱️ [PERF] Total getBalancesForNetworks: ${(performance.now() - apiCallStart).toFixed(0)}ms`);
2068
- return this.balances;
2069
- } catch (apiError: any) {
2070
- console.error(tag, 'GetPortfolioBalances API call failed:', apiError);
2071
- throw new Error(
2072
- `GetPortfolioBalances API call failed: ${apiError?.message || 'Unknown error'}`,
2073
- );
2074
- }
2075
- } catch (e) {
2076
- console.error(tag, 'Error: ', e);
1640
+ } catch (e: any) {
1641
+ console.error(tag, 'Error:', e?.message || e);
2077
1642
  throw e;
2078
1643
  }
2079
1644
  };
2080
- this.getBalances = async function (forceRefresh?: boolean) {
1645
+ this.getBalances = async function (forceRefresh?: boolean, caip?: string) {
2081
1646
  const tag = `${TAG} | getBalances | `;
2082
1647
  try {
2083
- // Simply call the shared function with all blockchains
1648
+ // If CAIP is provided, refresh only that specific asset
1649
+ if (caip) {
1650
+ console.log(tag, `🎯 Refreshing single asset: ${caip}`);
1651
+ const networkId = caip.split('/')[0];
1652
+ console.log(tag, `📍 Target network: ${networkId}`);
1653
+
1654
+ const results = await this.getBalancesForNetworks([networkId], forceRefresh);
1655
+ return results.filter((b) => b.caip === caip || b.networkId === networkId);
1656
+ }
1657
+
1658
+ // Default: refresh all blockchains
2084
1659
  return await this.getBalancesForNetworks(this.blockchains, forceRefresh);
2085
1660
  } catch (e) {
2086
1661
  console.error(tag, 'Error in getBalances: ', e);
@@ -2090,16 +1665,8 @@ export class SDK {
2090
1665
  this.getBalance = async function (networkId: string) {
2091
1666
  const tag = `${TAG} | getBalance | `;
2092
1667
  try {
2093
- // If we need to handle special logic like eip155: inside getBalance,
2094
- // we can do it here or just rely on getBalancesForNetworks to handle it.
2095
- // For example:
2096
- // if (networkId.includes('eip155:')) {
2097
- // networkId = 'eip155:*';
2098
- // }
2099
-
2100
1668
  // Call the shared function with a single-network array
2101
1669
  const results = await this.getBalancesForNetworks([networkId]);
2102
-
2103
1670
  // If needed, you can filter only those that match the specific network
2104
1671
  // (especially if you used wildcard eip155:*)
2105
1672
  const filtered = results.filter(
@@ -2111,11 +1678,6 @@ export class SDK {
2111
1678
  throw e;
2112
1679
  }
2113
1680
  };
2114
-
2115
- /**
2116
- * Get normalized fee rates for a specific network
2117
- * This method handles all fee complexity and returns a clean, consistent format
2118
- */
2119
1681
  this.getFees = async function (networkId: string): Promise<NormalizedFeeRates> {
2120
1682
  const tag = `${TAG} | getFees | `;
2121
1683
  try {
@@ -2239,171 +1801,47 @@ export class SDK {
2239
1801
  if (!asset.caip) throw Error('Invalid Asset! missing caip!');
2240
1802
  if (!asset.networkId) asset.networkId = caipToNetworkId(asset.caip);
2241
1803
 
2242
- // CRITICAL VALIDATION: Check if we have an address/xpub for this network
2243
- if (!this.pubkeys || this.pubkeys.length === 0) {
2244
- const errorMsg = `Cannot set asset context for ${asset.caip} - no pubkeys loaded. Please initialize wallet first.`;
2245
- console.error(tag, errorMsg);
2246
- throw new Error(errorMsg);
2247
- }
2248
-
2249
- // For EVM chains, check for wildcard eip155:* in addition to exact match
2250
- const pubkeysForNetwork = this.pubkeys.filter((e: any) => {
2251
- if (!e.networks || !Array.isArray(e.networks)) return false;
2252
-
2253
- // Exact match
2254
- if (e.networks.includes(asset.networkId)) return true;
2255
-
2256
- // For EVM chains, check if pubkey has eip155:* wildcard
2257
- if (asset.networkId.startsWith('eip155:') && e.networks.includes('eip155:*')) {
2258
- return true;
2259
- }
2260
-
2261
- return false;
2262
- });
2263
-
2264
- if (pubkeysForNetwork.length === 0) {
2265
- const errorMsg = `Cannot set asset context for ${asset.caip} - no address/xpub found for network ${asset.networkId}`;
2266
- console.error(tag, errorMsg);
2267
- console.error(tag, 'Available networks in pubkeys:', [
2268
- ...new Set(this.pubkeys.flatMap((p: any) => p.networks || [])),
2269
- ]);
2270
- throw new Error(errorMsg);
2271
- }
2272
-
2273
- // For UTXO chains, verify we have xpub
2274
- const isUtxoChain = asset.networkId.startsWith('bip122:');
2275
- if (isUtxoChain) {
2276
- const xpubFound = pubkeysForNetwork.some((p: any) => p.type === 'xpub' && p.pubkey);
2277
- if (!xpubFound) {
2278
- const errorMsg = `Cannot set asset context for UTXO chain ${asset.caip} - xpub required but not found`;
2279
- console.error(tag, errorMsg);
2280
- throw new Error(errorMsg);
2281
- }
2282
- }
2283
-
2284
- // Verify we have a valid address or pubkey
2285
- const hasValidAddress = pubkeysForNetwork.some(
2286
- (p: any) => p.address || p.master || p.pubkey,
2287
- );
2288
- if (!hasValidAddress) {
2289
- const errorMsg = `Cannot set asset context for ${asset.caip} - no valid address found in pubkeys`;
2290
- console.error(tag, errorMsg);
2291
- throw new Error(errorMsg);
2292
- }
2293
-
1804
+ // Validate pubkeys for network (throws descriptive errors)
1805
+ validatePubkeysForNetwork(this.pubkeys, asset.networkId, asset.caip);
1806
+ const pubkeysForNetwork = findPubkeysForNetwork(this.pubkeys, asset.networkId);
2294
1807
  console.log(
2295
1808
  tag,
2296
1809
  `✅ Validated: Found ${pubkeysForNetwork.length} addresses for ${asset.networkId}`,
2297
1810
  );
2298
1811
 
2299
- // ALWAYS fetch fresh market price for the asset
2300
- let freshPriceUsd = 0;
2301
- try {
2302
- // Validate CAIP before calling API
2303
- if (!asset.caip || typeof asset.caip !== 'string' || !asset.caip.includes(':')) {
2304
- console.warn(tag, 'Invalid or missing CAIP, skipping market price fetch:', asset.caip);
2305
- } else {
2306
- console.log(tag, 'Fetching fresh market price for:', asset.caip);
2307
- const marketData = await this.pioneer.GetMarketInfo([asset.caip]);
2308
- console.log(tag, 'Market data response:', marketData);
2309
-
2310
- if (marketData && marketData.data && marketData.data.length > 0) {
2311
- freshPriceUsd = marketData.data[0];
2312
- console.log(tag, '✅ Fresh market price:', freshPriceUsd);
2313
- } else {
2314
- console.warn(tag, 'No market data returned for:', asset.caip);
2315
- }
2316
- }
2317
- } catch (marketError) {
2318
- console.error(tag, 'Error fetching market price:', marketError);
2319
- // Continue without fresh price, will try to use cached data
2320
- }
1812
+ // Fetch fresh market price
1813
+ const freshPriceUsd = await fetchMarketPrice(this.pioneer, asset.caip);
2321
1814
 
2322
- // Try to find the asset in the local assetsMap
2323
- let assetInfo = this.assetsMap.get(asset.caip.toLowerCase());
2324
- console.log(tag, 'assetInfo: ', assetInfo);
2325
-
2326
- //check discovery
2327
- let assetInfoDiscovery = assetData[asset.caip];
2328
- console.log(tag, 'assetInfoDiscovery: ', assetInfoDiscovery);
2329
- if (assetInfoDiscovery) assetInfo = assetInfoDiscovery;
2330
-
2331
- // If the asset is not found, create a placeholder object
2332
- if (!assetInfo) {
2333
- console.log(tag, 'Building placeholder asset!');
2334
- // Create a placeholder asset if it's not found in Pioneer or locally
2335
- assetInfo = {
2336
- caip: asset.caip.toLowerCase(),
2337
- networkId: asset.networkId,
2338
- symbol: asset.symbol || 'UNKNOWN',
2339
- name: asset.name || 'Unknown Asset',
2340
- icon: asset.icon || 'https://pioneers.dev/coins/ethereum.png',
2341
- };
2342
- }
1815
+ // Resolve asset info from multiple sources
1816
+ let assetInfo = resolveAssetInfo(this.assetsMap, assetData, asset);
2343
1817
 
2344
- // Look for price and balance information in balances
2345
- // CRITICAL: For UTXO chains, we need to aggregate ALL balances across all xpubs
1818
+ // Get matching balances
2346
1819
  const matchingBalances = this.balances.filter((b) => b.caip === asset.caip);
2347
1820
 
1821
+ // Extract price from balances if available
2348
1822
  if (matchingBalances.length > 0) {
2349
- // Use price from first balance entry (all should have same price)
2350
- // Check for both priceUsd and price properties (different sources may use different names)
2351
- let priceValue = matchingBalances[0].priceUsd || matchingBalances[0].price;
2352
-
2353
- // If no price but we have valueUsd and balance, calculate the price
2354
- if ((!priceValue || priceValue === 0) && matchingBalances[0].valueUsd && matchingBalances[0].balance) {
2355
- const balance = parseFloat(matchingBalances[0].balance);
2356
- const valueUsd = parseFloat(matchingBalances[0].valueUsd);
2357
- if (balance > 0 && valueUsd > 0) {
2358
- priceValue = valueUsd / balance;
2359
- console.log(tag, 'calculated priceUsd from valueUsd/balance:', priceValue);
2360
- }
2361
- }
2362
-
2363
- if (priceValue && priceValue > 0) {
1823
+ const priceValue = extractPriceFromBalances(matchingBalances);
1824
+ if (priceValue > 0) {
2364
1825
  console.log(tag, 'detected priceUsd from balance:', priceValue);
2365
1826
  assetInfo.priceUsd = priceValue;
2366
1827
  }
2367
1828
  }
2368
1829
 
2369
- // Override with fresh price if we got one from the API
2370
- if (freshPriceUsd && freshPriceUsd > 0) {
1830
+ // Override with fresh price and aggregate balances
1831
+ if (freshPriceUsd > 0) {
2371
1832
  assetInfo.priceUsd = freshPriceUsd;
2372
1833
  console.log(tag, '✅ Using fresh market price:', freshPriceUsd);
2373
1834
 
2374
- // Aggregate all balances for this asset (critical for UTXO chains with multiple xpubs)
2375
- let totalBalance = 0;
2376
- let totalValueUsd = 0;
2377
-
2378
- console.log(tag, `Found ${matchingBalances.length} balance entries for ${asset.caip}`);
2379
- for (const balanceEntry of matchingBalances) {
2380
- const balance = parseFloat(balanceEntry.balance) || 0;
2381
- const valueUsd = parseFloat(balanceEntry.valueUsd) || 0;
2382
- totalBalance += balance;
2383
- totalValueUsd += valueUsd;
2384
- console.log(tag, ` Balance entry: ${balance} (${valueUsd} USD)`);
2385
- }
2386
-
1835
+ const { totalBalance, totalValueUsd } = aggregateBalances(matchingBalances, asset.caip);
2387
1836
  assetInfo.balance = totalBalance.toString();
2388
1837
  assetInfo.valueUsd = totalValueUsd.toFixed(2);
2389
- console.log(tag, `Aggregated balance: ${totalBalance} (${totalValueUsd.toFixed(2)} USD)`);
2390
1838
  }
2391
1839
 
2392
1840
  // Filter balances and pubkeys for this asset
2393
1841
  const assetBalances = this.balances.filter((b) => b.caip === asset.caip);
2394
- const assetPubkeys = this.pubkeys.filter(
2395
- (p) =>
2396
- (p.networks &&
2397
- Array.isArray(p.networks) &&
2398
- p.networks.includes(caipToNetworkId(asset.caip))) ||
2399
- (caipToNetworkId(asset.caip).includes('eip155') &&
2400
- p.networks &&
2401
- Array.isArray(p.networks) &&
2402
- p.networks.some((n) => n.startsWith('eip155'))),
2403
- );
1842
+ const assetPubkeys = filterPubkeysForAsset(this.pubkeys, asset.caip, caipToNetworkId);
2404
1843
 
2405
1844
  // Combine the user-provided asset with any additional info we have
2406
- // IMPORTANT: Don't let a 0 priceUsd from input override a valid price from balance
2407
1845
  const finalAssetContext = {
2408
1846
  ...assetInfo,
2409
1847
  ...asset,
@@ -2411,21 +1849,14 @@ export class SDK {
2411
1849
  balances: assetBalances,
2412
1850
  };
2413
1851
 
2414
- // If input has priceUsd of 0 but we found a valid price from balance, use the balance price
1852
+ // If input has priceUsd of 0 but we found a valid price, use the found price
2415
1853
  if ((!asset.priceUsd || asset.priceUsd === 0) && assetInfo.priceUsd && assetInfo.priceUsd > 0) {
2416
1854
  finalAssetContext.priceUsd = assetInfo.priceUsd;
2417
1855
  }
2418
1856
 
2419
1857
  // Update all matching balances with the fresh price
2420
- if (freshPriceUsd && freshPriceUsd > 0) {
2421
- for (const balance of assetBalances) {
2422
- balance.price = freshPriceUsd;
2423
- balance.priceUsd = freshPriceUsd;
2424
- // Recalculate valueUsd with fresh price
2425
- const balanceAmount = parseFloat(balance.balance || 0);
2426
- balance.valueUsd = (balanceAmount * freshPriceUsd).toString();
2427
- }
2428
- console.log(tag, 'Updated all balances with fresh price data');
1858
+ if (freshPriceUsd > 0) {
1859
+ updateBalancesWithPrice(assetBalances, freshPriceUsd);
2429
1860
  }
2430
1861
 
2431
1862
  this.assetContext = finalAssetContext;
@@ -2439,38 +1870,6 @@ export class SDK {
2439
1870
  ) {
2440
1871
  // Get the native asset for this network
2441
1872
  const networkId = asset.networkId || assetInfo.networkId;
2442
-
2443
- // Determine the native gas symbol based on the network
2444
- let nativeSymbol = 'GAS'; // default fallback
2445
- let nativeCaip = '';
2446
-
2447
- //TODO removeme
2448
- if (networkId.includes('mayachain')) {
2449
- nativeSymbol = 'CACAO';
2450
- nativeCaip = 'cosmos:mayachain-mainnet-v1/slip44:931';
2451
- } else if (networkId.includes('thorchain')) {
2452
- nativeSymbol = 'RUNE';
2453
- nativeCaip = 'cosmos:thorchain-mainnet-v1/slip44:931';
2454
- } else if (networkId.includes('cosmoshub')) {
2455
- nativeSymbol = 'ATOM';
2456
- nativeCaip = 'cosmos:cosmoshub-4/slip44:118';
2457
- } else if (networkId.includes('osmosis')) {
2458
- nativeSymbol = 'OSMO';
2459
- nativeCaip = 'cosmos:osmosis-1/slip44:118';
2460
- } else if (networkId.includes('eip155:1')) {
2461
- nativeSymbol = 'ETH';
2462
- nativeCaip = 'eip155:1/slip44:60';
2463
- } else if (networkId.includes('eip155:137')) {
2464
- nativeSymbol = 'MATIC';
2465
- nativeCaip = 'eip155:137/slip44:60';
2466
- } else if (networkId.includes('eip155:56')) {
2467
- nativeSymbol = 'BNB';
2468
- nativeCaip = 'eip155:56/slip44:60';
2469
- } else if (networkId.includes('eip155:43114')) {
2470
- nativeSymbol = 'AVAX';
2471
- nativeCaip = 'eip155:43114/slip44:60';
2472
- }
2473
-
2474
1873
  // Set the native symbol
2475
1874
  this.assetContext.nativeSymbol = nativeSymbol;
2476
1875
 
@@ -2569,125 +1968,54 @@ export class SDK {
2569
1968
  this.setOutboundAssetContext = async function (asset?: any): Promise<any> {
2570
1969
  const tag = `${TAG} | setOutputAssetContext | `;
2571
1970
  try {
2572
- console.log(tag, '0. asset: ', asset);
1971
+ console.log(tag, 'asset:', asset);
2573
1972
  // Accept null
2574
1973
  if (!asset) {
2575
1974
  this.outboundAssetContext = null;
2576
1975
  return;
2577
1976
  }
2578
1977
 
2579
- console.log(tag, '1 asset: ', asset);
2580
-
2581
1978
  if (!asset.caip) throw Error('Invalid Asset! missing caip!');
2582
1979
  if (!asset.networkId) asset.networkId = caipToNetworkId(asset.caip);
2583
1980
 
2584
- console.log(tag, 'networkId: ', asset.networkId);
2585
- console.log(tag, 'this.pubkeys: ', this.pubkeys);
2586
- //get a pubkey for network (handle EVM wildcard)
2587
- const pubkey = this.pubkeys.find((p) => {
2588
- if (!p.networks || !Array.isArray(p.networks)) return false;
2589
- // Exact match
2590
- if (p.networks.includes(asset.networkId)) return true;
2591
- // For EVM chains, check if pubkey has eip155:* wildcard
2592
- if (asset.networkId.startsWith('eip155:') && p.networks.includes('eip155:*')) return true;
2593
- return false;
2594
- });
2595
- if (!pubkey) throw Error('Invalid network! missing pubkey for network! ' + asset.networkId);
1981
+ console.log(tag, 'networkId:', asset.networkId);
2596
1982
 
2597
- // ALWAYS fetch fresh market price for the asset
2598
- let freshPriceUsd = 0;
2599
- try {
2600
- // Validate CAIP before calling API
2601
- if (!asset.caip || typeof asset.caip !== 'string' || !asset.caip.includes(':')) {
2602
- console.warn(tag, 'Invalid or missing CAIP, skipping market price fetch:', asset.caip);
2603
- } else {
2604
- console.log(tag, 'Fetching fresh market price for:', asset.caip);
2605
- const marketData = await this.pioneer.GetMarketInfo([asset.caip]);
2606
- console.log(tag, 'Market data response:', marketData);
1983
+ // Get pubkey for network (uses utility helper)
1984
+ const pubkey = findPubkeyForNetwork(this.pubkeys, asset.networkId);
1985
+ if (!pubkey) throw Error('Invalid network! missing pubkey for network! ' + asset.networkId);
2607
1986
 
2608
- if (marketData && marketData.data && marketData.data.length > 0) {
2609
- freshPriceUsd = marketData.data[0];
2610
- console.log(tag, '✅ Fresh market price:', freshPriceUsd);
2611
- } else {
2612
- console.warn(tag, 'No market data returned for:', asset.caip);
2613
- }
2614
- }
2615
- } catch (marketError) {
2616
- console.error(tag, 'Error fetching market price:', marketError);
2617
- // Continue without fresh price, will try to use cached data
2618
- }
1987
+ // Fetch fresh market price
1988
+ const freshPriceUsd = await fetchMarketPrice(this.pioneer, asset.caip);
2619
1989
 
2620
- // Try to find the asset in the local assetsMap
2621
- let assetInfo = this.assetsMap.get(asset.caip.toLowerCase());
2622
- console.log(tag, 'assetInfo: ', assetInfo);
2623
-
2624
- // If the asset is not found, create a placeholder object
2625
- if (!assetInfo) {
2626
- // Create a placeholder asset if it's not found in Pioneer or locally
2627
- assetInfo = {
2628
- caip: asset.caip.toLowerCase(),
2629
- networkId: asset.networkId,
2630
- symbol: asset.symbol || 'UNKNOWN',
2631
- name: asset.name || 'Unknown Asset',
2632
- icon: asset.icon || 'https://pioneers.dev/coins/ethereum.png',
2633
- };
2634
- }
1990
+ // Resolve asset info from multiple sources
1991
+ let assetInfo = resolveAssetInfo(this.assetsMap, assetData, asset);
2635
1992
 
2636
- // Look for price and balance information in balances
2637
- // CRITICAL: For UTXO chains, we need to aggregate ALL balances across all xpubs
1993
+ // Get matching balances
2638
1994
  const matchingBalances = this.balances.filter((b) => b.caip === asset.caip);
2639
1995
 
1996
+ // Extract price from balances if available
2640
1997
  if (matchingBalances.length > 0) {
2641
- // Use price from first balance entry (all should have same price)
2642
- // Check for both priceUsd and price properties (different sources may use different names)
2643
- let priceValue = matchingBalances[0].priceUsd || matchingBalances[0].price;
2644
-
2645
- // If no price but we have valueUsd and balance, calculate the price
2646
- if ((!priceValue || priceValue === 0) && matchingBalances[0].valueUsd && matchingBalances[0].balance) {
2647
- const balance = parseFloat(matchingBalances[0].balance);
2648
- const valueUsd = parseFloat(matchingBalances[0].valueUsd);
2649
- if (balance > 0 && valueUsd > 0) {
2650
- priceValue = valueUsd / balance;
2651
- console.log(tag, 'calculated priceUsd from valueUsd/balance:', priceValue);
2652
- }
2653
- }
2654
-
2655
- if (priceValue && priceValue > 0) {
1998
+ const priceValue = extractPriceFromBalances(matchingBalances);
1999
+ if (priceValue > 0) {
2656
2000
  console.log(tag, 'detected priceUsd from balance:', priceValue);
2657
2001
  assetInfo.priceUsd = priceValue;
2658
2002
  }
2659
2003
  }
2660
2004
 
2661
- // Override with fresh price if we got one from the API
2662
- if (freshPriceUsd && freshPriceUsd > 0) {
2005
+ // Override with fresh price and aggregate balances
2006
+ if (freshPriceUsd > 0) {
2663
2007
  assetInfo.priceUsd = freshPriceUsd;
2664
2008
  console.log(tag, '✅ Using fresh market price:', freshPriceUsd);
2665
2009
 
2666
- // Aggregate all balances for this asset (critical for UTXO chains with multiple xpubs)
2667
- let totalBalance = 0;
2668
- let totalValueUsd = 0;
2669
-
2670
- console.log(tag, `Found ${matchingBalances.length} balance entries for ${asset.caip}`);
2671
- for (const balanceEntry of matchingBalances) {
2672
- const balance = parseFloat(balanceEntry.balance) || 0;
2673
- const valueUsd = parseFloat(balanceEntry.valueUsd) || 0;
2674
- totalBalance += balance;
2675
- totalValueUsd += valueUsd;
2676
- console.log(tag, ` Balance entry: ${balance} (${valueUsd} USD)`);
2677
- }
2678
-
2010
+ const { totalBalance, totalValueUsd } = aggregateBalances(matchingBalances, asset.caip);
2679
2011
  assetInfo.balance = totalBalance.toString();
2680
2012
  assetInfo.valueUsd = totalValueUsd.toFixed(2);
2681
- console.log(tag, `Aggregated balance: ${totalBalance} (${totalValueUsd.toFixed(2)} USD)`);
2682
2013
  }
2683
2014
 
2684
- console.log(tag, 'CHECKPOINT 1');
2685
-
2686
2015
  // Combine the user-provided asset with any additional info we have
2687
2016
  this.outboundAssetContext = { ...assetInfo, ...asset, ...pubkey };
2688
2017
 
2689
- console.log(tag, 'CHECKPOINT 3');
2690
- console.log(tag, 'outboundAssetContext: assetInfo: ', assetInfo);
2018
+ console.log(tag, 'outboundAssetContext set:', this.outboundAssetContext.caip);
2691
2019
 
2692
2020
  // Set outbound blockchain context based on asset
2693
2021
  if (asset.caip) {
@@ -2717,18 +2045,8 @@ export class SDK {
2717
2045
  ownerAddress: string;
2718
2046
  spenderAddress: string;
2719
2047
  }) => {
2720
- const tag = TAG + ' | CheckERC20Allowance | ';
2721
- try {
2722
- console.log(tag, 'Checking ERC20 allowance:', params);
2723
-
2724
- const result = await this.pioneer.GetTokenAllowance(params);
2725
-
2726
- console.log(tag, 'Allowance result:', result);
2727
- return result.data;
2728
- } catch (e) {
2729
- console.error(tag, 'Error checking ERC20 allowance:', e);
2730
- throw e;
2731
- }
2048
+ const result = await this.pioneer.GetTokenAllowance(params);
2049
+ return result.data;
2732
2050
  };
2733
2051
 
2734
2052
  /**
@@ -2742,18 +2060,8 @@ export class SDK {
2742
2060
  ownerAddress: string;
2743
2061
  amount: string;
2744
2062
  }) => {
2745
- const tag = TAG + ' | BuildERC20ApprovalTx | ';
2746
- try {
2747
- console.log(tag, 'Building ERC20 approval transaction:', params);
2748
-
2749
- const result = await this.pioneer.BuildApprovalTransaction(params);
2750
-
2751
- console.log(tag, 'Approval tx built:', result);
2752
- return result.data;
2753
- } catch (e) {
2754
- console.error(tag, 'Error building approval transaction:', e);
2755
- throw e;
2756
- }
2063
+ const result = await this.pioneer.BuildApprovalTransaction(params);
2064
+ return result.data;
2757
2065
  };
2758
2066
  }
2759
2067