Merge branch 'master' into feature/tunnel

This commit is contained in:
hatim boufnichel 2021-02-26 16:52:24 +01:00
commit d7e7689e1f
30 changed files with 1785 additions and 953 deletions

View file

@ -6,3 +6,5 @@ CACHE_HEADERS_MANDATORY=true
SHOCK_CACHE=true SHOCK_CACHE=true
TRUSTED_KEYS=true TRUSTED_KEYS=true
LOCAL_TUNNEL_SERVER=http://tunnel.example.com LOCAL_TUNNEL_SERVER=http://tunnel.example.com
TORRENT_SEED_URL=https://webtorrent.shock.network
TORRENT_SEED_TOKEN=jibberish

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
*.ts

View file

@ -88,7 +88,10 @@
// I am now convinced TODO comments closer to the relevant code are better // I am now convinced TODO comments closer to the relevant code are better
// than GH issues. Especially when it only concerns a single function / // than GH issues. Especially when it only concerns a single function /
// routine. // routine.
"no-warning-comments": "off" "no-warning-comments": "off",
// broken
"sort-imports": "off"
}, },
"parser": "babel-eslint", "parser": "babel-eslint",
"env": { "env": {

36
.github/workflows/version.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Bump "package.json" Version
on:
release:
types: [prereleased, released]
jobs:
version-bump:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
with:
persist-credentials: false
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
- name: Get the last API TAG and current version in package.json
run: |
export RELEASE_TAG=$(git describe --tags --abbrev=0) && \
echo "VERSION=${RELEASE_TAG}" >> $GITHUB_ENV
export API_TAG=$(cat ./package.json | jq -r '.version')
echo $(if [ "$API_TAG" = "$RELEASE_TAG" ]; then echo "UPGRADEABLE=false"; else echo "UPGRADEABLE=true"; fi) >> $GITHUB_ENV
- name: Update and Commit files
if: ${{ env.UPGRADEABLE == 'true' }}
run: |
cat ./package.json | jq -r --arg API_TAG "${{ env.VERSION }}" '.version = $API_TAG' | tee a.json && mv a.json package.json
git config --local user.email "actions@shock.network"
git config --local user.name "Version Update Action"
git commit -m "version upgraded to ${{ env.VERSION }}" -a
- name: Push changes
if: ${{ env.UPGRADEABLE == 'true' }}
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: master

View file

@ -1,5 +1,7 @@
{ {
"eslint.enable": true, "eslint.enable": true,
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"debug.node.autoAttach": "on" "debug.node.autoAttach": "on",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
} }

33
.vscode/snippets.code-snippets vendored Normal file
View file

@ -0,0 +1,33 @@
{
// Place your api workspace snippets here. Each snippet is defined under a
// snippet name and has a scope, prefix, body and description. Add comma
// separated ids of the languages where the snippet is applicable in the scope
// field. If scope is left empty or omitted, the snippet gets applied to all
// languages. The prefix is what is used to trigger the snippet and the body
// will be expanded and inserted. Possible variables are: $1, $2 for tab
// stops, $0 for the final cursor position, and ${1:label}, ${2:another} for
// placeholders. Placeholders with the same ids are connected. Example: "Print
// to console": {"scope": "javascript,typescript", "prefix": "log", "body":
// ["console.log('$1');", "$2"
// ],
// "description": "Log output to console"
// }
"Route Body": {
"body": [
"try {",
" return res.json({",
"",
" })",
"} catch (e) {",
" console.log(e)",
" return res.status(500).json({",
" errorMessage: e.message",
" })",
"}"
],
"description": "Route Body",
"prefix": "rbody",
"scope": "javascript"
}
}

View file

@ -1,6 +1,6 @@
<h1>ShockAPI</h1> <h1>ShockAPI</h1>
![GitHub last commit](https://img.shields.io/github/last-commit/shocknet/wallet?style=flat-square) ![GitHub last commit](https://img.shields.io/github/last-commit/shocknet/api?style=flat-square)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
[![Chat](https://img.shields.io/badge/chat-on%20Telegram-blue?style=flat-square)](https://t.me/Shockwallet) [![Chat](https://img.shields.io/badge/chat-on%20Telegram-blue?style=flat-square)](https://t.me/Shockwallet)
[![Twitter Follow](https://img.shields.io/twitter/follow/ShockBTC?style=flat-square)](https://twitter.com/shockbtc) [![Twitter Follow](https://img.shields.io/twitter/follow/ShockBTC?style=flat-square)](https://twitter.com/shockbtc)

View file

@ -35,6 +35,7 @@ module.exports = (mainnet = false) => {
maxNumRoutesToQuery: 20, maxNumRoutesToQuery: 20,
lndProto: parsePath(`${__dirname}/rpc.proto`), lndProto: parsePath(`${__dirname}/rpc.proto`),
routerProto: parsePath(`${__dirname}/router.proto`), routerProto: parsePath(`${__dirname}/router.proto`),
invoicesProto: parsePath(`${__dirname}/invoices.proto`),
walletUnlockerProto: parsePath(`${__dirname}/walletunlocker.proto`), walletUnlockerProto: parsePath(`${__dirname}/walletunlocker.proto`),
lndHost: "localhost:10009", lndHost: "localhost:10009",
lndCertPath: parsePath(`${lndDirectory}/tls.cert`), lndCertPath: parsePath(`${lndDirectory}/tls.cert`),

122
config/invoices.proto Normal file
View file

@ -0,0 +1,122 @@
syntax = "proto3";
import "rpc.proto";
package invoicesrpc;
option go_package = "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc";
// Invoices is a service that can be used to create, accept, settle and cancel
// invoices.
service Invoices {
/*
SubscribeSingleInvoice returns a uni-directional stream (server -> client)
to notify the client of state transitions of the specified invoice.
Initially the current invoice state is always sent out.
*/
rpc SubscribeSingleInvoice (SubscribeSingleInvoiceRequest)
returns (stream lnrpc.Invoice);
/*
CancelInvoice cancels a currently open invoice. If the invoice is already
canceled, this call will succeed. If the invoice is already settled, it will
fail.
*/
rpc CancelInvoice (CancelInvoiceMsg) returns (CancelInvoiceResp);
/*
AddHoldInvoice creates a hold invoice. It ties the invoice to the hash
supplied in the request.
*/
rpc AddHoldInvoice (AddHoldInvoiceRequest) returns (AddHoldInvoiceResp);
/*
SettleInvoice settles an accepted invoice. If the invoice is already
settled, this call will succeed.
*/
rpc SettleInvoice (SettleInvoiceMsg) returns (SettleInvoiceResp);
}
message CancelInvoiceMsg {
// Hash corresponding to the (hold) invoice to cancel.
bytes payment_hash = 1;
}
message CancelInvoiceResp {
}
message AddHoldInvoiceRequest {
/*
An optional memo to attach along with the invoice. Used for record keeping
purposes for the invoice's creator, and will also be set in the description
field of the encoded payment request if the description_hash field is not
being used.
*/
string memo = 1;
// The hash of the preimage
bytes hash = 2;
/*
The value of this invoice in satoshis
The fields value and value_msat are mutually exclusive.
*/
int64 value = 3;
/*
The value of this invoice in millisatoshis
The fields value and value_msat are mutually exclusive.
*/
int64 value_msat = 10;
/*
Hash (SHA-256) of a description of the payment. Used if the description of
payment (memo) is too long to naturally fit within the description field
of an encoded payment request.
*/
bytes description_hash = 4;
// Payment request expiry time in seconds. Default is 3600 (1 hour).
int64 expiry = 5;
// Fallback on-chain address.
string fallback_addr = 6;
// Delta to use for the time-lock of the CLTV extended to the final hop.
uint64 cltv_expiry = 7;
/*
Route hints that can each be individually used to assist in reaching the
invoice's destination.
*/
repeated lnrpc.RouteHint route_hints = 8;
// Whether this invoice should include routing hints for private channels.
bool private = 9;
}
message AddHoldInvoiceResp {
/*
A bare-bones invoice for a payment within the Lightning Network. With the
details of the invoice, the sender has all the data necessary to send a
payment to the recipient.
*/
string payment_request = 1;
}
message SettleInvoiceMsg {
// Externally discovered pre-image that should be used to settle the hold
// invoice.
bytes preimage = 1;
}
message SettleInvoiceResp {
}
message SubscribeSingleInvoiceRequest {
reserved 1;
// Hash corresponding to the (hold) invoice to subscribe to.
bytes r_hash = 2;
}

View file

@ -1,6 +1,6 @@
{ {
"name": "shockapi", "name": "shockapi",
"version": "1.0.0", "version": "2021.1.04",
"description": "", "description": "",
"main": "src/server.js", "main": "src/server.js",
"scripts": { "scripts": {
@ -11,13 +11,16 @@
"test:watch": "jest --no-cache --watch", "test:watch": "jest --no-cache --watch",
"typecheck": "tsc", "typecheck": "tsc",
"lint": "eslint \"services/gunDB/**/*.js\"", "lint": "eslint \"services/gunDB/**/*.js\"",
"format": "prettier --write \"./**/*.js\"" "format": "prettier --write \"./**/*.js\"",
"test:gun": "ts-node src/__gun__tests__/*.ts && rimraf -rf GUN-TEST-*"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@grpc/proto-loader": "^0.5.1", "@grpc/grpc-js": "^1.2.2",
"axios": "^0.20.0", "@grpc/proto-loader": "^0.5.5",
"assert-never": "^1.2.1",
"axios": "^0.21.1",
"basic-auth": "^2.0.0", "basic-auth": "^2.0.0",
"big.js": "^5.2.2", "big.js": "^5.2.2",
"bitcore-lib": "^0.15.0", "bitcore-lib": "^0.15.0",
@ -31,7 +34,7 @@
"debug": "^3.1.0", "debug": "^3.1.0",
"dotenv": "^8.1.0", "dotenv": "^8.1.0",
"express": "^4.14.1", "express": "^4.14.1",
"express-session": "^1.15.1", "express-session": "^1.17.1",
"google-proto-files": "^1.0.3", "google-proto-files": "^1.0.3",
"graphviz": "0.0.8", "graphviz": "0.0.8",
"grpc": "1.24.4", "grpc": "1.24.4",
@ -42,15 +45,16 @@
"localtunnel": "git://github.com/shocknet/localtunnel#40cc2c2a46b05da2217bf2e20da11a5343a5cce7", "localtunnel": "git://github.com/shocknet/localtunnel#40cc2c2a46b05da2217bf2e20da11a5343a5cce7",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"method-override": "^2.3.7", "method-override": "^2.3.7",
"node-fetch": "^2.6.1",
"node-persist": "^3.1.0", "node-persist": "^3.1.0",
"promise": "^8.1.0", "promise": "^8.1.0",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"ramda": "^0.27.1", "ramda": "^0.27.1",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise": "^4.2.2", "request-promise": "^4.2.6",
"response-time": "^2.3.2", "response-time": "^2.3.2",
"shelljs": "^0.8.2", "shelljs": "^0.8.2",
"shock-common": "17.x.x", "shock-common": "32.0.0",
"socket.io": "2.1.1", "socket.io": "2.1.1",
"text-encoding": "^0.7.0", "text-encoding": "^0.7.0",
"tingodb": "^0.6.1", "tingodb": "^0.6.1",
@ -58,7 +62,7 @@
"winston-daily-rotate-file": "^4.5.0" "winston-daily-rotate-file": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-class-properties": "^7.12.1",
"@types/bluebird": "^3.5.32", "@types/bluebird": "^3.5.32",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
"@types/express": "^4.17.1", "@types/express": "^4.17.1",
@ -66,10 +70,12 @@
"@types/jest": "^24.0.18", "@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.3.7", "@types/jsonwebtoken": "^8.3.7",
"@types/lodash": "^4.14.141", "@types/lodash": "^4.14.141",
"@types/node-fetch": "^2.5.8",
"@types/ramda": "types/npm-ramda#dist", "@types/ramda": "types/npm-ramda#dist",
"@types/react": "16.x.x",
"@types/socket.io": "^2.1.11", "@types/socket.io": "^2.1.11",
"@types/uuid": "^3.4.5", "@types/uuid": "^3.4.5",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.1.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"eslint": "^6.6.0", "eslint": "^6.6.0",
"eslint-config-prettier": "^6.5.0", "eslint-config-prettier": "^6.5.0",
@ -80,8 +86,10 @@
"lint-staged": "^10.2.2", "lint-staged": "^10.2.2",
"nodemon": "^1.19.3", "nodemon": "^1.19.3",
"prettier": "^1.18.2", "prettier": "^1.18.2",
"rimraf": "^3.0.2",
"ts-node": "^9.1.1",
"ts-type": "^1.2.16", "ts-type": "^1.2.16",
"typescript": "^4.0.2" "typescript": "latest"
}, },
"lint-staged": { "lint-staged": {
"*.js": [ "*.js": [

63
services/coordinates.js Normal file
View file

@ -0,0 +1,63 @@
/**
* @format
*/
const Common = require('shock-common')
const mapValues = require('lodash/mapValues')
const pickBy = require('lodash/pickBy')
const Bluebird = require('bluebird')
const Logger = require('winston')
const Key = require('../services/gunDB/contact-api/key')
const { getUser, getMySecret, mySEA } = require('./gunDB/Mediator')
/**
* @param {string} coordID
* @param {Common.Coordinate} data
* @returns {Promise<void>}
*/
export const writeCoordinate = async (coordID, data) => {
if (coordID !== data.id) {
throw new Error('CoordID must be equal to data.id')
}
try {
/**
* Because there are optional properties, typescript can also allow them
* to be specified but with a value of `undefined`. Filter out these.
* @type {Record<string, number|boolean|string>}
*/
const sanitizedData = pickBy(data, v => typeof v !== 'undefined')
const encData = await Bluebird.props(
mapValues(sanitizedData, v => {
return mySEA.encrypt(v, getMySecret())
})
)
getUser()
.get(Key.COORDINATES)
.get(coordID)
.put(encData, ack => {
if (ack.err && typeof ack.err !== 'number') {
Logger.info(
`Error writting corrdinate, coordinate id: ${coordID}, data: ${JSON.stringify(
data,
null,
2
)}`
)
Logger.error(ack.err)
}
})
} catch (e) {
Logger.info(
`Error writing coordinate, coordinate id: ${coordID}, data: ${JSON.stringify(
data,
null,
2
)}`
)
Logger.error(e.message)
}
}

View file

@ -26,23 +26,13 @@ const SEAx = require('gun/sea')
/** @type {import('../contact-api/SimpleGUN').ISEA} */ /** @type {import('../contact-api/SimpleGUN').ISEA} */
const mySEA = {} const mySEA = {}
const $$__SHOCKWALLET__MSG__ = '$$__SHOCKWALLET__MSG__' // Avoid this: https://github.com/amark/gun/issues/804 and any other issues
const $$__SHOCKWALLET__ENCRYPTED__ = '$$_SHOCKWALLET__ENCRYPTED__' const $$__SHOCKWALLET__ENCRYPTED__ = '$$_SHOCKWALLET__ENCRYPTED__'
const $$__SHOCKWALLET__MSG__ = '$$__SHOCKWALLET__MSG__'
const $$__SHOCKWALLET__NUMBER__ = '$$__SHOCKWALLET__NUMBER__'
const $$__SHOCKWALLET__BOOLEAN__ = '$$__SHOCKWALLET__BOOLEAN__'
mySEA.encrypt = (msg, secret) => { mySEA.encrypt = (msg, secret) => {
if (typeof msg !== 'string') {
throw new TypeError(
'mySEA.encrypt() -> expected msg to be an string instead got: ' +
typeof msg
)
}
if (msg.length === 0) {
throw new TypeError(
'mySEA.encrypt() -> expected msg to be a populated string'
)
}
if (typeof secret !== 'string') { if (typeof secret !== 'string') {
throw new TypeError( throw new TypeError(
`mySEA.encrypt() -> expected secret to be a an string, args: |msg| -- ${JSON.stringify( `mySEA.encrypt() -> expected secret to be a an string, args: |msg| -- ${JSON.stringify(
@ -57,15 +47,35 @@ mySEA.encrypt = (msg, secret) => {
) )
} }
// Avoid this: https://github.com/amark/gun/issues/804 and any other issues let strToEncode = ''
const sanitizedMsg = $$__SHOCKWALLET__MSG__ + msg
return SEAx.encrypt(sanitizedMsg, secret).then(encMsg => { if (typeof msg === 'string') {
if (msg.length === 0) {
throw new TypeError(
'mySEA.encrypt() -> expected msg to be a populated string'
)
}
strToEncode = $$__SHOCKWALLET__MSG__ + msg
} else if (typeof msg === 'boolean') {
strToEncode = $$__SHOCKWALLET__BOOLEAN__ + msg
} else if (typeof msg === 'number') {
strToEncode = $$__SHOCKWALLET__NUMBER__ + msg
} else {
throw new TypeError('mySea.encrypt() -> Not a valid msg type.')
}
return SEAx.encrypt(strToEncode, secret).then(encMsg => {
return $$__SHOCKWALLET__ENCRYPTED__ + encMsg return $$__SHOCKWALLET__ENCRYPTED__ + encMsg
}) })
} }
mySEA.decrypt = (encMsg, secret) => { /**
* @param {string} encMsg
* @param {string} secret
* @returns {Promise<any>}
*/
const decryptBase = (encMsg, secret) => {
if (typeof encMsg !== 'string') { if (typeof encMsg !== 'string') {
throw new TypeError( throw new TypeError(
'mySEA.encrypt() -> expected encMsg to be an string instead got: ' + 'mySEA.encrypt() -> expected encMsg to be an string instead got: ' +
@ -104,10 +114,41 @@ mySEA.decrypt = (encMsg, secret) => {
throw new TypeError('Could not decrypt') throw new TypeError('Could not decrypt')
} }
if (decodedMsg.startsWith($$__SHOCKWALLET__MSG__)) {
return decodedMsg.slice($$__SHOCKWALLET__MSG__.length) return decodedMsg.slice($$__SHOCKWALLET__MSG__.length)
} else if (decodedMsg.startsWith($$__SHOCKWALLET__BOOLEAN__)) {
const dec = decodedMsg.slice($$__SHOCKWALLET__BOOLEAN__.length)
if (dec === 'true') {
return true
} else if (dec === 'false') {
return false
}
throw new Error('Could not decrypt boolean value.')
} else if (decodedMsg.startsWith($$__SHOCKWALLET__NUMBER__)) {
return Number(decodedMsg.slice($$__SHOCKWALLET__NUMBER__.length))
}
throw new TypeError(
`mySea.encrypt() -> Unexpected type of prefix found inside decrypted value, first 20 characters: ${decodedMsg.slice(
0,
20
)}`
)
}) })
} }
mySEA.decrypt = (encMsg, secret) => {
return decryptBase(encMsg, secret)
}
mySEA.decryptNumber = (encMsg, secret) => {
return decryptBase(encMsg, secret)
}
mySEA.decryptBoolean = (encMsg, secret) => {
return decryptBase(encMsg, secret)
}
mySEA.secret = async (recipientOrSenderEpub, recipientOrSenderSEA) => { mySEA.secret = async (recipientOrSenderEpub, recipientOrSenderSEA) => {
if (typeof recipientOrSenderEpub !== 'string') { if (typeof recipientOrSenderEpub !== 'string') {
throw new TypeError( throw new TypeError(
@ -273,7 +314,7 @@ const authenticate = async (alias, pass, __user) => {
// clock skew // clock skew
await new Promise(res => setTimeout(res, 2000)) await new Promise(res => setTimeout(res, 2000))
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
_user.get(Key.FOLLOWS).put( _user.get(Key.FOLLOWS).put(
{ {
unused: null unused: null
@ -286,7 +327,7 @@ const authenticate = async (alias, pass, __user) => {
} }
} }
) )
}) }))
return ack.sea.pub return ack.sea.pub
} else { } else {
@ -304,7 +345,7 @@ const authenticate = async (alias, pass, __user) => {
// clock skew // clock skew
await new Promise(res => setTimeout(res, 2000)) await new Promise(res => setTimeout(res, 2000))
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
_user.get(Key.FOLLOWS).put( _user.get(Key.FOLLOWS).put(
{ {
unused: null unused: null
@ -317,7 +358,7 @@ const authenticate = async (alias, pass, __user) => {
} }
} }
) )
}) }))
// move this to a subscription; implement off() ? todo // move this to a subscription; implement off() ? todo
API.Jobs.onAcceptedRequests(_user, mySEA) API.Jobs.onAcceptedRequests(_user, mySEA)
@ -363,7 +404,7 @@ const authenticate = async (alias, pass, __user) => {
await new Promise(res => setTimeout(res, 5000)) await new Promise(res => setTimeout(res, 5000))
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
_user.get(Key.FOLLOWS).put( _user.get(Key.FOLLOWS).put(
{ {
unused: null unused: null
@ -376,7 +417,7 @@ const authenticate = async (alias, pass, __user) => {
} }
} }
) )
}) }))
API.Jobs.onAcceptedRequests(_user, mySEA) API.Jobs.onAcceptedRequests(_user, mySEA)
API.Jobs.onOrders(_user, gun, mySEA) API.Jobs.onOrders(_user, gun, mySEA)

View file

@ -120,8 +120,19 @@ export interface UserGUNNode extends GUNNode {
} }
export interface ISEA { export interface ISEA {
encrypt(message: string, senderSecret: string): Promise<string> encrypt(
message: string | number | boolean,
senderSecret: string
): Promise<string>
decrypt(encryptedMessage: string, recipientSecret: string): Promise<string> decrypt(encryptedMessage: string, recipientSecret: string): Promise<string>
decryptNumber(
encryptedMessage: string,
recipientSecret: string
): Promise<number>
decryptBoolean(
encryptedMessage: string,
recipientSecret: string
): Promise<boolean>
secret( secret(
recipientOrSenderEpub: string, recipientOrSenderEpub: string,
recipientOrSenderUserPair: UserPair recipientOrSenderUserPair: UserPair

View file

@ -11,7 +11,8 @@ const { ErrorCode } = Constants
const { const {
sendPaymentV2Invoice, sendPaymentV2Invoice,
decodePayReq decodePayReq,
myLNDPub
} = require('../../../utils/lightningServices/v2') } = require('../../../utils/lightningServices/v2')
/** /**
@ -21,6 +22,7 @@ const {
const Getters = require('./getters') const Getters = require('./getters')
const Key = require('./key') const Key = require('./key')
const Utils = require('./utils') const Utils = require('./utils')
const { writeCoordinate } = require('../../coordinates')
/** /**
* @typedef {import('./SimpleGUN').GUNNode} GUNNode * @typedef {import('./SimpleGUN').GUNNode} GUNNode
@ -98,7 +100,7 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
timestamp: Date.now() timestamp: Date.now()
} }
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.OUTGOINGS) .get(Key.OUTGOINGS)
.get(newOutgoingFeedID) .get(newOutgoingFeedID)
@ -111,14 +113,14 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
res() res()
} }
}) })
}) }))
const encryptedForMeNewOutgoingFeedID = await SEA.encrypt( const encryptedForMeNewOutgoingFeedID = await SEA.encrypt(
newOutgoingFeedID, newOutgoingFeedID,
mySecret mySecret
) )
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.RECIPIENT_TO_OUTGOING) .get(Key.RECIPIENT_TO_OUTGOING)
.get(withPublicKey) .get(withPublicKey)
@ -129,7 +131,7 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
res() res()
} }
}) })
}) }))
outgoingFeedID = newOutgoingFeedID outgoingFeedID = newOutgoingFeedID
} }
@ -235,7 +237,7 @@ const acceptRequest = async (
const mySecret = require('../Mediator').getMySecret() const mySecret = require('../Mediator').getMySecret()
const encryptedForMeIncomingID = await SEA.encrypt(incomingID, mySecret) const encryptedForMeIncomingID = await SEA.encrypt(incomingID, mySecret)
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.USER_TO_INCOMING) .get(Key.USER_TO_INCOMING)
.get(senderPublicKey) .get(senderPublicKey)
@ -246,7 +248,7 @@ const acceptRequest = async (
res() res()
} }
}) })
}) }))
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// NOTE: perform non-reversable actions before destructive actions // NOTE: perform non-reversable actions before destructive actions
@ -259,7 +261,7 @@ const acceptRequest = async (
ourSecret ourSecret
) )
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
gun gun
.get(Key.HANDSHAKE_NODES) .get(Key.HANDSHAKE_NODES)
.get(handshakeAddress) .get(handshakeAddress)
@ -276,7 +278,7 @@ const acceptRequest = async (
} }
} }
) )
}) }))
} }
/** /**
@ -285,7 +287,7 @@ const acceptRequest = async (
* @param {UserGUNNode} userNode * @param {UserGUNNode} userNode
*/ */
const authenticate = (user, pass, userNode) => const authenticate = (user, pass, userNode) =>
new Promise((resolve, reject) => { /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
if (typeof user !== 'string') { if (typeof user !== 'string') {
throw new TypeError('expected user to be of type string') throw new TypeError('expected user to be of type string')
} }
@ -315,7 +317,7 @@ const authenticate = (user, pass, userNode) =>
resolve() resolve()
} }
}) })
}) }))
/** /**
* @param {string} publicKey * @param {string} publicKey
@ -347,7 +349,7 @@ const generateHandshakeAddress = async () => {
const address = uuidv1() const address = uuidv1()
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
user.get(Key.CURRENT_HANDSHAKE_ADDRESS).put(address, ack => { user.get(Key.CURRENT_HANDSHAKE_ADDRESS).put(address, ack => {
if (ack.err && typeof ack.err !== 'number') { if (ack.err && typeof ack.err !== 'number') {
rej(new Error(ack.err)) rej(new Error(ack.err))
@ -355,9 +357,9 @@ const generateHandshakeAddress = async () => {
res() res()
} }
}) })
}) }))
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
gun gun
.get(Key.HANDSHAKE_NODES) .get(Key.HANDSHAKE_NODES)
.get(address) .get(address)
@ -368,7 +370,7 @@ const generateHandshakeAddress = async () => {
res() res()
} }
}) })
}) }))
} }
/** /**
@ -385,7 +387,7 @@ const cleanup = async pub => {
const promises = [] const promises = []
promises.push( promises.push(
new Promise((res, rej) => { /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.USER_TO_INCOMING) .get(Key.USER_TO_INCOMING)
.get(pub) .get(pub)
@ -396,11 +398,11 @@ const cleanup = async pub => {
res() res()
} }
}) })
}) }))
) )
promises.push( promises.push(
new Promise((res, rej) => { /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.RECIPIENT_TO_OUTGOING) .get(Key.RECIPIENT_TO_OUTGOING)
.get(pub) .get(pub)
@ -411,11 +413,11 @@ const cleanup = async pub => {
res() res()
} }
}) })
}) }))
) )
promises.push( promises.push(
new Promise((res, rej) => { /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.USER_TO_LAST_REQUEST_SENT) .get(Key.USER_TO_LAST_REQUEST_SENT)
.get(pub) .get(pub)
@ -426,12 +428,12 @@ const cleanup = async pub => {
res() res()
} }
}) })
}) }))
) )
if (outGoingID) { if (outGoingID) {
promises.push( promises.push(
new Promise((res, rej) => { /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.OUTGOINGS) .get(Key.OUTGOINGS)
.get(outGoingID) .get(outGoingID)
@ -442,7 +444,7 @@ const cleanup = async pub => {
res() res()
} }
}) })
}) }))
) )
} }
@ -616,7 +618,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
}) })
}) })
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.USER_TO_LAST_REQUEST_SENT) .get(Key.USER_TO_LAST_REQUEST_SENT)
.get(recipientPublicKey) .get(recipientPublicKey)
@ -627,7 +629,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
res() res()
} }
}) })
}) }))
// This needs to come before the write to sent requests. Because that write // This needs to come before the write to sent requests. Because that write
// triggers Jobs.onAcceptedRequests and it in turn reads from request-to-user // triggers Jobs.onAcceptedRequests and it in turn reads from request-to-user
@ -642,7 +644,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
timestamp timestamp
} }
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
//@ts-ignore //@ts-ignore
user.get(Key.STORED_REQS).set(storedReq, ack => { user.get(Key.STORED_REQS).set(storedReq, ack => {
if (ack.err && typeof ack.err !== 'number') { if (ack.err && typeof ack.err !== 'number') {
@ -655,7 +657,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
res() res()
} }
}) })
}) }))
} }
/** /**
@ -922,6 +924,7 @@ const sendHRWithInitialMsg = async (
* @typedef {object} SpontPaymentOptions * @typedef {object} SpontPaymentOptions
* @prop {Common.Schema.OrderTargetType} type * @prop {Common.Schema.OrderTargetType} type
* @prop {string=} postID * @prop {string=} postID
* @prop {string=} ackInfo
*/ */
/** /**
@ -940,7 +943,7 @@ const sendSpontaneousPayment = async (
amount, amount,
memo, memo,
feeLimit, feeLimit,
opts = { type: 'user' } opts = { type: 'spontaneousPayment' }
) => { ) => {
try { try {
const SEA = require('../Mediator').mySEA const SEA = require('../Mediator').mySEA
@ -965,8 +968,8 @@ const sendSpontaneousPayment = async (
targetType: opts.type targetType: opts.type
} }
if (opts.type === 'post') { if (opts.type === 'tip') {
order.postID = opts.postID order.ackInfo = opts.postID
} }
logger.info(JSON.stringify(order)) logger.info(JSON.stringify(order))
@ -1074,6 +1077,32 @@ const sendSpontaneousPayment = async (
payment_request: orderResponse.response payment_request: orderResponse.response
}) })
await writeCoordinate(payment.payment_hash, {
id: payment.payment_hash,
type: (() => {
if (opts.type === 'tip') {
return 'tip'
} else if (opts.type === 'spontaneousPayment') {
return 'spontaneousPayment'
} else if (opts.type === 'contentReveal') {
return 'other' // TODO
} else if (opts.type === 'other') {
return 'other' // TODO
} else if (opts.type === 'torrentSeed') {
return 'other' // TODO
}
// ensures we handle all possible types
/** @type {never} */
const assertNever = opts.type
return assertNever && opts.type // please TS
})(),
amount: Number(payment.value_sat),
inbound: false,
timestamp: Date.now(),
toLndPub: await myLNDPub()
})
return payment return payment
} catch (e) { } catch (e) {
logger.error('Error inside sendPayment()') logger.error('Error inside sendPayment()')
@ -1125,7 +1154,7 @@ const generateOrderAddress = user =>
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const setBio = (bio, user) => const setBio = (bio, user) =>
new Promise((resolve, reject) => { /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
if (!user.is) { if (!user.is) {
throw new Error(ErrorCode.NOT_AUTH) throw new Error(ErrorCode.NOT_AUTH)
} }
@ -1149,7 +1178,7 @@ const setBio = (bio, user) =>
resolve() resolve()
} }
}) })
}).then( })).then(
() => () =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
user user
@ -1233,7 +1262,7 @@ const disconnect = async pub => {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const setLastSeenApp = () => const setLastSeenApp = () =>
new Promise((res, rej) => { /** @type {Promise<void>} */ (new Promise((res, rej) => {
require('../Mediator') require('../Mediator')
.getUser() .getUser()
.get(Key.LAST_SEEN_APP) .get(Key.LAST_SEEN_APP)
@ -1244,7 +1273,7 @@ const setLastSeenApp = () =>
res() res()
} }
}) })
}).then( })).then(
() => () =>
new Promise((res, rej) => { new Promise((res, rej) => {
require('../Mediator') require('../Mediator')
@ -1268,14 +1297,14 @@ const setLastSeenApp = () =>
* @returns {Promise<[string, Common.Schema.RawPost]>} * @returns {Promise<[string, Common.Schema.RawPost]>}
*/ */
const createPostNew = async (tags, title, content) => { const createPostNew = async (tags, title, content) => {
const SEA = require('../Mediator').mySEA
/** @type {Common.Schema.RawPost} */ /** @type {Common.Schema.RawPost} */
const newPost = { const newPost = {
date: Date.now(), date: Date.now(),
status: 'publish', status: 'publish',
tags: tags.join('-'), tags: tags.join('-'),
title, title,
contentItems: {}, contentItems: {}
tipCounter: 0
} }
content.forEach(c => { content.forEach(c => {
@ -1284,6 +1313,23 @@ const createPostNew = async (tags, title, content) => {
newPost.contentItems[uuid] = c newPost.contentItems[uuid] = c
}) })
const mySecret = require('../Mediator').getMySecret()
await Common.Utils.asyncForEach(content, async c => {
// @ts-expect-error
const uuid = Gun.text.random()
newPost.contentItems[uuid] = c
if (
(c.type === 'image/embedded' || c.type === 'video/embedded') &&
c.isPrivate
) {
const encryptedMagnet = await SEA.encrypt(c.magnetURI, mySecret)
newPost.contentItems[uuid] = { ...c, magnetURI: encryptedMagnet }
} else {
newPost.contentItems[uuid] = c
}
})
/** @type {string} */ /** @type {string} */
const postID = await Common.makePromise((res, rej) => { const postID = await Common.makePromise((res, rej) => {
const _n = require('../Mediator') const _n = require('../Mediator')
@ -1365,7 +1411,7 @@ const createPost = async (tags, title, content) => {
pageIdx = Number(pageIdx + 1).toString() pageIdx = Number(pageIdx + 1).toString()
} }
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
require('../Mediator') require('../Mediator')
.getUser() .getUser()
.get(Key.WALL) .get(Key.WALL)
@ -1386,7 +1432,7 @@ const createPost = async (tags, title, content) => {
res() res()
} }
) )
}) }))
const [postID, newPost] = await createPostNew(tags, title, content) const [postID, newPost] = await createPostNew(tags, title, content)
@ -1412,7 +1458,7 @@ const createPost = async (tags, title, content) => {
}) })
if (shouldBeNewPage || numOfPages === 0) { if (shouldBeNewPage || numOfPages === 0) {
await new Promise(res => { await /** @type {Promise<void>} */ (new Promise(res => {
require('../Mediator') require('../Mediator')
.getUser() .getUser()
.get(Key.WALL) .get(Key.WALL)
@ -1424,7 +1470,7 @@ const createPost = async (tags, title, content) => {
res() res()
}) })
}) }))
} }
const loadedPost = await new Promise(res => { const loadedPost = await new Promise(res => {
@ -1467,7 +1513,7 @@ const createPost = async (tags, title, content) => {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const deletePost = async (postId, page) => { const deletePost = async (postId, page) => {
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
require('../Mediator') require('../Mediator')
.getUser() .getUser()
.get(Key.WALL) .get(Key.WALL)
@ -1482,15 +1528,15 @@ const deletePost = async (postId, page) => {
res() res()
} }
}) })
}) }))
} }
/** /**
* @param {string} publicKey * @param {string} publicKey
* @param {boolean} isPrivate Will overwrite previous private status. * @param {boolean} isPrivate Will overwrite previous private status.
* @returns {Promise<string>} * @returns {Promise<void>}
*/ */
const follow = (publicKey, isPrivate) => { const follow = async (publicKey, isPrivate) => {
/** @type {import('shock-common').Schema.Follow} */ /** @type {import('shock-common').Schema.Follow} */
const newFollow = { const newFollow = {
private: isPrivate, private: isPrivate,
@ -1498,7 +1544,7 @@ const follow = (publicKey, isPrivate) => {
user: publicKey user: publicKey
} }
return new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
require('../Mediator') require('../Mediator')
.getUser() .getUser()
.get(Key.FOLLOWS) .get(Key.FOLLOWS)
@ -1511,7 +1557,7 @@ const follow = (publicKey, isPrivate) => {
res() res()
} }
}) })
}) }))
} }
/** /**
@ -1543,7 +1589,7 @@ const initWall = async () => {
const promises = [] const promises = []
promises.push( promises.push(
new Promise((res, rej) => { /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.WALL) .get(Key.WALL)
.get(Key.NUM_OF_PAGES) .get(Key.NUM_OF_PAGES)
@ -1554,11 +1600,11 @@ const initWall = async () => {
res() res()
} }
}) })
}) }))
) )
promises.push( promises.push(
new Promise((res, rej) => { /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.WALL) .get(Key.WALL)
.get(Key.PAGES) .get(Key.PAGES)
@ -1576,11 +1622,11 @@ const initWall = async () => {
} }
} }
) )
}) }))
) )
promises.push( promises.push(
new Promise((res, rej) => { /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.WALL) .get(Key.WALL)
.get(Key.PAGES) .get(Key.PAGES)
@ -1593,7 +1639,7 @@ const initWall = async () => {
res() res()
} }
}) })
}) }))
) )
await Promise.all(promises) await Promise.all(promises)

View file

@ -92,7 +92,7 @@ const onAcceptedRequests = (user, SEA) => {
const recipientEpub = await Utils.pubToEpub(recipientPub) const recipientEpub = await Utils.pubToEpub(recipientPub)
const ourSecret = await SEA.secret(recipientEpub, user._.sea) const ourSecret = await SEA.secret(recipientEpub, user._.sea)
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
gun gun
.get(Key.HANDSHAKE_NODES) .get(Key.HANDSHAKE_NODES)
.get(requestAddress) .get(requestAddress)
@ -151,7 +151,7 @@ const onAcceptedRequests = (user, SEA) => {
mySecret mySecret
) )
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.USER_TO_INCOMING) .get(Key.USER_TO_INCOMING)
.get(recipientPub) .get(recipientPub)
@ -162,9 +162,9 @@ const onAcceptedRequests = (user, SEA) => {
res() res()
} }
}) })
}) }))
await new Promise((res, rej) => { await /** @type {Promise<void>} */ (new Promise((res, rej) => {
user user
.get(Key.STORED_REQS) .get(Key.STORED_REQS)
.get(id) .get(id)
@ -175,12 +175,12 @@ const onAcceptedRequests = (user, SEA) => {
res() res()
} }
}) })
}) }))
// ensure this listeners gets called at least once // ensure this listeners gets called at least once
res() res()
}) })
}) }))
} catch (err) { } catch (err) {
logger.warn(`Jobs.onAcceptedRequests() -> ${err.message}`) logger.warn(`Jobs.onAcceptedRequests() -> ${err.message}`)
logger.error(err) logger.error(err)

View file

@ -1,21 +1,29 @@
/** /**
* @format * @format
*/ */
// @ts-check
const { performance } = require('perf_hooks')
const logger = require('winston') const logger = require('winston')
const isFinite = require('lodash/isFinite') const isFinite = require('lodash/isFinite')
const isNumber = require('lodash/isNumber') const isNumber = require('lodash/isNumber')
const isNaN = require('lodash/isNaN') const isNaN = require('lodash/isNaN')
const Common = require('shock-common')
const { const {
Constants: { ErrorCode }, Constants: { ErrorCode },
Schema Schema
} = require('shock-common') } = Common
const { assertNever } = require('assert-never')
const crypto = require('crypto')
const fetch = require('node-fetch')
const LightningServices = require('../../../../utils/lightningServices') const LightningServices = require('../../../../utils/lightningServices')
const {
addInvoice,
myLNDPub
} = require('../../../../utils/lightningServices/v2')
const { writeCoordinate } = require('../../../coordinates')
const Key = require('../key') const Key = require('../key')
const Utils = require('../utils') const Utils = require('../utils')
const { gunUUID } = require('../../../../utils')
const getUser = () => require('../../Mediator').getUser() const getUser = () => require('../../Mediator').getUser()
@ -55,28 +63,6 @@ const ordersProcessed = new Set()
let currentOrderAddr = '' let currentOrderAddr = ''
/**
* @param {InvoiceRequest} invoiceReq
* @returns {Promise<InvoiceResponse>}
*/
const _addInvoice = invoiceReq =>
new Promise((resolve, rej) => {
const {
services: { lightning }
} = LightningServices
lightning.addInvoice(invoiceReq, (
/** @type {any} */ error,
/** @type {InvoiceResponse} */ response
) => {
if (error) {
rej(error)
} else {
resolve(response)
}
})
})
/** /**
* @param {string} addr * @param {string} addr
* @param {ISEA} SEA * @param {ISEA} SEA
@ -103,8 +89,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
return return
} }
const listenerStartTime = performance.now()
ordersProcessed.add(orderID) ordersProcessed.add(orderID)
logger.info( logger.info(
@ -113,8 +97,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
)} -- addr: ${addr}` )} -- addr: ${addr}`
) )
const orderAnswerStartTime = performance.now()
const alreadyAnswered = await getUser() const alreadyAnswered = await getUser()
.get(Key.ORDER_TO_RESPONSE) .get(Key.ORDER_TO_RESPONSE)
.get(orderID) .get(orderID)
@ -125,12 +107,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
return return
} }
const orderAnswerEndTime = performance.now() - orderAnswerStartTime
logger.info(`[PERF] Order Already Answered: ${orderAnswerEndTime}ms`)
const decryptStartTime = performance.now()
const senderEpub = await Utils.pubToEpub(order.from) const senderEpub = await Utils.pubToEpub(order.from)
const secret = await SEA.secret(senderEpub, getUser()._.sea) const secret = await SEA.secret(senderEpub, getUser()._.sea)
@ -139,10 +115,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
SEA.decrypt(order.memo, secret) SEA.decrypt(order.memo, secret)
]) ])
const decryptEndTime = performance.now() - decryptStartTime
logger.info(`[PERF] Decrypt invoice info: ${decryptEndTime}ms`)
const amount = Number(decryptedAmount) const amount = Number(decryptedAmount)
if (!isNumber(amount)) { if (!isNumber(amount)) {
@ -174,26 +146,19 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
`onOrders() -> Will now create an invoice : ${JSON.stringify(invoiceReq)}` `onOrders() -> Will now create an invoice : ${JSON.stringify(invoiceReq)}`
) )
const invoiceStartTime = performance.now() const invoice = await addInvoice(
invoiceReq.value,
const invoice = await _addInvoice(invoiceReq) invoiceReq.memo,
true,
const invoiceEndTime = performance.now() - invoiceStartTime invoiceReq.expiry
)
logger.info(`[PERF] LND Invoice created in ${invoiceEndTime}ms`)
logger.info( logger.info(
'onOrders() -> Successfully created the invoice, will now encrypt it' 'onOrders() -> Successfully created the invoice, will now encrypt it'
) )
const invoiceEncryptStartTime = performance.now()
const encInvoice = await SEA.encrypt(invoice.payment_request, secret) const encInvoice = await SEA.encrypt(invoice.payment_request, secret)
const invoiceEncryptEndTime = performance.now() - invoiceEncryptStartTime
logger.info(`[PERF] Invoice encrypted in ${invoiceEncryptEndTime}ms`)
logger.info( logger.info(
`onOrders() -> Will now place the encrypted invoice in order to response usergraph: ${addr}` `onOrders() -> Will now place the encrypted invoice in order to response usergraph: ${addr}`
) )
@ -204,9 +169,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
type: 'invoice' type: 'invoice'
} }
const invoicePutStartTime = performance.now() await /** @type {Promise<void>} */ (new Promise((res, rej) => {
await new Promise((res, rej) => {
getUser() getUser()
.get(Key.ORDER_TO_RESPONSE) .get(Key.ORDER_TO_RESPONSE)
.get(orderID) .get(orderID)
@ -222,34 +185,231 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
res() res()
} }
}) })
}))
// invoices should be settled right away so we can rely on this single
// subscription instead of life-long all invoices subscription
if (order.targetType === 'tip') {
const { ackInfo } = order
if (!Common.isPopulatedString(ackInfo)) {
throw new TypeError(`ackInfo(postID) not a a populated string`)
}
}
// A post tip order lifecycle is short enough that we can do it like this.
const stream = LightningServices.invoices.subscribeSingleInvoice({
r_hash: invoice.r_hash
}) })
const invoicePutEndTime = performance.now() - invoicePutStartTime /** @type {Common.Coordinate} */
const coord = {
logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`) amount,
id: invoice.r_hash.toString(),
const listenerEndTime = performance.now() - listenerStartTime inbound: true,
timestamp: Date.now(),
logger.info(`[PERF] Invoice generation completed in ${listenerEndTime}ms`) type: 'invoice',
invoiceMemo: memo,
const hash = invoice.r_hash.toString('base64') fromGunPub: order.from,
toGunPub: getUser()._.sea.pub,
if (order.targetType === 'post') { toLndPub: await myLNDPub()
/** @type {TipPaymentStatus} */
const paymentStatus = {
hash,
state: 'OPEN',
targetType: order.targetType,
postID: order.postID
} }
if (order.targetType === 'tip') {
coord.type = 'tip'
} else {
coord.type = 'spontaneousPayment'
}
/**
* @param {Common.InvoiceWhenListed} invoice
*/
const onData = async invoice => {
if (invoice.settled) {
writeCoordinate(invoice.r_hash.toString(), coord)
if (order.targetType === 'tip') {
getUser() getUser()
.get(Key.TIPS_PAYMENT_STATUS) .get('postToTipCount')
.get(hash) // CAST: Checked above.
// @ts-ignore .get(/** @type {string} */ (order.ackInfo))
.put(paymentStatus, response => { .set(null) // each item in the set is a tip
console.log(response) } else if (order.targetType === 'contentReveal') {
}) // -----------------------------------------
logger.debug('Content Reveal')
//assuming digital product that only requires to be unlocked
const postID = order.ackInfo
if (!Common.isPopulatedString(postID)) {
logger.error(`Invalid post ID`)
logger.error(postID)
return
} }
// TODO: do this reactively
const selectedPost = await new Promise(res => {
getUser()
.get(Key.POSTS_NEW)
.get(postID)
.load(res)
})
logger.debug(selectedPost)
if (Common.isPost(selectedPost)) {
logger.error('Post id provided does not correspond to a valid post')
return
}
/**
* @type {Record<string,string>} <contentID,decryptedRef>
*/
const contentsToSend = {}
const mySecret = require('../../Mediator').getMySecret()
logger.debug('SECRET OK')
let privateFound = false
await Common.Utils.asyncForEach(
Object.entries(selectedPost.contentItems),
async ([contentID, item]) => {
if (
item.type !== 'image/embedded' &&
item.type !== 'video/embedded'
) {
return //only visual content can be private
}
if (!item.isPrivate) {
return
}
privateFound = true
const decrypted = await SEA.decrypt(item.magnetURI, mySecret)
contentsToSend[contentID] = decrypted
}
)
if (!privateFound) {
logger.error(`Post provided does not contain private content`)
return
}
const ackData = { unlockedContents: contentsToSend }
const toSend = JSON.stringify(ackData)
const encrypted = await SEA.encrypt(toSend, secret)
const ordResponse = {
type: 'orderAck',
response: encrypted
}
logger.debug('RES READY')
const uuid = gunUUID()
orderResponse.ackNode = uuid
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
getUser()
.get(Key.ORDER_TO_RESPONSE)
.get(uuid)
.put(ordResponse, ack => {
if (ack.err && typeof ack.err !== 'number') {
rej(
new Error(
`Error saving encrypted orderAck to order to response usergraph: ${ack}`
)
)
} else {
res()
}
})
}))
logger.debug('RES SENT CONTENT')
// ----------------------------------------------------------------------------------
} else if (order.targetType === 'spontaneousPayment') {
// no action required
} else if (order.targetType === 'torrentSeed') {
logger.debug('TORRENT')
const numberOfTokens = Number(order.ackInfo)
if (isNaN(numberOfTokens)) {
logger.error('ackInfo provided is not a valid number')
return
}
const seedUrl = process.env.TORRENT_SEED_URL
const seedToken = process.env.TORRENT_SEED_TOKEN
if (!seedUrl || !seedToken) {
logger.error('torrentSeed service not available')
return
}
logger.debug('SEED URL OK')
const tokens = Array(numberOfTokens)
for (let i = 0; i < numberOfTokens; i++) {
tokens[i] = crypto.randomBytes(32).toString('hex')
}
/**@param {string} token */
const enrollToken = async token => {
const reqData = {
seed_token: seedToken,
wallet_token: token
}
// @ts-expect-error TODO
const res = await fetch(`${seedUrl}/api/enroll_token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(reqData)
})
if (res.status !== 200) {
throw new Error('torrentSeed service currently not available')
}
}
await Promise.all(tokens.map(enrollToken))
logger.debug('RES SEED OK')
const ackData = { seedUrl, tokens }
const toSend = JSON.stringify(ackData)
const encrypted = await SEA.encrypt(toSend, secret)
const serviceResponse = {
type: 'orderAck',
response: encrypted
}
console.log('RES SEED SENT')
const uuid = gunUUID()
orderResponse.ackNode = uuid
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
getUser()
.get(Key.ORDER_TO_RESPONSE)
.get(uuid)
.put(serviceResponse, ack => {
if (ack.err && typeof ack.err !== 'number') {
rej(
new Error(
`Error saving encrypted orderAck to order to response usergraph: ${ack}`
)
)
} else {
res()
}
})
}))
logger.debug('RES SENT SEED')
} else if (order.targetType === 'other') {
// TODO
} else {
assertNever(order.targetType)
}
stream.off()
}
}
stream.on('data', onData)
stream.on('status', (/** @type {any} */ status) => {
logger.info(`Post tip, post: ${order.ackInfo}, invoice status:`, status)
})
stream.on('end', () => {
logger.warn(`Post tip, post: ${order.ackInfo}, invoice stream ended`)
})
stream.on('error', (/** @type {any} */ e) => {
logger.warn(`Post tip, post: ${order.ackInfo}, error:`, e)
})
} catch (err) { } catch (err) {
logger.error( logger.error(
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify( `error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(

View file

@ -59,8 +59,8 @@ exports.POSTS = 'posts'
// Tips counter for posts // Tips counter for posts
exports.TOTAL_TIPS = 'totalTips' exports.TOTAL_TIPS = 'totalTips'
exports.TIPS_PAYMENT_STATUS = 'tipsPaymentStatus'
exports.PROFILE_BINARY = 'profileBinary' exports.PROFILE_BINARY = 'profileBinary'
exports.POSTS_NEW = 'posts' exports.POSTS_NEW = 'posts'
exports.COORDINATES = 'coordinates'

View file

@ -212,29 +212,19 @@ const tryAndWait = async (promGen, shouldRetry = () => false) => {
*/ */
const pubToEpub = async pub => { const pubToEpub = async pub => {
try { try {
const epub = await tryAndWait(async gun => { const epub = await timeout10(
const _epub = await CommonUtils.makePromise(res => { CommonUtils.makePromise(res => {
gun require('../../Mediator/index')
.getGun()
.user(pub) .user(pub)
.get('epub') .get('epub')
.once( .on(data => {
data => { if (typeof data === 'string') {
res(data) res(data)
},
{
wait: 1000
} }
)
}) })
if (typeof _epub !== 'string') {
throw new TypeError(
`Expected gun.user(pub).get(epub) to be an string. Instead got: ${typeof _epub}`
)
}
return _epub
}) })
)
return epub return epub
} catch (err) { } catch (err) {

View file

@ -1,5 +1,5 @@
const Path = require("path"); const Path = require("path");
const grpc = require("grpc"); const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader"); const protoLoader = require("@grpc/proto-loader");
const logger = require("winston"); const logger = require("winston");
const fs = require("../../utils/fs"); const fs = require("../../utils/fs");
@ -10,6 +10,7 @@ const errorConstants = require("../../constants/errors");
* @typedef LightningConfig * @typedef LightningConfig
* @prop {string} lnrpcProtoPath * @prop {string} lnrpcProtoPath
* @prop {string} routerProtoPath * @prop {string} routerProtoPath
* @prop {string} invoicesProtoPath
* @prop {string} walletUnlockerProtoPath * @prop {string} walletUnlockerProtoPath
* @prop {string} lndHost * @prop {string} lndHost
* @prop {string} lndCertPath * @prop {string} lndCertPath
@ -21,6 +22,7 @@ const errorConstants = require("../../constants/errors");
* @prop {any} lightning * @prop {any} lightning
* @prop {any} walletUnlocker * @prop {any} walletUnlocker
* @prop {any} router * @prop {any} router
* @prop {any} invoices
*/ */
/** /**
@ -30,6 +32,7 @@ const errorConstants = require("../../constants/errors");
module.exports = async ({ module.exports = async ({
lnrpcProtoPath, lnrpcProtoPath,
routerProtoPath, routerProtoPath,
invoicesProtoPath,
walletUnlockerProtoPath, walletUnlockerProtoPath,
lndHost, lndHost,
lndCertPath, lndCertPath,
@ -46,9 +49,15 @@ module.exports = async ({
includeDirs: ["node_modules/google-proto-files", "proto", Path.resolve(__dirname, "../../config")] includeDirs: ["node_modules/google-proto-files", "proto", Path.resolve(__dirname, "../../config")]
} }
const [lnrpcProto, routerProto, walletUnlockerProto] = await Promise.all([protoLoader.load(lnrpcProtoPath, protoLoaderConfig), protoLoader.load(routerProtoPath, protoLoaderConfig), protoLoader.load(walletUnlockerProtoPath, protoLoaderConfig)]); const [lnrpcProto, routerProto, walletUnlockerProto, invoicesProto] = await Promise.all([
protoLoader.load(lnrpcProtoPath, protoLoaderConfig),
protoLoader.load(routerProtoPath, protoLoaderConfig),
protoLoader.load(walletUnlockerProtoPath, protoLoaderConfig),
protoLoader.load(invoicesProtoPath, protoLoaderConfig)
]);
const { lnrpc } = grpc.loadPackageDefinition(lnrpcProto); const { lnrpc } = grpc.loadPackageDefinition(lnrpcProto);
const { routerrpc } = grpc.loadPackageDefinition(routerProto); const { routerrpc } = grpc.loadPackageDefinition(routerProto);
const { invoicesrpc } = grpc.loadPackageDefinition(invoicesProto);
const { lnrpc: walletunlockerrpc } = grpc.loadPackageDefinition(walletUnlockerProto); const { lnrpc: walletunlockerrpc } = grpc.loadPackageDefinition(walletUnlockerProto);
const getCredentials = async () => { const getCredentials = async () => {
@ -93,11 +102,13 @@ module.exports = async ({
const walletUnlocker = new walletunlockerrpc.WalletUnlocker(lndHost, credentials); const walletUnlocker = new walletunlockerrpc.WalletUnlocker(lndHost, credentials);
// @ts-ignore // @ts-ignore
const router = new routerrpc.Router(lndHost, credentials); const router = new routerrpc.Router(lndHost, credentials);
// @ts-ignore
const invoices = new invoicesrpc.Invoices(lndHost, credentials);
return { return {
lightning, lightning,
walletUnlocker, walletUnlocker,
router router,
invoices
}; };
} }

View file

@ -1,80 +0,0 @@
// app/lnd.js
const logger = require("winston");
// TODO
module.exports = function(lightning) {
const module = {};
const invoiceListeners = [];
let lndInvoicesStream = null;
const openLndInvoicesStream = function() {
if (lndInvoicesStream) {
logger.debug("Lnd invoices subscription stream already opened.");
} else {
logger.debug("Opening lnd invoices subscription stream...");
lndInvoicesStream = lightning.subscribeInvoices({});
logger.debug("Lnd invoices subscription stream opened.");
lndInvoicesStream.on("data", function(data) {
logger.debug("SubscribeInvoices Data", data);
for (let i = 0; i < invoiceListeners.length; i++) {
try {
invoiceListeners[i].dataReceived(data);
} catch (err) {
logger.warn(err);
}
}
});
lndInvoicesStream.on("end", function() {
logger.debug("SubscribeInvoices End");
lndInvoicesStream = null;
openLndInvoicesStream(); // try opening stream again
});
lndInvoicesStream.on("error", function(err) {
logger.debug("SubscribeInvoices Error", err);
});
lndInvoicesStream.on("status", function(status) {
logger.debug("SubscribeInvoices Status", status);
if (status.code == 14) {
// Unavailable
lndInvoicesStream = null;
openLndInvoicesStream(); // try opening stream again
}
});
}
};
// register invoice listener
module.registerInvoiceListener = function(listener) {
invoiceListeners.push(listener);
logger.debug(
"New lnd invoice listener registered, " +
invoiceListeners.length +
" listening now"
);
};
// unregister invoice listener
module.unregisterInvoiceListener = function(listener) {
invoiceListeners.splice(invoiceListeners.indexOf(listener), 1);
logger.debug(
"Lnd invoice listener unregistered, " +
invoiceListeners.length +
" still listening"
);
};
// open lnd invoices stream on start
openLndInvoicesStream();
// check every minute that lnd invoices stream is still opened
setInterval(function() {
if (!lndInvoicesStream) {
openLndInvoicesStream();
}
}, 60 * 1000);
return module;
};

109
src/__gun__tests__/mySea.ts Normal file
View file

@ -0,0 +1,109 @@
/**
* @format
*/
import Gun from 'gun'
import uuid from 'uuid/v1'
import { mySEA } from '../../services/gunDB/Mediator'
import { UserGUNNode } from '../../services/gunDB/contact-api/SimpleGUN'
const setupUser = async (): Promise<[UserGUNNode]> => {
const gun = Gun({
file: 'GUN-TEST-' + uuid()
})
const user = (gun.user() as unknown) as UserGUNNode
await new Promise<void>((res, rej) => {
user.create('testAlias-' + uuid(), 'testPass', ack => {
if (typeof ack.err === 'string') {
rej(new Error(ack.err))
} else {
res()
}
})
})
return [user]
}
const encryptsDecryptsStrings = async () => {
const [user] = await setupUser()
const stringMessage = 'Lorem ipsum dolor'
const sec = await mySEA.secret(user._.sea.epub, user._.sea)
const encrypted = await mySEA.encrypt(stringMessage, sec)
const decrypted = await mySEA.decrypt(encrypted, sec)
if (decrypted !== stringMessage) {
throw new Error()
}
}
const encryptsDecryptsBooleans = async () => {
const [user] = await setupUser()
const truth = true
const lie = false
const sec = await mySEA.secret(user._.sea.epub, user._.sea)
const encryptedTruth = await mySEA.encrypt(truth, sec)
const decryptedTruth = await mySEA.decryptBoolean(encryptedTruth, sec)
if (decryptedTruth !== truth) {
throw new Error()
}
const encryptedLie = await mySEA.encrypt(lie, sec)
const decryptedLie = await mySEA.decryptBoolean(encryptedLie, sec)
if (decryptedLie !== lie) {
throw new Error(
`Expected false got: ${decryptedLie} - ${typeof decryptedLie}`
)
}
}
const encryptsDecryptsNumbers = async () => {
const [user] = await setupUser()
const number = Math.random() * 999999
const sec = await mySEA.secret(user._.sea.epub, user._.sea)
const encrypted = await mySEA.encrypt(number, sec)
const decrypted = await mySEA.decryptNumber(encrypted, sec)
if (decrypted !== number) {
throw new Error()
}
}
const encryptsDecryptsZero = async () => {
const [user] = await setupUser()
const zero = 0
const sec = await mySEA.secret(user._.sea.epub, user._.sea)
const encrypted = await mySEA.encrypt(zero, sec)
const decrypted = await mySEA.decryptNumber(encrypted, sec)
if (decrypted !== zero) {
throw new Error()
}
}
const runAllTests = async () => {
await encryptsDecryptsStrings()
await encryptsDecryptsBooleans()
await encryptsDecryptsNumbers()
await encryptsDecryptsZero()
console.log('\n--------------------------------')
console.log('All tests ran successfully')
console.log('--------------------------------\n')
process.exit(0)
}
runAllTests()

View file

@ -30,12 +30,7 @@ const {
const GunActions = require('../services/gunDB/contact-api/actions') const GunActions = require('../services/gunDB/contact-api/actions')
const GunGetters = require('../services/gunDB/contact-api/getters') const GunGetters = require('../services/gunDB/contact-api/getters')
const GunKey = require('../services/gunDB/contact-api/key') const GunKey = require('../services/gunDB/contact-api/key')
const { const LV2 = require('../utils/lightningServices/v2')
sendPaymentV2Keysend,
sendPaymentV2Invoice,
listPayments
} = require('../utils/lightningServices/v2')
const { startTipStatusJob } = require('../utils/lndJobs')
const GunWriteRPC = require('../services/gunDB/rpc') const GunWriteRPC = require('../services/gunDB/rpc')
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10 const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
@ -690,7 +685,6 @@ module.exports = async (
} }
onNewChannelBackup() onNewChannelBackup()
startTipStatusJob()
res.json({ res.json({
authorization: token, authorization: token,
@ -1026,30 +1020,15 @@ module.exports = async (
) )
}) })
// get lnd chan info // get lnd chan info
app.post('/api/lnd/getchaninfo', (req, res) => { app.post('/api/lnd/getchaninfo', async (req, res) => {
const { lightning } = LightningServices.services try {
return res.json(await LV2.getChanInfo(req.body.chan_id))
lightning.getChanInfo( } catch (e) {
{ chan_id: req.body.chan_id }, console.log(e)
async (err, response) => { return res.status(500).json({
if (err) { errorMessage: e.message
logger.debug('GetChanInfo Error:', err)
const health = await checkHealth()
if (health.LNDStatus.success) {
res.status(400)
res.json({
field: 'getChanInfo',
errorMessage: sanitizeLNDError(err.message)
}) })
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
} }
}
logger.debug('GetChanInfo:', response)
res.json(response)
}
)
}) })
app.get('/api/lnd/getnetworkinfo', (req, res) => { app.get('/api/lnd/getnetworkinfo', (req, res) => {
@ -1074,47 +1053,30 @@ module.exports = async (
}) })
// get lnd node active channels list // get lnd node active channels list
app.get('/api/lnd/listpeers', (req, res) => { app.get('/api/lnd/listpeers', async (req, res) => {
const { lightning } = LightningServices.services try {
lightning.listPeers({}, async (err, response) => { return res.json({
if (err) { peers: await LV2.listPeers(req.body.latestError)
logger.debug('ListPeers Error:', err)
const health = await checkHealth()
if (health.LNDStatus.success) {
res.status(400).json({
field: 'listPeers',
errorMessage: sanitizeLNDError(err.message)
}) })
} else { } catch (e) {
res.status(500) console.log(e)
res.json({ errorMessage: 'LND is down' }) return res.status(500).json({
} errorMessage: e.message
}
logger.debug('ListPeers:', response)
res.json(response)
}) })
}
}) })
// newaddress // newaddress
app.post('/api/lnd/newaddress', (req, res) => { app.post('/api/lnd/newaddress', async (req, res) => {
const { lightning } = LightningServices.services try {
lightning.newAddress({ type: req.body.type }, async (err, response) => { return res.json({
if (err) { address: await LV2.newAddress(req.body.type)
logger.debug('NewAddress Error:', err)
const health = await checkHealth()
if (health.LNDStatus.success) {
res.status(400).json({
field: 'newAddress',
errorMessage: sanitizeLNDError(err.message)
}) })
} else { } catch (e) {
res.status(500) return res.status(500).json({
res.json({ errorMessage: 'LND is down' }) errorMessage: e.message
}
}
logger.debug('NewAddress:', response)
res.json(response)
}) })
}
}) })
// connect peer to lnd node // connect peer to lnd node
@ -1159,47 +1121,28 @@ module.exports = async (
}) })
// get lnd node opened channels list // get lnd node opened channels list
app.get('/api/lnd/listchannels', (req, res) => { app.get('/api/lnd/listchannels', async (_, res) => {
const { lightning } = LightningServices.services try {
lightning.listChannels({}, async (err, response) => { return res.json({
if (err) { channels: await LV2.listChannels()
logger.debug('ListChannels Error:', err)
const health = await checkHealth()
if (health.LNDStatus.success) {
res.status(400).json({
field: 'listChannels',
errorMessage: sanitizeLNDError(err.message)
}) })
} else { } catch (e) {
res.status(500) console.log(e)
res.json({ errorMessage: 'LND is down' }) return res.status(500).json({
} errorMessage: e.message
}
logger.debug('ListChannels:', response)
res.json(response)
}) })
}
}) })
// get lnd node pending channels list app.get('/api/lnd/pendingchannels', async (req, res) => {
app.get('/api/lnd/pendingchannels', (req, res) => { try {
const { lightning } = LightningServices.services return res.json(await LV2.pendingChannels())
lightning.pendingChannels({}, async (err, response) => { } catch (e) {
if (err) { console.log(e)
logger.debug('PendingChannels Error:', err) return res.status(500).json({
const health = await checkHealth() errorMessage: e.message
if (health.LNDStatus.success) {
res.status(400).json({
field: 'pendingChannels',
errorMessage: sanitizeLNDError(err.message)
}) })
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
} }
}
logger.debug('PendingChannels:', response)
res.json(response)
})
}) })
app.get('/api/lnd/unifiedTrx', (req, res) => { app.get('/api/lnd/unifiedTrx', (req, res) => {
@ -1249,12 +1192,35 @@ module.exports = async (
app.post('/api/lnd/unifiedTrx', async (req, res) => { app.post('/api/lnd/unifiedTrx', async (req, res) => {
try { try {
const { type, amt, to, memo, feeLimit, postID } = req.body const { type, amt, to, memo, feeLimit, postID, ackInfo } = req.body
if (type !== 'spont' && type !== 'post') { if (
type !== 'spont' &&
type !== 'post' &&
type !== 'spontaneousPayment' &&
type !== 'tip' &&
type !== 'torrentSeed' &&
type !== 'contentReveal' &&
type !== 'other'
) {
return res.status(415).json({ return res.status(415).json({
field: 'type', field: 'type',
errorMessage: `Only 'spont' and 'post' payments supported via this endpoint for now.` errorMessage: `Only 'spontaneousPayment'| 'tip' | 'torrentSeed' | 'contentReveal' | 'other' payments supported via this endpoint for now.`
})
}
const typesThatShouldContainAckInfo = [
'tip',
'torrentSeed',
'contentReveal'
]
const shouldContainAckInfo = typesThatShouldContainAckInfo.includes(type)
if (shouldContainAckInfo && !Common.isPopulatedString(ackInfo)) {
return res.status(400).json({
field: 'ackInfo',
errorMessage: `Transactions of type ${typesThatShouldContainAckInfo} should contain an ackInfo field.`
}) })
} }
@ -1298,7 +1264,8 @@ module.exports = async (
return res.status(200).json( return res.status(200).json(
await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit, { await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit, {
type, type,
postID postID,
ackInfo
}) })
) )
} catch (e) { } catch (e) {
@ -1377,7 +1344,7 @@ module.exports = async (
} }
return res.status(200).json( return res.status(200).json(
await listPayments({ await LV2.listPayments({
include_incomplete, include_incomplete,
index_offset, index_offset,
max_payments, max_payments,
@ -1664,7 +1631,7 @@ module.exports = async (
}) })
} }
const payment = await sendPaymentV2Keysend({ const payment = await LV2.sendPaymentV2Keysend({
amt, amt,
dest, dest,
feeLimit, feeLimit,
@ -1677,7 +1644,7 @@ module.exports = async (
} }
const { payreq } = req.body const { payreq } = req.body
const payment = await sendPaymentV2Invoice({ const payment = await LV2.sendPaymentV2Invoice({
feeLimit, feeLimit,
payment_request: payreq, payment_request: payreq,
amt: req.body.amt, amt: req.body.amt,
@ -1766,49 +1733,12 @@ module.exports = async (
}) })
// addinvoice // addinvoice
app.post('/api/lnd/addinvoice', (req, res) => { app.post('/api/lnd/addinvoice', async (req, res) => {
const { lightning } = LightningServices.services const { expiry, value, memo } = req.body
const invoiceRequest = { memo: req.body.memo, private: true } const addInvoiceRes = await LV2.addInvoice(value, memo, true, expiry)
if (req.body.value) {
invoiceRequest.value = req.body.value if (value) {
} const channelsList = await LV2.listChannels({ active_only: true })
if (req.body.expiry) {
invoiceRequest.expiry = req.body.expiry
}
lightning.addInvoice(invoiceRequest, async (err, newInvoice) => {
if (err) {
logger.debug('AddInvoice Error:', err)
const health = await checkHealth()
if (health.LNDStatus.success) {
res.status(400).json({
field: 'addInvoice',
errorMessage: sanitizeLNDError(err.message)
})
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
}
return err
}
logger.debug('AddInvoice:', newInvoice)
if (req.body.value) {
logger.debug('AddInvoice liquidity check:')
lightning.listChannels({ active_only: true }, async (err, response) => {
if (err) {
logger.debug('ListChannels Error:', err)
const health = await checkHealth()
if (health.LNDStatus.success) {
res.status(400).json({
field: 'listChannels',
errorMessage: sanitizeLNDError(err.message)
})
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
}
}
logger.debug('ListChannels:', response)
const channelsList = response.channels
let remoteBalance = Big(0) let remoteBalance = Big(0)
channelsList.forEach(element => { channelsList.forEach(element => {
const remB = Big(element.remote_balance) const remB = Big(element.remote_balance)
@ -1816,14 +1746,19 @@ module.exports = async (
remoteBalance = remB remoteBalance = remB
} }
}) })
newInvoice.liquidityCheck = remoteBalance > req.body.value
addInvoiceRes.liquidityCheck = remoteBalance > value
//newInvoice.remoteBalance = remoteBalance //newInvoice.remoteBalance = remoteBalance
res.json(newInvoice)
})
} else {
res.json(newInvoice)
} }
try {
return res.json(addInvoiceRes)
} catch (e) {
console.log(e)
return res.status(500).json({
errorMessage: e.message
}) })
}
}) })
// signmessage // signmessage
@ -1882,7 +1817,8 @@ module.exports = async (
const sendCoinsRequest = { const sendCoinsRequest = {
addr: req.body.addr, addr: req.body.addr,
amount: req.body.amount, amount: req.body.amount,
sat_per_byte: req.body.satPerByte sat_per_byte: req.body.satPerByte,
send_all: req.body.send_all === true
} }
logger.debug('SendCoins', sendCoinsRequest) logger.debug('SendCoins', sendCoinsRequest)
lightning.sendCoins(sendCoinsRequest, async (err, response) => { lightning.sendCoins(sendCoinsRequest, async (err, response) => {
@ -1960,23 +1896,25 @@ module.exports = async (
) )
}) })
app.post('/api/lnd/listunspent', (req, res) => { const listunspent = async (req, res) => {
const { lightning } = LightningServices.services try {
const { minConfirmations = 3, maxConfirmations = 6 } = req.body return res.status(200).json({
lightning.listUnspent( utxos: await LV2.listUnspent(
{ req.body.minConfirmations,
min_confs: minConfirmations, req.body.maxConfirmations
max_confs: maxConfirmations
},
(err, unspent) => {
if (err) {
return handleError(res, err)
}
logger.debug('ListUnspent:', unspent)
res.json(unspent)
}
) )
}) })
} catch (e) {
return res.status(500).json({
errorMessage: e.message
})
}
}
app.get('/api/lnd/listunspent', listunspent)
// TODO: should be GET
app.post('/api/lnd/listunspent', listunspent)
app.get('/api/lnd/transactions', (req, res) => { app.get('/api/lnd/transactions', (req, res) => {
const { lightning } = LightningServices.services const { lightning } = LightningServices.services

View file

@ -167,10 +167,6 @@ const server = program => {
await LightningServices.init() await LightningServices.init()
} }
// init lnd module =================
const lnd = require('../services/lnd/lnd')(
LightningServices.services.lightning
)
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
LightningServices.services.lightning.getInfo({}, (err, res) => { LightningServices.services.lightning.getInfo({}, (err, res) => {
if (err && err.code !== 12) { if (err && err.code !== 12) {

View file

@ -66,66 +66,6 @@ module.exports = (
} }
} }
const parseJSON = data => {
try {
if (typeof data === 'string') {
return JSON.parse(data)
}
return data
} catch (err) {
return data
}
}
const decryptEvent = ({ eventName, data, socket }) => {
try {
const deviceId = socket.handshake.query['x-shockwallet-device-id']
if (Encryption.isNonEncrypted(eventName)) {
return data
}
if (!data) {
return data
}
const parsedData = parseJSON(data)
if (!deviceId) {
throw {
field: 'deviceId',
message: 'Please specify a device ID'
}
}
if (!Encryption.isAuthorizedDevice({ deviceId })) {
throw {
field: 'deviceId',
message: 'Please exchange keys with the API before using the socket'
}
}
const decryptedKey = Encryption.decryptKey({
deviceId,
message: parsedData.encryptedKey
})
const decryptedMessage = Encryption.decryptMessage({
message: parsedData.encryptedData,
key: decryptedKey,
iv: parsedData.iv
})
const decryptedData = JSON.parse(decryptedMessage)
return decryptedData
} catch (err) {
logger.error(
`[SOCKET] An error has occurred while decrypting an event (${eventName}):`,
err
)
return socket.emit('encryption:error', err)
}
}
const onNewInvoice = (socket, subID) => { const onNewInvoice = (socket, subID) => {
const { lightning } = LightningServices.services const { lightning } = LightningServices.services
logger.warn('Subscribing to invoices socket...' + subID) logger.warn('Subscribing to invoices socket...' + subID)
@ -677,7 +617,7 @@ module.exports = (
} }
/** /**
* @param {Common.Schema.SimpleReceivedRequest[]} receivedReqs * @param {ReadonlyArray<Common.SimpleReceivedRequest>} receivedReqs
*/ */
const onReceivedReqs = receivedReqs => { const onReceivedReqs = receivedReqs => {
const processed = receivedReqs.map(({ id, requestorPK, timestamp }) => { const processed = receivedReqs.map(({ id, requestorPK, timestamp }) => {

View file

@ -1,9 +1,21 @@
/** /**
* @format * @format
*/ */
const Gun = require('gun')
const { asyncFilter } = require('./helpers') const { asyncFilter } = require('./helpers')
module.exports = { /**
asyncFilter * @returns {string}
*/
const gunUUID = () => {
// @ts-expect-error Not typed
const uuid = Gun.Text.random()
return uuid
}
module.exports = {
asyncFilter,
gunUUID
} }

View file

@ -18,6 +18,7 @@ const lnrpc = require('../../services/lnd/lightning')
* @prop {string} macaroonPath * @prop {string} macaroonPath
* @prop {string} lndProto * @prop {string} lndProto
* @prop {string} routerProto * @prop {string} routerProto
* @prop {string} invoicesProto
* @prop {string} walletUnlockerProto * @prop {string} walletUnlockerProto
*/ */
@ -73,6 +74,13 @@ class LightningServices {
} }
} }
/**
* @returns {import('./types').Services}
*/
getServices() {
return this.services
}
get servicesData() { get servicesData() {
return this.lnServicesData return this.lnServicesData
} }
@ -119,6 +127,7 @@ class LightningServices {
const lnServices = await lnrpc({ const lnServices = await lnrpc({
lnrpcProtoPath: this.defaults.lndProto, lnrpcProtoPath: this.defaults.lndProto,
routerProtoPath: this.defaults.routerProto, routerProtoPath: this.defaults.routerProto,
invoicesProtoPath: this.defaults.invoicesProto,
walletUnlockerProtoPath: this.defaults.walletUnlockerProto, walletUnlockerProtoPath: this.defaults.walletUnlockerProto,
lndHost, lndHost,
lndCertPath, lndCertPath,
@ -127,10 +136,11 @@ class LightningServices {
if (!lnServices) { if (!lnServices) {
throw new Error(`Could not init lnServices`) throw new Error(`Could not init lnServices`)
} }
const { lightning, walletUnlocker, router } = lnServices const { lightning, walletUnlocker, router, invoices } = lnServices
this.lightning = lightning this.lightning = lightning
this.walletUnlocker = walletUnlocker this.walletUnlocker = walletUnlocker
this.router = router this.router = router
this.invoices = invoices
this.lnServicesData = { this.lnServicesData = {
lndProto: this.defaults.lndProto, lndProto: this.defaults.lndProto,
lndHost, lndHost,

View file

@ -1,6 +1,7 @@
/** /**
* @format * @format
*/ */
import * as Common from 'shock-common'
export interface PaymentV2 { export interface PaymentV2 {
payment_hash: string payment_hash: string
@ -106,3 +107,90 @@ export interface SendPaymentInvoiceParams {
payment_request: string payment_request: string
timeoutSeconds?: number timeoutSeconds?: number
} }
type StreamListener = (data: any) => void
/**
* Caution: Not all methods return an stream.
*/
interface LightningStream {
on(ev: 'data' | 'end' | 'error' | 'status', listener: StreamListener): void
}
type LightningCB = (err: Error, data: Record<string, any>) => void
type LightningMethod = (
args: Record<string, any>,
cb?: LightningCB
) => LightningStream
/**
* Makes it easier for code calling services.
*/
export interface Services {
lightning: Record<string, LightningMethod>
walletUnlocker: Record<string, LightningMethod>
router: Record<string, LightningMethod>
}
export interface ListChannelsReq {
active_only: boolean
inactive_only: boolean
public_only: boolean
private_only: boolean
/**
* Filters the response for channels with a target peer's pubkey. If peer is
* empty, all channels will be returned.
*/
peer: Common.Bytes
}
/**
* https://api.lightning.community/#pendingchannels
*/
export interface PendingChannelsRes {
/**
* The balance in satoshis encumbered in pending channels.
*/
total_limbo_balance: string
/**
* Channels pending opening.
*/
pending_open_channels: Common.PendingOpenChannel[]
/**
* Channels pending force closing.
*/
pending_force_closing_channels: Common.ForceClosedChannel[]
/**
* Channels waiting for closing tx to confirm.
*/
waiting_close_channels: Common.WaitingCloseChannel[]
}
/**
* https://github.com/lightningnetwork/lnd/blob/daf7c8a85420fc67fffa18fa5f7d08c2040946e4/lnrpc/rpc.proto#L2948
*/
export interface AddInvoiceRes {
/**
*
*/
r_hash: Common.Bytes
/**
* A bare-bones invoice for a payment within the Lightning Network. With the
* details of the invoice, the sender has all the data necessary to send a
* payment to the recipient.
*/
payment_request: string
/**
* The "add" index of this invoice. Each newly created invoice will increment
* this index making it monotonically increasing. Callers to the
* SubscribeInvoices call can use this to instantly get notified of all added
* invoices with an add_index greater than this one.
*/
add_index: string
/**
* The payment address of the generated invoice. This value should be used in
* all payments for this invoice as we require it for end to end security.
*/
payment_addr: Common.Bytes
}

View file

@ -6,6 +6,8 @@ const logger = require('winston')
const Common = require('shock-common') const Common = require('shock-common')
const Ramda = require('ramda') const Ramda = require('ramda')
const { writeCoordinate } = require('../../services/coordinates')
const lightningServices = require('./lightning-services') const lightningServices = require('./lightning-services')
/** /**
* @typedef {import('./types').PaymentV2} PaymentV2 * @typedef {import('./types').PaymentV2} PaymentV2
@ -213,12 +215,50 @@ const isValidSendPaymentInvoiceParams = sendPaymentInvoiceParams => {
return true return true
} }
/**
* @param {string} payReq
* @returns {Promise<Common.Schema.InvoiceWhenDecoded>}
*/
const decodePayReq = payReq =>
Common.Utils.makePromise((res, rej) => {
lightningServices.lightning.decodePayReq(
{ pay_req: payReq },
/**
* @param {{ message: any; }} err
* @param {any} paymentRequest
*/
(err, paymentRequest) => {
if (err) {
rej(new Error(err.message))
} else {
res(paymentRequest)
}
}
)
})
/**
* @returns {Promise<string>}
*/
const myLNDPub = () =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.getInfo({}, (err, data) => {
if (err) {
rej(new Error(err.message))
} else {
res(data.identity_pubkey)
}
})
})
/** /**
* aklssjdklasd * aklssjdklasd
* @param {SendPaymentV2Request} sendPaymentRequest * @param {SendPaymentV2Request} sendPaymentRequest
* @returns {Promise<PaymentV2>} * @returns {Promise<PaymentV2>}
*/ */
const sendPaymentV2 = sendPaymentRequest => { const sendPaymentV2 = async sendPaymentRequest => {
const { const {
services: { router } services: { router }
} = lightningServices } = lightningServices
@ -229,7 +269,10 @@ const sendPaymentV2 = sendPaymentRequest => {
) )
} }
return new Promise((res, rej) => { /**
* @type {import("./types").PaymentV2}
*/
const paymentV2 = await Common.makePromise((res, rej) => {
const stream = router.sendPaymentV2(sendPaymentRequest) const stream = router.sendPaymentV2(sendPaymentRequest)
stream.on( stream.on(
@ -268,6 +311,33 @@ const sendPaymentV2 = sendPaymentRequest => {
} }
) )
}) })
/** @type {Common.Coordinate} */
const coord = {
amount: Number(paymentV2.value_sat),
id: paymentV2.payment_hash,
inbound: false,
timestamp: Date.now(),
toLndPub: await myLNDPub(),
fromLndPub: undefined,
invoiceMemo: undefined,
type: 'payment'
}
if (sendPaymentRequest.payment_request) {
const invoice = await decodePayReq(sendPaymentRequest.payment_request)
coord.invoiceMemo = invoice.description
coord.toLndPub = invoice.destination
}
if (sendPaymentRequest.dest) {
coord.toLndPub = sendPaymentRequest.dest.toString('base64')
}
await writeCoordinate(paymentV2.payment_hash, coord)
return paymentV2
} }
/** /**
@ -381,22 +451,169 @@ const listPayments = req => {
} }
/** /**
* @param {string} payReq * @param {0|1} type
* @returns {Promise<Common.Schema.InvoiceWhenDecoded>} * @returns {Promise<string>}
*/ */
const decodePayReq = payReq => const newAddress = (type = 0) => {
Common.Utils.makePromise((res, rej) => { const { lightning } = lightningServices.getServices()
lightningServices.lightning.decodePayReq(
{ pay_req: payReq }, return Common.Utils.makePromise((res, rej) => {
/** lightning.newAddress({ type }, (err, response) => {
* @param {{ message: any; }} err
* @param {any} paymentRequest
*/
(err, paymentRequest) => {
if (err) { if (err) {
rej(new Error(err.message)) rej(new Error(err.message))
} else { } else {
res(paymentRequest) res(response.address)
}
})
})
}
/**
* @param {number} minConfs
* @param {number} maxConfs
* @returns {Promise<Common.Utxo[]>}
*/
const listUnspent = (minConfs = 3, maxConfs = 6) =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.listUnspent(
{
min_confs: minConfs,
max_confs: maxConfs
},
(err, unspent) => {
if (err) {
rej(new Error(err.message))
} else {
res(unspent.utxos)
}
}
)
})
/**
* @typedef {import('./types').ListChannelsReq} ListChannelsReq
*/
/**
* @param {ListChannelsReq} req
* @returns {Promise<Common.Channel[]>}
*/
const listChannels = req =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.listChannels(req, (err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
res(resp.channels)
}
})
})
/**
* https://api.lightning.community/#getchaninfo
* @param {string} chanID
* @returns {Promise<Common.ChannelEdge>}
*/
const getChanInfo = chanID =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.getChanInfo(
{
chan_id: chanID
},
(err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
// Needs cast because typescript refuses to assign Record<string, any>
// to an actual object :shrugs
res(/** @type {Common.ChannelEdge} */ (resp))
}
}
)
})
/**
* https://api.lightning.community/#listpeers
* @param {boolean=} latestError If true, only the last error that our peer sent
* us will be returned with the peer's information, rather than the full set of
* historic errors we have stored.
* @returns {Promise<Common.Peer[]>}
*/
const listPeers = latestError =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.listPeers(
{
latest_error: latestError
},
(err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
res(resp.peers)
}
}
)
})
/**
* @typedef {import('./types').PendingChannelsRes} PendingChannelsRes
*/
/**
* @returns {Promise<PendingChannelsRes>}
*/
const pendingChannels = () =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.pendingChannels({}, (err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
// Needs cast because typescript refuses to assign Record<string, any>
// to an actual object :shrugs
res(/** @type {PendingChannelsRes} */ (resp))
}
})
})
/**
* @typedef {import('./types').AddInvoiceRes} AddInvoiceRes
*/
/**
* https://api.lightning.community/#addinvoice
* @param {number} value
* @param {string=} memo
* @param {boolean=} confidential Alias for `private`.
* @param {number=} expiry
* @returns {Promise<AddInvoiceRes>}
*/
const addInvoice = (value, memo = '', confidential = true, expiry = 180) =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.addInvoice(
{
value,
memo,
private: confidential,
expiry
},
(err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
// Needs cast because typescript refuses to assign Record<string, any>
// to an actual object :shrugs
res(/** @type {AddInvoiceRes} */ (resp))
} }
} }
) )
@ -406,5 +623,13 @@ module.exports = {
sendPaymentV2Keysend, sendPaymentV2Keysend,
sendPaymentV2Invoice, sendPaymentV2Invoice,
listPayments, listPayments,
decodePayReq decodePayReq,
newAddress,
listUnspent,
listChannels,
getChanInfo,
listPeers,
pendingChannels,
addInvoice,
myLNDPub
} }

View file

@ -1,211 +0,0 @@
/**
* @prettier
*/
const Logger = require('winston')
const { wait } = require('./helpers')
const Key = require('../services/gunDB/contact-api/key')
const { getUser } = require('../services/gunDB/Mediator')
const LightningServices = require('./lightningServices')
const ERROR_TRIES_THRESHOLD = 3
const ERROR_TRIES_DELAY = 500
const INVOICE_STATE = {
OPEN: 'OPEN',
SETTLED: 'SETTLED',
CANCELLED: 'CANCELLED',
ACCEPTED: 'ACCEPTED'
}
const _lookupInvoice = hash =>
new Promise((resolve, reject) => {
const { lightning } = LightningServices.services
lightning.lookupInvoice({ r_hash: hash }, (err, response) => {
if (err) {
Logger.error(
'[TIP] An error has occurred while trying to lookup invoice:',
err,
'\nInvoice Hash:',
hash
)
reject(err)
return
}
Logger.info('[TIP] Invoice lookup result:', response)
resolve(response)
})
})
const _getPostTipInfo = ({ postID }) =>
new Promise((resolve, reject) => {
getUser()
.get(Key.POSTS_NEW)
.get(postID)
.once(post => {
if (post && post.date) {
const { tipCounter, tipValue } = post
resolve({
tipCounter: typeof tipCounter === 'number' ? tipCounter : 0,
tipValue: typeof tipValue === 'number' ? tipValue : 0
})
return
}
resolve(post)
})
})
const _incrementPost = ({ postID, orderAmount }) =>
new Promise((resolve, reject) => {
const parsedAmount = parseFloat(orderAmount)
if (typeof parsedAmount !== 'number') {
reject(new Error('Invalid order amount specified'))
return
}
Logger.info('[POST TIP] Getting Post Tip Values...')
return _getPostTipInfo({ postID })
.then(({ tipValue, tipCounter }) => {
const updatedTip = {
tipCounter: tipCounter + 1,
tipValue: tipValue + parsedAmount
}
getUser()
.get(Key.POSTS_NEW)
.get(postID)
.put(updatedTip, () => {
Logger.info('[POST TIP] Successfully updated Post tip info')
resolve(updatedTip)
})
})
.catch(err => {
Logger.error(err)
reject(err)
})
})
const _updateTipData = (invoiceHash, data) =>
new Promise((resolve, reject) => {
try {
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.get(invoiceHash)
.put(data, tip => {
if (tip === undefined) {
reject(new Error('Tip update failed'))
return
}
console.log(tip)
resolve(tip)
})
} catch (err) {
Logger.error('An error has occurred while updating tip^data')
throw err
}
})
const _getTipData = (invoiceHash, tries = 0) =>
new Promise((resolve, reject) => {
if (tries >= ERROR_TRIES_THRESHOLD) {
reject(new Error('Malformed data'))
return
}
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.get(invoiceHash)
.once(async tip => {
try {
if (tip === undefined) {
await wait(ERROR_TRIES_DELAY)
const tip = await _getTipData(invoiceHash, tries + 1)
if (tip) {
resolve(tip)
return
}
reject(new Error('Malformed data'))
return
}
resolve(tip)
} catch (err) {
reject(err)
}
})
})
const executeTipAction = (tip, invoice) => {
if (invoice.state !== INVOICE_STATE.SETTLED) {
return
}
// Execute actions once invoice is settled
Logger.info('Invoice settled!', invoice)
if (tip.targetType === 'post') {
_incrementPost({
postID: tip.postID,
orderAmount: invoice.amt_paid_sat
})
}
}
const updateUnverifiedTips = () => {
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.map()
.once(async (tip, id) => {
try {
if (
!tip ||
tip.state !== INVOICE_STATE.OPEN ||
(tip._errorCount && tip._errorCount >= ERROR_TRIES_THRESHOLD)
) {
return
}
Logger.info('Unverified invoice found!', tip)
const invoice = await _lookupInvoice(tip.hash)
Logger.info('Invoice located:', invoice)
if (invoice.state !== tip.state) {
await _updateTipData(id, { state: invoice.state })
// Actions to be executed when the tip's state is updated
executeTipAction(tip, invoice)
}
} catch (err) {
Logger.error('[TIP] An error has occurred while updating invoice', err)
const errorCount = tip._errorCount ? tip._errorCount : 0
_updateTipData(id, {
_errorCount: errorCount + 1
})
}
})
}
const startTipStatusJob = () => {
const { lightning } = LightningServices.services
const stream = lightning.subscribeInvoices({})
updateUnverifiedTips()
stream.on('data', async invoice => {
const hash = invoice.r_hash.toString('base64')
const tip = await _getTipData(hash)
if (tip.state !== invoice.state) {
await _updateTipData(hash, { state: invoice.state })
executeTipAction(tip, invoice)
}
})
stream.on('error', err => {
Logger.error('Tip Job error' + err.details)
})
}
module.exports = {
startTipStatusJob
}

641
yarn.lock

File diff suppressed because it is too large Load diff