2025-12-20 16:16:33 +00:00
// dependencies:
// https://supertestnet.github.io/bankify/super_nostr.js
2026-02-07 22:04:18 +00:00
// https://unpkg.com/@cmdcode/tapscript@1.5.3
2025-12-20 16:16:33 +00:00
// https://bundle.run/noble-secp256k1@1.2.14
var chain_client = {
connection_info : null ,
2025-12-20 17:03:34 +00:00
most_recent_fulfilled_command : null ,
2026-02-07 22:04:18 +00:00
// base_url: `https://supertestnet.github.io/testnet_generator/`,
base_url : `file:///home/supertestnet/bitcoin_projects/testnet_generator/testnet_generator.html` ,
2026-02-10 22:48:16 +00:00
private_url : null ,
public_url : null ,
2025-12-22 15:38:17 +00:00
main_connection : null ,
2025-12-20 16:16:33 +00:00
getPrivkey : () => window . crypto . getRandomValues ( new Uint8Array ( 32 ) ). toHex (),
getPubkey : privkey => nobleSecp256k1 . getPublicKey ( privkey , true ). substring ( 2 ),
2026-02-07 22:04:18 +00:00
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 ( '' ),
2025-12-20 16:16:33 +00:00
waitSomeTime : num => new Promise ( resolve => setTimeout ( resolve , num ) ),
textToHex : text => {
var encoded = new TextEncoder (). encode ( text );
return Array . from ( encoded )
. map ( x => x . toString ( 16 ). padStart ( 2 , "0" ) )
. join ( "" );
},
2026-02-07 22:04:18 +00:00
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 ) );
},
2026-02-10 22:48:16 +00:00
getNetworkUrl : ( privkey_or_pubkey , relays , is_public_key = true ) => {
2026-02-07 22:04:18 +00:00
var hex_relays = chain_client . textToHex ( JSON . stringify ( relays ) );
2026-02-10 22:48:16 +00:00
if ( is_public_key ) return ` ${ chain_client . base_url } #pubkey= ${ privkey_or_pubkey } #relays= ${ hex_relays } ` ;
return ` ${ chain_client . base_url } #privkey= ${ privkey_or_pubkey } #relays= ${ hex_relays } ` ;
2026-02-07 22:04:18 +00:00
},
2025-12-20 16:16:33 +00:00
createNetwork : async relays => {
return new Promise ( async resolve => {
var privkey = chain_client . getPrivkey ();
var say_when_connected = chain_client . getPrivkey ();
var pubkey = chain_client . getPubkey ( privkey );
var hex_relays = chain_client . textToHex ( JSON . stringify ( relays ) );
var iframe = document . createElement ( "iframe" );
2025-12-20 17:03:34 +00:00
// iframe.src = `${chain_client.base_url}#privkey=${privkey}#relays=${hex_relays}#say_when_connected=${say_when_connected}`;
iframe . src = ` ${ chain_client . base_url } #privkey= ${ privkey } #relays= ${ hex_relays } #say_when_connected= ${ say_when_connected } ` ;
2025-12-20 16:16:33 +00:00
iframe . style . display = "none" ;
iframe . className = `chain_client_network_ ${ privkey } ` ;
document . body . append ( iframe );
var network_string = ` ${ pubkey } , ${ relays [ 0 ] } ` ;
//return info when testnet is ready for commands
var listenFunction = async socket => {
var subId = super_nostr . bytesToHex ( crypto . getRandomValues ( new Uint8Array ( 8 ) ) );
var filter = {}
filter . kinds = [ 54091 ];
filter . authors = [ pubkey ];
filter [ "#e" ] = [ say_when_connected ];
var subscription = [ "REQ" , subId , filter ];
socket . send ( JSON . stringify ( subscription ) );
}
var handleFunction = async message => {
var [ type , subId , event ] = JSON . parse ( message . data );
if ( ! event || event === true ) return ;
chain_client . connection_info = [ privkey , network_string ];
2026-02-10 22:48:16 +00:00
var pubkey = chain_client . getPubkey ( privkey );
var public_url = chain_client . getNetworkUrl ( pubkey , relays , true );
var private_url = chain_client . getNetworkUrl ( privkey , relays , false );
chain_client . public_url = public_url ;
chain_client . private_url = private_url ;
resolve ( [ privkey , network_string , public_url , private_url ] );
2025-12-20 16:16:33 +00:00
}
var connection_id = await super_nostr . newPermanentConnection ( relays [ 0 ], listenFunction , handleFunction );
2025-12-22 15:38:17 +00:00
chain_client . main_connection = connection_id ;
2025-12-20 16:16:33 +00:00
//shut down
var loop = async () => {
if ( chain_client . connection_info ) {
super_nostr . sockets [ connection_id ]. socket . close ();
delete super_nostr . sockets [ connection_id ];
return chain_client . connection_info = null ;
}
await chain_client . waitSomeTime ( 1_000 );
return loop ();
}
loop ();
});
},
loadNetwork : async ( privkey , network_string ) => {
return new Promise ( async resolve => {
var [ pubkey , relay ] = network_string . split ( "," );
var relays = [ relay ];
var hex_relays = chain_client . textToHex ( JSON . stringify ( relays ) );
var iframe = document . createElement ( "iframe" );
2025-12-20 17:03:34 +00:00
// if ( privkey ) iframe.src = `${chain_client.base_url}#privkey=${privkey}#relays=${hex_relays}#say_when_connected=${say_when_connected}`;
// else iframe.src = `${chain_client.base_url}#pubkey=${pubkey}#relays=${hex_relays}#say_when_connected=${say_when_connected}`;
if ( privkey ) iframe . src = ` ${ chain_client . base_url } #privkey= ${ privkey } #relays= ${ hex_relays } ` ;
else iframe . src = ` ${ chain_client . base_url } #pubkey= ${ pubkey } #relays= ${ hex_relays } ` ;
2025-12-20 16:16:33 +00:00
iframe . style . display = "none" ;
iframe . className = `chain_client_network_ ${ privkey || pubkey } ` ;
document . body . append ( iframe );
//return info when testnet is ready for commands
2026-02-10 22:48:16 +00:00
var loop_limit = 8 ;
2025-12-20 16:16:33 +00:00
var loop = async () => {
2026-02-10 22:48:16 +00:00
if ( loop_limit < 1 ) return ;
var promiseA = async () => {
var commander = chain_client . createCommander ( network_string . split ( "," ) );
var test = await commander ( "rawtx" , "a" . repeat ( 64 ) );
if ( test === "tx not found" && privkey ) {
var pubkey = chain_client . getPubkey ( privkey );
var public_url = chain_client . getNetworkUrl ( pubkey , relays , true );
var private_url = chain_client . getNetworkUrl ( privkey , relays , false );
chain_client . public_url = public_url ;
chain_client . private_url = private_url ;
return resolve ( [ privkey , network_string , public_url , private_url ] );
}
if ( test === "tx not found" ) {
var publkey = network_string . split ( "," )[ 0 ];
var public_url = chain_client . getNetworkUrl ( pubkey , relays , true );
chain_client . public_url = public_url ;
return resolve ( [ null , network_string , public_url ] );
}
}
var promiseB = async () => {
await chain_client . waitSomeTime ( 1_000 );
return "timeout" ;
}
var reply = await Promise . any ( [ promiseA (), promiseB () ] );
if ( reply === "timeout" ) {
loop_limit = loop_limit - 1 ;
return await loop ();
}
return true ;
2025-12-20 16:16:33 +00:00
}
await loop ();
});
},
createCommander : network => {
var [ miner , relay ] = network ;
var commander = async ( command , params ) => {
//prepare requisite variables
var privkey = super_nostr . getPrivkey ();
var pubkey = super_nostr . getPubkey ( privkey );
var item_listened_for = null ;
//establish connection to relay
var listenFunction = async socket => {
var subId = super_nostr . bytesToHex ( crypto . getRandomValues ( new Uint8Array ( 8 ) ) );
var filter = {}
filter . kinds = [ 4 ];
filter [ "#p" ] = [ pubkey ];
filter . since = Math . floor ( Date . now () / 1000 );
var subscription = [ "REQ" , subId , filter ];
socket . send ( JSON . stringify ( subscription ) );
}
var handleFunction = async message => {
var [ type , subId , event ] = JSON . parse ( message . data );
if ( ! event || event === true ) return ;
var recipient = event . pubkey ;
var content = await super_nostr . alt_decrypt ( privkey , recipient , event . content );
var json = JSON . parse ( content );
item_listened_for = json . msg_value ;
}
var connection_id = await super_nostr . newPermanentConnection ( relay , listenFunction , handleFunction );
2025-12-22 15:38:17 +00:00
chain_client . main_connection = connection_id ;
2025-12-20 16:16:33 +00:00
//send command
var msg = JSON . stringify ({ msg_type : command , msg_value : params });
var emsg = await super_nostr . alt_encrypt ( privkey , miner , msg );
var event = await super_nostr . prepEvent ( privkey , emsg , 4 , [ [ "p" , miner ] ] );
2025-12-22 15:38:17 +00:00
var socket = super_nostr . sockets [ chain_client . main_connection ]. socket ;
var loop = async () => {
if ( socket . readyState === 1 ) return ;
await chain_client . waitSomeTime ( 10 );
return loop ();
}
await loop ();
super_nostr . sendEvent ( event , socket );
2025-12-20 16:16:33 +00:00
//listen for reply
var loop = async () => {
await chain_client . waitSomeTime ( 10 );
if ( ! item_listened_for ) return loop ();
}
await loop ();
//delete connection
try {
var loop = async () => {
if ( ! super_nostr . sockets . hasOwnProperty ( connection_id ) ) return ;
super_nostr . sockets [ connection_id ]. socket . close ();
delete super_nostr . sockets [ connection_id ];
var rand_interval = Math . floor ( Math . random () * 1000 ) + 1000 ;
await chain_client . waitSomeTime ( rand_interval );
loop ();
}
loop ();
} catch ( e ) {}
//return reply
return item_listened_for ;
}
return commander ;
},
commander : async ( network , command , params ) => {
2026-02-07 22:04:18 +00:00
//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 ();
2026-02-10 21:57:37 +00:00
var setup_id = chain_client . getPrivkey (). substring ( 0 , 16 );
2026-02-07 22:04:18 +00:00
var formatted_command = {
2026-02-10 21:57:37 +00:00
id : setup_id ,
2026-02-07 22:04:18 +00:00
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 ) );
2026-02-10 21:57:37 +00:00
} else if ( json . id !== setup_id ) {
2026-02-07 22:04:18 +00:00
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 );
2026-02-10 19:01:27 +00:00
if ( json . id !== command_id ) return ;
2026-02-07 22:04:18 +00:00
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
2026-02-10 19:01:27 +00:00
if ( json . result . hasOwnProperty ( "success" ) && json . result . success ) {
resolve ( chain_client . getPrivkey () );
return ;
}
2026-02-07 22:04:18 +00:00
resolve ( Object . keys ( json . result . txs )[ 0 ] );
2026-02-10 19:01:27 +00:00
return ;
2026-02-07 22:04:18 +00:00
}
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 ],
}
2026-02-10 19:59:55 +00:00
var processing_line = [];
var gathered = [];
2026-02-10 19:01:27 +00:00
var spend_txs = [];
2026-02-07 22:04:18 +00:00
var txids_and_heights = {}
var utxos = [];
var last_txid = null ;
2026-02-10 19:59:55 +00:00
var done_gathering_info = false ;
var txs_of_interest = {}
2026-02-07 22:04:18 +00:00
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 < json . result . length ; i ++ ) {
var txid = json . result [ i ]. tx_hash ;
txids_and_heights [ txid ] = json . result [ i ]. height ;
if ( i === json . result . length - 1 ) last_txid = txid ;
2026-02-10 19:59:55 +00:00
processing_line . push ( txid );
2026-02-07 22:04:18 +00:00
}
2026-02-10 21:57:37 +00:00
} else if ( json . id !== setup_id ) {
2026-02-07 22:04:18 +00:00
var txhex = json . result . hex ;
var block_hash = json . result . blockhash ;
var block _ time = json . result . blocktime ;
var txid = tapscript . Tx . util . getTxid ( txhex );
2026-02-10 19:01:27 +00:00
var inputs = tapscript . Tx . decode ( txhex ). vin ;
inputs . forEach ( input => spend_txs . push ( ` ${ input . txid } _ ${ input . vout } ` ) );
2026-02-10 19:59:55 +00:00
txs_of_interest [ txid ] = { outputs : [ ... tapscript . Tx . decode ( txhex ). vout ], block_hash , block_time };
gathered . push ( txid );
2026-02-07 22:04:18 +00:00
if ( txid === last_txid ) {
2026-02-10 19:59:55 +00:00
done_gathering_info = true ;
2026-02-07 22:04:18 +00:00
socket . close ();
}
}
}
socket . addEventListener ( 'message' , handleFunction );
socket . send ( JSON . stringify ( formatted_command ) );
2026-02-10 19:59:55 +00:00
//wait til the we finish processing the reply to the first command_id
var loop = async () => {
if ( last_txid ) return ;
await chain_client . waitSomeTime ( 10 );
return loop ();
}
await loop ();
//gather txs one by one
var i ; for ( i = 0 ; i < processing_line . length ; i ++ ) {
var current_num_of_gathered = gathered . length ;
var txid = processing_line [ i ];
var alt_command_id = chain_client . getPrivkey (). substring ( 0 , 16 );
var formatted_command = {
id : alt_command_id ,
method : "blockchain.transaction.get" ,
params : [ txid , true ],
}
socket . send ( JSON . stringify ( formatted_command ) );
var loop = async () => {
if ( gathered . length > current_num_of_gathered ) return ;
await chain_client . waitSomeTime ( 1 );
return loop ();
}
await loop ();
}
//wait til all the txs to investigate are gathered
var loop = async () => {
if ( done_gathering_info ) return ;
await chain_client . waitSomeTime ( 10 );
return loop ();
}
await loop ();
//investigate all the gathered txs
Object . keys ( txs_of_interest ). forEach ( txid => {
txs_of_interest [ txid ]. outputs . forEach ( ( vout , index ) => {
if ( vout . scriptPubKey !== scripthex ) return ;
if ( spend_txs . includes ( ` ${ txid } _ ${ index } ` ) ) return ;
var status = {
confirmed : false ,
}
if ( txids_and_heights [ txid ] > 0 ) status = {
confirmed : true ,
block_height : txids_and_heights [ txid ],
block_hash : txs_of_interest [ txid ]. block_hash ,
block_time : txs_of_interest [ txid ]. block_time ,
}
utxos . push ({
txid ,
vout : index ,
value : Number ( vout . value ),
status ,
});
});
});
resolve ( utxos );
2026-02-07 22:04:18 +00:00
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 );
2026-02-10 21:57:37 +00:00
if ( json . id === setup_id ) return ;
if ( json . id === command_id ) {
2026-02-07 22:04:18 +00:00
if ( ! json . result . length ) {
resolve ( [] );
return ;
}
var i ; for ( i = 0 ; i < json . result . length ; i ++ ) {
var txid = json . result [ i ]. tx_hash ;
if ( i === json . result . length - 1 ) last_a_txid = txid ;
var a_command_id = chain_client . getPrivkey (). substring ( 0 , 16 );
var formatted_command = {
id : "a_" + a_command_id ,
method : "blockchain.transaction.get" ,
params : [ txid ],
}
socket . send ( JSON . stringify ( formatted_command ) );
}
} else if ( json . id . startsWith ( "a_" ) ) {
//if we are in this section then we know this is a tx in our the history of the address we are investigating; I call it an a_tx. We must now find out if this a_tx spent a utxo locked to our address. To do that we get each input utxo in the a_tx and query for the transaction that created each one; I call that transaction a b_tx. We also remind ourselves, via the outpoints_and_inputs object, which specific output of the b_tx we are interested in; if that output of the b_tx is locked to our address, then we know our a_tx actually spent it, and we even know which input of our a_tx spent it (I call that j below).
var txhex = json . result ;
var txid = tapscript . Tx . util . getTxid ( txhex );
var tx = tapscript . Tx . decode ( txhex );
var j ; for ( j = 0 ; j < tx . vin . length ; j ++ ) {
var vin = tx . vin [ j ];
//the string vin.txid below refers to a b_tx which created an output spent by the a_tx; the output is specified as the key in an object created below, and we must check that b_tx to see if the vout referred to locked some money to the address we are investigating
//in the string `${txid}_${j}`, the txid refers to the a_tx, and the j refers to the input of that a_tx which spent the outpoint in question
if ( vin . txid === "0" . repeat ( 64 ) ) continue ;
if ( ! outpoints_and_inputs . hasOwnProperty ( vin . txid ) ) outpoints_and_inputs [ vin . txid ] = {};
outpoints_and_inputs [ vin . txid ][ vin . vout ] = ` ${ txid } _ ${ j } ` ;
//get the prev tx to see if it locked money to our utxo was spent in it
var b_command_id = chain_client . getPrivkey (). substring ( 0 , 16 );
var formatted_command = {
id : "b_" + b_command_id ,
method : "blockchain.transaction.get" ,
params : [ vin . txid ],
}
socket . send ( JSON . stringify ( formatted_command ) );
//if we are investigating the input of the last a_tx, set the last b_tx
2026-02-10 21:57:37 +00:00
if ( txid === last_a_txid && j === tx . vin . length - 1 ) {
last_b_txid = vin . txid ;
}
2026-02-07 22:04:18 +00:00
}
} else if ( json . id . startsWith ( "b_" ) ) {
//if we are in this section then we know this is a b_tx, that is, a tx that supposedly locked money to our address. We must check if the outputs references in the outpoints_and_inputs object actually locked money to our address; if they did, we can list add the string `${txid}_${j}` to spend_txs
var txhex = json . result ;
var txid = tapscript . Tx . util . getTxid ( txhex );
2026-02-10 21:57:37 +00:00
console . log ( 19 , 'possible spend tx:' , txid );
2026-02-07 22:04:18 +00:00
var outputs = tapscript . Tx . decode ( txhex ). vout ;
if ( ! outpoints_and_inputs . hasOwnProperty ( txid ) ) console . log ( 'i was asked to check' , txid , 'which does not exist in the object:' , outpoints_and_inputs );
var outputs_we_are_interested_in = Object . keys ( outpoints_and_inputs [ txid ] ). map ( Number );
outputs . forEach ( ( vout , index ) => {
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" );
});
}
2025-12-20 16:16:33 +00:00
},
}