This comprehensive guide details every aspect of the LanaCoin transaction process, from Electrum server connection to transaction broadcasting.
import socket
import json
class ElectrumClient:
def __init__(self, host='electrum1.lanacoin.com', port=5097):
self.host = host
self.port = port
self.socket = None
self.request_id = 0
def connect(self):
"""Establish TCP connection to Electrum server"""
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(30) # 30-second timeout
self.socket.connect((self.host, self.port))
print(f"Connected to {self.host}:{self.port}")
return True
except Exception as e:
print(f"Connection failed: {e}")
return False
{
"id": <request_id>,
"method": "<method_name>",
"params": [<parameters>]
}
Response Format:
{
"id": <request_id>,
"result": <result_data>,
"error": null
}
def send_request(self, method, params):
"""Send JSON-RPC request to Electrum server"""
if not self.socket:
if not self.connect():
raise Exception("Unable to connect to Electrum server")
# Increment request ID for each call
self.request_id += 1
# Prepare JSON-RPC request
request = {
"id": self.request_id,
"method": method,
"params": params
}
# Convert to JSON and add newline delimiter
message = json.dumps(request) + '\n'
try:
# Send request
self.socket.send(message.encode('utf-8'))
# Receive response
response_data = ""
while True:
chunk = self.socket.recv(4096).decode('utf-8')
response_data += chunk
if '\n' in response_data:
break
# Parse JSON response
response = json.loads(response_data.strip())
# Check for errors in response
if 'error' in response and response['error']:
raise Exception(f"Electrum error: {response['error']}")
return response.get('result')
except Exception as e:
print(f"Request failed: {e}")
self.socket = None # Reset connection on error
raise
blockchain.address.get_balance
- Get address balanceblockchain.address.listunspent
- Get unspent outputsblockchain.transaction.broadcast
- Broadcast signed transactionblockchain.headers.subscribe
- Get current blockchain heightblockchain.address.listunspent
def get_unspent(self, address):
"""Get unspent transaction outputs for an address"""
try:
# Validate address format first
if not self.validate_address(address):
raise Exception("Invalid address format")
# Request unspent outputs from Electrum server
unspent_list = self.send_request('blockchain.address.listunspent', [address])
# Process and validate each UTXO
processed_utxos = []
for utxo in unspent_list:
processed_utxo = {
'tx_hash': utxo['tx_hash'], # Transaction hash (hex)
'tx_pos': utxo['tx_pos'], # Output position (vout)
'value': utxo['value'], # Value in satoshis
'height': utxo.get('height', 0), # Block height (0 = unconfirmed)
'confirmations': self.calculate_confirmations(utxo.get('height', 0))
}
processed_utxos.append(processed_utxo)
return processed_utxos
except Exception as e:
print(f"Failed to get unspent outputs: {e}")
raise
def calculate_confirmations(self, utxo_height):
"""Calculate number of confirmations for a UTXO"""
if utxo_height == 0:
return 0 # Unconfirmed transaction
current_height = self.get_current_height()
return max(0, current_height - utxo_height + 1)
def select_inputs(self, unspent, target_amount, send_all=False):
"""Select optimal UTXOs for transaction"""
if send_all:
# Use all available UTXOs
return unspent
# Sort UTXOs by value (largest first) for optimal selection
sorted_utxos = sorted(unspent, key=lambda x: x['value'], reverse=True)
selected = []
total_selected = 0
for utxo in sorted_utxos:
selected.append(utxo)
total_selected += utxo['value']
# Check if we have enough to cover target amount + estimated fee
estimated_fee = self.estimate_fee(len(selected), 3) # 3 outputs typical
if total_selected >= target_amount + estimated_fee:
break
if total_selected < target_amount:
raise Exception("Insufficient funds")
return selected
def prepare_inputs(self, selected_utxos):
"""Prepare transaction inputs from selected UTXOs"""
inputs = []
for utxo in selected_utxos:
# Reverse byte order for tx_hash (little-endian)
tx_hash_bytes = bytes.fromhex(utxo['tx_hash'])[::-1]
# Convert output position to 4-byte little-endian
tx_pos_bytes = utxo['tx_pos'].to_bytes(4, 'little')
# Get the scriptPubKey from the original transaction
script_pub_key = self.get_script_pub_key(utxo['tx_hash'], utxo['tx_pos'])
input_data = {
'tx_hash': utxo['tx_hash'],
'tx_pos': utxo['tx_pos'],
'script_pub_key': script_pub_key,
'value': utxo['value'],
'sequence': 0xffffffff # Standard sequence number
}
inputs.append(input_data)
return inputs
def get_script_pub_key(self, tx_hash, tx_pos):
"""Retrieve scriptPubKey from original transaction"""
# Get raw transaction data
raw_tx = self.send_request('blockchain.transaction.get', [tx_hash, True])
# Extract the scriptPubKey from the specific output
output = raw_tx['vout'][tx_pos]
return output['scriptPubKey']['hex']
def prepare_outputs(self, destination_address, amount_satoshis, change_address, change_amount, donation_address, donation_amount):
"""Prepare transaction outputs"""
outputs = []
# Main output to destination
if amount_satoshis > 0:
dest_pubkey_hash = self.get_pubkey_hash_from_address(destination_address)
dest_script = self.create_p2pkh_script(dest_pubkey_hash)
outputs.append({
'value': amount_satoshis,
'script': dest_script,
'address': destination_address
})
# Change output (if needed)
if change_amount > 546: # Dust limit check
change_pubkey_hash = self.get_pubkey_hash_from_address(change_address)
change_script = self.create_p2pkh_script(change_pubkey_hash)
outputs.append({
'value': change_amount,
'script': change_script,
'address': change_address
})
# Donation output
if donation_amount > 0:
donation_pubkey_hash = self.get_pubkey_hash_from_address(donation_address)
donation_script = self.create_p2pkh_script(donation_pubkey_hash)
outputs.append({
'value': donation_amount,
'script': donation_script,
'address': donation_address
})
return outputs
def create_p2pkh_script(self, pubkey_hash):
"""Create Pay-to-Public-Key-Hash script"""
# Standard P2PKH script: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG
script = bytes([0x76, 0xa9, 0x14]) + pubkey_hash + bytes([0x88, 0xac])
return script.hex()
def create_preimage_for_signing(self, inputs, outputs, input_index, sighash_type=0x01):
"""Create the preimage that will be signed"""
preimage = bytearray()
# Version (4 bytes, little-endian)
preimage.extend((1).to_bytes(4, 'little'))
# nTime (4 bytes, little-endian)
current_time = int(time.time())
preimage.extend(current_time.to_bytes(4, 'little'))
# Input count
preimage.extend(len(inputs).to_bytes(1, 'little'))
# Process inputs
for i, inp in enumerate(inputs):
# Previous transaction hash (32 bytes, little-endian)
tx_hash_bytes = bytes.fromhex(inp['tx_hash'])[::-1]
preimage.extend(tx_hash_bytes)
# Previous output index (4 bytes, little-endian)
preimage.extend(inp['tx_pos'].to_bytes(4, 'little'))
# Script length and script
if i == input_index:
# For the input being signed, use the scriptPubKey
script_bytes = bytes.fromhex(inp['script_pub_key'])
else:
# For other inputs, use empty script
script_bytes = b''
preimage.extend(len(script_bytes).to_bytes(1, 'little'))
preimage.extend(script_bytes)
# Sequence (4 bytes, little-endian)
preimage.extend(inp['sequence'].to_bytes(4, 'little'))
# Output count
preimage.extend(len(outputs).to_bytes(1, 'little'))
# Process outputs
for output in outputs:
# Value (8 bytes, little-endian)
preimage.extend(output['value'].to_bytes(8, 'little'))
# Script length and script
script_bytes = bytes.fromhex(output['script'])
preimage.extend(len(script_bytes).to_bytes(1, 'little'))
preimage.extend(script_bytes)
# Lock time (4 bytes, little-endian)
preimage.extend((0).to_bytes(4, 'little'))
# SIGHASH type (4 bytes, little-endian)
preimage.extend(sighash_type.to_bytes(4, 'little'))
return bytes(preimage)
// JavaScript implementation for offline signing
class LanaTransactionSigner {
constructor() {
// Initialize elliptic curve cryptography
this.ec = new elliptic.ec('secp256k1');
}
async signTransaction(privateKeyWIF, unsignedTransaction) {
try {
// Decode WIF private key
const privateKey = this.decodeWIF(privateKeyWIF);
const keyPair = this.ec.keyFromPrivate(privateKey, 'hex');
// Sign each input
const signedInputs = [];
for (let i = 0; i < unsignedTransaction.inputs.length; i++) {
const signature = await this.signInput(keyPair, unsignedTransaction, i);
signedInputs.push(signature);
}
// Construct final signed transaction
return this.buildSignedTransaction(unsignedTransaction, signedInputs);
} catch (error) {
console.error('Signing failed:', error);
throw error;
}
}
async signInput(keyPair, transaction, inputIndex) {
// Create preimage for this input
const preimage = this.createPreimage(transaction, inputIndex);
// Double SHA-256 hash
const hash = await this.doubleSha256(preimage);
// Sign the hash
const signature = keyPair.sign(hash, {canonical: true});
// Convert signature to DER format
let derSig = signature.toDER();
// Add SIGHASH_ALL flag (0x01)
derSig.push(0x01);
// Get public key
const publicKey = keyPair.getPublic(true, 'hex');
return {
signature: derSig,
publicKey: publicKey
};
}
decodeWIF(wif) {
// Decode WIF (Wallet Import Format) private key
const decoded = this.base58CheckDecode(wif);
// Remove version byte and compression flag if present
let privateKey = decoded.slice(1);
if (privateKey.length === 33) {
privateKey = privateKey.slice(0, 32); // Remove compression flag
}
return privateKey.toString('hex');
}
async doubleSha256(data) {
// First SHA-256
const hash1 = await crypto.subtle.digest('SHA-256', data);
// Second SHA-256
const hash2 = await crypto.subtle.digest('SHA-256', hash1);
return new Uint8Array(hash2);
}
buildSignedTransaction(transaction, signatures) {
let signedTx = '';
// Version (4 bytes, little-endian)
signedTx += this.toLittleEndianHex(1, 4);
// nTime (4 bytes, little-endian)
signedTx += this.toLittleEndianHex(transaction.nTime, 4);
// Input count
signedTx += this.toVarInt(transaction.inputs.length);
// Inputs with signatures
for (let i = 0; i < transaction.inputs.length; i++) {
const input = transaction.inputs[i];
const sig = signatures[i];
// Previous transaction hash (32 bytes, little-endian)
signedTx += this.reverseHex(input.tx_hash);
// Previous output index (4 bytes, little-endian)
signedTx += this.toLittleEndianHex(input.tx_pos, 4);
// Create scriptSig (signature + public key)
const scriptSig = this.createScriptSig(sig.signature, sig.publicKey);
signedTx += this.toVarInt(scriptSig.length / 2);
signedTx += scriptSig;
// Sequence (4 bytes, little-endian)
signedTx += this.toLittleEndianHex(0xffffffff, 4);
}
// Output count
signedTx += this.toVarInt(transaction.outputs.length);
// Outputs
for (const output of transaction.outputs) {
// Value (8 bytes, little-endian)
signedTx += this.toLittleEndianHex(output.value, 8);
// Script length and script
const scriptBytes = output.script.length / 2;
signedTx += this.toVarInt(scriptBytes);
signedTx += output.script;
}
// Lock time (4 bytes, little-endian)
signedTx += this.toLittleEndianHex(0, 4);
return signedTx;
}
createScriptSig(signature, publicKey) {
// Push signature onto stack
let scriptSig = '';
scriptSig += this.pushData(signature);
// Push public key onto stack
const pubKeyBytes = publicKey.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
scriptSig += this.pushData(pubKeyBytes);
return scriptSig;
}
pushData(data) {
const length = Array.isArray(data) ? data.length : data.length;
let result = '';
if (length <= 75) {
// Direct push with length prefix
result += length.toString(16).padStart(2, '0');
result += Array.isArray(data) ?
data.map(b => b.toString(16).padStart(2, '0')).join('') :
data.map(b => b.toString(16).padStart(2, '0')).join('');
} else {
throw new Error('Data too large for simple push');
}
return result;
}
}
verifySignature(publicKey, signature, messageHash) {
try {
const key = this.ec.keyFromPublic(publicKey, 'hex');
const sig = {
r: signature.slice(0, 32),
s: signature.slice(32, 64)
};
return key.verify(messageHash, sig);
} catch (error) {
console.error('Signature verification failed:', error);
return false;
}
}
blockchain.transaction.broadcast
def broadcast_transaction(self, signed_tx_hex):
"""Broadcast signed transaction to the network"""
try:
# Validate transaction format before broadcasting
if not self.validate_transaction_hex(signed_tx_hex):
raise Exception("Invalid transaction format")
# Broadcast to Electrum server
result = self.send_request('blockchain.transaction.broadcast', [signed_tx_hex])
# The result should be the transaction ID
if isinstance(result, str) and len(result) == 64:
print(f"Transaction broadcasted successfully: {result}")
return {
'success': True,
'txid': result,
'message': 'Transaction submitted to network'
}
else:
raise Exception(f"Unexpected broadcast result: {result}")
except Exception as e:
error_msg = str(e)
print(f"Broadcast failed: {error_msg}")
# Parse common error messages
if "insufficient fee" in error_msg.lower():
return {
'success': False,
'error': 'Transaction fee is too low',
'suggestion': 'Increase the transaction fee and try again'
}
elif "dust" in error_msg.lower():
return {
'success': False,
'error': 'Transaction creates dust outputs',
'suggestion': 'Increase output amounts above dust threshold'
}
elif "double spend" in error_msg.lower():
return {
'success': False,
'error': 'Transaction inputs already spent',
'suggestion': 'Refresh wallet and create new transaction'
}
else:
return {
'success': False,
'error': error_msg,
'suggestion': 'Check transaction format and try again'
}
def validate_transaction_hex(self, tx_hex):
"""Validate transaction hex format"""
try:
# Check if hex string is valid
if not tx_hex or len(tx_hex) % 2 != 0:
return False
# Try to decode hex
tx_bytes = bytes.fromhex(tx_hex)
# Basic structure validation
if len(tx_bytes) < 10: # Minimum transaction size
return False
# Check version (first 4 bytes should be 01000000 for version 1)
version = int.from_bytes(tx_bytes[0:4], 'little')
if version != 1:
return False
return True
except Exception as e:
print(f"Transaction validation error: {e}")
return False
def monitor_transaction_status(self, txid, timeout_seconds=300):
"""Monitor transaction confirmation status"""
start_time = time.time()
while time.time() - start_time < timeout_seconds:
try:
# Get transaction details
tx_info = self.send_request('blockchain.transaction.get', [txid, True])
if 'confirmations' in tx_info:
confirmations = tx_info['confirmations']
if confirmations > 0:
return {
'status': 'confirmed',
'confirmations': confirmations,
'block_height': tx_info.get('height', 0)
}
else:
return {
'status': 'pending',
'confirmations': 0,
'in_mempool': True
}
else:
return {
'status': 'not_found',
'error': 'Transaction not found in mempool or blockchain'
}
except Exception as e:
print(f"Error monitoring transaction: {e}")
# Wait before next check
time.sleep(10)
return {
'status': 'timeout',
'error': 'Monitoring timeout reached'
}
Error Type | Cause | Solution |
---|---|---|
Connection Timeout | Electrum server unreachable | Implement retry logic with exponential backoff |
Insufficient Funds | UTXOs don't cover amount + fee | Reduce amount or wait for more UTXOs |
Invalid Signature | Signing process error | Verify preimage construction and private key |
Double Spend | UTXOs already used | Refresh UTXO list and rebuild transaction |
Fee Too Low | Network congestion | Increase fee rate and rebuild transaction |