feat(ltc): Implement dynamic fee estimation and fix send functionality
Some checks failed
Workflow push to Dappbundle / Build (push) Has been cancelled
Some checks failed
Workflow push to Dappbundle / Build (push) Has been cancelled
- Added "View on Explorer" links for transactions. - Enhanced UI validation for send amounts and balances. - Optimized transaction confirmation popup with estimated fee display.
This commit is contained in:
parent
50f9c97362
commit
0ec93dd7d2
4134
index.html
4134
index.html
File diff suppressed because it is too large
Load Diff
@ -3,9 +3,32 @@
|
|||||||
const ltcBlockchainAPI = EXPORTS;
|
const ltcBlockchainAPI = EXPORTS;
|
||||||
|
|
||||||
const DEFAULT = {
|
const DEFAULT = {
|
||||||
fee: 0.001,
|
// Fee rate in satoshis per byte (10 sat/byte is safe for Litecoin)
|
||||||
|
feeRateSatPerByte: 10,
|
||||||
|
// Fallback fixed fee in LTC (only used if calculation fails)
|
||||||
|
fallbackFee: 0.00001,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate transaction fee based on number of inputs and outputs
|
||||||
|
* Formula: (10 + inputs*148 + outputs*34) * satPerByte
|
||||||
|
* @param {number} numInputs - Number of transaction inputs
|
||||||
|
* @param {number} numOutputs - Number of transaction outputs
|
||||||
|
* @param {number} satPerByte - Fee rate in satoshis per byte (default 10)
|
||||||
|
* @returns {number} Fee in LTC
|
||||||
|
*/
|
||||||
|
function calculateFee(numInputs, numOutputs, satPerByte = DEFAULT.feeRateSatPerByte) {
|
||||||
|
// P2PKH transaction size estimation:
|
||||||
|
// - Overhead: ~10 bytes
|
||||||
|
// - Per input: ~148 bytes (for compressed pubkey signatures)
|
||||||
|
// - Per output: ~34 bytes
|
||||||
|
const estimatedSize = 10 + (numInputs * 148) + (numOutputs * 34);
|
||||||
|
const feeInSatoshis = estimatedSize * satPerByte;
|
||||||
|
const feeInLTC = feeInSatoshis / 100000000;
|
||||||
|
console.log(`Estimated tx size: ${estimatedSize} bytes, Fee: ${feeInLTC.toFixed(8)} LTC (${feeInSatoshis} satoshis)`);
|
||||||
|
return feeInLTC;
|
||||||
|
}
|
||||||
|
|
||||||
//Get balance for the given Address
|
//Get balance for the given Address
|
||||||
ltcBlockchainAPI.getBalance = function (addr) {
|
ltcBlockchainAPI.getBalance = function (addr) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -282,12 +305,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Litecoin transaction using direct RPC calls to GetBlock.io
|
* Send Litecoin transaction using client-side signing with bitjs library
|
||||||
* This method implements the full RPC workflow: createrawtransaction -> signrawtransaction -> sendrawtransaction
|
* Transaction is constructed and signed locally, then broadcast via RPC
|
||||||
* @param {string} senderAddr - Sender's Litecoin address
|
* @param {string} senderAddr - Sender's Litecoin address
|
||||||
* @param {string} receiverAddr - Receiver's Litecoin address
|
* @param {string} receiverAddr - Receiver's Litecoin address
|
||||||
* @param {number} sendAmt - Amount to send in LTC
|
* @param {number} sendAmt - Amount to send in LTC
|
||||||
* @param {string} privKey - Private key of the sender
|
* @param {string} privKey - Private key of the sender (WIF format)
|
||||||
* @returns {Promise} Promise that resolves with the transaction ID
|
* @returns {Promise} Promise that resolves with the transaction ID
|
||||||
*/
|
*/
|
||||||
ltcBlockchainAPI.sendLitecoinRPC = function (
|
ltcBlockchainAPI.sendLitecoinRPC = function (
|
||||||
@ -303,10 +326,14 @@
|
|||||||
return reject(`Invalid receiver address: ${receiverAddr}`);
|
return reject(`Invalid receiver address: ${receiverAddr}`);
|
||||||
if (typeof sendAmt !== "number" || sendAmt <= 0)
|
if (typeof sendAmt !== "number" || sendAmt <= 0)
|
||||||
return reject(`Invalid send amount: ${sendAmt}`);
|
return reject(`Invalid send amount: ${sendAmt}`);
|
||||||
|
|
||||||
|
// Minimum amount to avoid dust errors (GetBlock requires ~10000 satoshis minimum)
|
||||||
|
const MIN_SEND_AMOUNT = 0.0001; // 10000 satoshis
|
||||||
|
if (sendAmt < MIN_SEND_AMOUNT)
|
||||||
|
return reject(`Amount too small. Minimum is ${MIN_SEND_AMOUNT} LTC to avoid dust rejection.`);
|
||||||
if (privKey.length < 1 || !ltcCrypto.verifyPrivKey(privKey, senderAddr))
|
if (privKey.length < 1 || !ltcCrypto.verifyPrivKey(privKey, senderAddr))
|
||||||
return reject("Invalid Private key!");
|
return reject("Invalid Private key!");
|
||||||
|
|
||||||
const fee = DEFAULT.fee;
|
|
||||||
const apiToken = "31ea37c3a0c44b368e879007af7a64c8";
|
const apiToken = "31ea37c3a0c44b368e879007af7a64c8";
|
||||||
const rpcEndpoint = `https://go.getblock.io/${apiToken}/`;
|
const rpcEndpoint = `https://go.getblock.io/${apiToken}/`;
|
||||||
|
|
||||||
@ -319,9 +346,15 @@
|
|||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
if (data.error) throw new Error(JSON.stringify(data.error));
|
if (data.error) {
|
||||||
|
// Extract meaningful error message from RPC response
|
||||||
|
const errMsg = data.error.message || JSON.stringify(data.error);
|
||||||
|
throw new Error(`RPC Error: ${errMsg}`);
|
||||||
|
}
|
||||||
return data.result;
|
return data.result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Re-throw if it's already our formatted error
|
||||||
|
if (err.message.startsWith("RPC Error:")) throw err;
|
||||||
console.error("Raw RPC response:\n", text);
|
console.error("Raw RPC response:\n", text);
|
||||||
throw new Error("Failed to parse JSON-RPC response");
|
throw new Error("Failed to parse JSON-RPC response");
|
||||||
}
|
}
|
||||||
@ -336,61 +369,69 @@
|
|||||||
const utxoTotal = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
|
const utxoTotal = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
|
||||||
console.log("Total UTXO value:", utxoTotal);
|
console.log("Total UTXO value:", utxoTotal);
|
||||||
|
|
||||||
|
// Calculate fee based on transaction size
|
||||||
|
// Inputs = number of UTXOs, Outputs = 2 (receiver + change)
|
||||||
|
const numInputs = utxos.length;
|
||||||
|
const numOutputs = 2; // receiver + change output
|
||||||
|
const fee = calculateFee(numInputs, numOutputs);
|
||||||
|
console.log(`Dynamic fee calculated: ${fee.toFixed(8)} LTC for ${numInputs} inputs, ${numOutputs} outputs`);
|
||||||
|
|
||||||
if (utxoTotal < sendAmt + fee)
|
if (utxoTotal < sendAmt + fee)
|
||||||
return reject(
|
return reject(
|
||||||
`Insufficient funds: ${utxoTotal} < ${sendAmt + fee}`
|
`Insufficient funds: ${utxoTotal.toFixed(8)} LTC < ${(sendAmt + fee).toFixed(8)} LTC (${sendAmt} + ${fee.toFixed(8)} fee)`
|
||||||
);
|
);
|
||||||
|
|
||||||
const inputs = utxos.map((utxo) => ({
|
|
||||||
txid: utxo.txid,
|
|
||||||
vout: utxo.vout,
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log("inputs:", inputs);
|
|
||||||
|
|
||||||
// Calculate change amount
|
// Calculate change amount
|
||||||
const change = utxoTotal - sendAmt - fee;
|
const change = utxoTotal - sendAmt - fee;
|
||||||
|
|
||||||
const outputs = {
|
|
||||||
[senderAddr]: Number(change.toFixed(8)),
|
|
||||||
[receiverAddr]: Number(sendAmt.toFixed(8)),
|
|
||||||
};
|
|
||||||
console.log("outputs:", outputs);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create raw transaction
|
// Save original bitjs settings and set Litecoin version bytes
|
||||||
console.log("Creating raw transaction...");
|
const origPub = bitjs.pub;
|
||||||
const rawTx = await rpc("createrawtransaction", [inputs, outputs]);
|
const origPriv = bitjs.priv;
|
||||||
console.log("Raw transaction hex:", rawTx);
|
const origCompressed = bitjs.compressed;
|
||||||
// Sign raw transaction
|
|
||||||
console.log("Signing transaction...");
|
|
||||||
const signedTx = await rpc("signrawtransaction", [
|
|
||||||
rawTx,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
txid: utxos[0].txid,
|
|
||||||
vout: utxos[0].vout,
|
|
||||||
scriptPubKey: utxos[0].scriptPubKey,
|
|
||||||
amount: utxos[0].value.toFixed(8),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[privKey],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!signedTx.complete) {
|
// Litecoin mainnet version bytes
|
||||||
return reject(
|
bitjs.pub = 0x30; // Litecoin P2PKH address prefix
|
||||||
`Failed to sign transaction: ${JSON.stringify(signedTx.errors)}`
|
bitjs.priv = 0xb0; // Litecoin WIF prefix
|
||||||
);
|
bitjs.compressed = true;
|
||||||
|
|
||||||
|
// Create transaction using bitjs
|
||||||
|
console.log("Creating transaction with bitjs...");
|
||||||
|
const tx = bitjs.transaction();
|
||||||
|
|
||||||
|
// Add all UTXOs as inputs
|
||||||
|
for (const utxo of utxos) {
|
||||||
|
tx.addinput(utxo.txid, utxo.vout, utxo.scriptPubKey);
|
||||||
|
console.log(`Added input: ${utxo.txid}:${utxo.vout}`);
|
||||||
}
|
}
|
||||||
console.log("Signed transaction hex:", signedTx.hex);
|
|
||||||
|
|
||||||
// Send raw transaction
|
// Add outputs: receiver first, then change back to sender
|
||||||
|
tx.addoutput(receiverAddr, sendAmt);
|
||||||
|
console.log(`Added output to receiver: ${receiverAddr} = ${sendAmt} LTC`);
|
||||||
|
|
||||||
|
if (change > 0.00000546) { // Only add change if above dust threshold
|
||||||
|
tx.addoutput(senderAddr, change);
|
||||||
|
console.log(`Added change output: ${senderAddr} = ${change} LTC`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the transaction with private key
|
||||||
|
console.log("Signing transaction locally...");
|
||||||
|
const signedTxHex = tx.sign(privKey);
|
||||||
|
console.log("Signed transaction hex:", signedTxHex);
|
||||||
|
|
||||||
|
// Restore original bitjs settings
|
||||||
|
bitjs.pub = origPub;
|
||||||
|
bitjs.priv = origPriv;
|
||||||
|
bitjs.compressed = origCompressed;
|
||||||
|
|
||||||
|
// Broadcast the signed transaction
|
||||||
console.log("Broadcasting transaction...");
|
console.log("Broadcasting transaction...");
|
||||||
const txid = await rpc("sendrawtransaction", [signedTx.hex]);
|
const txid = await rpc("sendrawtransaction", [signedTxHex]);
|
||||||
|
console.log("Transaction broadcast successful! TXID:", txid);
|
||||||
|
|
||||||
resolve(txid);
|
resolve(txid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("RPC error:", error);
|
console.error("Transaction error:", error);
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user