From 4f8d55888a0d950a60ea0bf15174d00beadec95c Mon Sep 17 00:00:00 2001 From: supertestnet <58400631+supertestnet@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:04:18 +0000 Subject: [PATCH] Update chain_client.js --- chain_client.js | 293 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 287 insertions(+), 6 deletions(-) diff --git a/chain_client.js b/chain_client.js index 7b2ac07..f4bbbd9 100644 --- a/chain_client.js +++ b/chain_client.js @@ -1,14 +1,18 @@ // dependencies: // https://supertestnet.github.io/bankify/super_nostr.js +// https://unpkg.com/@cmdcode/tapscript@1.5.3 // https://bundle.run/noble-secp256k1@1.2.14 var chain_client = { connection_info: null, most_recent_fulfilled_command: null, - base_url: `https://supertestnet.github.io/testnet_generator/`, - // base_url: `file:///home/supertestnet/bitcoin_projects/testnet_generator/testnet_generator.html`, + // base_url: `https://supertestnet.github.io/testnet_generator/`, + base_url: `file:///home/supertestnet/bitcoin_projects/testnet_generator/testnet_generator.html`, main_connection: null, getPrivkey: () => window.crypto.getRandomValues( new Uint8Array( 32 ) ).toHex(), getPubkey: privkey => nobleSecp256k1.getPublicKey( privkey, true ).substring( 2 ), + hexToBytes: hex => Uint8Array.from( hex.match( /.{1,2}/g ).map( byte => parseInt( byte, 16 ) ) ), + bytesToHex: bytes => bytes.reduce( ( str, byte ) => str + byte.toString( 16 ).padStart( 2, "0" ), "" ), + reverseHexString: s => s.match( /[a-fA-F0-9]{2}/g ).reverse().join( '' ), waitSomeTime: num => new Promise( resolve => setTimeout( resolve, num ) ), textToHex: text => { var encoded = new TextEncoder().encode( text ); @@ -16,6 +20,15 @@ var chain_client = { .map( x => x.toString( 16 ).padStart( 2, "0" ) ) .join( "" ); }, + sha256: async s => { + if ( typeof s == "string" ) s = new TextEncoder().encode( s ); + var arr = await crypto.subtle.digest( 'SHA-256', s ); + return chain_client.bytesToHex( new Uint8Array( arr ) ); + }, + getNetworkUrl: ( privkey, relays ) => { + var hex_relays = chain_client.textToHex( JSON.stringify( relays ) ); + return `${chain_client.base_url}#privkey=${privkey}#relays=${hex_relays}`; + }, createNetwork: async relays => { return new Promise( async resolve => { var privkey = chain_client.getPrivkey(); @@ -156,9 +169,277 @@ var chain_client = { return commander; }, commander: async ( network, command, params ) => { - if ( typeof network === "object" ) var commander = chain_client.createCommander( network ); - var reply = await commander( command, params ); - chain_client.most_recent_fulfilled_command = [ command, reply ]; - return reply; + //this codeblock is for the use of testnet_generator + if ( typeof network === "object" ) { + var commander = chain_client.createCommander( network ); + var reply = await commander( command, params ); + chain_client.most_recent_fulfilled_command = [ command, reply ]; + return reply; + } + //this codeblock is for the use of electrum servers + if ( typeof network === "string" && network.startsWith( "ws" ) ) { + return new Promise( async resolve => { + var socket = new WebSocket( network ); + var loop = async () => { + if ( socket.readyState === 1 ) return; + await chain_client.waitSomeTime( 1 ); + return loop(); + } + await loop(); + var formatted_command = { + id: "setup_id", + method: "server.version", + params: [ "", "1.6" ], + } + socket.send( JSON.stringify( formatted_command ) ); + var command_id = chain_client.getPrivkey().substring( 0, 16 ); + if ( command === "blockheight" && !params ) { + var formatted_command = { + "id": command_id, + "method": "blockchain.headers.subscribe", + "params": [], + } + var handleFunction = async message => { + var json = JSON.parse( message.data ); + if ( json.id !== command_id ) return; + socket.close(); + resolve( json.result.height ); + } + socket.addEventListener( 'message', handleFunction ); + socket.send( JSON.stringify( formatted_command ) ); + return; + } + if ( command === "blockheight" && params ) { + var txid = params; + var txhex = null; + var confirmations = null; + var height = null; + var formatted_command = { + id: command_id, + method: "blockchain.transaction.get", + params: [ txid, true ], + } + var handleFunction = async message => { + var json = JSON.parse( message.data ); + if ( json.id === command_id ) { + if ( !json.result.confirmations ) { + resolve( 'unconfirmed' ); + return; + } + txhex = json.result.hex; + confirmations = json.result.confirmations; + var alt_command_id = chain_client.getPrivkey().substring( 0, 16 ); + var formatted_command = { + id: alt_command_id, + method: "blockchain.headers.subscribe", + params: [], + } + socket.send( JSON.stringify( formatted_command ) ); + } else if ( json.id !== "setup_id" ) { + socket.close(); + height = json.result.height; + } + } + socket.addEventListener( 'message', handleFunction ); + socket.send( JSON.stringify( formatted_command ) ); + var loop = async () => { + if ( height ) { + resolve( height + 1 - confirmations ); + return; + } + await chain_client.waitSomeTime( 10 ); + return loop(); + } + await loop(); + return; + } + if ( command === "rawtx" ) { + var txid = params; + var formatted_command = { + id: command_id, + method: "blockchain.transaction.get", + params: [ txid ], + } + var handleFunction = async message => { + var json = JSON.parse( message.data ); + if ( json.id !== command_id ) return; + socket.close(); + resolve( json.result ); + } + socket.addEventListener( 'message', handleFunction ); + socket.send( JSON.stringify( formatted_command ) ); + return; + } + if ( command === "broadcast" ) { + var package = params.split( "," ); + var formatted_command = { + id: command_id, + method: "blockchain.transaction.broadcast_package", + params: [ package ], + } + var handleFunction = async message => { + var json = JSON.parse( message.data ); + if ( json.id === command_id ) return; + if ( !json.result ) { + resolve( json.error.message ); + return; + } + //TODO: consider whether to rework this; I suspect it will return whichever txid comes first when sorted alphabetically, whereas chain_client always returns the txid of the parent. But I think all of my current software just checks if the result is a 64 byte hex string as an indicator of success, so maybe that's fine + resolve( Object.keys( json.result.txs )[ 0 ] ); + } + socket.addEventListener( 'message', handleFunction ); + socket.send( JSON.stringify( formatted_command ) ); + return; + } + if ( command === "utxos" ) { + var address = params; + var scripthex = tapscript.Script.encode( tapscript.Address.toScriptPubKey( address ) ).hex.substring( 2 ); + var scripthash = await chain_client.sha256( chain_client.hexToBytes( scripthex ) ); + var revhash = chain_client.reverseHexString( scripthash ); + var formatted_command = { + id: command_id, + method: "blockchain.scripthash.get_history", + params: [ revhash ], + } + var txids_and_heights = {} + var utxos = []; + var last_txid = null; + var handleFunction = async message => { + var json = JSON.parse( message.data ); + if ( json.id === command_id ) { + if ( !json.result.length ) { + resolve( [] ); + return; + } + var i; for ( i=0; i { + if ( vout.scriptPubKey !== scripthex ) return; + var status = { + confirmed: false, + } + if ( txids_and_heights[ txid ] > 0 ) status = { + confirmed: true, + block_height: txids_and_heights[ txid ], + block_hash, + block_time, + } + utxos.push({ + txid, + vout: index, + value: Number( vout.value ), + status, + }); + }); + if ( txid === last_txid ) { + socket.close(); + resolve( utxos ); + } + } + } + socket.addEventListener( 'message', handleFunction ); + console.log( 'sending' ); + socket.send( JSON.stringify( formatted_command ) ); + console.log( 'sent' ); + return; + } + if ( command === "spend_txs" ) { + var address = params; + var scripthex = tapscript.Script.encode( tapscript.Address.toScriptPubKey( address ) ).hex.substring( 2 ); + var scripthash = await chain_client.sha256( chain_client.hexToBytes( scripthex ) ); + var revhash = chain_client.reverseHexString( scripthash ); + var formatted_command = { + id: command_id, + method: "blockchain.scripthash.get_history", + params: [ revhash ], + } + var outpoints_and_inputs = {} + var spend_txs = []; + var last_a_txid = null; + var last_b_txid = null; + var handleFunction = async message => { + var json = JSON.parse( message.data ); + if ( json.id === command_id ) { + if ( !json.result.length ) { + resolve( [] ); + return; + } + var i; for ( i=0; i { + if ( !outputs_we_are_interested_in.includes( index ) ) return; + if ( vout.scriptPubKey !== scripthex ) return; + spend_txs.push( outpoints_and_inputs[ txid ][ index ] ); + }); + if ( txid === last_b_txid ) { + socket.close(); + resolve( spend_txs ); + } + } + } + socket.addEventListener( 'message', handleFunction ); + socket.send( JSON.stringify( formatted_command ) ); + return; + } + resolve( "unsupported command" ); + }); + } }, }