@pioneer-platform/pioneer-sdk 0.0.82 → 4.13.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,532 @@
1
+ import { caipToNetworkId } from '@pioneer-platform/pioneer-caip';
2
+ import { bip32ToAddressNList } from '@pioneer-platform/pioneer-coins';
3
+
4
+ const TAG = ' | createUnsignedEvmTx | ';
5
+
6
+ // Utility function to convert a number to hex string with "0x" prefix
7
+ const toHex = (value) => {
8
+ let hex = value.toString(16);
9
+ if (hex.length % 2) hex = '0' + hex; // Ensure even length
10
+ return '0x' + hex;
11
+ };
12
+
13
+ // Utility function to convert a UTF-8 string to hex
14
+ const utf8ToHex = (str) => {
15
+ return '0x' + Buffer.from(str, 'utf8').toString('hex');
16
+ };
17
+
18
+ // Classify asset type based on CAIP format
19
+ const classifyCaipEvm = (caip) => {
20
+ if (caip.includes('erc20')) return 'erc20';
21
+ if (caip.includes('eip721')) return 'nft';
22
+ if (caip.includes('slip44')) return 'gas';
23
+ return 'unknown';
24
+ };
25
+
26
+ // Extract numeric part from networkId and convert to number for chainId
27
+ const extractChainIdFromNetworkId = (networkId) => {
28
+ const id = networkId.split(':').pop();
29
+ if (!id || isNaN(parseInt(id))) {
30
+ throw new Error(`Malformed networkId: ${networkId}`);
31
+ }
32
+ return parseInt(id);
33
+ };
34
+
35
+ // Fetch the current ETH price in USD from CoinGecko
36
+ async function fetchEthPriceInUsd() {
37
+ const response = await fetch(
38
+ 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd',
39
+ );
40
+ const data = await response.json();
41
+ return data.ethereum.usd;
42
+ }
43
+
44
+ // Extract contract address from CAIP
45
+ const extractContractAddressFromCaip = (caip) => {
46
+ const parts = caip.split('/');
47
+ if (parts.length < 2) {
48
+ throw new Error(`Malformed CAIP: ${caip}`);
49
+ }
50
+ const assetId = parts[1];
51
+ const assetParts = assetId.split(':');
52
+ if (assetParts.length < 2) {
53
+ throw new Error(`Malformed CAIP asset ID: ${assetId}`);
54
+ }
55
+ const contractAddress = assetParts[1];
56
+ if (!/^0x[a-fA-F0-9]{40}$/.test(contractAddress)) {
57
+ throw new Error(`Invalid contract address in CAIP: ${contractAddress}`);
58
+ }
59
+ return contractAddress;
60
+ };
61
+
62
+ // Encode ERC20 transfer data
63
+ const encodeTransferData = (toAddress, amountWei) => {
64
+ const functionSignature = 'a9059cbb';
65
+
66
+ // Remove '0x' from addresses
67
+ const toAddressNoPrefix = toAddress.toLowerCase().replace(/^0x/, '');
68
+ const amountHex = amountWei.toString(16);
69
+
70
+ // Pad to 32 bytes
71
+ const toAddressPadded = toAddressNoPrefix.padStart(64, '0');
72
+ const amountPadded = amountHex.padStart(64, '0');
73
+
74
+ const data = '0x' + functionSignature + toAddressPadded + amountPadded;
75
+ return data;
76
+ };
77
+
78
+ //TODO use assetData here, this is horrible
79
+ // Helper function to fetch token price in USD
80
+ async function fetchTokenPriceInUsd(contractAddress) {
81
+ // Use CoinGecko API to get token price by contract address
82
+ const response = await fetch(
83
+ `https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=${contractAddress}&vs_currencies=usd`,
84
+ );
85
+ const data = await response.json();
86
+ const price = data[contractAddress.toLowerCase()]?.usd;
87
+ if (!price) {
88
+ throw new Error('Failed to fetch token price');
89
+ }
90
+ return price;
91
+ }
92
+
93
+ // Create an unsigned EVM transaction
94
+ export async function createUnsignedEvmTx(
95
+ caip,
96
+ to,
97
+ amount,
98
+ memo,
99
+ pubkeys,
100
+ pioneer,
101
+ keepKeySdk,
102
+ isMax,
103
+ feeLevel = 5, // Added feeLevel parameter with default of 5 (average)
104
+ ) {
105
+ const tag = TAG + ' | createUnsignedEvmTx | ';
106
+
107
+ try {
108
+ if (!pioneer) throw new Error('Failed to initialize Pioneer');
109
+
110
+ // Determine networkId from CAIP
111
+ const networkId = caipToNetworkId(caip);
112
+ // Extract chainId from networkId
113
+ const chainId = extractChainIdFromNetworkId(networkId);
114
+
115
+ // Check if context is valid for this network
116
+ const isValidForNetwork = (pubkey: any) => {
117
+ if (!pubkey?.networks) return false;
118
+ // For EVM, check if it has eip155:* wildcard OR the specific network
119
+ if (networkId.includes('eip155')) {
120
+ return pubkey.networks.includes('eip155:*') || pubkey.networks.includes(networkId);
121
+ }
122
+ // For non-EVM, check exact match
123
+ return pubkey.networks.includes(networkId);
124
+ };
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
+ }
141
+ }
142
+
143
+ const address = keepKeySdk.pubkeyContext?.address || keepKeySdk.pubkeyContext?.pubkey;
144
+ console.log(tag, '✅ Using FROM address from pubkeyContext:', address, 'note:', keepKeySdk.pubkeyContext?.note);
145
+ if (!address) throw new Error('No address found for the specified network');
146
+
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
150
+ 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
+ }
166
+
167
+ // Apply fee level multiplier
168
+ // feeLevel: 1 = slow (80% of base), 5 = average (100%), 9 = fast (150%)
169
+ 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)');
178
+ } else {
179
+ // Average - use base price
180
+ gasPrice = baseGasPrice;
181
+ console.log(tag, 'Using AVERAGE gas price (100% of base)');
182
+ }
183
+
184
+ console.log(tag, 'Using gasPrice:', gasPrice.toString(), 'wei (', Number(gasPrice) / 1e9, 'gwei)');
185
+
186
+ let nonce;
187
+ try {
188
+ const nonceData = await pioneer.GetNonceByNetwork({ networkId, address });
189
+ nonce = nonceData.data;
190
+
191
+ // Handle fresh addresses that have never sent a transaction
192
+ if (nonce === undefined || nonce === null) {
193
+ console.log(tag, 'No nonce found for address (likely fresh address), defaulting to 0');
194
+ nonce = 0;
195
+ }
196
+ } catch (nonceError) {
197
+ // If the API fails to fetch nonce (e.g., for a fresh address), default to 0
198
+ console.log(tag, 'Failed to fetch nonce (likely fresh address):', nonceError.message, '- defaulting to 0');
199
+ nonce = 0;
200
+ }
201
+ //console.log(tag, 'nonce:', nonce);
202
+
203
+ const balanceData = await pioneer.GetBalanceAddressByNetwork({ networkId, address });
204
+ const balanceEth = balanceData.data; // Assuming this is in ETH
205
+ const balance = BigInt(Math.round(balanceEth * 1e18)); // Convert to wei
206
+ //console.log(tag, 'balance (wei):', balance.toString());
207
+ if (balance <= 0n) throw new Error('Wallet balance is zero');
208
+
209
+ // Classify asset type by CAIP
210
+ const assetType = classifyCaipEvm(caip);
211
+ let unsignedTx;
212
+
213
+ if (memo === ' ') memo = '';
214
+
215
+ // Build transaction object based on asset type
216
+ switch (assetType) {
217
+ case 'gas': {
218
+ // Check if this is a THORChain swap (needs more gas for contract call)
219
+ const isThorchainOperation =
220
+ memo && (memo.startsWith('=') || memo.startsWith('SWAP') || memo.includes(':'));
221
+
222
+ let gasLimit;
223
+ if (isThorchainOperation) {
224
+ // THORChain depositWithExpiry requires more gas (90-120k typical)
225
+ // Use 120000 to be safe for all network conditions
226
+ gasLimit = BigInt(120000);
227
+ console.log(tag, 'Using higher gas limit for THORChain swap:', gasLimit.toString());
228
+ } else {
229
+ // Standard gas limit for ETH transfer
230
+ // Use higher gas limit for all chains except mainnet to be safe
231
+ gasLimit = chainId === 1 ? BigInt(21000) : BigInt(25000);
232
+ }
233
+
234
+ if (memo && memo !== '' && !isThorchainOperation) {
235
+ const memoBytes = Buffer.from(memo, 'utf8').length;
236
+ gasLimit += BigInt(memoBytes) * 68n; // Approximate additional gas
237
+ //console.log(tag, 'Adjusted gasLimit for memo:', gasLimit.toString());
238
+ }
239
+
240
+ const gasFee = gasPrice * gasLimit;
241
+ //console.log(tag, 'gasFee (wei):', gasFee.toString());
242
+
243
+ let amountWei;
244
+ if (isMax) {
245
+ if (balance <= gasFee) {
246
+ throw new Error('Insufficient funds to cover gas fees');
247
+ }
248
+ // Subtract a small buffer (100 wei) to avoid rounding issues
249
+ // This prevents "insufficient funds" errors when sending max amount
250
+ const buffer = BigInt(100);
251
+ amountWei = balance - gasFee - buffer;
252
+ console.log(tag, 'isMax calculation - balance:', balance.toString(), 'gasFee:', gasFee.toString(), 'buffer:', buffer.toString(), 'amountWei:', amountWei.toString());
253
+ } else {
254
+ amountWei = BigInt(Math.round(amount * 1e18));
255
+ if (amountWei + gasFee > balance) {
256
+ throw new Error('Insufficient funds for the transaction amount and gas fees');
257
+ }
258
+ }
259
+
260
+ //console.log(tag, 'amountWei:', amountWei.toString());
261
+
262
+ // Check if this is a THORChain swap (memo starts with '=' or 'SWAP' or contains ':')
263
+ const isThorchainSwap =
264
+ memo && (memo.startsWith('=') || memo.startsWith('SWAP') || memo.includes(':'));
265
+
266
+ let txData = '0x';
267
+
268
+ if (isThorchainSwap) {
269
+ // This is a THORChain swap - need to encode the deposit function call
270
+ console.log(tag, 'Detected THORChain swap, encoding deposit data for memo:', memo);
271
+
272
+ // Fix the memo format if it's missing the chain identifier
273
+ // Convert "=:b:address" to "=:BTC.BTC:address" for Bitcoin
274
+ let fixedMemo = memo;
275
+ if (memo.startsWith('=:b:') || memo.startsWith('=:btc:')) {
276
+ fixedMemo = memo.replace(/^=:(b|btc):/, '=:BTC.BTC:');
277
+ console.log(tag, 'Fixed Bitcoin swap memo from:', memo, 'to:', fixedMemo);
278
+ } else if (memo.startsWith('=:e:') || memo.startsWith('=:eth:')) {
279
+ fixedMemo = memo.replace(/^=:(e|eth):/, '=:ETH.ETH:');
280
+ console.log(tag, 'Fixed Ethereum swap memo from:', memo, 'to:', fixedMemo);
281
+ }
282
+
283
+ // Validate memo length (THORChain typically < 250 bytes)
284
+ if (fixedMemo.length > 250) {
285
+ throw new Error(`Memo too long for THORChain: ${fixedMemo.length} bytes (max 250)`);
286
+ }
287
+
288
+ try {
289
+ // CRITICAL: Fetch current inbound addresses from THORChain
290
+ // The 'to' address should be the router, but we need the vault address for the deposit
291
+ let vaultAddress = '0x0000000000000000000000000000000000000000';
292
+ let routerAddress = to; // The 'to' field should already be the router
293
+
294
+ try {
295
+ // Try to fetch inbound addresses from THORChain
296
+ // This would typically be: GET https://thornode.ninerealms.com/thorchain/inbound_addresses
297
+ const inboundResponse = await fetch('https://thornode.ninerealms.com/thorchain/inbound_addresses');
298
+ if (inboundResponse.ok) {
299
+ const inboundData = await inboundResponse.json();
300
+ // Find ETH inbound data
301
+ const ethInbound = inboundData.find(inbound =>
302
+ inbound.chain === 'ETH' && !inbound.halted
303
+ );
304
+ if (ethInbound) {
305
+ vaultAddress = ethInbound.address; // This is the Asgard vault
306
+ routerAddress = ethInbound.router || to; // Use fetched router or fallback to 'to'
307
+ console.log(tag, 'Using THORChain inbound addresses - vault:', vaultAddress, 'router:', routerAddress);
308
+
309
+ // Update the 'to' address to be the router (in case it wasn't)
310
+ to = routerAddress;
311
+ } else {
312
+ throw new Error('ETH inbound is halted or not found - cannot proceed with swap');
313
+ }
314
+ }
315
+ } catch (fetchError) {
316
+ console.error(tag, 'Failed to fetch inbound addresses:', fetchError);
317
+ // ABORT - cannot proceed without proper vault address
318
+ throw new Error(`Cannot proceed with THORChain swap - failed to fetch inbound addresses: ${fetchError.message}`);
319
+ }
320
+
321
+ // Final validation - never use 0x0 as vault
322
+ if (vaultAddress === '0x0000000000000000000000000000000000000000') {
323
+ throw new Error('Cannot proceed with THORChain swap - vault address is invalid (0x0)');
324
+ }
325
+
326
+ // Use depositWithExpiry for better safety
327
+ // Function signature: depositWithExpiry(address,address,uint256,string,uint256)
328
+ // Function selector: 0x44bc937b
329
+ const functionSelector = '44bc937b';
330
+
331
+ // For native ETH swaps, asset is 0x0000...0000
332
+ const assetAddress = '0x0000000000000000000000000000000000000000';
333
+
334
+ // Calculate expiry time (current time + 1 hour)
335
+ const expiryTime = Math.floor(Date.now() / 1000) + 3600;
336
+
337
+ // Encode the parameters
338
+ const vaultPadded = vaultAddress.toLowerCase().replace(/^0x/, '').padStart(64, '0');
339
+ const assetPadded = assetAddress.toLowerCase().replace(/^0x/, '').padStart(64, '0');
340
+ const amountPadded = amountWei.toString(16).padStart(64, '0');
341
+
342
+ // CRITICAL FIX: String offset for depositWithExpiry with 5 parameters
343
+ // The memo is the 4th parameter (dynamic string)
344
+ // Offset must point after all 5 head words: 5 * 32 = 160 = 0xa0
345
+ const stringOffset = (5 * 32).toString(16).padStart(64, '0'); // 0xa0
346
+
347
+ // Expiry time (5th parameter after the string offset)
348
+ const expiryPadded = expiryTime.toString(16).padStart(64, '0');
349
+
350
+ // String length in bytes
351
+ const memoBytes = Buffer.from(fixedMemo, 'utf8');
352
+ const stringLength = memoBytes.length.toString(16).padStart(64, '0');
353
+
354
+ // String data (padded to 32-byte boundary)
355
+ const memoHex = memoBytes.toString('hex');
356
+ const paddingLength = (32 - (memoBytes.length % 32)) % 32;
357
+ const memoPadded = memoHex + '0'.repeat(paddingLength * 2);
358
+
359
+ // Construct the complete transaction data for depositWithExpiry
360
+ txData = '0x' + functionSelector + vaultPadded + assetPadded + amountPadded + stringOffset + expiryPadded + stringLength + memoPadded;
361
+
362
+ console.log(tag, 'Encoded THORChain depositWithExpiry data:', {
363
+ functionSelector: '0x' + functionSelector,
364
+ vault: vaultAddress,
365
+ asset: assetAddress,
366
+ amount: amountWei.toString(),
367
+ memo: fixedMemo,
368
+ expiry: expiryTime,
369
+ stringOffset: '0x' + stringOffset,
370
+ fullData: txData
371
+ });
372
+
373
+ // CRITICAL: For native ETH, the value MUST be set in the transaction
374
+ // This is already handled below where we set value: toHex(amountWei)
375
+ // But let's make sure it's clear
376
+ console.log(tag, 'Native ETH swap - value will be set to:', amountWei.toString(), 'wei');
377
+
378
+ } catch (error) {
379
+ console.error(tag, 'Error encoding THORChain deposit:', error);
380
+ // Don't fallback to plain memo - this will fail on chain
381
+ throw new Error(`Failed to encode THORChain swap: ${error.message}`);
382
+ }
383
+ } else if (memo) {
384
+ // Regular transaction with memo
385
+ txData = utf8ToHex(memo);
386
+ }
387
+
388
+ unsignedTx = {
389
+ chainId,
390
+ nonce: toHex(nonce),
391
+ gas: toHex(gasLimit),
392
+ gasPrice: toHex(gasPrice),
393
+ to,
394
+ value: toHex(amountWei),
395
+ data: txData,
396
+ };
397
+ break;
398
+ }
399
+
400
+ case 'erc20': {
401
+ const contractAddress = extractContractAddressFromCaip(caip);
402
+
403
+ // Get token decimals - CRITICAL for correct amount calculation
404
+ // Common token decimals:
405
+ // USDT: 6, USDC: 6, DAI: 18, WETH: 18, most others: 18
406
+ let tokenDecimals = 18; // Default to 18 if not specified
407
+
408
+ // Check for known stablecoins with 6 decimals
409
+ const contractLower = contractAddress.toLowerCase();
410
+ if (
411
+ contractLower === '0xdac17f958d2ee523a2206206994597c13d831ec7' || // USDT on Ethereum
412
+ contractLower === '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' || // USDC on Ethereum
413
+ contractLower === '0x4fabb145d64652a948d72533023f6e7a623c7c53' || // BUSD on Ethereum
414
+ contractLower === '0x8e870d67f660d95d5be530380d0ec0bd388289e1'
415
+ ) {
416
+ // USDP on Ethereum
417
+ tokenDecimals = 6;
418
+ console.log(tag, 'Using 6 decimals for stablecoin:', contractAddress);
419
+ }
420
+
421
+ // TODO: Fetch decimals from contract in the future:
422
+ // const decimals = await getTokenDecimals(contractAddress, networkId);
423
+
424
+ // Use BigInt for precise decimal math (no float drift)
425
+ const tokenMultiplier = 10n ** BigInt(tokenDecimals);
426
+
427
+ // Increase gas limit for ERC-20 transfers - 60k was insufficient on Polygon
428
+ // Transaction 0x00ba81ce failed at 52,655/60,000 gas
429
+ let gasLimit = BigInt(100000); // Increased from 60000 to handle SSTORE operations
430
+
431
+ if (memo && memo !== '') {
432
+ const memoBytes = Buffer.from(memo, 'utf8').length;
433
+ gasLimit += BigInt(memoBytes) * 68n; // Approximate additional gas
434
+ //console.log(tag, 'Adjusted gasLimit for memo:', gasLimit.toString());
435
+ }
436
+
437
+ const gasFee = gasPrice * gasLimit;
438
+
439
+ let amountWei;
440
+ if (isMax) {
441
+ // For ERC20 tokens, need to get token balance
442
+ const tokenBalanceData = await pioneer.GetTokenBalance({
443
+ networkId,
444
+ address,
445
+ contractAddress,
446
+ });
447
+ // Use BigInt math to avoid precision loss
448
+ // Note: tokenBalanceData.data is a float which can lose precision
449
+ // Ideally the API should return base units as string/bigint
450
+ const tokenBalance = BigInt(Math.round(tokenBalanceData.data * Number(tokenMultiplier)));
451
+ amountWei = tokenBalance;
452
+ } else {
453
+ // Use BigInt math to avoid precision loss
454
+ amountWei = BigInt(Math.round(amount * Number(tokenMultiplier)));
455
+ console.log(tag, 'Token amount calculation:', {
456
+ inputAmount: amount,
457
+ decimals: tokenDecimals,
458
+ multiplier: tokenMultiplier,
459
+ resultWei: amountWei.toString(),
460
+ });
461
+ }
462
+
463
+ // Ensure user has enough ETH to pay for gas
464
+ if (gasFee > balance) {
465
+ throw new Error('Insufficient ETH balance to cover gas fees');
466
+ }
467
+
468
+ // Ensure user has enough tokens
469
+ // For simplicity, we assume user has enough tokens
470
+ // In practice, need to check token balance
471
+
472
+ const data = encodeTransferData(to, amountWei);
473
+
474
+ const ethPriceInUsd = await fetchEthPriceInUsd();
475
+ const gasFeeUsd = (Number(gasFee) / 1e18) * ethPriceInUsd;
476
+
477
+ // For token price, need to fetch from API
478
+ const tokenPriceInUsd = await fetchTokenPriceInUsd(contractAddress);
479
+ // Use the correct decimals for USD calculation
480
+ const amountUsd = (Number(amountWei) / Number(tokenMultiplier)) * tokenPriceInUsd;
481
+
482
+ unsignedTx = {
483
+ chainId,
484
+ nonce: toHex(nonce),
485
+ gas: toHex(gasLimit),
486
+ gasPrice: toHex(gasPrice),
487
+ to: contractAddress,
488
+ value: '0x0',
489
+ data,
490
+ // USD estimations
491
+ gasFeeUsd,
492
+ amountUsd,
493
+ };
494
+ break;
495
+ }
496
+
497
+ default: {
498
+ throw new Error(`Unsupported asset type for CAIP ${caip}`);
499
+ }
500
+ }
501
+
502
+ // Address path for hardware wallets - use the path from the pubkey context
503
+ // The pubkey context should have either addressNListMaster or pathMaster
504
+ if (keepKeySdk.pubkeyContext?.addressNListMaster) {
505
+ // Direct use if we have addressNListMaster
506
+ unsignedTx.addressNList = keepKeySdk.pubkeyContext.addressNListMaster;
507
+ console.log(tag, '✅ Using addressNListMaster from pubkey context:', unsignedTx.addressNList, 'for address:', address);
508
+ } else if (keepKeySdk.pubkeyContext?.pathMaster) {
509
+ // 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) {
513
+ // Use addressNList if available (but this would be the non-master path)
514
+ unsignedTx.addressNList = keepKeySdk.pubkeyContext.addressNList;
515
+ console.log(tag, '✅ Using addressNList from pubkey context:', unsignedTx.addressNList);
516
+ } else if (keepKeySdk.pubkeyContext?.path) {
517
+ // 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);
520
+ } else {
521
+ // Fallback to default account 0
522
+ unsignedTx.addressNList = [0x80000000 + 44, 0x80000000 + 60, 0x80000000, 0, 0];
523
+ console.warn(tag, '⚠️ No path info in pubkey context, using default account 0');
524
+ }
525
+
526
+ //console.log(tag, 'Unsigned Transaction:', unsignedTx);
527
+ return unsignedTx;
528
+ } catch (error) {
529
+ console.error(tag, 'Error:', error.message);
530
+ throw error;
531
+ }
532
+ }
@@ -0,0 +1,122 @@
1
+ /*
2
+ Create Unsigned UTXO Transaction
3
+ */
4
+ // import type { UTXO, PubKey } from './types';
5
+ // @ts-ignore
6
+ import { caipToNetworkId } from '@pioneer-platform/pioneer-caip';
7
+ //@ts-ignore
8
+
9
+ const TAG = ' | createUnsignedUxtoTx | ';
10
+
11
+ export async function createUnsignedRippleTx(
12
+ caip: string,
13
+ to: string,
14
+ amount: any,
15
+ memo: string,
16
+ pubkeys: any,
17
+ pioneer: any,
18
+ keepKeySdk: any,
19
+ isMax: boolean,
20
+ ): Promise<any> {
21
+ let tag = TAG + ' | createUnsignedRippleTx | ';
22
+
23
+ try {
24
+ if (!pioneer) throw new Error('Failed to init! pioneer');
25
+
26
+ // Determine networkId from caip
27
+ const networkId = caipToNetworkId(caip);
28
+ //console.log(tag, 'networkId:', networkId);
29
+
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
+ );
35
+ }
36
+
37
+ if (!keepKeySdk.pubkeyContext) {
38
+ throw new Error(`No relevant pubkeys found for networkId: ${networkId}`);
39
+ }
40
+
41
+ const fromAddress = keepKeySdk.pubkeyContext.address || keepKeySdk.pubkeyContext.pubkey;
42
+
43
+ let accountInfo = await pioneer.GetAccountInfo({
44
+ address: fromAddress,
45
+ network: 'ripple',
46
+ });
47
+ accountInfo = accountInfo.data;
48
+ //console.log(tag, 'accountInfo:', accountInfo);
49
+
50
+ const sequence = accountInfo.Sequence.toString();
51
+ const ledgerIndexCurrent = parseInt(accountInfo.ledger_index_current);
52
+ let desttag = memo;
53
+ // Check if desttag is null, undefined, a space, or any non-numeric value
54
+ //@ts-ignore
55
+ if (!desttag || /^\s*$/.test(desttag) || isNaN(desttag)) {
56
+ desttag = '0';
57
+ }
58
+
59
+ //console.log(tag, 'amount:', amount);
60
+ if (isMax) {
61
+ //balance - 1 (min) - fee
62
+ amount = Number(accountInfo.Balance) - 1000000 - 1;
63
+ amount = amount.toString();
64
+ } else {
65
+ //format amount
66
+ amount = amount * 1000000;
67
+ amount = amount.toString();
68
+ }
69
+
70
+ let tx = {
71
+ type: 'auth/StdTx',
72
+ value: {
73
+ fee: {
74
+ amount: [
75
+ {
76
+ amount: '1000',
77
+ denom: 'drop',
78
+ },
79
+ ],
80
+ gas: '28000',
81
+ },
82
+ memo: memo || '',
83
+ msg: [
84
+ {
85
+ type: 'ripple-sdk/MsgSend',
86
+ DestinationTag: desttag,
87
+ value: {
88
+ amount: [
89
+ {
90
+ amount: amount,
91
+ denom: 'drop',
92
+ },
93
+ ],
94
+ from_address: fromAddress,
95
+ to_address: to,
96
+ },
97
+ },
98
+ ],
99
+ signatures: null,
100
+ },
101
+ };
102
+
103
+ //Unsigned TX
104
+ let unsignedTx = {
105
+ addressNList: [2147483692, 2147483792, 2147483648, 0, 0],
106
+ tx: tx,
107
+ flags: undefined,
108
+ lastLedgerSequence: (ledgerIndexCurrent + 1000).toString(), // Add 1000 ledgers (~16 minutes) for transaction validity
109
+ sequence: sequence || '0',
110
+ payment: {
111
+ amount,
112
+ destination: to,
113
+ destinationTag: desttag,
114
+ },
115
+ };
116
+
117
+ return unsignedTx;
118
+ } catch (error) {
119
+ console.error(tag, 'Error:', error);
120
+ throw error;
121
+ }
122
+ }