2023-03-14 10:27:09 -05:00
<!DOCTYPE html>
< html >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, user-scalable=no" />
< link rel = "shortcut icon" type = "image/png" href = "favicon.png" >
< title > Inscribe the planet</ title >
< style >
* {
box-sizing : border-box ;
font-size : 1.15 rem ;
2023-03-14 14:49:17 -05:00
font-family : monospace ;
2023-03-14 10:27:09 -05:00
background-color : black ;
color : white ;
2023-03-13 17:00:12 -05:00
}
2023-03-14 10:27:09 -05:00
html {
max-width : 70 ch ;
padding : 3 rem 1 rem ;
margin : auto ;
line-height : 1.25 ;
}
h1 {
font-size : 2 rem ;
}
h2 {
font-size : 1.5 rem ;
}
input {
line-height : 1.25 ;
width : 100 % ;
height : 1.8 rem ;
font-size : 1.15 rem ;
border : 1 px solid white ;
}
. black-bg {
display : none ;
width : 100 % ;
position : fixed ;
top : 0 ;
left : 0 ;
background-color : black ;
opacity : .5 ;
width : 100 vw ;
height : 100 vh ;
}
. modal {
display : none ;
position : fixed ;
box-sizing : border-box ;
top : 50 % ;
left : 50 % ;
transform : translate ( -50 % , -50 % );
width : 100 % ;
max-width : 560 px ;
background-color : white ;
border-radius : 10 px ;
padding : 20 px ;
color : black ;
text-align : center ;
}
. display {
display : none ;
border : 1 px solid white ;
padding : 1 rem ;
border-radius : 1 rem ;
word-wrap : break-word ;
text-align : center ;
position : relative ;
}
. display p {
text-align : left ;
}
. checking_mempool , . checking_mempool span {
color : lightgreen ;
}
. feerates {
display : flex ;
justify-content : space-around ;
}
. fee {
width : 100 % ;
max-width : 6 rem ;
2023-03-14 14:45:05 -05:00
margin : 0 px 0.5 rem ;
2023-03-14 10:27:09 -05:00
text-align : center ;
cursor : pointer ;
}
. fee . num {
background-color : grey ;
border : 1 px solid white ;
padding : .3 rem ;
}
. maxfee . num {
background-color : green ;
}
. safari_warning {
display : none ;
}
@ media screen and ( max-width : 600px ) {
}
</ style >
2023-03-14 17:02:04 -05:00
< script src = "https://unpkg.com/@cmdcode/bton@1.0.4" ></ script >
2023-03-14 10:27:09 -05:00
< script > const BTON = window . bton </ script >
2023-03-14 17:02:04 -05:00
< script src = "https://unpkg.com/@cmdcode/crypto-utils@1.6.0" ></ script >
2023-03-14 10:27:09 -05:00
< script src = "https://bundle.run/bech32@2.0.0" ></ script >
< script src = "https://supertestnet.github.io/bitcoin-chess/js/qrcode.js" ></ script >
< script >
var $ = document . querySelector . bind ( document );
var $$ = document . querySelectorAll . bind ( document );
var url_params = new URLSearchParams ( window . location . search );
var url_keys = url_params . keys ();
var $_GET = {}
for ( var key of url_keys ) $_GET [ key ] = url_params . get ( key );
</ script >
< script >
function encodeBase64 ( file ) {
return new Promise ( function ( resolve , reject ) {
//the next three lines handle the case where an image is passed in from an external site
if ( "files" in file ) {
file = file . files [ 0 ];
}
2023-03-15 10:56:51 -05:00
var mimetype = file . type ;
if ( mimetype . includes ( "text/plain" ) ) {
mimetype += ";charset=utf-8" ;
}
sessionStorage [ "mimetype" ] = mimetype ;
2023-03-14 10:27:09 -05:00
var imgReader = new FileReader ();
imgReader . onloadend = function () {
resolve ( imgReader . result . toString () );
}
imgReader . readAsDataURL ( file );
});
}
function base64ToHex ( str ) {
const raw = atob ( str );
let result = '' ;
for ( let i = 0 ; i < raw . length ; i ++ ) {
const hex = raw . charCodeAt ( i ). toString ( 16 );
result += ( hex . length === 2 ? hex : '0' + hex );
}
return result . toLowerCase ();
}
function hexToBytes ( hex ) {
return Uint8Array . from ( hex . match ( /.{1,2}/g ). map ( ( byte ) => parseInt ( byte , 16 ) ) );
}
function bytesToHex ( bytes ) {
return bytes . reduce ( ( str , byte ) => str + byte . toString ( 16 ). padStart ( 2 , "0" ), "" );
}
function createQR ( content ) {
var dataUriPngImage = document . createElement ( "img" ),
s = QRCode . generatePNG ( content , {
ecclevel : "M" ,
format : "html" ,
fillcolor : "#FFFFFF" ,
textcolor : "#000000" ,
margin : 4 ,
modulesize : 8 ,
});
dataUriPngImage . src = s ;
dataUriPngImage . id = "qr_code" ;
return dataUriPngImage ;
}
async function getBitcoinPriceFromCoinbase () {
var data = await getData ( "https://api.coinbase.com/v2/prices/BTC-USD/spot" );
var json = JSON . parse ( data );
var price = json [ "data" ][ "amount" ];
return price ;
}
async function getBitcoinPriceFromKraken () {
var data = await getData ( "https://api.kraken.com/0/public/Ticker?pair=XBTUSD" );
var json = JSON . parse ( data );
var price = json [ "result" ][ "XXBTZUSD" ][ "a" ][ 0 ];
return price ;
}
async function getBitcoinPriceFromCoindesk () {
var data = await getData ( "https://api.coindesk.com/v1/bpi/currentprice.json" );
var json = JSON . parse ( data );
var price = json [ "bpi" ][ "USD" ][ "rate_float" ];
return price ;
}
async function getBitcoinPriceFromGemini () {
var data = await getData ( "https://api.gemini.com/v2/ticker/BTCUSD" );
var json = JSON . parse ( data );
var price = json [ "bid" ];
return price ;
}
async function getBitcoinPriceFromBybit () {
var data = await getData ( "https://api-testnet.bybit.com/derivatives/v3/public/order-book/L2?category=linear&symbol=BTCUSDT" );
var json = JSON . parse ( data );
var price = json [ "result" ][ "b" ][ 0 ][ 0 ];
return price ;
}
async function getBitcoinPrice () {
var prices = [];
var cbprice = await getBitcoinPriceFromCoinbase ();
var kprice = await getBitcoinPriceFromKraken ();
var cdprice = await getBitcoinPriceFromCoindesk ();
var gprice = await getBitcoinPriceFromGemini ();
var bprice = await getBitcoinPriceFromBybit ();
prices . push ( Number ( cbprice ), Number ( kprice ), Number ( cdprice ), Number ( gprice ), Number ( bprice ) );
prices . sort ();
return prices [ 2 ];
}
</ script >
</ head >
< body >
< h1 > Welcome to the online inscriber</ h1 >
< h2 class = "safari_warning" > This tool may not work in safari browser. If you are using that, consider switching.</ h2 >
< p > First enter a < span class = "type_of_address" > taproot</ span > address from an ordinal wallet</ p >
< p >< input class = "address" ></ p >
< p > Then upload a file you want that address to own</ p >
< form >
< p >< input type = "file" class = "form" /></ p >
< p > Select a feerate (measured in sats per byte)</ p >
< div class = "feerates" >< div class = "fee minfee" >< div class = "num" > ...</ div >< div class = "name" > Min</ div ></ div >< div class = "fee midfee" >< div class = "num" > ...</ div >< div class = "name" > Mid</ div ></ div >< div class = "fee maxfee" >< div class = "num" > ...</ div >< div class = "name" > Max</ div ></ div ></ div >
< div id = "sliderange" >
< p > Sats per byte: < span id = "sats_per_byte" > 1</ span ></ p >
< input id = "sats_range" type = "range" min = "1" max = "100" value = "1" >
</ div >
< script >
var slider = document . getElementById ( "sats_range" );
var output = document . getElementById ( "sats_per_byte" );
output . innerHTML = slider . value ;
slider . oninput = function () {
output . innerHTML = this . value ;
sessionStorage [ "feerate" ] = this . value ;
$$ ( '.fee .num' ). forEach ( function ( item ) {
item . style . backgroundColor = "grey" ;
});
}
</ script >
</ form >
< p >< button class = "submit" > Submit</ button ></ p >
< div class = "display" ></ div >
< div style = "display: none;" class = "file" ></ div >
< script >
sessionStorage . clear ();
const privkey = bytesToHex ( cryptoUtils . Noble . utils . randomPrivateKey () );
$ ( '.form' ). addEventListener ( "change" , async function () {
if ( this . files [ 0 ]. size >= 120000 && this . files [ 0 ]. size < 350000 ) {
var conf = confirm ( "You exceeded the 120 kb size limit. Click ok to override this warning or click cancel to try again." );
if ( conf ) {
var b64 = await encodeBase64 ( this );
} else {
this . value = null ;
}
} else if ( this . files [ 0 ]. size >= 350000 ) {
alert ( "You exceeded this software's hard size limit of 350 kb files. Modify the code to override the limit." )
this . value = null ;
} else {
var b64 = await encodeBase64 ( this );
}
var base64 = b64 . substring ( b64 . indexOf ( "base64," ) + 7 );
var hex = base64ToHex ( base64 );
// console.log( "hex:", hex );
// console.log( "bytes:", hexToBytes( hex ) );
$ ( '.file' ). innerText = hex ;
});
$ ( '.submit' ). addEventListener ( "click" , async function () {
const ec = new TextEncoder ();
const hex = $ ( '.file' ). innerText ;
const data = hexToBytes ( hex );
const KeyPair = cryptoUtils . KeyPair ;
const seckey = new KeyPair ( privkey );
const pubkey = seckey . pub . rawX ;
const mimetype = ec . encode ( sessionStorage [ "mimetype" ] );
const script = [ pubkey , 'OP_CHECKSIG' , 'OP_0' , 'OP_IF' , ec . encode ( 'ord' ), '01' , mimetype , 'OP_0' , data , 'OP_ENDIF' ];
const leaf = await BTON . Tap . getLeaf ( BTON . Script . encode ( script ));
const [ tapkey ] = await BTON . Tap . getPubkey ( pubkey , [ leaf ]);
const cblock = await BTON . Tap . getPath ( pubkey , leaf );
console . log ( 'Tapkey:' , tapkey );
var fundingAddress = BTON . Tap . encodeAddress ( tapkey , 'bc' );
console . log ( 'Funding address: ' , fundingAddress );
//var toAddress = prompt( "give me the address you your inscription to arrive in and consider using this one: tb1padwymyc6x2dfc3cwzvpxxcqsr3c46cjx9zxg0jmcuf44fjuwljxs7x68fm" );
//var toAddress = "tb1padwymyc6x2dfc3cwzvpxxcqsr3c46cjx9zxg0jmcuf44fjuwljxs7x68fm";
var toAddress = $ ( '.address' ). value ;
console . log ( 'Address that will receive the inscription:' , toAddress );
var decodedToAddress = "5120" + BTON . Tap . decodeAddress ( toAddress ). hex ;
console . log ( 'To address decoded:' , decodedToAddress );
var txsize = 200 + ( hex . length / 2 );
if ( sessionStorage [ "feerate" ] ) {
var feerate = Number ( sessionStorage [ "feerate" ] );
} else {
var feerate = await getMaxFeeRate ();
}
var fee = feerate * txsize ;
var btc_price = sessionStorage [ "bitcoin_price" ];
var sats_price = await satsToDollars ( fee + 1000 );
sats_price = Math . floor ( sats_price * 100 ) / 100 ;
var html = `<p>Please send at least ${ fee + 1000 } sats ($ ${ sats_price } ) to this address:</p><p> ${ fundingAddress } </p>` ;
$ ( '.display' ). innerHTML = html ;
var qr_value = "bitcoin:" + fundingAddress + "?amount=" + satsToBitcoin ( fee + 1000 );
console . log ( "qr:" , qr_value );
$ ( '.display' ). append ( createQR ( qr_value ) );
$ ( '.display' ). innerHTML += `<p class="checking_mempool">Checking the mempool<span class="dots">.</span></p>` ;
$ ( '.display' ). innerHTML += `<p>1000 sats will go to the address</p><p> ${ fee } sats will go to miners as a mining fee</p>` ;
$ ( '.display' ). style . display = "block" ;
await loopTilAddressReceivesMoney ( fundingAddress );
await waitSomeSeconds ( 2 );
var txinfo = await addressReceivedMoneyInThisTx ( fundingAddress );
var txid = txinfo [ 0 ];
var vout = txinfo [ 1 ];
var amt = txinfo [ 2 ];
// if ( !txid || !vout || !amt ) {
// await waitSomeSeconds( 2 );
// txinfo = await addressReceivedMoneyInThisTx( fundingAddress );
// txid = txinfo[ 0 ];
// vout = txinfo[ 1 ];
// amt = txinfo[ 2 ];
// }
// var amt = prompt( "give me the amount you sent" );
// amt = Number( amt );
// var txid = prompt( "give me the txid" );
// var vout = prompt( "give me the vout" );
// vout = Number( vout );
console . log ( "yay! txid:" , txid , "vout:" , vout , "amount:" , amt );
if ( sessionStorage [ "feerate" ] ) {
feerate = Number ( sessionStorage [ "feerate" ] );
} else {
feerate = await getMaxFeeRate ();
}
var fee = feerate * txsize ;
console . log ( "feerate:" , feerate , "txsize:" , txsize , "fee:" , fee );
console . log ( "fee:" , fee );
const redeemtx = {
version : 2 ,
input : [{
txid : txid ,
vout : vout ,
prevout : { value : amt , scriptPubKey : '5120' + tapkey },
witness : []
}],
output : [{
value : amt - fee ,
scriptPubKey : decodedToAddress
}],
locktime : 0
}
const sec = await BTON . Tap . getSeckey ( seckey . raw , [ leaf ]);
2023-03-14 17:02:04 -05:00
const sig = await BTON . Sig . taproot . sign ( seckey . raw , redeemtx , 0 , { extension : leaf });
2023-03-14 10:27:09 -05:00
redeemtx . input [ 0 ]. witness = [ sig , script , cblock ];
console . dir ( redeemtx , { depth : null });
var rawtx = BTON . Tx . encode ( redeemtx );
console . log ( 'Txdata:' , rawtx );
var txid = await pushBTCpmt ( rawtx );
var html = `<p style="background-color: white; color: black;">Success! Your image will shortly be inscribed on the bitcoin blockchain in this transaction:</p><p style="word-wrap: break-word;"><a href="https://mempool.space/tx/ ${ txid } " target="_blank">https://mempool.space/tx/ ${ txid } </a></p>` ;
html += `<p style="background-color: white; color: black;">When your transaction confirms you can view it on the ordinals explorer here:</p><p style="word-wrap: break-word;"><a href="https://ordinals.com/inscription/ ${ txid } i0" target="_blank">https://ordinals.com/inscription/ ${ txid } i0</a></p>` ;
$ ( '.modal' ). innerHTML = html ;
$ ( '.modal' ). style . display = "block" ;
$ ( '.black-bg' ). style . display = "block" ;
});
async function getMaxFeeRate () {
var fees = await getData ( "https://mempool.space/api/v1/fees/recommended" );
fees = JSON . parse ( fees );
// if ( !( "minimumFee" in fees ) ) return "error -- site down";
// var minfee = fees[ "minimumFee" ];
if ( ! ( "fastestFee" in fees ) ) return "error -- site down" ;
var minfee = fees [ "fastestFee" ];
return minfee ;
}
function isValidTaprootAddress ( address ) {
try {
BTON . Tap . decodeAddress ( address ). hex ;
return true ;
} catch ( e ) {}
return ;
}
async function getAllFeeRates () {
var fees = await getData ( "https://mempool.space/api/v1/fees/recommended" );
fees = JSON . parse ( fees );
return fees ;
}
function getData ( url ) {
return new Promise ( async function ( resolve , reject ) {
function inner_get ( url ) {
var xhttp = new XMLHttpRequest ();
xhttp . open ( "GET" , url , true );
xhttp . send ();
return xhttp ;
}
var data = inner_get ( url );
data . onerror = function ( e ) {
resolve ( "error" );
}
async function isResponseReady () {
return new Promise ( function ( resolve2 , reject ) {
if ( ! data . responseText || data . readyState != 4 ) {
setTimeout ( async function () {
var msg = await isResponseReady ();
resolve2 ( msg );
}, 1 );
} else {
resolve2 ( data . responseText );
}
});
}
var returnable = await isResponseReady ();
resolve ( returnable );
});
}
async function pushBTCpmt ( rawtx ) {
var txid = await postData ( "https://mempool.space/api/tx" , rawtx );
return txid ;
}
function waitSomeSeconds ( num ) {
var num = num . toString () + "000" ;
num = Number ( num );
return new Promise ( function ( resolve , reject ) {
setTimeout ( function () { resolve ( "" ); }, num );
});
}
async function postData ( url , json , content_type = "" , apikey = "" ) {
var rtext = "" ;
function inner_post ( url , json , content_type = "" , apikey = "" ) {
var xhttp = new XMLHttpRequest ();
xhttp . open ( "POST" , url , true );
if ( content_type ) {
xhttp . setRequestHeader ( `Content-Type` , content_type );
}
if ( apikey ) {
xhttp . setRequestHeader ( `X-Api-Key` , apikey );
}
xhttp . send ( json );
return xhttp ;
}
var data = inner_post ( url , json , content_type , apikey );
data . onerror = function ( e ) {
rtext = "error" ;
}
async function isResponseReady () {
return new Promise ( function ( resolve , reject ) {
if ( rtext == "error" ) {
resolve ( rtext );
}
if ( ! data . responseText || data . readyState != 4 ) {
setTimeout ( async function () {
var msg = await isResponseReady ();
resolve ( msg );
}, 50 );
} else {
resolve ( data . responseText );
}
});
}
var returnable = await isResponseReady ();
return returnable ;
}
async function loopTilAddressReceivesMoney ( address ) {
var itReceivedMoney = false ;
async function isDataSetYet ( data_i_seek ) {
return new Promise ( function ( resolve , reject ) {
if ( ! data_i_seek ) {
setTimeout ( async function () {
console . log ( "waiting for address to receive money..." );
itReceivedMoney = await addressOnceHadMoney ( address );
var msg = await isDataSetYet ( itReceivedMoney );
resolve ( msg );
}, 2000 );
} else {
resolve ( data_i_seek );
}
});
}
async function getTimeoutData () {
var data_i_seek = await isDataSetYet ( itReceivedMoney );
return data_i_seek ;
}
var returnable = await getTimeoutData ();
return returnable ;
}
async function addressReceivedMoneyInThisTx ( address ) {
var txid ;
var vout ;
var amt ;
var nonjson = await getData ( "https://mempool.space/api/address/" + address + "/txs" );
var json = JSON . parse ( nonjson );
json . forEach ( function ( tx ) {
tx [ "vout" ]. forEach ( function ( output , index ) {
if ( output [ "scriptpubkey_address" ] == address ) {
txid = tx [ "txid" ];
vout = index ;
amt = output [ "value" ];
}
});
});
return [ txid , vout , amt ];
}
async function addressOnceHadMoney ( address ) {
var url = "https://mempool.space/api/address/" + address ;
var nonjson = await getData ( url );
var json = JSON . parse ( nonjson );
if ( json [ "chain_stats" ][ "tx_count" ] > 0 || json [ "mempool_stats" ][ "tx_count" ] > 0 ) {
return true ;
}
return false ;
}
function dotLoop ( string ) {
if ( ! $ ( '.dots' ) ) {
setTimeout ( function () { dotLoop ( string );}, 1000 );
return ;
}
if ( string . length < 3 ) {
string = string + "." ;
} else {
string = "." ;
}
$ ( '.dots' ). innerText = string ;
setTimeout ( function () { dotLoop ( string );}, 1000 );
}
dotLoop ( "." );
function satsToBitcoin ( sats ) {
if ( sats >= 100000000 ) sats = sats * 10 ;
var string = String ( sats ). padStart ( 8 , "0" ). slice ( 0 , - 9 ) + "." + String ( sats ). padStart ( 8 , "0" ). slice ( - 9 );
if ( string . substring ( 0 , 1 ) == "." ) string = "0" + string ;
return string ;
}
async function satsToDollars ( sats ) {
if ( sats >= 100000000 ) sats = sats * 10 ;
var bitcoin_price = sessionStorage [ "bitcoin_price" ];
var value_in_dollars = Number ( String ( sats ). padStart ( 8 , "0" ). slice ( 0 , - 9 ) + "." + String ( sats ). padStart ( 8 , "0" ). slice ( - 9 ) ) * bitcoin_price ;
return value_in_dollars ;
}
function modalVanish () {
$ ( ".black-bg" ). style . display = "none" ;
$ ( ".modal" ). style . display = "none" ;
}
$$ ( '.fee' ). forEach ( function ( item ) {
item . onclick = function () {
$$ ( '.fee .num' ). forEach ( function ( item2 ) {
item2 . style . backgroundColor = "grey" ;
});
this . getElementsByClassName ( "num" )[ 0 ]. style . backgroundColor = "green" ;
sessionStorage [ "feerate" ] = this . getElementsByClassName ( "num" )[ 0 ]. innerText ;
$ ( '#sats_per_byte' ). innerText = Number ( this . getElementsByClassName ( "num" )[ 0 ]. innerText );
$ ( '#sats_range' ). value = Number ( this . getElementsByClassName ( "num" )[ 0 ]. innerText );
}
});
function checkAddress () {
if ( ! isValidTaprootAddress ( $ ( '.address' ). value ) && $ ( '.address' ). value ) {
$ ( '.address' ). style . backgroundColor = "#ff5252" ;
$ ( '.address' ). style . border = "2px solid red" ;
$ ( '.type_of_address' ). style . border = "1px solid white" ;
} else {
$ ( '.address' ). style . backgroundColor = "initial" ;
$ ( '.address' ). style . border = "1px solid white" ;
$ ( '.type_of_address' ). style . borderStyle = "none" ;
}
}
$ ( '.address' ). onchange = checkAddress ;
$ ( '.address' ). onpaste = checkAddress ;
$ ( '.address' ). onkeyup = checkAddress ;
async function init ( num ) {
if ( ! num ) {
var isSafari = /^((?!chrome|android).)*safari/i . test ( navigator . userAgent );
if ( isSafari ) $ ( '.safari_warning' ). style . display = "block" ;
var maxfee = await getMaxFeeRate ();
$ ( '#sats_per_byte' ). innerText = maxfee ;
$ ( '#sats_range' ). value = maxfee ;
}
num = num + 1 ;
var allrates = await getAllFeeRates ();
$ ( '.minfee .num' ). innerText = allrates [ "minimumFee" ];
$ ( '.midfee .num' ). innerText = allrates [ "hourFee" ];
$ ( '.maxfee .num' ). innerText = allrates [ "fastestFee" ];
2023-03-15 13:16:05 -05:00
var isgreen ;
$$ ( '.fee .num' ). forEach ( function ( item ) {
if ( item . style . backgroundColor == "green" ) isgreen = item ;
});
if ( isgreen ) {
$ ( '#sats_per_byte' ). innerText = Number ( isgreen . innerText );
$ ( '#sats_range' ). value = Number ( isgreen . innerText );
sessionStorage [ "feerate" ] = isgreen . innerText ;
}
2023-03-14 10:27:09 -05:00
sessionStorage [ "bitcoin_price" ] = await getBitcoinPrice ();
await waitSomeSeconds ( 10 );
init ( num );
}
init ( 0 );
</ script >
< div class = "black-bg" onclick = "modalVanish();" ></ div >
< div class = "modal" ></ div >
</ body >
</ html >