@pioneer-platform/pioneer-sdk 4.20.0 → 4.20.2

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.
@@ -98,7 +98,7 @@ export async function createUnsignedEvmTx(
98
98
  memo,
99
99
  pubkeys,
100
100
  pioneer,
101
- keepKeySdk,
101
+ pubkeyContext,
102
102
  isMax,
103
103
  feeLevel = 5, // Added feeLevel parameter with default of 5 (average)
104
104
  ) {
@@ -112,6 +112,11 @@ export async function createUnsignedEvmTx(
112
112
  // Extract chainId from networkId
113
113
  const chainId = extractChainIdFromNetworkId(networkId);
114
114
 
115
+ // Use the passed pubkeyContext directly - it's already been set by Pioneer SDK
116
+ if (!pubkeyContext) {
117
+ throw new Error(`No pubkey context provided for networkId: ${networkId}`);
118
+ }
119
+
115
120
  // Check if context is valid for this network
116
121
  const isValidForNetwork = (pubkey: any) => {
117
122
  if (!pubkey?.networks) return false;
@@ -122,66 +127,116 @@ export async function createUnsignedEvmTx(
122
127
  // For non-EVM, check exact match
123
128
  return pubkey.networks.includes(networkId);
124
129
  };
125
-
126
- // Check if we have a context at all
127
- if (!keepKeySdk.pubkeyContext) {
128
- console.log(tag, 'No context set - auto-selecting first matching pubkey');
129
- keepKeySdk.pubkeyContext = pubkeys.find(pk => isValidForNetwork(pk));
130
- } else {
131
- // We have a context - check if it's valid for this network
132
- console.log(tag, 'Current context networks:', keepKeySdk.pubkeyContext.networks, 'For networkId:', networkId);
133
-
134
- if (!isValidForNetwork(keepKeySdk.pubkeyContext)) {
135
- // Context exists but wrong network - auto-correct
136
- console.log(tag, 'Auto-correcting context - wrong network detected');
137
- keepKeySdk.pubkeyContext = pubkeys.find(pk => isValidForNetwork(pk));
138
- } else {
139
- console.log(tag, 'Context is valid for this network - using existing context');
140
- }
130
+
131
+ if (!isValidForNetwork(pubkeyContext)) {
132
+ throw new Error(`Pubkey context is for wrong network. Expected ${networkId}, got ${pubkeyContext.networks}`);
141
133
  }
142
-
143
- const address = keepKeySdk.pubkeyContext?.address || keepKeySdk.pubkeyContext?.pubkey;
144
- console.log(tag, '✅ Using FROM address from pubkeyContext:', address, 'note:', keepKeySdk.pubkeyContext?.note);
134
+
135
+ const address = pubkeyContext.address || pubkeyContext.pubkey;
136
+ console.log(tag, '✅ Using FROM address from pubkeyContext:', address, 'note:', pubkeyContext.note);
145
137
  if (!address) throw new Error('No address found for the specified network');
146
138
 
147
- // Fetch gas price in gwei and convert to wei
148
- // Note: In the future, we should fetch different gas prices for different fee levels
149
- // For now, we'll use a multiplier on the base gas price
139
+ // Fetch gas price and convert to wei
150
140
  const gasPriceData = await pioneer.GetGasPriceByNetwork({ networkId });
151
- let baseGasPrice: bigint;
152
-
153
- // Check if the returned value is reasonable (in wei or gwei)
154
- // If it's less than 1 gwei (1e9 wei), it's probably already in wei but too low
155
- // For mainnet, we need at least 10-30 gwei typically
156
- const MIN_GAS_PRICE_WEI = BigInt(10e9); // 10 gwei minimum for mainnet
157
-
158
- if (BigInt(gasPriceData.data) < MIN_GAS_PRICE_WEI) {
159
- // The API is returning a value that's way too low (like 0.296 gwei)
160
- // Use a reasonable default for mainnet
161
- console.log(tag, 'Gas price from API too low:', gasPriceData.data, 'wei - using minimum:', MIN_GAS_PRICE_WEI.toString());
162
- baseGasPrice = MIN_GAS_PRICE_WEI;
163
- } else {
164
- baseGasPrice = BigInt(gasPriceData.data);
165
- }
141
+ console.log(tag, 'Gas price data from API:', JSON.stringify(gasPriceData.data));
166
142
 
167
- // Apply fee level multiplier
168
- // feeLevel: 1 = slow (80% of base), 5 = average (100%), 9 = fast (150%)
169
143
  let gasPrice: bigint;
170
- if (feeLevel <= 2) {
171
- // Slow - 80% of base price
172
- gasPrice = (baseGasPrice * BigInt(80)) / BigInt(100);
173
- console.log(tag, 'Using SLOW gas price (80% of base)');
174
- } else if (feeLevel >= 8) {
175
- // Fast - 150% of base price
176
- gasPrice = (baseGasPrice * BigInt(150)) / BigInt(100);
177
- console.log(tag, 'Using FAST gas price (150% of base)');
144
+
145
+ // Default fallback gas prices by chain ID (in gwei)
146
+ const defaultGasPrices: Record<number, number> = {
147
+ 1: 30, // Ethereum mainnet
148
+ 56: 3, // BSC
149
+ 137: 50, // Polygon
150
+ 43114: 25, // Avalanche
151
+ 8453: 0.1, // Base
152
+ 10: 0.1, // Optimism
153
+ };
154
+
155
+ const fallbackGasGwei = defaultGasPrices[chainId] || 20; // Default 20 gwei
156
+ const MIN_GAS_PRICE_WEI = BigInt(fallbackGasGwei * 1e9);
157
+
158
+ // Check if API returned an object with fee levels or a single value
159
+ if (typeof gasPriceData.data === 'object' && gasPriceData.data !== null && !Array.isArray(gasPriceData.data)) {
160
+ // API returned object with fee levels (e.g., { slow, average, fastest })
161
+ let selectedGasPrice: string | number | undefined;
162
+
163
+ if (feeLevel <= 2) {
164
+ // Slow
165
+ selectedGasPrice = gasPriceData.data.slow || gasPriceData.data.average || gasPriceData.data.fastest;
166
+ console.log(tag, 'Selecting SLOW gas price from API');
167
+ } else if (feeLevel >= 8) {
168
+ // Fast
169
+ selectedGasPrice = gasPriceData.data.fastest || gasPriceData.data.fast || gasPriceData.data.average;
170
+ console.log(tag, 'Selecting FAST gas price from API');
171
+ } else {
172
+ // Average
173
+ selectedGasPrice = gasPriceData.data.average || gasPriceData.data.fast || gasPriceData.data.fastest;
174
+ console.log(tag, 'Selecting AVERAGE gas price from API');
175
+ }
176
+
177
+ // Convert to number and validate
178
+ let gasPriceNum: number;
179
+ if (selectedGasPrice === undefined || selectedGasPrice === null) {
180
+ console.warn(tag, 'No valid gas price found in API response, using fallback:', fallbackGasGwei, 'gwei');
181
+ gasPriceNum = fallbackGasGwei;
182
+ } else {
183
+ gasPriceNum = typeof selectedGasPrice === 'string' ? parseFloat(selectedGasPrice) : selectedGasPrice;
184
+
185
+ // Check for NaN
186
+ if (isNaN(gasPriceNum) || !isFinite(gasPriceNum)) {
187
+ console.warn(tag, 'Invalid gas price (NaN or Infinite):', selectedGasPrice, '- using fallback:', fallbackGasGwei, 'gwei');
188
+ gasPriceNum = fallbackGasGwei;
189
+ }
190
+ }
191
+
192
+ // Assume API returns gwei, convert to wei
193
+ gasPrice = BigInt(Math.round(gasPriceNum * 1e9));
194
+
195
+ // Apply minimum gas price if too low
196
+ if (gasPrice < MIN_GAS_PRICE_WEI) {
197
+ console.warn(tag, 'Gas price from API too low:', gasPrice.toString(), 'wei - using minimum:', MIN_GAS_PRICE_WEI.toString());
198
+ gasPrice = MIN_GAS_PRICE_WEI;
199
+ }
178
200
  } else {
179
- // Average - use base price
180
- gasPrice = baseGasPrice;
181
- console.log(tag, 'Using AVERAGE gas price (100% of base)');
201
+ // API returned a single value or something unexpected
202
+ let gasPriceNum: number;
203
+
204
+ if (gasPriceData.data === undefined || gasPriceData.data === null) {
205
+ console.warn(tag, 'Gas price API returned null/undefined, using fallback:', fallbackGasGwei, 'gwei');
206
+ gasPriceNum = fallbackGasGwei;
207
+ } else {
208
+ gasPriceNum = typeof gasPriceData.data === 'string' ? parseFloat(gasPriceData.data) : gasPriceData.data;
209
+
210
+ // Check for NaN
211
+ if (isNaN(gasPriceNum) || !isFinite(gasPriceNum)) {
212
+ console.warn(tag, 'Invalid gas price (NaN or Infinite):', gasPriceData.data, '- using fallback:', fallbackGasGwei, 'gwei');
213
+ gasPriceNum = fallbackGasGwei;
214
+ }
215
+ }
216
+
217
+ // Assume API returns gwei, convert to wei
218
+ const baseGasPrice = BigInt(Math.round(gasPriceNum * 1e9));
219
+
220
+ // Apply fee level multiplier
221
+ if (feeLevel <= 2) {
222
+ gasPrice = (baseGasPrice * BigInt(80)) / BigInt(100);
223
+ console.log(tag, 'Using SLOW gas price (80% of base)');
224
+ } else if (feeLevel >= 8) {
225
+ gasPrice = (baseGasPrice * BigInt(150)) / BigInt(100);
226
+ console.log(tag, 'Using FAST gas price (150% of base)');
227
+ } else {
228
+ gasPrice = baseGasPrice;
229
+ console.log(tag, 'Using AVERAGE gas price (100% of base)');
230
+ }
231
+
232
+ // Apply minimum gas price if too low
233
+ if (gasPrice < MIN_GAS_PRICE_WEI) {
234
+ console.warn(tag, 'Gas price too low:', gasPrice.toString(), 'wei - using minimum:', MIN_GAS_PRICE_WEI.toString());
235
+ gasPrice = MIN_GAS_PRICE_WEI;
236
+ }
182
237
  }
183
-
184
- console.log(tag, 'Using gasPrice:', gasPrice.toString(), 'wei (', Number(gasPrice) / 1e9, 'gwei)');
238
+
239
+ console.log(tag, 'Final gasPrice:', gasPrice.toString(), 'wei (', Number(gasPrice) / 1e9, 'gwei)');
185
240
 
186
241
  let nonce;
187
242
  try {
@@ -501,22 +556,22 @@ export async function createUnsignedEvmTx(
501
556
 
502
557
  // Address path for hardware wallets - use the path from the pubkey context
503
558
  // The pubkey context should have either addressNListMaster or pathMaster
504
- if (keepKeySdk.pubkeyContext?.addressNListMaster) {
559
+ if (pubkeyContext.addressNListMaster) {
505
560
  // Direct use if we have addressNListMaster
506
- unsignedTx.addressNList = keepKeySdk.pubkeyContext.addressNListMaster;
561
+ unsignedTx.addressNList = pubkeyContext.addressNListMaster;
507
562
  console.log(tag, '✅ Using addressNListMaster from pubkey context:', unsignedTx.addressNList, 'for address:', address);
508
- } else if (keepKeySdk.pubkeyContext?.pathMaster) {
563
+ } else if (pubkeyContext.pathMaster) {
509
564
  // Convert BIP32 path to addressNList if we have pathMaster
510
- unsignedTx.addressNList = bip32ToAddressNList(keepKeySdk.pubkeyContext.pathMaster);
511
- console.log(tag, '✅ Converted pathMaster to addressNList:', keepKeySdk.pubkeyContext.pathMaster, '→', unsignedTx.addressNList);
512
- } else if (keepKeySdk.pubkeyContext?.addressNList) {
565
+ unsignedTx.addressNList = bip32ToAddressNList(pubkeyContext.pathMaster);
566
+ console.log(tag, '✅ Converted pathMaster to addressNList:', pubkeyContext.pathMaster, '→', unsignedTx.addressNList);
567
+ } else if (pubkeyContext.addressNList) {
513
568
  // Use addressNList if available (but this would be the non-master path)
514
- unsignedTx.addressNList = keepKeySdk.pubkeyContext.addressNList;
569
+ unsignedTx.addressNList = pubkeyContext.addressNList;
515
570
  console.log(tag, '✅ Using addressNList from pubkey context:', unsignedTx.addressNList);
516
- } else if (keepKeySdk.pubkeyContext?.path) {
571
+ } else if (pubkeyContext.path) {
517
572
  // Last resort - convert regular path to addressNList
518
- unsignedTx.addressNList = bip32ToAddressNList(keepKeySdk.pubkeyContext.path);
519
- console.log(tag, '⚠️ Using regular path (not master):', keepKeySdk.pubkeyContext.path, '→', unsignedTx.addressNList);
573
+ unsignedTx.addressNList = bip32ToAddressNList(pubkeyContext.path);
574
+ console.log(tag, '⚠️ Using regular path (not master):', pubkeyContext.path, '→', unsignedTx.addressNList);
520
575
  } else {
521
576
  // Fallback to default account 0
522
577
  unsignedTx.addressNList = [0x80000000 + 44, 0x80000000 + 60, 0x80000000, 0, 0];
@@ -15,7 +15,7 @@ export async function createUnsignedRippleTx(
15
15
  memo: string,
16
16
  pubkeys: any,
17
17
  pioneer: any,
18
- keepKeySdk: any,
18
+ pubkeyContext: any,
19
19
  isMax: boolean,
20
20
  ): Promise<any> {
21
21
  let tag = TAG + ' | createUnsignedRippleTx | ';
@@ -25,20 +25,21 @@ export async function createUnsignedRippleTx(
25
25
 
26
26
  // Determine networkId from caip
27
27
  const networkId = caipToNetworkId(caip);
28
- //console.log(tag, 'networkId:', networkId);
29
28
 
30
- // Auto-correct context if wrong network
31
- if (!keepKeySdk.pubkeyContext?.networks?.includes(networkId)) {
32
- keepKeySdk.pubkeyContext = pubkeys.find((pk: any) =>
33
- pk.networks?.includes(networkId)
34
- );
29
+ // Use the passed pubkeyContext directly - it's already been set by Pioneer SDK
30
+ if (!pubkeyContext) {
31
+ throw new Error(`No pubkey context provided for networkId: ${networkId}`);
35
32
  }
36
-
37
- if (!keepKeySdk.pubkeyContext) {
38
- throw new Error(`No relevant pubkeys found for networkId: ${networkId}`);
33
+
34
+ if (!pubkeyContext.networks?.includes(networkId)) {
35
+ throw new Error(`Pubkey context is for wrong network. Expected ${networkId}, got ${pubkeyContext.networks}`);
39
36
  }
40
37
 
41
- const fromAddress = keepKeySdk.pubkeyContext.address || keepKeySdk.pubkeyContext.pubkey;
38
+ console.log(tag, `✅ Using pubkeyContext for network ${networkId}:`, {
39
+ address: pubkeyContext.address,
40
+ });
41
+
42
+ const fromAddress = pubkeyContext.address || pubkeyContext.pubkey;
42
43
 
43
44
  let accountInfo = await pioneer.GetAccountInfo({
44
45
  address: fromAddress,
@@ -24,7 +24,7 @@ export async function createUnsignedStakingTx(
24
24
  params: StakingTxParams,
25
25
  pubkeys: any[],
26
26
  pioneer: any,
27
- keepKeySdk: any,
27
+ pubkeyContext: any,
28
28
  ): Promise<any> {
29
29
  const tag = TAG + ' | createUnsignedStakingTx | ';
30
30
 
@@ -32,18 +32,20 @@ export async function createUnsignedStakingTx(
32
32
  if (!pioneer) throw new Error('Failed to init! pioneer');
33
33
 
34
34
  const networkId = caipToNetworkId(caip);
35
-
36
- // Auto-correct context if wrong network
37
- if (!keepKeySdk.pubkeyContext?.networks?.includes(networkId)) {
38
- keepKeySdk.pubkeyContext = pubkeys.find(pk =>
39
- pk.networks?.includes(networkId)
40
- );
35
+
36
+ // Use the passed pubkeyContext directly - it's already been set by Pioneer SDK
37
+ if (!pubkeyContext) {
38
+ throw new Error(`No pubkey context provided for networkId: ${networkId}`);
41
39
  }
42
-
43
- if (!keepKeySdk.pubkeyContext) {
44
- throw new Error(`No relevant pubkeys found for networkId: ${networkId}`);
40
+
41
+ if (!pubkeyContext.networks?.includes(networkId)) {
42
+ throw new Error(`Pubkey context is for wrong network. Expected ${networkId}, got ${pubkeyContext.networks}`);
45
43
  }
46
44
 
45
+ console.log(tag, `✅ Using pubkeyContext for network ${networkId}:`, {
46
+ address: pubkeyContext.address,
47
+ });
48
+
47
49
  // Map networkId to chain and get network-specific configs
48
50
  let chain: string;
49
51
  let chainId: string;
@@ -78,7 +80,7 @@ export async function createUnsignedStakingTx(
78
80
 
79
81
  console.log(tag, `Building ${params.type} transaction for ${chain}`);
80
82
 
81
- const fromAddress = keepKeySdk.pubkeyContext.address || keepKeySdk.pubkeyContext.pubkey;
83
+ const fromAddress = pubkeyContext.address || pubkeyContext.pubkey;
82
84
 
83
85
  // Get account info
84
86
  const accountInfo = (await pioneer.GetAccountInfo({ network: chain, address: fromAddress }))
@@ -14,7 +14,7 @@ export async function createUnsignedTendermintTx(
14
14
  memo: string,
15
15
  pubkeys: any[],
16
16
  pioneer: any,
17
- keepKeySdk: any,
17
+ pubkeyContext: any,
18
18
  isMax: boolean,
19
19
  to?: string,
20
20
  ): Promise<any> {
@@ -24,18 +24,22 @@ export async function createUnsignedTendermintTx(
24
24
  if (!pioneer) throw new Error('Failed to init! pioneer');
25
25
 
26
26
  const networkId = caipToNetworkId(caip);
27
-
28
- // Auto-correct context if wrong network
29
- if (!keepKeySdk.pubkeyContext?.networks?.includes(networkId)) {
30
- keepKeySdk.pubkeyContext = pubkeys.find(pk =>
31
- pk.networks?.includes(networkId)
32
- );
27
+
28
+ // Use the passed pubkeyContext directly - it's already been set by Pioneer SDK
29
+ // No need to auto-correct anymore since context management is centralized
30
+ if (!pubkeyContext) {
31
+ throw new Error(`No pubkey context provided for networkId: ${networkId}`);
33
32
  }
34
-
35
- if (!keepKeySdk.pubkeyContext) {
36
- throw new Error(`No relevant pubkeys found for networkId: ${networkId}`);
33
+
34
+ if (!pubkeyContext.networks?.includes(networkId)) {
35
+ throw new Error(`Pubkey context is for wrong network. Expected ${networkId}, got ${pubkeyContext.networks}`);
37
36
  }
38
37
 
38
+ console.log(tag, `✅ Using pubkeyContext for network ${networkId}:`, {
39
+ address: pubkeyContext.address,
40
+ addressNList: pubkeyContext.addressNList || pubkeyContext.addressNListMaster
41
+ });
42
+
39
43
  // Map networkId to a human-readable chain
40
44
  let chain: string;
41
45
  switch (networkId) {
@@ -57,13 +61,16 @@ export async function createUnsignedTendermintTx(
57
61
 
58
62
  //console.log(tag, `Resolved chain: ${chain} for networkId: ${networkId}`);
59
63
 
60
- const fromAddress = keepKeySdk.pubkeyContext.address || keepKeySdk.pubkeyContext.pubkey;
64
+ const fromAddress = pubkeyContext.address || pubkeyContext.pubkey;
61
65
  let asset = caip.split(':')[1]; // Assuming format is "network:asset"
66
+
67
+ console.log(tag, `🔍 Fetching account info for address: ${fromAddress}`);
62
68
  const accountInfo = (await pioneer.GetAccountInfo({ network: chain, address: fromAddress }))
63
69
  .data;
64
- //console.log('accountInfo: ', accountInfo);
70
+ console.log(tag, '📋 accountInfo:', JSON.stringify(accountInfo, null, 2));
71
+
65
72
  let balanceInfo = await pioneer.GetPubkeyBalance({ asset: chain, pubkey: fromAddress });
66
- //console.log(tag, `balanceInfo: `, balanceInfo);
73
+ console.log(tag, `💰 balanceInfo:`, balanceInfo);
67
74
 
68
75
  let account_number, sequence;
69
76
  if (networkId === 'cosmos:cosmoshub-4' || networkId === 'cosmos:osmosis-1') {
@@ -77,9 +84,22 @@ export async function createUnsignedTendermintTx(
77
84
  sequence = accountInfo.result.value.sequence || '0';
78
85
  }
79
86
 
87
+ console.log(tag, `📊 Extracted account_number: ${account_number}, sequence: ${sequence}`);
88
+
89
+ // CRITICAL: Pioneer API may return stale data. The mayachain-network module already
90
+ // queries multiple nodes directly and uses consensus - but we're using the Pioneer API wrapper
91
+ // which adds caching. If we get account_number 0, warn about potential Pioneer API cache issue.
92
+ if (account_number === '0' || account_number === 0) {
93
+ console.log(tag, `⚠️ WARNING: Account number is 0 from Pioneer API`);
94
+ console.log(tag, ` This is likely due to stale Pioneer API cache`);
95
+ console.log(tag, ` The mayachain-network module queries nodes directly but Pioneer API may be cached`);
96
+ console.log(tag, ` Proceeding with account_number: 0 but transaction will likely fail`);
97
+ console.log(tag, ` TODO: Fix Pioneer API to use fresh data from mayachain-network module`);
98
+ }
99
+
80
100
  const fees = {
81
101
  'cosmos:thorchain-mainnet-v1': 0.02,
82
- 'cosmos:mayachain-mainnet-v1': 0.2,
102
+ 'cosmos:mayachain-mainnet-v1': 0, // Increased to 0.5 CACAO (5000000000 base units) for reliable confirmation
83
103
  'cosmos:cosmoshub-4': 0.005,
84
104
  'cosmos:osmosis-1': 0.035,
85
105
  };
@@ -147,11 +167,14 @@ export async function createUnsignedTendermintTx(
147
167
  throw new Error(`Unsupported Maya chain CAIP: ${caip}`);
148
168
  }
149
169
 
170
+ // MAYA token uses 1e4 (4 decimals), CACAO uses 1e10 (10 decimals)
171
+ const decimals = mayaAsset === 'maya' ? 1e4 : 1e10;
172
+
150
173
  if (isMax) {
151
- const fee = Math.floor(fees[networkId] * 1e10); // Convert fee to smallest unit and floor to int
152
- amount = Math.max(0, Math.floor(balanceInfo.data * 1e10) - fee); // Floor to ensure no decimals
174
+ const fee = Math.floor(fees[networkId] * decimals); // Convert fee to smallest unit and floor to int
175
+ amount = Math.max(0, Math.floor(balanceInfo.data * decimals) - fee); // Floor to ensure no decimals
153
176
  } else {
154
- amount = Math.max(Math.floor(amount * 1e10), 0); // Floor the multiplication result
177
+ amount = Math.max(Math.floor(amount * decimals), 0); // Floor the multiplication result
155
178
  }
156
179
 
157
180
  //console.log(tag, `amount: ${amount}, isMax: ${isMax}, fee: ${fees[networkId]}, asset: ${mayaAsset}`);
@@ -174,6 +197,7 @@ export async function createUnsignedTendermintTx(
174
197
  amount: amount.toString(),
175
198
  memo,
176
199
  sequence,
200
+ addressNList: pubkeyContext.addressNList || pubkeyContext.addressNListMaster, // CRITICAL: Use correct derivation path for signing
177
201
  })
178
202
  : mayachainDepositTemplate({
179
203
  account_number,
@@ -1,5 +1,34 @@
1
1
  import { caipToNetworkId, NetworkIdToChain } from '@pioneer-platform/pioneer-caip';
2
- import { bip32ToAddressNList } from '@pioneer-platform/pioneer-coins';
2
+ import { bip32ToAddressNList, SLIP_44_BY_LONG, COIN_MAP_LONG } from '@pioneer-platform/pioneer-coins';
3
+ import coinSelect from 'coinselect';
4
+ import coinSelectSplit from 'coinselect/split';
5
+
6
+ /**
7
+ * Get SLIP-44 coin type for a given network ID
8
+ * @param networkId - CAIP network identifier
9
+ * @returns SLIP-44 coin type number
10
+ */
11
+ function getCoinTypeFromNetworkId(networkId: string): number {
12
+ const chain = NetworkIdToChain[networkId];
13
+ if (!chain) {
14
+ console.warn(`No chain mapping found for ${networkId}, defaulting to Bitcoin coin type 0`);
15
+ return 0;
16
+ }
17
+
18
+ const blockchainName = COIN_MAP_LONG[chain]?.toLowerCase();
19
+ if (!blockchainName) {
20
+ console.warn(`No blockchain name found for chain ${chain}, defaulting to Bitcoin coin type 0`);
21
+ return 0;
22
+ }
23
+
24
+ const coinType = SLIP_44_BY_LONG[blockchainName];
25
+ if (coinType === undefined) {
26
+ console.warn(`No SLIP-44 coin type found for ${blockchainName}, defaulting to Bitcoin coin type 0`);
27
+ return 0;
28
+ }
29
+
30
+ return coinType;
31
+ }
3
32
 
4
33
  export async function createUnsignedUxtoTx(
5
34
  caip: string,
@@ -8,7 +37,7 @@ export async function createUnsignedUxtoTx(
8
37
  memo: string,
9
38
  pubkeys: any,
10
39
  pioneer: any,
11
- keepKeySdk: any,
40
+ pubkeyContext: any,
12
41
  isMax: boolean, // Added isMax parameter
13
42
  feeLevel: number = 5, // Added feeLevel parameter with default of 5 (average)
14
43
  changeScriptType?: string, // Added changeScriptType parameter for user preference
@@ -20,11 +49,20 @@ export async function createUnsignedUxtoTx(
20
49
 
21
50
  const networkId = caipToNetworkId(caip);
22
51
 
23
- // Auto-correct context if wrong network
24
- if (!keepKeySdk.pubkeyContext?.networks?.includes(networkId)) {
25
- keepKeySdk.pubkeyContext = pubkeys.find((pk) => pk.networks?.includes(networkId));
52
+ // Use the passed pubkeyContext directly - it's already been set by Pioneer SDK
53
+ if (!pubkeyContext) {
54
+ throw new Error(`No pubkey context provided for networkId: ${networkId}`);
26
55
  }
27
56
 
57
+ if (!pubkeyContext.networks?.includes(networkId)) {
58
+ throw new Error(`Pubkey context is for wrong network. Expected ${networkId}, got ${pubkeyContext.networks}`);
59
+ }
60
+
61
+ console.log(tag, `✅ Using pubkeyContext for network ${networkId}:`, {
62
+ address: pubkeyContext.address,
63
+ scriptType: pubkeyContext.scriptType,
64
+ });
65
+
28
66
  // For UTXO, we still need all relevant pubkeys to aggregate UTXOs
29
67
  const relevantPubkeys = pubkeys.filter(
30
68
  (e) => e.networks && Array.isArray(e.networks) && e.networks.includes(networkId),
@@ -40,57 +78,11 @@ export async function createUnsignedUxtoTx(
40
78
 
41
79
  let chain = NetworkIdToChain[networkId];
42
80
 
43
- // Determine the change script type - use preference or default to p2wpkh for lower fees
44
- const actualChangeScriptType =
45
- changeScriptType ||
46
- relevantPubkeys.find((pk) => pk.scriptType === 'p2wpkh')?.scriptType || // Prefer native segwit if available
47
- relevantPubkeys[0].scriptType || // Fall back to first available
48
- 'p2wpkh'; // Ultimate default
49
- console.log(`${tag}: Using change script type: ${actualChangeScriptType}`);
50
-
51
- // Find the xpub that matches the desired script type
52
- const changeXpub =
53
- relevantPubkeys.find((pk) => pk.scriptType === actualChangeScriptType)?.pubkey ||
54
- relevantPubkeys.find((pk) => pk.scriptType === 'p2wpkh')?.pubkey || // Fall back to native segwit
55
- relevantPubkeys[0].pubkey; // Last resort: use first available
56
-
57
- console.log(
58
- `${tag}: Change xpub selected for ${actualChangeScriptType}:`,
59
- changeXpub?.substring(0, 10) + '...',
60
- );
61
-
62
- let changeAddressIndex = await pioneer.GetChangeAddress({
63
- network: chain,
64
- xpub: changeXpub,
65
- });
66
- changeAddressIndex = changeAddressIndex.data.changeIndex;
67
-
68
- // Determine BIP path based on script type
69
- let bipPath: string;
70
- switch (actualChangeScriptType) {
71
- case 'p2pkh':
72
- bipPath = `m/44'/0'/0'/1/${changeAddressIndex}`; // BIP44 for legacy
73
- break;
74
- case 'p2sh-p2wpkh':
75
- bipPath = `m/49'/0'/0'/1/${changeAddressIndex}`; // BIP49 for wrapped segwit
76
- break;
77
- case 'p2wpkh':
78
- default:
79
- bipPath = `m/84'/0'/0'/1/${changeAddressIndex}`; // BIP84 for native segwit
80
- break;
81
- }
82
-
83
- const path = bipPath;
84
- console.log(`${tag}: Change address path: ${path} (index: ${changeAddressIndex})`);
85
-
86
- const changeAddress = {
87
- path: path,
88
- isChange: true,
89
- index: changeAddressIndex,
90
- addressNList: bip32ToAddressNList(path),
91
- scriptType: actualChangeScriptType, // Store the script type with the change address
92
- };
81
+ // Get correct coin type for this network
82
+ const coinType = getCoinTypeFromNetworkId(networkId);
83
+ console.log(`${tag}: Using SLIP-44 coin type ${coinType} for ${chain}`);
93
84
 
85
+ // Collect UTXOs first to determine the appropriate change address type
94
86
  const utxos: any[] = [];
95
87
  for (const pubkey of relevantPubkeys) {
96
88
  //console.log('pubkey: ',pubkey)
@@ -129,6 +121,81 @@ export async function createUnsignedUxtoTx(
129
121
  utxo.value = Number(utxo.value);
130
122
  }
131
123
 
124
+ // NOW determine change script type based on collected UTXOs
125
+ // Find the most common script type among input UTXOs to maintain consistency
126
+ const inputScriptTypes = utxos.map((u) => u.scriptType).filter(Boolean);
127
+ const scriptTypeCount: Record<string, number> = inputScriptTypes.reduce((acc, type) => {
128
+ acc[type] = (acc[type] || 0) + 1;
129
+ return acc;
130
+ }, {} as Record<string, number>);
131
+
132
+ // Get the most common input script type
133
+ const mostCommonInputType =
134
+ Object.entries(scriptTypeCount).sort(([, a], [, b]) => b - a)[0]?.[0] || 'p2pkh';
135
+
136
+ // Determine the change script type - priority order:
137
+ // 1. User explicit preference (changeScriptType parameter)
138
+ // 2. Most common input script type (maintain consistency)
139
+ // 3. First available pubkey script type
140
+ // 4. Default to p2pkh (legacy)
141
+ const actualChangeScriptType =
142
+ changeScriptType ||
143
+ mostCommonInputType ||
144
+ relevantPubkeys[0]?.scriptType ||
145
+ 'p2pkh';
146
+
147
+ console.log(`${tag}: Input script types:`, scriptTypeCount);
148
+ console.log(`${tag}: Using change script type: ${actualChangeScriptType} (matches inputs: ${mostCommonInputType})`);
149
+
150
+ // Find and validate the xpub that matches the desired script type
151
+ const changeXpubInfo = relevantPubkeys.find((pk) => pk.scriptType === actualChangeScriptType);
152
+ if (!changeXpubInfo) {
153
+ throw new Error(
154
+ `No ${actualChangeScriptType} xpub available for change address. ` +
155
+ `Available types: ${relevantPubkeys.map((pk) => pk.scriptType).join(', ')}. ` +
156
+ `Cannot create change output with mismatched script type.`,
157
+ );
158
+ }
159
+
160
+ const changeXpub = changeXpubInfo.pubkey;
161
+ console.log(
162
+ `${tag}: Change xpub selected for ${actualChangeScriptType}:`,
163
+ changeXpub?.substring(0, 10) + '...',
164
+ );
165
+
166
+ // Get change address index from Pioneer API
167
+ let changeAddressIndex = await pioneer.GetChangeAddress({
168
+ network: chain,
169
+ xpub: changeXpub,
170
+ });
171
+ changeAddressIndex = changeAddressIndex.data.changeIndex;
172
+
173
+ // Determine BIP path based on script type AND coin type
174
+ let bipPath: string;
175
+ switch (actualChangeScriptType) {
176
+ case 'p2pkh':
177
+ bipPath = `m/44'/${coinType}'/0'/1/${changeAddressIndex}`; // BIP44 for legacy
178
+ break;
179
+ case 'p2sh-p2wpkh':
180
+ bipPath = `m/49'/${coinType}'/0'/1/${changeAddressIndex}`; // BIP49 for wrapped segwit
181
+ break;
182
+ case 'p2wpkh':
183
+ default:
184
+ bipPath = `m/84'/${coinType}'/0'/1/${changeAddressIndex}`; // BIP84 for native segwit
185
+ break;
186
+ }
187
+
188
+ const path = bipPath;
189
+ console.log(`${tag}: Change address path: ${path} (coin type: ${coinType}, index: ${changeAddressIndex})`);
190
+
191
+ const changeAddress = {
192
+ path: path,
193
+ isChange: true,
194
+ index: changeAddressIndex,
195
+ addressNList: bip32ToAddressNList(path),
196
+ scriptType: actualChangeScriptType,
197
+ };
198
+
132
199
  let feeRateFromNode: any;
133
200
 
134
201
  // HARDCODE DOGE FEES - API is unreliable for DOGE
@@ -294,12 +361,10 @@ export async function createUnsignedUxtoTx(
294
361
  if (isMax) {
295
362
  //console.log(tag, 'isMax:', isMax);
296
363
  // For max send, use coinSelectSplit
297
- const { default: coinSelectSplit } = await import('coinselect/split');
298
364
  result = coinSelectSplit(utxos, [{ address: to }], effectiveFeeRate);
299
365
  } else {
300
366
  //console.log(tag, 'isMax:', isMax)
301
367
  // Regular send
302
- const { default: coinSelect } = await import('coinselect');
303
368
  result = coinSelect(utxos, [{ address: to, value: amount }], effectiveFeeRate);
304
369
  }
305
370