Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions bos
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const lnurl = importLazy('./lnurl');
const {lnurlFunctions} = commandConstants;
const network = importLazy('./network');
const nodes = importLazy('./nodes');
const nostr = importLazy('./nostr');
const offchain = importLazy('./offchain');
const {peerSortOptions} = commandConstants;
const peers = importLazy('./peers');
Expand Down Expand Up @@ -1243,6 +1244,36 @@ prog
});
})

.command('nostr', 'Collection of nostr features')
.help('--add-relay flag saves a relay to publish data to')
.help('--broadcast-message broadcasts an event to relays')
.help('--nostr-key flag saves nostr private key encrypted to your node')
.help('--remove-relay flag deletes a saved relay')
.option('--add-relay <relay_uri>', 'Saves a relay to broadcast events to', REPEATABLE)
.option('--broadcast-message <broadcast_message>', 'Broadcast an event to relays')
.option('--node <node_name>', 'Saved node to encrypt nostr key to')
.option('--nostr-key <nostr_private_key>', 'Saves nostr private key')
.option('--remove-relay <relay_uri>', 'Removes a saved relay', REPEATABLE)
.action((args, options, logger) => {
return new Promise(async (resolve, reject) => {
return nostr.manageNostr({
logger,
add: flatten([options.addRelay].filter(n => !!n)),
fs: {
writeFile,
getFile: readFile,
makeDirectory: mkdir,
},
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
message: options.broadcastMessage || undefined,
node: options.node || undefined,
nostr_key: options.nostrKey || undefined,
remove: flatten([options.removeRelay].filter(n => !!n)),
},
responses.returnObject({logger, reject, resolve}));
});
})

// Open channels
.command('open', 'Open channels, optionally using an external wallet')
.help('When creating channels from an external wallet do not self-broadcast')
Expand Down
135 changes: 135 additions & 0 deletions nostr/adjust_relays.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');

const {homePath} = require('../storage');

const defaultRelaysFile = {relays: []};
const {isArray} = Array;
const isWebsocket = (n) => /^wss?:\/\/(([^:]+)(:(\d+))?)/.test(n);
const {parse} = JSON;
const relayFilePath = () => homePath({file: 'nostr.json'}).path;
const stringify = obj => JSON.stringify(obj, null, 2);

/** Adjust relays

{
[add]: [<Relay Uri To Add String>]
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
makeDirectory: <Make Directory Function> (path, cbk) => {}
writeFile: <Write File Contents Function> (path, contents, cbk) => {}
}
logger: <Winston Logger Object>
node: <Saved Node Name String>
[remove]: [<Relay Uri To String>]
}

@returns via cbk or Promise
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!isArray(args.add)) {
return cbk([400, 'ExpectedArrayOfRelaysToAddToAdjustRelays']);
}

if (!args.fs) {
return cbk([400, 'ExpectedFilesystemMethodsToAdjustRelays']);
}

if (!args.logger) {
return cbk([400, 'ExpectedLoggerToAdjustRelays']);
}

if (!isArray(args.remove)) {
return cbk([400, 'ExpectedArrayOfRelaysToRemoveToAdjustRelays']);
}

if (!args.add.length && !args.remove.length) {
return cbk([400, 'ExpectedEitherAddOrRemoveRelayListToAdjustRelays']);
}

if (!!args.add.filter(n => !isWebsocket(n)).length) {
return cbk([400, 'RelaysToAddMustBeValidWebSocketUris']);
}

if (!!args.remove.filter(n => !isWebsocket(n)).length) {
return cbk([400, 'RelaysToRemoveMustBeValidWebSocketUris']);
}

return cbk();
},

// Register the home directory
registerHomeDir: ['validate', ({}, cbk) => {
return args.fs.makeDirectory(homePath({}).path, err => {
// Ignore errors, the directory may already be there
return cbk();
});
}],

// Read file and adjust
adjustRelays: ['registerHomeDir', ({}, cbk) => {
const node = args.node || '';

return args.fs.getFile(relayFilePath(), (err, res) => {
// Exit if there is no relays file
if (!!err || !res) {
return cbk([400, 'ExpectedValidJsonNostrFileToAdjustRelays']);
}

try {
const file = parse(res.toString());

if (!file.nostr || !isArray(file.nostr) || !file.nostr.length) {
return cbk([400, 'ExpectedAtLeastOneNostrKeyInNostrFileToAdjustRelays']);
}

const findNode = file.nostr.find(n => n.node === node);

if (!findNode) {
return cbk([400, 'ExpectedSavedNostrKeyInNostrFileToAdjustRelays']);
}

// Adjust the relays file
args.add.forEach(n => {
const findRelay = findNode.relays.find(relay => relay === n);

if (!findRelay) {
findNode.relays.push(n);
}
});

args.remove.forEach(n => {
const findRelay = findNode.relays.find(relay => relay === n);

if (!!findRelay) {
findNode.relays = findNode.relays.filter(relay => relay !== n)
}
});

return cbk(null, {file, relays: findNode.relays});
} catch (err) {
return cbk([400, 'ExpectedValidJsonRelaysFileToAdjustRelays', {err}]);
}
});
}],

// Adjust relays
writeFile: ['adjustRelays', ({adjustRelays}, cbk) => {
return args.fs.writeFile(relayFilePath(), stringify(adjustRelays.file), err => {
if (!!err) {
return cbk([503, 'UnexpectedErrorSavingRelayFileUpdate', {err}]);
}

args.logger.info({relays_adjusted: adjustRelays.relays});

return cbk();
});
}],
},
returnResult({reject, resolve}, cbk));
});
};
151 changes: 151 additions & 0 deletions nostr/build_event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');
const {createHash} = require('crypto');

const tinysecp256k1 = require('tiny-secp256k1');

const {decryptWithNode} = require('../encryption');
const {homePath} = require('../storage');
const publishToRelays = require('./publish_to_relays');

const createdAt = () => Math.round(Date.now() / 1000);
const eventKind = 1;
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
const {isArray} = Array;
const nostrFilePath = () => homePath({file: 'nostr.json'}).path;
const {parse} = JSON;
const sha256 = n => createHash('sha256').update(n).digest();
const stringAsUtf8 = n => Buffer.from(n, 'utf-8');
const {stringify} = JSON;
const unit8AsHex = n => Buffer.from(n).toString('hex');

/** Build nostr event to publish

{
fs: {
getFile: <Read File Contents Function> (path, cbk) => {}
}
lnd: <Authenticated LND API Object>
logger: <Winston Logger Object>
message: <Message For Event String>
node: <Saved Node Name String>
}

@returns via cbk or Promise
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Import the ECPair library
ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp256k1),

// Check arguments
validate: cbk => {
if (!args.fs) {
return cbk([400, 'ExpectedFilesystemMethodsToBuildEvent']);
}

if (!args.message) {
return cbk([400, 'ExpectedMessageEventToBuildEvent']);
}

if (!args.lnd) {
return cbk([400, 'ExpectedLndToBuildEvent']);
}

if (!args.logger) {
return cbk([400, 'ExpectedLoggerToBuildEvent']);
}

return cbk();
},

// Get relays and nostr key
readFile: ['validate', ({}, cbk) => {
const node = args.node || '';

return args.fs.getFile(nostrFilePath(), (err, res) => {
if (!!err || !res) {
return cbk([400, 'FailedToReadRelaysJsonFileToBuildEvent']);
}

try {
const result = parse(res.toString());

if (!result.nostr || !isArray(result.nostr) || !result.nostr.length) {
return cbk([400, 'ExpectedNostrKeyAndRelaysToBuildEvent']);
}

const findNode = result.nostr.find(n => n.node === node);

if (!findNode) {
return cbk([400, 'ExpectedNostrKeyAndRelaysForSavedNode']);
}

if (!findNode.key) {
return cbk([400, 'ExpectedNostrKeyToBuildEvent']);
}

if (!findNode.relays.length) {
return cbk([400, 'ExpectedAtLeastOneRelayToBuildEvent']);
}

return cbk(null, {key: findNode.key, relays: findNode.relays})
} catch (err) {
return cbk([400, 'FailedToParseRelaysJsonFileToBuildEvent']);
}
});
}],

// Decrypt nostr private key
decrypt: ['readFile', ({readFile}, cbk) => {
return decryptWithNode({
encrypted: readFile.key,
lnd: args.lnd,
},
cbk);
}],

// Build the nostr event
buildEvent: [
'decrypt',
'ecp',
'readFile', ({decrypt, ecp}, cbk) => {
const key = ecp.fromPrivateKey(hexAsBuffer(decrypt.message));
const publicKey = unit8AsHex(key.publicKey.slice(1));
const created = createdAt();
const content = `This is a test from BalanceOfSatoshis: \n Group Open Invite Code: ${args.message}`;

const commit = stringify([0, publicKey, created, eventKind, [], content]);
const buf = stringAsUtf8(commit);
const hash = sha256(buf);

const eventId = unit8AsHex(hash);

const signature = unit8AsHex(tinysecp256k1.signSchnorr(hash, hexAsBuffer(decrypt.message)));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work? Wouldn't it be the key signing?


const event = {
content,
id: eventId,
pubkey: publicKey,
created_at: created,
kind: eventKind,
tags: [],
sig: signature,
}

return cbk(null, {event});
}],

// Publish event to relays
publish: ['buildEvent', 'readFile', ({buildEvent, readFile}, cbk) => {
return publishToRelays({
event: stringify(['EVENT', buildEvent.event]),
logger: args.logger,
relays: readFile.relays
}, cbk);
}],
},
returnResult({reject, resolve}, cbk));
});
};
3 changes: 3 additions & 0 deletions nostr/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const manageNostr = require('./manage_nostr');

module.exports = {manageNostr};
Loading