Update chain_client.js
This commit is contained in:
1 parent
2e5922d060
commit
4f8d55888a
1 file changed
+287
-6
+287
-6
@@ -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<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;
|
||||
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 ) );
|
||||
}
|
||||
} else if ( json.id !== "setup_id" ) {
|
||||
var txhex = json.result.hex;
|
||||
var block_hash = json.result.blockhash;
|
||||
var block_time = json.result.blocktime;
|
||||
var txid = tapscript.Tx.util.getTxid( txhex );
|
||||
var outputs = tapscript.Tx.decode( txhex ).vout;
|
||||
outputs.forEach( ( vout, index ) => {
|
||||
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<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
|
||||
if ( txid === last_a_txid && j === tx.vin.length - 1 ) last_b_txid = vin.txid;
|
||||
}
|
||||
} 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 );
|
||||
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" );
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user