Import supertestnet/testnet_generator with full upstream history

git-subtree-dir: upstream/testnet_generator
git-subtree-mainline: 70419cffc2
git-subtree-split: 498be4cc77
This commit is contained in:
Codex committed 2026-10-04 22:28:31 +00:00
commit 60d02f9669
5 files changed
+4041

No files matched your search

+1
View File
@@ -0,0 +1 @@
+121
View File
@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
+46
View File
@@ -0,0 +1,46 @@
# Testnet generator
Make your own bitcoin testnet
# What is this?
When you click [this link](https://supertestnet.github.io/testnet_generator/) you'll generate your own bitcoin testnet. You get to mine a block by clicking a button, you get 50 fake bitcoins whenever you do, you can send those fake bitcoins to other people and receive them from them, and you can explore your chain by clicking blocks, transactions, and addresses.
# How can I try it?
Just click here: https://supertestnet.github.io/testnet_generator/
# Why did you make this?
To alleviate three of my frustrations with existing testnet solutions.
## #1: Rate limits
One is a frustration with testnet faucets, which always have a "rate limiting" problem. That is, every testnet faucet has to choose between one of two bad options: if a testnet faucet *doesn't* limit how often or how much money users can take from it per-click, then trolls will spam it til it runs out of money, whereupon it won't be useful to anyone. But if it *does* limit one or both of those things, then users who are rapidly testing their software will soon hit the rate limit, and then their progress slows down.
Testnet generator fixes this problem by having the blockchain be lightweight enough that you can run it and all associated infrastructure (including a faucet) directly in your browser. That way, you can only spam yourself, so you don't need to guard against that, meaning you don't need rate limits.
## #2: Slow blocks
Another frustration I have is with blocks that come too slowly. Most testnets use the same 10 minute blocktime bitcoin uses. Some are more "generous" and reduce the blocktime to 30 seconds. But that's not always good enough. If I'm testing something like a lightning force closure, you have to wait 2016 blocks before finalizing, and even with 30 second blocks, that means waiting about 17 hours to finalize the force closure. Too long.
Testnet generator fixes this problem by giving you a button to insta-mine a block. That way, if you need to wait 2016 blocks, you can just rapid-fire click it a couple thousand times and be on your way. (I should probably ease this further by adding a dropdown that lets you specify how many blocks you want to mine per click.)
## #3: Regtest sucks
Another frustration I have is with bitcoin's "regtest" mode. When testing my software I often use that *instead of* testnets, largely because there's a command you can use to insta-mine as many blocks as you like. But regtest mode has several quirks: it's kind of a pain to get it started because, not only do you have to download and configure bitcoin core to run in that mode, but you also can't start testing apps immediately, you have to mine 101 blocks first due to one of bitcoin's consensus rules regarding mining. Also, if you get something to work on regtest, you can't easily share what you've done on social media by e.g. sharing a link, because your regtest node is likely not exposed to the public internet, and even if it was, regtest mode *looks ugly* and confusing, so no one would like it if you shared your work that way.
Testnet generator fixes these problems by (1) eliminating the consensus rule that makes you mine 101 blocks, (2) creating a lightweight blockchain directly in your browser so you don't have to download or configure anything, (3) looking nice enough (with lots of pretty colors) that you can feel good about sharing links to this stuff on social media. (Links to blocks and transactions are a work in progress though, so you can't do that part yet.)
# So you put a blockchain in a browser?
Yes. Every time you visit the website you spin up a new blockchain just for you. It shares a lot of bitcoin's consensus rules too, so you can use it for testing bitcoin apps.
Also...technically, no, this isn't a blockchain, it just looks and acts like one. In a "real" blockchain, later blocks commit to prior blocks, and each one contains proof that it costed something to produce. That way, the cost of doublespending a transaction grows as the transaction gets "buried" under more blocks, because you have to re-pay to create all of the blocks "on top" of it or no one will accept your doublespend. The "blockchain" in this software doesn't do any of that. It's expected to be used in an environment where the person mining the blocks is the only user, and creating blocks is supposed to be free. In that environment, the miner doesn't need to prove anything to anyone, so later blocks don't need to commit to prior blocks, which means this isn't technically a real blockchain. But it's close enough for what I want to do with it.
# There's no way this testnet is the same as bitcoin
It's not, it's missing lots of important consensus rules. For example, there's no blocksize limit, there are no halvings (so coins are theoretically unlimited), there's no proof of work, there's no difficulty adjustment, there's no 100-block "waiting period" applied to freshly mined coins, later blocks don't commit to prior blocks, blocks are formatted differently from how bitcoin formats them, transactions are not merkelized, there's no concept of "median time past" (so blocks can have any timestamp), and this blockchain only understands how to validate transactions that exclusively spend segwit v1 utxos (i.e. taproot outputs) -- segwit v0 and legacy utxos are not supported. They're too old-fashioned. There are also *consequences* to the above things, e.g. due to there not being a blocksize limit, fees are always low (1 sat per byte) -- you don't need to compete with people to get into a block when there's no blocksize limit.
# Sounds like you omitted everything that makes bitcoin bitcoin. So what in the world is this thing?
I didn't omit *everything.* This "blockchain" uses the same transaction format as bitcoin, the same elliptic curve, the same signature format, the same utxo model, and it understands bitcoin script, taproot, and bitcoin's sighash schemes. This means you can create transactions which validly spend utxos in this software with confidence that you can run the same code on bitcoin to spend "real" utxos, unless you specifically design a transaction to rely on one of the consensus rules that this software omits.
+527
View File
@@ -0,0 +1,527 @@
// 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`,
private_url: null,
public_url: null,
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 );
return Array.from( encoded )
.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_or_pubkey, relays, is_public_key = true ) => {
var hex_relays = chain_client.textToHex( JSON.stringify( relays ) );
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}`;
},
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" );
// 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}`;
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 ];
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 ] );
}
var connection_id = await super_nostr.newPermanentConnection( relays[ 0 ], listenFunction, handleFunction );
chain_client.main_connection = connection_id;
//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" );
// 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}`;
iframe.style.display = "none";
iframe.className = `chain_client_network_${privkey || pubkey}`;
document.body.append( iframe );
//return info when testnet is ready for commands
var loop_limit = 8;
var loop = async () => {
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;
}
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 );
chain_client.main_connection = connection_id;
//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 ] ] );
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 );
//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 ) => {
//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 setup_id = chain_client.getPrivkey().substring( 0, 16 );
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
if ( json.result.hasOwnProperty( "success" ) && json.result.success ) {
resolve( chain_client.getPrivkey() );
return;
}
resolve( Object.keys( json.result.txs )[ 0 ] );
return;
}
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 processing_line = [];
var gathered = [];
var spend_txs = [];
var txids_and_heights = {}
var utxos = [];
var last_txid = null;
var done_gathering_info = false;
var txs_of_interest = {}
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;
processing_line.push( txid );
}
} 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 inputs = tapscript.Tx.decode( txhex ).vin;
inputs.forEach( input => spend_txs.push( `${input.txid}_${input.vout}` ) );
txs_of_interest[ txid ] = { outputs: [ ...tapscript.Tx.decode( txhex ).vout ], block_hash, block_time };
gathered.push( txid );
if ( txid === last_txid ) {
done_gathering_info = true;
socket.close();
}
}
}
socket.addEventListener( 'message', handleFunction );
socket.send( JSON.stringify( formatted_command ) );
//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 );
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 === setup_id ) return;
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 );
console.log( 19, 'possible spend tx:', txid );
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" );
});
}
},
}
File diff suppressed because it is too large. Load diff