This technical documentation explains the detailed implementation of transaction creation, signing, and broadcasting in our LanaCoin wallet system.
Our wallet implements careful selection of unspent transaction outputs (UTXOs) to cover the required amount plus fees:
// Python code from electrum_client.py def select_inputs(self, unspent, amount, send_all=False): """ Select inputs from unspent outputs to cover the amount + fee. If send_all is True, it will use all unspent outputs. """ if send_all: # For "send all" we use all available UTXOs return unspent # Sort UTXOs by value (largest first) sorted_unspent = sorted(unspent, key=lambda utxo: utxo['value'], reverse=True) selected = [] total_selected = 0 # Select UTXOs until we have enough to cover the amount for utxo in sorted_unspent: selected.append(utxo) total_selected += utxo['value'] if total_selected >= amount: break # If we don't have enough funds, return all unspent if total_selected < amount: return sorted_unspent return selected
Fee calculation is based on transaction size with a safety buffer:
// Python code from app.py # For LanaCoin transactions, we'll always use 2 inputs: # 1. Main transaction input (user funds) # 2. System donation input (if applicable) # Always set fixed input count to 2 as per requirements fixed_input_count = 2 # Outputs include the recipient, donation, and change if applicable output_count = 2 # Default: recipient + change # Calculate transaction size using the formula # Formula: 10 + (num_inputs * 148) + (num_outputs * 34) estimated_size = 10 + (fixed_input_count * 148) + (output_count * 34) # Calculate base fee using the estimated size and fee rate base_fee = estimated_size * fee_rate # Apply 10% safety buffer as required safe_fee = base_fee * 1.10 # Ensure minimum fee of 0.0001 LANA (10,000 lanoshis) per input min_fee_per_input = 0.0001 min_total_fee = min_fee_per_input * fixed_input_count # Use the larger of safe_fee or minimum fee fee = max(safe_fee, min_total_fee)
Every transaction includes a donation to support the LanaCoin ecosystem:
// Donation is calculated as 1% of the transaction amount donation_percent = 0.01 // 1% donation donation_satoshis = int(amount_satoshis * donation_percent) // Donation is capped at 0.5 EUR max_donation_eur = 0.5 max_donation_lana = max_donation_eur / rate_eur_per_lana max_donation_satoshis = int(max_donation_lana * 100000000) // Use the smaller value between percentage and max cap donation_satoshis = min(donation_satoshis, max_donation_satoshis) // Donation address is fixed donation_address = "LNW3yUPf4jUGLwQmFsuvuZX79kLGR41VqE"
To prevent dust outputs that could be rejected by the network:
// Dust handling logic dust_limit = 546 // satoshis (standard Bitcoin/LanaCoin dust limit) // Check if change amount is below dust limit if (0 < change_amount && change_amount < dust_limit) { // Add dust to fee instead of creating a dust output fee_satoshis += change_amount fee = round(fee_satoshis / 100000000, 8) // Update fee in LANA // Set change to zero change_amount = 0 }
LanaCoin transactions have the following structure:
// Transaction structure tx_data = { // Transaction metadata "version": 1, "sender": sender_address, "destination": destination_address, "amount": amount, "fee": fee, "balance": balance, "nTime": current_timestamp, // Current Unix timestamp for nTime field // Inputs section with scriptPubKey data for each input "inputs": [ { "txid": "previous_transaction_id", "vout": output_index, "value": amount_in_satoshis, "scriptPubKey": "hex_script_pub_key", "sequence": 0xffffffff }, // More inputs... ], // Outputs section with destination, donation, then change (if applicable) "outputs": [ { "address": destination_address, "amount": amount_satoshis, "pubKeyHash": destination_pubkey_hash, "description": "Payment to destination" }, { "address": donation_address, "amount": donation_satoshis, "pubKeyHash": donation_pubkey_hash, "description": "Safe Lana Donation" }, // If change_amount > 0, include a change output { "address": sender_address, "amount": change_amount, "pubKeyHash": sender_pubkey_hash, "description": "Change back to sender" } ] }
Transaction signing follows the Bitcoin signature protocol with LanaCoin-specific additions:
// Code from offline-signer.js async createAndSignTransaction(wif) { try { const tx = this.transactionData; const decoded = this.base58decode(wif); const privBytes = decoded.slice(1, 33); const privHex = this.bytesToHex(privBytes); // Initialize elliptic curve key pair const key = this.ec.keyFromPrivate(privHex); const pub = key.getPublic(); const pubX = pub.getX().toString(16).padStart(64, '0'); const pubY = pub.getY().toString(16).padStart(64, '0'); const pubkey = this.hexToBytes('04' + pubX + pubY); // Transaction header const version = new Uint8Array([1, 0, 0, 0]); const nTime = new Uint8Array((() => { const t = tx.nTime || Math.floor(Date.now() / 1000); return [t & 0xff, (t >> 8) & 0xff, (t >> 16) & 0xff, (t >> 24) & 0xff]; })()); const inputCount = tx.inputs.length; const validOutputs = tx.outputs.filter(output => output && output.amount > 0); const outputCount = new Uint8Array([validOutputs.length]); const outputBuffers = validOutputs.map(o => this.constructOutput(o)); const outputsData = new Uint8Array(outputBuffers.reduce((acc, b) => [...acc, ...b], [])); // Array to store all signatures and inputs const signedInputs = []; // Process and sign each input for (let i = 0; i < inputCount; i++) { const input = tx.inputs[i]; const txid = new Uint8Array(input.txid.match(/.{2}/g).reverse().map(b => parseInt(b, 16))); const vout = new Uint8Array(this.toLittleEndian(input.vout, 4)); const sequence = new Uint8Array([0xff, 0xff, 0xff, 0xff]); const scriptPubKey = this.hexToBytes(input.scriptPubKey); // Build preimage with ALL inputs, but only set scriptPubKey for the one being signed let inputsPreimage = []; // Include all inputs in the preimage for (let j = 0; j < inputCount; j++) { const other = tx.inputs[j]; const otherTxid = new Uint8Array(other.txid.match(/.{2}/g).reverse().map(b => parseInt(b, 16))); const otherVout = new Uint8Array(this.toLittleEndian(other.vout, 4)); const otherSequence = new Uint8Array([0xff, 0xff, 0xff, 0xff]); // Only use scriptPubKey for the input being signed, others get empty script const otherScript = (j === i) ? this.hexToBytes(other.scriptPubKey) : new Uint8Array([]); const scriptLen = new Uint8Array([otherScript.length]); // Add this input to the preimage inputsPreimage.push(...otherTxid, ...otherVout, ...scriptLen, ...otherScript, ...otherSequence); } // Create preimage with ALL inputs (proper Bitcoin/LanaCoin sighash construction) const preimage = new Uint8Array([ ...version, ...nTime, inputCount, // All inputs are included ...inputsPreimage, // But only one has scriptPubKey ...outputCount, // Dynamic output count ...outputsData, // Dynamic outputs data 0, 0, 0, 0, // locktime 0x01, 0x00, 0x00, 0x00 // SIGHASH_ALL ]); // Create signature for this input const sighash = await this.doubleSha256(preimage); const sigObj = key.sign(sighash, { canonical: true }); const derSig = this.hexToBytes(sigObj.toDER('hex')); const sigWithHashType = new Uint8Array([...derSig, 0x01]); // Construct script sig for this input using canonical push encoding const scriptSig = new Uint8Array([ ...this.pushData(sigWithHashType), ...this.pushData(pubkey) ]); // Store the input data signedInputs.push({ txid: txid, vout: vout, scriptSig: scriptSig, sequence: sequence }); } // Build final transaction let finalTxArray = [...version, ...nTime, inputCount]; // Add all inputs for (const input of signedInputs) { finalTxArray = [ ...finalTxArray, ...input.txid, ...input.vout, input.scriptSig.length, ...input.scriptSig, ...input.sequence ]; } // Add output count and outputs finalTxArray = [ ...finalTxArray, ...outputCount, ...outputsData ]; // Add locktime (only once) const locktime = new Uint8Array([0, 0, 0, 0]); const finalTx = new Uint8Array([...finalTxArray, ...locktime]); // Convert to hex for broadcasting const finalHex = this.bytesToHex(finalTx); // Validate the hex before returning const validationResult = this.validateHex(finalHex); if (validationResult !== "OK") { throw new Error(`Transaction validation failed: ${validationResult}`); } return finalHex; } catch (error) { console.error('Transaction signing error:', error); throw error; } }
For correct serialization of signatures and public keys:
/** * Helper function to correctly push data onto the stack according to Bitcoin script rules * Small data uses simple length prefix, larger data uses OP_PUSHDATA opcodes */ pushData(data) { const length = data.length; if (length < 0x4c) { // For small data (< 76 bytes), use a simple length byte return new Uint8Array([length, ...data]); } else if (length < 0x100) { // For medium data (< 256 bytes), use OP_PUSHDATA1 return new Uint8Array([0x4c, length, ...data]); } else if (length < 0x10000) { // For larger data (< 65536 bytes), use OP_PUSHDATA2 return new Uint8Array([ 0x4d, length & 0xff, (length >> 8) & 0xff, ...data ]); } else { // For very large data, use OP_PUSHDATA4 return new Uint8Array([ 0x4e, length & 0xff, (length >> 8) & 0xff, (length >> 16) & 0xff, (length >> 24) & 0xff, ...data ]); } }
After signing, the transaction is broadcast to the LanaCoin network:
// Code from app.py - broadcast_transaction route @app.route('/broadcast_transaction', methods=['POST']) def broadcast_transaction(): try: data = request.json raw_tx = data.get('hex') # Validate the input if not raw_tx or not isinstance(raw_tx, str): return jsonify({'error': 'Missing or invalid transaction hex'}), 400 # Additional validation to ensure the transaction is properly formatted if not electrum_client.validate_transaction_hex(raw_tx): return jsonify({'error': 'Invalid transaction format'}), 400 # Broadcast the transaction result = electrum_client.broadcast_transaction(raw_tx) if not result.get('success'): return jsonify({ 'success': False, 'error': result.get('error', 'Unknown broadcast error') }), 400 # Return the transaction ID along with success message return jsonify({ 'success': True, 'txid': result.get('txid'), 'message': 'Transaction broadcast successfully!' }) except Exception as e: return jsonify({ 'success': False, 'error': str(e) }), 400
The electrum client sends the transaction to LanaCoin's Electrum network:
# Code from electrum_client.py def broadcast_transaction(self, raw_tx): """ Broadcast a signed transaction to the LanaCoin network Args: raw_tx (str): The hex-encoded signed transaction Returns: dict: Result of the broadcast operation """ try: # Broadcast transaction using blockchain.transaction.broadcast method result = self.send_request('blockchain.transaction.broadcast', [raw_tx]) # If successful, result will be the transaction ID (txid) if isinstance(result, str) and len(result) == 64: return { 'success': True, 'txid': result } else: # If we get here, the result is not a txid, likely an error return { 'success': False, 'error': f'Unexpected response: {result}' } except Exception as e: return { 'success': False, 'error': str(e) }