Import supertestnet/testnet_generator with full upstream history
git-subtree-dir: upstream/testnet_generator git-subtree-mainline:70419cffc2git-subtree-split:498be4cc77
This commit is contained in:
commit
60d02f9669
5 files changed
+4041
No files matched your search
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
Reference in new issue
Block a user