Mayank
5 years ago
commit
e92b765a61
67 changed files with 8986 additions and 0 deletions
@ -0,0 +1,20 @@ |
|||
.dockerignore |
|||
buildspec.yml |
|||
build-docker.sh |
|||
node_modules |
|||
npm-debug.log |
|||
logs |
|||
README.md |
|||
.git |
|||
.gitignore |
|||
.env.default |
|||
.idea |
|||
Dockerfile |
|||
Dockerfile.armhf |
|||
pre-commit |
|||
.eslintrc |
|||
test/ |
|||
.eslintignore |
|||
coverage |
|||
.nyc_output |
|||
Makefile |
@ -0,0 +1 @@ |
|||
coverage |
@ -0,0 +1,276 @@ |
|||
{ |
|||
"root": true, |
|||
"parser": "babel-eslint", |
|||
"extends": "eslint:recommended", |
|||
"env": { |
|||
"es6": true, |
|||
"node": true |
|||
}, |
|||
"globals": {}, |
|||
"plugins": [], |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.integration.js","*.spec.js"], |
|||
"rules": { |
|||
"no-magic-numbers": "off" |
|||
} |
|||
}, |
|||
{ |
|||
"files": ["*.js"], |
|||
"rules": { |
|||
"prefer-arrow-callback": 0, |
|||
"no-process-env": 0, |
|||
"no-warning-comments": 0, |
|||
"prefer-template": 0, |
|||
"no-sync": 0 |
|||
} |
|||
} |
|||
], |
|||
"rules": { |
|||
//Possible Errors |
|||
"comma-dangle": 0, //disallow or enforce trailing commas |
|||
"no-cond-assign": 2, //disallow assignment in conditional expressions |
|||
"no-console": 2, //disallow use of console in the node environment |
|||
"no-constant-condition": 1, //disallow use of constant expressions in conditions |
|||
"no-control-regex": 2, //disallow control characters in regular expressions |
|||
"no-debugger": 2, //disallow use of debugger |
|||
"no-dupe-args": 2, //disallow duplicate arguments in functions |
|||
"no-dupe-keys": 2, //disallow duplicate keys when creating object literals |
|||
"no-duplicate-case": 2, //disallow a duplicate case label. |
|||
"no-empty-character-class": 2, //disallow the use of empty character classes in regular expressions |
|||
"no-empty": 2, //disallow empty statements |
|||
"no-ex-assign": 2, //disallow assigning to the exception in a catch block |
|||
"no-extra-boolean-cast": 2, //disallow double-negation boolean casts in a boolean context |
|||
"no-extra-parens": 2, //disallow unnecessary parentheses |
|||
"no-extra-semi": 2, //disallow unnecessary semicolons |
|||
"no-func-assign": 2, //disallow overwriting functions written as function declarations |
|||
"no-inner-declarations": 1, //disallow function or variable declarations in nested blocks |
|||
"no-invalid-regexp": 2, //disallow invalid regular expression strings in the RegExp constructor |
|||
"no-irregular-whitespace": 2, //disallow irregular whitespace outside of strings and comments |
|||
"no-negated-in-lhs": 2, //disallow negation of the left operand of an in expression |
|||
"no-obj-calls": 2, //disallow the use of object properties of the global object (Math and JSON) as functions |
|||
"no-prototype-builtins": 2, //Disallow use of Object.prototypes builtins directly |
|||
"no-regex-spaces": 2, //disallow multiple spaces in a regular expression literal |
|||
"no-sparse-arrays": 1, //disallow sparse arrays |
|||
"no-unexpected-multiline": 2, //Avoid code that looks like two expressions but is actually one |
|||
"no-unreachable": 2, //disallow unreachable statements after a return, throw, continue, or break statement |
|||
"no-unsafe-finally": 2, //disallow control flow statements in finally blocks |
|||
"use-isnan": 2, //disallow comparisons with the value NaN |
|||
"valid-jsdoc": 2, //Ensure JSDoc comments are valid |
|||
"valid-typeof": 2, //Ensure that the results of typeof are compared against a valid string |
|||
|
|||
//Best Practices |
|||
"accessor-pairs": 0, //Enforces getter/setter pairs in objects |
|||
"array-callback-return": 2, //Enforces return statements in callbacks of array"s methods |
|||
"block-scoped-var": 2, //treat var statements as if they were block scoped |
|||
"complexity": 1, //specify the maximum cyclomatic complexity allowed in a program |
|||
"consistent-return": 0, //require return statements to either always or never specify values |
|||
"curly": 2, //specify curly brace conventions for all control statements |
|||
"default-case": 2, //require default case in switch statements |
|||
"dot-location": [2, "property"], //enforces consistent newlines before or after dots |
|||
"dot-notation": 2, //encourages use of dot notation whenever possible |
|||
"eqeqeq": 2, //require the use of === and !== |
|||
"guard-for-in": 2, //make sure for-in loops have an if statement |
|||
"no-alert": 2, //disallow the use of alert, confirm, and prompt |
|||
"no-caller": 2, //disallow use of arguments.caller or arguments.callee |
|||
"no-case-declarations": 0, //disallow lexical declarations in case clauses |
|||
"no-div-regex": 2, //disallow division operators explicitly at beginning of regular expression |
|||
"no-else-return": 0, //disallow else after a return in an if |
|||
"no-empty-function": 2, //disallow use of empty functions |
|||
"no-empty-pattern": 2, //disallow use of empty destructuring patterns |
|||
"no-eq-null": 2, //disallow comparisons to null without a type-checking operator |
|||
"no-eval": 2, //disallow use of eval() |
|||
"no-extend-native": 0, //disallow adding to native types |
|||
"no-extra-bind": 1, //disallow unnecessary function binding |
|||
"no-extra-label": 2, //disallow unnecessary labels |
|||
"no-fallthrough": 2, //disallow fallthrough of case statements |
|||
"no-floating-decimal": 2, //disallow the use of leading or trailing decimal points in numeric literals |
|||
"no-implicit-coercion": 0, //disallow the type conversions with shorter notations |
|||
"no-implicit-globals": 0, //disallow var and named functions in global scope |
|||
"no-implied-eval": 2, //disallow use of eval()-like methods |
|||
"no-invalid-this": 2, //disallow this keywords outside of classes or class-like objects |
|||
"no-iterator": 2, //disallow usage of __iterator__ property |
|||
"no-labels": 2, //disallow use of labeled statements |
|||
"no-lone-blocks": 2, //disallow unnecessary nested blocks |
|||
"no-loop-func": 2, //disallow creation of functions within loops |
|||
"no-magic-numbers": [2, { "ignore": [0,1,-1] }], //disallow the use of magic numbers other than 0 or 1 |
|||
"no-multi-spaces": 2, //disallow use of multiple spaces |
|||
"no-multi-str": 0, //disallow use of multiline strings |
|||
"no-native-reassign": 2, //disallow reassignments of native objects |
|||
"no-new-func": 1, //disallow use of new operator for Function object |
|||
"no-new-wrappers": 2, //disallows creating new instances of String,Number, and Boolean |
|||
"no-new": 2, //disallow use of the new operator when not part of an assignment or comparison |
|||
"no-octal-escape": 0, //disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; |
|||
"no-octal": 0, //disallow use of octal literals |
|||
"no-param-reassign": 2, //disallow reassignment of function parameters |
|||
"no-process-env": 2, //disallow use of process.env |
|||
"no-proto": 2, //disallow usage of __proto__ property |
|||
"no-redeclare": 2, //disallow declaring the same variable more than once |
|||
"no-return-assign": 2, //disallow use of assignment in return statement |
|||
"no-script-url": 2, //disallow use of javascript: urls. |
|||
"no-self-assign": 2, //disallow assignments where both sides are exactly the same |
|||
"no-self-compare": 2, //disallow comparisons where both sides are exactly the same |
|||
"no-sequences": 2, //disallow use of the comma operator |
|||
"no-throw-literal": 0, //restrict what can be thrown as an exception |
|||
"no-unmodified-loop-condition": 2, //disallow unmodified conditions of loops |
|||
"no-unused-expressions": 0, //disallow usage of expressions in statement position |
|||
"no-unused-labels": 2, //disallow unused labels |
|||
"no-useless-call": 2, //disallow unnecessary .call() and .apply() |
|||
"no-useless-concat": 2, //disallow unnecessary concatenation of literals or template literals |
|||
"no-useless-escape": 2, //disallow unnecessary escape characters |
|||
"no-void": 0, //disallow use of the void operator |
|||
"no-warning-comments": 1, //disallow usage of configurable warning terms in comments (e.g. TODO or FIXME) |
|||
"no-with": 2, //disallow use of the with statement |
|||
"radix": 2, //require use of the second argument for parseInt() |
|||
"vars-on-top": 0, //require declaration of all vars at the top of their containing scope |
|||
"wrap-iife": 2, //require immediate function invocation to be wrapped in parentheses |
|||
"yoda": 2, //require or disallow Yoda conditions |
|||
|
|||
//Strict Mode |
|||
"strict": 0, //controls location of Use Strict Directives |
|||
|
|||
//Variables |
|||
"init-declarations": 0, //enforce or disallow variable initializations at definition |
|||
"no-catch-shadow": 2, //disallow the catch clause parameter name being the same as a variable in the outer scope |
|||
"no-delete-var": 2, //disallow deletion of variables |
|||
"no-label-var": 2, //disallow labels that share a name with a variable |
|||
"no-restricted-globals": 0, //restrict usage of specified global variables |
|||
"no-shadow-restricted-names": 2, //disallow shadowing of names such as arguments |
|||
"no-shadow": [2, {"allow": ["err"]}], //disallow declaration of variables already declared in the outer scope |
|||
"no-undef-init": 2, //disallow use of undefined when initializing variables |
|||
"no-undef": 2, //disallow use of undeclared variables unless mentioned in a /*global */ block |
|||
"no-undefined": 0, //disallow use of undefined variable |
|||
"no-unused-vars": 2, //disallow declaration of variables that are not used in the code |
|||
"no-use-before-define": [2, { "functions": false }], //disallow use of variables before they are defined |
|||
|
|||
//Node.js and CommonJS |
|||
"callback-return": 2, //enforce return after a callback |
|||
"global-require": 0, //enforce require() on top-level module scope |
|||
"handle-callback-err": 2, //enforce error handling in callbacks |
|||
"no-mixed-requires": 2, //disallow mixing regular variable and require declarations |
|||
"no-new-require": 2, //disallow use of new operator with the require function |
|||
"no-path-concat": 2, //disallow string concatenation with __dirname and __filename |
|||
"no-process-exit": 2, //disallow process.exit() |
|||
"no-restricted-imports": 0, //restrict usage of specified node imports |
|||
"no-restricted-modules": 0, //restrict usage of specified node modules |
|||
"no-sync": 1, //disallow use of synchronous methods |
|||
|
|||
//Stylistic Issues |
|||
"array-bracket-spacing": [2, "never"], //enforce spacing inside array brackets |
|||
"block-spacing": 0, //disallow or enforce spaces inside of single line blocks |
|||
"brace-style": 2, //enforce one true brace style |
|||
"camelcase": 1, //require camel case names |
|||
"comma-spacing": [2, {"before": false, "after": true}], //enforce spacing before and after comma |
|||
"comma-style": 2, //enforce one true comma style |
|||
"computed-property-spacing": 2, //require or disallow padding inside computed properties |
|||
"consistent-this": 2, //enforce consistent naming when capturing the current execution context |
|||
"eol-last": 2, //enforce newline at the end of file, with no multiple empty lines |
|||
"func-names": 0, //require function expressions to have a name |
|||
"func-style": 0, //enforce use of function declarations or expressions |
|||
"id-blacklist": 0, //blacklist certain identifiers to prevent them being used |
|||
"id-length": [2, { //this option enforces minimum and maximum identifier lengths (variable names, property names etc.) |
|||
"min": 2, |
|||
"max": 25, |
|||
"exceptions": ["_"] |
|||
}], |
|||
"id-match": 0, //require identifiers to match the provided regular expression |
|||
"indent": ["error", 2], //specify tab or space width for your code |
|||
"jsx-quotes": 0, //specify whether double or single quotes should be used in JSX attributes |
|||
"key-spacing": 2, //enforce spacing between keys and values in object literal properties |
|||
"keyword-spacing": [2, { |
|||
"before": true, |
|||
"after": true |
|||
}], //enforce spacing before and after keywords |
|||
"linebreak-style": 2, //disallow mixed "LF" and "CRLF" as linebreaks |
|||
"lines-around-comment": ["error", { |
|||
"beforeLineComment": true, |
|||
"allowBlockStart": true |
|||
} |
|||
], //enforce empty lines around comments |
|||
"max-depth": 1, //specify the maximum depth that blocks can be nested |
|||
"max-len": [1, 120], //specify the maximum length of a line in your program |
|||
"max-lines": [1, 500], //enforce a maximum file length |
|||
"max-nested-callbacks": 2, //specify the maximum depth callbacks can be nested |
|||
"max-params": [1, 5], //limits the number of parameters that can be used in the function declaration. |
|||
"max-statements": [1, 50], //specify the maximum number of statement allowed in a function |
|||
"max-statements-per-line": 1, //enforce a maximum number of statements allowed per line |
|||
"new-cap": 0, //require a capital letter for constructors |
|||
"new-parens": 2, //disallow the omission of parentheses when invoking a constructor with no arguments |
|||
"newline-after-var": 0, //require or disallow an empty newline after variable declarations |
|||
"newline-before-return": 2, //require newline before return statement |
|||
"newline-per-chained-call": 0, //enforce newline after each call when chaining the calls |
|||
"no-array-constructor": 2, //disallow use of the Array constructor |
|||
"no-bitwise": 0, //disallow use of bitwise operators |
|||
"no-continue": 0, //disallow use of the continue statement |
|||
"no-inline-comments": 0, //disallow comments inline after code |
|||
"no-lonely-if": 2, //disallow if as the only statement in an else block |
|||
"no-mixed-operators": 0, //disallow mixes of different operators |
|||
"no-mixed-spaces-and-tabs": 2, //disallow mixed spaces and tabs for indentation |
|||
"no-multiple-empty-lines": 2, //disallow multiple empty lines |
|||
"no-negated-condition": 0, //disallow negated conditions |
|||
"no-nested-ternary": 2, //disallow nested ternary expressions |
|||
"no-new-object": 2, //disallow the use of the Object constructor |
|||
"no-plusplus": 0, //disallow use of unary operators, ++ and -- |
|||
"no-restricted-syntax": 0, //disallow use of certain syntax in code |
|||
"no-spaced-func": 2, //disallow space between function identifier and application |
|||
"no-ternary": 0, //disallow the use of ternary operators |
|||
"no-trailing-spaces": 2, //disallow trailing whitespace at the end of lines |
|||
"no-underscore-dangle": 0, //disallow dangling underscores in identifiers |
|||
"no-unneeded-ternary": 2, //disallow the use of ternary operators when a simpler alternative exists |
|||
"no-whitespace-before-property": 2, //disallow whitespace before properties |
|||
"object-curly-newline": 2, //enforce consistent line breaks inside braces |
|||
"object-curly-spacing": 2, //require or disallow padding inside curly braces |
|||
"object-property-newline": [2, { //enforce placing object properties on either one line or all on separate lines |
|||
"allowAllPropertiesOnSameLine": true |
|||
} |
|||
], |
|||
"one-var": [2, "never"], //require or disallow one variable declaration per function |
|||
"one-var-declaration-per-line": 2, //require or disallow an newline around variable declarations |
|||
"operator-assignment": 0, //require assignment operator shorthand where possible or prohibit it entirely |
|||
"operator-linebreak": [1, "before"], //enforce operators to be placed before or after line breaks |
|||
"padded-blocks": 0, //enforce padding within blocks |
|||
"quote-props": [2, "as-needed"], //require quotes around object literal property names |
|||
"quotes": [2, "single"], //specify whether backticks, double or single quotes should be used |
|||
"require-jsdoc": 0, //Require JSDoc comment |
|||
"semi-spacing": 2, //enforce spacing before and after semicolons |
|||
"sort-imports": 0, //sort import declarations within module |
|||
"semi": 2, //require or disallow use of semicolons instead of ASI |
|||
"sort-vars": 0, //sort variables within the same declaration block |
|||
"space-before-blocks": 2, //require or disallow a space before blocks |
|||
"space-before-function-paren": [2, "never"], //require or disallow a space before function opening parenthesis |
|||
"space-in-parens": 2, //require or disallow spaces inside parentheses |
|||
"space-infix-ops": 2, //require spaces around operators |
|||
"space-unary-ops": 2, //require or disallow spaces before/after unary operators |
|||
"spaced-comment": [2, "always", { "exceptions": ["*"] }], //require or disallow a space immediately following the // or /* in a comment |
|||
"unicode-bom": 0, //require or disallow the Unicode BOM |
|||
"wrap-regex": 0, //require regex literals to be wrapped in parentheses |
|||
|
|||
//ECMAScript 6 |
|||
"arrow-body-style": [2, "as-needed"], //require braces in arrow function body |
|||
"arrow-parens": [2, "as-needed"], //require parens in arrow function arguments |
|||
"arrow-spacing": 2, //require space before/after arrow function"s arrow |
|||
"constructor-super": 2, //verify calls of super() in constructors |
|||
"generator-star-spacing": 0, //enforce spacing around the * in generator functions |
|||
"no-class-assign": 2, //disallow modifying variables of class declarations |
|||
"no-confusing-arrow": 2, //disallow arrow functions where they could be confused with comparisons |
|||
"no-const-assign": 2, //disallow modifying variables that are declared using const |
|||
"no-dupe-class-members": 2, //disallow duplicate name in class members |
|||
"no-duplicate-imports": 2, //disallow duplicate module imports |
|||
"no-new-symbol": 2, //disallow use of the new operator with the Symbol object |
|||
"no-this-before-super": 2, //disallow use of this/super before calling super() in constructors. |
|||
"no-useless-computed-key": 2, //disallow unnecessary computed property keys in object literals |
|||
"no-useless-constructor": 2, //disallow unnecessary constructor |
|||
"no-useless-rename": 2, //disallow renaming import, export, and destructured assignments to the same name |
|||
"no-var": 0, //require let or const instead of var |
|||
"object-shorthand": 1, //require method and property shorthand syntax for object literals |
|||
"prefer-arrow-callback": 1, //suggest using arrow functions as callbacks |
|||
"prefer-const": 1, //suggest using const declaration for variables that are never modified after declared |
|||
"prefer-rest-params": 1, //suggest using the rest parameters instead of arguments |
|||
"prefer-spread": 1, //suggest using the spread operator instead of .apply(). |
|||
"prefer-template": 1, //suggest using template literals instead of strings concatenation |
|||
"require-yield": 0, //disallow generator functions that do not have yield |
|||
"rest-spread-spacing": ["error", "never"], //enforce spacing between rest and spread operators and their expressions |
|||
"template-curly-spacing": 2, //enforce spacing around embedded expressions of template strings |
|||
"yield-star-spacing": [2, "after"] //enforce spacing around the * in yield* expressions |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
--- |
|||
name: Feature request |
|||
about: Suggest an idea for this project |
|||
title: '' |
|||
labels: '' |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Issue** |
|||
A clear and concise description of what the problem is. |
|||
|
|||
**Proposed Solution** |
|||
A clear and concise description of what you want to happen. |
|||
|
|||
**Dependencies** |
|||
1. Links to PRs |
|||
2. Links to Issues |
|||
3. Description of other dependencies |
|||
|
|||
**Additional context** |
|||
Add any other context or screenshots about the feature request here. |
@ -0,0 +1,13 @@ |
|||
node_modules/ |
|||
npm-debug.log |
|||
/.idea/ |
|||
/tls.cert |
|||
*.log |
|||
*.env |
|||
logs/ |
|||
package-lock.json |
|||
*.bak |
|||
lb_settings.json |
|||
.env |
|||
.nyc_output |
|||
coverage |
@ -0,0 +1,28 @@ |
|||
# specify the node base image with your desired version node:<version> |
|||
FROM node:8-slim |
|||
|
|||
# install tools |
|||
RUN apt-get update --no-install-recommends \ |
|||
&& apt-get install -y --no-install-recommends vim \ |
|||
&& apt-get install -y --no-install-recommends rsync \ |
|||
&& rm -rf /var/lib/apt/lists/* |
|||
|
|||
# Create app directory |
|||
WORKDIR /usr/src/app |
|||
|
|||
# Install app dependencies |
|||
# A wildcard is used to ensure both package.json AND package-lock.json are copied |
|||
# where available (npm@5+) |
|||
COPY package*.json ./ |
|||
|
|||
RUN npm install |
|||
# If you are building your code for production |
|||
# RUN npm install --only=production |
|||
|
|||
# Bundle app source |
|||
COPY . . |
|||
|
|||
RUN mkdir -p /root/.lnd |
|||
|
|||
EXPOSE 3005 |
|||
CMD [ "npm", "start" ] |
@ -0,0 +1,33 @@ |
|||
# specify the node base image with your desired version |
|||
FROM balenalib/armv7hf-node:8-stretch-run |
|||
|
|||
# need qemu to emulate arm architecture |
|||
# can be downloaded here, $ docker run -v /usr/bin/qemu-arm-static:/usr/bin/qemu-arm-static --rm -ti arm32v7/debian:stretch-slim |
|||
COPY ./qemu-arm-static /usr/bin/qemu-arm-static |
|||
|
|||
# install tools |
|||
RUN apt-get update --no-install-recommends \ |
|||
&& apt-get install -y --no-install-recommends curl \ |
|||
&& apt-get install -y --no-install-recommends rsync \ |
|||
&& apt-get install -y --no-install-recommends vim \ |
|||
&& rm -rf /var/lib/apt/lists/* |
|||
|
|||
# Create app directory |
|||
WORKDIR /usr/src/app |
|||
|
|||
# Install app dependencies |
|||
# A wildcard is used to ensure both package.json AND package-lock.json are copied |
|||
# where available (npm@5+) |
|||
COPY package*.json ./ |
|||
|
|||
RUN npm install |
|||
# If you are building your code for production |
|||
# RUN npm install --only=production |
|||
|
|||
# Bundle app source |
|||
COPY . . |
|||
|
|||
RUN mkdir -p /root/.lnd |
|||
|
|||
EXPOSE 3005 |
|||
CMD [ "npm", "start" ] |
@ -0,0 +1,21 @@ |
|||
The MIT License |
|||
|
|||
Copyright (c) 2018-2019 Casa, Inc. https://keys.casa/ |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in |
|||
all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
THE SOFTWARE. |
@ -0,0 +1,16 @@ |
|||
.PHONY: install-prehook install install-dev lint test |
|||
|
|||
install-prehook: |
|||
cp pre-commit .git/hooks/ |
|||
|
|||
install: install-prehook |
|||
npm install |
|||
|
|||
install-dev: |
|||
npm install --dev |
|||
|
|||
lint: |
|||
eslint . |
|||
|
|||
test: |
|||
npm test |
@ -0,0 +1,34 @@ |
|||
# API connecting the Umbrel Dashboard to bitcoind & lnd |
|||
|
|||
### How to use: |
|||
|
|||
1. To `/home/umbrel/docker-compose.yml`, add the following service: |
|||
``` |
|||
middleware: |
|||
image: getumbrel/middleware:v0.0.1 |
|||
depends_on: [ bitcoin, lnd ] |
|||
logging: *default-logging |
|||
restart: on-failure |
|||
network_mode: host |
|||
volumes: |
|||
- "/home/umbrel/lnd:/lnd" |
|||
environment: |
|||
BITCOIN_NETWORK: "mainnet" |
|||
BITCOIN_HOST: "0.0.0.0" |
|||
RPC_USER: "<your rpc username>" |
|||
RPC_PASSWORD: "<your rpc password>" |
|||
LND_NETWORK: "mainnet" |
|||
LND_HOST: "127.0.0.1" |
|||
``` |
|||
|
|||
2. Run `docker-compose up -d` |
|||
|
|||
3. Test bitcoin from a computer connected to the same network |
|||
``` |
|||
curl http://umbrel.local:3005/v1/bitcoind/info/status |
|||
``` |
|||
|
|||
4. Test lightning from a computer connected to the same network |
|||
``` |
|||
curl http://umbrel.local:3005/v1/lnd/info/status |
|||
``` |
@ -0,0 +1,64 @@ |
|||
require('module-alias/register'); |
|||
require('module-alias').addPath('.'); |
|||
const express = require('express'); |
|||
const path = require('path'); |
|||
const morgan = require('morgan'); |
|||
const bodyParser = require('body-parser'); |
|||
const passport = require('passport'); |
|||
const cors = require('cors'); |
|||
|
|||
// Keep requestCorrelationId middleware as the first middleware. Otherwise we risk losing logs.
|
|||
const requestCorrelationMiddleware = require('middlewares/requestCorrelationId.js'); // eslint-disable-line id-length
|
|||
const camelCaseReqMiddleware = require('middlewares/camelCaseRequest.js').camelCaseRequest; |
|||
const onionOriginMiddleware = require('middlewares/onionOrigin.js'); |
|||
const corsOptions = require('middlewares/cors.js').corsOptions; |
|||
const errorHandleMiddleware = require('middlewares/errorHandling.js'); |
|||
require('middlewares/auth.js'); |
|||
|
|||
const logger = require('utils/logger.js'); |
|||
|
|||
const bitcoind = require('routes/v1/bitcoind/info.js'); |
|||
const address = require('routes/v1/lnd/address.js'); |
|||
const channel = require('routes/v1/lnd/channel.js'); |
|||
const info = require('routes/v1/lnd/info.js'); |
|||
const lightning = require('routes/v1/lnd/lightning.js'); |
|||
const transaction = require('routes/v1/lnd/transaction.js'); |
|||
const util = require('routes/v1/lnd/util.js'); |
|||
const wallet = require('routes/v1/lnd/wallet.js'); |
|||
const pages = require('routes/v1/pages.js'); |
|||
const ping = require('routes/ping.js'); |
|||
const app = express(); |
|||
|
|||
// Handle Cors for Tor Browser 9.0.0 bug and options requests
|
|||
app.use(onionOriginMiddleware); |
|||
|
|||
// Handles Cors for normal requests
|
|||
app.use(cors(corsOptions)); |
|||
|
|||
app.use(bodyParser.json()); |
|||
app.use(bodyParser.urlencoded({extended: true})); |
|||
app.use(express.static(path.join(__dirname, 'public'))); |
|||
app.use(passport.initialize()); |
|||
app.use(passport.session()); |
|||
|
|||
app.use(requestCorrelationMiddleware); |
|||
app.use(camelCaseReqMiddleware); |
|||
app.use(morgan(logger.morganConfiguration)); |
|||
|
|||
app.use('/v1/bitcoind/info', bitcoind); |
|||
app.use('/v1/lnd/address', address); |
|||
app.use('/v1/lnd/channel', channel); |
|||
app.use('/v1/lnd/info', info); |
|||
app.use('/v1/lnd/lightning', lightning); |
|||
app.use('/v1/lnd/transaction', transaction); |
|||
app.use('/v1/lnd/wallet', wallet); |
|||
app.use('/v1/lnd/util', util); |
|||
app.use('/v1/pages', pages); |
|||
app.use('/ping', ping); |
|||
|
|||
app.use(errorHandleMiddleware); |
|||
app.use((req, res) => { |
|||
res.status(404).json(); // eslint-disable-line no-magic-numbers
|
|||
}); |
|||
|
|||
module.exports = app; |
@ -0,0 +1,91 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
/** |
|||
* Module dependencies. |
|||
*/ |
|||
|
|||
var app = require('../app'); |
|||
var debug = require('debug')('nodejs-regular-webapp2:server'); |
|||
var http = require('http'); |
|||
|
|||
/** |
|||
* Get port from environment and store in Express. |
|||
*/ |
|||
|
|||
var port = normalizePort(process.env.PORT || '3005'); |
|||
app.set('port', port); |
|||
|
|||
/** |
|||
* Create HTTP server. |
|||
*/ |
|||
|
|||
var server = http.createServer(app); |
|||
|
|||
/** |
|||
* Listen on provided port, on all network interfaces. |
|||
*/ |
|||
|
|||
server.listen(port); |
|||
server.on('error', onError); |
|||
server.on('listening', onListening); |
|||
|
|||
/** |
|||
* Normalize a port into a number, string, or false. |
|||
*/ |
|||
|
|||
function normalizePort(val) { |
|||
var port = parseInt(val, 10); |
|||
|
|||
if (isNaN(port)) { |
|||
// named pipe |
|||
return val; |
|||
} |
|||
|
|||
if (port >= 0) { |
|||
// port number |
|||
return port; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Event listener for HTTP server "error" event. |
|||
*/ |
|||
|
|||
function onError(error) { |
|||
if (error.syscall !== 'listen') { |
|||
throw error; |
|||
} |
|||
|
|||
var bind = typeof port === 'string' |
|||
? 'Pipe ' + port |
|||
: 'Port ' + port; |
|||
|
|||
// handle specific listen errors with friendly messages |
|||
switch (error.code) { |
|||
case 'EACCES': |
|||
console.error(bind + ' requires elevated privileges'); |
|||
process.exit(1); |
|||
break; |
|||
case 'EADDRINUSE': |
|||
console.error(bind + ' is already in use'); |
|||
process.exit(1); |
|||
break; |
|||
default: |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Event listener for HTTP server "listening" event. |
|||
*/ |
|||
|
|||
function onListening() { |
|||
var addr = server.address(); |
|||
var bind = typeof addr === 'string' |
|||
? 'pipe ' + addr |
|||
: 'port ' + addr.port; |
|||
debug('Listening on ' + bind); |
|||
console.log('Listening on ' + bind); |
|||
} |
@ -0,0 +1,19 @@ |
|||
#!/usr/bin/env bash |
|||
|
|||
if [[ "$ARCH" != "arm" && "$ARCH" != "x86" ]] ; then |
|||
echo 'Please set an ARCH to x86 or arm.' |
|||
return |
|||
fi |
|||
|
|||
if [[ -z "$ENV" ]] ; then |
|||
echo 'Please set an ENV variable.' |
|||
return |
|||
fi |
|||
|
|||
DOCKERFILE="Dockerfile" |
|||
if [ "$ARCH" = "arm" ]; then |
|||
DOCKERFILE="Dockerfile.armhf" |
|||
fi |
|||
|
|||
docker build . -f $DOCKERFILE -t casanodeinternal/lnapi:$ARCH-$ENV |
|||
docker push casanodeinternal/lnapi:$ARCH-$ENV |
@ -0,0 +1,88 @@ |
|||
version: 0.2 |
|||
env: |
|||
parameter-store: |
|||
DOCKER_HUB_USER: "/Casanode/DockerHub/User" |
|||
DOCKER_HUB_PASS: "/Casanode/DockerHub/Pass" |
|||
GITHUB_USER: "/Casanode/Git/User" |
|||
GITHUB_PASS: "/Casanode/Git/Pass" |
|||
phases: |
|||
pre_build: |
|||
commands: |
|||
- echo Installing source NPM dependencies... |
|||
- npm install |
|||
install: |
|||
commands: |
|||
# CodePipeline creates artifacts using zip format, which does not preserve the permissions/modes. |
|||
# we must reset permissions here |
|||
- chmod 755 pre-commit qemu-arm-static |
|||
build: |
|||
commands: |
|||
- echo Running tests |
|||
- if [ -z $CODECOV_TOKEN ]; then npm run test; else npm run coverage; fi |
|||
|
|||
- echo Building the Docker image ... |
|||
|
|||
# building |
|||
# Remove qemu-static if non-arm, or register |
|||
- | |
|||
if [ $ARCH != arm ]; then |
|||
rm qemu-arm-static |
|||
else |
|||
docker run --rm --privileged multiarch/qemu-user-static:register --reset |
|||
fi |
|||
- docker build . -f $DOCKERFILE -t $ORGANIZATION/$REPOSITORY:$ARCH |
|||
- docker tag $ORGANIZATION/$REPOSITORY:$ARCH $ORGANIZATION/$REPOSITORY:$ARCH |
|||
|
|||
# Hacky way to determine what branch we are in. $CODEBUILD_SOURCE_VERSION is the git commit we are currently |
|||
# building. We search all local branches to get a list of branches that include that git commit. We return all |
|||
# alpha characters from the branch we are looking for. |
|||
# |
|||
# Ex. |
|||
# master => master |
|||
# release/1.0.0 => release |
|||
|
|||
- masterBranchText=$(git branch --contains $CODEBUILD_SOURCE_VERSION | grep master | sed 's/[^a-zA-Z]//g') |
|||
- releaseBranchText=$(git branch --contains $CODEBUILD_SOURCE_VERSION | grep release | sed 's/[^a-zA-Z]//g') |
|||
- developBranchText=$(git branch --contains $CODEBUILD_SOURCE_VERSION | grep develop | sed 's/[^a-zA-Z]//g') |
|||
|
|||
# push image to docker |
|||
- docker login --username=$DOCKER_HUB_USER --password=$DOCKER_HUB_PASS |
|||
|
|||
# Since a git commit can be in multiple branches, we will start with master and work our way down. If a git commit |
|||
# exists in master, release, and develop, it really means we want to deploy master. Likewise, if a git commit exists |
|||
# in release and develop, we release want to deploy release. |
|||
# |
|||
# Public vs Private |
|||
# Casa Inc releases code publicly for various reasons. We also develop features in private before the public |
|||
# release. Historically we have used the private casacomputer organization on docker hub. We have since migrated |
|||
# to casanode for our public releases. We will support legacy nodes running on casacomputer until March 2020. |
|||
- | |
|||
if [ "$masterBranchText" = "master" ] && [ "$PUBLIC" = "false" ]; then |
|||
echo "pushing master branch to docker hub" |
|||
docker tag $ORGANIZATION/$REPOSITORY:$ARCH $ORGANIZATION/$REPOSITORY:$ARCH |
|||
docker push $ORGANIZATION/$REPOSITORY:$ARCH |
|||
elif [ "$masterBranchText" = "master" ] && [ "$PUBLIC" = "true" ]; then |
|||
echo "pushing master branch to docker hub" |
|||
docker tag $ORGANIZATION/$REPOSITORY:$ARCH $ORGANIZATION/$REPOSITORY:$ARCH |
|||
docker push $ORGANIZATION/$REPOSITORY:$ARCH |
|||
|
|||
echo "pushing master branch to legacy docker hub" |
|||
docker tag casacomputer/$REPOSITORY:$ARCH casacomputer/$REPOSITORY:$ARCH |
|||
docker push casacomputer/$REPOSITORY:$ARCH |
|||
elif [ "$releaseBranchText" = "release" ] && [ "$PUBLIC" = "false" ]; then |
|||
echo "pushing release branch to docker hub" |
|||
docker tag $ORGANIZATION/$REPOSITORY:$ARCH $ORGANIZATION/$REPOSITORY:$ARCH-stage |
|||
docker push $ORGANIZATION/$REPOSITORY:$ARCH-stage |
|||
elif [ "$developBranchText" = "develop" ] && [ "$PUBLIC" = "false" ]; then |
|||
echo "pushing develop branch to docker hub" |
|||
docker tag $ORGANIZATION/$REPOSITORY:$ARCH $ORGANIZATION/$REPOSITORY:$ARCH-develop |
|||
docker push $ORGANIZATION/$REPOSITORY:$ARCH-develop |
|||
else |
|||
echo "docker image has been built, but not pushed to docker hub" |
|||
fi |
|||
post_build: |
|||
commands: |
|||
- echo Build completed on `date` |
|||
cache: |
|||
paths: |
|||
- 'node_modules/**/*' |
@ -0,0 +1,12 @@ |
|||
const bashService = require('services/bash.js'); |
|||
|
|||
const LND_DATA_SOURCE_DIRECTORY = '/lnd/'; |
|||
const LND_BACKUP_DEST_DIRECTORY = '/lndBackup'; |
|||
async function lndBackup() { |
|||
|
|||
// eslint-disable-next-line max-len
|
|||
await bashService.exec('rsync', ['-r', '--delete', LND_DATA_SOURCE_DIRECTORY, LND_BACKUP_DEST_DIRECTORY]); |
|||
} |
|||
module.exports = { |
|||
lndBackup, |
|||
}; |
@ -0,0 +1,112 @@ |
|||
const bitcoindService = require('services/bitcoind.js'); |
|||
const BitcoindError = require('models/errors.js').BitcoindError; |
|||
|
|||
async function getBlockCount() { |
|||
const blockCount = await bitcoindService.getBlockCount(); |
|||
|
|||
return {blockCount: blockCount.result}; |
|||
} |
|||
|
|||
async function getConnectionsCount() { |
|||
const peerInfo = await bitcoindService.getPeerInfo(); |
|||
|
|||
var outBoundConnections = 0; |
|||
var inBoundConnections = 0; |
|||
|
|||
peerInfo.result.forEach(function(peer) { |
|||
if (peer.inbound === false) { |
|||
outBoundConnections++; |
|||
|
|||
return; |
|||
} |
|||
inBoundConnections++; |
|||
}); |
|||
|
|||
const connections = { |
|||
total: inBoundConnections + outBoundConnections, |
|||
inbound: inBoundConnections, |
|||
outbound: outBoundConnections |
|||
}; |
|||
|
|||
return connections; |
|||
} |
|||
|
|||
async function getStatus() { |
|||
try { |
|||
await bitcoindService.help(); |
|||
|
|||
return {operational: true}; |
|||
} catch (error) { |
|||
if (error instanceof BitcoindError) { |
|||
return {operational: false}; |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
// Return the max synced header for all connected peers or -1 if no data is available.
|
|||
async function getMaxSyncHeader() { |
|||
const peerInfo = (await bitcoindService.getPeerInfo()).result; |
|||
|
|||
if (peerInfo.length === 0) { |
|||
return -1; |
|||
} |
|||
|
|||
const maxPeer = peerInfo.reduce(function(prev, current) { |
|||
return prev.syncedHeaders > current.syncedHeaders ? prev : current; |
|||
}); |
|||
|
|||
return maxPeer.syncedHeaders; |
|||
} |
|||
|
|||
async function getMempoolInfo() { |
|||
return await bitcoindService.getMempoolInfo(); |
|||
} |
|||
|
|||
async function getLocalSyncInfo() { |
|||
const info = await bitcoindService.getBlockChainInfo(); |
|||
|
|||
var blockChainInfo = info.result; |
|||
|
|||
var blockCount = blockChainInfo.blocks; |
|||
var headerCount = blockChainInfo.headers; |
|||
|
|||
const percentSynced = (Math.trunc(blockCount / headerCount * 10000) / 10000).toFixed(4); // eslint-disable-line no-magic-numbers, max-len
|
|||
|
|||
return { |
|||
percent: percentSynced, |
|||
currentBlock: blockCount, |
|||
headerCount: headerCount // eslint-disable-line object-shorthand
|
|||
}; |
|||
} |
|||
|
|||
async function getSyncStatus() { |
|||
const maxPeerHeader = await getMaxSyncHeader(); |
|||
const localSyncInfo = await getLocalSyncInfo(); |
|||
|
|||
if (maxPeerHeader > localSyncInfo.headerCount) { |
|||
localSyncInfo.headerCount = maxPeerHeader; |
|||
} |
|||
|
|||
return localSyncInfo; |
|||
} |
|||
|
|||
async function getVersion() { |
|||
const networkInfo = await bitcoindService.getNetworkInfo(); |
|||
const unformattedVersion = networkInfo.result.subversion; |
|||
|
|||
// Remove all non-digits or decimals.
|
|||
const version = unformattedVersion.replace(/[^\d.]/g, ''); |
|||
|
|||
return {version: version}; // eslint-disable-line object-shorthand
|
|||
} |
|||
|
|||
module.exports = { |
|||
getBlockCount, |
|||
getConnectionsCount, |
|||
getMempoolInfo, |
|||
getStatus, |
|||
getSyncStatus, |
|||
getVersion |
|||
}; |
@ -0,0 +1,16 @@ |
|||
const constants = require('utils/const.js'); |
|||
const diskService = require('services/disk'); |
|||
|
|||
function readManagedChannelsFile() { |
|||
return diskService.readJsonFile(constants.MANAGED_CHANNELS_FILE) |
|||
.catch(() => Promise.resolve({})); |
|||
} |
|||
|
|||
function writeManagedChannelsFile(data) { |
|||
return diskService.writeJsonFile(constants.MANAGED_CHANNELS_FILE, data); |
|||
} |
|||
|
|||
module.exports = { |
|||
readManagedChannelsFile, |
|||
writeManagedChannelsFile, |
|||
}; |
@ -0,0 +1,844 @@ |
|||
/** |
|||
* All Lightning business logic. |
|||
*/ |
|||
|
|||
/* eslint-disable id-length, max-lines, max-statements */ |
|||
|
|||
const LndError = require('models/errors.js').LndError; |
|||
const NodeError = require('models/errors.js').NodeError; |
|||
|
|||
const lndService = require('services/lnd.js'); |
|||
const diskLogic = require('logic/disk'); |
|||
const bitcoindLogic = require('logic/bitcoind.js'); |
|||
|
|||
const constants = require('utils/const.js'); |
|||
const convert = require('utils/convert.js'); |
|||
|
|||
const UNIMPLEMENTED_CODE = 12; |
|||
|
|||
const PENDING_OPEN_CHANNELS = 'pendingOpenChannels'; |
|||
const PENDING_CLOSING_CHANNELS = 'pendingClosingChannels'; |
|||
const PENDING_FORCE_CLOSING_CHANNELS = 'pendingForceClosingChannels'; |
|||
const WAITING_CLOSE_CHANNELS = 'waitingCloseChannels'; |
|||
const PENDING_CHANNEL_TYPES = [PENDING_OPEN_CHANNELS, PENDING_CLOSING_CHANNELS, PENDING_FORCE_CLOSING_CHANNELS, |
|||
WAITING_CLOSE_CHANNELS]; |
|||
|
|||
const MAINNET_GENESIS_BLOCK_TIMESTAMP = 1231035305; |
|||
const TESTNET_GENESIS_BLOCK_TIMESTAMP = 1296717402; |
|||
|
|||
const FAST_BLOCK_CONF_TARGET = 1; |
|||
const NORMAL_BLOCK_CONF_TARGET = 6; |
|||
const SLOW_BLOCK_CONF_TARGET = 24; |
|||
const CHEAPEST_BLOCK_CONF_TARGET = 144; |
|||
|
|||
const OPEN_CHANNEL_EXTRA_WEIGHT = 10; |
|||
|
|||
const FEE_RATE_TOO_LOW_ERROR = { |
|||
code: 'FEE_RATE_TOO_LOW', |
|||
text: 'Mempool reject low fee transaction. Increase fee rate.', |
|||
}; |
|||
|
|||
const INSUFFICIENT_FUNDS_ERROR = { |
|||
code: 'INSUFFICIENT_FUNDS', |
|||
text: 'Lower amount or increase confirmation target.' |
|||
}; |
|||
|
|||
const INVALID_ADDRESS = { |
|||
code: 'INVALID_ADDRESS', |
|||
text: 'Please validate the Bitcoin address is correct.' |
|||
}; |
|||
|
|||
const OUTPUT_IS_DUST_ERROR = { |
|||
code: 'OUTPUT_IS_DUST', |
|||
text: 'Transaction output is dust.' |
|||
}; |
|||
|
|||
// Converts a byte object into a hex string.
|
|||
function toHexString(byteObject) { |
|||
const bytes = Object.values(byteObject); |
|||
|
|||
return bytes.map(function(byte) { |
|||
|
|||
return ('00' + (byte & 0xFF).toString(16)).slice(-2); // eslint-disable-line no-magic-numbers
|
|||
}).join(''); |
|||
} |
|||
|
|||
// Creates a new invoice; more commonly known as a payment request.
|
|||
async function addInvoice(amt, memo) { |
|||
const invoice = await lndService.addInvoice(amt, memo); |
|||
invoice.rHashStr = toHexString(invoice.rHash); |
|||
|
|||
return invoice; |
|||
} |
|||
|
|||
// Creates a new managed channel.
|
|||
async function addManagedChannel(channelPoint, name, purpose) { |
|||
const managedChannels = await getManagedChannels(); |
|||
|
|||
// Create a new managed channel. If one exists, it will be rewritten.
|
|||
// However, Lnd should guarantee chanId is always unique.
|
|||
managedChannels[channelPoint] = { |
|||
name: name, // eslint-disable-line object-shorthand
|
|||
purpose: purpose, // eslint-disable-line object-shorthand
|
|||
}; |
|||
|
|||
await setManagedChannels(managedChannels); |
|||
} |
|||
|
|||
// Change your lnd password. Wallet must exist and be unlocked.
|
|||
async function changePassword(currentPassword, newPassword) { |
|||
return await lndService.changePassword(currentPassword, newPassword); |
|||
} |
|||
|
|||
// Closes the channel that corresponds to the given channelPoint. Force close is optional.
|
|||
async function closeChannel(txHash, index, force) { |
|||
return await lndService.closeChannel(txHash, index, force); |
|||
} |
|||
|
|||
// Decode the payment request into useful information.
|
|||
function decodePaymentRequest(paymentRequest) { |
|||
return lndService.decodePaymentRequest(paymentRequest); |
|||
} |
|||
|
|||
// Estimate the cost of opening a channel. We do this by repurposing the existing estimateFee grpc route from lnd. We
|
|||
// generate our own unused address and then feed that into the existing call. Then we add an extra 10 sats per
|
|||
// feerateSatPerByte. This is because the actual cost is slightly more than the default one output estimate.
|
|||
async function estimateChannelOpenFee(amt, confTarget) { |
|||
const address = (await generateAddress()).address; |
|||
const baseFeeEstimate = await estimateFee(address, amt, confTarget, false); |
|||
|
|||
if (confTarget === 0) { |
|||
const keys = Object.keys(baseFeeEstimate); |
|||
|
|||
for (const key of keys) { |
|||
|
|||
if (baseFeeEstimate[key].feeSat) { |
|||
baseFeeEstimate[key].feeSat = String(parseInt(baseFeeEstimate[key].feeSat, 10) + OPEN_CHANNEL_EXTRA_WEIGHT |
|||
* baseFeeEstimate[key].feerateSatPerByte); |
|||
} |
|||
|
|||
} |
|||
|
|||
} else if (baseFeeEstimate.feeSat) { |
|||
baseFeeEstimate.feeSat = String(parseInt(baseFeeEstimate.feeSat, 10) + OPEN_CHANNEL_EXTRA_WEIGHT |
|||
* baseFeeEstimate.feerateSatPerByte); |
|||
} |
|||
|
|||
return baseFeeEstimate; |
|||
} |
|||
|
|||
// Estimate an on chain transaction fee.
|
|||
async function estimateFee(address, amt, confTarget, sweep) { |
|||
const mempoolInfo = (await bitcoindLogic.getMempoolInfo()).result; |
|||
|
|||
if (sweep) { |
|||
|
|||
const balance = parseInt((await lndService.getWalletBalance()).confirmedBalance, 10); |
|||
const amtToEstimate = balance; |
|||
|
|||
if (confTarget === 0) { |
|||
return await estimateFeeGroupSweep(address, amtToEstimate, mempoolInfo.mempoolminfee); |
|||
} |
|||
|
|||
return await estimateFeeSweep(address, amtToEstimate, mempoolInfo.mempoolminfee, confTarget, 0, amtToEstimate); |
|||
} else { |
|||
|
|||
try { |
|||
if (confTarget === 0) { |
|||
return await estimateFeeGroup(address, amt, mempoolInfo.mempoolminfee); |
|||
} |
|||
|
|||
return await estimateFeeWrapper(address, amt, mempoolInfo.mempoolminfee, confTarget); |
|||
} catch (error) { |
|||
return handleEstimateFeeError(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Use binary search strategy to determine the largest amount that can be sent.
|
|||
async function estimateFeeSweep(address, fullAmtToEstimate, mempoolMinFee, confTarget, l, r) { |
|||
|
|||
const amtToEstimate = l + Math.floor((r - l) / 2); // eslint-disable-line no-magic-numbers
|
|||
|
|||
try { |
|||
const successfulEstimate = await lndService.estimateFee(address, amtToEstimate, confTarget); |
|||
|
|||
// Return after we have completed our search.
|
|||
if (l === amtToEstimate) { |
|||
successfulEstimate.sweepAmount = amtToEstimate; |
|||
|
|||
if (successfulEstimate.feeSat < convert(mempoolMinFee, 'btc', 'sat', 'Number')) { |
|||
throw new NodeError('FEE_RATE_TOO_LOW'); |
|||
} |
|||
|
|||
return successfulEstimate; |
|||
} |
|||
|
|||
return await estimateFeeSweep(address, fullAmtToEstimate, mempoolMinFee, confTarget, amtToEstimate, r); |
|||
|
|||
} catch (error) { |
|||
|
|||
// Return after we have completed our search.
|
|||
if (l === amtToEstimate) { |
|||
return handleEstimateFeeError(error); |
|||
} |
|||
|
|||
return await estimateFeeSweep(address, fullAmtToEstimate, mempoolMinFee, confTarget, l, amtToEstimate); |
|||
} |
|||
} |
|||
|
|||
async function estimateFeeGroupSweep(address, amt, mempoolMinFee) { |
|||
const calls = [estimateFeeSweep(address, amt, mempoolMinFee, FAST_BLOCK_CONF_TARGET, 0, amt), |
|||
estimateFeeSweep(address, amt, mempoolMinFee, NORMAL_BLOCK_CONF_TARGET, 0, amt), |
|||
estimateFeeSweep(address, amt, mempoolMinFee, SLOW_BLOCK_CONF_TARGET, 0, amt), |
|||
estimateFeeSweep(address, amt, mempoolMinFee, CHEAPEST_BLOCK_CONF_TARGET, 0, amt), |
|||
]; |
|||
|
|||
const [fast, normal, slow, cheapest] |
|||
= await Promise.all(calls.map(p => p.catch(error => handleEstimateFeeError(error)))); |
|||
|
|||
return { |
|||
fast: fast, // eslint-disable-line object-shorthand
|
|||
normal: normal, // eslint-disable-line object-shorthand
|
|||
slow: slow, // eslint-disable-line object-shorthand
|
|||
cheapest: cheapest, // eslint-disable-line object-shorthand
|
|||
}; |
|||
} |
|||
|
|||
async function estimateFeeWrapper(address, amt, mempoolMinFee, confTarget) { |
|||
const estimate = await lndService.estimateFee(address, amt, confTarget); |
|||
|
|||
if (estimate.feeSat < convert(mempoolMinFee, 'btc', 'sat', 'Number')) { |
|||
throw new NodeError('FEE_RATE_TOO_LOW'); |
|||
} |
|||
|
|||
return estimate; |
|||
} |
|||
|
|||
async function estimateFeeGroup(address, amt, mempoolMinFee) { |
|||
const calls = [estimateFeeWrapper(address, amt, mempoolMinFee, FAST_BLOCK_CONF_TARGET), |
|||
estimateFeeWrapper(address, amt, mempoolMinFee, NORMAL_BLOCK_CONF_TARGET), |
|||
estimateFeeWrapper(address, amt, mempoolMinFee, SLOW_BLOCK_CONF_TARGET), |
|||
estimateFeeWrapper(address, amt, mempoolMinFee, CHEAPEST_BLOCK_CONF_TARGET), |
|||
]; |
|||
|
|||
const [fast, normal, slow, cheapest] |
|||
= await Promise.all(calls.map(p => p.catch(error => handleEstimateFeeError(error)))); |
|||
|
|||
return { |
|||
fast: fast, // eslint-disable-line object-shorthand
|
|||
normal: normal, // eslint-disable-line object-shorthand
|
|||
slow: slow, // eslint-disable-line object-shorthand
|
|||
cheapest: cheapest, // eslint-disable-line object-shorthand
|
|||
}; |
|||
} |
|||
|
|||
function handleEstimateFeeError(error) { |
|||
|
|||
if (error.message === 'FEE_RATE_TOO_LOW') { |
|||
return FEE_RATE_TOO_LOW_ERROR; |
|||
} else if (error.error.details === 'transaction output is dust') { |
|||
return OUTPUT_IS_DUST_ERROR; |
|||
} else if (error.error.details === 'insufficient funds available to construct transaction') { |
|||
return INSUFFICIENT_FUNDS_ERROR; |
|||
} |
|||
|
|||
return INVALID_ADDRESS; |
|||
} |
|||
|
|||
// Generates a new on chain segwit bitcoin address.
|
|||
async function generateAddress() { |
|||
return await lndService.generateAddress(); |
|||
} |
|||
|
|||
// Generates a new 24 word seed phrase.
|
|||
async function generateSeed() { |
|||
|
|||
const lndStatus = await getStatus(); |
|||
|
|||
if (lndStatus.operational) { |
|||
const response = await lndService.generateSeed(); |
|||
|
|||
return {seed: response.cipherSeedMnemonic}; |
|||
} |
|||
|
|||
throw new LndError('Lnd is not operational, therefore a seed cannot be created.'); |
|||
} |
|||
|
|||
// Returns the total funds in channels and the total pending funds in channels.
|
|||
function getChannelBalance() { |
|||
return lndService.getChannelBalance(); |
|||
} |
|||
|
|||
// Returns a count of all open channels.
|
|||
function getChannelCount() { |
|||
return lndService.getOpenChannels() |
|||
.then(response => ({count: response.length})); |
|||
} |
|||
|
|||
function getChannelPolicy() { |
|||
return lndService.getFeeReport() |
|||
.then(feeReport => feeReport.channelFees); |
|||
} |
|||
|
|||
function getForwardingEvents(startTime, endTime, indexOffset) { |
|||
return lndService.getForwardingEvents(startTime, endTime, indexOffset); |
|||
} |
|||
|
|||
// Returns a list of all invoices.
|
|||
async function getInvoices() { |
|||
const invoices = await lndService.getInvoices(); |
|||
|
|||
const reversedInvoices = []; |
|||
for (const invoice of invoices.invoices) { |
|||
reversedInvoices.unshift(invoice); |
|||
} |
|||
|
|||
return reversedInvoices; |
|||
} |
|||
|
|||
// Return all managed channels. Managed channels are channels the user has manually created.
|
|||
// TODO: how to handle if file becomes corrupt? Suggest simply wiping the file. The channel will still exist.
|
|||
function getManagedChannels() { |
|||
return diskLogic.readManagedChannelsFile(); |
|||
} |
|||
|
|||
// Returns a list of all on chain transactions.
|
|||
async function getOnChainTransactions() { |
|||
const transactions = await lndService.getOnChainTransactions(); |
|||
const openChannels = await lndService.getOpenChannels(); |
|||
const closedChannels = await lndService.getClosedChannels(); |
|||
const pendingChannelRPC = await lndService.getPendingChannels(); |
|||
|
|||
const pendingOpeningChannelTransactions = []; |
|||
for (const pendingChannel of pendingChannelRPC.pendingOpenChannels) { |
|||
const pendingTransaction = pendingChannel.channel.channelPoint.split(':').shift(); |
|||
pendingOpeningChannelTransactions.push(pendingTransaction); |
|||
} |
|||
|
|||
const pendingClosingChannelTransactions = []; |
|||
for (const pendingGroup of [ |
|||
pendingChannelRPC.pendingClosingChannels, |
|||
pendingChannelRPC.pendingForceClosingChannels, |
|||
pendingChannelRPC.waitingCloseChannels]) { |
|||
|
|||
if (pendingGroup.length === 0) { |
|||
continue; |
|||
} |
|||
for (const pendingChannel of pendingGroup) { |
|||
pendingClosingChannelTransactions.push(pendingChannel.closingTxid); |
|||
} |
|||
} |
|||
|
|||
const openChannelTransactions = []; |
|||
for (const channel of openChannels) { |
|||
const openTransaction = channel.channelPoint.split(':').shift(); |
|||
openChannelTransactions.push(openTransaction); |
|||
} |
|||
|
|||
const closedChannelTransactions = []; |
|||
for (const channel of closedChannels) { |
|||
const closedTransaction = channel.closingTxHash.split(':').shift(); |
|||
closedChannelTransactions.push(closedTransaction); |
|||
|
|||
const openTransaction = channel.channelPoint.split(':').shift(); |
|||
openChannelTransactions.push(openTransaction); |
|||
} |
|||
|
|||
const reversedTransactions = []; |
|||
for (const transaction of transactions) { |
|||
const txHash = transaction.txHash; |
|||
|
|||
if (openChannelTransactions.includes(txHash)) { |
|||
transaction.type = 'CHANNEL_OPEN'; |
|||
} else if (closedChannelTransactions.includes(txHash)) { |
|||
transaction.type = 'CHANNEL_CLOSE'; |
|||
} else if (pendingOpeningChannelTransactions.includes(txHash)) { |
|||
transaction.type = 'PENDING_OPEN'; |
|||
} else if (pendingClosingChannelTransactions.includes(txHash)) { |
|||
transaction.type = 'PENDING_CLOSE'; |
|||
} else if (transaction.amount < 0) { |
|||
transaction.type = 'ON_CHAIN_TRANSACTION_SENT'; |
|||
} else if (transaction.amount > 0 && transaction.destAddresses.length > 0) { |
|||
transaction.type = 'ON_CHAIN_TRANSACTION_RECEIVED'; |
|||
|
|||
// Positive amounts are either incoming transactions or a WaitingCloseChannel. There is no way to determine which
|
|||
// until the transaction has at least one confirmation. Then a WaitingCloseChannel will become a pending Closing
|
|||
// channel and will have an associated tx id.
|
|||
} else if (transaction.amount > 0 && transaction.destAddresses.length === 0) { |
|||
transaction.type = 'PENDING_CLOSE'; |
|||
} else { |
|||
transaction.type = 'UNKNOWN'; |
|||
} |
|||
|
|||
reversedTransactions.unshift(transaction); |
|||
} |
|||
|
|||
return reversedTransactions; |
|||
} |
|||
|
|||
function getTxnHashFromChannelPoint(channelPoint) { |
|||
return channelPoint.split(':')[0]; |
|||
} |
|||
|
|||
// Returns a list of all open channels.
|
|||
const getChannels = async() => { |
|||
const managedChannelsCall = getManagedChannels(); |
|||
const openChannelsCall = lndService.getOpenChannels(); |
|||
const pendingChannels = await lndService.getPendingChannels(); |
|||
|
|||
const allChannels = []; |
|||
|
|||
// Combine all pending channel types
|
|||
for (const channel of pendingChannels.waitingCloseChannels) { |
|||
channel.type = 'WAITING_CLOSING_CHANNEL'; |
|||
allChannels.push(channel); |
|||
} |
|||
|
|||
for (const channel of pendingChannels.pendingForceClosingChannels) { |
|||
channel.type = 'FORCE_CLOSING_CHANNEL'; |
|||
allChannels.push(channel); |
|||
} |
|||
|
|||
for (const channel of pendingChannels.pendingClosingChannels) { |
|||
channel.type = 'PENDING_CLOSING_CHANNEL'; |
|||
allChannels.push(channel); |
|||
} |
|||
|
|||
for (const channel of pendingChannels.pendingOpenChannels) { |
|||
channel.type = 'PENDING_OPEN_CHANNEL'; |
|||
|
|||
// Make our best guess as to if this channel was created by us.
|
|||
if (channel.channel.remoteBalance === '0') { |
|||
channel.initiator = true; |
|||
} else { |
|||
channel.initiator = false; |
|||
} |
|||
|
|||
// Include commitFee in balance. This helps us avoid the leaky sats issue by making balances more consistent.
|
|||
if (channel.initiator) { |
|||
channel.channel.localBalance |
|||
= String(parseInt(channel.channel.localBalance, 10) + parseInt(channel.commitFee, 10)); |
|||
} else { |
|||
channel.channel.remoteBalance |
|||
= String(parseInt(channel.channel.remoteBalance, 10) + parseInt(channel.commitFee, 10)); |
|||
} |
|||
|
|||
allChannels.push(channel); |
|||
} |
|||
|
|||
// If we have any pending channels, we need to call get chain transactions to determine how many confirmations are
|
|||
// left for each pending channel. This gets the entire history of on chain transactions.
|
|||
// TODO: Once pagination is available, we should develop a different strategy.
|
|||
let chainTxnCall = null; |
|||
let chainTxns = null; |
|||
if (allChannels.length > 0) { |
|||
chainTxnCall = lndService.getOnChainTransactions(); |
|||
} |
|||
|
|||
// Combine open channels
|
|||
const openChannels = await openChannelsCall; |
|||
|
|||
for (const channel of openChannels) { |
|||
channel.type = 'OPEN'; |
|||
|
|||
// Include commitFee in balance. This helps us avoid the leaky sats issue by making balances more consistent.
|
|||
if (channel.initiator) { |
|||
channel.localBalance |
|||
= String(parseInt(channel.localBalance, 10) + parseInt(channel.commitFee, 10)); |
|||
} else { |
|||
channel.remoteBalance |
|||
= String(parseInt(channel.remoteBalance, 10) + parseInt(channel.commitFee, 10)); |
|||
} |
|||
|
|||
allChannels.push(channel); |
|||
} |
|||
|
|||
// Add additional managed channel data if it exists
|
|||
// Call this async, because it reads from disk
|
|||
const managedChannels = await managedChannelsCall; |
|||
|
|||
if (chainTxnCall !== null) { |
|||
const chainTxnList = await chainTxnCall; |
|||
|
|||
// Convert list to object for efficient searching
|
|||
chainTxns = {}; |
|||
for (const txn of chainTxnList) { |
|||
chainTxns[txn.txHash] = txn; |
|||
} |
|||
} |
|||
|
|||
// Iterate through all channels
|
|||
for (const channel of allChannels) { |
|||
|
|||
// Pending channels have an inner channel object.
|
|||
if (channel.channel) { |
|||
// Use remotePubkey for consistency with open channels
|
|||
channel.remotePubkey = channel.channel.remoteNodePub; |
|||
channel.channelPoint = channel.channel.channelPoint; |
|||
channel.capacity = channel.channel.capacity; |
|||
channel.localBalance = channel.channel.localBalance; |
|||
channel.remoteBalance = channel.channel.remoteBalance; |
|||
|
|||
delete channel.channel; |
|||
|
|||
// Determine the number of confirmation remaining for this channel
|
|||
|
|||
// We might have invalid channels that dne in the onChainTxList. Skip these channels
|
|||
const knownChannel = chainTxns[getTxnHashFromChannelPoint(channel.channelPoint)]; |
|||
if (!knownChannel) { |
|||
channel.managed = false; |
|||
channel.name = ''; |
|||
channel.purpose = ''; |
|||
|
|||
continue; |
|||
} |
|||
const numConfirmations = knownChannel.numConfirmations; |
|||
|
|||
if (channel.type === 'FORCE_CLOSING_CHANNEL') { |
|||
|
|||
// BlocksTilMaturity is provided by Lnd for forced closing channels once they have one confirmation
|
|||
channel.remainingConfirmations = channel.blocksTilMaturity; |
|||
} else if (channel.type === 'PENDING_CLOSING_CHANNEL') { |
|||
|
|||
// Lnd seams to be clearing these channels after just one confirmation and thus they never exist in this state.
|
|||
// Defaulting to 1 just in case.
|
|||
channel.remainingConfirmations = 1; |
|||
} else if (channel.type === 'PENDING_OPEN_CHANNEL') { |
|||
|
|||
channel.remainingConfirmations = constants.LN_REQUIRED_CONFIRMATIONS - numConfirmations; |
|||
} |
|||
} |
|||
|
|||
// If a managed channel exists, set the name and purpose
|
|||
if (Object.prototype.hasOwnProperty.call(managedChannels, channel.channelPoint)) { |
|||
channel.managed = true; |
|||
channel.name = managedChannels[channel.channelPoint].name; |
|||
channel.purpose = managedChannels[channel.channelPoint].purpose; |
|||
} else { |
|||
channel.managed = false; |
|||
channel.name = ''; |
|||
channel.purpose = ''; |
|||
} |
|||
} |
|||
|
|||
return allChannels; |
|||
}; |
|||
|
|||
// Returns a list of all outgoing payments.
|
|||
async function getPayments() { |
|||
const payments = await lndService.getPayments(); |
|||
|
|||
const reversedPayments = []; |
|||
for (const payment of payments.payments) { |
|||
reversedPayments.unshift(payment); |
|||
} |
|||
|
|||
return reversedPayments; |
|||
} |
|||
|
|||
// Returns the full channel details of a pending channel.
|
|||
async function getPendingChannelDetails(channelType, pubKey) { |
|||
const pendingChannels = await getPendingChannels(); |
|||
|
|||
// make sure correct type is used
|
|||
if (!PENDING_CHANNEL_TYPES.includes(channelType)) { |
|||
throw Error('unknown pending channel type: ' + channelType); |
|||
} |
|||
|
|||
const typePendingChannel = pendingChannels[channelType]; |
|||
|
|||
for (let index = 0; index < typePendingChannel.length; index++) { |
|||
const curChannel = typePendingChannel[index]; |
|||
if (curChannel.channel && curChannel.channel.remoteNodePub && curChannel.channel.remoteNodePub === pubKey) { |
|||
return curChannel.channel; |
|||
} |
|||
} |
|||
|
|||
throw new Error('Could not find a pending channel for pubKey: ' + pubKey); |
|||
} |
|||
|
|||
// Returns a list of all pending channels.
|
|||
function getPendingChannels() { |
|||
return lndService.getPendingChannels(); |
|||
} |
|||
|
|||
// Returns all associated public uris for this node.
|
|||
function getPublicUris() { |
|||
return lndService.getInfo() |
|||
.then(info => info.uris); |
|||
} |
|||
|
|||
function getGeneralInfo() { |
|||
return lndService.getInfo(); |
|||
} |
|||
|
|||
// Returns the status on lnd syncing to the current chain.
|
|||
// LND info returns "best_header_timestamp" from getInfo which is the timestamp of the latest Bitcoin block processed
|
|||
// by LND. Using known date of the genesis block to roughly calculate a percent processed.
|
|||
async function getSyncStatus() { |
|||
const info = await lndService.getInfo(); |
|||
|
|||
let percentSynced = null; |
|||
let processedBlocks = null; |
|||
|
|||
if (!info.syncedToChain) { |
|||
const genesisTimestamp = info.testnet ? TESTNET_GENESIS_BLOCK_TIMESTAMP : MAINNET_GENESIS_BLOCK_TIMESTAMP; |
|||
|
|||
const currentTime = Math.floor(new Date().getTime() / 1000); // eslint-disable-line no-magic-numbers
|
|||
|
|||
percentSynced = ((info.bestHeaderTimestamp - genesisTimestamp) / (currentTime - genesisTimestamp)) |
|||
.toFixed(4); // eslint-disable-line no-magic-numbers
|
|||
|
|||
// let's not return a value over the 100% or processedBlocks > blockHeight
|
|||
// space-fleet can determine how to handle this error state if it detects -1
|
|||
if (percentSynced < 1.0) { |
|||
processedBlocks = Math.floor(percentSynced * info.blockHeight); |
|||
} else { |
|||
processedBlocks = -1; |
|||
percentSynced = -1; |
|||
} |
|||
|
|||
} else { |
|||
percentSynced = (1).toFixed(4); // eslint-disable-line no-magic-numbers
|
|||
processedBlocks = info.blockHeight; |
|||
} |
|||
|
|||
return { |
|||
percent: percentSynced, |
|||
knownBlockCount: info.blockHeight, |
|||
processedBlocks: processedBlocks, // eslint-disable-line object-shorthand
|
|||
}; |
|||
} |
|||
|
|||
// Returns the wallet balance and pending confirmation balance.
|
|||
function getWalletBalance() { |
|||
return lndService.getWalletBalance(); |
|||
} |
|||
|
|||
// Creates and initialized a Lightning wallet.
|
|||
async function initializeWallet(password, seed) { |
|||
|
|||
const lndStatus = await getStatus(); |
|||
|
|||
if (lndStatus.operational) { |
|||
|
|||
await lndService.initWallet({ |
|||
mnemonic: seed, |
|||
password: password // eslint-disable-line object-shorthand
|
|||
}); |
|||
|
|||
return; |
|||
} |
|||
|
|||
throw new LndError('Lnd is not operational, therefore a wallet cannot be created.'); |
|||
} |
|||
|
|||
// Opens a channel to the node with the given public key with the given amount.
|
|||
async function openChannel(pubKey, ip, port, amt, satPerByte, name, purpose) { // eslint-disable-line max-params
|
|||
|
|||
var peers = await lndService.getPeers(); |
|||
|
|||
var existingPeer = false; |
|||
|
|||
for (const peer of peers) { |
|||
if (peer.pubKey === pubKey) { |
|||
existingPeer = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!existingPeer) { |
|||
await lndService.connectToPeer(pubKey, ip, port); |
|||
} |
|||
|
|||
// only returns a transactions id
|
|||
// TODO: Can we get the channel index from here? The channel point is transaction id:index. It could save us a call
|
|||
// to pendingChannelDetails.
|
|||
const channel = await lndService.openChannel(pubKey, amt, satPerByte); |
|||
|
|||
// Lnd only allows one channel to be created with a node per block. By searching pending open channels, we can find
|
|||
// a unique identifier for the newly created channe. We will use ChannelPoint.
|
|||
const pendingChannel = await getPendingChannelDetails(PENDING_OPEN_CHANNELS, pubKey); |
|||
await addManagedChannel(pendingChannel.channelPoint, name, purpose); |
|||
|
|||
return channel; |
|||
} |
|||
|
|||
// Pays the given invoice.
|
|||
async function payInvoice(paymentRequest, amt) { |
|||
const invoice = await decodePaymentRequest(paymentRequest); |
|||
|
|||
if (invoice.numSatoshis !== '0' && amt) { // numSatoshis is returned from lnd as a string
|
|||
throw Error('Payment Request with non zero amount and amt value supplied.'); |
|||
} |
|||
|
|||
if (invoice.numSatoshis === '0' && !amt) { // numSatoshis is returned from lnd as a string
|
|||
throw Error('Payment Request with zero amount requires an amt value supplied.'); |
|||
} |
|||
|
|||
return await lndService.sendPaymentSync(paymentRequest, amt); |
|||
} |
|||
|
|||
// Removes a managed channel.
|
|||
// TODO: Figure out when an appropriate time to cleanup closed managed channel data. We need it during the closing
|
|||
// process to display to users.
|
|||
/* |
|||
async function removeManagedChannel(fundingTxId, index) { |
|||
const managedChannels = await getManagedChannels(); |
|||
|
|||
const channelPoint = fundingTxId + ':' + index; |
|||
|
|||
if (Object.prototype.hasOwnProperty.call(managedChannels, channelPoint)) { |
|||
delete managedChannels[channelPoint]; |
|||
} |
|||
|
|||
return await setManagedChannels(managedChannels); |
|||
} |
|||
*/ |
|||
|
|||
// Send bitcoins on chain to the given address with the given amount. Sats per byte is optional.
|
|||
function sendCoins(addr, amt, satPerByte, sendAll) { |
|||
|
|||
// Lnd requires we ignore amt if sendAll is true.
|
|||
if (sendAll) { |
|||
return lndService.sendCoins(addr, undefined, satPerByte, sendAll); |
|||
} |
|||
|
|||
return lndService.sendCoins(addr, amt, satPerByte, sendAll); |
|||
} |
|||
|
|||
// Sets the managed channel data store.
|
|||
// TODO: How to prevent this from getting out of data with multiple calling threads?
|
|||
// perhaps create a mutex for reading and writing?
|
|||
function setManagedChannels(managedChannelsObject) { |
|||
return diskLogic.writeManagedChannelsFile(managedChannelsObject); |
|||
} |
|||
|
|||
// Returns if lnd is operation and if the wallet is unlocked.
|
|||
async function getStatus() { |
|||
const bitcoindStatus = await bitcoindLogic.getStatus(); |
|||
|
|||
// lnd requires bitcoind to be operational.
|
|||
if (!bitcoindStatus.operational) { |
|||
|
|||
return { |
|||
operational: false, |
|||
unlocked: false |
|||
}; |
|||
} |
|||
|
|||
try { |
|||
// The getInfo function requires that the wallet be unlocked in order to succeed. Lnd requires this for all
|
|||
// encrypted wallets.
|
|||
await lndService.getInfo(); |
|||
|
|||
return { |
|||
operational: true, |
|||
unlocked: true |
|||
}; |
|||
} catch (error) { |
|||
|
|||
// lnd might be active, but not possible to contact
|
|||
// using RPC if the wallet is encrypted. If we get
|
|||
// error code Unimplemented, it means that lnd is
|
|||
// running, but the RPC server is not active yet (only
|
|||
// WalletUnlocker server active) and most likely this
|
|||
// is because of an encrypted wallet.
|
|||
if (error instanceof LndError) { |
|||
|
|||
if (error.error && error.error.code === UNIMPLEMENTED_CODE) { |
|||
|
|||
return { |
|||
operational: true, |
|||
unlocked: false |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
operational: false, |
|||
unlocked: false |
|||
}; |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
// Unlock and existing wallet.
|
|||
async function unlockWallet(password) { |
|||
const lndStatus = await getStatus(); |
|||
|
|||
if (lndStatus.operational) { |
|||
try { |
|||
await lndService.unlockWallet(password); |
|||
|
|||
return; |
|||
} catch (error) { |
|||
// If it's a command for the UnlockerService (like
|
|||
// 'create' or 'unlock') but the wallet is already
|
|||
// unlocked, then these methods aren't recognized any
|
|||
// more because this service is shut down after
|
|||
// successful unlock. That's why the code
|
|||
// 'Unimplemented' means something different for these
|
|||
// two commands.
|
|||
if (error instanceof LndError) { |
|||
|
|||
// wallet is already unlocked
|
|||
if (error.error && error.error.code === UNIMPLEMENTED_CODE) { |
|||
return; |
|||
} |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
throw new LndError('Lnd is not operational, therefore the wallet cannot be unlocked.'); |
|||
} |
|||
|
|||
async function getVersion() { |
|||
const info = await lndService.getInfo(); |
|||
const unformattedVersion = info.version; |
|||
|
|||
// Remove all beta/commit info. Fragile, LND may one day GA.
|
|||
const version = unformattedVersion.split('-', 1)[0]; |
|||
|
|||
return {version: version}; // eslint-disable-line object-shorthand
|
|||
} |
|||
|
|||
function updateChannelPolicy(global, fundingTxid, outputIndex, baseFeeMsat, feeRate, timeLockDelta) { |
|||
return lndService.updateChannelPolicy(global, fundingTxid, outputIndex, baseFeeMsat, feeRate, timeLockDelta); |
|||
} |
|||
|
|||
module.exports = { |
|||
addInvoice, |
|||
changePassword, |
|||
closeChannel, |
|||
decodePaymentRequest, |
|||
estimateChannelOpenFee, |
|||
estimateFee, |
|||
generateAddress, |
|||
generateSeed, |
|||
getChannelBalance, |
|||
getChannelPolicy, |
|||
getChannelCount, |
|||
getInvoices, |
|||
getChannels, |
|||
getForwardingEvents, |
|||
getOnChainTransactions, |
|||
getPayments, |
|||
getPendingChannels, |
|||
getPublicUris, |
|||
getStatus, |
|||
getSyncStatus, |
|||
getWalletBalance, |
|||
initializeWallet, |
|||
openChannel, |
|||
payInvoice, |
|||
sendCoins, |
|||
unlockWallet, |
|||
getGeneralInfo, |
|||
getVersion, |
|||
updateChannelPolicy, |
|||
}; |
@ -0,0 +1,89 @@ |
|||
const bitcoindService = require('services/bitcoind.js'); |
|||
const bashService = require('services/bash.js'); |
|||
|
|||
async function getBitcoindAddresses() { |
|||
|
|||
const addresses = []; |
|||
|
|||
// Find standard ip address
|
|||
const peerInfo = (await bitcoindService.getPeerInfo()).result; |
|||
|
|||
if (peerInfo.length === 0) { |
|||
addresses.push(await getExternalIPFromIPInfo()); |
|||
} else { |
|||
|
|||
const mostValidIp = getMostValidatedIP(peerInfo); |
|||
|
|||
// TODO don't call third party service if running with TOR_ONLY
|
|||
if (mostValidIp.includes('onion')) { |
|||
addresses.push(await getExternalIPFromIPInfo()); |
|||
} else { |
|||
addresses.push(mostValidIp); |
|||
} |
|||
} |
|||
|
|||
// Try to find that Tor onion address.
|
|||
const networkInfo = (await bitcoindService.getNetworkInfo()).result; |
|||
|
|||
if (Object.prototype.hasOwnProperty.call(networkInfo, 'localaddresses') |
|||
&& networkInfo.localaddresses.length > 0) { |
|||
|
|||
// If Tor is initialized there should only be one local address
|
|||
addresses.push(networkInfo.localaddresses[0].address); |
|||
} |
|||
|
|||
return addresses; // eslint-disable-line object-shorthand
|
|||
} |
|||
|
|||
async function getExternalIPFromIPInfo() { |
|||
|
|||
const options = {}; |
|||
|
|||
// use ipinfo.io to get ip address if unable to from peers
|
|||
const data = await bashService.exec('curl', ['https://ipinfo.io/ip'], options); |
|||
|
|||
// clean return characters
|
|||
return data.out.replace(/[^a-zA-Z0-9 .:]/g, ''); |
|||
} |
|||
|
|||
function getMostValidatedIP(peerInfo) { |
|||
const peerCount = {}; |
|||
const mostValidatedExternalIp = { |
|||
count: 0, |
|||
externalIP: 'UNKNOWN' |
|||
}; |
|||
|
|||
for (const peer of peerInfo) { |
|||
|
|||
// Make sure addrlocal exists, sometimes peers don't supply it
|
|||
if (Object.prototype.hasOwnProperty.call(peer, 'addrlocal')) { |
|||
|
|||
// Use the semi colon to account for ipv4 and ipv6
|
|||
const semi = peer.addrlocal.lastIndexOf(':'); |
|||
const externalIP = peer.addrlocal.substr(0, semi); |
|||
|
|||
// Ignore localhost, this is incorrect data from bitcoind
|
|||
if (externalIP !== '127.0.0.1' || externalIP !== '0.0.0.0') { |
|||
|
|||
// Increment the count for this external ip
|
|||
if (Object.prototype.hasOwnProperty.call(peerCount, externalIP)) { |
|||
peerCount[externalIP]++; |
|||
} else { |
|||
peerCount[externalIP] = 1; |
|||
} |
|||
|
|||
// Set the most validated external ip
|
|||
if (peerCount[externalIP] > mostValidatedExternalIp.count) { |
|||
mostValidatedExternalIp.count = peerCount[externalIP]; |
|||
mostValidatedExternalIp.externalIP = externalIP; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return mostValidatedExternalIp.externalIP; |
|||
} |
|||
|
|||
module.exports = { |
|||
getBitcoindAddresses, |
|||
}; |
@ -0,0 +1,31 @@ |
|||
const lightningLogic = require('logic/lightning.js'); |
|||
const networkLogic = require('logic/network.js'); |
|||
|
|||
async function lndDetails() { |
|||
|
|||
const calls = [networkLogic.getBitcoindAddresses(), |
|||
lightningLogic.getChannelBalance(), |
|||
lightningLogic.getWalletBalance(), |
|||
lightningLogic.getChannels(), |
|||
lightningLogic.getGeneralInfo() |
|||
]; |
|||
|
|||
// prevent fail fast, ux will expect a null on failed calls
|
|||
const [externalIP, channelBalance, walletBalance, channels, lightningInfo] |
|||
= await Promise.all(calls.map(p => p.catch(err => null))); // eslint-disable-line
|
|||
|
|||
return { |
|||
externalIP: externalIP, // eslint-disable-line object-shorthand
|
|||
balance: { |
|||
wallet: walletBalance, |
|||
channel: channelBalance, |
|||
}, |
|||
channels: channels, // eslint-disable-line object-shorthand
|
|||
lightningInfo: lightningInfo // eslint-disable-line object-shorthand
|
|||
}; |
|||
|
|||
} |
|||
|
|||
module.exports = { |
|||
lndDetails |
|||
}; |
@ -0,0 +1,42 @@ |
|||
const passport = require('passport'); |
|||
const passportJWT = require('passport-jwt'); |
|||
const constants = require('utils/const.js'); |
|||
const NodeError = require('models/errors.js').NodeError; |
|||
|
|||
var JwtStrategy = passportJWT.Strategy; |
|||
var ExtractJwt = passportJWT.ExtractJwt; |
|||
|
|||
const JWT_AUTH = 'jwt'; |
|||
|
|||
passport.serializeUser(function (user, done) { |
|||
return done(null, user.id); |
|||
}); |
|||
|
|||
const jwtOptions = { |
|||
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'), |
|||
secretOrKey: Buffer.from(constants.JWT_PUBLIC_KEY, 'hex'), // The `manager` will pass the public key as hex.
|
|||
algorithm: 'RS256' |
|||
}; |
|||
|
|||
passport.use(JWT_AUTH, new JwtStrategy(jwtOptions, function (jwtPayload, done) { |
|||
return done(null, { id: jwtPayload.id }); |
|||
})); |
|||
|
|||
function jwt(req, res, next) { |
|||
passport.authenticate(JWT_AUTH, { session: false }, function (error, user) { |
|||
//if (error || user === false) {
|
|||
//return next(new NodeError('Invalid JWT', 401)); // eslint-disable-line no-magic-numbers
|
|||
//}
|
|||
req.logIn(user, function (err) { |
|||
//if (err) {
|
|||
//return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
|||
//}
|
|||
|
|||
return next(null, user); |
|||
}); |
|||
})(req, res, next); |
|||
} |
|||
|
|||
module.exports = { |
|||
jwt, |
|||
}; |
@ -0,0 +1,12 @@ |
|||
const camelizeKeys = require('camelize-keys'); |
|||
|
|||
function camelCaseRequest(req, res, next) { |
|||
if (req && req.body) { |
|||
req.body = camelizeKeys(req.body, '_'); |
|||
} |
|||
next(); |
|||
} |
|||
|
|||
module.exports = { |
|||
camelCaseRequest, |
|||
}; |
@ -0,0 +1,26 @@ |
|||
const corsOptions = { |
|||
origin: (origin, callback) => { |
|||
const whitelist = [ |
|||
'http://localhost:3000', |
|||
'http://casa-node.local', |
|||
'http://debug.keys.casa', |
|||
'chrome-extension://lnaedehiikghclgaikolambpbpeknpef', |
|||
process.env.DEVICE_HOST, |
|||
]; |
|||
|
|||
// Whitelist hidden service if exists.
|
|||
if (process.env.CASA_NODE_HIDDEN_SERVICE) { |
|||
whitelist.push(process.env.CASA_NODE_HIDDEN_SERVICE); |
|||
} |
|||
|
|||
if (whitelist.indexOf(origin) !== -1 || !origin) { |
|||
return callback(null, true); |
|||
} else { |
|||
return callback(new Error('Not allowed by CORS')); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
module.exports = { |
|||
corsOptions, |
|||
}; |
@ -0,0 +1,28 @@ |
|||
/* eslint-disable no-unused-vars, no-magic-numbers */ |
|||
const logger = require('utils/logger.js'); |
|||
const LndError = require('models/errors.js').LndError; |
|||
|
|||
function handleError(error, req, res, next) { |
|||
|
|||
var statusCode = error.statusCode || 500; |
|||
var route = req.url || ''; |
|||
var message = error.message || ''; |
|||
|
|||
if (error instanceof LndError) { |
|||
if (error.error && error.error.code === 12) { |
|||
statusCode = 403; |
|||
message = 'Must unlock wallet'; |
|||
|
|||
// add additional details if available
|
|||
} else if (error.error && error.error.details) { |
|||
// this may be too much information to return
|
|||
message += ', ' + error.error.details; |
|||
} |
|||
} |
|||
|
|||
logger.error(message, route, error.stack); |
|||
|
|||
res.status(statusCode).json(message); |
|||
} |
|||
|
|||
module.exports = handleError; |
@ -0,0 +1,30 @@ |
|||
/* |
|||
Tor 9.0.0 does not automatically include the origin header. This causes cross origin errors. This middleware handles |
|||
this bug until Tor releases a bugfix. |
|||
|
|||
https://trac.torproject.org/projects/tor/ticket/32255
|
|||
*/ |
|||
function onionOrigin(req, res, next) { |
|||
|
|||
// Get just the onion address.
|
|||
let hiddenService; |
|||
if (process.env.CASA_NODE_HIDDEN_SERVICE |
|||
&& process.env.CASA_NODE_HIDDEN_SERVICE.startsWith('http://')) { |
|||
|
|||
hiddenService = process.env.CASA_NODE_HIDDEN_SERVICE.substring(7, // eslint-disable-line no-magic-numbers
|
|||
process.env.CASA_NODE_HIDDEN_SERVICE.length); |
|||
} |
|||
|
|||
// If a hidden service is known and the request has the Tor Browser 9.0.0 bug.
|
|||
if (hiddenService |
|||
&& req.headers.host.includes(hiddenService) |
|||
&& !req.headers.origin) { |
|||
|
|||
// Manually add hidden service (with http://) to the response header.
|
|||
res.setHeader('Access-Control-Allow-Origin', process.env.CASA_NODE_HIDDEN_SERVICE); |
|||
} |
|||
|
|||
next(); |
|||
} |
|||
|
|||
module.exports = onionOrigin; |
@ -0,0 +1,15 @@ |
|||
const UUID = require('utils/UUID.js'); |
|||
const constants = require('utils/const.js'); |
|||
const createNamespace = require('continuation-local-storage').createNamespace; |
|||
const apiRequest = createNamespace(constants.REQUEST_CORRELATION_NAMESPACE_KEY); |
|||
|
|||
function addCorrelationId(req, res, next) { |
|||
apiRequest.bindEmitter(req); |
|||
apiRequest.bindEmitter(res); |
|||
apiRequest.run(function() { |
|||
apiRequest.set(constants.REQUEST_CORRELATION_ID_KEY, UUID.create()); |
|||
next(); |
|||
}); |
|||
} |
|||
|
|||
module.exports = addCorrelationId; |
@ -0,0 +1,42 @@ |
|||
/* eslint-disable no-magic-numbers */ |
|||
function NodeError(message, statusCode) { |
|||
Error.captureStackTrace(this, this.constructor); |
|||
this.name = this.constructor.name; |
|||
this.message = message; |
|||
this.statusCode = statusCode; |
|||
} |
|||
require('util').inherits(NodeError, Error); |
|||
|
|||
function BitcoindError(message, error, statusCode) { |
|||
Error.captureStackTrace(this, this.constructor); |
|||
this.name = this.constructor.name; |
|||
this.message = message; |
|||
this.error = error; |
|||
this.statusCode = statusCode; |
|||
} |
|||
require('util').inherits(BitcoindError, Error); |
|||
|
|||
function LndError(message, error, statusCode) { |
|||
Error.captureStackTrace(this, this.constructor); |
|||
this.name = this.constructor.name; |
|||
this.message = message; |
|||
this.error = error; |
|||
this.statusCode = statusCode; |
|||
} |
|||
require('util').inherits(LndError, Error); |
|||
|
|||
function ValidationError(message, statusCode) { |
|||
Error.captureStackTrace(this, this.constructor); |
|||
this.name = this.constructor.name; |
|||
this.message = message; |
|||
this.statusCode = statusCode || 400; |
|||
} |
|||
require('util').inherits(ValidationError, Error); |
|||
|
|||
module.exports = { |
|||
NodeError, |
|||
BitcoindError, |
|||
LndError, |
|||
ValidationError |
|||
}; |
|||
|
@ -0,0 +1,57 @@ |
|||
{ |
|||
"name": "lnapi", |
|||
"version": "1.18.0", |
|||
"description": "Application endpoints for Casa Lightning Node", |
|||
"author": "Casa, INC", |
|||
"scripts": { |
|||
"lint": "eslint", |
|||
"start": "node ./bin/www", |
|||
"test": "mocha --file test.setup 'test/**/*.js'", |
|||
"coverage": "nyc --all mocha --file test.setup 'test/**/*.js'", |
|||
"postcoverage": "codecov" |
|||
}, |
|||
"dependencies": { |
|||
"big.js": "^5.2.2", |
|||
"bitcoind-rpc": "^0.7.2", |
|||
"body-parser": "^1.18.2", |
|||
"camelize-keys": "^1.0.0", |
|||
"continuation-local-storage": "^3.2.1", |
|||
"cors": "^2.8.5", |
|||
"debug": "^2.6.1", |
|||
"express": "^4.16.3", |
|||
"grpc": "^1.8.0", |
|||
"module-alias": "^2.1.0", |
|||
"morgan": "^1.9.0", |
|||
"npm": "^5.6.0", |
|||
"passport": "^0.4.0", |
|||
"passport-jwt": "^4.0.0", |
|||
"request-promise": "^4.2.2", |
|||
"uuid": "^3.3.2", |
|||
"validator": "^9.2.0", |
|||
"winston": "^3.0.0-rc5", |
|||
"winston-daily-rotate-file": "^3.1.3" |
|||
}, |
|||
"devDependencies": { |
|||
"babel-eslint": "^8.2.6", |
|||
"chai": "^4.1.2", |
|||
"chai-http": "^4.2.0", |
|||
"codecov": "3.0.4", |
|||
"eslint": "^5.3.0", |
|||
"mocha": "^5.2.0", |
|||
"nyc": "13.0.1", |
|||
"proxyquire": "^2.0.1", |
|||
"sinon": "^6.1.4" |
|||
}, |
|||
"nyc": { |
|||
"exclude": [ |
|||
"test", |
|||
"test.setup.js" |
|||
], |
|||
"sourceMap": false, |
|||
"reporter": [ |
|||
"lcov", |
|||
"text-summary" |
|||
], |
|||
"cache": "false" |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
#!/usr/bin/env bash |
|||
echo "Checking style, apply linting!" |
|||
npm run lint -- . |
|||
|
|||
make test |
Binary file not shown.
File diff suppressed because it is too large
@ -0,0 +1,9 @@ |
|||
const express = require('express'); |
|||
const pjson = require('../package.json'); |
|||
const router = express.Router(); |
|||
|
|||
router.get('/', function(req, res) { |
|||
res.json({version: 'lnapi-' + pjson.version}); |
|||
}); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,38 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
const networkLogic = require('logic/network.js'); |
|||
const bitcoind = require('logic/bitcoind.js'); |
|||
const auth = require('middlewares/auth.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
|
|||
router.get('/addresses', auth.jwt, safeHandler((req, res) => |
|||
networkLogic.getBitcoindAddresses() |
|||
.then(addresses => res.json(addresses)) |
|||
)); |
|||
|
|||
router.get('/blockcount', auth.jwt, safeHandler((req, res) => |
|||
bitcoind.getBlockCount() |
|||
.then(blockCount => res.json(blockCount)) |
|||
)); |
|||
|
|||
router.get('/connections', auth.jwt, safeHandler((req, res) => |
|||
bitcoind.getConnectionsCount() |
|||
.then(connections => res.json(connections)) |
|||
)); |
|||
|
|||
router.get('/status', auth.jwt, safeHandler((req, res) => |
|||
bitcoind.getStatus() |
|||
.then(status => res.json(status)) |
|||
)); |
|||
|
|||
router.get('/sync', auth.jwt, safeHandler((req, res) => |
|||
bitcoind.getSyncStatus() |
|||
.then(status => res.json(status)) |
|||
)); |
|||
|
|||
router.get('/version', auth.jwt, safeHandler((req, res) => |
|||
bitcoind.getVersion() |
|||
.then(version => res.json(version)) |
|||
)); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,12 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
const lightningLogic = require('logic/lightning.js'); |
|||
const auth = require('middlewares/auth.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
|
|||
router.get('/', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.generateAddress() |
|||
.then(address => res.json(address)) |
|||
)); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,140 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
const lightningLogic = require('logic/lightning.js'); |
|||
const auth = require('middlewares/auth.js'); |
|||
const ValidationError = require('models/errors.js').ValidationError; |
|||
const safeHandler = require('utils/safeHandler'); |
|||
const validator = require('utils/validator.js'); |
|||
|
|||
const DEFAULT_TIME_LOCK_DELTA = 144; // eslint-disable-line no-magic-numbers
|
|||
|
|||
router.get('/', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getChannels() |
|||
.then(channels => res.json(channels)) |
|||
)); |
|||
|
|||
router.get('/estimateFee', auth.jwt, safeHandler(async(req, res, next) => { |
|||
|
|||
const amt = req.query.amt; // Denominated in Satoshi
|
|||
const confTarget = req.query.confTarget; |
|||
|
|||
try { |
|||
validator.isPositiveIntegerOrZero(confTarget); |
|||
validator.isPositiveInteger(amt); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return await lightningLogic.estimateChannelOpenFee(parseInt(amt, 10), parseInt(confTarget, 10)) |
|||
.then(response => res.json(response)); |
|||
})); |
|||
|
|||
router.get('/pending', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getPendingChannels() |
|||
.then(channels => res.json(channels)) |
|||
)); |
|||
|
|||
router.get('/policy', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getChannelPolicy() |
|||
.then(policies => res.json(policies)) |
|||
)); |
|||
|
|||
router.put('/policy', auth.jwt, safeHandler((req, res, next) => { |
|||
const global = req.body.global || false; |
|||
const chanPoint = req.body.chanPoint; |
|||
const baseFeeMsat = req.body.baseFeeMsat; |
|||
const feeRate = req.body.feeRate; |
|||
const timeLockDelta = req.body.timeLockDelta || DEFAULT_TIME_LOCK_DELTA; |
|||
let fundingTxid; |
|||
let outputIndex; |
|||
|
|||
try { |
|||
validator.isBoolean(global); |
|||
|
|||
if (!global) { |
|||
[fundingTxid, outputIndex] = chanPoint.split(':'); |
|||
|
|||
if (fundingTxid === undefined || outputIndex === undefined) { |
|||
throw new ValidationError('Invalid channelPoint.'); |
|||
} |
|||
|
|||
validator.isAlphanumeric(fundingTxid); |
|||
validator.isPositiveIntegerOrZero(outputIndex); |
|||
} |
|||
|
|||
validator.isPositiveIntegerOrZero(baseFeeMsat); |
|||
validator.isDecimal(feeRate + ''); |
|||
validator.isPositiveInteger(timeLockDelta); |
|||
|
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return lightningLogic.updateChannelPolicy(global, fundingTxid, parseInt(outputIndex, 10), baseFeeMsat, feeRate, |
|||
timeLockDelta) |
|||
.then(res.json()); |
|||
})); |
|||
|
|||
router.delete('/close', auth.jwt, safeHandler((req, res, next) => { |
|||
|
|||
const channelPoint = req.body.channelPoint; |
|||
const force = req.body.force; |
|||
|
|||
const parts = channelPoint.split(':'); |
|||
|
|||
if (parts.length !== 2) { // eslint-disable-line no-magic-numbers
|
|||
return next(new Error('Invalid channel point: ' + channelPoint)); |
|||
} |
|||
|
|||
var fundingTxId; |
|||
var index; |
|||
|
|||
try { |
|||
// TODO: fundingTxId, index
|
|||
fundingTxId = parts[0]; |
|||
index = parseInt(parts[1], 10); |
|||
|
|||
validator.isBoolean(force); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return lightningLogic.closeChannel(fundingTxId, index, force) |
|||
.then(channel => res.json(channel)); |
|||
})); |
|||
|
|||
router.get('/count', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getChannelCount() |
|||
.then(count => res.json(count)) |
|||
)); |
|||
|
|||
router.post('/open', auth.jwt, safeHandler((req, res, next) => { |
|||
|
|||
const pubKey = req.body.pubKey; |
|||
const ip = req.body.ip || '127.0.0.1'; |
|||
const port = req.body.port || 9735; // eslint-disable-line no-magic-numbers
|
|||
const amt = req.body.amt; |
|||
const satPerByte = req.body.satPerByte; |
|||
const name = req.body.name; |
|||
const purpose = req.body.purpose; |
|||
|
|||
try { |
|||
|
|||
// TODO validate ip address as ip4 or ip6 address
|
|||
validator.isAlphanumeric(pubKey); |
|||
validator.isPositiveInteger(port); |
|||
validator.isPositiveInteger(amt); |
|||
if (satPerByte) { |
|||
validator.isPositiveInteger(satPerByte); |
|||
} |
|||
validator.isAlphanumericAndSpaces(name); |
|||
validator.isAlphanumericAndSpaces(purpose); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return lightningLogic.openChannel(pubKey, ip, port, amt, satPerByte, name, purpose) |
|||
.then(channel => res.json(channel)); |
|||
})); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,28 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
|
|||
const auth = require('middlewares/auth.js'); |
|||
const lightning = require('logic/lightning.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
|
|||
router.get('/uris', auth.jwt, safeHandler((req, res) => |
|||
lightning.getPublicUris() |
|||
.then(uris => res.json(uris)) |
|||
)); |
|||
|
|||
router.get('/status', auth.jwt, safeHandler((req, res) => |
|||
lightning.getStatus() |
|||
.then(status => res.json(status)) |
|||
)); |
|||
|
|||
router.get('/sync', auth.jwt, safeHandler((req, res) => |
|||
lightning.getSyncStatus() |
|||
.then(status => res.json(status)) |
|||
)); |
|||
|
|||
router.get('/version', auth.jwt, safeHandler((req, res) => |
|||
lightning.getVersion() |
|||
.then(version => res.json(version)) |
|||
)); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,92 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
|
|||
const auth = require('middlewares/auth.js'); |
|||
const lightningLogic = require('logic/lightning.js'); |
|||
const validator = require('utils/validator.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
|
|||
router.post('/addInvoice', safeHandler(async(req, res, next) => { |
|||
|
|||
const amt = req.body.amt; // Denominated in Satoshi
|
|||
const memo = req.body.memo || ''; |
|||
|
|||
try { |
|||
validator.isPositiveIntegerOrZero(amt); |
|||
validator.isValidMemoLength(memo); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return await lightningLogic.addInvoice(amt, memo) |
|||
.then(invoice => res.json(invoice)); |
|||
})); |
|||
|
|||
router.get('/forwardingEvents', auth.jwt, safeHandler((req, res, next) => { |
|||
|
|||
const startTime = req.query.startTime; |
|||
const endTime = req.query.endTime; |
|||
const indexOffset = req.query.indexOffset; |
|||
|
|||
try { |
|||
if (startTime) { |
|||
validator.isPositiveIntegerOrZero(startTime); |
|||
} |
|||
if (endTime) { |
|||
validator.isPositiveIntegerOrZero(endTime); |
|||
} |
|||
if (indexOffset) { |
|||
validator.isPositiveIntegerOrZero(indexOffset); |
|||
} |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return lightningLogic.getForwardingEvents(startTime, endTime, indexOffset) |
|||
.then(events => res.json(events)); |
|||
})); |
|||
|
|||
router.get('/invoice', auth.jwt, safeHandler((req, res, next) => { |
|||
|
|||
const paymentRequest = req.query.paymentRequest; |
|||
|
|||
try { |
|||
validator.isAlphanumeric(paymentRequest); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return lightningLogic.decodePaymentRequest(paymentRequest) |
|||
.then(invoice => res.json(invoice)); |
|||
})); |
|||
|
|||
router.get('/invoices', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getInvoices() |
|||
.then(invoices => res.json(invoices)) |
|||
)); |
|||
|
|||
router.post('/payInvoice', auth.jwt, safeHandler(async(req, res, next) => { |
|||
|
|||
const paymentRequest = req.body.paymentRequest; |
|||
const amt = req.body.amt; |
|||
|
|||
try { |
|||
validator.isAlphanumeric(paymentRequest); |
|||
|
|||
if (amt) { |
|||
validator.isPositiveIntegerOrZero(amt); |
|||
} |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return await lightningLogic.payInvoice(paymentRequest, amt) |
|||
.then(invoice => res.json(invoice)); |
|||
})); |
|||
|
|||
router.get('/payments', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getPayments() |
|||
.then(payments => res.json(payments)) |
|||
)); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,57 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
const validator = require('utils/validator.js'); |
|||
const lightningLogic = require('logic/lightning.js'); |
|||
const auth = require('middlewares/auth.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
|
|||
router.get('/', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getOnChainTransactions() |
|||
.then(transactions => res.json(transactions)) |
|||
)); |
|||
|
|||
router.post('/', auth.jwt, safeHandler((req, res, next) => { |
|||
|
|||
const addr = req.body.addr; |
|||
const amt = req.body.amt; |
|||
const satPerByte = req.body.satPerByte; |
|||
const sendAll = req.body.sendAll === true; |
|||
|
|||
try { |
|||
// TODO: addr
|
|||
validator.isPositiveInteger(amt); |
|||
validator.isBoolean(sendAll); |
|||
if (satPerByte) { |
|||
validator.isPositiveInteger(satPerByte); |
|||
} |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return lightningLogic.sendCoins(addr, amt, satPerByte, sendAll) |
|||
.then(transaction => res.json(transaction)); |
|||
})); |
|||
|
|||
router.get('/estimateFee', auth.jwt, safeHandler(async(req, res, next) => { |
|||
|
|||
const address = req.query.address; |
|||
const amt = req.query.amt; // Denominated in Satoshi
|
|||
const confTarget = req.query.confTarget; |
|||
const sweep = req.query.sweep === 'true'; |
|||
|
|||
try { |
|||
validator.isAlphanumeric(address); |
|||
validator.isPositiveIntegerOrZero(confTarget); |
|||
|
|||
if (!sweep) { |
|||
validator.isPositiveInteger(amt); |
|||
} |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
return await lightningLogic.estimateFee(address, parseInt(amt, 10), parseInt(confTarget, 10), sweep) |
|||
.then(response => res.json(response)); |
|||
})); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,13 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
|
|||
const auth = require('middlewares/auth.js'); |
|||
const applicationLogic = require('logic/application.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
|
|||
router.post('/backup', auth.jwt, safeHandler((req, res) => |
|||
applicationLogic.lndBackup() |
|||
.then(response => res.json(response)) |
|||
)); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,91 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
const lightningLogic = require('logic/lightning.js'); |
|||
const auth = require('middlewares/auth.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
const constants = require('utils/const.js'); |
|||
const logger = require('utils/logger.js'); |
|||
const validator = require('utils/validator.js'); |
|||
const LndError = require('models/errors.js').LndError; |
|||
|
|||
router.get('/btc', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getWalletBalance() |
|||
.then(balance => res.json(balance)) |
|||
)); |
|||
|
|||
// API endpoint to change your lnd password. Wallet must exist and be unlocked.
|
|||
router.post('/changePassword', auth.jwt, safeHandler(async(req, res, next) => { |
|||
|
|||
const currentPassword = req.body.currentPassword; |
|||
const newPassword = req.body.newPassword; |
|||
|
|||
try { |
|||
validator.isString(currentPassword); |
|||
validator.isMinPasswordLength(currentPassword); |
|||
validator.isString(newPassword); |
|||
validator.isMinPasswordLength(newPassword); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
try { |
|||
await lightningLogic.changePassword(currentPassword, newPassword); |
|||
|
|||
return res.status(constants.STATUS_CODES.OK).json(); |
|||
} catch (error) { |
|||
if (error instanceof LndError && error.message === 'Unable to change password') { |
|||
|
|||
logger.info(error, 'changePassword'); |
|||
|
|||
// Invalid passphrase for master public key
|
|||
if (error.error.code === constants.LND_STATUS_CODES.UNKNOWN) { |
|||
|
|||
return res.status(constants.STATUS_CODES.FORBIDDEN).json(); |
|||
|
|||
// Connect Failed (lnd is probably restarting)
|
|||
} else if (error.error.code === constants.LND_STATUS_CODES.UNAVAILABLE) { |
|||
|
|||
return res.status(constants.STATUS_CODES.BAD_GATEWAY).json(); |
|||
} |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
|
|||
})); |
|||
|
|||
// Should not include auth because the user isn't registered yet. Once the user initializes a wallet, that wallet is
|
|||
// locked and cannot be updated unless a full system reset is initiated.
|
|||
router.post('/init', safeHandler((req, res) => { |
|||
|
|||
const password = req.body.password; |
|||
const seed = req.body.seed; |
|||
|
|||
if (seed.length !== 24) { // eslint-disable-line no-magic-numbers
|
|||
throw new Error('Invalid seed length'); |
|||
} |
|||
|
|||
// TODO validate password requirements
|
|||
|
|||
return lightningLogic.initializeWallet(password, seed) |
|||
.then(response => res.json(response)); |
|||
})); |
|||
|
|||
router.get('/lightning', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.getChannelBalance() |
|||
.then(balance => res.json(balance)) |
|||
)); |
|||
|
|||
// Should not include auth because the user isn't registered yet. The user can get a seed phrase as many times as they
|
|||
// would like until the wallet has been initialized.
|
|||
router.get('/seed', safeHandler((req, res) => |
|||
lightningLogic.generateSeed() |
|||
.then(seed => res.json(seed)) |
|||
)); |
|||
|
|||
router.post('/unlock', auth.jwt, safeHandler((req, res) => |
|||
lightningLogic.unlockWallet(req.body.password) |
|||
.then(response => res.json(response)) |
|||
)); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,12 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
const pagesLogic = require('logic/pages.js'); |
|||
const auth = require('middlewares/auth.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
|
|||
router.get('/lnd', auth.jwt, safeHandler((req, res) => |
|||
pagesLogic.lndDetails() |
|||
.then(address => res.json(address)) |
|||
)); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,56 @@ |
|||
const childProcess = require('child_process'); |
|||
|
|||
// Sets environment variables on container.
|
|||
// Env should not contain sensitive data, because environment variables are not secure.
|
|||
function extendProcessEnv(env) { |
|||
Object.keys(env).map(function(objectKey) { // eslint-disable-line array-callback-return
|
|||
process.env[objectKey] = env[objectKey]; |
|||
}); |
|||
} |
|||
|
|||
// Executes docker-compose command with common options
|
|||
const exec = (command, args, opts) => new Promise((resolve, reject) => { |
|||
const options = opts || {}; |
|||
|
|||
const cwd = options.cwd || null; |
|||
|
|||
if (options.env) { |
|||
extendProcessEnv(options.env); |
|||
} |
|||
|
|||
const childProc = childProcess.spawn(command, args, {cwd}); |
|||
|
|||
childProc.on('error', err => { |
|||
reject(err); |
|||
}); |
|||
|
|||
const result = { |
|||
err: '', |
|||
out: '' |
|||
}; |
|||
|
|||
childProc.stdout.on('data', chunk => { |
|||
result.out += chunk.toString(); |
|||
}); |
|||
|
|||
childProc.stderr.on('data', chunk => { |
|||
result.err += chunk.toString(); |
|||
}); |
|||
|
|||
childProc.on('close', code => { |
|||
if (code === 0) { |
|||
resolve(result); |
|||
} else { |
|||
reject(result.err); |
|||
} |
|||
}); |
|||
|
|||
if (options.log) { |
|||
childProc.stdout.pipe(process.stdout); |
|||
childProc.stderr.pipe(process.stderr); |
|||
} |
|||
}); |
|||
|
|||
module.exports = { |
|||
exec, |
|||
}; |
@ -0,0 +1,74 @@ |
|||
const RpcClient = require('bitcoind-rpc'); |
|||
const camelizeKeys = require('camelize-keys'); |
|||
|
|||
const BitcoindError = require('models/errors.js').BitcoindError; |
|||
|
|||
const BITCOIND_RPC_PORT = process.env.BITCOIN_NETWORK === 'testnet' ? 18332 : 8332; // eslint-disable-line no-magic-numbers, max-len
|
|||
const BITCOIND_HOST = process.env.BITCOIN_HOST || '127.0.0.1'; |
|||
const BITCOIND_RPC_USER = process.env.RPC_USER; |
|||
const BITCOIND_RPC_PASSWORD = process.env.RPC_PASSWORD; |
|||
|
|||
const rpcClient = new RpcClient({ |
|||
protocol: 'http', |
|||
user: BITCOIND_RPC_USER, // eslint-disable-line object-shorthand
|
|||
pass: BITCOIND_RPC_PASSWORD, // eslint-disable-line object-shorthand
|
|||
host: BITCOIND_HOST, |
|||
port: BITCOIND_RPC_PORT, |
|||
}); |
|||
|
|||
function promiseify(rpcObj, rpcFn, what) { |
|||
return new Promise((resolve, reject) => { |
|||
try { |
|||
rpcFn.call(rpcObj, (err, info) => { |
|||
if (err) { |
|||
reject(new BitcoindError(`Unable to obtain ${what}`, err)); |
|||
} else { |
|||
resolve(camelizeKeys(info, '_')); |
|||
} |
|||
}); |
|||
} catch (error) { |
|||
reject(error); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function getBlockChainInfo() { |
|||
return promiseify(rpcClient, rpcClient.getBlockchainInfo, 'blockchain info'); |
|||
} |
|||
|
|||
function getPeerInfo() { |
|||
return promiseify(rpcClient, rpcClient.getPeerInfo, 'peer info'); |
|||
} |
|||
|
|||
function getBlockCount() { |
|||
return promiseify(rpcClient, rpcClient.getBlockCount, 'block count'); |
|||
} |
|||
|
|||
function getMempoolInfo() { |
|||
return promiseify(rpcClient, rpcClient.getMemPoolInfo, 'get mempool info'); |
|||
} |
|||
|
|||
function getNetworkInfo() { |
|||
return promiseify(rpcClient, rpcClient.getNetworkInfo, 'network info'); |
|||
} |
|||
|
|||
function help() { |
|||
// TODO: missing from the library, but can add it not sure how to package.
|
|||
// rpc.uptime(function (err, res) {
|
|||
// if (err) {
|
|||
// deferred.reject({status: 'offline'});
|
|||
// } else {
|
|||
// deferred.resolve({status: 'online'});
|
|||
// }
|
|||
// });
|
|||
return promiseify(rpcClient, rpcClient.help, 'help data'); |
|||
} |
|||
|
|||
module.exports = { |
|||
getBlockChainInfo, |
|||
getBlockCount, |
|||
getPeerInfo, |
|||
getMempoolInfo, |
|||
getNetworkInfo, |
|||
help, |
|||
}; |
@ -0,0 +1,68 @@ |
|||
/** |
|||
* Generic disk functions. |
|||
*/ |
|||
|
|||
const logger = require('utils/logger'); |
|||
const fs = require('fs'); |
|||
const crypto = require('crypto'); |
|||
const uint32Bytes = 4; |
|||
|
|||
// Reads a file. Wraps fs.readFile into a native promise
|
|||
function readFile(filePath, encoding) { |
|||
return new Promise((resolve, reject) => fs.readFile(filePath, encoding, (err, str) => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(str); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
// Reads a file as a utf8 string. Wraps fs.readFile into a native promise
|
|||
function readUtf8File(filePath) { |
|||
return readFile(filePath, 'utf8'); |
|||
} |
|||
|
|||
function readJsonFile(filePath) { |
|||
return readUtf8File(filePath).then(JSON.parse); |
|||
} |
|||
|
|||
// Writes a string to a file. Wraps fs.writeFile into a native promise
|
|||
// This is _not_ concurrency safe, so don't export it without making it like writeJsonFile
|
|||
function writeFile(filePath, data, encoding) { |
|||
return new Promise((resolve, reject) => fs.writeFile(filePath, data, encoding, err => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
function writeJsonFile(filePath, obj) { |
|||
const tempFileName = `${filePath}.${crypto.randomBytes(uint32Bytes).readUInt32LE(0)}`; |
|||
|
|||
return writeFile(tempFileName, JSON.stringify(obj), 'utf8') |
|||
.then(() => new Promise((resolve, reject) => fs.rename(tempFileName, filePath, err => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(); |
|||
} |
|||
}))) |
|||
.catch(err => { |
|||
if (err) { |
|||
fs.unlink(tempFileName, err => { |
|||
logger.warn('Error removing temporary file after error', 'disk', {err, tempFileName}); |
|||
}); |
|||
} |
|||
throw err; |
|||
}); |
|||
} |
|||
|
|||
module.exports = { |
|||
readFile, |
|||
readUtf8File, |
|||
readJsonFile, |
|||
writeJsonFile, |
|||
}; |
@ -0,0 +1,441 @@ |
|||
/* eslint-disable camelcase, max-lines */ |
|||
const grpc = require('grpc'); |
|||
const camelizeKeys = require('camelize-keys'); |
|||
|
|||
const diskService = require('services/disk'); |
|||
const LndError = require('models/errors.js').LndError; |
|||
|
|||
const LND_HOST = process.env.LND_HOST || '127.0.0.1'; |
|||
const TLS_FILE = process.env.TLS_FILE || '/lnd/tls.cert'; |
|||
const PROTO_FILE = process.env.PROTO_FILE || './resources/rpc.proto'; |
|||
const LND_PORT = process.env.LND_PORT || 10009; // eslint-disable-line no-magic-numbers
|
|||
|
|||
// LND changed the macaroon path to ~/.lnd/data/chain/{chain}/{network}/admin.macaroon. We are currently only
|
|||
// supporting bitcoind and have that hard coded. However, we are leaving the ability to switch between testnet and
|
|||
// mainnet. This can be done with the /reset route. LND_NETWORK will be defaulted in /usr/local/casa/applications/.env.
|
|||
// LND_NETWORK will be overwritten in the settings file.
|
|||
let MACAROON_FILE = '/lnd/data/chain/bitcoin/' + process.env.LND_NETWORK + '/admin.macaroon'; |
|||
|
|||
// Developers should overwrite MACAROON_DIR in their .env file or ide. We recommend 'os.homedir() + /lightning-node/'.
|
|||
if (process.env.MACAROON_DIR) { |
|||
MACAROON_FILE = process.env.MACAROON_DIR + 'admin.macaroon'; |
|||
} |
|||
|
|||
// TODO move this to volume
|
|||
const lnrpcDescriptor = grpc.load(PROTO_FILE); |
|||
const lnrpc = lnrpcDescriptor.lnrpc; |
|||
|
|||
const DEFAULT_RECOVERY_WINDOW = 250; |
|||
|
|||
// Initialize RPC client will attempt to connect to the lnd rpc with a tls.cert and admin.macaroon. If the wallet has
|
|||
// not bee created yet, then the client will only be initialized with the tls.cert. There may be times when lnd wallet
|
|||
// is reset and the tls.cert and admin.macaroon will change.
|
|||
async function initializeRPCClient() { |
|||
return diskService.readFile(TLS_FILE) |
|||
.then(lndCert => { |
|||
const sslCreds = grpc.credentials.createSsl(lndCert); |
|||
|
|||
return diskService.readFile(MACAROON_FILE) |
|||
.then(macaroon => { |
|||
// build meta data credentials
|
|||
const metadata = new grpc.Metadata(); |
|||
metadata.add('macaroon', macaroon.toString('hex')); |
|||
const macaroonCreds = grpc.credentials.createFromMetadataGenerator((_args, callback) => { |
|||
callback(null, metadata); |
|||
}); |
|||
|
|||
// combine the cert credentials and the macaroon auth credentials
|
|||
// such that every call is properly encrypted and authenticated
|
|||
return { |
|||
credentials: grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds), |
|||
state: true |
|||
}; |
|||
}) |
|||
.catch(() => ({ credentials: sslCreds, state: 'WALLET_CREATION_ONLY' })); |
|||
}) |
|||
.then(({ credentials, state }) => ({ |
|||
lightning: new lnrpc.Lightning(LND_HOST + ':' + LND_PORT, credentials), |
|||
walletUnlocker: new lnrpc.WalletUnlocker(LND_HOST + ':' + LND_PORT, credentials), |
|||
state: state // eslint-disable-line object-shorthand
|
|||
})); |
|||
} |
|||
|
|||
async function promiseify(rpcObj, rpcFn, payload, description) { |
|||
return new Promise((resolve, reject) => { |
|||
try { |
|||
rpcFn.call(rpcObj, payload, (error, grpcResponse) => { |
|||
if (error) { |
|||
reject(new LndError(`Unable to ${description}`, error)); |
|||
} else { |
|||
resolve(camelizeKeys(grpcResponse, '_')); |
|||
} |
|||
}); |
|||
} catch (error) { |
|||
reject(error); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// an amount, an options memo, and can only be paid to node that created it.
|
|||
async function addInvoice(amount, memo) { |
|||
const rpcPayload = { |
|||
value: amount, |
|||
memo: memo, // eslint-disable-line object-shorthand
|
|||
expiry: 3600 // Should we make this ENV specific for ease of testing?
|
|||
}; |
|||
|
|||
const conn = await initializeRPCClient(); |
|||
|
|||
const grpcResponse = await promiseify(conn.lightning, conn.lightning.addInvoice, rpcPayload, 'create new invoice'); |
|||
|
|||
if (grpcResponse && grpcResponse.paymentRequest) { |
|||
return { |
|||
rHash: grpcResponse.rHash, |
|||
paymentRequest: grpcResponse.paymentRequest, |
|||
}; |
|||
} else { |
|||
throw new LndError('Unable to parse invoice from lnd'); |
|||
} |
|||
} |
|||
|
|||
// Change your lnd password. Wallet must exist and be unlocked.
|
|||
async function changePassword(currentPassword, newPassword) { |
|||
|
|||
const currentPasswordBuff = Buffer.from(currentPassword, 'utf8'); |
|||
const newPasswordBuff = Buffer.from(newPassword, 'utf8'); |
|||
|
|||
const rpcPayload = { |
|||
current_password: currentPasswordBuff, |
|||
new_password: newPasswordBuff, |
|||
}; |
|||
|
|||
const conn = await initializeRPCClient(); |
|||
|
|||
return await promiseify(conn.walletUnlocker, conn.walletUnlocker.changePassword, rpcPayload, 'change password'); |
|||
} |
|||
|
|||
function closeChannel(fundingTxId, index, force) { |
|||
const rpcPayload = { |
|||
channel_point: { |
|||
funding_txid_str: fundingTxId, |
|||
output_index: index |
|||
}, |
|||
force: force // eslint-disable-line object-shorthand
|
|||
}; |
|||
|
|||
return initializeRPCClient().then(({ lightning }) => new Promise((resolve, reject) => { |
|||
try { |
|||
const call = lightning.CloseChannel(rpcPayload); |
|||
|
|||
call.on('data', chan => { |
|||
if (chan.update === 'close_pending') { |
|||
resolve(); |
|||
} |
|||
}); |
|||
|
|||
call.on('error', error => { |
|||
reject(new LndError('Unable to close channel', error)); |
|||
}); |
|||
} catch (error) { |
|||
reject(error); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
// Connects this lnd node to a peer.
|
|||
function connectToPeer(pubKey, ip, port) { |
|||
const rpcPayload = { |
|||
addr: { |
|||
pubkey: pubKey, |
|||
host: ip + ':' + port |
|||
} |
|||
}; |
|||
|
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.ConnectPeer, rpcPayload, 'connect to peer')); |
|||
} |
|||
|
|||
function decodePaymentRequest(paymentRequest) { |
|||
const rpcPayload = { |
|||
pay_req: paymentRequest |
|||
}; |
|||
|
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.decodePayReq, rpcPayload, 'decode payment request')) |
|||
.then(invoice => { |
|||
// add on payment request for extra details
|
|||
invoice.paymentRequest = paymentRequest; |
|||
|
|||
return invoice; |
|||
}); |
|||
} |
|||
|
|||
async function estimateFee(address, amt, confTarget) { |
|||
const addrToAmount = {}; |
|||
addrToAmount[address] = amt; |
|||
|
|||
const rpcPayload = { |
|||
AddrToAmount: addrToAmount, |
|||
target_conf: confTarget, |
|||
}; |
|||
|
|||
const conn = await initializeRPCClient(); |
|||
|
|||
return await promiseify(conn.lightning, conn.lightning.estimateFee, rpcPayload, 'estimate fee request'); |
|||
} |
|||
|
|||
async function generateAddress() { |
|||
const rpcPayload = { |
|||
type: 1 |
|||
}; |
|||
|
|||
const conn = await initializeRPCClient(); |
|||
|
|||
return await promiseify(conn.lightning, conn.lightning.NewAddress, rpcPayload, 'generate address'); |
|||
} |
|||
|
|||
function generateSeed() { |
|||
return initializeRPCClient().then(({ walletUnlocker, state }) => { |
|||
if (state === true) { |
|||
throw new LndError('Macaroon exists, therefore wallet already exists'); |
|||
} |
|||
|
|||
return promiseify(walletUnlocker, walletUnlocker.GenSeed, {}, 'generate seed'); |
|||
}); |
|||
} |
|||
|
|||
function getChannelBalance() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.ChannelBalance, {}, 'get channel balance')); |
|||
} |
|||
|
|||
function getFeeReport() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.FeeReport, {}, 'get fee report')); |
|||
} |
|||
|
|||
function getForwardingEvents(startTime, endTime, indexOffset) { |
|||
const rpcPayload = { |
|||
start_time: startTime, |
|||
end_time: endTime, |
|||
index_offset: indexOffset, |
|||
}; |
|||
|
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.ForwardingHistory, rpcPayload, 'get forwarding events')); |
|||
} |
|||
|
|||
function getInfo() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.GetInfo, {}, 'get lnd information')); |
|||
} |
|||
|
|||
// Returns a list of lnd's currently open channels. Channels are considered open by this node and it's directly
|
|||
// connected peer after three confirmation. After six confirmations, the channel is broadcasted by this node and it's
|
|||
// directly connected peer to the broader lightning network.
|
|||
function getOpenChannels() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.ListChannels, {}, 'list channels')) |
|||
.then(grpcResponse => grpcResponse.channels); |
|||
} |
|||
|
|||
function getClosedChannels() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.ClosedChannels, {}, 'closed channels')) |
|||
.then(grpcResponse => grpcResponse.channels); |
|||
} |
|||
|
|||
// Returns a list of all outgoing payments.
|
|||
function getPayments() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.ListPayments, {}, 'get payments')); |
|||
} |
|||
|
|||
// Returns a list of all lnd's currently connected and active peers.
|
|||
function getPeers() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.ListPeers, {}, 'get peer information')) |
|||
.then(grpcResponse => { |
|||
if (grpcResponse && grpcResponse.peers) { |
|||
return grpcResponse.peers; |
|||
} else { |
|||
throw new LndError('Unable to parse peer information'); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// Returns a list of lnd's currently pending channels. Pending channels include, channels that are in the process of
|
|||
// being opened, but have not reached three confirmations. Channels that are pending closed, but have not reached
|
|||
// one confirmation. Forced close channels that require potentially hundreds of confirmations.
|
|||
function getPendingChannels() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.PendingChannels, {}, 'list pending channels')); |
|||
} |
|||
|
|||
function getWalletBalance() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.WalletBalance, {}, 'get wallet balance')); |
|||
} |
|||
|
|||
function initWallet(options) { |
|||
const passwordBuff = Buffer.from(options.password, 'utf8'); |
|||
|
|||
const rpcPayload = { |
|||
wallet_password: passwordBuff, |
|||
cipher_seed_mnemonic: options.mnemonic, |
|||
recovery_window: DEFAULT_RECOVERY_WINDOW |
|||
}; |
|||
|
|||
return initializeRPCClient().then(({ walletUnlocker, state }) => { |
|||
if (state === true) { |
|||
throw new LndError('Macaroon exists, therefore wallet already exists'); |
|||
} |
|||
|
|||
return promiseify(walletUnlocker, walletUnlocker.InitWallet, rpcPayload, 'initialize wallet') |
|||
.then(() => options.mnemonic); |
|||
}); |
|||
} |
|||
|
|||
// Returns a list of all invoices.
|
|||
function getInvoices() { |
|||
const rpcPayload = { |
|||
reversed: true, // Returns most recent
|
|||
num_max_invoices: 100, |
|||
}; |
|||
|
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.ListInvoices, rpcPayload, 'list invoices')); |
|||
} |
|||
|
|||
// Returns a list of all on chain transactions.
|
|||
function getOnChainTransactions() { |
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.GetTransactions, {}, 'list on-chain transactions')) |
|||
.then(grpcResponse => grpcResponse.transactions); |
|||
} |
|||
|
|||
async function listUnspent() { |
|||
const rpcPayload = { |
|||
min_confs: 1, |
|||
max_confs: 10000000, // Use arbitrarily high maximum confirmation limit.
|
|||
}; |
|||
|
|||
const conn = await initializeRPCClient(); |
|||
|
|||
return await promiseify(conn.lightning, conn.lightning.listUnspent, rpcPayload, 'estimate fee request'); |
|||
} |
|||
|
|||
function openChannel(pubKey, amt, satPerByte) { |
|||
const rpcPayload = { |
|||
node_pubkey_string: pubKey, |
|||
local_funding_amount: amt, |
|||
}; |
|||
|
|||
if (satPerByte) { |
|||
rpcPayload.sat_per_byte = satPerByte; |
|||
} else { |
|||
rpcPayload.target_conf = 6; |
|||
} |
|||
|
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.OpenChannelSync, rpcPayload, 'open channel')); |
|||
} |
|||
|
|||
function sendCoins(addr, amt, satPerByte, sendAll) { |
|||
const rpcPayload = { |
|||
addr: addr, // eslint-disable-line object-shorthand
|
|||
amount: amt, |
|||
send_all: sendAll, |
|||
}; |
|||
|
|||
if (satPerByte) { |
|||
rpcPayload.sat_per_byte = satPerByte; |
|||
} else { |
|||
rpcPayload.target_conf = 6; |
|||
} |
|||
|
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.SendCoins, rpcPayload, 'send coins')); |
|||
} |
|||
|
|||
function sendPaymentSync(paymentRequest, amt) { |
|||
const rpcPayload = { |
|||
payment_request: paymentRequest, |
|||
amt: amt, // eslint-disable-line object-shorthand
|
|||
}; |
|||
|
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.SendPaymentSync, rpcPayload, 'send lightning payment')) |
|||
.then(response => { |
|||
// sometimes the error comes in on the response...
|
|||
if (response.paymentError) { |
|||
throw new LndError(`Unable to send lightning payment: ${response.paymentError}`); |
|||
} |
|||
|
|||
return response; |
|||
}); |
|||
} |
|||
|
|||
function unlockWallet(password) { |
|||
const passwordBuff = Buffer.from(password, 'utf8'); |
|||
|
|||
const rpcPayload = { |
|||
wallet_password: passwordBuff |
|||
}; |
|||
|
|||
// TODO how to determine if wallet is already unlocked?
|
|||
// This will throw code 12 unimplemented, which is not very helpful
|
|||
return initializeRPCClient() |
|||
.then(({ walletUnlocker }) => promiseify(walletUnlocker, walletUnlocker.UnlockWallet, rpcPayload, 'unlock wallet')); |
|||
} |
|||
|
|||
function updateChannelPolicy(global, fundingTxid, outputIndex, baseFeeMsat, feeRate, timeLockDelta) { |
|||
const rpcPayload = { |
|||
base_fee_msat: baseFeeMsat, |
|||
fee_rate: feeRate, |
|||
time_lock_delta: timeLockDelta, |
|||
}; |
|||
|
|||
if (global) { |
|||
rpcPayload.global = global; |
|||
} else { |
|||
rpcPayload.chan_point = { |
|||
funding_txid_str: fundingTxid, |
|||
output_index: outputIndex, |
|||
}; |
|||
} |
|||
|
|||
return initializeRPCClient() |
|||
.then(({ lightning }) => promiseify(lightning, lightning.UpdateChannelPolicy, rpcPayload, |
|||
'update channel policy coins')); |
|||
} |
|||
|
|||
module.exports = { |
|||
addInvoice, |
|||
changePassword, |
|||
closeChannel, |
|||
connectToPeer, |
|||
decodePaymentRequest, |
|||
estimateFee, |
|||
getChannelBalance, |
|||
getClosedChannels, |
|||
getFeeReport, |
|||
getForwardingEvents, |
|||
getInfo, |
|||
getInvoices, |
|||
getOpenChannels, |
|||
getPayments, |
|||
getPeers, |
|||
getPendingChannels, |
|||
getWalletBalance, |
|||
generateAddress, |
|||
generateSeed, |
|||
getOnChainTransactions, |
|||
initWallet, |
|||
listUnspent, |
|||
openChannel, |
|||
sendCoins, |
|||
sendPaymentSync, |
|||
unlockWallet, |
|||
updateChannelPolicy, |
|||
}; |
@ -0,0 +1,15 @@ |
|||
// This file contains things that must happen before the app is imported (ie. things that happen on import)
|
|||
/* eslint-disable max-len */ |
|||
process.env.MACAROON_FILE = './test/fixtures/lnd/admin.macaroon'; |
|||
process.env.TLS_FILE = './test/fixtures/lnd/tls.cert'; |
|||
process.env.RPC_USER = 'test-user'; |
|||
process.env.RPC_PASSWORD = 'test-pass'; |
|||
process.env.JWT_PUBLIC_KEY = '2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d4677774451594a4b6f5a496876634e41514542425141445377417753414a42414a6949444e682b6770544f3937627135574748657476323267465a47736f4a0a6e6b54665058774335726a61674b4d56455a4a4a47584e6d51544e7441596e53615a31754a6e692f48356b4b32594e614a333933326730434177454141513d3d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d'; |
|||
|
|||
const sinon = require('sinon'); |
|||
global.Lightning = sinon.stub(); |
|||
global.WalletUnlocker = sinon.stub(); |
|||
sinon.stub(require('grpc'), 'load').returns({lnrpc: { |
|||
Lightning: global.Lightning, |
|||
WalletUnlocker: global.WalletUnlocker |
|||
}}); |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"extends": "../.eslintrc", |
|||
"env": { |
|||
"mocha": true |
|||
}, |
|||
"rules": { |
|||
"no-magic-numbers": "off" |
|||
} |
|||
} |
@ -0,0 +1,17 @@ |
|||
/* globals requester */ |
|||
|
|||
describe('ping', () => { |
|||
it('should respond on /ping GET', done => { |
|||
requester |
|||
.get('/ping') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('version'); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,567 @@ |
|||
/* eslint-disable max-len,id-length */ |
|||
/* globals requester, reset */ |
|||
const sinon = require('sinon'); |
|||
const bitcoindMocks = require('../../../mocks/bitcoind.js'); |
|||
|
|||
describe('v1/bitcoind/info endpoint', () => { |
|||
let token; |
|||
|
|||
before(async() => { |
|||
reset(); |
|||
|
|||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A'; |
|||
}); |
|||
|
|||
describe('/addresses GET', function() { |
|||
let bitcoindRPCGetPeerInfo; |
|||
let bitcoindRPCGetNetworkInfo; |
|||
|
|||
afterEach(() => { |
|||
bitcoindRPCGetPeerInfo.restore(); |
|||
bitcoindRPCGetNetworkInfo.restore(); |
|||
}); |
|||
|
|||
it('should respond for an IPv4 address', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo').callsFake(callback => callback(undefined, { |
|||
result: |
|||
[ |
|||
{ |
|||
addrlocal: '100.101.102.103:10249' |
|||
} |
|||
] |
|||
})); |
|||
bitcoindRPCGetNetworkInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getNetworkInfo') |
|||
.callsFake(callback => callback(undefined, bitcoindMocks.getNetworkInfoWithoutTor())); |
|||
requester |
|||
.get('/v1/bitcoind/info/addresses') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.length.should.equal(1); |
|||
res.body[0].should.equal('100.101.102.103'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should respond for an IPv6 address', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo').callsFake(callback => callback(undefined, { |
|||
result: |
|||
[ |
|||
{ |
|||
addrlocal: '2001:0db8:85a3:0000:0000:8a2e:0370:10249' |
|||
} |
|||
] |
|||
})); |
|||
bitcoindRPCGetNetworkInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getNetworkInfo') |
|||
.callsFake(callback => callback(undefined, bitcoindMocks.getNetworkInfoWithoutTor())); |
|||
requester |
|||
.get('/v1/bitcoind/info/addresses') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.length.should.equal(1); |
|||
res.body[0].should.equal('2001:0db8:85a3:0000:0000:8a2e:0370'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 401 without a valid token', done => { |
|||
requester |
|||
.get('/v1/bitcoind/info/addresses') |
|||
.set('authorization', 'JWT invalid') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(401); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 500 on error', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo').callsFake(callback => callback('error', {})); |
|||
requester |
|||
.get('/v1/bitcoind/info/addresses') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(500); |
|||
res.body.should.equal('Unable to obtain peer info'); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('/blockCount GET', function() { |
|||
let bitcoindRPCGetBlockCount; |
|||
|
|||
afterEach(() => { |
|||
bitcoindRPCGetBlockCount.restore(); |
|||
}); |
|||
|
|||
it('should respond with blockCount', done => { |
|||
bitcoindRPCGetBlockCount = sinon.stub(require('bitcoind-rpc').prototype, 'getBlockCount').callsFake(callback => callback(undefined, {result: 515055})); |
|||
requester |
|||
.get('/v1/bitcoind/info/blockcount') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('blockCount'); |
|||
res.body.blockCount.should.equal(515055); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 401 without a valid token', done => { |
|||
requester |
|||
.get('/v1/bitcoind/info/blockcount') |
|||
.set('authorization', 'JWT invalid') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(401); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 500 on error', done => { |
|||
bitcoindRPCGetBlockCount = sinon.stub(require('bitcoind-rpc').prototype, 'getBlockCount').callsFake(callback => callback('error', {})); |
|||
requester |
|||
.get('/v1/bitcoind/info/blockcount') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(500); |
|||
res.body.should.equal('Unable to obtain block count'); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('/connections GET', function() { |
|||
let bitcoindRPCGetPeerInfo; |
|||
|
|||
afterEach(() => { |
|||
bitcoindRPCGetPeerInfo.restore(); |
|||
}); |
|||
|
|||
it('should respond with connections', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: [ |
|||
{ |
|||
inbound: false |
|||
}, |
|||
{ |
|||
inbound: false |
|||
}, |
|||
{ |
|||
inbound: true |
|||
} |
|||
] |
|||
})); |
|||
requester |
|||
.get('/v1/bitcoind/info/connections') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.total.should.equal(3); |
|||
res.body.inbound.should.equal(1); |
|||
res.body.outbound.should.equal(2); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should respond with zero connections', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: [] |
|||
})); |
|||
requester |
|||
.get('/v1/bitcoind/info/connections') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.total.should.equal(0); |
|||
res.body.inbound.should.equal(0); |
|||
res.body.outbound.should.equal(0); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 401 without a valid token', done => { |
|||
requester |
|||
.get('/v1/bitcoind/info/connections') |
|||
.set('authorization', 'JWT invalid') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(401); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 500 on error', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo').callsFake(callback => callback('error', {})); |
|||
requester |
|||
.get('/v1/bitcoind/info/connections') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(500); |
|||
res.body.should.equal('Unable to obtain peer info'); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('/status GET', function() { |
|||
let bitcoindRPCGetHelp; |
|||
|
|||
afterEach(() => { |
|||
bitcoindRPCGetHelp.restore(); |
|||
}); |
|||
|
|||
it('should respond operational true', done => { |
|||
bitcoindRPCGetHelp = sinon.stub(require('bitcoind-rpc').prototype, 'help').callsFake(callback => callback(undefined, {})); |
|||
requester |
|||
.get('/v1/bitcoind/info/status') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('operational'); |
|||
res.body.operational.should.equal(true); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 401 without a valid token', done => { |
|||
requester |
|||
.get('/v1/bitcoind/info/status') |
|||
.set('authorization', 'JWT invalid') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(401); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should respond operational false on error', done => { |
|||
bitcoindRPCGetHelp = sinon.stub(require('bitcoind-rpc').prototype, 'help').callsFake(callback => callback('error', {})); |
|||
requester |
|||
.get('/v1/bitcoind/info/status') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('operational'); |
|||
res.body.operational.should.equal(false); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('/sync GET', function() { |
|||
let bitcoindRPCGetPeerInfo; |
|||
let bitcoindRPCGetBlockChainInfo; |
|||
|
|||
afterEach(() => { |
|||
bitcoindRPCGetPeerInfo.restore(); |
|||
|
|||
if (bitcoindRPCGetBlockChainInfo) { |
|||
bitcoindRPCGetBlockChainInfo.restore(); |
|||
} |
|||
}); |
|||
|
|||
it('should respond with local info if no peers', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: [] |
|||
})); |
|||
bitcoindRPCGetBlockChainInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getBlockchainInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: { |
|||
blocks: 515055, |
|||
headers: 515055, |
|||
} |
|||
})); |
|||
requester |
|||
.get('/v1/bitcoind/info/sync') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
|
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.not.have.property('status'); |
|||
res.body.currentBlock.should.equal(515055); |
|||
res.body.headerCount.should.equal(515055); |
|||
res.body.percent.should.equal('1.0000'); // testing precision
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should respond with local info if one peer without headers', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: [ |
|||
{ |
|||
syncedHeaders: -1, |
|||
}, |
|||
] |
|||
})); |
|||
bitcoindRPCGetBlockChainInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getBlockchainInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: { |
|||
blocks: 515055, |
|||
headers: 515055, |
|||
} |
|||
})); |
|||
requester |
|||
.get('/v1/bitcoind/info/sync') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
|
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.not.have.property('status'); |
|||
res.body.currentBlock.should.equal(515055); |
|||
res.body.headerCount.should.equal(515055); |
|||
res.body.percent.should.equal('1.0000'); // testing precision
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should respond with peer data if active peers ahead of local', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: [ |
|||
{ |
|||
syncedHeaders: -1, |
|||
}, |
|||
{ |
|||
syncedHeaders: 515055, |
|||
} |
|||
] |
|||
})); |
|||
bitcoindRPCGetBlockChainInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getBlockchainInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: { |
|||
blocks: 515035, |
|||
headers: 515045, |
|||
} |
|||
})); |
|||
requester |
|||
.get('/v1/bitcoind/info/sync') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
|
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.not.have.property('status'); |
|||
res.body.currentBlock.should.equal(515035); |
|||
res.body.headerCount.should.equal(515055); |
|||
res.body.percent.should.not.equal(1.0000); // testing precision
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should respond with local data if active peers behind local', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: [ |
|||
{ |
|||
syncedHeaders: -1, |
|||
}, |
|||
{ |
|||
syncedHeaders: 515035, |
|||
} |
|||
] |
|||
})); |
|||
bitcoindRPCGetBlockChainInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getBlockchainInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: { |
|||
blocks: 515035, |
|||
headers: 515055, |
|||
} |
|||
})); |
|||
requester |
|||
.get('/v1/bitcoind/info/sync') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
|
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.not.have.property('status'); |
|||
res.body.currentBlock.should.equal(515035); |
|||
res.body.headerCount.should.equal(515055); |
|||
res.body.percent.should.not.equal(1.0000); // testing precision
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 401 without a valid token', done => { |
|||
requester |
|||
.get('/v1/bitcoind/info/sync') |
|||
.set('authorization', 'JWT invalid') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(401); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 500 on getPeerInfo error', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo').callsFake(callback => callback('error', {})); |
|||
requester |
|||
.get('/v1/bitcoind/info/sync') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(500); |
|||
res.body.should.equal('Unable to obtain peer info'); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 500 on getBlockchainInfo error', done => { |
|||
bitcoindRPCGetPeerInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getPeerInfo') |
|||
.callsFake(callback => callback(undefined, { |
|||
result: [ |
|||
{ |
|||
syncedHeaders: 515055, |
|||
} |
|||
] |
|||
})); |
|||
bitcoindRPCGetBlockChainInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getBlockchainInfo') |
|||
.callsFake(callback => callback('error', {})); |
|||
requester |
|||
.get('/v1/bitcoind/info/sync') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(500); |
|||
res.body.should.equal('Unable to obtain blockchain info'); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('/version GET', function() { |
|||
let bitcoindRPCGetNetworkInfo; |
|||
|
|||
afterEach(() => { |
|||
bitcoindRPCGetNetworkInfo.restore(); |
|||
}); |
|||
|
|||
it('should respond with a valid version', done => { |
|||
bitcoindRPCGetNetworkInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getNetworkInfo').callsFake(callback => callback(undefined, { |
|||
result: |
|||
{ |
|||
subversion: '/Satoshi:0.17.0/' |
|||
} |
|||
})); |
|||
requester |
|||
.get('/v1/bitcoind/info/version') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('version'); |
|||
res.body.version.should.equal('0.17.0'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 401 without a valid token', done => { |
|||
requester |
|||
.get('/v1/bitcoind/info/version') |
|||
.set('authorization', 'JWT invalid') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(401); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 500 on error', done => { |
|||
bitcoindRPCGetNetworkInfo = sinon.stub(require('bitcoind-rpc').prototype, 'getNetworkInfo').callsFake(callback => callback('error', {})); |
|||
requester |
|||
.get('/v1/bitcoind/info/version') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(500); |
|||
res.body.should.equal('Unable to obtain network info'); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,226 @@ |
|||
/* eslint-disable max-len,id-length */ |
|||
/* globals requester, reset */ |
|||
const sinon = require('sinon'); |
|||
const LndError = require('../../../../models/errors.js').LndError; |
|||
|
|||
const bitcoindMocks = require('../../../mocks/bitcoind.js'); |
|||
const lndMocks = require('../../../mocks/lnd.js'); |
|||
|
|||
describe('v1/lnd/channel endpoints', () => { |
|||
let token; |
|||
|
|||
before(async() => { |
|||
reset(); |
|||
|
|||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A'; |
|||
}); |
|||
|
|||
describe('/estimateFee GET', function() { |
|||
|
|||
let bitcoindMempoolInfo; |
|||
let lndEstimateFee; |
|||
let lndGenerateAddress; |
|||
let lndUnspentUtxos; |
|||
let lndWalletBalance; |
|||
|
|||
afterEach(() => { |
|||
bitcoindMempoolInfo.restore(); |
|||
|
|||
lndEstimateFee.restore(); |
|||
lndGenerateAddress.restore(); |
|||
|
|||
if (lndUnspentUtxos) { |
|||
lndUnspentUtxos.restore(); |
|||
} |
|||
|
|||
if (lndWalletBalance) { |
|||
lndWalletBalance.restore(); |
|||
} |
|||
}); |
|||
|
|||
it('should return a fee estimate', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const estimateFee = lndMocks.getEstimateFee(); |
|||
const generateAddress = lndMocks.generateAddress(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.resolves(estimateFee); |
|||
lndGenerateAddress = sinon.stub(require('../../../../services/lnd.js'), 'generateAddress') |
|||
.resolves(generateAddress); |
|||
|
|||
requester |
|||
.get('/v1/lnd/channel/estimateFee?amt=100000&confTarget=1') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.not.have.property('fast'); |
|||
res.body.should.not.have.property('normal'); |
|||
res.body.should.not.have.property('slow'); |
|||
res.body.should.not.have.property('cheapest'); |
|||
res.body.should.have.property('feeSat'); |
|||
res.body.should.have.property('feerateSatPerByte'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return a fee estimate, group2', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const estimateFee = lndMocks.getEstimateFee(); |
|||
const generateAddress = lndMocks.generateAddress(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.resolves(estimateFee); |
|||
lndGenerateAddress = sinon.stub(require('../../../../services/lnd.js'), 'generateAddress') |
|||
.resolves(generateAddress); |
|||
|
|||
requester |
|||
.get('/v1/lnd/channel/estimateFee?amt=100000&confTarget=0') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('fast'); |
|||
res.body.fast.should.have.property('feeSat'); |
|||
res.body.fast.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('normal'); |
|||
res.body.normal.should.have.property('feeSat'); |
|||
res.body.normal.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('slow'); |
|||
res.body.slow.should.have.property('feeSat'); |
|||
res.body.slow.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('cheapest'); |
|||
res.body.cheapest.should.have.property('feeSat'); |
|||
res.body.cheapest.should.have.property('feerateSatPerByte'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return insufficient funds', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const generateAddress = lndMocks.generateAddress(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.throws(new LndError('Unable to estimate fee request', {details: 'insufficient funds available to construct transaction'})); |
|||
lndGenerateAddress = sinon.stub(require('../../../../services/lnd.js'), 'generateAddress') |
|||
.resolves(generateAddress); |
|||
|
|||
requester |
|||
.get('/v1/lnd/channel/estimateFee?amt=100000&confTarget=1') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('code'); |
|||
res.body.code.should.equal('INSUFFICIENT_FUNDS'); |
|||
res.body.should.have.property('text'); |
|||
res.body.text.should.equal('Lower amount or increase confirmation target.'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return output is dust', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const generateAddress = lndMocks.generateAddress(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.throws(new LndError('Unable to estimate fee request', {details: 'transaction output is dust'})); |
|||
lndGenerateAddress = sinon.stub(require('../../../../services/lnd.js'), 'generateAddress') |
|||
.resolves(generateAddress); |
|||
|
|||
requester |
|||
.get('/v1/lnd/channel/estimateFee?amt=100000&confTarget=1') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('/channel/policy GET', function() { |
|||
let lndGetFeeReport; |
|||
|
|||
afterEach(() => { |
|||
lndGetFeeReport.restore(); |
|||
}); |
|||
|
|||
it('should return all channel policies', done => { |
|||
|
|||
lndGetFeeReport = sinon.stub(require('../../../../services/lnd.js'), 'getFeeReport') |
|||
.resolves(lndMocks.getFeeReport()); |
|||
|
|||
requester |
|||
.get('/v1/lnd/channel/policy') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.be.an('array'); |
|||
|
|||
let channelPolicy = res.body[0]; |
|||
channelPolicy.should.have.property('channelPoint'); |
|||
channelPolicy.channelPoint.should.equal('231e0634b9d283200c1f59f5f4be1ba04464130c788ab97ba6ec2f7270e50167:0'); |
|||
channelPolicy.should.have.property('baseFeeMsat'); |
|||
channelPolicy.baseFeeMsat.should.equal('1000'); |
|||
channelPolicy.should.have.property('feePerMil'); |
|||
channelPolicy.feePerMil.should.equal('1'); |
|||
channelPolicy.should.have.property('feeRate'); |
|||
channelPolicy.feeRate.should.equal(0.000001); |
|||
|
|||
channelPolicy = res.body[1]; |
|||
channelPolicy.should.have.property('channelPoint'); |
|||
channelPolicy.channelPoint.should.equal('d93d83c28a719e1a8689948a87a7025497643757d8cd23746e7af4d2710da09d:1'); |
|||
channelPolicy.should.have.property('baseFeeMsat'); |
|||
channelPolicy.baseFeeMsat.should.equal('2000'); |
|||
channelPolicy.should.have.property('feePerMil'); |
|||
channelPolicy.feePerMil.should.equal('2'); |
|||
channelPolicy.should.have.property('feeRate'); |
|||
channelPolicy.feeRate.should.equal(0.000002); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
}); |
@ -0,0 +1,94 @@ |
|||
/* eslint-disable max-len,id-length */ |
|||
/* globals requester, reset */ |
|||
const sinon = require('sinon'); |
|||
const LndError = require('../../../../models/errors.js').LndError; |
|||
const lndMocks = require('../../../mocks/lnd.js'); |
|||
|
|||
describe('v1/lnd/lightning endpoints', () => { |
|||
let token; |
|||
|
|||
before(async() => { |
|||
reset(); |
|||
|
|||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A'; |
|||
}); |
|||
|
|||
describe('/forwardingEvents GET', function() { |
|||
let lndForwardingHistory; |
|||
|
|||
afterEach(() => { |
|||
lndForwardingHistory.restore(); |
|||
}); |
|||
|
|||
it('should return forwarding events', done => { |
|||
|
|||
lndForwardingHistory = sinon.stub(require('../../../../services/lnd.js'), 'getForwardingEvents') |
|||
.resolves(lndMocks.getForwardingEvents()); |
|||
|
|||
requester |
|||
.get('/v1/lnd/lightning/forwardingEvents?startTime=1548178729853&endTime=1548178729853&indexOffset=1548178729853') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('forwardingEvents'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 400 with invalid query parameters', done => { |
|||
|
|||
lndForwardingHistory = sinon.stub(require('../../../../services/lnd.js'), 'getForwardingEvents') |
|||
.resolves(lndMocks.getForwardingEvents()); |
|||
|
|||
requester |
|||
.get('/v1/lnd/lightning/forwardingEvents?startTime=beginingOfUniverse') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
|
|||
res.should.have.status(400); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 401 without a valid token', done => { |
|||
requester |
|||
.get('/v1/lnd/lightning/forwardingEvents') |
|||
.set('authorization', 'JWT invalid') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(401); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 500 on lnd error', done => { |
|||
|
|||
lndForwardingHistory = sinon.stub(require('../../../../services/lnd.js'), 'getForwardingEvents') |
|||
.throws(new LndError('error getting forwarding events')); |
|||
|
|||
requester |
|||
.get('/v1/lnd/lightning/forwardingEvents?startTime=1548178729853&endTime=1548178729853&indexOffset=1548178729853') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(500); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
}); |
@ -0,0 +1,400 @@ |
|||
/* eslint-disable max-len,id-length */ |
|||
/* globals requester, reset */ |
|||
const sinon = require('sinon'); |
|||
const LndError = require('../../../../models/errors.js').LndError; |
|||
|
|||
const bitcoindMocks = require('../../../mocks/bitcoind.js'); |
|||
const lndMocks = require('../../../mocks/lnd.js'); |
|||
|
|||
describe('v1/lnd/transaction endpoints', () => { |
|||
let token; |
|||
|
|||
before(async() => { |
|||
reset(); |
|||
|
|||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A'; |
|||
}); |
|||
|
|||
describe('/ GET', function() { |
|||
|
|||
let lndListChainTxns; |
|||
let lndOpenChannels; |
|||
let lndClosedChannels; |
|||
let lndPendingChannels; |
|||
|
|||
afterEach(() => { |
|||
lndListChainTxns.restore(); |
|||
lndOpenChannels.restore(); |
|||
lndClosedChannels.restore(); |
|||
lndPendingChannels.restore(); |
|||
}); |
|||
|
|||
it('should return one of each transaction type', done => { |
|||
|
|||
const onChainRecieved = lndMocks.getOnChainTransaction(); |
|||
const onChainSent = lndMocks.getOnChainTransaction(); |
|||
onChainSent.amount = '-1000000'; |
|||
const onChainChannelClosed = lndMocks.getOnChainTransaction(); |
|||
const onChainChannelOpen = lndMocks.getOnChainTransaction(); |
|||
const onChainChannelPreviouslyOpen = lndMocks.getOnChainTransaction(); |
|||
const onChainPendingOpen = lndMocks.getOnChainTransaction('c0b7045595f4f5c024af22312055497e99ed8b7b62b0c7e181d16382a07ae58b'); |
|||
const onChainPendingClose = lndMocks.getOnChainTransaction('653c87589da62b5fef18538a62ecce154f94236f158d1148efab98136756ed36'); |
|||
|
|||
const openChannels = [lndMocks.getChannelOpen(onChainChannelOpen.txHash)]; |
|||
const closedChannel = [lndMocks.getChannelClosed(undefined, onChainChannelClosed.txHash), |
|||
lndMocks.getChannelClosed(onChainChannelPreviouslyOpen.txHash, undefined)]; |
|||
const pendingChannels = lndMocks.getPendingChannels(); |
|||
|
|||
lndListChainTxns = sinon.stub(require('../../../../services/lnd.js'), 'getOnChainTransactions') |
|||
.resolves([onChainChannelPreviouslyOpen, onChainPendingClose, onChainPendingOpen, onChainRecieved, onChainSent, |
|||
onChainChannelClosed, onChainChannelOpen]); |
|||
lndOpenChannels = sinon.stub(require('../../../../services/lnd.js'), 'getOpenChannels') |
|||
.resolves(openChannels); |
|||
lndClosedChannels = sinon.stub(require('../../../../services/lnd.js'), 'getClosedChannels') |
|||
.resolves(closedChannel); |
|||
lndPendingChannels = sinon.stub(require('../../../../services/lnd.js'), 'getPendingChannels') |
|||
.resolves(pendingChannels); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
|
|||
res.body[0].type.should.equal('CHANNEL_OPEN'); |
|||
res.body[1].type.should.equal('CHANNEL_CLOSE'); |
|||
res.body[2].type.should.equal('ON_CHAIN_TRANSACTION_SENT'); |
|||
res.body[3].type.should.equal('ON_CHAIN_TRANSACTION_RECEIVED'); |
|||
res.body[4].type.should.equal('PENDING_OPEN'); |
|||
res.body[5].type.should.equal('PENDING_CLOSE'); |
|||
res.body[6].type.should.equal('CHANNEL_OPEN'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('/estimateFee GET', function() { |
|||
let bitcoindMempoolInfo; |
|||
let lndEstimateFee; |
|||
let lndUnspentUtxos; |
|||
let lndWalletBalance; |
|||
|
|||
afterEach(() => { |
|||
bitcoindMempoolInfo.restore(); |
|||
lndEstimateFee.restore(); |
|||
|
|||
if (lndUnspentUtxos) { |
|||
lndUnspentUtxos.restore(); |
|||
} |
|||
|
|||
if (lndWalletBalance) { |
|||
lndWalletBalance.restore(); |
|||
} |
|||
}); |
|||
|
|||
it('should return a fee estimate', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const estimateFee = lndMocks.getEstimateFee(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.resolves(estimateFee); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.not.have.property('fast'); |
|||
res.body.should.not.have.property('normal'); |
|||
res.body.should.not.have.property('slow'); |
|||
res.body.should.not.have.property('cheapest'); |
|||
res.body.should.have.property('feeSat'); |
|||
res.body.should.have.property('feerateSatPerByte'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return a fee estimate, group', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const estimateFee = lndMocks.getEstimateFee(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.resolves(estimateFee); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=0&sweep=false') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('fast'); |
|||
res.body.fast.should.have.property('feeSat'); |
|||
res.body.fast.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('normal'); |
|||
res.body.normal.should.have.property('feeSat'); |
|||
res.body.normal.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('slow'); |
|||
res.body.slow.should.have.property('feeSat'); |
|||
res.body.slow.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('cheapest'); |
|||
res.body.cheapest.should.have.property('feeSat'); |
|||
res.body.cheapest.should.have.property('feerateSatPerByte'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return insufficient funds', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.throws(new LndError('Unable to estimate fee request', {details: 'insufficient funds available to construct transaction'})); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('code'); |
|||
res.body.code.should.equal('INSUFFICIENT_FUNDS'); |
|||
res.body.should.have.property('text'); |
|||
res.body.text.should.equal('Lower amount or increase confirmation target.'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return output is dust', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.throws(new LndError('Unable to estimate fee request', {details: 'transaction output is dust'})); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('code'); |
|||
res.body.code.should.equal('OUTPUT_IS_DUST'); |
|||
res.body.should.have.property('text'); |
|||
res.body.text.should.equal('Transaction output is dust.'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return invalid address', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.throws(new LndError('Unable to estimate fee request', {details: 'checksum mismatch'})); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('code'); |
|||
res.body.code.should.equal('INVALID_ADDRESS'); |
|||
res.body.should.have.property('text'); |
|||
res.body.text.should.equal('Please validate the Bitcoin address is correct.'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return a sweep estimate, group', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const estimateFee = lndMocks.getEstimateFee(); |
|||
const walletBalance = lndMocks.getWalletBalance(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.resolves(estimateFee); |
|||
|
|||
lndWalletBalance = sinon.stub(require('../../../../services/lnd.js'), 'getWalletBalance') |
|||
.resolves(walletBalance); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=0&sweep=true') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('fast'); |
|||
res.body.fast.should.have.property('feeSat'); |
|||
res.body.fast.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('normal'); |
|||
res.body.normal.should.have.property('feeSat'); |
|||
res.body.normal.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('slow'); |
|||
res.body.slow.should.have.property('feeSat'); |
|||
res.body.slow.should.have.property('feerateSatPerByte'); |
|||
res.body.should.have.property('cheapest'); |
|||
res.body.cheapest.should.have.property('feeSat'); |
|||
res.body.cheapest.should.have.property('feerateSatPerByte'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
|
|||
it('should return a sweep estimate', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const estimateFee = lndMocks.getEstimateFee(); |
|||
const walletBalance = lndMocks.getWalletBalance(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.resolves(estimateFee); |
|||
|
|||
lndWalletBalance = sinon.stub(require('../../../../services/lnd.js'), 'getWalletBalance') |
|||
.resolves(walletBalance); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=true') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.not.have.property('fast'); |
|||
res.body.should.not.have.property('normal'); |
|||
res.body.should.not.have.property('slow'); |
|||
res.body.should.not.have.property('cheapest'); |
|||
res.body.should.have.property('feeSat'); |
|||
res.body.should.have.property('feerateSatPerByte'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return insufficient funds for sweep estimate', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const walletBalance = lndMocks.getWalletBalance(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.throws(new LndError('Unable to estimate fee request', {details: 'insufficient funds available to construct transaction'})); |
|||
|
|||
lndWalletBalance = sinon.stub(require('../../../../services/lnd.js'), 'getWalletBalance') |
|||
.resolves(walletBalance); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=true') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('code'); |
|||
res.body.code.should.equal('INSUFFICIENT_FUNDS'); |
|||
res.body.should.have.property('text'); |
|||
res.body.text.should.equal('Lower amount or increase confirmation target.'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return a fee rate too low error', done => { |
|||
|
|||
const mempoolInfo = bitcoindMocks.getMempoolInfo(); |
|||
mempoolInfo.result.mempoolminfee = 0.01; |
|||
|
|||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo') |
|||
.resolves(mempoolInfo); |
|||
|
|||
const estimateFee = lndMocks.getEstimateFee(); |
|||
|
|||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee') |
|||
.resolves(estimateFee); |
|||
|
|||
requester |
|||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false') |
|||
.set('authorization', `JWT ${token}`) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.not.have.property('fast'); |
|||
res.body.should.not.have.property('normal'); |
|||
res.body.should.not.have.property('slow'); |
|||
res.body.should.not.have.property('cheapest'); |
|||
res.body.should.have.property('code'); |
|||
res.body.code.should.equal('FEE_RATE_TOO_LOW'); |
|||
res.body.should.have.property('text'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
}); |
@ -0,0 +1,112 @@ |
|||
/* eslint-disable max-len,id-length */ |
|||
/* globals requester, reset */ |
|||
const sinon = require('sinon'); |
|||
|
|||
const lndErrorMocks = require('../../../mocks/LndError.js'); |
|||
|
|||
describe('v1/lnd/wallet endpoints', () => { |
|||
let token; |
|||
|
|||
before(async() => { |
|||
reset(); |
|||
|
|||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A'; |
|||
}); |
|||
|
|||
describe('/changePassword GET', function() { |
|||
let lndChangePassword; |
|||
|
|||
afterEach(() => { |
|||
lndChangePassword.restore(); |
|||
}); |
|||
|
|||
it('should return 200 on success', done => { |
|||
|
|||
lndChangePassword = sinon.stub(require('../../../../services/lnd.js'), 'changePassword') |
|||
.resolves({}); |
|||
|
|||
requester |
|||
.post('/v1/lnd/wallet/changePassword') |
|||
.set('authorization', `JWT ${token}`) |
|||
.send({currentPassword: 'currentPassword', newPassword: 'newPassword1'}) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 400 with invalid currentPassword', done => { |
|||
|
|||
requester |
|||
.post('/v1/lnd/wallet/changePassword') |
|||
.set('authorization', `JWT ${token}`) |
|||
.send({currentPassword: undefined, newPassword: 'newPassword1'}) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(400); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 401 without a valid token', done => { |
|||
requester |
|||
.post('/v1/lnd/wallet/changePassword') |
|||
.set('authorization', 'JWT invalid') |
|||
.send({currentPassword: 'currentPassword', newPassword: 'newPassword1'}) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(401); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 403 when lnd says current password is wrong', done => { |
|||
|
|||
lndChangePassword = sinon.stub(require('../../../../services/lnd.js'), 'changePassword') |
|||
.throws(lndErrorMocks.invalidPasswordError()); |
|||
|
|||
requester |
|||
.post('/v1/lnd/wallet/changePassword') |
|||
.set('authorization', `JWT ${token}`) |
|||
.send({currentPassword: 'currentPassword', newPassword: 'newPassword1'}) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(403); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should 502 then lnd is restarting', done => { |
|||
|
|||
lndChangePassword = sinon.stub(require('../../../../services/lnd.js'), 'changePassword') |
|||
.throws(lndErrorMocks.connectionFailedError()); |
|||
|
|||
requester |
|||
.post('/v1/lnd/wallet/changePassword') |
|||
.set('authorization', `JWT ${token}`) |
|||
.send({currentPassword: 'currentPassword', newPassword: 'newPassword1'}) |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(502); |
|||
res.should.be.json; |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
}); |
Binary file not shown.
@ -0,0 +1,13 @@ |
|||
-----BEGIN CERTIFICATE----- |
|||
MIIB6zCCAZGgAwIBAgIRALnfzV970zEhf+9IFvkjeaswCgYIKoZIzj0EAwIwODEf |
|||
MB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMMGJlNWJh |
|||
MWJkMDM3MB4XDTE4MDkwMzIxNDYzNVoXDTE5MTAyOTIxNDYzNVowODEfMB0GA1UE |
|||
ChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMMGJlNWJhMWJkMDM3 |
|||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjeinHi2dnFK7S/jgzb0xLtYHnQtB |
|||
5A5v2446ZCOK7wgCXCv3lohLlqfrk1kmhCBOKKFWfUp4cHT74U0GWdq48qN8MHow |
|||
DgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wVwYDVR0RBFAwToIMMGJl |
|||
NWJhMWJkMDM3gglsb2NhbGhvc3SCA2xuZIIEdW5peIIKdW5peHBhY2tldIcEfwAA |
|||
AYcQAAAAAAAAAAAAAAAAAAAAAYcErBUABTAKBggqhkjOPQQDAgNIADBFAiBe56v6 |
|||
p+bIyx5u01FApm17p5E5p5/ZD4OeW13RqXD2iQIhAJxNXD2vcgMbN8+pAsblqlTi |
|||
0UwIDwe5Cvg3bibTv6to |
|||
-----END CERTIFICATE----- |
@ -0,0 +1,22 @@ |
|||
const chai = require('chai'); |
|||
const chaiHttp = require('chai-http'); |
|||
const server = require('../app.js'); |
|||
|
|||
chai.use(chaiHttp); |
|||
chai.should(); |
|||
|
|||
global.expect = chai.expect; |
|||
global.assert = chai.assert; |
|||
|
|||
before(() => { |
|||
global.requester = chai.request(server).keepOpen(); |
|||
}); |
|||
|
|||
global.reset = () => { |
|||
global.Lightning.reset(); |
|||
global.WalletUnlocker.reset(); |
|||
}; |
|||
|
|||
after(() => { |
|||
global.requester.close(); |
|||
}); |
@ -0,0 +1,24 @@ |
|||
const LndError = require('../../models/errors.js').LndError; |
|||
|
|||
function connectionFailedError() { |
|||
return new LndError('Unable to change password', { |
|||
Error: 2, |
|||
UNKNOWN: 'Connect Failed', |
|||
code: 14, |
|||
details: 'Connect Failed' |
|||
}); |
|||
} |
|||
|
|||
function invalidPasswordError() { |
|||
return new LndError('Unable to change password', { |
|||
Error: 2, |
|||
UNKNOWN: 'invalid passphrase for master public key', |
|||
code: 2, |
|||
details: 'invalid passphrase for master public key' |
|||
}); |
|||
} |
|||
|
|||
module.exports = { |
|||
connectionFailedError, |
|||
invalidPasswordError, |
|||
}; |
@ -0,0 +1,334 @@ |
|||
/* eslint-disable indent, id-length, camelcase */ |
|||
function getMempoolInfo() { |
|||
return { |
|||
result: { |
|||
size: 4524, |
|||
bytes: 2071293, |
|||
usage: 6141256, |
|||
maxmempool: 20000000, |
|||
mempoolminfee: 0.00000001, |
|||
minrelaytxfee: 0.00000001, |
|||
} |
|||
}; |
|||
} |
|||
|
|||
function getNetworkInfoWithTor() { |
|||
return { |
|||
result: { |
|||
version: 170100, |
|||
subversion: '/Satoshi:0.17.1/', |
|||
protocolversion: 70015, |
|||
localservices: '000000000000040d', |
|||
localrelay: true, |
|||
timeoffset: -1, |
|||
networkactive: true, |
|||
connections: 10, |
|||
networks: [ |
|||
{ |
|||
name: 'ipv4', |
|||
limited: false, |
|||
reachable: true, |
|||
proxy: '127.0.0.1:9050', |
|||
proxy_randomize_credentials: true, |
|||
}, |
|||
{ |
|||
name: 'ipv6', |
|||
limited: false, |
|||
reachable: true, |
|||
proxy: '127.0.0.1:9050', |
|||
proxy_randomize_credentials: true, |
|||
}, |
|||
{ |
|||
name: 'onion', |
|||
limited: false, |
|||
reachable: true, |
|||
proxy: '127.0.0.1:9050', |
|||
proxy_randomize_credentials: true, |
|||
} |
|||
], |
|||
relayfee: 0.00001000, |
|||
incrementalfee: 0.00001000, |
|||
localaddresses: [ |
|||
{ |
|||
address: 'zfd4bzpkmr3zqxs3.onion', |
|||
port: 8333, |
|||
score: 14 |
|||
} |
|||
], |
|||
warnings: '', |
|||
} |
|||
}; |
|||
} |
|||
|
|||
function getNetworkInfoWithoutTor() { |
|||
return { |
|||
result: { |
|||
version: 170100, |
|||
subversion: '/Satoshi:0.17.1/', |
|||
protocolversion: 70015, |
|||
localservices: '000000000000040d', |
|||
localrelay: true, |
|||
timeoffset: -1, |
|||
networkactive: true, |
|||
connections: 10, |
|||
networks: [ |
|||
{ |
|||
name: 'ipv4', |
|||
limited: false, |
|||
reachable: true, |
|||
proxy: '127.0.0.1:9050', |
|||
proxy_randomize_credentials: true, |
|||
}, |
|||
{ |
|||
name: 'ipv6', |
|||
limited: false, |
|||
reachable: true, |
|||
proxy: '127.0.0.1:9050', |
|||
proxy_randomize_credentials: true, |
|||
}, |
|||
{ |
|||
name: 'onion', |
|||
limited: true, |
|||
reachable: false, |
|||
proxy: '', |
|||
proxy_randomize_credentials: false, |
|||
} |
|||
], |
|||
relayfee: 0.00001000, |
|||
incrementalfee: 0.00001000, |
|||
localaddresses: [], |
|||
warnings: '', |
|||
} |
|||
}; |
|||
} |
|||
|
|||
function getPeerInfo() { |
|||
return { |
|||
result: |
|||
[{ |
|||
id: 0, |
|||
addr: '18.212.212.24:18333', |
|||
addrlocal: '10.11.12.13:10249', |
|||
addrbind: '10.12.4.104:45686', |
|||
services: '000000000000000d', |
|||
relaytxes: true, |
|||
lastsend: 1540495700, |
|||
lastrecv: 1540495674, |
|||
bytessent: 16610, |
|||
bytesrecv: 65062, |
|||
conntime: 1540491274, |
|||
timeoffset: 0, |
|||
pingtime: 0.066499, |
|||
minping: 0.065828, |
|||
version: 70015, |
|||
subver: '/Satoshi:0.13.2/', |
|||
inbound: false, |
|||
addnode: false, |
|||
startingheight: 1440561, |
|||
banscore: 0, |
|||
syncedHeaders: 1440563, |
|||
syncedBlocks: 1440563, |
|||
inflight: [], |
|||
whitelisted: false, |
|||
}, |
|||
{ |
|||
id: 1, |
|||
addr: '122.128.107.148:18333', |
|||
addrlocal: '10.11.12.13:47083', |
|||
addrbind: '10.12.4.104:41318', |
|||
services: '000000000000000d', |
|||
relaytxes: true, |
|||
lastsend: 1540495700, |
|||
lastrecv: 1540495685, |
|||
bytessent: 14855, |
|||
bytesrecv: 46473, |
|||
conntime: 1540491281, |
|||
timeoffset: 0, |
|||
pingtime: 0.161769, |
|||
minping: 0.160893, |
|||
version: 70015, |
|||
subver: '/Satoshi:0.15.1/', |
|||
inbound: false, |
|||
addnode: false, |
|||
startingheight: 1440561, |
|||
banscore: 0, |
|||
syncedHeaders: 1440567, |
|||
syncedBlocks: 1440567, |
|||
inflight: [], |
|||
whitelisted: false, |
|||
}, |
|||
{ |
|||
id: 4, |
|||
addr: '94.130.201.174:18333', |
|||
addrlocal: '10.11.12.13:51897', |
|||
addrbind: '10.12.4.104:60258', |
|||
services: '000000000000040d', |
|||
relaytxes: true, |
|||
lastsend: 1540495700, |
|||
lastrecv: 1540495694, |
|||
bytessent: 15972, |
|||
bytesrecv: 48829, |
|||
conntime: 1540491320, |
|||
timeoffset: 0, |
|||
pingtime: 0.189388, |
|||
minping: 0.188924, |
|||
version: 70015, |
|||
subver: '/Satoshi:0.17.0/', |
|||
inbound: false, |
|||
addnode: false, |
|||
startingheight: 1440561, |
|||
banscore: 0, |
|||
syncedHeaders: 1440567, |
|||
syncedBlocks: 1440567, |
|||
inflight: [], |
|||
whitelisted: false, |
|||
}, |
|||
{ |
|||
id: 5, |
|||
addr: '5.189.173.60:18333', |
|||
addrlocal: '10.11.12.13:27348', |
|||
addrbind: '10.12.4.104:41220', |
|||
services: '000000000000040d', |
|||
relaytxes: true, |
|||
lastsend: 1540495700, |
|||
lastrecv: 1540495693, |
|||
bytessent: 14511, |
|||
bytesrecv: 48206, |
|||
conntime: 1540491320, |
|||
timeoffset: -1, |
|||
pingtime: 0.159785, |
|||
minping: 0.15948, |
|||
version: 70015, |
|||
subver: '/Satoshi:0.17.99/', |
|||
inbound: false, |
|||
addnode: false, |
|||
startingheight: 1440561, |
|||
banscore: 0, |
|||
syncedHeaders: 1440567, |
|||
syncedBlocks: 1440567, |
|||
inflight: [], |
|||
whitelisted: false, |
|||
}, |
|||
{ |
|||
id: 6, |
|||
addr: '142.93.121.198:18333', |
|||
addrlocal: '10.11.12.13:35127', |
|||
addrbind: '10.12.4.104:33322', |
|||
services: '000000000000040d', |
|||
relaytxes: true, |
|||
lastsend: 1540495699, |
|||
lastrecv: 1540495699, |
|||
bytessent: 15185, |
|||
bytesrecv: 52568, |
|||
conntime: 1540491332, |
|||
timeoffset: 0, |
|||
pingtime: 0.070795, |
|||
minping: 0.070323, |
|||
version: 70015, |
|||
subver: '/Satoshi:0.16.2/', |
|||
inbound: false, |
|||
addnode: false, |
|||
startingheight: 1440561, |
|||
banscore: 0, |
|||
syncedHeaders: 1440567, |
|||
syncedBlocks: 1440567, |
|||
inflight: [], |
|||
whitelisted: false, |
|||
}, |
|||
{ |
|||
id: 7, |
|||
addr: '159.65.202.252:18333', |
|||
addrlocal: '10.11.12.13:21101', |
|||
addrbind: '10.12.4.104:57942', |
|||
services: '000000000000040d', |
|||
relaytxes: true, |
|||
lastsend: 1540495700, |
|||
lastrecv: 1540495692, |
|||
bytessent: 16334, |
|||
bytesrecv: 50973, |
|||
conntime: 1540491334, |
|||
timeoffset: 0, |
|||
pingtime: 0.150593, |
|||
minping: 0.150379, |
|||
version: 70015, |
|||
subver: '/Satoshi:0.16.0/', |
|||
inbound: false, |
|||
addnode: false, |
|||
startingheight: 1440561, |
|||
banscore: 0, |
|||
syncedHeaders: 1440567, |
|||
syncedBlocks: 1440567, |
|||
inflight: [], |
|||
whitelisted: false, |
|||
}, |
|||
{ |
|||
id: 8, |
|||
addr: '206.189.39.36:18333', |
|||
addrlocal: '10.11.12.13:24189', |
|||
addrbind: '10.12.4.104:39760', |
|||
services: '000000000000040d', |
|||
relaytxes: true, |
|||
lastsend: 1540495684, |
|||
lastrecv: 1540495701, |
|||
bytessent: 10896, |
|||
bytesrecv: 50807, |
|||
conntime: 1540492619, |
|||
timeoffset: 0, |
|||
pingtime: 0.17156, |
|||
minping: 0.171309, |
|||
version: 70015, |
|||
subver: '/Satoshi:0.16.2/', |
|||
inbound: false, |
|||
addnode: false, |
|||
startingheight: 1440562, |
|||
banscore: 0, |
|||
syncedHeaders: 1440567, |
|||
syncedBlocks: 1440567, |
|||
inflight: [], |
|||
whitelisted: false, |
|||
}, |
|||
{ |
|||
id: 9, |
|||
addr: '92.53.89.123:18333', |
|||
addrlocal: '10.11.12.13:25479', |
|||
addrbind: '10.12.4.104:39574', |
|||
services: '000000000000040d', |
|||
relaytxes: true, |
|||
lastsend: 1540495700, |
|||
lastrecv: 1540495678, |
|||
bytessent: 11274, |
|||
bytesrecv: 44787, |
|||
conntime: 1540492620, |
|||
timeoffset: 0, |
|||
pingtime: 0.181107, |
|||
minping: 0.180474, |
|||
version: 70015, |
|||
subver: '/Satoshi:0.17.0/', |
|||
inbound: false, |
|||
addnode: false, |
|||
startingheight: 1440562, |
|||
banscore: 0, |
|||
syncedHeaders: 1440567, |
|||
syncedBlocks: 1440567, |
|||
inflight: [], |
|||
whitelisted: false, |
|||
}], |
|||
error: null, |
|||
id: 56305 |
|||
}; |
|||
} |
|||
|
|||
function getPeerInfoEmpty() { |
|||
return { |
|||
result: |
|||
[] |
|||
}; |
|||
} |
|||
|
|||
module.exports = { |
|||
getMempoolInfo, |
|||
getNetworkInfoWithTor, |
|||
getNetworkInfoWithoutTor, |
|||
getPeerInfo, |
|||
getPeerInfoEmpty, |
|||
}; |
@ -0,0 +1,426 @@ |
|||
/* eslint-disable camelcase, id-length, max-len */ |
|||
|
|||
function randomString(length, chars) { |
|||
let result = ''; |
|||
for (let i = length; i > 0; --i) { |
|||
result += chars[Math.floor(Math.random() * chars.length)]; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
function randomTxId() { |
|||
return randomString(64, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); |
|||
} |
|||
|
|||
function generateAddress() { |
|||
return '2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg'; |
|||
} |
|||
|
|||
function getChannelOpen(channelPoint) { |
|||
|
|||
return { |
|||
active: true, |
|||
remotePubkey: '03311aebc4d9eb8a237d89ae771dec0d1b8a64aa31625c105800fdf5f934d824d2', |
|||
channelPoint: (channelPoint || randomTxId()) + ':0', |
|||
chanId: '440904162803712', |
|||
capacity: '100000', |
|||
localBalance: '40950', |
|||
remoteBalance: '50000', |
|||
commitFee: '9050', |
|||
commitWeight: '724', |
|||
feePerKw: '12500', |
|||
unsettledBalance: '0', |
|||
totalSatoshisSent: '0', |
|||
totalSatoshisReceived: '0', |
|||
numUpdates: '0', |
|||
pendingHtlcs: [], |
|||
csvDelay: 144, |
|||
private: false, |
|||
initiator: true, |
|||
chan_status_flags: 'ChanStatusDefault', |
|||
}; |
|||
} |
|||
|
|||
function getChannelClosed(channelPoint, closingTxHash) { |
|||
return { |
|||
channelPoint: (channelPoint || randomTxId()) + ':0', |
|||
chanId: '440904162803712', |
|||
chainHash: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', |
|||
closingTxHash: closingTxHash || randomTxId(), |
|||
remotePubkey: '03311aebc4d9eb8a237d89ae771dec0d1b8a64aa31625c105800fdf5f934d824d2', |
|||
capacity: '100000', |
|||
closeHeight: 1564209, |
|||
settledBalance: '99817', |
|||
timeLockedBalance: '0', |
|||
closeType: 'COOPERATIVE_CLOSE', |
|||
}; |
|||
} |
|||
|
|||
function getChannelBalance() { |
|||
return 42000; |
|||
} |
|||
|
|||
function getEstimateFee() { |
|||
return { |
|||
feeSat: '44115', |
|||
feerateSatPerByte: '83', |
|||
}; |
|||
} |
|||
|
|||
function getFeeReport() { |
|||
return { |
|||
channelFees: [ |
|||
{ |
|||
channelPoint: '231e0634b9d283200c1f59f5f4be1ba04464130c788ab97ba6ec2f7270e50167:0', |
|||
baseFeeMsat: '1000', |
|||
feePerMil: '1', |
|||
feeRate: 0.000001 |
|||
}, |
|||
{ |
|||
channelPoint: 'd93d83c28a719e1a8689948a87a7025497643757d8cd23746e7af4d2710da09d:1', |
|||
baseFeeMsat: '2000', |
|||
feePerMil: '2', |
|||
feeRate: 0.000002 |
|||
}, |
|||
], |
|||
dayFeeSum: '0', |
|||
weekFeeSum: '0', |
|||
monthFeeSum: '0', |
|||
}; |
|||
} |
|||
|
|||
function getForwardingEvents() { |
|||
return { |
|||
forwardingEvents: [ |
|||
{ |
|||
timestamp: '1548021945', |
|||
chanIdIn: '614599512239964161', |
|||
chanIdOut: '614438983628095489', |
|||
amtIn: '2', |
|||
amtOut: '1', |
|||
fee: '1' |
|||
} |
|||
], |
|||
lastOffsetIndex: 1, |
|||
}; |
|||
} |
|||
|
|||
function getInfo() { |
|||
return { |
|||
identity_pubkey: '036dfd60929cb57836a65daa763ceb36a26f4691c670fca91f9ee8b9bf2b445fb8', |
|||
alias: 'nicks-node', |
|||
num_pending_channels: 0, |
|||
num_active_channels: 0, |
|||
num_peers: 0, |
|||
block_height: 1382511, |
|||
block_hash: '0000000000000068cc4f6dccdd7efeecd718a19217025205515d3b3a898370c6', |
|||
syncedToChain: false, |
|||
testnet: true, |
|||
chains: [ |
|||
'bitcoin' |
|||
], |
|||
uris: ['036dfd60929cb57836a65daa763ceb36a26f4691c670fca91f9ee8b9bf2b445fb8:192.168.0.2:10009'], |
|||
best_header_timestamp: '1533778315', |
|||
version: '0.4.2-beta commit=33a5567a0fef801800cd56267e2b264d32c93173' |
|||
}; |
|||
} |
|||
|
|||
function getWalletBalance() { |
|||
return { |
|||
totalBalance: '140000', |
|||
confirmedBalance: '140000', |
|||
unconfirmedBalance: '140000' |
|||
}; |
|||
} |
|||
|
|||
function getManagedChannelsFile() { |
|||
return '{"6efe84b44bc9d184979f2527b73cbf0223a5549a3932e78a1460499166f2639e:0":{"name":"Test Node","purpose":"Much Lightning"}}'; |
|||
} |
|||
|
|||
function getOpenChannels() { |
|||
return [ |
|||
getChannelOpen('a6997a3b054265acb1a05e84f1b49f34e87a4758ea9b629839fe7311a0ac3c94'), |
|||
getChannelOpen('47e615ba7d35b5c7e93a62e9adb84fddc11df43dc0790b7000a0a42be243e210'), |
|||
]; |
|||
} |
|||
|
|||
function getPendingChannels() { |
|||
return { |
|||
total_limbo_balance: '0', |
|||
pendingOpenChannels: [ |
|||
{ |
|||
channel: { |
|||
remoteNodePub: '03a13a469bae4785e27fae24e7664e648cfdb976b97f95c694dea5e55e7d302846', |
|||
channelPoint: 'c0b7045595f4f5c024af22312055497e99ed8b7b62b0c7e181d16382a07ae58b:0', |
|||
capacity: '10000000', |
|||
localBalance: '9999817', |
|||
remoteBalance: '0' |
|||
}, |
|||
confirmationHeight: 0, |
|||
commitFee: '183', |
|||
commitWeight: '600', |
|||
feePerKw: '253' |
|||
}, |
|||
{ |
|||
channel: { |
|||
remoteNodePub: '03a13a469bae4785e27fae24e7664e648cfdb976b97f95c694dea5e55e7d302846', |
|||
channelPoint: 'c1b7045595f4f5c024af22287755b21f65e1ec7fbe11ee0181d16382a07ae58b:0', |
|||
capacity: '10000000', |
|||
localBalance: '9999817', |
|||
remoteBalance: '0' |
|||
}, |
|||
confirmationHeight: 0, |
|||
commitFee: '183', |
|||
commitWeight: '600', |
|||
feePerKw: '253' |
|||
} |
|||
], |
|||
pendingClosingChannels: [], |
|||
pendingForceClosingChannels: [ |
|||
{ |
|||
channel: { |
|||
remoteNodePub: '03ce542ac3320900154ea33c8dfb0e8faa5e6facd88d5de22b011d135e3f5e906f', |
|||
channelPoint: '76cf2031469c8cd16114dc3dddf72e6fa845e433553bdc11388b7e3b0871b296:0', |
|||
capacity: '100000', |
|||
localBalance: '99817', |
|||
remoteBalance: '0' |
|||
}, |
|||
closingTxid: '653c87589da62b5fef18538a62ecce154f94236f158d1148efab98136756ed36', |
|||
limboBalance: '99817', |
|||
maturityHeight: 1564543, |
|||
blocksTilMaturity: 144, |
|||
recoveredBalance: '0', |
|||
pendingHtlcs: [ |
|||
] |
|||
} |
|||
], |
|||
waitingCloseChannels: [] |
|||
}; |
|||
} |
|||
|
|||
function getOnChainTransaction(txHash) { |
|||
return { |
|||
txHash: txHash || randomTxId(), |
|||
amount: '100000', |
|||
numConfirmations: 21353, |
|||
blockHash: '000000000000030984420cbf3cbbcdfe57f9cf9afa05b3b04ef8267a53f52c43', |
|||
blockHeight: 1542864, |
|||
timeStamp: 1560382362, |
|||
totalFees: 0, |
|||
destAddresses: [ |
|||
'2N9Dj2NCZhKZs4QaCHuXk5jYev4CHhQTywW', |
|||
'2MvikCGP9D2hwz7ocRqWdJHnNFmExLn6Hw8' |
|||
] |
|||
}; |
|||
} |
|||
|
|||
function getOnChainTransactions() { |
|||
return [ |
|||
{ |
|||
active: true, |
|||
remotePubKey: '0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b', |
|||
channelPoint: '9449c2cba3cb9a94bad58eeff3287755b21f65e1ec7fbe11ee0f485a6bb4094e:0', |
|||
chanId: '1582956994904784896', |
|||
capacity: '10000000', |
|||
localBalance: '9739816', |
|||
remoteBalance: '260000', |
|||
commitFee: '184', |
|||
commitWeight: '724', |
|||
feePerKw: '253', |
|||
unsettledBalance: '0', |
|||
totalSatoshisSent: '260000', |
|||
totalSatoshisReceived: '0', |
|||
numUpdates: '10', |
|||
pendingHtlcs: [ |
|||
], |
|||
csvDelay: 1201, |
|||
private: false |
|||
}, |
|||
{ |
|||
active: true, |
|||
remotePubKey: '036b96e4713c5f84dcb8030592e1bd42a2d9a43d91fa2e535b9bfd05f2c5def9b9', |
|||
channelPoint: '2786816bc527ec570c6fd249ce85fa4e6bddc70675b6a03f1a4a5eefaaae3663:0', |
|||
chanId: '1582956994904915968', |
|||
capacity: '10000000', |
|||
localBalance: '9999817', |
|||
remoteBalance: '0', |
|||
commitFee: '183', |
|||
commitWeight: '600', |
|||
feePerKw: '253', |
|||
unsettledBalance: '0', |
|||
totalSatoshisSent: '0', |
|||
totalSatoshisReceived: '0', |
|||
numUpdates: '0', |
|||
pendingHtlcs: [ |
|||
], |
|||
csvDelay: 1201, |
|||
private: false |
|||
}, |
|||
{ |
|||
active: true, |
|||
remotePubKey: '03819ddbe246214d4c57b7ea4d28bfe5c09c03bb4308b40c26f1468532131e0cc0', |
|||
channelPoint: 'bea04831d2f479de97a08cd12af688e930eadf2e470e7e6c1719cdf4d5982114:0', |
|||
chanId: '1582956994904719360', |
|||
capacity: '10000000', |
|||
localBalance: '9999817', |
|||
remoteBalance: '0', |
|||
commitFee: '183', |
|||
commitWeight: '600', |
|||
feePerKw: '253', |
|||
unsettledBalance: '0', |
|||
totalSatoshisSent: '0', |
|||
totalSatoshisReceived: '0', |
|||
numUpdates: '0', |
|||
pendingHtlcs: [ |
|||
], |
|||
csvDelay: 1201, |
|||
private: false |
|||
}, |
|||
{ |
|||
active: true, |
|||
remotePubKey: '03adf1a17ab83438f23bc6c3b87ed8664757923988d5907c469840ddba1a7d1415', |
|||
channelPoint: 'da6d80297ec79cf115140c4272a4e07b9c275bdd0692b85b3167c58b8c556328:0', |
|||
chanId: '1582956994904850432', |
|||
capacity: '10000000', |
|||
localBalance: '9999817', |
|||
remoteBalance: '0', |
|||
commitFee: '183', |
|||
commitWeight: '600', |
|||
feePerKw: '253', |
|||
unsettledBalance: '0', |
|||
totalSatoshisSent: '0', |
|||
totalSatoshisReceived: '0', |
|||
numUpdates: '0', |
|||
pendingHtlcs: [ |
|||
], |
|||
csvDelay: 1201, |
|||
private: false |
|||
}, |
|||
{ |
|||
active: false, |
|||
remotePubKey: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463', |
|||
channelPoint: '12d3f818e82f448f780539c3b51616c23bc739f2b18bb8f6838200b111230d0e:0', |
|||
chanId: '1583392401509449728', |
|||
capacity: '3999000', |
|||
localBalance: '2000000', |
|||
remoteBalance: '1998817', |
|||
commitFee: '183', |
|||
commitWeight: '724', |
|||
feePerKw: '253', |
|||
unsettledBalance: '0', |
|||
totalSatoshisSent: '0', |
|||
totalSatoshisReceived: '0', |
|||
numUpdates: '0', |
|||
pendingHtlcs: [ |
|||
], |
|||
csvDelay: 480, |
|||
private: false |
|||
}, |
|||
{ |
|||
active: true, |
|||
remotePubKey: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463', |
|||
channelPoint: '6efe84b44bc9d184979f2527b73cbf0223a5549a3932e78a1460499166f2639e:0', |
|||
chanId: '1582997676835799040', |
|||
capacity: '15000000', |
|||
localBalance: '14259822', |
|||
remoteBalance: '739994', |
|||
commitFee: '184', |
|||
commitWeight: '724', |
|||
feePerKw: '253', |
|||
unsettledBalance: '0', |
|||
totalSatoshisSent: '1500000', |
|||
totalSatoshisReceived: '760005', |
|||
numUpdates: '16', |
|||
pendingHtlcs: [ |
|||
], |
|||
csvDelay: 1802, |
|||
private: false |
|||
}, |
|||
{ |
|||
active: true, |
|||
remotePubKey: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463', |
|||
channelPoint: 'c1b7045595f4f5c024af22287755b21f65e1ec7fbe11ee0181d16382a07ae58b:0', |
|||
chanId: '1582997676835799040', |
|||
capacity: '15000000', |
|||
localBalance: '14259822', |
|||
remoteBalance: '739994', |
|||
commitFee: '184', |
|||
commitWeight: '724', |
|||
feePerKw: '253', |
|||
unsettledBalance: '0', |
|||
totalSatoshisSent: '1500000', |
|||
totalSatoshisReceived: '760005', |
|||
numUpdates: '16', |
|||
pendingHtlcs: [ |
|||
], |
|||
csvDelay: 1802, |
|||
private: false |
|||
} |
|||
]; |
|||
} |
|||
|
|||
function getUnspectUtxos() { |
|||
return { |
|||
utxos: [ |
|||
{ |
|||
address_type: 0, |
|||
address: 'bc1qp6dxazrsju834sucudvcxy6t3tem3henkf9dfe', |
|||
amountSat: 50000, |
|||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33', |
|||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1', |
|||
confirmations: 51 |
|||
}, |
|||
{ |
|||
address_type: 0, |
|||
address: 'bc1qp6dxazrsju834sucudvcxy6t3tem3henkf9dfe', |
|||
amountSat: 30000, |
|||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33', |
|||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1', |
|||
confirmations: 3307 |
|||
}, |
|||
{ |
|||
address_type: 0, |
|||
address: 'bc1qp6dxazrsju834sucudvcxy6t3tem3henkf9dfe', |
|||
amountSat: 20000, |
|||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33', |
|||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1', |
|||
confirmations: 3996 |
|||
}, |
|||
{ |
|||
address_type: 0, |
|||
address: 'bc1qp6dxazrsju834sucudvcxy6t3tem3henkf9dfe', |
|||
amountSat: 20000, |
|||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33', |
|||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1', |
|||
confirmations: 26309 |
|||
}, |
|||
{ |
|||
address_type: 1, |
|||
address: '2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg', |
|||
amountSat: 20000, |
|||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33', |
|||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1', |
|||
confirmations: 27335 |
|||
} |
|||
] |
|||
}; |
|||
} |
|||
|
|||
module.exports = { |
|||
generateAddress, |
|||
getChannelOpen, |
|||
getChannelClosed, |
|||
getChannelBalance, |
|||
getEstimateFee, |
|||
getFeeReport, |
|||
getForwardingEvents, |
|||
getInfo, |
|||
getWalletBalance, |
|||
getOpenChannels, |
|||
getOnChainTransaction, |
|||
getOnChainTransactions, |
|||
getPendingChannels, |
|||
getManagedChannelsFile, |
|||
getUnspectUtxos, |
|||
}; |
@ -0,0 +1,410 @@ |
|||
/* globals reset, requester, expect, assert, Lightning */ |
|||
/* eslint-disable max-len */ |
|||
|
|||
const sinon = require('sinon'); |
|||
const proxyquire = require('proxyquire'); |
|||
const lndMocks = require('../mocks/lnd.js'); |
|||
|
|||
describe('lightning API', () => { |
|||
let jwt; |
|||
let bitcoindHelp; |
|||
|
|||
before(async() => { |
|||
reset(); |
|||
|
|||
bitcoindHelp = sinon.stub(require('bitcoind-rpc').prototype, 'help').callsFake(callback => callback(undefined, {})); |
|||
|
|||
// TODO: expires Dec 1st, 2019
|
|||
jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A'; |
|||
}); |
|||
|
|||
after(async() => { |
|||
bitcoindHelp.restore(); |
|||
}); |
|||
|
|||
it('should look operational', async() => { |
|||
|
|||
Lightning.prototype.GetInfo = sinon.stub().callsFake((_, callback) => callback(undefined, {})); |
|||
|
|||
const status = await requester |
|||
.get('/v1/lnd/info/status') |
|||
.set('authorization', `jwt ${jwt}`) |
|||
.then(res => { |
|||
expect(res).to.have.status(200); |
|||
expect(res).to.be.json; |
|||
|
|||
return res.body; |
|||
}); |
|||
assert.equal(status.operational, true); |
|||
assert.isTrue(bitcoindHelp.called); |
|||
assert.isTrue(Lightning.prototype.GetInfo.called); |
|||
}); |
|||
}); |
|||
|
|||
describe('lightningLogic', function() { |
|||
|
|||
describe('getChannelBalance', function() { |
|||
|
|||
it('should return channel balance', function(done) { |
|||
|
|||
const originalChannelbalance = lndMocks.getChannelBalance(); |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getChannelBalance: () => Promise.resolve(originalChannelbalance) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getChannelBalance().then(function(channelBalance) { |
|||
assert.equal(channelBalance, originalChannelbalance); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should throw an error', function(done) { |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getChannelBalance: () => Promise.reject(new Error()) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getChannelBalance().catch(function(error) { |
|||
assert.isNotNull(error); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
describe('generateAddress', function() { |
|||
|
|||
it('should return a segwit address', function(done) { |
|||
|
|||
const originalAddress = lndMocks.generateAddress(); |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
generateAddress: () => Promise.resolve(originalAddress) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.generateAddress() |
|||
.then(function(address) { |
|||
assert.equal(address, originalAddress); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should throw an error', function(done) { |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
generateAddress: () => Promise.reject(new Error()) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.generateAddress() |
|||
.catch(function(error) { |
|||
assert.isNotNull(error); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('getChannelCount', function() { |
|||
|
|||
it('should return channel count', function(done) { |
|||
|
|||
const originalChannels = lndMocks.getOpenChannels(); |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getOpenChannels: () => Promise.resolve(originalChannels) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getChannelCount() |
|||
.then(function(channelCount) { |
|||
assert.equal(channelCount.count, originalChannels.length); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
it('should throw an error', function(done) { |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getOpenChannels: () => Promise.reject(new Error()) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getChannelCount() |
|||
.catch(function(error) { |
|||
assert.isNotNull(error); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
}); |
|||
|
|||
describe('getChannels', function() { |
|||
it('should return a list of channels', function(done) { |
|||
|
|||
const originalChannelList = lndMocks.getOpenChannels(); |
|||
const originalPendingChannelList = lndMocks.getPendingChannels(); // eslint-disable-line id-length
|
|||
const originalOnChainTransactions = lndMocks.getOnChainTransactions(); // eslint-disable-line id-length
|
|||
const managedChannelsFile = lndMocks.getManagedChannelsFile(); |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getOpenChannels: () => Promise.resolve(originalChannelList), |
|||
getPendingChannels: () => Promise.resolve(originalPendingChannelList), |
|||
getOnChainTransactions: () => Promise.resolve(originalOnChainTransactions) |
|||
}, |
|||
'services/disk.js': { |
|||
readJsonFile: () => Promise.resolve(managedChannelsFile) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getChannels() |
|||
.then(function(channels) { |
|||
assert.equal(channels.count, originalChannelList.count); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
}); |
|||
|
|||
describe('getPublicUris', function() { |
|||
|
|||
it('should return a list of uris', function(done) { |
|||
|
|||
const originalGetInfo = lndMocks.getInfo(); |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getInfo: () => Promise.resolve(originalGetInfo) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getPublicUris() |
|||
.then(function(uris) { |
|||
assert.deepEqual(uris, originalGetInfo.uris); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
it('should throw an error', function(done) { |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getInfo: () => Promise.reject(new Error()) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getPublicUris() |
|||
.catch(function(error) { |
|||
assert.isNotNull(error); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
}); |
|||
|
|||
describe('getSyncStatus', function() { |
|||
|
|||
it('should return the sync status', function(done) { |
|||
const originalGetInfo = { |
|||
synchedToChain: false, |
|||
testnet: false, |
|||
bestHeaderTimestamp: 1535905615, |
|||
blockHeight: 1408630 |
|||
}; |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getInfo: () => Promise.resolve(originalGetInfo) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getSyncStatus() |
|||
.then(function(status) { |
|||
assert.property(status, 'percent'); |
|||
assert.property(status, 'knownBlockCount'); |
|||
assert.property(status, 'processedBlocks'); |
|||
assert.notEqual(status.percent, -1); |
|||
assert.notEqual(status.processedBlocks, -1); |
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
it('should return -1 if calculation is greater than 100%', function(done) { |
|||
const invaildInfoData = { |
|||
synchedToChain: false, |
|||
testnet: false, |
|||
bestHeaderTimestamp: 1845905615, |
|||
blockHeight: 1408630 |
|||
}; |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getInfo: () => Promise.resolve(invaildInfoData) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getSyncStatus() |
|||
.then(function(status) { |
|||
assert.property(status, 'percent'); |
|||
assert.property(status, 'knownBlockCount'); |
|||
assert.property(status, 'processedBlocks'); |
|||
assert.equal(status.percent, -1); |
|||
assert.equal(status.processedBlocks, -1); |
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
it('should thrown an error', function(done) { |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getInfo: () => Promise.reject(new Error()) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getSyncStatus() |
|||
.catch(function(error) { |
|||
assert.isNotNull(error); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
}); |
|||
|
|||
describe('getWalletBalance', function() { |
|||
|
|||
it('should return a wallet balance', function(done) { |
|||
|
|||
const originalWalletBalance = lndMocks.getWalletBalance(); |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getWalletBalance: () => Promise.resolve(originalWalletBalance) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getWalletBalance() |
|||
.then(function(walletBalance) { |
|||
assert.deepEqual(walletBalance, originalWalletBalance); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
it('should throw an error', function(done) { |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getWalletBalance: () => Promise.reject(new Error()) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getWalletBalance() |
|||
.catch(function(error) { |
|||
assert.isNotNull(error); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
}); |
|||
|
|||
describe('getVersion', function() { |
|||
|
|||
it('should return a version', function(done) { |
|||
|
|||
const originalGetInfo = lndMocks.getInfo(); |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getInfo: () => Promise.resolve(originalGetInfo) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getVersion() |
|||
.then(function(version) { |
|||
assert.equal(version.version, '0.4.2'); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
it('should throw an error', function(done) { |
|||
|
|||
const lndServiceStub = { |
|||
'services/lnd.js': { |
|||
getInfo: () => Promise.reject(new Error()) |
|||
} |
|||
}; |
|||
|
|||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub); |
|||
|
|||
lightningLogic.getPublicUris() |
|||
.catch(function(error) { |
|||
assert.isNotNull(error); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
}); |
|||
}); |
@ -0,0 +1,196 @@ |
|||
/* globals assert */ |
|||
/* eslint-disable max-len */ |
|||
|
|||
const proxyquire = require('proxyquire'); |
|||
const bitcoindMocks = require('../mocks/bitcoind.js'); |
|||
|
|||
describe('networkLogic', function() { |
|||
|
|||
describe('getBitcoindAddresses', function() { |
|||
|
|||
it('should return an ipv4 address', function(done) { |
|||
|
|||
const peerInfo = bitcoindMocks.getPeerInfo(); |
|||
|
|||
const ipv4 = '10.11.12.13'; |
|||
const port = '10000'; |
|||
peerInfo.result[0].addrlocal = ipv4 + ':' + port; |
|||
|
|||
const bitcoindServiceStub = { |
|||
'services/bitcoind.js': { |
|||
getPeerInfo: () => Promise.resolve(peerInfo), |
|||
getNetworkInfo: () => Promise.resolve(bitcoindMocks.getNetworkInfoWithoutTor()), |
|||
} |
|||
}; |
|||
|
|||
const networkLogic = proxyquire('logic/network.js', bitcoindServiceStub); |
|||
|
|||
networkLogic.getBitcoindAddresses().then(function(response) { |
|||
assert.equal(response.length, 1); |
|||
assert.equal(response[0], ipv4); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return an ipv4 address and onion address', function(done) { |
|||
|
|||
const peerInfo = bitcoindMocks.getPeerInfo(); |
|||
|
|||
const ipv4 = '10.11.12.13'; |
|||
const port = '10000'; |
|||
peerInfo.result[0].addrlocal = ipv4 + ':' + port; |
|||
|
|||
const bitcoindServiceStub = { |
|||
'services/bitcoind.js': { |
|||
getPeerInfo: () => Promise.resolve(peerInfo), |
|||
getNetworkInfo: () => Promise.resolve(bitcoindMocks.getNetworkInfoWithTor()), |
|||
} |
|||
}; |
|||
|
|||
const networkLogic = proxyquire('logic/network.js', bitcoindServiceStub); |
|||
|
|||
networkLogic.getBitcoindAddresses().then(function(response) { |
|||
assert.equal(response.length, 2); |
|||
assert.equal(response[0], ipv4); |
|||
assert.equal(true, response[1].includes('onion')); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return an ipv6 address', function(done) { |
|||
|
|||
const peerInfo = bitcoindMocks.getPeerInfo(); |
|||
|
|||
const ipv6 = '566f:2401:22be:9a6d:23ef:2558:5545:b3fe'; |
|||
const port = '10000'; |
|||
|
|||
for (const peer of peerInfo.result) { |
|||
peer.addrlocal = ipv6 + ':' + port; |
|||
} |
|||
|
|||
const bitcoindServiceStub = { |
|||
'services/bitcoind.js': { |
|||
getPeerInfo: () => Promise.resolve(peerInfo), |
|||
getNetworkInfo: () => Promise.resolve(bitcoindMocks.getNetworkInfoWithoutTor()), |
|||
} |
|||
}; |
|||
|
|||
const networkLogic = proxyquire('logic/network.js', bitcoindServiceStub); |
|||
|
|||
networkLogic.getBitcoindAddresses().then(function(response) { |
|||
assert.equal(response.length, 1); |
|||
assert.equal(response[0], ipv6); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should handle missing addrlocal information', function(done) { |
|||
|
|||
const peerInfo = bitcoindMocks.getPeerInfo(); |
|||
|
|||
const ipv4 = '10.11.12.13'; |
|||
delete peerInfo.result[0].addrlocal; |
|||
|
|||
const bitcoindServiceStub = { |
|||
'services/bitcoind.js': { |
|||
getPeerInfo: () => Promise.resolve(peerInfo), |
|||
getNetworkInfo: () => Promise.resolve(bitcoindMocks.getNetworkInfoWithoutTor()), |
|||
} |
|||
}; |
|||
|
|||
const networkLogic = proxyquire('logic/network.js', bitcoindServiceStub); |
|||
|
|||
networkLogic.getBitcoindAddresses().then(function(response) { |
|||
assert.equal(response.length, 1); |
|||
assert.equal(response[0], ipv4); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should handle discrepancies in ip addresses', function(done) { |
|||
|
|||
const peerInfo = bitcoindMocks.getPeerInfo(); |
|||
|
|||
const ipv4 = '10.11.12.14'; |
|||
const port = '10000'; |
|||
peerInfo.result[0].addrlocal = ipv4 + ':' + port; |
|||
|
|||
const bitcoindServiceStub = { |
|||
'services/bitcoind.js': { |
|||
getPeerInfo: () => Promise.resolve(peerInfo), |
|||
getNetworkInfo: () => Promise.resolve(bitcoindMocks.getNetworkInfoWithoutTor()), |
|||
} |
|||
}; |
|||
|
|||
const networkLogic = proxyquire('logic/network.js', bitcoindServiceStub); |
|||
|
|||
networkLogic.getBitcoindAddresses().then(function(response) { |
|||
assert.equal(response.length, 1); |
|||
assert.equal(response[0], '10.11.12.13'); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should handle calls to ipinfo for ipv4', function(done) { |
|||
|
|||
const ipv4 = '10.11.12.15'; |
|||
const peerInfo = bitcoindMocks.getPeerInfoEmpty(); |
|||
const ipInfo = { |
|||
out: ipv4 + '\n' |
|||
}; |
|||
|
|||
const serviceStubs = { |
|||
'services/bash.js': { |
|||
exec: () => Promise.resolve(ipInfo) |
|||
}, |
|||
'services/bitcoind.js': { |
|||
getPeerInfo: () => Promise.resolve(peerInfo), |
|||
getNetworkInfo: () => Promise.resolve(bitcoindMocks.getNetworkInfoWithoutTor()), |
|||
} |
|||
}; |
|||
|
|||
const networkLogic = proxyquire('logic/network.js', serviceStubs); |
|||
|
|||
networkLogic.getBitcoindAddresses().then(function(response) { |
|||
assert.equal(response.length, 1); |
|||
assert.equal(response[0], ipv4); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should handle calls to ipinfo for ipv6', function(done) { |
|||
|
|||
const ipv6 = '566f:2401:22be:9a6d:23ef:2558:5545:b3fe'; |
|||
const peerInfo = bitcoindMocks.getPeerInfoEmpty(); |
|||
const ipInfo = { |
|||
out: ipv6 + '\n' |
|||
}; |
|||
|
|||
const serviceStubs = { |
|||
'services/bash.js': { |
|||
exec: () => Promise.resolve(ipInfo) |
|||
}, |
|||
'services/bitcoind.js': { |
|||
getPeerInfo: () => Promise.resolve(peerInfo), |
|||
getNetworkInfo: () => Promise.resolve(bitcoindMocks.getNetworkInfoWithoutTor()), |
|||
} |
|||
}; |
|||
|
|||
const networkLogic = proxyquire('logic/network.js', serviceStubs); |
|||
|
|||
networkLogic.getBitcoindAddresses().then(function(response) { |
|||
assert.equal(response.length, 1); |
|||
assert.equal(response[0], ipv6); |
|||
|
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
}); |
|||
}); |
@ -0,0 +1,5 @@ |
|||
const uuidv4 = require('uuid/v4'); |
|||
|
|||
module.exports = { |
|||
create: uuidv4, |
|||
}; |
@ -0,0 +1,17 @@ |
|||
/* eslint-disable id-length */ |
|||
module.exports = { |
|||
LN_REQUIRED_CONFIRMATIONS: 3, |
|||
LND_STATUS_CODES: { |
|||
UNAVAILABLE: 14, |
|||
UNKNOWN: 2, |
|||
}, |
|||
JWT_PUBLIC_KEY: process.env.JWT_PUBLIC_KEY || 'UNKNOWN', |
|||
MANAGED_CHANNELS_FILE: '/channel-data/managedChannels.json', |
|||
REQUEST_CORRELATION_NAMESPACE_KEY: 'lnapi-request', |
|||
REQUEST_CORRELATION_ID_KEY: 'reqId', |
|||
STATUS_CODES: { |
|||
BAD_GATEWAY: 502, |
|||
FORBIDDEN: 403, |
|||
OK: 200, |
|||
}, |
|||
}; |
@ -0,0 +1,66 @@ |
|||
// Source from https://github.com/richardschneider/bitcoin-convert/
|
|||
const Big = require('big.js'); |
|||
const BTC = 1; |
|||
const SAT = 0.00000001; |
|||
|
|||
const units = { |
|||
btc: new Big(BTC), |
|||
sat: new Big(SAT), |
|||
}; |
|||
|
|||
function convert(from, fromUnit, toUnit, representation) { |
|||
const fromFactor = units[fromUnit]; |
|||
if (fromFactor === undefined) { |
|||
throw new Error(`'${fromUnit}' is not a bitcoin unit`); |
|||
} |
|||
const toFactor = units[toUnit]; |
|||
if (toFactor === undefined) { |
|||
throw new Error(`'${toUnit}' is not a bitcoin unit`); |
|||
} |
|||
|
|||
if (Number.isNaN(from)) { |
|||
if (!representation || representation === 'Number') { |
|||
return from; |
|||
} else if (representation === 'Big') { |
|||
return new Big(from); // throws BigError
|
|||
} else if (representation === 'String') { |
|||
return from.toString(); |
|||
} |
|||
throw new Error(`'${representation}' is not a valid representation`); |
|||
} |
|||
|
|||
const result = new Big(from).times(fromFactor).div(toFactor); |
|||
|
|||
if (!representation || representation === 'Number') { |
|||
return Number(result); |
|||
} else if (representation === 'Big') { |
|||
return result; |
|||
} else if (representation === 'String') { |
|||
return result.toString(); |
|||
} |
|||
|
|||
throw new Error(`'${representation}' is not a valid representation`); |
|||
} |
|||
|
|||
convert.units = function() { |
|||
return Object.keys(units); |
|||
}; |
|||
|
|||
convert.addUnit = function addUnit(unit, factor) { |
|||
const bigFactor = new Big(factor); |
|||
const existing = units[unit]; |
|||
if (existing && !existing.eq(bigFactor)) { |
|||
throw new Error(`'${unit}' already exists with a different conversion factor`); |
|||
} |
|||
units[unit] = bigFactor; |
|||
}; |
|||
|
|||
const predefinedUnits = convert.units(); |
|||
convert.removeUnit = function removeUnit(unit) { |
|||
if (predefinedUnits.indexOf(unit) >= 0) { |
|||
throw new Error(`'${unit}' is predefined and cannot be removed`); |
|||
} |
|||
delete units[unit]; |
|||
}; |
|||
|
|||
module.exports = convert; |
@ -0,0 +1,134 @@ |
|||
/* eslint-disable no-shadow, no-unused-vars, max-len, no-console, object-shorthand*/ |
|||
require('winston-daily-rotate-file'); |
|||
const constants = require('utils/const.js'); |
|||
const fs = require('fs'); |
|||
const path = require('path'); |
|||
const winston = require('winston'); |
|||
const {format} = require('winston'); |
|||
const {combine, timestamp, printf} = format; |
|||
const getNamespace = require('continuation-local-storage').getNamespace; |
|||
|
|||
const LOCAL = 'local'; |
|||
const logDir = './logs'; |
|||
const ENV = process.env.ENV; |
|||
|
|||
if (!fs.existsSync(logDir)) { |
|||
fs.mkdirSync(logDir); |
|||
} |
|||
|
|||
const appendCorrelationId = format((info, opts) => { |
|||
var apiRequest = getNamespace(constants.REQUEST_CORRELATION_NAMESPACE_KEY); |
|||
if (apiRequest) { |
|||
info.internalCorrelationId = apiRequest.get(constants.REQUEST_CORRELATION_ID_KEY); |
|||
} |
|||
|
|||
return info; |
|||
}); |
|||
|
|||
const errorFileTransport = new winston.transports.DailyRotateFile({ |
|||
filename: path.join(logDir, 'error-%DATE%.log'), |
|||
datePattern: 'YYYY-MM-DD', |
|||
level: 'error', |
|||
maxSize: '10m', |
|||
maxFiles: '7d' |
|||
}); |
|||
|
|||
const apiFileTransport = new winston.transports.DailyRotateFile({ |
|||
filename: path.join(logDir, 'api-%DATE%.log'), |
|||
datePattern: 'YYYY-MM-DD', |
|||
maxSize: '10m', |
|||
maxFiles: '7d' |
|||
}); |
|||
|
|||
const localLogFormat = printf(info => { |
|||
var data = ''; |
|||
if (info.data) { |
|||
data = JSON.stringify({data: info.data}); |
|||
} |
|||
|
|||
return `${info.timestamp} ${info.level.toUpperCase()}: ${info.internalCorrelationId} [${info._module}] ${info.message} ${data}`; |
|||
}); |
|||
|
|||
const localLoggerTransports = [ |
|||
errorFileTransport, |
|||
apiFileTransport, |
|||
]; |
|||
|
|||
if (ENV === 'dev') { |
|||
localLoggerTransports.push(new winston.transports.Console()); |
|||
} |
|||
|
|||
winston.loggers.add(LOCAL, { |
|||
level: 'info', |
|||
format: combine( |
|||
timestamp(), |
|||
appendCorrelationId(), |
|||
localLogFormat |
|||
), |
|||
transports: localLoggerTransports |
|||
}); |
|||
|
|||
const morganConfiguration = { |
|||
stream: { |
|||
write: function(message) { |
|||
info(message, 'lnapi'); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const localLogger = winston.loggers.get(LOCAL); |
|||
|
|||
function printToStandardOut(data) { |
|||
if (data) { |
|||
console.log(data); |
|||
} |
|||
} |
|||
|
|||
function error(message, _module, data) { |
|||
printToStandardOut(message); |
|||
printToStandardOut(_module); |
|||
printToStandardOut(data); |
|||
localLogger.error(message, { |
|||
_module: _module, |
|||
data: data |
|||
}); |
|||
} |
|||
|
|||
function warn(message, _module, data) { |
|||
printToStandardOut(message); |
|||
printToStandardOut(_module); |
|||
printToStandardOut(data); |
|||
localLogger.warn(message, { |
|||
_module: _module, |
|||
data: data |
|||
}); |
|||
} |
|||
|
|||
function info(message, _module, data) { |
|||
printToStandardOut(message); |
|||
printToStandardOut(_module); |
|||
printToStandardOut(data); |
|||
localLogger.info(message, { |
|||
_module: _module, |
|||
data: data |
|||
}); |
|||
} |
|||
|
|||
function debug(message, _module, data) { |
|||
printToStandardOut(message); |
|||
printToStandardOut(_module); |
|||
printToStandardOut(data); |
|||
localLogger.debug(message, { |
|||
_module: _module, |
|||
data: data |
|||
}); |
|||
} |
|||
|
|||
module.exports = { |
|||
error: error, |
|||
warn: warn, |
|||
info: info, |
|||
debug: debug, |
|||
morganConfiguration: morganConfiguration, |
|||
}; |
|||
|
@ -0,0 +1,15 @@ |
|||
// this safe handler is used to wrap our api methods
|
|||
// so that we always fallback and return an exception if there is an error
|
|||
// inside of an async function
|
|||
// Mostly copied from vault/server/utils/safeHandler.js
|
|||
function safeHandler(handler) { |
|||
return async(req, res, next) => { |
|||
try { |
|||
return await handler(req, res, next); |
|||
} catch (err) { |
|||
return next(err); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
module.exports = safeHandler; |
@ -0,0 +1,86 @@ |
|||
const validator = require('validator'); |
|||
|
|||
const ValidationError = require('models/errors.js').ValidationError; |
|||
|
|||
// Max length is listed here,
|
|||
// https://github.com/lightningnetwork/lnd/blob/fd1f6a7bc46b1e50ff3879b8bd3876d347dbb73d/channeldb/invoices.go#L84
|
|||
const MAX_MEMO_LENGTH = 1024; |
|||
const MIN_PASSWORD_LENGTH = 12; |
|||
|
|||
function isAlphanumeric(string) { |
|||
|
|||
isDefined(string); |
|||
|
|||
if (!validator.isAlphanumeric(string)) { |
|||
throw new ValidationError('Must include only alpha numeric characters.'); |
|||
} |
|||
} |
|||
|
|||
function isAlphanumericAndSpaces(string) { |
|||
|
|||
isDefined(string); |
|||
|
|||
if (!validator.matches(string, '^[a-zA-Z0-9\\s]*$')) { |
|||
throw new ValidationError('Must include only alpha numeric characters and spaces.'); |
|||
} |
|||
} |
|||
|
|||
function isBoolean(value) { |
|||
if (value !== true && value !== false) { |
|||
throw new ValidationError('Must be true or false.'); |
|||
} |
|||
} |
|||
|
|||
function isDecimal(amount) { |
|||
if (!validator.isDecimal(amount)) { |
|||
throw new ValidationError('Must be decimal.'); |
|||
} |
|||
} |
|||
|
|||
function isDefined(object) { |
|||
if (object === undefined) { |
|||
throw new ValidationError('Must define variable.'); |
|||
} |
|||
} |
|||
|
|||
function isMinPasswordLength(password) { |
|||
if (password.length < MIN_PASSWORD_LENGTH) { |
|||
throw new ValidationError('Must be ' + MIN_PASSWORD_LENGTH + ' or more characters.'); |
|||
} |
|||
} |
|||
|
|||
function isPositiveInteger(amount) { |
|||
if (!validator.isInt(amount + '', {gt: 0})) { |
|||
throw new ValidationError('Must be positive integer.'); |
|||
} |
|||
} |
|||
|
|||
function isPositiveIntegerOrZero(amount) { |
|||
if (!validator.isInt(amount + '', {gt: -1})) { |
|||
throw new ValidationError('Must be positive integer.'); |
|||
} |
|||
} |
|||
|
|||
function isString(object) { |
|||
if (typeof object !== 'string') { |
|||
throw new ValidationError('Object must be of type string.'); |
|||
} |
|||
} |
|||
|
|||
function isValidMemoLength(string) { |
|||
if (Buffer.byteLength(string, 'utf8') > MAX_MEMO_LENGTH) { |
|||
throw new ValidationError('Must be less than ' + MAX_MEMO_LENGTH + ' bytes.'); |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
isAlphanumeric, |
|||
isAlphanumericAndSpaces, |
|||
isBoolean, |
|||
isDecimal, |
|||
isMinPasswordLength, |
|||
isPositiveInteger, |
|||
isPositiveIntegerOrZero, |
|||
isString, |
|||
isValidMemoLength, |
|||
}; |
Loading…
Reference in new issue