@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.
@@ -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
+ }