@pioneer-platform/pioneer-sdk 8.15.13 → 8.15.15

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
@@ -21,43 +21,47 @@ import { detectKkApiAvailability } from './utils/kkapi-detection.js';
21
21
  import { formatTime } from './utils/format-time.js';
22
22
  import { buildDashboardFromBalances } from './utils/build-dashboard.js';
23
23
  import { syncMarket } from './utils/sync-market.js';
24
+ import {
25
+ getPubkeyKey,
26
+ deduplicatePubkeys,
27
+ validatePubkey,
28
+ findPubkeysForNetwork,
29
+ findPubkeyForNetwork,
30
+ validatePubkeysForNetwork,
31
+ filterPubkeysForAsset
32
+ } from './utils/pubkey-management.js';
33
+ import {
34
+ isCacheDataValid,
35
+ buildDashboardFromPortfolioData,
36
+ fetchMarketPrice,
37
+ extractPriceFromBalances,
38
+ aggregateBalances,
39
+ updateBalancesWithPrice
40
+ } from './utils/portfolio-helpers.js';
41
+ import {
42
+ createInitialSyncState,
43
+ createCacheSyncState,
44
+ createFreshSyncState,
45
+ type SyncState,
46
+ resolveAssetInfo
47
+ } from './utils/sync-state.js';
48
+ import { getMaxSendableAmount } from './utils/fee-reserves.js';
49
+ import {
50
+ matchesNetwork,
51
+ normalizeNetworkId,
52
+ validatePubkeysNetworks
53
+ } from './utils/network-helpers.js';
54
+ import {
55
+ buildAssetQuery,
56
+ logQueryDiagnostics,
57
+ enrichBalancesWithAssetInfo
58
+ } from './utils/portfolio-helpers.js';
59
+ import { ensurePathsForBlockchains } from './utils/path-discovery.js';
60
+ import { syncPubkeysForBlockchains } from './utils/pubkey-sync.js';
61
+ // import { ASSET_COLORS } from './constants/asset-colors.js'; // TODO: Create this file or remove usage
24
62
 
25
63
  const TAG = ' | Pioneer-sdk | ';
26
64
 
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
65
  export interface PioneerSDKConfig {
62
66
  appName: string;
63
67
  appIcon: string;
@@ -84,18 +88,6 @@ export interface PioneerSDKConfig {
84
88
  skipKeeperEndpoint?: boolean; // Skip vault endpoint detection
85
89
  }
86
90
 
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
91
  export class SDK {
100
92
  public status: string;
101
93
  public username: string;
@@ -159,18 +151,8 @@ export class SDK {
159
151
  public appName: string;
160
152
  public appIcon: any;
161
153
  public init: (walletsVerbose: any, setup: any) => Promise<any>;
162
- // public initOffline: () => Promise<any>;
163
- // public backgroundSync: () => Promise<void>;
164
154
  public getUnifiedPortfolio: () => Promise<any>;
165
155
  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
156
  public app: {
175
157
  getAddress: (options: {
176
158
  networkId?: string;
@@ -272,16 +254,7 @@ export class SDK {
272
254
  this.contextType = '';
273
255
 
274
256
  // 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
- };
257
+ this.syncState = createInitialSyncState(this.blockchains.length);
285
258
 
286
259
  // Initialize offline client if offline-first mode is enabled
287
260
  this.offlineClient = config.offlineFirst
@@ -314,40 +287,17 @@ export class SDK {
314
287
  });
315
288
  };
316
289
 
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
- };
290
+ // Pubkey helper methods using utilities
291
+ this.getPubkeyKey = getPubkeyKey;
292
+ this.deduplicatePubkeys = deduplicatePubkeys;
335
293
 
336
294
  // Helper method to validate and add a single pubkey
337
295
  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);
296
+ if (!validatePubkey(pubkey)) return false;
344
297
 
345
- // Check if already exists
346
- if (this.pubkeySet.has(key)) {
347
- return false;
348
- }
298
+ const key = getPubkeyKey(pubkey);
299
+ if (this.pubkeySet.has(key)) return false;
349
300
 
350
- // Add to both array and set
351
301
  this.pubkeys.push(pubkey);
352
302
  this.pubkeySet.add(key);
353
303
  return true;
@@ -378,20 +328,6 @@ export class SDK {
378
328
  this.pubkeys = [];
379
329
  this.pubkeySet.clear();
380
330
  }
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
331
  // Fast portfolio loading from kkapi:// cache
396
332
  this.getUnifiedPortfolio = async function () {
397
333
  const tag = `${TAG} | getUnifiedPortfolio | `;
@@ -439,10 +375,8 @@ export class SDK {
439
375
 
440
376
  // Update pubkeys from cache
441
377
  if (portfolioData.pubkeys && portfolioData.pubkeys.length > 0) {
442
- // Convert vault pubkey format to pioneer-sdk format
443
- const convertedPubkeys = this.convertVaultPubkeysToPioneerFormat(portfolioData.pubkeys);
444
378
  // Use setPubkeys to ensure deduplication
445
- this.setPubkeys(convertedPubkeys);
379
+ this.setPubkeys(portfolioData.pubkeys);
446
380
  this.events.emit('SET_PUBKEYS', this.pubkeys);
447
381
  }
448
382
 
@@ -459,75 +393,12 @@ export class SDK {
459
393
  }
460
394
 
461
395
  // 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
396
  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;
397
+ this.dashboard = buildDashboardFromPortfolioData(portfolioData);
514
398
  this.events.emit('SET_DASHBOARD', this.dashboard);
515
399
 
516
400
  // 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
- };
401
+ this.syncState = createCacheSyncState(portfolioData.lastUpdated, this.blockchains.length);
531
402
  this.events.emit('SYNC_STATE_CHANGED', this.syncState);
532
403
  } else {
533
404
  console.warn(
@@ -784,249 +655,106 @@ export class SDK {
784
655
  this.buildDashboardFromBalances = function () {
785
656
  return buildDashboardFromBalances(this.balances, this.blockchains, this.assetsMap);
786
657
  };
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
658
  this.syncMarket = async function () {
795
659
  return syncMarket(this.balances, this.pioneer);
796
660
  };
797
661
  this.sync = async function () {
798
662
  const tag = `${TAG} | sync | `;
663
+ const log = require('@pioneer-platform/loggerdog')();
664
+
799
665
  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;
666
+ // Update sync state: starting
667
+ this.syncState = {
668
+ ...createInitialSyncState(this.blockchains.length),
669
+ syncProgress: 10
806
670
  };
671
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
807
672
 
808
- //at least 1 path per chain
673
+ // Step 1: Initial pubkey fetch
674
+ log.info(tag, 'Fetching initial pubkeys...');
809
675
  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
676
 
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
- }
677
+ this.syncState.syncProgress = 20;
678
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
679
+
680
+ // Step 2: Ensure paths for all blockchains
681
+ log.info(tag, 'Discovering paths for blockchains...');
682
+ this.paths = await ensurePathsForBlockchains(
683
+ this.blockchains,
684
+ this.paths,
685
+ tag
686
+ );
687
+
688
+ this.syncState.syncProgress = 30;
689
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
690
+
691
+ // Step 3: Sync pubkeys for all paths
692
+ log.info(tag, 'Synchronizing pubkeys...');
693
+ await syncPubkeysForBlockchains(
694
+ this.blockchains,
695
+ this.paths,
696
+ this.pubkeys,
697
+ this.keepKeySdk,
698
+ this.context,
699
+ getPubkey,
700
+ (pubkey) => this.addPubkey(pubkey),
701
+ tag
702
+ );
703
+
704
+ this.syncState.syncProgress = 50;
705
+ this.syncState.syncedChains = this.blockchains.length;
706
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
707
+
708
+ // Step 4: Fetch balances
709
+ log.info(tag, 'Fetching balances...');
848
710
  await this.getBalances();
849
711
 
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)...');
712
+ this.syncState.syncProgress = 70;
713
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
714
+
715
+ // Step 5: Load charts (critical for token discovery)
716
+ log.info(tag, 'Loading charts (tokens + portfolio)...');
853
717
  await this.getCharts();
854
- console.log(tag, `Charts loaded. Total balances: ${this.balances.length}`);
718
+ log.info(tag, `Charts loaded. Total balances: ${this.balances.length}`);
855
719
 
856
- // Sync market prices for all balances
857
- await this.syncMarket();
720
+ this.syncState.syncProgress = 85;
721
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
858
722
 
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
- };
723
+ // Step 6: Sync market prices
724
+ log.info(tag, 'Syncing market prices...');
725
+ await this.syncMarket();
877
726
 
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
- });
727
+ this.syncState.syncProgress = 95;
728
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
901
729
 
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
- });
730
+ // Step 7: Build dashboard
731
+ log.info(tag, 'Building dashboard...');
732
+ this.dashboard = buildDashboardFromBalances(
733
+ this.balances,
734
+ [...new Set(this.blockchains)], // Deduplicate blockchains
735
+ this.assetsMap
736
+ );
917
737
 
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
- }
738
+ // Step 8: Update sync state - complete
739
+ this.syncState = createFreshSyncState(this.blockchains.length);
740
+ this.events.emit('SYNC_STATE_CHANGED', this.syncState);
741
+ this.events.emit('SYNC_COMPLETE', this.syncState);
948
742
 
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
- });
743
+ log.info(tag, '✅ Sync complete!');
744
+ return true;
986
745
 
987
- totalPortfolioValue += networkTotal;
988
- }
746
+ } catch (e) {
747
+ log.error(tag, 'Sync failed:', e);
989
748
 
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
749
+ // Update sync state with error
1014
750
  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'
751
+ ...this.syncState,
752
+ isSynced: false,
753
+ syncProgress: 0
1023
754
  };
1024
755
  this.events.emit('SYNC_STATE_CHANGED', this.syncState);
1025
- this.events.emit('SYNC_COMPLETE', this.syncState);
756
+ this.events.emit('SYNC_ERROR', e);
1026
757
 
1027
- return true;
1028
- } catch (e) {
1029
- console.error(tag, 'Error in sync:', e);
1030
758
  throw e;
1031
759
  }
1032
760
  };
@@ -1213,16 +941,6 @@ export class SDK {
1213
941
 
1214
942
  //get quote
1215
943
  // 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
944
  const pubkeys = this.pubkeys.filter((e: any) =>
1227
945
  matchesNetwork(e, this.assetContext.networkId),
1228
946
  );
@@ -1280,23 +998,12 @@ export class SDK {
1280
998
  ).toFixed(2);
1281
999
  console.log(tag, `Updated assetContext balance to aggregated total: ${totalBalance}`);
1282
1000
 
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);
1001
+ // Calculate max sendable amount using centralized fee reserve utility
1002
+ inputAmount = getMaxSendableAmount(totalBalance, swapPayload.caipIn);
1296
1003
 
1297
1004
  console.log(
1298
1005
  tag,
1299
- `Using max amount for swap: ${inputAmount} (total balance: ${totalBalance}, reserve: ${reserve})`,
1006
+ `Using max amount for swap: ${inputAmount} (total balance: ${totalBalance})`,
1300
1007
  );
1301
1008
  } else {
1302
1009
  // Convert amount to number for type safety
@@ -1780,31 +1487,8 @@ export class SDK {
1780
1487
  }
1781
1488
  }
1782
1489
 
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
- }
1490
+ // All gas assets and tokens are now loaded from assetData
1491
+ // MAYA token is included in generatedAssetData.json
1808
1492
 
1809
1493
  return this.assetsMap;
1810
1494
  } catch (e) {
@@ -1892,195 +1576,85 @@ export class SDK {
1892
1576
  this.getBalancesForNetworks = async function (networkIds: string[], forceRefresh?: boolean) {
1893
1577
  const tag = `${TAG} | getBalancesForNetworks | `;
1894
1578
  try {
1895
- // Add defensive check for pioneer initialization
1896
1579
  if (!this.pioneer) {
1897
- console.error(
1898
- tag,
1899
- 'ERROR: Pioneer client not initialized! this.pioneer is:',
1900
- this.pioneer,
1901
- );
1902
1580
  throw new Error('Pioneer client not initialized. Call init() first.');
1903
1581
  }
1904
1582
 
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));
1583
+ if (forceRefresh) console.log(tag, '🔄 Force refresh requested');
1917
1584
 
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
- }
1585
+ // Validate pubkeys and log diagnostics
1586
+ console.log('🔍 [DIAGNOSTIC] Networks:', networkIds.length, 'Pubkeys:', this.pubkeys.length);
1587
+ const { valid, invalid } = validatePubkeysNetworks(this.pubkeys, '🔍 [DIAGNOSTIC]');
1588
+ console.log('🔍 [DIAGNOSTIC] Pubkeys:', { valid: valid.length, invalid: invalid.length });
1927
1589
 
1590
+ // Build asset query for all networks
1928
1591
  const assetQuery: { caip: string; pubkey: string }[] = [];
1929
-
1930
1592
  for (const networkId of networkIds) {
1931
- let adjustedNetworkId = networkId;
1932
-
1933
- if (adjustedNetworkId.includes('eip155:')) {
1934
- adjustedNetworkId = 'eip155:*';
1935
- }
1593
+ const pubkeys = findPubkeysForNetwork(this.pubkeys, networkId, this.paths, tag);
1594
+ const caip = await networkIdToCaip(networkId);
1595
+ assetQuery.push(...buildAssetQuery(pubkeys, caip));
1596
+ }
1936
1597
 
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
- );
1598
+ logQueryDiagnostics(assetQuery, '🔍 [DIAGNOSTIC]');
1947
1599
 
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');
1600
+ // Fetch balances from API
1601
+ console.log(`⏱️ [PERF] Starting GetPortfolioBalances...`);
1602
+ const apiStart = performance.now();
1603
+ console.time('GetPortfolioBalances Response');
1952
1604
 
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
- );
1605
+ const marketInfo = await this.pioneer.GetPortfolioBalances(
1606
+ { pubkeys: assetQuery },
1607
+ forceRefresh ? { forceRefresh: true } : undefined
1608
+ );
1964
1609
 
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
- }
1610
+ console.timeEnd('GetPortfolioBalances Response');
1611
+ console.log(`⏱️ [PERF] API completed in ${(performance.now() - apiStart).toFixed(0)}ms`);
1970
1612
 
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
- }
1613
+ // Enrich balances with asset metadata
1614
+ const enrichStart = performance.now();
1615
+ console.log(`⏱️ [PERF] Enriching ${marketInfo.data?.length || 0} balances...`);
1977
1616
 
1978
- const caipNative = await networkIdToCaip(networkId);
1979
- for (const pubkey of pubkeys) {
1980
- assetQuery.push({ caip: caipNative, pubkey: pubkey.pubkey });
1981
- }
1982
- }
1617
+ const balances = enrichBalancesWithAssetInfo(
1618
+ marketInfo.data,
1619
+ this.assetsMap,
1620
+ caipToNetworkId
1621
+ );
1983
1622
 
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));
1623
+ console.log(`⏱️ [PERF] Enrichment completed in ${(performance.now() - enrichStart).toFixed(0)}ms`);
1987
1624
 
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
- });
1625
+ // Update state and emit events
1626
+ this.balances = balances;
1627
+ this.events.emit('SET_BALANCES', this.balances);
1997
1628
 
1998
- console.log(`⏱️ [PERF] Starting GetPortfolioBalances API call...`);
1999
- const apiCallStart = performance.now();
2000
- console.time('GetPortfolioBalances Response Time');
1629
+ // Build and emit dashboard
1630
+ const dashStart = performance.now();
1631
+ this.dashboard = this.buildDashboardFromBalances();
1632
+ this.events.emit('SET_DASHBOARD', this.dashboard);
1633
+ console.log(`⏱️ [PERF] Dashboard built in ${(performance.now() - dashStart).toFixed(0)}ms`);
1634
+ console.log(`📊 Dashboard: ${this.dashboard?.networks?.length || 0} networks, $${this.dashboard?.totalValueUsd?.toFixed(2) || '0.00'}`);
2001
1635
 
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
- }
1636
+ console.log(`⏱️ [PERF] Total: ${(performance.now() - apiStart).toFixed(0)}ms`);
1637
+ return this.balances;
2038
1638
 
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);
1639
+ } catch (e: any) {
1640
+ console.error(tag, 'Error:', e?.message || e);
2077
1641
  throw e;
2078
1642
  }
2079
1643
  };
2080
- this.getBalances = async function (forceRefresh?: boolean) {
1644
+ this.getBalances = async function (forceRefresh?: boolean, caip?: string) {
2081
1645
  const tag = `${TAG} | getBalances | `;
2082
1646
  try {
2083
- // Simply call the shared function with all blockchains
1647
+ // If CAIP is provided, refresh only that specific asset
1648
+ if (caip) {
1649
+ console.log(tag, `🎯 Refreshing single asset: ${caip}`);
1650
+ const networkId = caip.split('/')[0];
1651
+ console.log(tag, `📍 Target network: ${networkId}`);
1652
+
1653
+ const results = await this.getBalancesForNetworks([networkId], forceRefresh);
1654
+ return results.filter((b) => b.caip === caip || b.networkId === networkId);
1655
+ }
1656
+
1657
+ // Default: refresh all blockchains
2084
1658
  return await this.getBalancesForNetworks(this.blockchains, forceRefresh);
2085
1659
  } catch (e) {
2086
1660
  console.error(tag, 'Error in getBalances: ', e);
@@ -2090,16 +1664,8 @@ export class SDK {
2090
1664
  this.getBalance = async function (networkId: string) {
2091
1665
  const tag = `${TAG} | getBalance | `;
2092
1666
  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
1667
  // Call the shared function with a single-network array
2101
1668
  const results = await this.getBalancesForNetworks([networkId]);
2102
-
2103
1669
  // If needed, you can filter only those that match the specific network
2104
1670
  // (especially if you used wildcard eip155:*)
2105
1671
  const filtered = results.filter(
@@ -2111,11 +1677,6 @@ export class SDK {
2111
1677
  throw e;
2112
1678
  }
2113
1679
  };
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
1680
  this.getFees = async function (networkId: string): Promise<NormalizedFeeRates> {
2120
1681
  const tag = `${TAG} | getFees | `;
2121
1682
  try {
@@ -2239,171 +1800,47 @@ export class SDK {
2239
1800
  if (!asset.caip) throw Error('Invalid Asset! missing caip!');
2240
1801
  if (!asset.networkId) asset.networkId = caipToNetworkId(asset.caip);
2241
1802
 
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
-
1803
+ // Validate pubkeys for network (throws descriptive errors)
1804
+ validatePubkeysForNetwork(this.pubkeys, asset.networkId, asset.caip);
1805
+ const pubkeysForNetwork = findPubkeysForNetwork(this.pubkeys, asset.networkId);
2294
1806
  console.log(
2295
1807
  tag,
2296
1808
  `✅ Validated: Found ${pubkeysForNetwork.length} addresses for ${asset.networkId}`,
2297
1809
  );
2298
1810
 
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
- }
1811
+ // Fetch fresh market price
1812
+ const freshPriceUsd = await fetchMarketPrice(this.pioneer, asset.caip);
2321
1813
 
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
- }
1814
+ // Resolve asset info from multiple sources
1815
+ let assetInfo = resolveAssetInfo(this.assetsMap, assetData, asset);
2343
1816
 
2344
- // Look for price and balance information in balances
2345
- // CRITICAL: For UTXO chains, we need to aggregate ALL balances across all xpubs
1817
+ // Get matching balances
2346
1818
  const matchingBalances = this.balances.filter((b) => b.caip === asset.caip);
2347
1819
 
1820
+ // Extract price from balances if available
2348
1821
  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) {
1822
+ const priceValue = extractPriceFromBalances(matchingBalances);
1823
+ if (priceValue > 0) {
2364
1824
  console.log(tag, 'detected priceUsd from balance:', priceValue);
2365
1825
  assetInfo.priceUsd = priceValue;
2366
1826
  }
2367
1827
  }
2368
1828
 
2369
- // Override with fresh price if we got one from the API
2370
- if (freshPriceUsd && freshPriceUsd > 0) {
1829
+ // Override with fresh price and aggregate balances
1830
+ if (freshPriceUsd > 0) {
2371
1831
  assetInfo.priceUsd = freshPriceUsd;
2372
1832
  console.log(tag, '✅ Using fresh market price:', freshPriceUsd);
2373
1833
 
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
-
1834
+ const { totalBalance, totalValueUsd } = aggregateBalances(matchingBalances, asset.caip);
2387
1835
  assetInfo.balance = totalBalance.toString();
2388
1836
  assetInfo.valueUsd = totalValueUsd.toFixed(2);
2389
- console.log(tag, `Aggregated balance: ${totalBalance} (${totalValueUsd.toFixed(2)} USD)`);
2390
1837
  }
2391
1838
 
2392
1839
  // Filter balances and pubkeys for this asset
2393
1840
  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
- );
1841
+ const assetPubkeys = filterPubkeysForAsset(this.pubkeys, asset.caip, caipToNetworkId);
2404
1842
 
2405
1843
  // 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
1844
  const finalAssetContext = {
2408
1845
  ...assetInfo,
2409
1846
  ...asset,
@@ -2411,21 +1848,14 @@ export class SDK {
2411
1848
  balances: assetBalances,
2412
1849
  };
2413
1850
 
2414
- // If input has priceUsd of 0 but we found a valid price from balance, use the balance price
1851
+ // If input has priceUsd of 0 but we found a valid price, use the found price
2415
1852
  if ((!asset.priceUsd || asset.priceUsd === 0) && assetInfo.priceUsd && assetInfo.priceUsd > 0) {
2416
1853
  finalAssetContext.priceUsd = assetInfo.priceUsd;
2417
1854
  }
2418
1855
 
2419
1856
  // 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');
1857
+ if (freshPriceUsd > 0) {
1858
+ updateBalancesWithPrice(assetBalances, freshPriceUsd);
2429
1859
  }
2430
1860
 
2431
1861
  this.assetContext = finalAssetContext;
@@ -2439,38 +1869,6 @@ export class SDK {
2439
1869
  ) {
2440
1870
  // Get the native asset for this network
2441
1871
  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
1872
  // Set the native symbol
2475
1873
  this.assetContext.nativeSymbol = nativeSymbol;
2476
1874
 
@@ -2569,125 +1967,54 @@ export class SDK {
2569
1967
  this.setOutboundAssetContext = async function (asset?: any): Promise<any> {
2570
1968
  const tag = `${TAG} | setOutputAssetContext | `;
2571
1969
  try {
2572
- console.log(tag, '0. asset: ', asset);
1970
+ console.log(tag, 'asset:', asset);
2573
1971
  // Accept null
2574
1972
  if (!asset) {
2575
1973
  this.outboundAssetContext = null;
2576
1974
  return;
2577
1975
  }
2578
1976
 
2579
- console.log(tag, '1 asset: ', asset);
2580
-
2581
1977
  if (!asset.caip) throw Error('Invalid Asset! missing caip!');
2582
1978
  if (!asset.networkId) asset.networkId = caipToNetworkId(asset.caip);
2583
1979
 
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);
1980
+ console.log(tag, 'networkId:', asset.networkId);
2596
1981
 
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);
1982
+ // Get pubkey for network (uses utility helper)
1983
+ const pubkey = findPubkeyForNetwork(this.pubkeys, asset.networkId);
1984
+ if (!pubkey) throw Error('Invalid network! missing pubkey for network! ' + asset.networkId);
2607
1985
 
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
- }
1986
+ // Fetch fresh market price
1987
+ const freshPriceUsd = await fetchMarketPrice(this.pioneer, asset.caip);
2619
1988
 
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
- }
1989
+ // Resolve asset info from multiple sources
1990
+ let assetInfo = resolveAssetInfo(this.assetsMap, assetData, asset);
2635
1991
 
2636
- // Look for price and balance information in balances
2637
- // CRITICAL: For UTXO chains, we need to aggregate ALL balances across all xpubs
1992
+ // Get matching balances
2638
1993
  const matchingBalances = this.balances.filter((b) => b.caip === asset.caip);
2639
1994
 
1995
+ // Extract price from balances if available
2640
1996
  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) {
1997
+ const priceValue = extractPriceFromBalances(matchingBalances);
1998
+ if (priceValue > 0) {
2656
1999
  console.log(tag, 'detected priceUsd from balance:', priceValue);
2657
2000
  assetInfo.priceUsd = priceValue;
2658
2001
  }
2659
2002
  }
2660
2003
 
2661
- // Override with fresh price if we got one from the API
2662
- if (freshPriceUsd && freshPriceUsd > 0) {
2004
+ // Override with fresh price and aggregate balances
2005
+ if (freshPriceUsd > 0) {
2663
2006
  assetInfo.priceUsd = freshPriceUsd;
2664
2007
  console.log(tag, '✅ Using fresh market price:', freshPriceUsd);
2665
2008
 
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
-
2009
+ const { totalBalance, totalValueUsd } = aggregateBalances(matchingBalances, asset.caip);
2679
2010
  assetInfo.balance = totalBalance.toString();
2680
2011
  assetInfo.valueUsd = totalValueUsd.toFixed(2);
2681
- console.log(tag, `Aggregated balance: ${totalBalance} (${totalValueUsd.toFixed(2)} USD)`);
2682
2012
  }
2683
2013
 
2684
- console.log(tag, 'CHECKPOINT 1');
2685
-
2686
2014
  // Combine the user-provided asset with any additional info we have
2687
2015
  this.outboundAssetContext = { ...assetInfo, ...asset, ...pubkey };
2688
2016
 
2689
- console.log(tag, 'CHECKPOINT 3');
2690
- console.log(tag, 'outboundAssetContext: assetInfo: ', assetInfo);
2017
+ console.log(tag, 'outboundAssetContext set:', this.outboundAssetContext.caip);
2691
2018
 
2692
2019
  // Set outbound blockchain context based on asset
2693
2020
  if (asset.caip) {
@@ -2717,18 +2044,8 @@ export class SDK {
2717
2044
  ownerAddress: string;
2718
2045
  spenderAddress: string;
2719
2046
  }) => {
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
- }
2047
+ const result = await this.pioneer.GetTokenAllowance(params);
2048
+ return result.data;
2732
2049
  };
2733
2050
 
2734
2051
  /**
@@ -2742,18 +2059,8 @@ export class SDK {
2742
2059
  ownerAddress: string;
2743
2060
  amount: string;
2744
2061
  }) => {
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
- }
2062
+ const result = await this.pioneer.BuildApprovalTransaction(params);
2063
+ return result.data;
2757
2064
  };
2758
2065
  }
2759
2066