@pioneer-platform/pioneer-sdk 8.15.14 → 8.15.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +673 -649
- package/dist/index.es.js +674 -650
- package/dist/index.js +674 -650
- package/package.json +2 -3
- package/src/index.ts +223 -915
- package/src/utils/fee-reserves.ts +162 -0
- package/src/utils/logger.ts +21 -0
- package/src/utils/network-helpers.ts +85 -0
- package/src/utils/path-discovery.ts +67 -0
- package/src/utils/portfolio-helpers.ts +209 -0
- package/src/utils/pubkey-management.ts +124 -0
- package/src/utils/pubkey-sync.ts +85 -0
- package/src/utils/sync-state.ts +93 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fee Reserve Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides conservative fee reserves for max amount calculations.
|
|
5
|
+
* These reserves ensure users don't send their entire balance and leave
|
|
6
|
+
* insufficient funds for transaction fees.
|
|
7
|
+
*
|
|
8
|
+
* Integration with fees module planned for dynamic reserve calculation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const TAG = ' | Pioneer-sdk | fee-reserves | ';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fee reserve configuration for each network
|
|
15
|
+
* Values are conservative estimates in native currency units
|
|
16
|
+
*/
|
|
17
|
+
const FEE_RESERVE_MAP: Record<string, number> = {
|
|
18
|
+
// Bitcoin (BTC)
|
|
19
|
+
'bip122:000000000019d6689c085ae165831e93/slip44:0': 0.00005, // ~$3-5 at current prices
|
|
20
|
+
|
|
21
|
+
// Ethereum (ETH)
|
|
22
|
+
'eip155:1/slip44:60': 0.001, // ~$2-4 for ERC20 transfer at 20 gwei
|
|
23
|
+
|
|
24
|
+
// THORChain (RUNE)
|
|
25
|
+
'cosmos:thorchain-mainnet-v1/slip44:931': 0.02, // Network standard fee
|
|
26
|
+
|
|
27
|
+
// Dogecoin (DOGE)
|
|
28
|
+
'bip122:00000000001a91e3dace36e2be3bf030/slip44:3': 1, // ~$0.10-0.20, DOGE has cheap fees
|
|
29
|
+
|
|
30
|
+
// Dash (DASH)
|
|
31
|
+
'bip122:000007d91d1254d60e2dd1ae58038307/slip44:5': 0.001, // ~$0.03-0.05
|
|
32
|
+
|
|
33
|
+
// Bitcoin Cash (BCH)
|
|
34
|
+
'bip122:000000000000000000651ef99cb9fcbe/slip44:145': 0.0005, // ~$0.20-0.40
|
|
35
|
+
|
|
36
|
+
// Litecoin (LTC)
|
|
37
|
+
'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2': 0.001, // ~$0.10-0.20
|
|
38
|
+
|
|
39
|
+
// DigiByte (DGB)
|
|
40
|
+
'bip122:4da631f2ac1bed857bd968c67c913978/slip44:20': 0.1, // Very cheap fees
|
|
41
|
+
|
|
42
|
+
// Cosmos (ATOM)
|
|
43
|
+
'cosmos:cosmoshub-4/slip44:118': 0.005, // Network standard fee
|
|
44
|
+
|
|
45
|
+
// Osmosis (OSMO)
|
|
46
|
+
'cosmos:osmosis-1/slip44:118': 0.035, // Network standard fee
|
|
47
|
+
|
|
48
|
+
// Maya (CACAO)
|
|
49
|
+
'cosmos:mayachain-mainnet-v1/slip44:931': 0.5, // Network standard fee
|
|
50
|
+
|
|
51
|
+
// BNB Smart Chain (BNB)
|
|
52
|
+
'eip155:56/slip44:60': 0.001, // ~$0.50-1.00 for BEP20 transfer
|
|
53
|
+
|
|
54
|
+
// Polygon (MATIC)
|
|
55
|
+
'eip155:137/slip44:60': 0.01, // ~$0.01-0.02, very cheap
|
|
56
|
+
|
|
57
|
+
// Avalanche (AVAX)
|
|
58
|
+
'eip155:43114/slip44:60': 0.01, // ~$0.30-0.60
|
|
59
|
+
|
|
60
|
+
// Optimism (OP/ETH)
|
|
61
|
+
'eip155:10/slip44:60': 0.0001, // L2, very cheap
|
|
62
|
+
|
|
63
|
+
// Base (ETH)
|
|
64
|
+
'eip155:8453/slip44:60': 0.0001, // L2, very cheap
|
|
65
|
+
|
|
66
|
+
// Arbitrum (ETH)
|
|
67
|
+
'eip155:42161/slip44:60': 0.0001, // L2, very cheap
|
|
68
|
+
|
|
69
|
+
// Ripple (XRP)
|
|
70
|
+
'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144': 0.00001, // Fixed network fee
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Default fee reserve for unknown networks
|
|
75
|
+
* Uses a conservative estimate suitable for most chains
|
|
76
|
+
*/
|
|
77
|
+
const DEFAULT_FEE_RESERVE = 0.001;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get fee reserve for a given network (CAIP identifier)
|
|
81
|
+
*
|
|
82
|
+
* @param caip - CAIP identifier (e.g., 'bip122:000000000019d6689c085ae165831e93/slip44:0')
|
|
83
|
+
* @returns Fee reserve amount in native currency units
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const reserve = getFeeReserve('eip155:1/slip44:60'); // 0.001 ETH
|
|
88
|
+
* const maxSendable = balance - reserve;
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function getFeeReserve(caip: string): number {
|
|
92
|
+
// Direct lookup
|
|
93
|
+
if (FEE_RESERVE_MAP[caip]) {
|
|
94
|
+
return FEE_RESERVE_MAP[caip];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// EVM wildcard matching (all EVM chains not explicitly defined)
|
|
98
|
+
if (caip.startsWith('eip155:') && caip.includes('/slip44:60')) {
|
|
99
|
+
// Use Ethereum's reserve as default for all EVM chains
|
|
100
|
+
return FEE_RESERVE_MAP['eip155:1/slip44:60'] || 0.001;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Cosmos SDK chains
|
|
104
|
+
if (caip.startsWith('cosmos:')) {
|
|
105
|
+
// Conservative default for Cosmos chains
|
|
106
|
+
return 0.01;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// UTXO chains
|
|
110
|
+
if (caip.startsWith('bip122:')) {
|
|
111
|
+
// Conservative default for UTXO chains
|
|
112
|
+
return 0.0001;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Unknown network - use conservative default
|
|
116
|
+
console.warn(TAG, `No fee reserve defined for ${caip}, using default: ${DEFAULT_FEE_RESERVE}`);
|
|
117
|
+
return DEFAULT_FEE_RESERVE;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Calculate max sendable amount for a given balance
|
|
122
|
+
* Subtracts conservative fee reserve to ensure transaction can be completed
|
|
123
|
+
*
|
|
124
|
+
* @param balance - Total balance in native units
|
|
125
|
+
* @param caip - CAIP identifier for the network
|
|
126
|
+
* @returns Maximum amount that can be safely sent
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const maxSendable = getMaxSendableAmount(1.5, 'eip155:1/slip44:60');
|
|
131
|
+
* // Returns 1.499 (1.5 - 0.001 ETH reserve)
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export function getMaxSendableAmount(balance: number, caip: string): number {
|
|
135
|
+
const reserve = getFeeReserve(caip);
|
|
136
|
+
const maxAmount = Math.max(0, balance - reserve);
|
|
137
|
+
|
|
138
|
+
console.log(TAG, `Max sendable for ${caip}: ${maxAmount} (balance: ${balance}, reserve: ${reserve})`);
|
|
139
|
+
|
|
140
|
+
return maxAmount;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if balance is sufficient for a transaction
|
|
145
|
+
*
|
|
146
|
+
* @param balance - Available balance
|
|
147
|
+
* @param amount - Amount to send
|
|
148
|
+
* @param caip - CAIP identifier
|
|
149
|
+
* @returns true if balance covers amount + fees
|
|
150
|
+
*/
|
|
151
|
+
export function hasSufficientBalance(balance: number, amount: number, caip: string): boolean {
|
|
152
|
+
const reserve = getFeeReserve(caip);
|
|
153
|
+
return balance >= (amount + reserve);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get fee reserve configuration for display/debugging
|
|
158
|
+
* @returns Complete fee reserve map
|
|
159
|
+
*/
|
|
160
|
+
export function getFeeReserveMap(): Readonly<Record<string, number>> {
|
|
161
|
+
return FEE_RESERVE_MAP;
|
|
162
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Simple client-side logger for Pioneer SDK
|
|
2
|
+
// Replaces server-side loggerdog dependency
|
|
3
|
+
|
|
4
|
+
const DEBUG = process.env.DEBUG === 'true' || typeof window !== 'undefined' && (window as any).DEBUG;
|
|
5
|
+
|
|
6
|
+
export const logger = {
|
|
7
|
+
info: (tag: string, ...args: any[]) => {
|
|
8
|
+
console.log(`[INFO] ${tag}`, ...args);
|
|
9
|
+
},
|
|
10
|
+
debug: (tag: string, ...args: any[]) => {
|
|
11
|
+
if (DEBUG) {
|
|
12
|
+
console.log(`[DEBUG] ${tag}`, ...args);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
warn: (tag: string, ...args: any[]) => {
|
|
16
|
+
console.warn(`[WARN] ${tag}`, ...args);
|
|
17
|
+
},
|
|
18
|
+
error: (tag: string, ...args: any[]) => {
|
|
19
|
+
console.error(`[ERROR] ${tag}`, ...args);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Network matching utilities
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if item (path or pubkey) matches network with EVM wildcard support
|
|
5
|
+
*/
|
|
6
|
+
export function matchesNetwork(item: any, networkId: string): boolean {
|
|
7
|
+
if (!item.networks || !Array.isArray(item.networks)) return false;
|
|
8
|
+
if (item.networks.includes(networkId)) return true;
|
|
9
|
+
if (networkId.startsWith('eip155:') && item.networks.includes('eip155:*')) return true;
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize network ID for EVM chains (converts specific chain to wildcard)
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeNetworkId(networkId: string): string {
|
|
17
|
+
return networkId.includes('eip155:') ? 'eip155:*' : networkId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find pubkeys that match a specific network
|
|
22
|
+
*/
|
|
23
|
+
export function findPubkeysForNetwork(
|
|
24
|
+
pubkeys: any[],
|
|
25
|
+
networkId: string,
|
|
26
|
+
paths: any[],
|
|
27
|
+
tag: string = ''
|
|
28
|
+
): any[] {
|
|
29
|
+
const isEip155 = networkId.includes('eip155');
|
|
30
|
+
|
|
31
|
+
// Primary query: find pubkeys with matching networks field
|
|
32
|
+
let matchingPubkeys = pubkeys.filter((pubkey) =>
|
|
33
|
+
pubkey.networks?.some((network: string) =>
|
|
34
|
+
isEip155 ? network.startsWith('eip155:') : network === networkId
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Fallback: if no pubkeys found, try path matching
|
|
39
|
+
if (matchingPubkeys.length === 0) {
|
|
40
|
+
if (tag) console.warn(tag, `⚠️ No pubkeys found for ${networkId}, attempting path fallback`);
|
|
41
|
+
|
|
42
|
+
const pathsForNetwork = paths.filter(p =>
|
|
43
|
+
p.networks?.includes(networkId) ||
|
|
44
|
+
(isEip155 && p.networks?.includes('eip155:*'))
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
for (const path of pathsForNetwork) {
|
|
48
|
+
const matchingPubkey = pubkeys.find(pk =>
|
|
49
|
+
JSON.stringify(pk.addressNList) === JSON.stringify(path.addressNList)
|
|
50
|
+
);
|
|
51
|
+
if (matchingPubkey) {
|
|
52
|
+
if (tag) console.warn(tag, `✓ Found via path: ${matchingPubkey.note || matchingPubkey.pubkey.slice(0, 10)}`);
|
|
53
|
+
matchingPubkeys.push(matchingPubkey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (tag && matchingPubkeys.length > 0) {
|
|
58
|
+
console.warn(tag, `✅ Fallback found ${matchingPubkeys.length} pubkeys for ${networkId}`);
|
|
59
|
+
} else if (tag) {
|
|
60
|
+
console.error(tag, `❌ No pubkeys found for ${networkId}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return matchingPubkeys;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate pubkeys have required networks field
|
|
69
|
+
*/
|
|
70
|
+
export function validatePubkeysNetworks(pubkeys: any[], tag: string = ''): {
|
|
71
|
+
valid: any[];
|
|
72
|
+
invalid: any[];
|
|
73
|
+
} {
|
|
74
|
+
const valid = pubkeys.filter(p => p.networks && Array.isArray(p.networks));
|
|
75
|
+
const invalid = pubkeys.filter(p => !p.networks || !Array.isArray(p.networks));
|
|
76
|
+
|
|
77
|
+
if (tag && invalid.length > 0) {
|
|
78
|
+
console.warn(tag, `⚠️ ${invalid.length} pubkeys missing networks field`);
|
|
79
|
+
invalid.forEach(pk => {
|
|
80
|
+
console.warn(tag, ` - ${pk.note || pk.pubkey.slice(0, 10)}: networks=${pk.networks}`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { valid, invalid };
|
|
85
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Path discovery utilities for blockchain synchronization
|
|
2
|
+
|
|
3
|
+
import { getPaths } from '@pioneer-platform/pioneer-coins';
|
|
4
|
+
import { matchesNetwork, normalizeNetworkId } from './network-helpers.js';
|
|
5
|
+
import { logger as log } from './logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ensure paths exist for all blockchains
|
|
9
|
+
* Discovers and adds missing paths automatically
|
|
10
|
+
*/
|
|
11
|
+
export async function ensurePathsForBlockchains(
|
|
12
|
+
blockchains: string[],
|
|
13
|
+
currentPaths: any[],
|
|
14
|
+
tag: string
|
|
15
|
+
): Promise<any[]> {
|
|
16
|
+
let allPaths = [...currentPaths];
|
|
17
|
+
|
|
18
|
+
for (const blockchain of blockchains) {
|
|
19
|
+
const networkId = normalizeNetworkId(blockchain);
|
|
20
|
+
const existingPaths = allPaths.filter(path => matchesNetwork(path, networkId));
|
|
21
|
+
|
|
22
|
+
if (existingPaths.length === 0) {
|
|
23
|
+
log.info(tag, `Discovering paths for ${networkId}...`);
|
|
24
|
+
const newPaths = getPaths([networkId]);
|
|
25
|
+
|
|
26
|
+
if (!newPaths || newPaths.length === 0) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Path discovery failed for ${networkId}. ` +
|
|
29
|
+
`Available blockchains: ${blockchains.join(', ')}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
log.debug(tag, `Added ${newPaths.length} paths for ${networkId}`);
|
|
34
|
+
allPaths = allPaths.concat(newPaths);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return allPaths;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate that paths exist for a blockchain
|
|
43
|
+
* Throws descriptive error if validation fails
|
|
44
|
+
*/
|
|
45
|
+
export function validatePathsForBlockchain(
|
|
46
|
+
paths: any[],
|
|
47
|
+
blockchain: string,
|
|
48
|
+
tag: string = ''
|
|
49
|
+
): void {
|
|
50
|
+
const networkId = normalizeNetworkId(blockchain);
|
|
51
|
+
const pathsForChain = paths.filter(path => matchesNetwork(path, networkId));
|
|
52
|
+
|
|
53
|
+
if (pathsForChain.length === 0) {
|
|
54
|
+
const availableNetworks = [
|
|
55
|
+
...new Set(paths.flatMap(p => p.networks || []))
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
throw new Error(
|
|
59
|
+
`No paths found for blockchain ${networkId}. ` +
|
|
60
|
+
`Available paths for: ${availableNetworks.join(', ')}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (tag) {
|
|
65
|
+
log.debug(tag, `Validated ${pathsForChain.length} paths for ${networkId}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Portfolio validation and helper utilities
|
|
2
|
+
|
|
3
|
+
import { logger as log } from './logger.js';
|
|
4
|
+
const TAG = ' | portfolio-helpers | ';
|
|
5
|
+
|
|
6
|
+
export function isCacheDataValid(portfolioData: any): boolean {
|
|
7
|
+
// Check if networks data is reasonable (should be < 50 networks, not thousands)
|
|
8
|
+
if (!portfolioData.networks || !Array.isArray(portfolioData.networks)) {
|
|
9
|
+
console.warn('[CACHE VALIDATION] Networks is not an array');
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (portfolioData.networks.length > 50) {
|
|
14
|
+
console.error(
|
|
15
|
+
`[CACHE VALIDATION] CORRUPTED: ${portfolioData.networks.length} networks (should be < 50)`,
|
|
16
|
+
);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if at least some networks have required fields
|
|
21
|
+
const validNetworks = portfolioData.networks.filter(
|
|
22
|
+
(n: any) => n.networkId && n.totalValueUsd !== undefined && n.gasAssetSymbol,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (validNetworks.length === 0 && portfolioData.networks.length > 0) {
|
|
26
|
+
console.error('[CACHE VALIDATION] CORRUPTED: No networks have required fields');
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(
|
|
31
|
+
`[CACHE VALIDATION] Found ${portfolioData.networks.length} networks, ${validNetworks.length} valid`,
|
|
32
|
+
);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch fresh market price for an asset
|
|
38
|
+
*/
|
|
39
|
+
export async function fetchMarketPrice(pioneer: any, caip: string): Promise<number> {
|
|
40
|
+
const tag = TAG + ' | fetchMarketPrice | ';
|
|
41
|
+
try {
|
|
42
|
+
// Validate CAIP before calling API
|
|
43
|
+
if (!caip || typeof caip !== 'string' || !caip.includes(':')) {
|
|
44
|
+
log.warn(tag, 'Invalid or missing CAIP, skipping market price fetch:', caip);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log.debug(tag, 'Fetching fresh market price for:', caip);
|
|
49
|
+
const marketData = await pioneer.GetMarketInfo([caip]);
|
|
50
|
+
log.debug(tag, 'Market data response:', marketData);
|
|
51
|
+
|
|
52
|
+
if (marketData && marketData.data && marketData.data.length > 0) {
|
|
53
|
+
const price = marketData.data[0];
|
|
54
|
+
log.debug(tag, '✅ Fresh market price:', price);
|
|
55
|
+
return price;
|
|
56
|
+
} else {
|
|
57
|
+
log.warn(tag, 'No market data returned for:', caip);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
} catch (marketError) {
|
|
61
|
+
log.error(tag, 'Error fetching market price:', marketError);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract price from balance entries
|
|
68
|
+
*/
|
|
69
|
+
export function extractPriceFromBalances(balances: any[]): number {
|
|
70
|
+
const tag = TAG + ' | extractPriceFromBalances | ';
|
|
71
|
+
if (balances.length === 0) return 0;
|
|
72
|
+
|
|
73
|
+
// Use price from first balance entry (all should have same price)
|
|
74
|
+
let priceValue = balances[0].priceUsd || balances[0].price;
|
|
75
|
+
|
|
76
|
+
// If no price but we have valueUsd and balance, calculate the price
|
|
77
|
+
if ((!priceValue || priceValue === 0) && balances[0].valueUsd && balances[0].balance) {
|
|
78
|
+
const balance = parseFloat(balances[0].balance);
|
|
79
|
+
const valueUsd = parseFloat(balances[0].valueUsd);
|
|
80
|
+
if (balance > 0 && valueUsd > 0) {
|
|
81
|
+
priceValue = valueUsd / balance;
|
|
82
|
+
log.debug(tag, 'Calculated priceUsd from valueUsd/balance:', priceValue);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return priceValue || 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Aggregate balances for an asset (critical for UTXO chains with multiple xpubs)
|
|
91
|
+
*/
|
|
92
|
+
export function aggregateBalances(balances: any[], caip: string): {
|
|
93
|
+
totalBalance: number;
|
|
94
|
+
totalValueUsd: number;
|
|
95
|
+
} {
|
|
96
|
+
const tag = TAG + ' | aggregateBalances | ';
|
|
97
|
+
let totalBalance = 0;
|
|
98
|
+
let totalValueUsd = 0;
|
|
99
|
+
|
|
100
|
+
log.debug(tag, `Aggregating ${balances.length} balance entries for ${caip}`);
|
|
101
|
+
for (const balanceEntry of balances) {
|
|
102
|
+
const balance = parseFloat(balanceEntry.balance) || 0;
|
|
103
|
+
const valueUsd = parseFloat(balanceEntry.valueUsd) || 0;
|
|
104
|
+
totalBalance += balance;
|
|
105
|
+
totalValueUsd += valueUsd;
|
|
106
|
+
log.debug(tag, ` Balance entry: ${balance} (${valueUsd} USD)`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log.debug(tag, `Aggregated balance: ${totalBalance} (${totalValueUsd.toFixed(2)} USD)`);
|
|
110
|
+
return { totalBalance, totalValueUsd };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Update balances with fresh price data
|
|
115
|
+
*/
|
|
116
|
+
export function updateBalancesWithPrice(balances: any[], freshPriceUsd: number): void {
|
|
117
|
+
const tag = TAG + ' | updateBalancesWithPrice | ';
|
|
118
|
+
for (const balance of balances) {
|
|
119
|
+
balance.price = freshPriceUsd;
|
|
120
|
+
balance.priceUsd = freshPriceUsd;
|
|
121
|
+
// Recalculate valueUsd with fresh price
|
|
122
|
+
const balanceAmount = parseFloat(balance.balance || 0);
|
|
123
|
+
balance.valueUsd = (balanceAmount * freshPriceUsd).toString();
|
|
124
|
+
}
|
|
125
|
+
log.debug(tag, 'Updated all balances with fresh price data');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildDashboardFromPortfolioData(portfolioData: any) {
|
|
129
|
+
const cacheAge = portfolioData.lastUpdated
|
|
130
|
+
? Math.floor((Date.now() - portfolioData.lastUpdated) / 1000)
|
|
131
|
+
: 0;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
totalValueUsd: portfolioData.totalValueUsd,
|
|
135
|
+
pairedDevices: portfolioData.pairedDevices,
|
|
136
|
+
devices: portfolioData.devices || [],
|
|
137
|
+
networks: portfolioData.networks || [],
|
|
138
|
+
assets: portfolioData.assets || [],
|
|
139
|
+
statistics: portfolioData.statistics || {},
|
|
140
|
+
cached: portfolioData.cached,
|
|
141
|
+
lastUpdated: portfolioData.lastUpdated,
|
|
142
|
+
cacheAge,
|
|
143
|
+
networkPercentages:
|
|
144
|
+
portfolioData.networks?.map((network: any) => ({
|
|
145
|
+
networkId: network.network_id || network.networkId,
|
|
146
|
+
percentage: network.percentage || 0,
|
|
147
|
+
})) || [],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build asset query from pubkeys and CAIP
|
|
153
|
+
*/
|
|
154
|
+
export function buildAssetQuery(pubkeys: any[], caip: string): Array<{ caip: string; pubkey: string }> {
|
|
155
|
+
return pubkeys.map(pubkey => ({ caip, pubkey: pubkey.pubkey }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Log query diagnostics (grouped by CAIP)
|
|
160
|
+
*/
|
|
161
|
+
export function logQueryDiagnostics(
|
|
162
|
+
assetQuery: Array<{ caip: string; pubkey: string }>,
|
|
163
|
+
tag: string = ''
|
|
164
|
+
): void {
|
|
165
|
+
const caipCounts = new Map<string, number>();
|
|
166
|
+
for (const query of assetQuery) {
|
|
167
|
+
caipCounts.set(query.caip, (caipCounts.get(query.caip) || 0) + 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (tag) {
|
|
171
|
+
console.log(tag, 'Built assetQuery with', assetQuery.length, 'entries');
|
|
172
|
+
console.log(tag, 'Sample queries:', assetQuery.slice(0, 5));
|
|
173
|
+
console.log(tag, 'Queries by chain:');
|
|
174
|
+
caipCounts.forEach((count, caip) => {
|
|
175
|
+
console.log(` - ${caip}: ${count} queries`);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Enrich balances with asset metadata
|
|
182
|
+
*/
|
|
183
|
+
export function enrichBalancesWithAssetInfo(
|
|
184
|
+
balances: any[],
|
|
185
|
+
assetsMap: Map<string, any>,
|
|
186
|
+
caipToNetworkId: (caip: string) => string
|
|
187
|
+
): any[] {
|
|
188
|
+
const tag = TAG + ' | enrichBalancesWithAssetInfo | ';
|
|
189
|
+
|
|
190
|
+
for (const balance of balances) {
|
|
191
|
+
const assetInfo = assetsMap.get(balance.caip.toLowerCase()) || assetsMap.get(balance.caip);
|
|
192
|
+
|
|
193
|
+
if (!assetInfo) {
|
|
194
|
+
throw new Error(`Missing AssetInfo for ${balance.caip}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Object.assign(balance, assetInfo, {
|
|
198
|
+
type: balance.type || assetInfo.type,
|
|
199
|
+
isNative: balance.isNative ?? assetInfo.isNative,
|
|
200
|
+
networkId: caipToNetworkId(balance.caip),
|
|
201
|
+
icon: assetInfo.icon || 'https://pioneers.dev/coins/etherum.png',
|
|
202
|
+
identifier: `${balance.caip}:${balance.pubkey}`,
|
|
203
|
+
updated: Date.now(),
|
|
204
|
+
color: assetInfo.color,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return balances;
|
|
209
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Pubkey management utilities
|
|
2
|
+
|
|
3
|
+
export function getPubkeyKey(pubkey: any): string {
|
|
4
|
+
return `${pubkey.pubkey}_${pubkey.pathMaster}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function deduplicatePubkeys(pubkeys: any[]): any[] {
|
|
8
|
+
const seen = new Set<string>();
|
|
9
|
+
return pubkeys.filter((pubkey) => {
|
|
10
|
+
const key = getPubkeyKey(pubkey);
|
|
11
|
+
if (seen.has(key)) return false;
|
|
12
|
+
seen.add(key);
|
|
13
|
+
return true;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validatePubkey(pubkey: any): boolean {
|
|
18
|
+
return !!(pubkey.pubkey && pubkey.pathMaster);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find pubkeys that support a given network
|
|
23
|
+
* Handles EVM wildcard matching (eip155:*)
|
|
24
|
+
*/
|
|
25
|
+
export function findPubkeysForNetwork(pubkeys: any[], networkId: string): any[] {
|
|
26
|
+
return pubkeys.filter((p: any) => {
|
|
27
|
+
if (!p.networks || !Array.isArray(p.networks)) return false;
|
|
28
|
+
|
|
29
|
+
// Exact match
|
|
30
|
+
if (p.networks.includes(networkId)) return true;
|
|
31
|
+
|
|
32
|
+
// For EVM chains, check if pubkey has eip155:* wildcard
|
|
33
|
+
if (networkId.startsWith('eip155:') && p.networks.includes('eip155:*')) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return false;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find a single pubkey for a network
|
|
43
|
+
*/
|
|
44
|
+
export function findPubkeyForNetwork(pubkeys: any[], networkId: string): any | undefined {
|
|
45
|
+
return pubkeys.find((p: any) => {
|
|
46
|
+
if (!p.networks || !Array.isArray(p.networks)) return false;
|
|
47
|
+
|
|
48
|
+
// Exact match
|
|
49
|
+
if (p.networks.includes(networkId)) return true;
|
|
50
|
+
|
|
51
|
+
// For EVM chains, check if pubkey has eip155:* wildcard
|
|
52
|
+
if (networkId.startsWith('eip155:') && p.networks.includes('eip155:*')) return true;
|
|
53
|
+
|
|
54
|
+
return false;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate that pubkeys exist for a given network
|
|
60
|
+
* Throws descriptive errors if validation fails
|
|
61
|
+
*/
|
|
62
|
+
export function validatePubkeysForNetwork(
|
|
63
|
+
pubkeys: any[],
|
|
64
|
+
networkId: string,
|
|
65
|
+
caip: string,
|
|
66
|
+
): void {
|
|
67
|
+
if (!pubkeys || pubkeys.length === 0) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Cannot set asset context for ${caip} - no pubkeys loaded. Please initialize wallet first.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pubkeysForNetwork = findPubkeysForNetwork(pubkeys, networkId);
|
|
74
|
+
|
|
75
|
+
if (pubkeysForNetwork.length === 0) {
|
|
76
|
+
const availableNetworks = [...new Set(pubkeys.flatMap((p: any) => p.networks || []))];
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Cannot set asset context for ${caip} - no address/xpub found for network ${networkId}. Available networks: ${availableNetworks.join(', ')}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// For UTXO chains, verify we have xpub
|
|
83
|
+
const isUtxoChain = networkId.startsWith('bip122:');
|
|
84
|
+
if (isUtxoChain) {
|
|
85
|
+
const xpubFound = pubkeysForNetwork.some((p: any) => p.type === 'xpub' && p.pubkey);
|
|
86
|
+
if (!xpubFound) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Cannot set asset context for UTXO chain ${caip} - xpub required but not found`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Verify we have a valid address or pubkey
|
|
94
|
+
const hasValidAddress = pubkeysForNetwork.some((p: any) => p.address || p.master || p.pubkey);
|
|
95
|
+
if (!hasValidAddress) {
|
|
96
|
+
throw new Error(`Cannot set asset context for ${caip} - no valid address found in pubkeys`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Filter pubkeys for a specific asset
|
|
102
|
+
* Used to attach relevant pubkeys to asset context
|
|
103
|
+
*/
|
|
104
|
+
export function filterPubkeysForAsset(
|
|
105
|
+
pubkeys: any[],
|
|
106
|
+
caip: string,
|
|
107
|
+
caipToNetworkId: (caip: string) => string,
|
|
108
|
+
): any[] {
|
|
109
|
+
const networkId = caipToNetworkId(caip);
|
|
110
|
+
|
|
111
|
+
return pubkeys.filter((p) => {
|
|
112
|
+
if (!p.networks || !Array.isArray(p.networks)) return false;
|
|
113
|
+
|
|
114
|
+
// Exact match
|
|
115
|
+
if (p.networks.includes(networkId)) return true;
|
|
116
|
+
|
|
117
|
+
// For EVM chains, check if pubkey has eip155:* wildcard
|
|
118
|
+
if (networkId.includes('eip155') && p.networks.some((n) => n.startsWith('eip155'))) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
});
|
|
124
|
+
}
|