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