1116 lines
61 KiB
HTML
1116 lines
61 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
|
<script src="https://supertestnet.github.io/bitcoin-chess/js/bolt11.js"></script>
|
|
<script src="https://supertestnet.github.io/blind-sig-js/blindSigJS.js"></script>
|
|
<script src="https://bundle.run/browserify-cipher@1.0.1"></script>
|
|
<script src="https://bundle.run/noble-secp256k1@1.2.14"></script>
|
|
<script>
|
|
// dependencies:
|
|
// https://bundle.run/noble-secp256k1@1.2.14
|
|
// https://bundle.run/browserify-cipher@1.0.1
|
|
var super_nostr = {
|
|
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" ), "" ),
|
|
base64ToHex: str => {
|
|
var raw = atob( str );
|
|
var result = '';
|
|
var i; for ( i=0; i<raw.length; i++ ) {
|
|
var hex = raw.charCodeAt( i ).toString( 16 );
|
|
result += hex.length % 2 ? '0' + hex : hex;
|
|
}
|
|
return result.toLowerCase();
|
|
},
|
|
getPrivkey: () => super_nostr.bytesToHex( nobleSecp256k1.utils.randomPrivateKey() ),
|
|
getPubkey: privkey => nobleSecp256k1.getPublicKey( privkey, true ).substring( 2 ),
|
|
sha256: async text_or_bytes => {if ( typeof text_or_bytes === "string" ) text_or_bytes = ( new TextEncoder().encode( text_or_bytes ) );return super_nostr.bytesToHex( await nobleSecp256k1.utils.sha256( text_or_bytes ) )},
|
|
waitSomeSeconds: num => {
|
|
var num = num.toString() + "000";
|
|
num = Number( num );
|
|
return new Promise( resolve => setTimeout( resolve, num ) );
|
|
},
|
|
getEvents: async ( relay_or_socket, ids, authors, kinds, until, since, limit, etags, ptags ) => {
|
|
var socket_is_permanent = false;
|
|
if ( typeof socket !== "string" ) socket_is_permanent = true;
|
|
if ( typeof socket === "string" ) var socket = new WebSocket( relay_or_socket );
|
|
else var socket = relay_or_socket;
|
|
var events = [];
|
|
var opened = false;
|
|
if ( socket_is_permanent ) {
|
|
var subId = super_nostr.bytesToHex( nobleSecp256k1.utils.randomPrivateKey() ).substring( 0, 16 );
|
|
var filter = {}
|
|
if ( ids ) filter.ids = ids;
|
|
if ( authors ) filter.authors = authors;
|
|
if ( kinds ) filter.kinds = kinds;
|
|
if ( until ) filter.until = until;
|
|
if ( since ) filter.since = since;
|
|
if ( limit ) filter.limit = limit;
|
|
if ( etags ) filter[ "#e" ] = etags;
|
|
if ( ptags ) filter[ "#p" ] = ptags;
|
|
var subscription = [ "REQ", subId, filter ];
|
|
socket.send( JSON.stringify( subscription ) );
|
|
return;
|
|
}
|
|
socket.addEventListener( 'message', async function( message ) {
|
|
var [ type, subId, event ] = JSON.parse( message.data );
|
|
var { kind, content } = event || {}
|
|
if ( !event || event === true ) return;
|
|
events.push( event );
|
|
});
|
|
socket.addEventListener( 'open', async function( e ) {
|
|
opened = true;
|
|
var subId = super_nostr.bytesToHex( nobleSecp256k1.utils.randomPrivateKey() ).substring( 0, 16 );
|
|
var filter = {}
|
|
if ( ids ) filter.ids = ids;
|
|
if ( authors ) filter.authors = authors;
|
|
if ( kinds ) filter.kinds = kinds;
|
|
if ( until ) filter.until = until;
|
|
if ( since ) filter.since = since;
|
|
if ( limit ) filter.limit = limit;
|
|
if ( etags ) filter[ "#e" ] = etags;
|
|
if ( ptags ) filter[ "#p" ] = ptags;
|
|
var subscription = [ "REQ", subId, filter ];
|
|
socket.send( JSON.stringify( subscription ) );
|
|
});
|
|
var loop = async () => {
|
|
if ( !opened ) {
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
return await loop();
|
|
}
|
|
var len = events.length;
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
if ( len !== events.length ) return await loop();
|
|
socket.close();
|
|
return events;
|
|
}
|
|
return await loop();
|
|
},
|
|
prepEvent: async ( privkey, msg, kind, tags ) => {
|
|
pubkey = super_nostr.getPubkey( privkey );
|
|
if ( !tags ) tags = [];
|
|
var event = {
|
|
"content": msg,
|
|
"created_at": Math.floor( Date.now() / 1000 ),
|
|
"kind": kind,
|
|
"tags": tags,
|
|
"pubkey": pubkey,
|
|
}
|
|
var signedEvent = await super_nostr.getSignedEvent( event, privkey );
|
|
return signedEvent;
|
|
},
|
|
sendEvent: ( event, relay_or_socket ) => {
|
|
var socket_is_permanent = false;
|
|
if ( typeof socket !== "string" ) socket_is_permanent = true;
|
|
if ( typeof socket === "string" ) var socket = new WebSocket( relay_or_socket );
|
|
else var socket = relay_or_socket;
|
|
if ( !socket_is_permanent ) {
|
|
socket.addEventListener( 'open', async () => {
|
|
socket.send( JSON.stringify( [ "EVENT", event ] ) );
|
|
setTimeout( () => {socket.close();}, 1000 );
|
|
});
|
|
} else {
|
|
socket.send( JSON.stringify( [ "EVENT", event ] ) );
|
|
}
|
|
return event.id;
|
|
},
|
|
getSignedEvent: async ( event, privkey ) => {
|
|
var eventData = JSON.stringify([
|
|
0,
|
|
event['pubkey'],
|
|
event['created_at'],
|
|
event['kind'],
|
|
event['tags'],
|
|
event['content'],
|
|
]);
|
|
event.id = await super_nostr.sha256( eventData );
|
|
event.sig = await nobleSecp256k1.schnorr.sign( event.id, privkey );
|
|
return event;
|
|
},
|
|
encrypt: ( privkey, pubkey, text ) => {
|
|
var key = nobleSecp256k1.getSharedSecret( privkey, '02' + pubkey, true ).substring( 2 );
|
|
var iv = window.crypto.getRandomValues( new Uint8Array( 16 ) );
|
|
var cipher = browserifyCipher.createCipheriv( 'aes-256-cbc', super_nostr.hexToBytes( key ), iv );
|
|
var encryptedMessage = cipher.update(text,"utf8","base64");
|
|
emsg = encryptedMessage + cipher.final( "base64" );
|
|
var uint8View = new Uint8Array( iv.buffer );
|
|
var decoder = new TextDecoder();
|
|
return emsg + "?iv=" + btoa( String.fromCharCode.apply( null, uint8View ) );
|
|
},
|
|
decrypt: ( privkey, pubkey, ciphertext ) => {
|
|
var [ emsg, iv ] = ciphertext.split( "?iv=" );
|
|
var key = nobleSecp256k1.getSharedSecret( privkey, '02' + pubkey, true ).substring( 2 );
|
|
var decipher = browserifyCipher.createDecipheriv(
|
|
'aes-256-cbc',
|
|
super_nostr.hexToBytes( key ),
|
|
super_nostr.hexToBytes( super_nostr.base64ToHex( iv ) )
|
|
);
|
|
var decryptedMessage = decipher.update( emsg, "base64" );
|
|
dmsg = decryptedMessage + decipher.final( "utf8" );
|
|
return dmsg;
|
|
},
|
|
}
|
|
</script>
|
|
<script>
|
|
var isValidHex = hex => {
|
|
if ( !hex ) return;
|
|
var length = hex.length;
|
|
if ( length % 2 ) return;
|
|
try {
|
|
var bigint = BigInt( "0x" + hex, "hex" );
|
|
} catch( e ) {
|
|
return;
|
|
}
|
|
var prepad = bigint.toString( 16 );
|
|
var i; for ( i=0; i<length; i++ ) prepad = "0" + prepad;
|
|
var padding = prepad.slice( -Math.abs( length ) );
|
|
return ( padding === hex );
|
|
}
|
|
|
|
var hexToBytes = hex => Uint8Array.from( hex.match( /.{1,2}/g ).map( byte => parseInt( byte, 16 ) ) );
|
|
|
|
var bytesToHex = bytes => bytes.reduce( ( str, byte ) => str + byte.toString( 16 ).padStart( 2, "0" ), "" );
|
|
|
|
var hexToText = hex => {
|
|
var bytes = new Uint8Array(Math.ceil(hex.length / 2));
|
|
for (var i = 0; i < hex.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
var text = new TextDecoder().decode( bytes );
|
|
return text;
|
|
}
|
|
|
|
var waitASec=num=>new Promise(res=>setTimeout(res,num*1000));
|
|
|
|
var decomposeAmount = amount_to_decompose => {
|
|
var decomposed = [];
|
|
var getBaseLog = ( x, y ) => Math.log(y) / Math.log(x);
|
|
var inner_fn = amt => {
|
|
var exponent = Math.floor( getBaseLog( 2, amt ) );
|
|
decomposed.push( 2 ** exponent );
|
|
amount_to_decompose = amt - 2 ** exponent;
|
|
if ( amount_to_decompose ) inner_fn(amount_to_decompose);
|
|
}
|
|
inner_fn( amount_to_decompose );
|
|
return decomposed;
|
|
}
|
|
|
|
var getUtxosAndSecrets = async ( amounts_to_get, keyset, make_blank, full_utxos ) => {
|
|
var num_of_iterations = amounts_to_get.length;
|
|
if ( make_blank ) num_of_iterations = amounts_to_get;
|
|
var outputs = [];
|
|
var secrets = [];
|
|
var i; for ( i=0; i<num_of_iterations; i++ ) {
|
|
if ( !make_blank && !full_utxos ) {
|
|
var item = amounts_to_get[ i ];
|
|
var amount = item;
|
|
} else if ( !make_blank && full_utxos ) {
|
|
var item = amounts_to_get[ i ];
|
|
var amount = item[ "amount" ];
|
|
var keyset = item[ "id" ];
|
|
} else {
|
|
var amount = 1;
|
|
}
|
|
var secret_for_msg = bytesToHex(blindSigJS.getRand(32));
|
|
var message = new blindSigJS.bsjMsg();
|
|
var B_ = await message.createBlindedMessageFromString( secret_for_msg );
|
|
var B_hex = blindSigJS.ecPointToHex( B_ );
|
|
outputs.push({
|
|
amount,
|
|
id: keyset,
|
|
"B_": B_hex,
|
|
});
|
|
secrets.push( [ secret_for_msg, message ] );
|
|
}
|
|
return [ outputs, secrets ];
|
|
}
|
|
|
|
var processSigs = ( sigs, secrets, pubkeys ) => {
|
|
var utxos_to_return = [];
|
|
var i; for ( i=0; i<sigs.length; i++ ) {
|
|
var sig_data = sigs[ i ];
|
|
var id = sig_data[ "id" ];
|
|
var amount = sig_data[ "amount" ];
|
|
var secret = secrets[ i ][ 0 ];
|
|
var blinded_sig = sig_data[ "C_" ];
|
|
var message = secrets[ i ][ 1 ];
|
|
var C_ = nobleSecp256k1.Point.fromCompressedHex( hexToBytes( blinded_sig ) );
|
|
var amt_pubkey = pubkeys[ amount ];
|
|
amt_pubkey = nobleSecp256k1.Point.fromCompressedHex( hexToBytes( amt_pubkey ) );
|
|
var {C} = message.unblindSignature(C_, amt_pubkey);
|
|
var compressed_C = nobleSecp256k1.Point.fromHex( blindSigJS.ecPointToHex( C ) ).toHex( true );
|
|
var utxo = {
|
|
id,
|
|
amount,
|
|
secret,
|
|
C: compressed_C,
|
|
}
|
|
utxos_to_return.push( utxo );
|
|
}
|
|
return utxos_to_return;
|
|
}
|
|
</script>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
font-size: 1.15rem;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
html {
|
|
max-width: 800px;
|
|
padding: 3rem 1rem;
|
|
margin: auto;
|
|
line-height: 1.25;
|
|
padding: 0;
|
|
}
|
|
body {
|
|
margin: 3rem 1rem;
|
|
}
|
|
h1 {
|
|
font-size: 2rem;
|
|
}
|
|
h2 {
|
|
font-size: 1.5rem;
|
|
}
|
|
input {
|
|
line-height: 1.25;
|
|
width: 100%;
|
|
height: 1.8rem;
|
|
font-size: 1.15rem;
|
|
border: 1px solid grey;
|
|
}
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
.invoice_box {
|
|
border: 1px solid black;
|
|
color: black;
|
|
font-family: monospace;
|
|
padding: 0.5rem;
|
|
max-width: 15rem;
|
|
word-wrap: break-word;
|
|
}
|
|
@media screen and (max-width: 600px) {
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<center><h2 class="balance">0</h2></center></h1>
|
|
<center class="send_and_receive_btns hidden">
|
|
<button class="send">Send</button>
|
|
<button class="receive">Receive</button>
|
|
</center>
|
|
<center>
|
|
<p class="invoice_box hidden"></p>
|
|
<p class="checking_connection">Checking for nwc connection...</p>
|
|
<div class="nwc_btns hidden">
|
|
<br>
|
|
<button class="create_nwc_connection">Create NWC connection</button>
|
|
<!-- <br><br> -->
|
|
<!-- <button class="connect_to_mint">Connect to mint</button> -->
|
|
<br>
|
|
</div>
|
|
<div class="nwc_string_div"></div>
|
|
</center>
|
|
<script>
|
|
var utxos = [];
|
|
var invoice_requests = {}
|
|
var mymint = null;
|
|
// var mymint = `https://testnut.cashu.space`;
|
|
// var mymint = "https://mint.minibits.cash/Bitcoin";
|
|
var $ = document.querySelector.bind( document );
|
|
var $$ = document.querySelectorAll.bind( document );
|
|
var getBalance = () => {
|
|
var bal = 0;
|
|
utxos.forEach( item => bal = bal + item[ "amount" ] );
|
|
return bal;
|
|
}
|
|
async function getBlockheight() {
|
|
var data = await fetch( `https://mempool.local/api/blocks/tip/height` );
|
|
return Number( await data.text() );
|
|
}
|
|
async function getBlockhash( blocknum ) {
|
|
var data = await fetch( `https://mempool.local/api/block-height/${blocknum}` );
|
|
return data.text();
|
|
}
|
|
var getInvoicePmthash = invoice => {
|
|
var decoded = bolt11.decode( invoice );
|
|
var i; for ( i=0; i<decoded[ "tags" ].length; i++ ) {
|
|
if ( decoded[ "tags" ][ i ][ "tagName" ] == "payment_hash" ) var pmthash = decoded[ "tags" ][ i ][ "data" ].toString();
|
|
}
|
|
return pmthash;
|
|
}
|
|
var getInvoiceDescription = invoice => {
|
|
var description = "";
|
|
var decoded = bolt11.decode( invoice );
|
|
var i; for ( i=0; i<decoded[ "tags" ].length; i++ ) {
|
|
if ( decoded[ "tags" ][ i ][ "tagName" ] == "description" ) description = decoded[ "tags" ][ i ][ "data" ].toString();
|
|
}
|
|
return description;
|
|
}
|
|
var getInvoiceDeschash = invoice => {
|
|
var deschash = "";
|
|
var decoded = bolt11.decode( invoice );
|
|
var i; for ( i=0; i<decoded[ "tags" ].length; i++ ) {
|
|
if ( decoded[ "tags" ][ i ][ "tagName" ] == "purpose_commit_hash" ) var deschash = decoded[ "tags" ][ i ][ "data" ].toString();
|
|
}
|
|
return deschash;
|
|
}
|
|
var checkInvoiceTilPaidOrError = async ( invoice_data, app_pubkey ) => {
|
|
var is_paid = await checkLNInvoice( invoice_data, app_pubkey );
|
|
if ( is_paid ) return;
|
|
var pmthash = getInvoicePmthash( invoice_data[ "request" ] );
|
|
var expiry = nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ][ "expires_at" ];
|
|
var now = Math.floor( Date.now() / 1000 );
|
|
if ( now >= expiry ) return;
|
|
await super_nostr.waitSomeSeconds( 20 );
|
|
checkInvoiceTilPaidOrError( invoice_data, app_pubkey );
|
|
}
|
|
var getLNInvoice = async full_amount => {
|
|
var amounts_to_get = decomposeAmount( full_amount );
|
|
amounts_to_get.sort();
|
|
var post_data = {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({"amount": full_amount, "unit": "sat"}),
|
|
}
|
|
var invoice_data = await fetch( `${mymint}/v1/mint/quote/bolt11`, post_data );
|
|
invoice_data = await invoice_data.json();
|
|
return invoice_data;
|
|
}
|
|
var checkLNInvoice = async ( invoice_data, app_pubkey ) => {
|
|
if ( typeof invoice_data !== "object" ) {
|
|
//I normally pass in an invoice_data object which I got
|
|
//from the mint. But when this is an invoice *I* am
|
|
//paying, the mint doesn't have any info about this
|
|
//invoice, so instead, I do this: I pass an actual
|
|
//"invoice" to this function -- which detect that it is
|
|
//not an object, and thus it is not the kind of thing
|
|
//the mint knows about -- and I simply check if my
|
|
//tx_history has a settled_at value. If so, it is
|
|
//settled and I don't need to ask the mint.
|
|
var pmthash = getInvoicePmthash( invoice_data );
|
|
return !!nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ].settled_at;
|
|
}
|
|
var pmthash = getInvoicePmthash( invoice_data[ "request" ] );
|
|
var url = `${mymint}/v1/mint/quote/bolt11/${invoice_data[ "quote" ]}`;
|
|
var settled_status = nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ][ "settled_at" ];
|
|
var is_paid_info = await fetch( url );
|
|
is_paid_info = await is_paid_info.json();
|
|
var is_paid = is_paid_info[ "paid" ];
|
|
if ( is_paid ) nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ][ "paid" ] = true;
|
|
var status_changed = is_paid && !settled_status;
|
|
if ( status_changed ) nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ][ "settled_at" ] = Math.floor( Date.now() / 1000 );
|
|
if ( status_changed ) nostr_state.nwc_info[ app_pubkey ].balance = nostr_state.nwc_info[ app_pubkey ].balance + nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ][ "amount" ];
|
|
if ( status_changed ) getSigsAfterLNInvoiceIsPaid( invoice_data );
|
|
if ( is_paid ) $( '.invoice_box' ).classList.add( "hidden" );
|
|
return is_paid;
|
|
}
|
|
var getSigsAfterLNInvoiceIsPaid = async invoice_data => {
|
|
var pre_amount = bolt11.decode( invoice_data[ "request" ] ).satoshis;
|
|
var amounts_to_get = decomposeAmount( pre_amount );
|
|
var keysets = await fetch( `${mymint}/v1/keysets` );
|
|
keysets = await keysets.json();
|
|
keysets = keysets[ "keysets" ];
|
|
var keyset = null;
|
|
keysets.every( item => {if ( isValidHex( item.id ) && item.active ) {keyset = item.id;return;} return true;});
|
|
var pubkeys = await fetch(`${mymint}/v1/keys/${keyset}`);
|
|
pubkeys = await pubkeys.json();
|
|
pubkeys = pubkeys[ "keysets" ][ 0 ][ "keys" ];
|
|
var [ outputs, secrets ] = await getUtxosAndSecrets( amounts_to_get, keyset );
|
|
var sig_request = {"quote": invoice_data[ "quote" ], outputs}
|
|
var post_data = {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify(sig_request),
|
|
}
|
|
var blinded_sigs = await fetch( `${mymint}/v1/mint/bolt11`, post_data );
|
|
blinded_sigs = await blinded_sigs.json();
|
|
var new_utxos = processSigs( blinded_sigs[ "signatures" ], secrets, pubkeys );
|
|
utxos.push( ...new_utxos );
|
|
$( '.balance' ).innerText = getBalance();
|
|
}
|
|
var depositToMint = async full_amount => {
|
|
// Pick one of the mint's keysets
|
|
if ( !mymint ) return alert( `you need to connect to a mint first` );
|
|
var keysets = await fetch( `${mymint}/v1/keysets` );
|
|
keysets = await keysets.json();
|
|
keysets = keysets[ "keysets" ];
|
|
var keyset = null;
|
|
keysets.every( item => {if ( isValidHex( item.id ) && item.active ) {keyset = item.id;return;} return true;});
|
|
// Pick an amount and get its pubkey
|
|
var pubkeys = await fetch(`${mymint}/v1/keys/${keyset}`);
|
|
pubkeys = await pubkeys.json();
|
|
pubkeys = pubkeys[ "keysets" ][ 0 ][ "keys" ];
|
|
var amounts_to_get = decomposeAmount( full_amount );
|
|
amounts_to_get.sort();
|
|
// Get an LN invoice for that amount
|
|
var post_data = {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({"amount": full_amount, "unit": "sat"}),
|
|
}
|
|
var invoice_data = await fetch( `${mymint}/v1/mint/quote/bolt11`, post_data );
|
|
invoice_data = await invoice_data.json();
|
|
// Pay that invoice (but not on testnut)
|
|
console.log( `pay this: ${invoice_data[ "request" ]}` );
|
|
var isPaid = async invoice => {
|
|
await waitASec( 5 );
|
|
var is_paid_info = await fetch( `${mymint}/v1/mint/quote/bolt11/${invoice_data[ "quote" ]}` );
|
|
is_paid_info = await is_paid_info.json();
|
|
var is_paid = is_paid_info[ "paid" ];
|
|
console.log( "it is paid, right?", is_paid, is_paid_info );
|
|
return is_paid || isPaid( invoice );
|
|
}
|
|
await isPaid( invoice_data[ "request" ] );
|
|
// Prepare a sig request
|
|
var [ outputs, secrets ] = await getUtxosAndSecrets( amounts_to_get, keyset );
|
|
var sig_request = {"quote": invoice_data[ "quote" ], outputs}
|
|
// Get blinded sigs
|
|
var post_data = {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify(sig_request),
|
|
}
|
|
var blinded_sigs = await fetch( `${mymint}/v1/mint/bolt11`, post_data );
|
|
blinded_sigs = await blinded_sigs.json();
|
|
// Unblind the sigs and add utxos
|
|
var new_utxos = processSigs( blinded_sigs[ "signatures" ], secrets, pubkeys );
|
|
utxos.push( ...new_utxos );
|
|
$( '.balance' ).innerText = getBalance();
|
|
}
|
|
var send = async ( invoice_or_amount, amnt_for_amountless_invoice, app_pubkey ) => {
|
|
if ( !invoice_or_amount ) invoice_or_amount = prompt( `enter a lightning invoice or the amount you want to send` );
|
|
if ( !invoice_or_amount ) return;
|
|
if ( isNaN( invoice_or_amount ) ) {
|
|
var pre_amount = bolt11.decode( invoice_or_amount ).satoshis;
|
|
var post_data = {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({request: invoice_or_amount, unit: "sat"}),
|
|
}
|
|
var quote_info = await fetch( `${mymint}/v1/melt/quote/bolt11`, post_data );
|
|
quote_info = await quote_info.json();
|
|
var quote_id = quote_info[ "quote" ];
|
|
var amount = quote_info[ "amount" ] + quote_info[ "fee_reserve" ];
|
|
} else {
|
|
var amount = Number( invoice_or_amount );
|
|
}
|
|
var err_msg = `you cannot send more than you have`;
|
|
if ( isNaN( invoice_or_amount ) ) err_msg = `you cannot send this amount because you need an extra ${amount - getBalance()} sats to pay for potential LN routing fees. Try sending a bit less`;
|
|
if ( isNaN( amount ) || Number( amount ) < 1 || amount > getBalance() ) {
|
|
if ( app_pubkey ) return err_msg;
|
|
return alert( err_msg );
|
|
}
|
|
var change = getBalance() - amount;
|
|
var change_decomposed = decomposeAmount( change );
|
|
if ( change_decomposed.length === 1 && change_decomposed[ 0 ] === 0 ) change_decomposed = [];
|
|
var send_amnt_decomposed = decomposeAmount( amount );
|
|
var keyset = utxos[ 0 ][ "id" ];
|
|
var [ potential_change_outputs, change_secrets ] = await getUtxosAndSecrets( change_decomposed, keyset );
|
|
var [ potential_send_outputs, send_secrets ] = await getUtxosAndSecrets( send_amnt_decomposed, keyset );
|
|
var balance_before_paying = getBalance();
|
|
if ( potential_change_outputs.length ) {
|
|
var swap_data = {
|
|
"inputs": utxos,
|
|
"outputs": [ ...potential_change_outputs, ...potential_send_outputs ],
|
|
}
|
|
//TODO: ensure all your utxos use the same mint before this part
|
|
var post_data = {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify( swap_data ),
|
|
}
|
|
var blinded_sigs = await fetch( `${mymint}/v1/swap`, post_data );
|
|
blinded_sigs = await blinded_sigs.json();
|
|
var change_blinded_sigs = [];
|
|
var send_blinded_sigs = [];
|
|
blinded_sigs[ "signatures" ].forEach( ( sig, index ) => {
|
|
if ( index < potential_change_outputs.length ) change_blinded_sigs.push( sig );
|
|
else send_blinded_sigs.push( sig );
|
|
});
|
|
var pubkeys = await fetch(`${mymint}/v1/keys/${keyset}`);
|
|
pubkeys = await pubkeys.json();
|
|
pubkeys = pubkeys[ "keysets" ][ 0 ][ "keys" ];
|
|
var real_change_utxos = processSigs( change_blinded_sigs, change_secrets, pubkeys );
|
|
utxos.push( ...real_change_utxos );
|
|
var real_send_utxos = processSigs( send_blinded_sigs, send_secrets, pubkeys );
|
|
utxos = real_change_utxos;
|
|
} else {
|
|
var real_send_utxos = utxos;
|
|
utxos = [];
|
|
}
|
|
if ( isNaN( invoice_or_amount ) ) {
|
|
if ( quote_info[ "fee_reserve" ] ) var [ potential_outputs, secrets ] = await getUtxosAndSecrets( change_decomposed, keyset, true );
|
|
else var potential_outputs = [];
|
|
var pay_data = {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({quote: quote_id, inputs: real_send_utxos, outputs: potential_outputs}),
|
|
}
|
|
if ( app_pubkey ) {
|
|
var state_balance = nostr_state.nwc_info[ app_pubkey ].balance;
|
|
nostr_state.nwc_info[ app_pubkey ].balance = state_balance - ( pre_amount * 1000 );
|
|
}
|
|
var pay_info = await fetch( `${mymint}/v1/melt/bolt11`, pay_data );
|
|
pay_info = await pay_info.json();
|
|
var response = null;
|
|
if ( app_pubkey ) var state_balance = nostr_state.nwc_info[ app_pubkey ].balance;
|
|
else var state_balance = "no app pubkey";
|
|
if ( pay_info[ "paid" ] ) {
|
|
if ( "change" in pay_info && pay_info[ "change" ].length ) {
|
|
var pubkeys = await fetch( `${mymint}/v1/keys/${keyset}` );
|
|
pubkeys = await pubkeys.json();
|
|
pubkeys = pubkeys[ "keysets" ][ 0 ][ "keys" ];
|
|
var change_utxos = processSigs( pay_info[ "change" ], secrets, pubkeys );
|
|
utxos.push( ...change_utxos );
|
|
}
|
|
if ( app_pubkey ) {
|
|
var pmthash = getInvoicePmthash( invoice_or_amount );
|
|
nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ][ "preimage" ] = pay_info[ "payment_preimage" ];
|
|
nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ][ "settled_at" ] = Math.floor( Date.now() / 1000 );
|
|
nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ][ "paid" ] = true;
|
|
var balance_now = getBalance();
|
|
var fees_paid = balance_before_paying - pre_amount - balance_now;
|
|
nostr_state.nwc_info[ app_pubkey ].tx_history[ pmthash ].fees_paid = fees_paid;
|
|
var state_balance = nostr_state.nwc_info[ app_pubkey ].balance;
|
|
nostr_state.nwc_info[ app_pubkey ].balance = state_balance - ( fees_paid * 1000 );
|
|
}
|
|
console.log( "preimage:" );
|
|
console.log( pay_info[ "payment_preimage" ] );
|
|
$( '.balance' ).innerText = getBalance();
|
|
response = `payment succeeded -- the preimage is in your browser console`;
|
|
} else {
|
|
response = `payment failed`;
|
|
utxos.push( ...real_send_utxos );
|
|
$( '.balance' ).innerText = getBalance();
|
|
if ( app_pubkey ) {
|
|
var state_balance = nostr_state.nwc_info[ app_pubkey ].balance;
|
|
nostr_state.nwc_info[ app_pubkey ].balance = state_balance + ( pre_amount * 1000 );
|
|
}
|
|
}
|
|
if ( app_pubkey ) return response;
|
|
return alert( response );
|
|
}
|
|
var nut = {
|
|
mint: mymint,
|
|
proofs: real_send_utxos,
|
|
}
|
|
nut = "cashuA" + btoa( JSON.stringify( {token: [nut]} ) );
|
|
console.log( nut );
|
|
$( '.balance' ).innerText = getBalance();
|
|
}
|
|
</script>
|
|
<script>
|
|
var nostr_state = {
|
|
socket: null,
|
|
nwc_info: {}
|
|
}
|
|
</script>
|
|
<script>
|
|
var createNWCconnection = async () => {
|
|
var listen = async socket => {
|
|
var subId = super_nostr.bytesToHex( nobleSecp256k1.utils.randomPrivateKey() ).substring( 0, 16 );
|
|
var filter = {}
|
|
filter.kinds = [ 23194 ];
|
|
filter.since = Math.floor( Date.now() / 1000 );
|
|
filter[ "#p" ] = [ Object.keys( nostr_state.nwc_info )[ 0 ] ];
|
|
var subscription = [ "REQ", subId, filter ];
|
|
socket.send( JSON.stringify( subscription ) );
|
|
var state = nostr_state.nwc_info[ Object.keys( nostr_state.nwc_info )[ 0 ] ];
|
|
var msg = `pay_invoice get_balance make_invoice lookup_invoice list_transactions get_info`;
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], msg, 13194 );
|
|
return super_nostr.sendEvent( event, socket );
|
|
}
|
|
var handleEvent = async message => {
|
|
var [ type, subId, event ] = JSON.parse( message.data );
|
|
var { kind, content } = event || {}
|
|
if ( !event || event === true ) return;
|
|
var app_pubkey = getRecipientFromNostrEvent( event );
|
|
if ( !( app_pubkey in nostr_state.nwc_info ) ) return;
|
|
var state = nostr_state.nwc_info[ app_pubkey ];
|
|
if ( event.pubkey !== state[ "user_pubkey" ] ) return;
|
|
var command = super_nostr.decrypt( state[ "app_privkey" ], event.pubkey, content );
|
|
try {
|
|
command = JSON.parse( command );
|
|
console.log( command );
|
|
if ( command.method === "get_info" ) {
|
|
var blockheight = await getBlockheight();
|
|
var blockhash = await getBlockhash( blockheight );
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
result: {
|
|
alias: "",
|
|
color: "",
|
|
pubkey: "",
|
|
network: "mainnet",
|
|
block_height: blockheight,
|
|
block_hash: blockhash,
|
|
methods: [ "pay_invoice", "get_balance", "make_invoice", "lookup_invoice", "list_transactions", "get_info" ],
|
|
},
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
if ( command.method === "get_balance" ) {
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
result: {
|
|
balance: nostr_state.nwc_info[ app_pubkey ].balance,
|
|
},
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
if ( command.method === "make_invoice" ) {
|
|
if ( !String( command.params.amount ).endsWith( "000" ) ) {
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
error: {
|
|
code: "OTHER",
|
|
message: "amount must end in 000 (remember, we require millisats! But they must always be zero!)",
|
|
},
|
|
result: {}
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
var invoice_data = await getLNInvoice( Math.floor( command.params.amount / 1000 ) );
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
result: {
|
|
type: "incoming",
|
|
invoice: invoice_data.request,
|
|
bolt11: invoice_data.request,
|
|
description: command.params.description,
|
|
description_hash: "",
|
|
preimage: "",
|
|
payment_hash: getInvoicePmthash( invoice_data.request ),
|
|
amount: command.params.amount,
|
|
fees_paid: 0,
|
|
created_at: bolt11.decode( invoice_data.request ).timestamp,
|
|
expires_at: bolt11.decode( invoice_data.request ).timeExpireDate,
|
|
settled_at: null,
|
|
},
|
|
});
|
|
state.tx_history[ getInvoicePmthash( invoice_data[ "request" ] ) ] = {
|
|
invoice_data,
|
|
pmthash: getInvoicePmthash( invoice_data[ "request" ] ),
|
|
amount: command.params.amount,
|
|
invoice: invoice_data[ "request" ],
|
|
bolt11: invoice_data[ "request" ],
|
|
quote: invoice_data[ "quote" ],
|
|
type: "incoming",
|
|
description: command.params.description,
|
|
description_hash: "",
|
|
preimage: "",
|
|
payment_hash: getInvoicePmthash( invoice_data[ "request" ] ),
|
|
fees_paid: 0,
|
|
created_at: bolt11.decode( invoice_data.request ).timestamp,
|
|
expires_at: bolt11.decode( invoice_data.request ).timeExpireDate,
|
|
settled_at: null,
|
|
paid: false,
|
|
}
|
|
checkInvoiceTilPaidOrError( invoice_data, app_pubkey );
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
if ( command.method === "lookup_invoice" ) {
|
|
var invoice = null;
|
|
if ( "bolt11" in command.params ) invoice = command.params.bolt11;
|
|
if ( "invoice" in command.params && !invoice ) invoice = command.params.invoice;
|
|
if ( invoice ) var pmthash = getInvoicePmthash( invoice );
|
|
if ( "payment_hash" in command.params && !pmthash ) {
|
|
var pmthash = command.params.payment_hash;
|
|
}
|
|
if ( !pmthash || !( pmthash in state.tx_history ) ) {
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
error: {
|
|
code: "INTERNAL",
|
|
message: "invoice not found",
|
|
},
|
|
result: {}
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
if ( !invoice ) invoice = state.tx_history[ pmthash ].invoice;
|
|
var invoice_data = state.tx_history[ pmthash ][ "invoice_data" ];
|
|
if ( !invoice_data ) invoice_data = invoice;
|
|
var invoice_is_settled = await checkLNInvoice( invoice_data, app_pubkey );
|
|
var preimage_to_return = state.tx_history[ pmthash ][ "preimage" ];
|
|
var reply = JSON.stringify({
|
|
result_type: "lookup_invoice",
|
|
result: {
|
|
type: "incoming",
|
|
invoice: invoice,
|
|
bolt11: invoice,
|
|
description: state.tx_history[ pmthash ][ "description" ],
|
|
description_hash: state.tx_history[ pmthash ][ "description_hash" ],
|
|
preimage: preimage_to_return,
|
|
payment_hash: state.tx_history[ "payment_hash" ],
|
|
amount: state.tx_history[ pmthash ][ "amount" ],
|
|
fees_paid: state.tx_history[ pmthash ][ "fees_paid" ],
|
|
created_at: state.tx_history[ pmthash ][ "created_at" ],
|
|
expires_at: state.tx_history[ pmthash ][ "expires_at" ],
|
|
settled_at: state.tx_history[ pmthash ][ "settled_at" ],
|
|
}
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
if ( command.method === "list_transactions" ) {
|
|
var txids = Object.keys( nostr_state.nwc_info[ app_pubkey ].tx_history );
|
|
var txs = [];
|
|
var include_unpaid = false;
|
|
var include_incoming = true;
|
|
var include_outgoing = true;
|
|
if ( "unpaid" in command.params && command.params[ "unpaid" ] ) include_unpaid = true;
|
|
if ( "type" in command.params && command.params[ "type" ] === "incoming" ) include_outgoing = false;
|
|
if ( "type" in command.params && command.params[ "type" ] === "outgoing" ) include_incoming = false;
|
|
txids.forEach( item => {
|
|
var tx = nostr_state.nwc_info[ app_pubkey ].tx_history[ item ];
|
|
if ( !include_unpaid && !tx[ "paid" ] ) return;
|
|
if ( !include_incoming && tx[ "type" ] === "incoming" ) return;
|
|
if ( !include_outgoing && tx[ "type" ] === "outgoing" ) return;
|
|
txs.push( tx );
|
|
});
|
|
txs = JSON.parse( JSON.stringify( txs ) );
|
|
txs.forEach( item => delete item[ "invoice_data" ] );
|
|
txs.sort( ( a, b ) => b[ "created_at" ] - a[ "created_at" ] );
|
|
// var findIndexOfTimestamp = ( timestamp, txs ) => {
|
|
// var timestamps = [];
|
|
// txs.forEach( tx => timestamps.push( tx[ "created_at" ] ) );
|
|
// var idx = -1;
|
|
// var comparison_time = 0;
|
|
// timestamps.every( ( time, index ) => {
|
|
// if ( Math.abs( time - timestamp ) < Math.abs( timestamp - comparison_time ) ) {
|
|
// idx = index;
|
|
// comparison_time = time;
|
|
// return true;
|
|
// }
|
|
// return;
|
|
// });
|
|
// return idx;
|
|
// }
|
|
if ( "from" in command.params ) {
|
|
var new_txs = [];
|
|
txs.forEach( item => {
|
|
if ( item.created_at < command.params[ "from" ] ) return;
|
|
new_txs.push( item );
|
|
});
|
|
txs = JSON.parse( JSON.stringify( new_txs ) );
|
|
}
|
|
if ( "until" in command.params ) {
|
|
var new_txs = [];
|
|
txs.forEach( item => {
|
|
if ( item.created_at > command.params[ "until" ] ) return;
|
|
new_txs.push( item );
|
|
});
|
|
txs = JSON.parse( JSON.stringify( new_txs ) );
|
|
}
|
|
if ( "offset" in command.params ) {
|
|
var new_txs = [];
|
|
txs.every( ( item, index ) => {
|
|
if ( index < command.params[ "offset" ] ) return true;
|
|
new_txs.push( item );
|
|
});
|
|
txs = JSON.parse( JSON.stringify( new_txs ) );
|
|
return true;
|
|
}
|
|
if ( "limit" in command.params ) {
|
|
var new_txs = [];
|
|
txs.every( item => {
|
|
if ( new_txs.length >= command.params[ "limit" ] ) return;
|
|
new_txs.push( item );
|
|
return true;
|
|
});
|
|
txs = JSON.parse( JSON.stringify( new_txs ) );
|
|
}
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
result: {
|
|
transactions: txs,
|
|
},
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
if ( command.method === "pay_invoice" ) {
|
|
var invoice = null;
|
|
if ( "bolt11" in command.params ) invoice = command.params.bolt11;
|
|
if ( "invoice" in command.params && !invoice ) invoice = command.params.invoice;
|
|
if ( invoice ) var pmthash = getInvoicePmthash( invoice );
|
|
var invoice_amt = bolt11.decode( invoice ).satoshis;
|
|
if ( !invoice_amt ) {
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
error: {
|
|
code: "NOT_IMPLEMENTED",
|
|
message: `amountless invoices are not yet supported by this backend`,
|
|
},
|
|
result: {}
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
var balance = state.balance;
|
|
if ( Math.floor( .99 * balance ) - ( invoice_amt * 1000 ) < 0 ) {
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
error: {
|
|
code: "INSUFFICIENT_BALANCE",
|
|
message: `you must leave 1% in reserve to pay routing fees so the max amount you can pay is ${Math.floor( ( .99 * balance ) / 1000 )} sats and this invoice is for ${invoice_amt} sats`,
|
|
},
|
|
result: {}
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
|
|
//put the tx info in tx_history
|
|
|
|
state.tx_history[ pmthash ] = {
|
|
type: "outgoing",
|
|
invoice: invoice,
|
|
bolt11: invoice,
|
|
description: getInvoiceDescription( invoice ),
|
|
description_hash: getInvoiceDeschash( invoice ),
|
|
preimage: "",
|
|
payment_hash: pmthash,
|
|
amount: Number( bolt11.decode( invoice ).millisatoshis ),
|
|
fees_paid: 0,
|
|
created_at: bolt11.decode( invoice ).timestamp,
|
|
expires_at: bolt11.decode( invoice ).timeExpireDate,
|
|
settled_at: null,
|
|
paid: false,
|
|
}
|
|
|
|
var response_from_mint = await send( invoice, null, app_pubkey );
|
|
//response is one of three things:
|
|
//1. payment failed
|
|
//2. payment succeeded -- the preimage is in your browser console
|
|
//3. you cannot send this amount because you need an extra ${amount - getBalance()} sats to pay for potential LN routing fees. Try sending a bit less
|
|
|
|
if ( !response_from_mint.startsWith( "payment succeeded" ) ) {
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
error: {
|
|
code: "OTHER",
|
|
message: response_from_mint,
|
|
},
|
|
result: {}
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
|
|
var preimage_to_return = state.tx_history[ pmthash ][ "preimage" ];
|
|
var reply = JSON.stringify({
|
|
result_type: "pay_invoice",
|
|
result: {
|
|
preimage: preimage_to_return,
|
|
},
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
}
|
|
} catch ( e ) {
|
|
try {
|
|
var reply = JSON.stringify({
|
|
result_type: command.method,
|
|
error: {
|
|
code: "OTHER",
|
|
message: `unknown error`,
|
|
},
|
|
result: {}
|
|
});
|
|
var event = await super_nostr.prepEvent( state[ "app_privkey" ], super_nostr.encrypt( state[ "app_privkey" ], event.pubkey, reply ), 23195, [ [ "p", event.pubkey ], [ "e", event.id ] ] );
|
|
return super_nostr.sendEvent( event, nostr_state.socket );
|
|
} catch( e2 ) {}
|
|
}
|
|
}
|
|
var getRecipientFromNostrEvent = event => {
|
|
var i; for ( i=0; i<event.tags.length; i++ ) {
|
|
if ( event.tags[ i ] && event.tags[ i ][ 0 ] && event.tags[ i ][ 1 ] && event.tags[ i ][ 0 ] == "p" ) return event.tags[ i ][ 1 ];
|
|
}
|
|
}
|
|
var nostr_loop = async () => {
|
|
var relay = "wss://slick.mjex.me";
|
|
nostr_state.socket = new WebSocket( relay );
|
|
nostr_state.socket.addEventListener( 'message', handleEvent );
|
|
nostr_state.socket.addEventListener( 'open', ()=>{listen( nostr_state.socket );} );
|
|
var connection_failure = false;
|
|
var inner_loop = async ( tries = 0 ) => {
|
|
if ( connection_failure ) return alert( `your connection to nostr failed and could not be restarted, please refresh the page` );
|
|
if ( nostr_state.socket.readyState === 1 ) {
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
return inner_loop();
|
|
}
|
|
// if there is no connection, check if we are still connecting
|
|
// give it two chances to connect if so
|
|
if ( nostr_state.socket.readyState === 0 && !tries ) {
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
return inner_loop( 1 );
|
|
}
|
|
if ( nostr_state.socket.readyState === 0 && tries ) {
|
|
connection_failure = true;
|
|
return;
|
|
}
|
|
// otherwise, it is either closing or closed
|
|
// ensure it is closed, then make a new connection
|
|
nostr_state.socket.close();
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
nostr_state.socket = new WebSocket( relay );
|
|
nostr_state.socket.addEventListener( 'message', handleEvent );
|
|
nostr_state.socket.addEventListener( 'open', ()=>{listen( nostr_state.socket );} );
|
|
await inner_loop();
|
|
}
|
|
await inner_loop();
|
|
await nostr_loop();
|
|
}
|
|
if ( !Object.keys( nostr_state.nwc_info ).length ) {
|
|
var relay = "wss://slick.mjex.me";
|
|
var app_privkey = super_nostr.bytesToHex( nobleSecp256k1.utils.randomPrivateKey() );
|
|
var app_pubkey = nobleSecp256k1.getPublicKey( app_privkey, true ).substring( 2 );
|
|
var user_secret = super_nostr.bytesToHex( nobleSecp256k1.utils.randomPrivateKey() );
|
|
var user_pubkey = nobleSecp256k1.getPublicKey( user_secret, true ).substring( 2 );
|
|
var nwc_string = `nostr+walletconnect://${app_pubkey}?relay=${relay}&secret=${user_secret}`;
|
|
nostr_state.nwc_info[ app_pubkey ] = {
|
|
mymint,
|
|
nwc_string,
|
|
app_privkey,
|
|
app_pubkey,
|
|
user_secret,
|
|
user_pubkey,
|
|
relay,
|
|
balance: 0,
|
|
tx_history: {},
|
|
}
|
|
console.log( nwc_string );
|
|
}
|
|
nostr_loop();
|
|
var waitForConnection = async () => {
|
|
if ( nostr_state.socket.readyState === 1 ) return;
|
|
console.log( 'waiting for connection...' );
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
return waitForConnection();
|
|
}
|
|
await waitForConnection();
|
|
console.log( `connected!` );
|
|
}
|
|
// $( '.connect_to_mint' ).onclick = () => {
|
|
// var mint = prompt( `Pick a cashu mint from bitcoinmints.com (pick one that supports NUTS not MODULES) and enter its url here` );
|
|
// if ( mint.includes( "..." ) ) return alert( `your mint url is invalid, be sure you clicked the clipboard icon on bitcoinmints.com -- if you just highlight + copy it won't work` );
|
|
// if ( mint.startsWith( "fed1" ) ) return alert( `your mint is not a cashu mint, if you look on bitcoinmints.com you'll see you accidentally clicked on that supports MODULES and you need one that supports NUTS. Please try again` );
|
|
// if ( !mint.startsWith( "https://" ) ) return alert( `your mint is not a cashu mint url, you must copy the mint's url and nothing else. Please try again` );
|
|
// mymint = mint;
|
|
// }
|
|
$( '.create_nwc_connection' ).onclick = () => {
|
|
var mint = prompt( `Pick a cashu mint from bitcoinmints.com (pick one that supports NUTS not MODULES) and enter its url here` );
|
|
if ( mint.includes( "..." ) ) return alert( `your mint url is invalid, be sure you clicked the clipboard icon on bitcoinmints.com -- if you just highlight + copy it won't work` );
|
|
if ( mint.startsWith( "fed1" ) ) return alert( `your mint is not a cashu mint, if you look on bitcoinmints.com you'll see you accidentally clicked on that supports MODULES and you need one that supports NUTS. Please try again` );
|
|
if ( !mint.startsWith( "https://" ) ) return alert( `your mint is not a cashu mint url, you must copy the mint's url and nothing else. Please try again` );
|
|
mymint = mint;
|
|
createNWCconnection();
|
|
}
|
|
var show_string_loop = async () => {
|
|
if ( !Object.keys( nostr_state.nwc_info ).length ) {
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
$( '.checking_connection' ).classList.add( "hidden" );
|
|
$( '.nwc_btns' ).classList.remove( "hidden" );
|
|
show_string_loop();
|
|
return;
|
|
}
|
|
var connection_id = Object.keys( nostr_state.nwc_info )[ 0 ];
|
|
var nwc_string = nostr_state.nwc_info[ connection_id ].nwc_string;
|
|
mymint = nostr_state.nwc_info[ connection_id ].mymint;
|
|
$( '.checking_connection' ).classList.add( "hidden" );
|
|
$( '.nwc_btns' ).classList.add( "hidden" );
|
|
$( '.nwc_string_div' ).innerHTML = `<p>Creating nwc connection...</p>`;
|
|
$( '.create_nwc_connection' ).classList.add( "hidden" );
|
|
// $( '.connect_to_mint' ).classList.add( "hidden" );
|
|
if ( !nostr_state.socket ) createNWCconnection();
|
|
var waitForConnection = async () => {
|
|
if ( nostr_state.socket.readyState === 1 ) return;
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
return waitForConnection();
|
|
}
|
|
await waitForConnection();
|
|
$( '.nwc_string_div' ).innerHTML = `<p>Here is your NWC string:</p><p style="border: 1px solid black; background-color: #aaaaaa; color: black; font-family: monospace; padding: 0.5rem; max-width: 15rem; word-wrap: break-word;">${nwc_string}</p><p><button onclick="destroyConnection( '${connection_id}' )">Destroy this connection</button></p>`;
|
|
$( '.send_and_receive_btns' ).classList.remove( "hidden" );
|
|
$( '.send' ).onclick = async () => {
|
|
var app_pubkey = Object.keys( nostr_state.nwc_info )[ 0 ];
|
|
var invoice = prompt( `enter a lightning invoice` );
|
|
var invoice_amt = bolt11.decode( invoice ).satoshis;
|
|
if ( !invoice_amt ) return alert( `amountless invoices are not yet supported by this wallet` );
|
|
var amnt_for_amountless_invoice = null;
|
|
var state = nostr_state.nwc_info[ app_pubkey ];
|
|
state.tx_history[ getInvoicePmthash( invoice ) ] = {
|
|
type: "outgoing",
|
|
invoice: invoice,
|
|
bolt11: invoice,
|
|
description: getInvoiceDescription( invoice ),
|
|
description_hash: getInvoiceDeschash( invoice ),
|
|
preimage: "",
|
|
payment_hash: getInvoicePmthash( invoice ),
|
|
amount: Number( bolt11.decode( invoice ).millisatoshis ),
|
|
fees_paid: 0,
|
|
created_at: bolt11.decode( invoice ).timestamp,
|
|
expires_at: bolt11.decode( invoice ).timeExpireDate,
|
|
settled_at: null,
|
|
paid: false,
|
|
}
|
|
var reply = await send( invoice, amnt_for_amountless_invoice, app_pubkey );
|
|
alert( reply );
|
|
}
|
|
$( '.receive' ).onclick = async () => {
|
|
var app_pubkey = Object.keys( nostr_state.nwc_info )[ 0 ];
|
|
var amount = prompt( `enter how much you want in satoshis` );
|
|
if ( !amount || isNaN( amount ) ) return;
|
|
$( '.invoice_box' ).innerText = `loading...`;
|
|
$( '.invoice_box' ).classList.remove( "hidden" );
|
|
amount = Number( amount );
|
|
var invoice_data = await getLNInvoice( amount );
|
|
var state = nostr_state.nwc_info[ app_pubkey ];
|
|
state.tx_history[ getInvoicePmthash( invoice_data[ "request" ] ) ] = {
|
|
invoice_data,
|
|
pmthash: getInvoicePmthash( invoice_data[ "request" ] ),
|
|
amount: amount * 1000,
|
|
invoice: invoice_data[ "request" ],
|
|
bolt11: invoice_data[ "request" ],
|
|
quote: invoice_data[ "quote" ],
|
|
type: "incoming",
|
|
description: "NWC invoice",
|
|
description_hash: "",
|
|
preimage: "",
|
|
payment_hash: getInvoicePmthash( invoice_data[ "request" ] ),
|
|
fees_paid: 0,
|
|
created_at: bolt11.decode( invoice_data.request ).timestamp,
|
|
expires_at: bolt11.decode( invoice_data.request ).timeExpireDate,
|
|
settled_at: null,
|
|
paid: false,
|
|
}
|
|
checkInvoiceTilPaidOrError( invoice_data, app_pubkey );
|
|
$( '.invoice_box' ).innerText = invoice_data[ "request" ];
|
|
}
|
|
if ( utxos.length && !Number( $( '.balance' ).innerText ) ) $( '.balance' ).innerText = getBalance();
|
|
}
|
|
show_string_loop();
|
|
var destroyConnection = connection_id => {
|
|
delete nostr_state.nwc_info[ connection_id ];
|
|
localStorage[ "nwc_server_state" ] = JSON.stringify( nostr_state.nwc_info );
|
|
window.location.reload();
|
|
}
|
|
if ( localStorage[ "nwc_server_state" ] ) nostr_state.nwc_info = JSON.parse( localStorage[ "nwc_server_state" ] );
|
|
if ( localStorage[ "cashu_utxos" ] ) utxos = JSON.parse( localStorage[ "cashu_utxos" ] );
|
|
var saveState = async () => {
|
|
localStorage[ "nwc_server_state" ] = JSON.stringify( nostr_state.nwc_info );
|
|
localStorage[ "cashu_utxos" ] = JSON.stringify( utxos );
|
|
await super_nostr.waitSomeSeconds( 1 );
|
|
saveState();
|
|
}
|
|
saveState();
|
|
</script>
|
|
</body>
|
|
</html> |