@pioneer-platform/pioneer-sdk 0.0.82 → 4.14.0
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 +5358 -0
- package/dist/index.es.js +5537 -0
- package/dist/index.js +5537 -0
- package/package.json +55 -37
- package/src/TransactionManager.ts +333 -0
- package/src/charts/cosmos-staking.ts +171 -0
- package/src/charts/evm.ts +199 -0
- package/src/charts/index.ts +46 -0
- package/src/charts/maya.ts +110 -0
- package/src/charts/types.ts +77 -0
- package/src/charts/utils.ts +24 -0
- package/src/fees/index.ts +620 -0
- package/src/getPubkey.ts +151 -0
- package/src/index.ts +2250 -0
- package/src/kkapi-batch-client.ts +191 -0
- package/src/offline-client.ts +287 -0
- package/src/supportedCaips.ts +36 -0
- package/src/txbuilder/createUnsignedEvmTx.ts +532 -0
- package/src/txbuilder/createUnsignedRippleTx.ts +122 -0
- package/src/txbuilder/createUnsignedStakingTx.ts +188 -0
- package/src/txbuilder/createUnsignedTendermintTx.ts +249 -0
- package/src/txbuilder/createUnsignedUxtoTx.ts +450 -0
- package/src/txbuilder/templates/cosmos-staking.ts +157 -0
- package/src/txbuilder/templates/cosmos.ts +30 -0
- package/src/txbuilder/templates/mayachain.ts +60 -0
- package/src/txbuilder/templates/osmosis.ts +30 -0
- package/src/txbuilder/templates/thorchain.ts +60 -0
- package/src/utils/build-dashboard.ts +181 -0
- package/src/utils/format-time.ts +12 -0
- package/src/utils/kkapi-detection.ts +64 -0
- package/src/utils/pubkey-helpers.ts +75 -0
- package/lib/index.d.ts +0 -66
- package/lib/index.js +0 -493
- package/tsconfig.json +0 -13
@@ -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
|
+
}
|