@pioneer-platform/pioneer-sdk 8.15.31 → 8.15.33
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 +229 -46
- package/dist/index.es.js +229 -46
- package/dist/index.js +229 -46
- package/package.json +6 -6
- package/src/fees/index.ts +1 -0
- package/src/index.ts +85 -8
- package/src/txbuilder/createUnsignedUxtoTx.ts +20 -3
- package/src/utils/build-dashboard.ts +86 -46
- package/src/utils/portfolio-helpers.ts +135 -1
|
@@ -23,7 +23,7 @@ export function buildDashboardFromBalances(balances: any[], blockchains: string[
|
|
|
23
23
|
networkPercentages: [],
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
let
|
|
26
|
+
let totalPortfolioValueCents = 0; // Use cents for integer math
|
|
27
27
|
const networksTemp: {
|
|
28
28
|
networkId: string;
|
|
29
29
|
totalValueUsd: number;
|
|
@@ -46,73 +46,81 @@ export function buildDashboardFromBalances(balances: any[], blockchains: string[
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// Deduplicate balances based on caip + pubkey combination
|
|
49
|
+
// NOTE: Each pubkey (xpub/ypub/zpub) represents a DIFFERENT address space with potentially
|
|
50
|
+
// different balances - they are NOT duplicates! Keep all of them.
|
|
49
51
|
const balanceMap = new Map();
|
|
50
52
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
77
|
-
}
|
|
53
|
+
// Standard deduplication for all networks (including Bitcoin)
|
|
54
|
+
// Only deduplicate if BOTH caip AND pubkey are identical (true duplicates)
|
|
55
|
+
filteredBalances.forEach((balance) => {
|
|
56
|
+
const key = `${balance.caip}_${balance.pubkey || 'default'}`;
|
|
57
|
+
|
|
58
|
+
// Only keep the first occurrence or the one with higher value for TRUE duplicates
|
|
59
|
+
if (
|
|
60
|
+
!balanceMap.has(key) ||
|
|
61
|
+
parseFloat(balance.valueUsd || '0') > parseFloat(balanceMap.get(key).valueUsd || '0')
|
|
62
|
+
) {
|
|
63
|
+
balanceMap.set(key, balance);
|
|
64
|
+
} else {
|
|
65
|
+
// DEBUG: Log when we skip a balance due to deduplication
|
|
66
|
+
const existing = balanceMap.get(key);
|
|
67
|
+
const existingValue = parseFloat(existing.valueUsd || '0');
|
|
68
|
+
const newValue = parseFloat(balance.valueUsd || '0');
|
|
69
|
+
|
|
70
|
+
// Always log duplicates being skipped (removed the 0.01 threshold)
|
|
71
|
+
console.log(`⚠️ [BUILD-DASHBOARD] ${blockchain} dedup skipped (same caip+pubkey):`, {
|
|
72
|
+
caip: balance.caip,
|
|
73
|
+
pubkey: (balance.pubkey || 'default').substring(0, 20),
|
|
74
|
+
existing: existingValue,
|
|
75
|
+
new: newValue,
|
|
76
|
+
diff: Math.abs(existingValue - newValue)
|
|
77
|
+
});
|
|
78
78
|
}
|
|
79
|
-
}
|
|
80
|
-
// Standard deduplication for non-Bitcoin networks
|
|
81
|
-
filteredBalances.forEach((balance) => {
|
|
82
|
-
const key = `${balance.caip}_${balance.pubkey || 'default'}`;
|
|
83
|
-
// Only keep the first occurrence or the one with higher value
|
|
84
|
-
if (
|
|
85
|
-
!balanceMap.has(key) ||
|
|
86
|
-
parseFloat(balance.valueUsd || '0') > parseFloat(balanceMap.get(key).valueUsd || '0')
|
|
87
|
-
) {
|
|
88
|
-
balanceMap.set(key, balance);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|
|
79
|
+
});
|
|
92
80
|
|
|
93
81
|
const networkBalances = Array.from(balanceMap.values());
|
|
94
82
|
|
|
95
83
|
// Ensure we're working with numbers for calculations
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
// Accumulate in cents (integer math) to avoid floating point errors
|
|
85
|
+
const networkTotalCents = networkBalances.reduce((sumCents, balance, idx) => {
|
|
86
|
+
// Normalize valueUsd to number type
|
|
87
|
+
let valueUsd =
|
|
98
88
|
typeof balance.valueUsd === 'string'
|
|
99
89
|
? parseFloat(balance.valueUsd)
|
|
100
90
|
: balance.valueUsd || 0;
|
|
101
91
|
|
|
92
|
+
// Check for NaN values which can cause calculation errors
|
|
93
|
+
if (isNaN(valueUsd)) {
|
|
94
|
+
console.warn(`⚠️ [BUILD-DASHBOARD] NaN value detected for ${balance.caip}:`, balance.valueUsd);
|
|
95
|
+
return sumCents;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Normalize the balance object to always use number type for valueUsd
|
|
99
|
+
balance.valueUsd = valueUsd;
|
|
100
|
+
|
|
101
|
+
// Convert to cents with higher precision to reduce rounding errors
|
|
102
|
+
// Use Math.round with extended precision, then scale back
|
|
103
|
+
const valueCents = Math.round(valueUsd * 100 * 1000) / 1000;
|
|
104
|
+
|
|
102
105
|
// Debug first few balances for each network
|
|
103
106
|
if (idx < 2) {
|
|
104
107
|
console.log(`💰 [BUILD-DASHBOARD] ${blockchain} balance #${idx}:`, {
|
|
105
108
|
caip: balance.caip,
|
|
106
109
|
balance: balance.balance,
|
|
107
110
|
valueUsd: balance.valueUsd,
|
|
111
|
+
valueUsdType: typeof balance.valueUsd,
|
|
108
112
|
parsedValueUsd: valueUsd,
|
|
109
|
-
|
|
113
|
+
valueCents: valueCents,
|
|
114
|
+
runningSumCents: sumCents + valueCents
|
|
110
115
|
});
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
return
|
|
118
|
+
return sumCents + valueCents;
|
|
114
119
|
}, 0);
|
|
115
120
|
|
|
121
|
+
// Convert back to dollars
|
|
122
|
+
const networkTotal = networkTotalCents / 100;
|
|
123
|
+
|
|
116
124
|
console.log(`💰 [BUILD-DASHBOARD] ${blockchain} totals:`, {
|
|
117
125
|
balancesCount: networkBalances.length,
|
|
118
126
|
networkTotal,
|
|
@@ -149,9 +157,13 @@ export function buildDashboardFromBalances(balances: any[], blockchains: string[
|
|
|
149
157
|
totalNativeBalance,
|
|
150
158
|
});
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
// Add to total using cents (integer math) to avoid floating point errors
|
|
161
|
+
totalPortfolioValueCents += networkTotalCents;
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
// Convert total from cents to dollars
|
|
165
|
+
const totalPortfolioValue = totalPortfolioValueCents / 100;
|
|
166
|
+
|
|
155
167
|
// Sort networks by USD value and assign to dashboard
|
|
156
168
|
dashboardData.networks = networksTemp.sort((a, b) => b.totalValueUsd - a.totalValueUsd);
|
|
157
169
|
dashboardData.totalValueUsd = totalPortfolioValue;
|
|
@@ -172,5 +184,33 @@ export function buildDashboardFromBalances(balances: any[], blockchains: string[
|
|
|
172
184
|
dashboardData.networks.length
|
|
173
185
|
} networks, $${totalPortfolioValue.toFixed(2)} total`,
|
|
174
186
|
);
|
|
187
|
+
|
|
188
|
+
// ===== BALANCE RECONCILIATION CHECK =====
|
|
189
|
+
// Verify that dashboard total matches the sum of all input balances
|
|
190
|
+
const inputBalancesTotal = balances.reduce((sum, b) => {
|
|
191
|
+
const value = typeof b.valueUsd === 'string' ? parseFloat(b.valueUsd) : (b.valueUsd || 0);
|
|
192
|
+
return sum + (isNaN(value) ? 0 : value);
|
|
193
|
+
}, 0);
|
|
194
|
+
|
|
195
|
+
const difference = Math.abs(inputBalancesTotal - totalPortfolioValue);
|
|
196
|
+
|
|
197
|
+
if (difference > 0.01) {
|
|
198
|
+
console.error(`🚨 [BUILD-DASHBOARD] BALANCE MISMATCH DETECTED!`);
|
|
199
|
+
console.error(` Input balances total: $${inputBalancesTotal.toFixed(2)} USD`);
|
|
200
|
+
console.error(` Dashboard total: $${totalPortfolioValue.toFixed(2)} USD`);
|
|
201
|
+
console.error(` Difference: $${difference.toFixed(2)} USD`);
|
|
202
|
+
console.error(` This indicates lost balances during dashboard aggregation!`);
|
|
203
|
+
|
|
204
|
+
// Log per-network breakdown to help debug
|
|
205
|
+
console.error(` Network breakdown:`);
|
|
206
|
+
dashboardData.networks.forEach(network => {
|
|
207
|
+
console.error(` ${network.networkId}: $${network.totalValueUsd.toFixed(2)}`);
|
|
208
|
+
});
|
|
209
|
+
} else if (difference > 0.001) {
|
|
210
|
+
console.warn(`⚠️ [BUILD-DASHBOARD] Minor balance rounding difference: $${difference.toFixed(4)} USD`);
|
|
211
|
+
} else {
|
|
212
|
+
console.log(`✅ [BUILD-DASHBOARD] Balance reconciliation passed (diff: $${difference.toFixed(4)})`);
|
|
213
|
+
}
|
|
214
|
+
|
|
175
215
|
return dashboardData;
|
|
176
216
|
}
|
|
@@ -3,6 +3,100 @@
|
|
|
3
3
|
import { logger as log } from './logger.js';
|
|
4
4
|
const TAG = ' | portfolio-helpers | ';
|
|
5
5
|
|
|
6
|
+
// CAIP-based explorer URL mapping
|
|
7
|
+
const EXPLORER_BASE_URLS: Record<string, { address: string; tx: string }> = {
|
|
8
|
+
// Ethereum mainnet
|
|
9
|
+
'eip155:1': {
|
|
10
|
+
address: 'https://etherscan.io/address/',
|
|
11
|
+
tx: 'https://etherscan.io/tx/'
|
|
12
|
+
},
|
|
13
|
+
// Polygon
|
|
14
|
+
'eip155:137': {
|
|
15
|
+
address: 'https://polygonscan.com/address/',
|
|
16
|
+
tx: 'https://polygonscan.com/tx/'
|
|
17
|
+
},
|
|
18
|
+
// Base
|
|
19
|
+
'eip155:8453': {
|
|
20
|
+
address: 'https://basescan.org/address/',
|
|
21
|
+
tx: 'https://basescan.org/tx/'
|
|
22
|
+
},
|
|
23
|
+
// BSC
|
|
24
|
+
'eip155:56': {
|
|
25
|
+
address: 'https://bscscan.com/address/',
|
|
26
|
+
tx: 'https://bscscan.com/tx/'
|
|
27
|
+
},
|
|
28
|
+
// Monad
|
|
29
|
+
'eip155:41454': {
|
|
30
|
+
address: 'https://explorer.monad.xyz/address/',
|
|
31
|
+
tx: 'https://explorer.monad.xyz/tx/'
|
|
32
|
+
},
|
|
33
|
+
// Hyperliquid
|
|
34
|
+
'eip155:2868': {
|
|
35
|
+
address: 'https://app.hyperliquid.xyz/explorer/address/',
|
|
36
|
+
tx: 'https://app.hyperliquid.xyz/explorer/tx/'
|
|
37
|
+
},
|
|
38
|
+
// Bitcoin
|
|
39
|
+
'bip122:000000000019d6689c085ae165831e93': {
|
|
40
|
+
address: 'https://blockstream.info/address/',
|
|
41
|
+
tx: 'https://blockstream.info/tx/'
|
|
42
|
+
},
|
|
43
|
+
// Litecoin
|
|
44
|
+
'bip122:12a765e31ffd4059bada1e25190f6e98': {
|
|
45
|
+
address: 'https://blockchair.com/litecoin/address/',
|
|
46
|
+
tx: 'https://blockchair.com/litecoin/transaction/'
|
|
47
|
+
},
|
|
48
|
+
// Dogecoin
|
|
49
|
+
'bip122:00000000001a91e3dace36e2be3bf030': {
|
|
50
|
+
address: 'https://dogechain.info/address/',
|
|
51
|
+
tx: 'https://dogechain.info/tx/'
|
|
52
|
+
},
|
|
53
|
+
// Bitcoin Cash
|
|
54
|
+
'bip122:000000000000000000651ef99cb9fcbe': {
|
|
55
|
+
address: 'https://blockchair.com/bitcoin-cash/address/',
|
|
56
|
+
tx: 'https://blockchair.com/bitcoin-cash/transaction/'
|
|
57
|
+
},
|
|
58
|
+
// Dash
|
|
59
|
+
'bip122:000007d91d1254d60e2dd1ae58038307': {
|
|
60
|
+
address: 'https://chainz.cryptoid.info/dash/address.dws?',
|
|
61
|
+
tx: 'https://chainz.cryptoid.info/dash/tx.dws?'
|
|
62
|
+
},
|
|
63
|
+
// DigiByte
|
|
64
|
+
'bip122:4da631f2ac1bed857bd968c67c913978': {
|
|
65
|
+
address: 'https://digiexplorer.info/address/',
|
|
66
|
+
tx: 'https://digiexplorer.info/tx/'
|
|
67
|
+
},
|
|
68
|
+
// Zcash
|
|
69
|
+
'bip122:00040fe8ec8471911baa1db1266ea15d': {
|
|
70
|
+
address: 'https://explorer.zcha.in/accounts/',
|
|
71
|
+
tx: 'https://explorer.zcha.in/transactions/'
|
|
72
|
+
},
|
|
73
|
+
// Cosmos Hub
|
|
74
|
+
'cosmos:cosmoshub-4': {
|
|
75
|
+
address: 'https://www.mintscan.io/cosmos/address/',
|
|
76
|
+
tx: 'https://www.mintscan.io/cosmos/tx/'
|
|
77
|
+
},
|
|
78
|
+
// Osmosis
|
|
79
|
+
'cosmos:osmosis-1': {
|
|
80
|
+
address: 'https://www.mintscan.io/osmosis/address/',
|
|
81
|
+
tx: 'https://www.mintscan.io/osmosis/tx/'
|
|
82
|
+
},
|
|
83
|
+
// THORChain
|
|
84
|
+
'cosmos:thorchain-mainnet-v1': {
|
|
85
|
+
address: 'https://thorchain.net/address/',
|
|
86
|
+
tx: 'https://thorchain.net/tx/'
|
|
87
|
+
},
|
|
88
|
+
// Maya Protocol
|
|
89
|
+
'cosmos:mayachain-mainnet-v1': {
|
|
90
|
+
address: 'https://www.mayascan.org/address/',
|
|
91
|
+
tx: 'https://www.mayascan.org/tx/'
|
|
92
|
+
},
|
|
93
|
+
// Ripple
|
|
94
|
+
'ripple:4109c6f2045fc7eff4cde8f9905d19c2': {
|
|
95
|
+
address: 'https://xrpscan.com/account/',
|
|
96
|
+
tx: 'https://xrpscan.com/tx/'
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
6
100
|
export function isCacheDataValid(portfolioData: any): boolean {
|
|
7
101
|
// Check if networks data is reasonable (should be < 50 networks, not thousands)
|
|
8
102
|
if (!portfolioData.networks || !Array.isArray(portfolioData.networks)) {
|
|
@@ -220,14 +314,54 @@ export function enrichBalancesWithAssetInfo(
|
|
|
220
314
|
continue;
|
|
221
315
|
}
|
|
222
316
|
|
|
317
|
+
// ===== CRITICAL: Normalize valueUsd to NUMBER type =====
|
|
318
|
+
// Ensures consistent type throughout the SDK to prevent calculation errors
|
|
319
|
+
if (typeof balance.valueUsd === 'string') {
|
|
320
|
+
balance.valueUsd = parseFloat(balance.valueUsd) || 0;
|
|
321
|
+
} else if (typeof balance.valueUsd !== 'number') {
|
|
322
|
+
balance.valueUsd = 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Also normalize balance amount to number
|
|
326
|
+
if (typeof balance.balance === 'string') {
|
|
327
|
+
balance.balance = balance.balance; // Keep as string for precision (e.g., "0.00000001")
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Normalize priceUsd to number as well
|
|
331
|
+
if (typeof balance.priceUsd === 'string') {
|
|
332
|
+
balance.priceUsd = parseFloat(balance.priceUsd) || 0;
|
|
333
|
+
} else if (typeof balance.priceUsd !== 'number') {
|
|
334
|
+
balance.priceUsd = 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Get networkId for explorer lookup
|
|
338
|
+
const networkId = caipToNetworkId(balance.caip);
|
|
339
|
+
const explorerUrls = EXPLORER_BASE_URLS[networkId];
|
|
340
|
+
|
|
341
|
+
// Generate explorer links if we have a pubkey/address and explorer URLs for this network
|
|
342
|
+
const explorerAddressLink = (explorerUrls && balance.pubkey)
|
|
343
|
+
? explorerUrls.address + balance.pubkey
|
|
344
|
+
: undefined;
|
|
345
|
+
|
|
346
|
+
const explorerTxLink = explorerUrls?.tx; // Base URL for txs, actual txid added later
|
|
347
|
+
|
|
223
348
|
Object.assign(balance, assetInfo, {
|
|
224
349
|
type: balance.type || assetInfo.type,
|
|
225
350
|
isNative: balance.isNative ?? assetInfo.isNative,
|
|
226
|
-
networkId
|
|
351
|
+
networkId,
|
|
227
352
|
icon: assetInfo.icon || 'https://pioneers.dev/coins/etherum.png',
|
|
228
353
|
identifier: `${balance.caip}:${balance.pubkey}`,
|
|
229
354
|
updated: Date.now(),
|
|
230
355
|
color: assetInfo.color,
|
|
356
|
+
// CRITICAL: Always use decimals from assetInfo if available
|
|
357
|
+
decimals: assetInfo.decimals || balance.decimals || 8,
|
|
358
|
+
// Add explorer links
|
|
359
|
+
explorerAddressLink,
|
|
360
|
+
explorerTxLink,
|
|
361
|
+
explorer: explorerUrls?.address, // Base explorer URL
|
|
362
|
+
// Ensure these are numbers (redundant but explicit)
|
|
363
|
+
valueUsd: balance.valueUsd,
|
|
364
|
+
priceUsd: balance.priceUsd,
|
|
231
365
|
});
|
|
232
366
|
|
|
233
367
|
enrichedBalances.push(balance);
|