Files
inscriboor/nwcjs.js
T
2024-06-03 11:04:03 -05:00

278 lines
12 KiB
JavaScript

// dependencies: browserify-cipher and noble-secp256k1
// https://bundle.run/browserify-cipher@1.0.1
// https://bundle.run/noble-secp256k1@1.2.14
// https://bundle.run/bech32@2.0.0
var nwcjs = {
nwc_objs: [],
response: null,
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();
},
sha256: async text_or_bytes => {
if ( typeof text_or_bytes === "string" ) text_or_bytes = ( new TextEncoder().encode( text_or_bytes ) );
var hash = await nobleSecp256k1.utils.sha256( text_or_bytes );
return nwcjs.bytesToHex( hash );
},
processNWCstring: string => {
if ( !string.startsWith( "nostr+walletconnect://" ) ) return alert( `Your pairing string was invalid, try one that starts with this: nostr+walletconnect://` );
string = string.substring( 22 );
var arr = string.split( "&" );
arr.splice( 0, 1, ...arr[ 0 ].split( "?" ) );
arr[ 0 ] = "wallet_pubkey=" + arr[ 0 ];
var arr2 = [];
var obj = {}
arr.forEach( item => arr2.push( ...item.split( "=" ) ) );
arr2.forEach( ( item, index ) => {if ( item === "secret" ) arr2[ index ] = "app_privkey";});
arr2.forEach( ( item, index ) => {if ( index % 2 ) {obj[ arr2[ index - 1 ] ] = item;}});
obj[ "app_pubkey" ] = nobleSecp256k1.getPublicKey( obj[ "app_privkey" ], true ).substring( 2 );
nwcjs.nwc_objs.push( obj );
return obj;
},
getSignedEvent: async ( event, privateKey ) => {
var eventData = JSON.stringify([
0,
event['pubkey'],
event['created_at'],
event['kind'],
event['tags'],
event['content']
]);
event.id = await nwcjs.sha256( ( new TextEncoder().encode( eventData ) ) );
event.sig = await nobleSecp256k1.schnorr.sign( event.id, privateKey );
return event;
},
sendEvent: ( event, relay ) => {
var socket = new WebSocket( relay );
socket.addEventListener( 'open', async () => {
socket.send( JSON.stringify( [ "EVENT", event ] ) );
setTimeout( () => {socket.close();}, 1000 );
});
return event.id;
},
getEvents: async ( relay, kinds, until, since, limit, etags, ptags, seconds_of_delay_tolerable, debug ) => {
var socket = new WebSocket( relay );
var events = [];
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 ) {
var subId = nwcjs.bytesToHex( nobleSecp256k1.utils.randomPrivateKey() ).substring( 0, 16 );
var filter = {}
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 num_of_seconds_waited = 0;
var loop = async () => {
await nwcjs.waitSomeSeconds( 1 );
num_of_seconds_waited = num_of_seconds_waited + 1;
var time_is_up = num_of_seconds_waited >= seconds_of_delay_tolerable;
if ( debug ) console.log( `num_of_seconds_waited:`, num_of_seconds_waited, `out of`, seconds_of_delay_tolerable );
if ( time_is_up ) {
socket.close();
return events;
}
if ( events.length > 0 ) {
socket.close();
return events;
}
if ( !time_is_up ) return await loop();
}
return await loop();
},
getResponse: async ( nwc_obj, event_id, seconds_of_delay_tolerable = 3 ) => {
nwcjs.response = null;
var relay = nwc_obj[ "relay" ];
var kinds = [ 23195 ];
var until = null;
var since = null;
var limit = 1;
var etags = [ event_id ];
var ptags = [ nwc_obj[ "app_pubkey" ] ];
var events = await nwcjs.getEvents( relay, kinds, until, since, limit, etags, ptags, seconds_of_delay_tolerable );
if ( !events.length ) {
nwcjs.response = {
error: "timed out",
}
return;
}
var dmsg = nwcjs.decrypt( nwc_obj[ "app_privkey" ], events[ 0 ].pubkey, events[ 0 ].content );
nwcjs.response = JSON.parse( dmsg );
},
makeInvoice: async ( nwc_obj, amt, desc, seconds_of_delay_tolerable = 3 ) => {
var msg = JSON.stringify({
method: "make_invoice",
params: {
amount: amt * 1000,
description: desc,
}
});
var emsg = nwcjs.encrypt( nwc_obj[ "app_privkey" ], nwc_obj[ "wallet_pubkey" ], msg );
var obj = {
kind: 23194,
content: emsg,
tags: [ [ "p", nwc_obj[ "wallet_pubkey" ] ] ],
created_at: Math.floor( Date.now() / 1000 ),
pubkey: nwc_obj[ "app_pubkey" ],
}
var event = await nwcjs.getSignedEvent( obj, nwc_obj[ "app_privkey" ] );
var id = event.id;
nwcjs.getResponse( nwc_obj, id, seconds_of_delay_tolerable );
await nwcjs.waitSomeSeconds( 1 );
var relay = nwc_obj[ "relay" ];
nwcjs.sendEvent( event, relay );
var loop = async () => {
await nwcjs.waitSomeSeconds( 1 );
if ( !nwcjs.response ) return await loop();
return nwcjs.response;
}
return await loop();
},
checkInvoice: async ( nwc_obj, invoice, seconds_of_delay_tolerable = 3 ) => {
var msg = JSON.stringify({
method: "lookup_invoice",
params: {
invoice,
}
});
var emsg = nwcjs.encrypt( nwc_obj[ "app_privkey" ], nwc_obj[ "wallet_pubkey" ], msg );
var obj = {
kind: 23194,
content: emsg,
tags: [ [ "p", nwc_obj[ "wallet_pubkey" ] ] ],
created_at: Math.floor( Date.now() / 1000 ),
pubkey: nwc_obj[ "app_pubkey" ],
}
var event = await nwcjs.getSignedEvent( obj, nwc_obj[ "app_privkey" ] );
var id = event.id;
nwcjs.getResponse( nwc_obj, id, seconds_of_delay_tolerable );
await nwcjs.waitSomeSeconds( 1 );
var relay = nwc_obj[ "relay" ];
nwcjs.sendEvent( event, relay );
var loop = async () => {
await nwcjs.waitSomeSeconds( 1 );
if ( !nwcjs.response ) return await loop();
return nwcjs.response;
}
return await loop();
// an error looks like this:
// {error: {code: "INTERNAL", message: "Something went wrong while looking up invoice: "}, result_type: "lookup_invoice"}
},
didPaymentSucceed: async ( nwc_obj, invoice, seconds_of_delay_tolerable = 3 ) => {
var invoice_info = await nwcjs.checkInvoice( nwc_obj, invoice, seconds_of_delay_tolerable );
if ( invoice_info && !( "error" in invoice_info ) && "result" in invoice_info && "preimage" in invoice_info[ "result" ] )
return invoice_info[ "result" ][ "preimage" ];
return false;
},
tryToPayInvoice: async ( nwc_obj, invoice, amnt ) => {
var msg = {
method: "pay_invoice",
params: {
invoice,
}
}
if ( amnt ) msg[ "params" ][ "amount" ] = amnt;
msg = JSON.stringify( msg );
var emsg = nwcjs.encrypt( nwc_obj[ "app_privkey" ], nwc_obj[ "wallet_pubkey" ], msg );
var obj = {
kind: 23194,
content: emsg,
tags: [ [ "p", nwc_obj[ "wallet_pubkey" ] ] ],
created_at: Math.floor( Date.now() / 1000 ),
pubkey: nwc_obj[ "app_pubkey" ],
}
var event = await nwcjs.getSignedEvent( obj, nwc_obj[ "app_privkey" ] );
var id = event.id;
var relay = nwc_obj[ "relay" ];
nwcjs.sendEvent( event, relay );
},
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', nwcjs.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',
nwcjs.hexToBytes( key ),
nwcjs.hexToBytes( nwcjs.base64ToHex( iv ) )
);
var decryptedMessage = decipher.update( emsg, "base64" );
dmsg = decryptedMessage + decipher.final( "utf8" );
return dmsg;
},
waitSomeSeconds: num => {
var num = num.toString() + "000";
num = Number( num );
return new Promise( resolve => setTimeout( resolve, num ) );
},
getZapRequest: async ( endpoint, amount, relay = "wss://nostrue.com" ) => {
amount = amount * 1000;
var endpoint = endpoint.split( "@" );
var url = "https://" + endpoint[ 1 ] + "/.well-known/lnurlp/" + endpoint[ 0 ];
var url_bytes = new TextEncoder().encode( url );
var lnurl = bech32.bech32.encode( "lnurl", bech32.bech32.toWords( url_bytes ), 100_000 );
var data = await fetch( url );
data = await data.json();
var serverpub = data[ "nostrPubkey" ];
var privkey = nwcjs.bytesToHex( nobleSecp256k1.utils.randomPrivateKey() )
var pubkey = nobleSecp256k1.getPublicKey( privkey, true ).substring( 2 );
var obj = {
kind: 9734,
content: "",
tags: [
[ "relays", relay ],
[ "amount", `${amount}` ],
[ "p", serverpub ],
[ "e", "ab".repeat( 32 ) ],
[ "lnurl", lnurl ],
],
created_at: Math.floor( Date.now() / 1000 ),
pubkey: pubkey,
}
var event = await nwcjs.getSignedEvent( obj, privkey );
var id = event.id;
var encoded = encodeURI( JSON.stringify( event ) );
var callback = data[ "callback" ] + "?amount=" + amount + "&nostr=" + encoded + "&lnurl=" + lnurl;
var invoice_data = await fetch( callback );
var {pr: invoice} = await invoice_data.json();
return invoice;
},
checkZapStatus: async ( invoice, relay = "wss://nostrue.com" ) => {
var bolt11;
var events = await nwcjs.getEvents( relay, [ 9735 ], null, null, 1, [ "ab".repeat( 32 ) ], null, 3 );
if ( !events.length ) return "not paid yet";
var receipt = events[ 0 ];
receipt.tags.every( item => {
if ( item[ 0 ] !== "bolt11" ) return true;
bolt11 = item[ 1 ];
return;
});
if ( bolt11 != invoice ) return "not paid yet";
return events[ 0 ];
}
}