Merge branch 'master' into feature/tunnel
This commit is contained in:
commit
d7e7689e1f
30 changed files with 1785 additions and 953 deletions
|
|
@ -5,4 +5,6 @@ DISABLE_SHOCK_ENCRYPTION=false
|
|||
CACHE_HEADERS_MANDATORY=true
|
||||
SHOCK_CACHE=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
1
.eslintignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.ts
|
||||
|
|
@ -88,7 +88,10 @@
|
|||
// I am now convinced TODO comments closer to the relevant code are better
|
||||
// than GH issues. Especially when it only concerns a single function /
|
||||
// routine.
|
||||
"no-warning-comments": "off"
|
||||
"no-warning-comments": "off",
|
||||
|
||||
// broken
|
||||
"sort-imports": "off"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
|
|
|
|||
36
.github/workflows/version.yml
vendored
Normal file
36
.github/workflows/version.yml
vendored
Normal 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
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"eslint.enable": true,
|
||||
"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
33
.vscode/snippets.code-snippets
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<h1>ShockAPI</h1>
|
||||
|
||||

|
||||

|
||||
[](http://makeapullrequest.com)
|
||||
[](https://t.me/Shockwallet)
|
||||
[](https://twitter.com/shockbtc)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ module.exports = (mainnet = false) => {
|
|||
maxNumRoutesToQuery: 20,
|
||||
lndProto: parsePath(`${__dirname}/rpc.proto`),
|
||||
routerProto: parsePath(`${__dirname}/router.proto`),
|
||||
invoicesProto: parsePath(`${__dirname}/invoices.proto`),
|
||||
walletUnlockerProto: parsePath(`${__dirname}/walletunlocker.proto`),
|
||||
lndHost: "localhost:10009",
|
||||
lndCertPath: parsePath(`${lndDirectory}/tls.cert`),
|
||||
|
|
|
|||
122
config/invoices.proto
Normal file
122
config/invoices.proto
Normal 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;
|
||||
}
|
||||
28
package.json
28
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "shockapi",
|
||||
"version": "1.0.0",
|
||||
"version": "2021.1.04",
|
||||
"description": "",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
|
|
@ -11,13 +11,16 @@
|
|||
"test:watch": "jest --no-cache --watch",
|
||||
"typecheck": "tsc",
|
||||
"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": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.5.1",
|
||||
"axios": "^0.20.0",
|
||||
"@grpc/grpc-js": "^1.2.2",
|
||||
"@grpc/proto-loader": "^0.5.5",
|
||||
"assert-never": "^1.2.1",
|
||||
"axios": "^0.21.1",
|
||||
"basic-auth": "^2.0.0",
|
||||
"big.js": "^5.2.2",
|
||||
"bitcore-lib": "^0.15.0",
|
||||
|
|
@ -31,7 +34,7 @@
|
|||
"debug": "^3.1.0",
|
||||
"dotenv": "^8.1.0",
|
||||
"express": "^4.14.1",
|
||||
"express-session": "^1.15.1",
|
||||
"express-session": "^1.17.1",
|
||||
"google-proto-files": "^1.0.3",
|
||||
"graphviz": "0.0.8",
|
||||
"grpc": "1.24.4",
|
||||
|
|
@ -42,15 +45,16 @@
|
|||
"localtunnel": "git://github.com/shocknet/localtunnel#40cc2c2a46b05da2217bf2e20da11a5343a5cce7",
|
||||
"lodash": "^4.17.20",
|
||||
"method-override": "^2.3.7",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-persist": "^3.1.0",
|
||||
"promise": "^8.1.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"ramda": "^0.27.1",
|
||||
"request": "^2.88.2",
|
||||
"request-promise": "^4.2.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"response-time": "^2.3.2",
|
||||
"shelljs": "^0.8.2",
|
||||
"shock-common": "17.x.x",
|
||||
"shock-common": "32.0.0",
|
||||
"socket.io": "2.1.1",
|
||||
"text-encoding": "^0.7.0",
|
||||
"tingodb": "^0.6.1",
|
||||
|
|
@ -58,7 +62,7 @@
|
|||
"winston-daily-rotate-file": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@types/bluebird": "^3.5.32",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/express": "^4.17.1",
|
||||
|
|
@ -66,10 +70,12 @@
|
|||
"@types/jest": "^24.0.18",
|
||||
"@types/jsonwebtoken": "^8.3.7",
|
||||
"@types/lodash": "^4.14.141",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@types/ramda": "types/npm-ramda#dist",
|
||||
"@types/react": "16.x.x",
|
||||
"@types/socket.io": "^2.1.11",
|
||||
"@types/uuid": "^3.4.5",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-prettier": "^6.5.0",
|
||||
|
|
@ -80,8 +86,10 @@
|
|||
"lint-staged": "^10.2.2",
|
||||
"nodemon": "^1.19.3",
|
||||
"prettier": "^1.18.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^9.1.1",
|
||||
"ts-type": "^1.2.16",
|
||||
"typescript": "^4.0.2"
|
||||
"typescript": "latest"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
|
|
|
|||
63
services/coordinates.js
Normal file
63
services/coordinates.js
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,23 +26,13 @@ const SEAx = require('gun/sea')
|
|||
/** @type {import('../contact-api/SimpleGUN').ISEA} */
|
||||
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__MSG__ = '$$__SHOCKWALLET__MSG__'
|
||||
const $$__SHOCKWALLET__NUMBER__ = '$$__SHOCKWALLET__NUMBER__'
|
||||
const $$__SHOCKWALLET__BOOLEAN__ = '$$__SHOCKWALLET__BOOLEAN__'
|
||||
|
||||
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') {
|
||||
throw new TypeError(
|
||||
`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
|
||||
const sanitizedMsg = $$__SHOCKWALLET__MSG__ + msg
|
||||
let strToEncode = ''
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
mySEA.decrypt = (encMsg, secret) => {
|
||||
/**
|
||||
* @param {string} encMsg
|
||||
* @param {string} secret
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const decryptBase = (encMsg, secret) => {
|
||||
if (typeof encMsg !== 'string') {
|
||||
throw new TypeError(
|
||||
'mySEA.encrypt() -> expected encMsg to be an string instead got: ' +
|
||||
|
|
@ -104,10 +114,41 @@ mySEA.decrypt = (encMsg, secret) => {
|
|||
throw new TypeError('Could not decrypt')
|
||||
}
|
||||
|
||||
return decodedMsg.slice($$__SHOCKWALLET__MSG__.length)
|
||||
if (decodedMsg.startsWith($$__SHOCKWALLET__MSG__)) {
|
||||
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) => {
|
||||
if (typeof recipientOrSenderEpub !== 'string') {
|
||||
throw new TypeError(
|
||||
|
|
@ -273,7 +314,7 @@ const authenticate = async (alias, pass, __user) => {
|
|||
// clock skew
|
||||
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(
|
||||
{
|
||||
unused: null
|
||||
|
|
@ -286,7 +327,7 @@ const authenticate = async (alias, pass, __user) => {
|
|||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}))
|
||||
|
||||
return ack.sea.pub
|
||||
} else {
|
||||
|
|
@ -304,7 +345,7 @@ const authenticate = async (alias, pass, __user) => {
|
|||
// clock skew
|
||||
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(
|
||||
{
|
||||
unused: null
|
||||
|
|
@ -317,7 +358,7 @@ const authenticate = async (alias, pass, __user) => {
|
|||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}))
|
||||
|
||||
// move this to a subscription; implement off() ? todo
|
||||
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, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
_user.get(Key.FOLLOWS).put(
|
||||
{
|
||||
unused: null
|
||||
|
|
@ -376,7 +417,7 @@ const authenticate = async (alias, pass, __user) => {
|
|||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}))
|
||||
|
||||
API.Jobs.onAcceptedRequests(_user, mySEA)
|
||||
API.Jobs.onOrders(_user, gun, mySEA)
|
||||
|
|
|
|||
|
|
@ -120,8 +120,19 @@ export interface UserGUNNode extends GUNNode {
|
|||
}
|
||||
|
||||
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>
|
||||
decryptNumber(
|
||||
encryptedMessage: string,
|
||||
recipientSecret: string
|
||||
): Promise<number>
|
||||
decryptBoolean(
|
||||
encryptedMessage: string,
|
||||
recipientSecret: string
|
||||
): Promise<boolean>
|
||||
secret(
|
||||
recipientOrSenderEpub: string,
|
||||
recipientOrSenderUserPair: UserPair
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ const { ErrorCode } = Constants
|
|||
|
||||
const {
|
||||
sendPaymentV2Invoice,
|
||||
decodePayReq
|
||||
decodePayReq,
|
||||
myLNDPub
|
||||
} = require('../../../utils/lightningServices/v2')
|
||||
|
||||
/**
|
||||
|
|
@ -21,6 +22,7 @@ const {
|
|||
const Getters = require('./getters')
|
||||
const Key = require('./key')
|
||||
const Utils = require('./utils')
|
||||
const { writeCoordinate } = require('../../coordinates')
|
||||
|
||||
/**
|
||||
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
||||
|
|
@ -98,7 +100,7 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
|
|||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.OUTGOINGS)
|
||||
.get(newOutgoingFeedID)
|
||||
|
|
@ -111,14 +113,14 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
const encryptedForMeNewOutgoingFeedID = await SEA.encrypt(
|
||||
newOutgoingFeedID,
|
||||
mySecret
|
||||
)
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.RECIPIENT_TO_OUTGOING)
|
||||
.get(withPublicKey)
|
||||
|
|
@ -129,7 +131,7 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
outgoingFeedID = newOutgoingFeedID
|
||||
}
|
||||
|
|
@ -235,7 +237,7 @@ const acceptRequest = async (
|
|||
const mySecret = require('../Mediator').getMySecret()
|
||||
const encryptedForMeIncomingID = await SEA.encrypt(incomingID, mySecret)
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.USER_TO_INCOMING)
|
||||
.get(senderPublicKey)
|
||||
|
|
@ -246,7 +248,7 @@ const acceptRequest = async (
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// NOTE: perform non-reversable actions before destructive actions
|
||||
|
|
@ -259,7 +261,7 @@ const acceptRequest = async (
|
|||
ourSecret
|
||||
)
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
.get(handshakeAddress)
|
||||
|
|
@ -276,7 +278,7 @@ const acceptRequest = async (
|
|||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -285,7 +287,7 @@ const acceptRequest = async (
|
|||
* @param {UserGUNNode} userNode
|
||||
*/
|
||||
const authenticate = (user, pass, userNode) =>
|
||||
new Promise((resolve, reject) => {
|
||||
/** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
||||
if (typeof user !== 'string') {
|
||||
throw new TypeError('expected user to be of type string')
|
||||
}
|
||||
|
|
@ -315,7 +317,7 @@ const authenticate = (user, pass, userNode) =>
|
|||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
/**
|
||||
* @param {string} publicKey
|
||||
|
|
@ -347,7 +349,7 @@ const generateHandshakeAddress = async () => {
|
|||
|
||||
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 => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
rej(new Error(ack.err))
|
||||
|
|
@ -355,9 +357,9 @@ const generateHandshakeAddress = async () => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
.get(address)
|
||||
|
|
@ -368,7 +370,7 @@ const generateHandshakeAddress = async () => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -385,7 +387,7 @@ const cleanup = async pub => {
|
|||
const promises = []
|
||||
|
||||
promises.push(
|
||||
new Promise((res, rej) => {
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.USER_TO_INCOMING)
|
||||
.get(pub)
|
||||
|
|
@ -396,11 +398,11 @@ const cleanup = async pub => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
promises.push(
|
||||
new Promise((res, rej) => {
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.RECIPIENT_TO_OUTGOING)
|
||||
.get(pub)
|
||||
|
|
@ -411,11 +413,11 @@ const cleanup = async pub => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
promises.push(
|
||||
new Promise((res, rej) => {
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.USER_TO_LAST_REQUEST_SENT)
|
||||
.get(pub)
|
||||
|
|
@ -426,12 +428,12 @@ const cleanup = async pub => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
if (outGoingID) {
|
||||
promises.push(
|
||||
new Promise((res, rej) => {
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.OUTGOINGS)
|
||||
.get(outGoingID)
|
||||
|
|
@ -442,7 +444,7 @@ const cleanup = async pub => {
|
|||
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
|
||||
.get(Key.USER_TO_LAST_REQUEST_SENT)
|
||||
.get(recipientPublicKey)
|
||||
|
|
@ -627,7 +629,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
// 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
|
||||
|
|
@ -642,7 +644,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
|
|||
timestamp
|
||||
}
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
//@ts-ignore
|
||||
user.get(Key.STORED_REQS).set(storedReq, ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
|
|
@ -655,7 +657,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -922,6 +924,7 @@ const sendHRWithInitialMsg = async (
|
|||
* @typedef {object} SpontPaymentOptions
|
||||
* @prop {Common.Schema.OrderTargetType} type
|
||||
* @prop {string=} postID
|
||||
* @prop {string=} ackInfo
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -940,7 +943,7 @@ const sendSpontaneousPayment = async (
|
|||
amount,
|
||||
memo,
|
||||
feeLimit,
|
||||
opts = { type: 'user' }
|
||||
opts = { type: 'spontaneousPayment' }
|
||||
) => {
|
||||
try {
|
||||
const SEA = require('../Mediator').mySEA
|
||||
|
|
@ -965,8 +968,8 @@ const sendSpontaneousPayment = async (
|
|||
targetType: opts.type
|
||||
}
|
||||
|
||||
if (opts.type === 'post') {
|
||||
order.postID = opts.postID
|
||||
if (opts.type === 'tip') {
|
||||
order.ackInfo = opts.postID
|
||||
}
|
||||
|
||||
logger.info(JSON.stringify(order))
|
||||
|
|
@ -1074,6 +1077,32 @@ const sendSpontaneousPayment = async (
|
|||
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
|
||||
} catch (e) {
|
||||
logger.error('Error inside sendPayment()')
|
||||
|
|
@ -1125,7 +1154,7 @@ const generateOrderAddress = user =>
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
const setBio = (bio, user) =>
|
||||
new Promise((resolve, reject) => {
|
||||
/** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
|
@ -1149,7 +1178,7 @@ const setBio = (bio, user) =>
|
|||
resolve()
|
||||
}
|
||||
})
|
||||
}).then(
|
||||
})).then(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
user
|
||||
|
|
@ -1233,7 +1262,7 @@ const disconnect = async pub => {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
const setLastSeenApp = () =>
|
||||
new Promise((res, rej) => {
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
require('../Mediator')
|
||||
.getUser()
|
||||
.get(Key.LAST_SEEN_APP)
|
||||
|
|
@ -1244,7 +1273,7 @@ const setLastSeenApp = () =>
|
|||
res()
|
||||
}
|
||||
})
|
||||
}).then(
|
||||
})).then(
|
||||
() =>
|
||||
new Promise((res, rej) => {
|
||||
require('../Mediator')
|
||||
|
|
@ -1268,14 +1297,14 @@ const setLastSeenApp = () =>
|
|||
* @returns {Promise<[string, Common.Schema.RawPost]>}
|
||||
*/
|
||||
const createPostNew = async (tags, title, content) => {
|
||||
const SEA = require('../Mediator').mySEA
|
||||
/** @type {Common.Schema.RawPost} */
|
||||
const newPost = {
|
||||
date: Date.now(),
|
||||
status: 'publish',
|
||||
tags: tags.join('-'),
|
||||
title,
|
||||
contentItems: {},
|
||||
tipCounter: 0
|
||||
contentItems: {}
|
||||
}
|
||||
|
||||
content.forEach(c => {
|
||||
|
|
@ -1284,6 +1313,23 @@ const createPostNew = async (tags, title, content) => {
|
|||
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} */
|
||||
const postID = await Common.makePromise((res, rej) => {
|
||||
const _n = require('../Mediator')
|
||||
|
|
@ -1365,7 +1411,7 @@ const createPost = async (tags, title, content) => {
|
|||
pageIdx = Number(pageIdx + 1).toString()
|
||||
}
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
require('../Mediator')
|
||||
.getUser()
|
||||
.get(Key.WALL)
|
||||
|
|
@ -1386,7 +1432,7 @@ const createPost = async (tags, title, content) => {
|
|||
res()
|
||||
}
|
||||
)
|
||||
})
|
||||
}))
|
||||
|
||||
const [postID, newPost] = await createPostNew(tags, title, content)
|
||||
|
||||
|
|
@ -1412,7 +1458,7 @@ const createPost = async (tags, title, content) => {
|
|||
})
|
||||
|
||||
if (shouldBeNewPage || numOfPages === 0) {
|
||||
await new Promise(res => {
|
||||
await /** @type {Promise<void>} */ (new Promise(res => {
|
||||
require('../Mediator')
|
||||
.getUser()
|
||||
.get(Key.WALL)
|
||||
|
|
@ -1424,7 +1470,7 @@ const createPost = async (tags, title, content) => {
|
|||
|
||||
res()
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const loadedPost = await new Promise(res => {
|
||||
|
|
@ -1467,7 +1513,7 @@ const createPost = async (tags, title, content) => {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deletePost = async (postId, page) => {
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
require('../Mediator')
|
||||
.getUser()
|
||||
.get(Key.WALL)
|
||||
|
|
@ -1482,15 +1528,15 @@ const deletePost = async (postId, page) => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} publicKey
|
||||
* @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} */
|
||||
const newFollow = {
|
||||
private: isPrivate,
|
||||
|
|
@ -1498,7 +1544,7 @@ const follow = (publicKey, isPrivate) => {
|
|||
user: publicKey
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
require('../Mediator')
|
||||
.getUser()
|
||||
.get(Key.FOLLOWS)
|
||||
|
|
@ -1511,7 +1557,7 @@ const follow = (publicKey, isPrivate) => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1543,7 +1589,7 @@ const initWall = async () => {
|
|||
const promises = []
|
||||
|
||||
promises.push(
|
||||
new Promise((res, rej) => {
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.WALL)
|
||||
.get(Key.NUM_OF_PAGES)
|
||||
|
|
@ -1554,11 +1600,11 @@ const initWall = async () => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
promises.push(
|
||||
new Promise((res, rej) => {
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.WALL)
|
||||
.get(Key.PAGES)
|
||||
|
|
@ -1576,11 +1622,11 @@ const initWall = async () => {
|
|||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
promises.push(
|
||||
new Promise((res, rej) => {
|
||||
/** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.WALL)
|
||||
.get(Key.PAGES)
|
||||
|
|
@ -1593,7 +1639,7 @@ const initWall = async () => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ const onAcceptedRequests = (user, SEA) => {
|
|||
const recipientEpub = await Utils.pubToEpub(recipientPub)
|
||||
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
.get(requestAddress)
|
||||
|
|
@ -151,7 +151,7 @@ const onAcceptedRequests = (user, SEA) => {
|
|||
mySecret
|
||||
)
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.USER_TO_INCOMING)
|
||||
.get(recipientPub)
|
||||
|
|
@ -162,9 +162,9 @@ const onAcceptedRequests = (user, SEA) => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.STORED_REQS)
|
||||
.get(id)
|
||||
|
|
@ -175,12 +175,12 @@ const onAcceptedRequests = (user, SEA) => {
|
|||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
// ensure this listeners gets called at least once
|
||||
res()
|
||||
})
|
||||
})
|
||||
}))
|
||||
} catch (err) {
|
||||
logger.warn(`Jobs.onAcceptedRequests() -> ${err.message}`)
|
||||
logger.error(err)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
const { performance } = require('perf_hooks')
|
||||
// @ts-check
|
||||
const logger = require('winston')
|
||||
const isFinite = require('lodash/isFinite')
|
||||
const isNumber = require('lodash/isNumber')
|
||||
const isNaN = require('lodash/isNaN')
|
||||
const Common = require('shock-common')
|
||||
const {
|
||||
Constants: { ErrorCode },
|
||||
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 {
|
||||
addInvoice,
|
||||
myLNDPub
|
||||
} = require('../../../../utils/lightningServices/v2')
|
||||
const { writeCoordinate } = require('../../../coordinates')
|
||||
const Key = require('../key')
|
||||
const Utils = require('../utils')
|
||||
const { gunUUID } = require('../../../../utils')
|
||||
|
||||
const getUser = () => require('../../Mediator').getUser()
|
||||
|
||||
|
|
@ -55,28 +63,6 @@ const ordersProcessed = new Set()
|
|||
|
||||
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 {ISEA} SEA
|
||||
|
|
@ -103,8 +89,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
return
|
||||
}
|
||||
|
||||
const listenerStartTime = performance.now()
|
||||
|
||||
ordersProcessed.add(orderID)
|
||||
|
||||
logger.info(
|
||||
|
|
@ -113,8 +97,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
)} -- addr: ${addr}`
|
||||
)
|
||||
|
||||
const orderAnswerStartTime = performance.now()
|
||||
|
||||
const alreadyAnswered = await getUser()
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(orderID)
|
||||
|
|
@ -125,12 +107,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
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 secret = await SEA.secret(senderEpub, getUser()._.sea)
|
||||
|
||||
|
|
@ -139,10 +115,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
SEA.decrypt(order.memo, secret)
|
||||
])
|
||||
|
||||
const decryptEndTime = performance.now() - decryptStartTime
|
||||
|
||||
logger.info(`[PERF] Decrypt invoice info: ${decryptEndTime}ms`)
|
||||
|
||||
const amount = Number(decryptedAmount)
|
||||
|
||||
if (!isNumber(amount)) {
|
||||
|
|
@ -174,26 +146,19 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
`onOrders() -> Will now create an invoice : ${JSON.stringify(invoiceReq)}`
|
||||
)
|
||||
|
||||
const invoiceStartTime = performance.now()
|
||||
|
||||
const invoice = await _addInvoice(invoiceReq)
|
||||
|
||||
const invoiceEndTime = performance.now() - invoiceStartTime
|
||||
|
||||
logger.info(`[PERF] LND Invoice created in ${invoiceEndTime}ms`)
|
||||
const invoice = await addInvoice(
|
||||
invoiceReq.value,
|
||||
invoiceReq.memo,
|
||||
true,
|
||||
invoiceReq.expiry
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'onOrders() -> Successfully created the invoice, will now encrypt it'
|
||||
)
|
||||
|
||||
const invoiceEncryptStartTime = performance.now()
|
||||
|
||||
const encInvoice = await SEA.encrypt(invoice.payment_request, secret)
|
||||
|
||||
const invoiceEncryptEndTime = performance.now() - invoiceEncryptStartTime
|
||||
|
||||
logger.info(`[PERF] Invoice encrypted in ${invoiceEncryptEndTime}ms`)
|
||||
|
||||
logger.info(
|
||||
`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'
|
||||
}
|
||||
|
||||
const invoicePutStartTime = performance.now()
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
getUser()
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(orderID)
|
||||
|
|
@ -222,34 +185,231 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
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
|
||||
|
||||
logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`)
|
||||
|
||||
const listenerEndTime = performance.now() - listenerStartTime
|
||||
|
||||
logger.info(`[PERF] Invoice generation completed in ${listenerEndTime}ms`)
|
||||
|
||||
const hash = invoice.r_hash.toString('base64')
|
||||
|
||||
if (order.targetType === 'post') {
|
||||
/** @type {TipPaymentStatus} */
|
||||
const paymentStatus = {
|
||||
hash,
|
||||
state: 'OPEN',
|
||||
targetType: order.targetType,
|
||||
postID: order.postID
|
||||
}
|
||||
getUser()
|
||||
.get(Key.TIPS_PAYMENT_STATUS)
|
||||
.get(hash)
|
||||
// @ts-ignore
|
||||
.put(paymentStatus, response => {
|
||||
console.log(response)
|
||||
})
|
||||
/** @type {Common.Coordinate} */
|
||||
const coord = {
|
||||
amount,
|
||||
id: invoice.r_hash.toString(),
|
||||
inbound: true,
|
||||
timestamp: Date.now(),
|
||||
type: 'invoice',
|
||||
invoiceMemo: memo,
|
||||
fromGunPub: order.from,
|
||||
toGunPub: getUser()._.sea.pub,
|
||||
toLndPub: await myLNDPub()
|
||||
}
|
||||
|
||||
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()
|
||||
.get('postToTipCount')
|
||||
// CAST: Checked above.
|
||||
.get(/** @type {string} */ (order.ackInfo))
|
||||
.set(null) // each item in the set is a tip
|
||||
} 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) {
|
||||
logger.error(
|
||||
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ exports.POSTS = 'posts'
|
|||
// Tips counter for posts
|
||||
exports.TOTAL_TIPS = 'totalTips'
|
||||
|
||||
exports.TIPS_PAYMENT_STATUS = 'tipsPaymentStatus'
|
||||
|
||||
exports.PROFILE_BINARY = 'profileBinary'
|
||||
|
||||
exports.POSTS_NEW = 'posts'
|
||||
|
||||
exports.COORDINATES = 'coordinates'
|
||||
|
|
|
|||
|
|
@ -212,29 +212,19 @@ const tryAndWait = async (promGen, shouldRetry = () => false) => {
|
|||
*/
|
||||
const pubToEpub = async pub => {
|
||||
try {
|
||||
const epub = await tryAndWait(async gun => {
|
||||
const _epub = await CommonUtils.makePromise(res => {
|
||||
gun
|
||||
const epub = await timeout10(
|
||||
CommonUtils.makePromise(res => {
|
||||
require('../../Mediator/index')
|
||||
.getGun()
|
||||
.user(pub)
|
||||
.get('epub')
|
||||
.once(
|
||||
data => {
|
||||
.on(data => {
|
||||
if (typeof data === 'string') {
|
||||
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
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const Path = require("path");
|
||||
const grpc = require("grpc");
|
||||
const grpc = require("@grpc/grpc-js");
|
||||
const protoLoader = require("@grpc/proto-loader");
|
||||
const logger = require("winston");
|
||||
const fs = require("../../utils/fs");
|
||||
|
|
@ -10,6 +10,7 @@ const errorConstants = require("../../constants/errors");
|
|||
* @typedef LightningConfig
|
||||
* @prop {string} lnrpcProtoPath
|
||||
* @prop {string} routerProtoPath
|
||||
* @prop {string} invoicesProtoPath
|
||||
* @prop {string} walletUnlockerProtoPath
|
||||
* @prop {string} lndHost
|
||||
* @prop {string} lndCertPath
|
||||
|
|
@ -21,6 +22,7 @@ const errorConstants = require("../../constants/errors");
|
|||
* @prop {any} lightning
|
||||
* @prop {any} walletUnlocker
|
||||
* @prop {any} router
|
||||
* @prop {any} invoices
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -30,10 +32,11 @@ const errorConstants = require("../../constants/errors");
|
|||
module.exports = async ({
|
||||
lnrpcProtoPath,
|
||||
routerProtoPath,
|
||||
invoicesProtoPath,
|
||||
walletUnlockerProtoPath,
|
||||
lndHost,
|
||||
lndCertPath,
|
||||
macaroonPath
|
||||
lndHost,
|
||||
lndCertPath,
|
||||
macaroonPath
|
||||
}) => {
|
||||
try {
|
||||
process.env.GRPC_SSL_CIPHER_SUITES = "HIGH+ECDSA";
|
||||
|
|
@ -46,9 +49,15 @@ module.exports = async ({
|
|||
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 { routerrpc } = grpc.loadPackageDefinition(routerProto);
|
||||
const { invoicesrpc } = grpc.loadPackageDefinition(invoicesProto);
|
||||
const { lnrpc: walletunlockerrpc } = grpc.loadPackageDefinition(walletUnlockerProto);
|
||||
|
||||
const getCredentials = async () => {
|
||||
|
|
@ -93,11 +102,13 @@ module.exports = async ({
|
|||
const walletUnlocker = new walletunlockerrpc.WalletUnlocker(lndHost, credentials);
|
||||
// @ts-ignore
|
||||
const router = new routerrpc.Router(lndHost, credentials);
|
||||
|
||||
// @ts-ignore
|
||||
const invoices = new invoicesrpc.Invoices(lndHost, credentials);
|
||||
return {
|
||||
lightning,
|
||||
walletUnlocker,
|
||||
router
|
||||
router,
|
||||
invoices
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
109
src/__gun__tests__/mySea.ts
Normal 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()
|
||||
318
src/routes.js
318
src/routes.js
|
|
@ -30,12 +30,7 @@ const {
|
|||
const GunActions = require('../services/gunDB/contact-api/actions')
|
||||
const GunGetters = require('../services/gunDB/contact-api/getters')
|
||||
const GunKey = require('../services/gunDB/contact-api/key')
|
||||
const {
|
||||
sendPaymentV2Keysend,
|
||||
sendPaymentV2Invoice,
|
||||
listPayments
|
||||
} = require('../utils/lightningServices/v2')
|
||||
const { startTipStatusJob } = require('../utils/lndJobs')
|
||||
const LV2 = require('../utils/lightningServices/v2')
|
||||
const GunWriteRPC = require('../services/gunDB/rpc')
|
||||
|
||||
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
|
||||
|
|
@ -690,7 +685,6 @@ module.exports = async (
|
|||
}
|
||||
|
||||
onNewChannelBackup()
|
||||
startTipStatusJob()
|
||||
|
||||
res.json({
|
||||
authorization: token,
|
||||
|
|
@ -1026,30 +1020,15 @@ module.exports = async (
|
|||
)
|
||||
})
|
||||
// get lnd chan info
|
||||
app.post('/api/lnd/getchaninfo', (req, res) => {
|
||||
const { lightning } = LightningServices.services
|
||||
|
||||
lightning.getChanInfo(
|
||||
{ chan_id: req.body.chan_id },
|
||||
async (err, response) => {
|
||||
if (err) {
|
||||
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.post('/api/lnd/getchaninfo', async (req, res) => {
|
||||
try {
|
||||
return res.json(await LV2.getChanInfo(req.body.chan_id))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return res.status(500).json({
|
||||
errorMessage: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/lnd/getnetworkinfo', (req, res) => {
|
||||
|
|
@ -1074,47 +1053,30 @@ module.exports = async (
|
|||
})
|
||||
|
||||
// get lnd node active channels list
|
||||
app.get('/api/lnd/listpeers', (req, res) => {
|
||||
const { lightning } = LightningServices.services
|
||||
lightning.listPeers({}, async (err, response) => {
|
||||
if (err) {
|
||||
logger.debug('ListPeers Error:', err)
|
||||
const health = await checkHealth()
|
||||
if (health.LNDStatus.success) {
|
||||
res.status(400).json({
|
||||
field: 'listPeers',
|
||||
errorMessage: sanitizeLNDError(err.message)
|
||||
})
|
||||
} else {
|
||||
res.status(500)
|
||||
res.json({ errorMessage: 'LND is down' })
|
||||
}
|
||||
}
|
||||
logger.debug('ListPeers:', response)
|
||||
res.json(response)
|
||||
})
|
||||
app.get('/api/lnd/listpeers', async (req, res) => {
|
||||
try {
|
||||
return res.json({
|
||||
peers: await LV2.listPeers(req.body.latestError)
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return res.status(500).json({
|
||||
errorMessage: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// newaddress
|
||||
app.post('/api/lnd/newaddress', (req, res) => {
|
||||
const { lightning } = LightningServices.services
|
||||
lightning.newAddress({ type: req.body.type }, async (err, response) => {
|
||||
if (err) {
|
||||
logger.debug('NewAddress Error:', err)
|
||||
const health = await checkHealth()
|
||||
if (health.LNDStatus.success) {
|
||||
res.status(400).json({
|
||||
field: 'newAddress',
|
||||
errorMessage: sanitizeLNDError(err.message)
|
||||
})
|
||||
} else {
|
||||
res.status(500)
|
||||
res.json({ errorMessage: 'LND is down' })
|
||||
}
|
||||
}
|
||||
logger.debug('NewAddress:', response)
|
||||
res.json(response)
|
||||
})
|
||||
app.post('/api/lnd/newaddress', async (req, res) => {
|
||||
try {
|
||||
return res.json({
|
||||
address: await LV2.newAddress(req.body.type)
|
||||
})
|
||||
} catch (e) {
|
||||
return res.status(500).json({
|
||||
errorMessage: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// connect peer to lnd node
|
||||
|
|
@ -1159,47 +1121,28 @@ module.exports = async (
|
|||
})
|
||||
|
||||
// get lnd node opened channels list
|
||||
app.get('/api/lnd/listchannels', (req, res) => {
|
||||
const { lightning } = LightningServices.services
|
||||
lightning.listChannels({}, 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)
|
||||
res.json(response)
|
||||
})
|
||||
app.get('/api/lnd/listchannels', async (_, res) => {
|
||||
try {
|
||||
return res.json({
|
||||
channels: await LV2.listChannels()
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return res.status(500).json({
|
||||
errorMessage: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// get lnd node pending channels list
|
||||
app.get('/api/lnd/pendingchannels', (req, res) => {
|
||||
const { lightning } = LightningServices.services
|
||||
lightning.pendingChannels({}, async (err, response) => {
|
||||
if (err) {
|
||||
logger.debug('PendingChannels Error:', err)
|
||||
const health = await checkHealth()
|
||||
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/pendingchannels', async (req, res) => {
|
||||
try {
|
||||
return res.json(await LV2.pendingChannels())
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return res.status(500).json({
|
||||
errorMessage: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/lnd/unifiedTrx', (req, res) => {
|
||||
|
|
@ -1249,12 +1192,35 @@ module.exports = async (
|
|||
|
||||
app.post('/api/lnd/unifiedTrx', async (req, res) => {
|
||||
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({
|
||||
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(
|
||||
await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit, {
|
||||
type,
|
||||
postID
|
||||
postID,
|
||||
ackInfo
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
|
|
@ -1377,7 +1344,7 @@ module.exports = async (
|
|||
}
|
||||
|
||||
return res.status(200).json(
|
||||
await listPayments({
|
||||
await LV2.listPayments({
|
||||
include_incomplete,
|
||||
index_offset,
|
||||
max_payments,
|
||||
|
|
@ -1664,7 +1631,7 @@ module.exports = async (
|
|||
})
|
||||
}
|
||||
|
||||
const payment = await sendPaymentV2Keysend({
|
||||
const payment = await LV2.sendPaymentV2Keysend({
|
||||
amt,
|
||||
dest,
|
||||
feeLimit,
|
||||
|
|
@ -1677,7 +1644,7 @@ module.exports = async (
|
|||
}
|
||||
const { payreq } = req.body
|
||||
|
||||
const payment = await sendPaymentV2Invoice({
|
||||
const payment = await LV2.sendPaymentV2Invoice({
|
||||
feeLimit,
|
||||
payment_request: payreq,
|
||||
amt: req.body.amt,
|
||||
|
|
@ -1766,64 +1733,32 @@ module.exports = async (
|
|||
})
|
||||
|
||||
// addinvoice
|
||||
app.post('/api/lnd/addinvoice', (req, res) => {
|
||||
const { lightning } = LightningServices.services
|
||||
const invoiceRequest = { memo: req.body.memo, private: true }
|
||||
if (req.body.value) {
|
||||
invoiceRequest.value = req.body.value
|
||||
}
|
||||
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' })
|
||||
app.post('/api/lnd/addinvoice', async (req, res) => {
|
||||
const { expiry, value, memo } = req.body
|
||||
const addInvoiceRes = await LV2.addInvoice(value, memo, true, expiry)
|
||||
|
||||
if (value) {
|
||||
const channelsList = await LV2.listChannels({ active_only: true })
|
||||
let remoteBalance = Big(0)
|
||||
channelsList.forEach(element => {
|
||||
const remB = Big(element.remote_balance)
|
||||
if (remB.gt(remoteBalance)) {
|
||||
remoteBalance = remB
|
||||
}
|
||||
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)
|
||||
channelsList.forEach(element => {
|
||||
const remB = Big(element.remote_balance)
|
||||
if (remB.gt(remoteBalance)) {
|
||||
remoteBalance = remB
|
||||
}
|
||||
})
|
||||
newInvoice.liquidityCheck = remoteBalance > req.body.value
|
||||
//newInvoice.remoteBalance = remoteBalance
|
||||
res.json(newInvoice)
|
||||
})
|
||||
} else {
|
||||
res.json(newInvoice)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
addInvoiceRes.liquidityCheck = remoteBalance > value
|
||||
//newInvoice.remoteBalance = remoteBalance
|
||||
}
|
||||
|
||||
try {
|
||||
return res.json(addInvoiceRes)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return res.status(500).json({
|
||||
errorMessage: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// signmessage
|
||||
|
|
@ -1882,7 +1817,8 @@ module.exports = async (
|
|||
const sendCoinsRequest = {
|
||||
addr: req.body.addr,
|
||||
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)
|
||||
lightning.sendCoins(sendCoinsRequest, async (err, response) => {
|
||||
|
|
@ -1960,23 +1896,25 @@ module.exports = async (
|
|||
)
|
||||
})
|
||||
|
||||
app.post('/api/lnd/listunspent', (req, res) => {
|
||||
const { lightning } = LightningServices.services
|
||||
const { minConfirmations = 3, maxConfirmations = 6 } = req.body
|
||||
lightning.listUnspent(
|
||||
{
|
||||
min_confs: minConfirmations,
|
||||
max_confs: maxConfirmations
|
||||
},
|
||||
(err, unspent) => {
|
||||
if (err) {
|
||||
return handleError(res, err)
|
||||
}
|
||||
logger.debug('ListUnspent:', unspent)
|
||||
res.json(unspent)
|
||||
}
|
||||
)
|
||||
})
|
||||
const listunspent = async (req, res) => {
|
||||
try {
|
||||
return res.status(200).json({
|
||||
utxos: await LV2.listUnspent(
|
||||
req.body.minConfirmations,
|
||||
req.body.maxConfirmations
|
||||
)
|
||||
})
|
||||
} 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) => {
|
||||
const { lightning } = LightningServices.services
|
||||
|
|
|
|||
|
|
@ -167,10 +167,6 @@ const server = program => {
|
|||
await LightningServices.init()
|
||||
}
|
||||
|
||||
// init lnd module =================
|
||||
const lnd = require('../services/lnd/lnd')(
|
||||
LightningServices.services.lightning
|
||||
)
|
||||
await new Promise((resolve, reject) => {
|
||||
LightningServices.services.lightning.getInfo({}, (err, res) => {
|
||||
if (err && err.code !== 12) {
|
||||
|
|
|
|||
|
|
@ -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 { lightning } = LightningServices.services
|
||||
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 processed = receivedReqs.map(({ id, requestorPK, timestamp }) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,21 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
const Gun = require('gun')
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const lnrpc = require('../../services/lnd/lightning')
|
|||
* @prop {string} macaroonPath
|
||||
* @prop {string} lndProto
|
||||
* @prop {string} routerProto
|
||||
* @prop {string} invoicesProto
|
||||
* @prop {string} walletUnlockerProto
|
||||
*/
|
||||
|
||||
|
|
@ -73,6 +74,13 @@ class LightningServices {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./types').Services}
|
||||
*/
|
||||
getServices() {
|
||||
return this.services
|
||||
}
|
||||
|
||||
get servicesData() {
|
||||
return this.lnServicesData
|
||||
}
|
||||
|
|
@ -119,6 +127,7 @@ class LightningServices {
|
|||
const lnServices = await lnrpc({
|
||||
lnrpcProtoPath: this.defaults.lndProto,
|
||||
routerProtoPath: this.defaults.routerProto,
|
||||
invoicesProtoPath: this.defaults.invoicesProto,
|
||||
walletUnlockerProtoPath: this.defaults.walletUnlockerProto,
|
||||
lndHost,
|
||||
lndCertPath,
|
||||
|
|
@ -127,10 +136,11 @@ class LightningServices {
|
|||
if (!lnServices) {
|
||||
throw new Error(`Could not init lnServices`)
|
||||
}
|
||||
const { lightning, walletUnlocker, router } = lnServices
|
||||
const { lightning, walletUnlocker, router, invoices } = lnServices
|
||||
this.lightning = lightning
|
||||
this.walletUnlocker = walletUnlocker
|
||||
this.router = router
|
||||
this.invoices = invoices
|
||||
this.lnServicesData = {
|
||||
lndProto: this.defaults.lndProto,
|
||||
lndHost,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
import * as Common from 'shock-common'
|
||||
|
||||
export interface PaymentV2 {
|
||||
payment_hash: string
|
||||
|
|
@ -106,3 +107,90 @@ export interface SendPaymentInvoiceParams {
|
|||
payment_request: string
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ const logger = require('winston')
|
|||
const Common = require('shock-common')
|
||||
const Ramda = require('ramda')
|
||||
|
||||
const { writeCoordinate } = require('../../services/coordinates')
|
||||
|
||||
const lightningServices = require('./lightning-services')
|
||||
/**
|
||||
* @typedef {import('./types').PaymentV2} PaymentV2
|
||||
|
|
@ -213,12 +215,50 @@ const isValidSendPaymentInvoiceParams = sendPaymentInvoiceParams => {
|
|||
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
|
||||
* @param {SendPaymentV2Request} sendPaymentRequest
|
||||
* @returns {Promise<PaymentV2>}
|
||||
*/
|
||||
const sendPaymentV2 = sendPaymentRequest => {
|
||||
const sendPaymentV2 = async sendPaymentRequest => {
|
||||
const {
|
||||
services: { router }
|
||||
} = 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)
|
||||
|
||||
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
|
||||
* @returns {Promise<Common.Schema.InvoiceWhenDecoded>}
|
||||
* @param {0|1} type
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const decodePayReq = payReq =>
|
||||
Common.Utils.makePromise((res, rej) => {
|
||||
lightningServices.lightning.decodePayReq(
|
||||
{ pay_req: payReq },
|
||||
/**
|
||||
* @param {{ message: any; }} err
|
||||
* @param {any} paymentRequest
|
||||
*/
|
||||
(err, paymentRequest) => {
|
||||
const newAddress = (type = 0) => {
|
||||
const { lightning } = lightningServices.getServices()
|
||||
|
||||
return Common.Utils.makePromise((res, rej) => {
|
||||
lightning.newAddress({ type }, (err, response) => {
|
||||
if (err) {
|
||||
rej(new Error(err.message))
|
||||
} else {
|
||||
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(paymentRequest)
|
||||
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,
|
||||
sendPaymentV2Invoice,
|
||||
listPayments,
|
||||
decodePayReq
|
||||
decodePayReq,
|
||||
newAddress,
|
||||
listUnspent,
|
||||
listChannels,
|
||||
getChanInfo,
|
||||
listPeers,
|
||||
pendingChannels,
|
||||
addInvoice,
|
||||
myLNDPub
|
||||
}
|
||||
|
|
|
|||
211
utils/lndJobs.js
211
utils/lndJobs.js
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue