mirror of
https://github.com/Retropex/umbrel-bitcoin.git
synced 2025-05-12 03:00:49 +02:00
Initial commit
This commit is contained in:
commit
4607e73b00
22
.dockerignore
Normal file
22
.dockerignore
Normal file
@ -0,0 +1,22 @@
|
||||
.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
|
||||
*.env
|
||||
.github/
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
coverage
|
276
.eslintrc
Normal file
276
.eslintrc
Normal file
@ -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
|
||||
}
|
||||
}
|
52
.github/workflows/on-push.yml
vendored
Normal file
52
.github/workflows/on-push.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
name: Docker build on push
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
name: Build and push middleware image
|
||||
steps:
|
||||
- name: Set env variables
|
||||
run: echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV
|
||||
|
||||
- name: Show set env variables
|
||||
run: |
|
||||
printf " BRANCH: %s\n" "$BRANCH"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
id: qemu
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Show available Docker buildx platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Run Docker buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/middleware:$BRANCH \
|
||||
--output "type=registry" ./
|
65
.github/workflows/on-tag.yml
vendored
Normal file
65
.github/workflows/on-tag.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
name: Docker build on tag
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
name: Build and push middleware image
|
||||
steps:
|
||||
- name: Setup Environment
|
||||
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
- name: Show set environment variables
|
||||
run: |
|
||||
printf " TAG: %s\n" "$TAG"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
id: qemu
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Show available Docker buildx platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Run Docker buildx against tag
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/middleware:$TAG \
|
||||
--output "type=registry" ./
|
||||
|
||||
- name: Run Docker buildx against latest
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/middleware:latest \
|
||||
--output "type=registry" ./
|
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
/.idea/
|
||||
/tls.cert
|
||||
*.log
|
||||
*.env
|
||||
logs/
|
||||
*.bak
|
||||
lb_settings.json
|
||||
.nyc_output
|
||||
coverage
|
||||
.todo
|
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@ -0,0 +1,31 @@
|
||||
# Build Stage
|
||||
FROM node:12-buster-slim AS umbrel-middleware-builder
|
||||
|
||||
# Install tools
|
||||
# RUN apt-get update \
|
||||
# && apt-get install -y build-essential \
|
||||
# && apt-get install -y python3
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy 'yarn.lock' and 'package.json'
|
||||
COPY yarn.lock package.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN yarn install --production
|
||||
|
||||
# Copy project files and folders to the current working directory (i.e. '/app')
|
||||
COPY . .
|
||||
|
||||
# Final image
|
||||
FROM node:12-buster-slim AS umbrel-middleware
|
||||
|
||||
# Copy built code from build stage to '/app' directory
|
||||
COPY --from=umbrel-middleware-builder /app /app
|
||||
|
||||
# Change directory to '/app'
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 3006
|
||||
CMD [ "yarn", "start" ]
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2018-2019 Casa, Inc. https://keys.casa/
|
||||
Copyright (c) 2020 Umbrel. https://getumbrel.com/
|
||||
|
||||
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.
|
79
README.md
Normal file
79
README.md
Normal file
@ -0,0 +1,79 @@
|
||||
[](https://github.com/getumbrel/umbrel-middleware)
|
||||
|
||||
[](https://github.com/getumbrel/umbrel-middleware/releases)
|
||||
[](https://github.com/getumbrel/umbrel-middleware/actions?query=workflow%3A"Docker+build+on+push")
|
||||
[](https://hub.docker.com/repository/registry-1.docker.io/getumbrel/middleware/tags?page=1)
|
||||
[](https://t.me/getumbrel)
|
||||
[](https://keybase.io/team/getumbrel)
|
||||
|
||||
[](https://twitter.com/getumbrel)
|
||||
[](https://reddit.com/r/getumbrel)
|
||||
|
||||
|
||||
# ☂️ middleware
|
||||
|
||||
Middleware runs by-default on [Umbrel OS](https://github.com/getumbrel/umbrel-os) as a containerized service. It wraps [Bitcoin Core](https://github.com/bitcoin/bitcoin)'s RPC and [LND](https://github.com/lightningnetwork/lnd)'s gRPC, and exposes them via a RESTful API.
|
||||
|
||||
Umbrel OS's [web dashboard](https://github.com/getumbrel/umbrel-dashboard) uses middleware to interact with both Bitcoin and Lightning Network.
|
||||
|
||||
## 🚀 Getting started
|
||||
|
||||
If you are looking to run Umbrel on your hardware, you do not need to run this service on it's own. Just download [Umbrel OS](https://github.com/getumbrel/umbrel-os/releases) and you're good to go.
|
||||
|
||||
## 🛠 Running middleware
|
||||
|
||||
Make sure a [`bitcoind`](https://github.com/bitcoin/bitcoin) and [`lnd`](https://github.com/lightningnetwork/lnd) instance is running and available on the same machine.
|
||||
|
||||
### Step 1. Install dependencies
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Step 2. Set environment variables
|
||||
Set the following environment variables directly or by placing them in `.env` file of project's root.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| `PORT` | Port where middleware should listen for requests | `3005` |
|
||||
| `DEVICE_HOSTS` | Comma separated list of IPs or domain names to whitelist for CORS | `http://umbrel.local` |
|
||||
| `BITCOIN_HOST` | IP or domain where `bitcoind` RPC is listening | `127.0.0.1` |
|
||||
| `RPC_USER` | `bitcoind` RPC username | |
|
||||
| `RPC_PASSWORD` | `bitcoind` RPC password | |
|
||||
| `LND_HOST` | IP or domain where `lnd` RPC is listening | `127.0.0.1` |
|
||||
| `TLS_FILE` | Path to `lnd`'s TLS certificate | `/lnd/tls.cert` |
|
||||
| `LND_PORT` | Port where `lnd` RPC is listening | `10009` |
|
||||
| `LND_NETWORK` | The chain `bitcoind` is running on (mainnet, testnet, regtest, simnet) | `mainnet` |
|
||||
| `LND_WALLET_PASSWORD` | The password for the LND wallet which will be automatically unlocked on boot | ` ` |
|
||||
| `MACAROON_DIR` | Path to `lnd`'s macaroon directory | `/lnd/data/chain/bitcoin/mainnet/` |
|
||||
| `JWT_PUBLIC_KEY_FILE` | Path to the JWT public key created by [`umbrel-manager`](https://github.com/getumbrel/umbrel-manager) | `/jwt-public-key/jwt.pem` |
|
||||
|
||||
### Step 3. Run middleware
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
You can browse through the available API endpoints [here](https://github.com/getumbrel/umbrel-middleware/tree/master/routes/v1).
|
||||
|
||||
---
|
||||
|
||||
### ⚡️ Don't be too reckless
|
||||
|
||||
> Umbrel is still in an early stage and things are expected to break every now and then. We **DO NOT** recommend running it on the mainnet with real money just yet, unless you want to be really *#reckless*.
|
||||
|
||||
## ❤️ Contributing
|
||||
|
||||
We welcome and appreciate new contributions!
|
||||
|
||||
If you're a developer looking to help but not sure where to begin, check out [these issues](https://github.com/getumbrel/umbrel-middleware/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) that have specifically been marked as being friendly to new contributors.
|
||||
|
||||
If you're looking for a bigger challenge, before opening a pull request please [create an issue](https://github.com/getumbrel/umbrel-middleware/issues/new/choose) or [join our community chat](https://t.me/getumbrel) to get feedback, discuss the best way to tackle the challenge, and to ensure that there's no duplication of work.
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
Umbrel Middleware is built upon the work done by [Casa](https://github.com/casa) on its open-source [API](https://github.com/Casa/Casa-Node-API).
|
||||
|
||||
---
|
||||
|
||||
[](https://github.com/getumbrel/umbrel-middleware/blob/master/LICENSE)
|
||||
|
||||
[getumbrel.com](https://getumbrel.com)
|
71
app.js
Normal file
71
app.js
Normal file
@ -0,0 +1,71 @@
|
||||
require('module-alias/register');
|
||||
require('module-alias').addPath('.');
|
||||
require('dotenv').config();
|
||||
|
||||
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');
|
||||
|
||||
const constants = require('utils/const.js');
|
||||
|
||||
// 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 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();
|
||||
|
||||
// Handles CORS
|
||||
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;
|
||||
|
||||
// LND Unlocker
|
||||
if (constants.LND_WALLET_PASSWORD) {
|
||||
const LndUnlocker = require('logic/lnd-unlocker');
|
||||
lndUnlocker = new LndUnlocker(constants.LND_WALLET_PASSWORD);
|
||||
lndUnlocker.start();
|
||||
}
|
91
bin/www
Normal file
91
bin/www
Normal file
@ -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);
|
||||
}
|
20
logic/application.js
Normal file
20
logic/application.js
Normal file
@ -0,0 +1,20 @@
|
||||
const bashService = require('services/bash.js');
|
||||
|
||||
const LND_DATA_SOURCE_DIRECTORY = '/lnd/';
|
||||
const LND_BACKUP_DEST_DIRECTORY = '/lndBackup';
|
||||
const CHANNEL_BACKUP_FILE = process.env.CHANNEL_BACKUP_FILE || '/lnd/data/chain/bitcoin/' + process.env.LND_NETWORK + '/channel.backup'
|
||||
|
||||
async function lndBackup() {
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
await bashService.exec('rsync', ['-r', '--delete', LND_DATA_SOURCE_DIRECTORY, LND_BACKUP_DEST_DIRECTORY]);
|
||||
}
|
||||
|
||||
async function lndChannnelBackup() {
|
||||
return CHANNEL_BACKUP_FILE;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lndBackup,
|
||||
lndChannnelBackup
|
||||
};
|
235
logic/bitcoind.js
Normal file
235
logic/bitcoind.js
Normal file
@ -0,0 +1,235 @@
|
||||
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 chain = blockChainInfo.chain;
|
||||
var blockCount = blockChainInfo.blocks;
|
||||
var headerCount = blockChainInfo.headers;
|
||||
var percent = blockChainInfo.verificationprogress;
|
||||
|
||||
return {
|
||||
chain,
|
||||
percent,
|
||||
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
|
||||
}
|
||||
|
||||
async function getTransaction(txid) {
|
||||
const transactionObj = await bitcoindService.getTransaction(txid);
|
||||
return {
|
||||
txid: txid,
|
||||
timestamp: transactionObj.result.time,
|
||||
confirmations: transactionObj.result.confirmations,
|
||||
blockhash: transactionObj.result.blockhash,
|
||||
size: transactionObj.result.size,
|
||||
input: transactionObj.result.vin.txid,
|
||||
utxo: transactionObj.result.vout,
|
||||
rawtx: transactionObj.result.hex
|
||||
}
|
||||
}
|
||||
|
||||
async function getNetworkInfo() {
|
||||
const networkInfo = await bitcoindService.getNetworkInfo();
|
||||
|
||||
return networkInfo.result; // eslint-disable-line object-shorthand
|
||||
}
|
||||
|
||||
async function getBlock(hash) {
|
||||
const blockObj = await bitcoindService.getBlock(hash);
|
||||
return {
|
||||
block: hash,
|
||||
confirmations: blockObj.result.confirmations,
|
||||
size: blockObj.result.size,
|
||||
height: blockObj.result.height,
|
||||
blocktime: blockObj.result.time,
|
||||
prevblock: blockObj.result.previousblockhash,
|
||||
nextblock: blockObj.result.nextblockhash,
|
||||
transactions: blockObj.result.tx
|
||||
}
|
||||
}
|
||||
|
||||
async function getBlocks(fromHeight, toHeight) {
|
||||
|
||||
|
||||
let startingBlockHashRaw;
|
||||
|
||||
try {
|
||||
startingBlockHashRaw = await bitcoindService.getBlockHash(toHeight);
|
||||
} catch (error) {
|
||||
if (error instanceof BitcoindError) {
|
||||
return error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let currentHash = startingBlockHashRaw.result;
|
||||
|
||||
const blocks = [];
|
||||
|
||||
//loop from 'to height' till 'from Height'
|
||||
for (let currentHeight = toHeight; currentHeight >= fromHeight; currentHeight--) {
|
||||
|
||||
const blockRaw = await bitcoindService.getBlock(currentHash);
|
||||
const block = blockRaw.result;
|
||||
|
||||
const formattedBlock = {
|
||||
hash: block.hash,
|
||||
height: block.height,
|
||||
numTransactions: block.tx.length,
|
||||
confirmations: block.confirmations,
|
||||
time: block.time,
|
||||
size: block.size
|
||||
};
|
||||
|
||||
blocks.push(formattedBlock);
|
||||
|
||||
currentHash = block.previousblockhash;
|
||||
//terminate loop if we reach the genesis block
|
||||
if (!currentHash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { blocks: blocks };
|
||||
}
|
||||
|
||||
async function getBlockHash(height) {
|
||||
const getBlockHashObj = await bitcoindService.getBlockHash(height);
|
||||
|
||||
return {
|
||||
hash: getBlockHashObj.result
|
||||
}
|
||||
}
|
||||
|
||||
async function nodeStatusDump() {
|
||||
const blockchainInfo = await bitcoindService.getBlockChainInfo();
|
||||
const networkInfo = await bitcoindService.getNetworkInfo();
|
||||
const mempoolInfo = await bitcoindService.getMempoolInfo();
|
||||
const miningInfo = await bitcoindService.getMiningInfo();
|
||||
|
||||
return {
|
||||
blockchain_info: blockchainInfo.result,
|
||||
network_info: networkInfo.result,
|
||||
mempool: mempoolInfo.result,
|
||||
mining_info: miningInfo.result
|
||||
}
|
||||
}
|
||||
|
||||
async function nodeStatusSummary() {
|
||||
const blockchainInfo = await bitcoindService.getBlockChainInfo();
|
||||
const networkInfo = await bitcoindService.getNetworkInfo();
|
||||
const mempoolInfo = await bitcoindService.getMempoolInfo();
|
||||
const miningInfo = await bitcoindService.getMiningInfo();
|
||||
|
||||
return {
|
||||
difficulty: blockchainInfo.result.difficulty,
|
||||
size: blockchainInfo.result.sizeOnDisk,
|
||||
mempool: mempoolInfo.result.bytes,
|
||||
connections: networkInfo.result.connections,
|
||||
networkhashps: miningInfo.result.networkhashps
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBlockHash,
|
||||
getTransaction,
|
||||
getBlock,
|
||||
getBlockCount,
|
||||
getBlocks,
|
||||
getConnectionsCount,
|
||||
getNetworkInfo,
|
||||
getMempoolInfo,
|
||||
getStatus,
|
||||
getSyncStatus,
|
||||
getVersion,
|
||||
nodeStatusDump,
|
||||
nodeStatusSummary
|
||||
};
|
16
logic/disk.js
Normal file
16
logic/disk.js
Normal file
@ -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,
|
||||
};
|
865
logic/lightning.js
Normal file
865
logic/lightning.js
Normal file
@ -0,0 +1,865 @@
|
||||
/**
|
||||
* 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, sweep) {
|
||||
const address = (await generateAddress()).address;
|
||||
const baseFeeEstimate = await estimateFee(address, amt, confTarget, sweep);
|
||||
|
||||
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;
|
||||
|
||||
const estimatedFeeSatPerKiloByte = successfulEstimate.feerateSatPerByte * 1000;
|
||||
|
||||
if (estimatedFeeSatPerKiloByte < 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);
|
||||
|
||||
const estimatedFeeSatPerKiloByte = estimate.feerateSatPerByte * 1000;
|
||||
|
||||
if (estimatedFeeSatPerKiloByte < 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fetch remote node alias and set it
|
||||
const { alias } = await getNodeAlias(channel.remotePubkey);
|
||||
channel.remoteAlias = alias || "";
|
||||
|
||||
// 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 when processedBlocks > blockHeight
|
||||
if (percentSynced < 1.0) {
|
||||
processedBlocks = Math.floor(percentSynced * info.blockHeight);
|
||||
} else {
|
||||
processedBlocks = info.blockHeight;
|
||||
percentSynced = (1).toFixed(4);
|
||||
}
|
||||
|
||||
} 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);
|
||||
|
||||
//No need for disk logic for now
|
||||
// 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 or not yet created.
|
||||
if (error instanceof LndError) {
|
||||
const operationalErrors = [
|
||||
'wallet locked, unlock it to enable full RPC access',
|
||||
'wallet not created, create one to enable full RPC access',
|
||||
];
|
||||
|
||||
if (error.error && operationalErrors.includes(error.error.details)) {
|
||||
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
|
||||
}
|
||||
|
||||
async function getNodeAlias(pubkey) {
|
||||
const includeChannels = false;
|
||||
let nodeInfo;
|
||||
try {
|
||||
nodeInfo = await lndService.getNodeInfo(pubkey, includeChannels);
|
||||
} catch (error) {
|
||||
return { alias: "" };
|
||||
}
|
||||
return { alias: nodeInfo.node.alias }; // 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,
|
||||
getNodeAlias,
|
||||
getChannelBalance,
|
||||
getChannelPolicy,
|
||||
getChannelCount,
|
||||
getInvoices,
|
||||
getChannels,
|
||||
getForwardingEvents,
|
||||
getOnChainTransactions,
|
||||
getPayments,
|
||||
getPendingChannels,
|
||||
getPublicUris,
|
||||
getStatus,
|
||||
getSyncStatus,
|
||||
getWalletBalance,
|
||||
initializeWallet,
|
||||
openChannel,
|
||||
payInvoice,
|
||||
sendCoins,
|
||||
unlockWallet,
|
||||
getGeneralInfo,
|
||||
getVersion,
|
||||
updateChannelPolicy,
|
||||
};
|
49
logic/lnd-unlocker.js
Normal file
49
logic/lnd-unlocker.js
Normal file
@ -0,0 +1,49 @@
|
||||
const lightningLogic = require('logic/lightning');
|
||||
|
||||
const SECONDS = 1000;
|
||||
const MINUTES = 60 * SECONDS;
|
||||
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
module.exports = class LndUnlocker {
|
||||
constructor(password) {
|
||||
this.password = password;
|
||||
this.running = false;
|
||||
this.unlocked = false;
|
||||
}
|
||||
|
||||
async unlock() {
|
||||
try {
|
||||
await lightningLogic.getGeneralInfo();
|
||||
if (!this.unlocked) {
|
||||
console.log('LndUnlocker: Wallet unlocked!');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
try {
|
||||
await lightningLogic.unlockWallet(this.password);
|
||||
console.log('LndUnlocker: Wallet unlocked!');
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log('LndUnlocker: Wallet failed to unlock!');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async start() {
|
||||
if (this.running) {
|
||||
throw new Error('Already running');
|
||||
}
|
||||
this.running = true;
|
||||
while (this.running) {
|
||||
this.unlocked = await this.unlock();
|
||||
await delay(this.unlocked ? 1 * MINUTES : 10 * SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = false;
|
||||
this.unlocked = false;
|
||||
}
|
||||
}
|
89
logic/network.js
Normal file
89
logic/network.js
Normal file
@ -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,
|
||||
};
|
31
logic/pages.js
Normal file
31
logic/pages.js
Normal file
@ -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
|
||||
};
|
53
middlewares/auth.js
Normal file
53
middlewares/auth.js
Normal file
@ -0,0 +1,53 @@
|
||||
const passport = require('passport');
|
||||
const passportJWT = require('passport-jwt');
|
||||
const constants = require('utils/const.js');
|
||||
const NodeError = require('models/errors.js').NodeError;
|
||||
const diskService = require('services/disk.js');
|
||||
|
||||
var JwtStrategy = passportJWT.Strategy;
|
||||
var ExtractJwt = passportJWT.ExtractJwt;
|
||||
|
||||
const JWT_AUTH = 'jwt';
|
||||
|
||||
passport.serializeUser(function (user, done) {
|
||||
return done(null, user.id);
|
||||
});
|
||||
|
||||
async function createJwtOptions() {
|
||||
const pubKey = await diskService.readFile(constants.JWT_PUBLIC_KEY_FILE);
|
||||
return {
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'),
|
||||
secretOrKey: pubKey,
|
||||
algorithm: 'RS256'
|
||||
};
|
||||
}
|
||||
|
||||
createJwtOptions().then(function (data) {
|
||||
|
||||
const jwtOptions = data;
|
||||
|
||||
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,
|
||||
};
|
12
middlewares/camelCaseRequest.js
Normal file
12
middlewares/camelCaseRequest.js
Normal file
@ -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,
|
||||
};
|
21
middlewares/cors.js
Normal file
21
middlewares/cors.js
Normal file
@ -0,0 +1,21 @@
|
||||
const corsOptions = {
|
||||
origin: (origin, callback) => {
|
||||
const whitelist = [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:8080',
|
||||
'http://localhost',
|
||||
'http://umbrel.local',
|
||||
...process.env.DEVICE_HOSTS.split(",")
|
||||
];
|
||||
|
||||
if (whitelist.indexOf(origin) !== -1 || !origin) {
|
||||
return callback(null, true);
|
||||
} else {
|
||||
return callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
corsOptions,
|
||||
};
|
28
middlewares/errorHandling.js
Normal file
28
middlewares/errorHandling.js
Normal file
@ -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;
|
15
middlewares/requestCorrelationId.js
Normal file
15
middlewares/requestCorrelationId.js
Normal file
@ -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;
|
42
models/errors.js
Normal file
42
models/errors.js
Normal file
@ -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
|
||||
};
|
||||
|
58
package.json
Normal file
58
package.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "umbrel-middleware",
|
||||
"version": "0.1.15",
|
||||
"description": "Middleware for Umbrel Node",
|
||||
"author": "Umbrel",
|
||||
"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": {
|
||||
"axios": "^0.19.2",
|
||||
"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",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.16.3",
|
||||
"grpc": "^1.8.0",
|
||||
"module-alias": "^2.1.0",
|
||||
"morgan": "^1.9.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.7.1",
|
||||
"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"
|
||||
}
|
||||
}
|
5
pre-commit
Executable file
5
pre-commit
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "Checking style, apply linting!"
|
||||
npm run lint -- .
|
||||
|
||||
make test
|
2600
resources/rpc.proto
Normal file
2600
resources/rpc.proto
Normal file
File diff suppressed because it is too large
Load Diff
9
routes/ping.js
Normal file
9
routes/ping.js
Normal file
@ -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: 'umbrel-middleware-' + pjson.version });
|
||||
});
|
||||
|
||||
module.exports = router;
|
85
routes/v1/bitcoind/info.js
Normal file
85
routes/v1/bitcoind/info.js
Normal file
@ -0,0 +1,85 @@
|
||||
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('/mempool', auth.jwt, safeHandler((req, res) =>
|
||||
bitcoind.getMempoolInfo()
|
||||
.then(mempool => res.json(mempool.result))
|
||||
));
|
||||
|
||||
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))
|
||||
));
|
||||
|
||||
//requires no authentication as it is used to fetch loading status
|
||||
//which could be fetched at login/signup page
|
||||
router.get('/status', 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))
|
||||
));
|
||||
|
||||
router.get('/statsDump', auth.jwt, safeHandler((req, res) =>
|
||||
bitcoind.nodeStatusDump()
|
||||
.then(statusdump => res.json(statusdump))
|
||||
));
|
||||
|
||||
router.get('/stats', auth.jwt, safeHandler((req, res) =>
|
||||
bitcoind.nodeStatusSummary()
|
||||
.then(statussumarry => res.json(statussumarry))
|
||||
));
|
||||
|
||||
router.get('/block', auth.jwt, safeHandler((req, res) => {
|
||||
if (req.query.hash !== undefined && req.query.hash !== null) {
|
||||
bitcoind.getBlock(req.query.hash)
|
||||
.then(blockhash => res.json(blockhash))
|
||||
} else if (req.query.height !== undefined && req.query.height !== null) {
|
||||
bitcoind.getBlockHash(req.query.height)
|
||||
.then(blockhash => res.json(blockhash))
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
// /v1/bitcoind/info/block/<hash>
|
||||
router.get('/block/:id', auth.jwt, safeHandler((req, res) =>
|
||||
bitcoind.getBlock(req.params.id)
|
||||
.then(blockhash => res.json(blockhash))
|
||||
));
|
||||
|
||||
router.get('/blocks', auth.jwt, safeHandler((req, res) => {
|
||||
const fromHeight = parseInt(req.query.from);
|
||||
const toHeight = parseInt(req.query.to);
|
||||
bitcoind.getBlocks(fromHeight, toHeight)
|
||||
.then(blocks => res.json(blocks))
|
||||
}
|
||||
));
|
||||
|
||||
router.get('/txid/:id', auth.jwt, safeHandler((req, res) =>
|
||||
bitcoind.getTransaction(req.params.id)
|
||||
.then(txhash => res.json(txhash))
|
||||
));
|
||||
|
||||
module.exports = router;
|
12
routes/v1/lnd/address.js
Normal file
12
routes/v1/lnd/address.js
Normal file
@ -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;
|
141
routes/v1/lnd/channel.js
Normal file
141
routes/v1/lnd/channel.js
Normal file
@ -0,0 +1,141 @@
|
||||
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;
|
||||
const sweep = req.query.sweep === 'true';
|
||||
|
||||
try {
|
||||
validator.isPositiveIntegerOrZero(confTarget);
|
||||
validator.isPositiveInteger(amt);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return await lightningLogic.estimateChannelOpenFee(parseInt(amt, 10), parseInt(confTarget, 10), sweep)
|
||||
.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;
|
45
routes/v1/lnd/info.js
Normal file
45
routes/v1/lnd/info.js
Normal file
@ -0,0 +1,45 @@
|
||||
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');
|
||||
const validator = require('utils/validator.js');
|
||||
|
||||
router.get('/uris', auth.jwt, safeHandler((req, res) =>
|
||||
lightning.getPublicUris()
|
||||
.then(uris => res.json(uris))
|
||||
));
|
||||
|
||||
//requires no authentication as it is used to fetch loading status
|
||||
//which could be fetched at login/signup page
|
||||
router.get('/status', 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))
|
||||
));
|
||||
|
||||
router.get('/alias', auth.jwt, safeHandler((req, res, next) => {
|
||||
|
||||
const pubkey = req.query.pubkey;
|
||||
|
||||
try {
|
||||
validator.isAlphanumeric(pubkey);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return lightning.getNodeAlias(pubkey)
|
||||
.then(alias => res.json(alias));
|
||||
}));
|
||||
|
||||
module.exports = router;
|
92
routes/v1/lnd/lightning.js
Normal file
92
routes/v1/lnd/lightning.js
Normal file
@ -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', auth.jwt, 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;
|
57
routes/v1/lnd/transaction.js
Normal file
57
routes/v1/lnd/transaction.js
Normal file
@ -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;
|
19
routes/v1/lnd/util.js
Normal file
19
routes/v1/lnd/util.js
Normal file
@ -0,0 +1,19 @@
|
||||
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))
|
||||
));
|
||||
|
||||
router.get('/download-channel-backup', auth.jwt, safeHandler((req, res) =>
|
||||
applicationLogic.lndChannnelBackup()
|
||||
.then(backupFile => res.download(backupFile, 'channel.backup'))
|
||||
));
|
||||
|
||||
|
||||
module.exports = router;
|
86
routes/v1/lnd/wallet.js
Normal file
86
routes/v1/lnd/wallet.js
Normal file
@ -0,0 +1,86 @@
|
||||
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.
|
||||
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))
|
||||
));
|
||||
|
||||
module.exports = router;
|
12
routes/v1/pages.js
Normal file
12
routes/v1/pages.js
Normal file
@ -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;
|
56
services/bash.js
Normal file
56
services/bash.js
Normal file
@ -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,
|
||||
};
|
130
services/bitcoind.js
Normal file
130
services/bitcoind.js
Normal file
@ -0,0 +1,130 @@
|
||||
const RpcClient = require('bitcoind-rpc');
|
||||
const camelizeKeys = require('camelize-keys');
|
||||
|
||||
const BitcoindError = require('models/errors.js').BitcoindError;
|
||||
|
||||
const BITCOIND_RPC_PORT = process.env.RPC_PORT || 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 promiseifyParam(rpcObj, rpcFn, param, what) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
rpcFn.call(rpcObj, param, (err, info) => {
|
||||
if (err) {
|
||||
reject(new BitcoindError(`Unable to obtain ${what}`, err));
|
||||
} else {
|
||||
resolve(camelizeKeys(info, '_'));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function promiseifyParamTwo(rpcObj, rpcFn, param1, param2, what) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
rpcFn.call(rpcObj, param1, param2, (err, info) => {
|
||||
if (err) {
|
||||
reject(new BitcoindError(`Unable to obtain ${what}`, err));
|
||||
} else {
|
||||
resolve(camelizeKeys(info, '_'));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getBestBlockHash() {
|
||||
return promiseify(rpcClient, rpcClient.getBestBlockHash, 'best block hash');
|
||||
}
|
||||
|
||||
function getBlockHash(height) {
|
||||
return promiseifyParam(rpcClient, rpcClient.getBlockHash, height, 'block height');
|
||||
}
|
||||
|
||||
function getBlock(hash) {
|
||||
return promiseifyParam(rpcClient, rpcClient.getBlock, hash, 'block info');
|
||||
}
|
||||
|
||||
function getTransaction(txid) {
|
||||
return promiseifyParamTwo(rpcClient, rpcClient.getRawTransaction, txid, 1, 'transaction info');
|
||||
}
|
||||
|
||||
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 getMiningInfo() {
|
||||
return promiseify(rpcClient, rpcClient.getMiningInfo, 'mining 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 = {
|
||||
getMiningInfo,
|
||||
getBestBlockHash,
|
||||
getBlockHash,
|
||||
getBlock,
|
||||
getTransaction,
|
||||
getBlockChainInfo,
|
||||
getBlockCount,
|
||||
getPeerInfo,
|
||||
getMempoolInfo,
|
||||
getNetworkInfo,
|
||||
help,
|
||||
};
|
68
services/disk.js
Normal file
68
services/disk.js
Normal file
@ -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,
|
||||
};
|
457
services/lnd.js
Normal file
457
services/lnd.js
Normal file
@ -0,0 +1,457 @@
|
||||
/* 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
|
||||
const LND_NETWORK = process.env.LND_NETWORK || 'mainnet';
|
||||
|
||||
// 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/' + 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.
|
||||
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: 0
|
||||
};
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
function getNodeInfo(pubkey, includeChannels) {
|
||||
const rpcPayload = {
|
||||
pub_key: pubkey,
|
||||
include_channels: includeChannels
|
||||
};
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.GetNodeInfo, rpcPayload, 'get node 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() {
|
||||
// Limit to 1k to prevent crazy response sizes
|
||||
// https://github.com/getumbrel/umbrel/issues/1245
|
||||
const rpcPayload = {
|
||||
max_payments: 1000,
|
||||
};
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ListPayments, rpcPayload, '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,
|
||||
getNodeInfo,
|
||||
getInvoices,
|
||||
getOpenChannels,
|
||||
getPayments,
|
||||
getPeers,
|
||||
getPendingChannels,
|
||||
getWalletBalance,
|
||||
generateAddress,
|
||||
generateSeed,
|
||||
getOnChainTransactions,
|
||||
initWallet,
|
||||
listUnspent,
|
||||
openChannel,
|
||||
sendCoins,
|
||||
sendPaymentSync,
|
||||
unlockWallet,
|
||||
updateChannelPolicy,
|
||||
};
|
15
test.setup.js
Normal file
15
test.setup.js
Normal file
@ -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
|
||||
}});
|
9
test/.eslintrc
Normal file
9
test/.eslintrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../.eslintrc",
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
"no-magic-numbers": "off"
|
||||
}
|
||||
}
|
17
test/endpoints/ping.js
Normal file
17
test/endpoints/ping.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
567
test/endpoints/v1/bitcoind/info.js
Normal file
567
test/endpoints/v1/bitcoind/info.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
226
test/endpoints/v1/lnd/channel.js
Normal file
226
test/endpoints/v1/lnd/channel.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
94
test/endpoints/v1/lnd/lightning.js
Normal file
94
test/endpoints/v1/lnd/lightning.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
400
test/endpoints/v1/lnd/transaction.js
Normal file
400
test/endpoints/v1/lnd/transaction.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
112
test/endpoints/v1/lnd/wallet.js
Normal file
112
test/endpoints/v1/lnd/wallet.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
BIN
test/fixtures/lnd/admin.macaroon
vendored
Normal file
BIN
test/fixtures/lnd/admin.macaroon
vendored
Normal file
Binary file not shown.
13
test/fixtures/lnd/tls.cert
vendored
Normal file
13
test/fixtures/lnd/tls.cert
vendored
Normal file
@ -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-----
|
22
test/global.js
Normal file
22
test/global.js
Normal file
@ -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();
|
||||
});
|
24
test/mocks/LndError.js
Normal file
24
test/mocks/LndError.js
Normal file
@ -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,
|
||||
};
|
334
test/mocks/bitcoind.js
Normal file
334
test/mocks/bitcoind.js
Normal file
@ -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,
|
||||
};
|
426
test/mocks/lnd.js
Normal file
426
test/mocks/lnd.js
Normal file
@ -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,
|
||||
};
|
410
test/unit/lightning.js
Normal file
410
test/unit/lightning.js
Normal file
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
196
test/unit/network.js
Normal file
196
test/unit/network.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
5
utils/UUID.js
Normal file
5
utils/UUID.js
Normal file
@ -0,0 +1,5 @@
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
module.exports = {
|
||||
create: uuidv4,
|
||||
};
|
18
utils/const.js
Normal file
18
utils/const.js
Normal file
@ -0,0 +1,18 @@
|
||||
/* eslint-disable id-length */
|
||||
module.exports = {
|
||||
LN_REQUIRED_CONFIRMATIONS: 3,
|
||||
LND_STATUS_CODES: {
|
||||
UNAVAILABLE: 14,
|
||||
UNKNOWN: 2,
|
||||
},
|
||||
JWT_PUBLIC_KEY_FILE: process.env.JWT_PUBLIC_KEY_FILE || 'UNKNOWN',
|
||||
MANAGED_CHANNELS_FILE: '/channel-data/managedChannels.json',
|
||||
LND_WALLET_PASSWORD: process.env.LND_WALLET_PASSWORD || 'moneyprintergobrrr',
|
||||
REQUEST_CORRELATION_NAMESPACE_KEY: 'umbrel-middleware-request',
|
||||
REQUEST_CORRELATION_ID_KEY: 'reqId',
|
||||
STATUS_CODES: {
|
||||
BAD_GATEWAY: 502,
|
||||
FORBIDDEN: 403,
|
||||
OK: 200,
|
||||
},
|
||||
};
|
66
utils/convert.js
Normal file
66
utils/convert.js
Normal file
@ -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;
|
134
utils/logger.js
Normal file
134
utils/logger.js
Normal file
@ -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, 'umbrel-middleware');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
15
utils/safeHandler.js
Normal file
15
utils/safeHandler.js
Normal file
@ -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;
|
86
utils/validator.js
Normal file
86
utils/validator.js
Normal file
@ -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,
|
||||
};
|
40
wait-for-node-manager.sh
Executable file
40
wait-for-node-manager.sh
Executable file
@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
# Usage
|
||||
# ./wait-for-node-manager.sh <hostname> <command>
|
||||
# Other documentation: https://docs.docker.com/compose/startup-order/
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
host="$1"
|
||||
shift
|
||||
cmd="$@"
|
||||
|
||||
found=1
|
||||
iterations=1
|
||||
until [ $found = 2 ]; do
|
||||
if node -e "const axios = require('axios').default; axios.get('http://$host:3006/ping').then((resp) => {console.log(resp.data); process.exit(0); }).catch((error) => {process.exit(1) } );" ; then
|
||||
echo "Can connect, lets proceed with server starting"
|
||||
found=2
|
||||
else
|
||||
echo "Can't connect, keep trying"
|
||||
fi
|
||||
if [ $iterations -gt 14 ]; then
|
||||
echo "Cannot connect after 15 tries, giving up"
|
||||
exit 1
|
||||
fi
|
||||
((iterations=iterations+1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
>&2 echo "Pre-condition found, Running service"
|
||||
exec $cmd
|
Loading…
Reference in New Issue
Block a user