From 7d3ba93fed1f5ccb66268c015fcadcc60030c048 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 15 Jul 2021 14:43:30 -0400 Subject: [PATCH 01/52] Partial create lnurl wdraw, wdrawrequest, wdraw --- .dockerignore | 3 + .eslintignore | 3 + .eslintrc.json | 21 + .gitignore | 6 + Dockerfile | 55 + README.md | 100 + cypherapps/data/config.json | 18 + cypherapps/docker-compose.yaml | 26 + package-lock.json | 2658 +++++++++++++++++ package.json | 51 + src/config/LnurlConfig.ts | 18 + src/config/lnurl.sql | 18 + src/entity/LnurlWithdrawRequest.ts | 68 + src/index.ts | 15 + src/lib/CyphernodeClient.ts | 453 +++ src/lib/HttpServer.ts | 161 + src/lib/LnurlDB.ts | 57 + src/lib/LnurlWithdraw.ts | 121 + src/lib/Log2File.ts | 22 + src/lib/Utils.ts | 100 + src/types/IReqCreateLnurlWithdraw.ts | 7 + src/types/IReqLnurlWithdraw.ts | 5 + src/types/IRespCreateLnurlWithdraw.ts | 7 + src/types/IRespLnserviceStatus.ts | 4 + src/types/IRespLnserviceWithdrawRequest.ts | 11 + src/types/cyphernode/IAddToBatchResult.ts | 14 + src/types/cyphernode/IBatchDetails.ts | 26 + src/types/cyphernode/IBatchState.ts | 5 + src/types/cyphernode/IBatchTx.ts | 12 + src/types/cyphernode/IBatcher.ts | 13 + src/types/cyphernode/IBatcherIdent.ts | 4 + src/types/cyphernode/IOutput.ts | 3 + src/types/cyphernode/IReqAddToBatch.ts | 15 + src/types/cyphernode/IReqBatchSpend.ts | 9 + src/types/cyphernode/IReqGetBatchDetails.ts | 10 + src/types/cyphernode/IReqSpend.ts | 10 + src/types/cyphernode/IRespAddToBatch.ts | 7 + src/types/cyphernode/IRespBatchSpend.ts | 7 + src/types/cyphernode/IRespGetBatchDetails.ts | 7 + src/types/cyphernode/IRespGetBatcher.ts | 7 + src/types/cyphernode/IRespSpend.ts | 7 + src/types/cyphernode/ITx.ts | 14 + src/types/jsonrpc/IMessage.ts | 3 + src/types/jsonrpc/IRequestMessage.ts | 18 + src/types/jsonrpc/IResponseMessage.ts | 55 + .../CreateLnurlWithdrawValidator.ts | 13 + tsconfig.json | 70 + 47 files changed, 4337 insertions(+) create mode 100644 .dockerignore create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 Dockerfile create mode 100644 cypherapps/data/config.json create mode 100644 cypherapps/docker-compose.yaml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/config/LnurlConfig.ts create mode 100644 src/config/lnurl.sql create mode 100644 src/entity/LnurlWithdrawRequest.ts create mode 100644 src/index.ts create mode 100644 src/lib/CyphernodeClient.ts create mode 100644 src/lib/HttpServer.ts create mode 100644 src/lib/LnurlDB.ts create mode 100644 src/lib/LnurlWithdraw.ts create mode 100644 src/lib/Log2File.ts create mode 100644 src/lib/Utils.ts create mode 100644 src/types/IReqCreateLnurlWithdraw.ts create mode 100644 src/types/IReqLnurlWithdraw.ts create mode 100644 src/types/IRespCreateLnurlWithdraw.ts create mode 100644 src/types/IRespLnserviceStatus.ts create mode 100644 src/types/IRespLnserviceWithdrawRequest.ts create mode 100644 src/types/cyphernode/IAddToBatchResult.ts create mode 100644 src/types/cyphernode/IBatchDetails.ts create mode 100644 src/types/cyphernode/IBatchState.ts create mode 100644 src/types/cyphernode/IBatchTx.ts create mode 100644 src/types/cyphernode/IBatcher.ts create mode 100644 src/types/cyphernode/IBatcherIdent.ts create mode 100644 src/types/cyphernode/IOutput.ts create mode 100644 src/types/cyphernode/IReqAddToBatch.ts create mode 100644 src/types/cyphernode/IReqBatchSpend.ts create mode 100644 src/types/cyphernode/IReqGetBatchDetails.ts create mode 100644 src/types/cyphernode/IReqSpend.ts create mode 100644 src/types/cyphernode/IRespAddToBatch.ts create mode 100644 src/types/cyphernode/IRespBatchSpend.ts create mode 100644 src/types/cyphernode/IRespGetBatchDetails.ts create mode 100644 src/types/cyphernode/IRespGetBatcher.ts create mode 100644 src/types/cyphernode/IRespSpend.ts create mode 100644 src/types/cyphernode/ITx.ts create mode 100644 src/types/jsonrpc/IMessage.ts create mode 100644 src/types/jsonrpc/IRequestMessage.ts create mode 100644 src/types/jsonrpc/IResponseMessage.ts create mode 100644 src/validators/CreateLnurlWithdrawValidator.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5b1d57f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +Dockerfile +node_modules +build diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..bb64915 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules +build + diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..76b7e67 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + }, + "plugins": [ + "@typescript-eslint", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "@typescript-eslint/interface-name-prefix": ["off"] + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "prettier/@typescript-eslint" + ] +} diff --git a/.gitignore b/.gitignore index 6704566..bda903c 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,9 @@ dist # TernJS port file .tern-port + +logs +build +data +!cypherapps/data +*.sqlite diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..02f3b26 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +#FROM node:14.11.0-alpine3.11 + +#WORKDIR /lnurl + +#COPY package.json /lnurl + +#RUN apk add --update --no-cache --virtual .gyp \ +# python \ +# make \ +# g++ +#RUN npm install +#RUN apk del .gyp + +#COPY tsconfig.json /lnurl +#COPY src /lnurl/src + +#RUN npm run build + +#EXPOSE 9229 3000 + +#ENTRYPOINT [ "npm", "run", "start" ] + +#--------------------------------------------------- + +FROM node:14.11.0-alpine3.11 as build-base +WORKDIR /lnurl + +COPY package.json /lnurl + +RUN apk add --update --no-cache --virtual .gyp \ + python \ + make \ + g++ +RUN npm install + +#--------------------------------------------------- + +FROM build-base as base-slim +WORKDIR /lnurl + +RUN apk del .gyp + +#--------------------------------------------------- + +FROM base-slim +WORKDIR /lnurl + +#COPY tsconfig.json /lnurl +#COPY src /lnurl/src + +#RUN npm run build + +EXPOSE 9229 3000 + +ENTRYPOINT [ "npm", "run", "start" ] diff --git a/README.md b/README.md index 8d7b1ca..5418782 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,102 @@ # lnurl_cypherapp LNURL cypherapp for cyphernode + +======================== + +LNURL-withdraw + +1. Web app calls lnurl.createLnurl + +params: { + +} + +========================= +Web app + +### createLNURL + amount = msats + description + expiration = timestamp + secretToken = k1 + webhookUrl = called back when invoice paid + +returns: + id + LNURL string + +https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df +LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS + + +### deleteLNURL + id + +### getLNURL + id + +returns: + id + LNURL string + + +========================= +User/LN Wallet + +### GET LNURL modalities + +returns: +{ + tag: "withdrawRequest", // type of LNURL + callback: String, // the URL which LN SERVICE would accept a withdrawal Lightning invoice as query parameter + k1: String, // random or non-random string to identify the user's LN WALLET when using the callback URL + defaultDescription: String, // A default withdrawal invoice description + minWithdrawable: Integer, // Min amount (in millisatoshis) the user can withdraw from LN SERVICE, or 0 + maxWithdrawable: Integer, // Max amount (in millisatoshis) the user can withdraw from LN SERVICE, or equal to minWithdrawable if the user has no choice over the amounts + balanceCheck: String, // Optional, an URL that can be called next time the wallet wants to perform a balance check, the call will be the same as performed in this step and the expected response is the same +} + +### GET payment request + + + // either '?' or '&' depending on whether there is a query string already in the callback + k1= // the k1 specified in the response above + &pr= // the payment request generated by the wallet + &balanceNotify= // optional, see note below + + +========================= +========================= + +DOCKER_BUILDKIT=0 docker build -t lnurl . +docker run --rm -it -v "$PWD:/lnurl" --entrypoint ash bff4412e444c +npm install + +docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" --entrypoint ash lnurl +npm run build +npm run start + +-- + +kexkey@AlcodaZ-2 ~ % docker exec -it lnurl ash +/lnurl # apk add curl +/lnurl # curl -d '{"id":0,"method":"getConfig","params":[]}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":{"LOG":"DEBUG","BASE_DIR":"/lnurl","DATA_DIR":"data","DB_NAME":"lnurl.sqlite","URL_SERVER":"http://lnurl","URL_PORT":8000,"URL_CTX_WEBHOOKS":"webhooks","SESSION_TIMEOUT":600,"CN_URL":"https://gatekeeper:2009/v0","CN_API_ID":"003","CN_API_KEY":"39b83c35972aeb81a242bfe189dc0a22da5ac6cbb64072b492f2d46519a97618"}} + + + +sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw_request" + + amount: number; + description?: string; + expiration?: Date; + secretToken: string; + webhookUrl?: string; + +curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"amount":0.01,"description":"desc01","expiration":"2021-07-15 12:12","secretToken":"abc01","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15 12:12","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"http://.onion/lnurlWithdraw?s=abc01","withdrawnDetails":null,"withdrawnTimestamp":null,"lnurlWithdrawId":1,"createdAt":"2021-07-15 15:42:43","updatedAt":"2021-07-15 15:42:43"}} + +sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw_request" +id|amount|description|expiration|secret_token|webhook_url|lnurl|withdrawn_details|withdrawn_ts|created_ts|updated_ts +1|0.01|desc01|2021-07-15 12:12|abc01|https://webhookUrl01|http://.onion/lnurlWithdraw?s=abc01|||2021-07-15 15:42:43|2021-07-15 15:42:43 + diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json new file mode 100644 index 0000000..2ed7615 --- /dev/null +++ b/cypherapps/data/config.json @@ -0,0 +1,18 @@ +{ + "LOG": "DEBUG", + "BASE_DIR": "/lnurl", + "DATA_DIR": "data", + "DB_NAME": "lnurl.sqlite", + "URL_API_SERVER": "http://lnurl", + "URL_API_PORT": 8000, + "URL_API_CTX": "/api", + "SESSION_TIMEOUT": 600, + "CN_URL": "https://gatekeeper:2009/v0", + "CN_API_ID": "003", + "CN_API_KEY": "39b83c35972aeb81a242bfe189dc0a22da5ac6cbb64072b492f2d46519a97618", + "LN_SERVICE_SERVER": "http://.onion", + "LN_SERVICE_PORT": 80, + "LN_SERVICE_CTX": "/lnurl", + "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", + "LN_SERVICE_WITHDRAW_CTX": "/withdraw" +} diff --git a/cypherapps/docker-compose.yaml b/cypherapps/docker-compose.yaml new file mode 100644 index 0000000..34146df --- /dev/null +++ b/cypherapps/docker-compose.yaml @@ -0,0 +1,26 @@ +version: "3" + +services: + lnurl: + environment: + - "TRACING=1" + - "CYPHERNODE_URL=https://gatekeeper:${GATEKEEPER_PORT}" + image: cyphernode/lnurl:v0.1.0 + entrypoint: ["npm", "run", "start:dev"] + volumes: + - "$APP_SCRIPT_PATH/data:/lnurl/data" + - "$GATEKEEPER_DATAPATH/certs/cert.pem:/lnurl/cert.pem:ro" + - "$LOGS_DATAPATH:/lnurl/logs" + networks: + - cyphernodeappsnet + restart: always + labels: + - "traefik.docker.network=cyphernodeappsnet" + - "traefik.frontend.rule=PathPrefixStrip:/lnurl" + - "traefik.frontend.passHostHeader=true" + - "traefik.enable=true" + - "traefik.port=8000" + - "traefik.frontend.auth.basic.users=:$$2y$$05$$LFKGjKBkmWbI5RUFBqwonOWEcen4Yu.mU139fvD3flWcP8gUqLLaC" +networks: + cyphernodeappsnet: + external: true diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b0f9512 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2658 @@ +{ + "name": "lnurl", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@sqltools/formatter": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz", + "integrity": "sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==" + }, + "@types/async-lock": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.3.tgz", + "integrity": "sha512-UpeDcjGKsYEQMeqEbfESm8OWJI305I7b9KE4ji3aBjoKWyN5CTdn8izcA1FM1DVDne30R5fNEnIy89vZw5LXJQ==" + }, + "@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/json-schema": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", + "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", + "dev": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/node": { + "version": "7.10.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.14.tgz", + "integrity": "sha512-29GS75BE8asnTno3yB6ubOJOO0FboExEqNJy4bpz0GSmW/8wPTNL4h9h63c6s1uTrOopCmJYe/4yJLh5r92ZUA==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/sqlite3": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.7.tgz", + "integrity": "sha512-8FHV/8Uzd7IwdHm5mvmF2Aif4aC/gjrt4axWD9SmfaxITnOjtOhCbOSTuqv/VbH1uq0QrwlaTj9aTz3gmR6u4w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/zen-observable": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", + "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==" + }, + "@typescript-eslint/eslint-plugin": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.34.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "app-root-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", + "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async-lock": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.0.tgz", + "integrity": "sha512-8A7SkiisnEgME2zEedtDYPxUPzdv3x//E7n5IFktPAtMYSEAV7eNJF0rMwrVyUFj6d/8rgajLantbjcNRQYXIg==" + }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "requires": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-plugin-prettier": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz", + "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figlet": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.0.tgz", + "integrity": "sha512-ZQJM4aifMpz6H19AW1VqvZ7l4pOE9p7i/3LyxgO2kp+PO/VcDYNqIHEMtkccqIhTXMKci4kjueJr/iCQEaT/Ww==" + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-status-codes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-1.4.0.tgz", + "integrity": "sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" + }, + "mime-types": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "requires": { + "mime-db": "1.48.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "needle": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.8.0.tgz", + "integrity": "sha512-ZTq6WYkN/3782H1393me3utVYdq2XyqNUFBsprEE3VMAT0+hP/cItpnITpqsY6ep2yeFE4Tqtqwc74VqUlUYtw==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parent-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", + "integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc=" + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "requires": { + "parse5": "^6.0.1" + }, + "dependencies": { + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + } + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sqlite3": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz", + "integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==", + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.11.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "tslog": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.2.0.tgz", + "integrity": "sha512-xOCghepl5w+wcI4qXI7vJy6c53loF8OoC/EuKz1ktAPMtltEDz00yo1poKuyBYIQaq4ZDYKYFPD9PfqVrFXh0A==", + "requires": { + "source-map-support": "^0.5.19" + } + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typeorm": { + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.34.tgz", + "integrity": "sha512-FZAeEGGdSGq7uTH3FWRQq67JjKu0mgANsSZ04j3kvDYNgy9KwBl/6RFgMVgiSgjf7Rqd7NrhC2KxVT7I80qf7w==", + "requires": { + "@sqltools/formatter": "^1.2.2", + "app-root-path": "^3.0.0", + "buffer": "^6.0.3", + "chalk": "^4.1.0", + "cli-highlight": "^2.1.10", + "debug": "^4.3.1", + "dotenv": "^8.2.0", + "glob": "^7.1.6", + "js-yaml": "^4.0.0", + "mkdirp": "^1.0.4", + "reflect-metadata": "^0.1.13", + "sha.js": "^2.4.11", + "tslib": "^2.1.0", + "xml2js": "^0.4.23", + "yargonaut": "^1.1.4", + "yargs": "^16.2.0", + "zen-observable-ts": "^1.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "yargonaut": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz", + "integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==", + "requires": { + "chalk": "^1.1.1", + "figlet": "^1.1.1", + "parent-require": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "zen-observable-ts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.0.0.tgz", + "integrity": "sha512-KmWcbz+9kKUeAQ8btY8m1SsEFgBcp7h/Uf3V5quhan7ZWdjGsf0JcGLULQiwOZibbFWnHkYq8Nn2AZbJabovQg==", + "requires": { + "@types/zen-observable": "^0.8.2", + "zen-observable": "^0.8.15" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..eb94d76 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "lnurl", + "version": "0.1.0", + "description": "", + "main": "app.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "rimraf ./build && tsc", + "start:dev": "node --inspect=0.0.0.0:9229 --require ts-node/register ./src/index.ts", + "start": "npm run build && node build/index.js", + "lint": "eslint . --ext .ts", + "lintfix": "eslint . --ext .ts --fix" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SatoshiPortal/lnurl_cypherapp.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/SatoshiPortal/lnurl_cypherapp/issues" + }, + "homepage": "https://github.com/SatoshiPortal/lnurl_cypherapp#readme", + "dependencies": { + "@types/node": "^7.0.5", + "@types/async-lock": "^1.1.2", + "async-lock": "^1.2.4", + "axios": "^0.21.1", + "express": "^4.17.1", + "http-status-codes": "^1.4.0", + "reflect-metadata": "^0.1.13", + "sqlite3": "^4.2.0", + "typeorm": "^0.2.25", + "tslog": "^3.2.0", + "bech32": "^2.0.0" + }, + "devDependencies": { + "@types/express": "^4.17.6", + "@types/node": "^13.13.12", + "@types/sqlite3": "^3.1.6", + "@typescript-eslint/eslint-plugin": "^2.24.0", + "@typescript-eslint/parser": "^2.24.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-prettier": "^3.1.4", + "prettier": "2.0.5", + "rimraf": "^3.0.2", + "ts-node": "^8.10.2", + "typescript": "^3.9.5" + } +} diff --git a/src/config/LnurlConfig.ts b/src/config/LnurlConfig.ts new file mode 100644 index 0000000..936b437 --- /dev/null +++ b/src/config/LnurlConfig.ts @@ -0,0 +1,18 @@ +export default interface LnurlConfig { + LOG: string; + BASE_DIR: string; + DATA_DIR: string; + DB_NAME: string; + URL_API_SERVER: string; + URL_API_PORT: number; + URL_API_CTX: string; + SESSION_TIMEOUT: number; + CN_URL: string; + CN_API_ID: string; + CN_API_KEY: string; + LN_SERVICE_SERVER: string; + LN_SERVICE_PORT: number; + LN_SERVICE_CTX: string; + LN_SERVICE_WITHDRAW_REQUEST_CTX: string; + LN_SERVICE_WITHDRAW_CTX: string; +} diff --git a/src/config/lnurl.sql b/src/config/lnurl.sql new file mode 100644 index 0000000..6749e10 --- /dev/null +++ b/src/config/lnurl.sql @@ -0,0 +1,18 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE lnurl_withdraw_request ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + amount REAL, + description TEXT, + expiration INTEGER, + secret_token TEXT UNIQUE, + webhook_url TEXT, + lnurl TEXT, + withdrawn_details TEXT, + withdrawn_ts INTEGER, + active INTEGER DEFAULT TRUE, + created_ts INTEGER DEFAULT CURRENT_TIMESTAMP, + updated_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_lnurl_withdraw_description ON lnurl_withdraw (description); +CREATE INDEX idx_lnurl_withdraw_lnurl ON lnurl_withdraw (lnurl); diff --git a/src/entity/LnurlWithdrawRequest.ts b/src/entity/LnurlWithdrawRequest.ts new file mode 100644 index 0000000..872c2ef --- /dev/null +++ b/src/entity/LnurlWithdrawRequest.ts @@ -0,0 +1,68 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + OneToMany, + Index, + CreateDateColumn, + UpdateDateColumn, + Unique, +} from "typeorm"; + +// CREATE TABLE lnurl_withdraw_request ( +// id INTEGER PRIMARY KEY AUTOINCREMENT, +// amount REAL, +// description TEXT, +// expiration INTEGER, +// secret_token TEXT UNIQUE, +// webhook_url TEXT, +// lnurl TEXT, +// withdrawn_details TEXT, +// withdrawn_ts INTEGER, +// active INTEGER, +// created_ts INTEGER DEFAULT CURRENT_TIMESTAMP, +// updated_ts INTEGER DEFAULT CURRENT_TIMESTAMP +// ); +// CREATE INDEX idx_lnurl_withdraw_description ON lnurl_withdraw (description); +// CREATE INDEX idx_lnurl_withdraw_lnurl ON lnurl_withdraw (lnurl); + +@Entity() +export class LnurlWithdrawRequest { + @PrimaryGeneratedColumn({ name: "id" }) + lnurlWithdrawId!: number; + + @Column({ type: "real", name: "amount" }) + amount!: number; + + @Index("idx_lnurl_withdraw_description") + @Column({ type: "text", name: "description", nullable: true }) + description?: string; + + @Column({ type: "integer", name: "expiration", nullable: true }) + expiration?: Date; + + @Column({ type: "text", name: "secret_token", unique: true }) + secretToken!: string; + + @Column({ type: "text", name: "webhook_url", nullable: true }) + webhookUrl?: string; + + @Index("idx_lnurl_withdraw_lnurl") + @Column({ type: "text", name: "lnurl", nullable: true }) + lnurl?: string; + + @Column({ type: "text", name: "withdrawn_details", nullable: true }) + withdrawnDetails?: string; + + @Column({ type: "integer", name: "withdrawn_ts", nullable: true }) + withdrawnTimestamp?: Date; + + @Column({ type: "integer", name: "active", nullable: true, default: true }) + active?: boolean; + + @CreateDateColumn({ type: "integer", name: "created_ts" }) + createdAt?: Date; + + @UpdateDateColumn({ type: "integer", name: "updated_ts" }) + updatedAt?: Date; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c3d588d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,15 @@ +import { HttpServer } from "./lib/HttpServer"; +import logger from "./lib/Log2File"; + +const setup = async (): Promise => { + logger.debug("setup"); +}; + +const main = async (): Promise => { + await setup(); + + const httpServer = new HttpServer(); + httpServer.start(); +}; + +main(); diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts new file mode 100644 index 0000000..822ff86 --- /dev/null +++ b/src/lib/CyphernodeClient.ts @@ -0,0 +1,453 @@ +import logger from "./Log2File"; +import crypto from "crypto"; +import axios, { AxiosRequestConfig } from "axios"; +import https from "https"; +import path from "path"; +import fs from "fs"; +import LnurlConfig from "../config/LnurlConfig"; +import IRespGetBatchDetails from "../types/cyphernode/IRespGetBatchDetails"; +import IRespAddToBatch from "../types/cyphernode/IRespAddToBatch"; +import IReqBatchSpend from "../types/cyphernode/IReqBatchSpend"; +import IReqGetBatchDetails from "../types/cyphernode/IReqGetBatchDetails"; +import IRespBatchSpend from "../types/cyphernode/IRespBatchSpend"; +import IReqAddToBatch from "../types/cyphernode/IReqAddToBatch"; +import { IResponseError, ErrorCodes } from "../types/jsonrpc/IResponseMessage"; +import IReqSpend from "../types/cyphernode/IReqSpend"; +import IRespSpend from "../types/cyphernode/IRespSpend"; + +class CyphernodeClient { + private baseURL: string; + private readonly h64: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg=="; + private apiId: string; + private apiKey: string; + private caFile: string; + + constructor(lnurlConfig: LnurlConfig) { + this.baseURL = lnurlConfig.CN_URL; + this.apiId = lnurlConfig.CN_API_ID; + this.apiKey = lnurlConfig.CN_API_KEY; + this.caFile = path.resolve(lnurlConfig.BASE_DIR, "cert.pem"); + } + + configureCyphernode(lnurlConfig: LnurlConfig): void { + this.baseURL = lnurlConfig.CN_URL; + this.apiId = lnurlConfig.CN_API_ID; + this.apiKey = lnurlConfig.CN_API_KEY; + this.caFile = path.resolve(lnurlConfig.BASE_DIR, "cert.pem"); + } + + _generateToken(): string { + logger.info("CyphernodeClient._generateToken"); + + const current = Math.round(new Date().getTime() / 1000) + 10; + const p = '{"id":"' + this.apiId + '","exp":' + current + "}"; + const p64 = Buffer.from(p).toString("base64"); + const msg = this.h64 + "." + p64; + const s = crypto + .createHmac("sha256", this.apiKey) + .update(msg) + .digest("hex"); + const token = msg + "." + s; + + logger.debug("CyphernodeClient._generateToken :: token=" + token); + + return token; + } + + async _post( + url: string, + postdata: unknown, + addedOptions?: unknown + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + logger.info("CyphernodeClient._post %s %s %s", url, postdata, addedOptions); + + let configs: AxiosRequestConfig = { + url: url, + method: "post", + baseURL: this.baseURL, + headers: { + Authorization: "Bearer " + this._generateToken(), + }, + data: postdata, + httpsAgent: new https.Agent({ + ca: fs.readFileSync(this.caFile), + // rejectUnauthorized: false, + }), + }; + if (addedOptions) { + configs = Object.assign(configs, addedOptions); + } + + // logger.debug( + // "CyphernodeClient._post :: configs: %s", + // JSON.stringify(configs) + // ); + + try { + const response = await axios.request(configs); + logger.debug( + "CyphernodeClient._post :: response.data = %s", + JSON.stringify(response.data) + ); + + return { status: response.status, data: response.data }; + } catch (error) { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.info( + "CyphernodeClient._post :: error.response.data = %s", + JSON.stringify(error.response.data) + ); + logger.info( + "CyphernodeClient._post :: error.response.status = %d", + error.response.status + ); + logger.info( + "CyphernodeClient._post :: error.response.headers = %s", + error.response.headers + ); + + return { status: error.response.status, data: error.response.data }; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.info( + "CyphernodeClient._post :: error.message = %s", + error.message + ); + + return { status: -1, data: error.message }; + } else { + // Something happened in setting up the request that triggered an Error + logger.info("CyphernodeClient._post :: Error: %s", error.message); + + return { status: -2, data: error.message }; + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async _get(url: string, addedOptions?: unknown): Promise { + logger.info("CyphernodeClient._get %s %s", url, addedOptions); + + let configs: AxiosRequestConfig = { + url: url, + method: "get", + baseURL: this.baseURL, + headers: { + Authorization: "Bearer " + this._generateToken(), + }, + httpsAgent: new https.Agent({ + ca: fs.readFileSync(this.caFile), + // rejectUnauthorized: false, + }), + }; + if (addedOptions) { + configs = Object.assign(configs, addedOptions); + } + + try { + const response = await axios.request(configs); + logger.debug( + "CyphernodeClient._get :: response.data = %s", + JSON.stringify(response.data) + ); + + return { status: response.status, data: response.data }; + } catch (error) { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.info( + "CyphernodeClient._get :: error.response.data = %s", + JSON.stringify(error.response.data) + ); + logger.info( + "CyphernodeClient._get :: error.response.status = %d", + error.response.status + ); + logger.info( + "CyphernodeClient._get :: error.response.headers = %s", + error.response.headers + ); + + return { status: error.response.status, data: error.response.data }; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.info( + "CyphernodeClient._get :: error.message = %s", + error.message + ); + + return { status: -1, data: error.message }; + } else { + // Something happened in setting up the request that triggered an Error + logger.info("CyphernodeClient._get :: Error: %s", error.message); + + return { status: -2, data: error.message }; + } + } + } + + async addToBatch(batchRequestTO: IReqAddToBatch): Promise { + // POST http://192.168.111.152:8080/addtobatch + + // args: + // - address, required, desination address + // - amount, required, amount to send to the destination address + // - batchId, optional, the id of the batch to which the output will be added, default batch if not supplied, overrides batchLabel + // - batchLabel, optional, the label of the batch to which the output will be added, default batch if not supplied + // - webhookUrl, optional, the webhook to call when the batch is broadcast + + // response: + // - lnurlId, the id of the lnurl + // - outputId, the id of the added output + // - nbOutputs, the number of outputs currently in the batch + // - oldest, the timestamp of the oldest output in the batch + // - total, the current sum of the batch's output amounts + + // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} + // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batchId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"} + // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batchLabel":"lowfees","webhookUrl":"https://myCypherApp:3000/batchExecuted"} + // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batchId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"} + + logger.info("CyphernodeClient.addToBatch: %s", batchRequestTO); + + let result: IRespAddToBatch; + const response = await this._post("/addtobatch", batchRequestTO); + + if (response.status >= 200 && response.status < 400) { + result = { result: response.data.result }; + } else { + result = { + error: { + code: response.data.error.code, + message: response.data.error.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespBatchSpend; + } + return result; + } + + async removeFromBatch(outputId: number): Promise { + // POST http://192.168.111.152:8080/removefrombatch + // + // args: + // - outputId, required, id of the output to remove + // + // response: + // - lnurlId, the id of the lnurl + // - outputId, the id of the removed output if found + // - nbOutputs, the number of outputs currently in the batch + // - oldest, the timestamp of the oldest output in the batch + // - total, the current sum of the batch's output amounts + // + // BODY {"id":72} + + logger.info("CyphernodeClient.removeFromBatch: %d", outputId); + + let result: IRespAddToBatch; + const response = await this._post("/removefrombatch", { + outputId, + }); + + if (response.status >= 200 && response.status < 400) { + result = { result: response.data.result }; + } else { + result = { + error: { + code: response.data.error.code, + message: response.data.error.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespBatchSpend; + } + return result; + } + + async getBatchDetails( + batchIdent: IReqGetBatchDetails + ): Promise { + // POST (GET) http://192.168.111.152:8080/getbatchdetails + // + // args: + // - lnurlId, optional, id of the lnurl, overrides lnurlLabel, default lnurl will be spent if not supplied + // - lnurlLabel, optional, label of the lnurl, default lnurl will be used if not supplied + // - txid, optional, if you want the details of an executed batch, supply the batch txid, will return current pending batch + // if not supplied + // + // response: + // {"result":{ + // "lnurlId":34, + // "lnurlLabel":"Special lnurl for a special client", + // "confTarget":6, + // "nbOutputs":83, + // "oldest":123123, + // "total":10.86990143, + // "txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648", + // "hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648", + // "details":{ + // "firstseen":123123, + // "size":424, + // "vsize":371, + // "replaceable":true, + // "fee":0.00004112 + // }, + // "outputs":[ + // "1abc":0.12, + // "3abc":0.66, + // "bc1abc":2.848, + // ... + // ] + // } + // },"error":null} + // + // BODY {} + // BODY {"lnurlId":34} + + logger.info("CyphernodeClient.getBatchDetails: %s", batchIdent); + + let result: IRespGetBatchDetails; + const response = await this._post("/getbatchdetails", batchIdent); + + if (response.status >= 200 && response.status < 400) { + result = { result: response.data.result }; + } else { + result = { + error: { + code: response.data.error.code, + message: response.data.error.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespBatchSpend; + } + return result; + } + + async batchSpend(batchSpendTO: IReqBatchSpend): Promise { + // POST http://192.168.111.152:8080/batchspend + // + // args: + // - lnurlId, optional, id of the lnurl to execute, overrides lnurlLabel, default lnurl will be spent if not supplied + // - lnurlLabel, optional, label of the lnurl to execute, default lnurl will be executed if not supplied + // - confTarget, optional, overrides default value of createlnurl, default to value of createlnurl, default Bitcoin Core conf_target will be used if not supplied + // NOTYET - feeRate, optional, overrides confTarget if supplied, overrides default value of createlnurl, default to value of createlnurl, default Bitcoin Core value will be used if not supplied + // + // response: + // - txid, the transaction txid + // - hash, the transaction hash + // - nbOutputs, the number of outputs spent in the batch + // - oldest, the timestamp of the oldest output in the spent batch + // - total, the sum of the spent batch's output amounts + // - tx details: size, vsize, replaceable, fee + // - outputs + // + // {"result":{ + // "lnurlId":34, + // "lnurlLabel":"Special lnurl for a special client", + // "confTarget":6, + // "nbOutputs":83, + // "oldest":123123, + // "total":10.86990143, + // "txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648", + // "hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648", + // "details":{ + // "firstseen":123123, + // "size":424, + // "vsize":371, + // "replaceable":true, + // "fee":0.00004112 + // }, + // "outputs":{ + // "1abc":0.12, + // "3abc":0.66, + // "bc1abc":2.848, + // ... + // } + // } + // },"error":null} + // + // BODY {} + // BODY {"lnurlId":34,"confTarget":12} + // NOTYET BODY {"lnurlLabel":"highfees","feeRate":233.7} + // BODY {"lnurlId":411,"confTarget":6} + + logger.info("CyphernodeClient.batchSpend: %s", batchSpendTO); + + let result: IRespBatchSpend; + const response = await this._post("/batchspend", batchSpendTO); + if (response.status >= 200 && response.status < 400) { + result = { result: response.data.result }; + } else { + result = { + error: { + code: response.data.error.code, + message: response.data.error.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespBatchSpend; + } + return result; + } + + async spend(spendTO: IReqSpend): Promise { + // POST http://192.168.111.152:8080/spend + // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"confTarget":6,"replaceable":true,"subtractfeefromamount":false} + + // args: + // - address, required, desination address + // - amount, required, amount to send to the destination address + // - confTarget, optional, overrides default value, default Bitcoin Core conf_target will be used if not supplied + // - replaceable, optional, overrides default value, default Bitcoin Core walletrbf will be used if not supplied + // - subtractfeefromamount, optional, if true will subtract fee from the amount sent instead of adding to it + // + // response: + // - txid, the transaction txid + // - hash, the transaction hash + // - tx details: address, aount, firstseen, size, vsize, replaceable, fee, subtractfeefromamount + // + // {"result":{ + // "status":"accepted", + // "txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648", + // "hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648", + // "details":{ + // "address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp", + // "amount":0.00233, + // "firstseen":123123, + // "size":424, + // "vsize":371, + // "replaceable":true, + // "fee":0.00004112, + // "subtractfeefromamount":true + // } + // } + // },"error":null} + // + // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} + + logger.info("CyphernodeClient.spend: %s", spendTO); + + let result: IRespSpend; + const response = await this._post("/spend", spendTO); + if (response.status >= 200 && response.status < 400) { + result = { result: response.data }; + } else { + result = { + error: { + code: ErrorCodes.InternalError, + message: response.data.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespSpend; + } + return result; + } +} + +export { CyphernodeClient }; diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts new file mode 100644 index 0000000..32369b4 --- /dev/null +++ b/src/lib/HttpServer.ts @@ -0,0 +1,161 @@ +// lib/HttpServer.ts +import express from "express"; +import logger from "./Log2File"; +import AsyncLock from "async-lock"; +import LnurlConfig from "../config/LnurlConfig"; +import fs from "fs"; +import { LnurlWithdraw } from "./LnurlWithdraw"; +import { + IResponseMessage, + ErrorCodes, +} from "../types/jsonrpc/IResponseMessage"; +import { IRequestMessage } from "../types/jsonrpc/IRequestMessage"; +import { Utils } from "./Utils"; +import IRespCreateLnurlWithdraw from "../types/IRespCreateLnurlWithdraw"; +import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; +import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; + +class HttpServer { + // Create a new express application instance + private readonly _httpServer: express.Application = express(); + private readonly _lock = new AsyncLock(); + private _lnurlConfig: LnurlConfig = JSON.parse( + fs.readFileSync("data/config.json", "utf8") + ); + private _lnurlWithdraw: LnurlWithdraw = new LnurlWithdraw(this._lnurlConfig); + + setup(): void { + logger.debug("setup"); + this._httpServer.use(express.json()); + } + + async loadConfig(): Promise { + logger.debug("loadConfig"); + + this._lnurlConfig = JSON.parse( + fs.readFileSync("data/config.json", "utf8") + ); + + this._lnurlWithdraw.configureLnurl(this._lnurlConfig); + } + + async createLnurlWithdraw( + params: object | undefined + ): Promise { + logger.debug("/createLnurlWithdraw params:", params); + + const reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw = params as IReqCreateLnurlWithdraw; + + return await this._lnurlWithdraw.createLnurlWithdraw(reqCreateLnurlWithdraw); + } + + async start(): Promise { + logger.info("Starting incredible service"); + + this.setup(); + + this._httpServer.post(this._lnurlConfig.URL_API_CTX, async (req, res) => { + logger.debug(this._lnurlConfig.URL_API_CTX); + + const reqMessage: IRequestMessage = req.body; + logger.debug("reqMessage.method:", reqMessage.method); + logger.debug("reqMessage.params:", reqMessage.params); + + const response: IResponseMessage = { + id: reqMessage.id, + } as IResponseMessage; + + // Check the method and call the corresponding function + switch (reqMessage.method) { + + case "createLnurlWithdraw": { + const result: IRespCreateLnurlWithdraw = await this.createLnurlWithdraw( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + + case "encodeBech32": { + response.result = await Utils.encodeBech32((reqMessage.params as any).s); + break; + } + + case "decodeBech32": { + response.result = await Utils.decodeBech32((reqMessage.params as any).s); + break; + } + + case "reloadConfig": + await this.loadConfig(); + + // eslint-disable-next-line no-fallthrough + case "getConfig": + response.result = this._lnurlConfig; + break; + + default: + response.error = { + code: ErrorCodes.MethodNotFound, + message: "No such method!", + }; + break; + } + + if (response.error) { + response.error.data = reqMessage.params as never; + res.status(400).json(response); + } else { + res.status(200).json(response); + } + }); + + // LN Service LNURL Withdraw Request + this._httpServer.get( + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX, + async (req, res) => { + logger.info( + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + ":", + req.query + ); + + const response = await this._lnurlWithdraw.processLnurlWithdrawRequest(req.query.s as string); + + if (response.status) { + res.status(400).json(response); + } else { + res.status(200).json(response); + } + } + ); + + // LN Service LNURL Withdraw + this._httpServer.get( + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, + async (req, res) => { + logger.info( + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX + ":", + req.query + ); + + const response = await this._lnurlWithdraw.processLnurlWithdraw({ k1: req.query.k1, pr: req.query.pr, balanceNotify: req.query.balanceNotify } as IReqLnurlWithdraw); + + if (response.status === "ERROR") { + res.status(400).json(response); + } else { + res.status(200).json(response); + } + } + ); + + this._httpServer.listen(this._lnurlConfig.URL_API_PORT, () => { + logger.info( + "Express HTTP server listening on port:", + this._lnurlConfig.URL_API_PORT + ); + }); + } +} + +export { HttpServer }; diff --git a/src/lib/LnurlDB.ts b/src/lib/LnurlDB.ts new file mode 100644 index 0000000..8783b0d --- /dev/null +++ b/src/lib/LnurlDB.ts @@ -0,0 +1,57 @@ +import logger from "./Log2File"; +import path from "path"; +import LnurlConfig from "../config/LnurlConfig"; +import { Connection, createConnection, IsNull } from "typeorm"; +import { LnurlWithdrawRequest } from "../entity/LnurlWithdrawRequest"; + +class LnurlDB { + private _db?: Connection; + + constructor(lnurlConfig: LnurlConfig) { + this.configureDB(lnurlConfig); + } + + async configureDB(lnurlConfig: LnurlConfig): Promise { + logger.info("LnurlDB.configureDB", lnurlConfig); + + if (this._db?.isConnected) { + await this._db.close(); + } + this._db = await this.initDatabase( + path.resolve( + lnurlConfig.BASE_DIR, + lnurlConfig.DATA_DIR, + lnurlConfig.DB_NAME + ) + ); + } + + async initDatabase(dbName: string): Promise { + logger.info("LnurlDB.initDatabase", dbName); + + return await createConnection({ + type: "sqlite", + database: dbName, + entities: [LnurlWithdrawRequest], + synchronize: true, + logging: true, + }); + } + + async saveLnurlWithdrawRequest(lnurlWithdrawRequest: LnurlWithdrawRequest): Promise { + const lwr = await this._db?.manager.getRepository(LnurlWithdrawRequest).save(lnurlWithdrawRequest); + + return lwr as LnurlWithdrawRequest; + } + + async getLnurlWithdrawRequestBySecret(secretToken: string): Promise { + const wr = await this._db?.manager + .getRepository(LnurlWithdrawRequest) + .findOne({ where: { secretToken } }); + + return wr as LnurlWithdrawRequest; + } + +} + +export { LnurlDB }; diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts new file mode 100644 index 0000000..74c94b3 --- /dev/null +++ b/src/lib/LnurlWithdraw.ts @@ -0,0 +1,121 @@ +import logger from "./Log2File"; +import LnurlConfig from "../config/LnurlConfig"; +import { CyphernodeClient } from "./CyphernodeClient"; +import { LnurlDB } from "./LnurlDB"; +import { + ErrorCodes, + IResponseMessage, +} from "../types/jsonrpc/IResponseMessage"; +import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; +import IRespCreateLnurlWithdraw from "../types/IRespCreateLnurlWithdraw"; +import { CreateLnurlWithdrawValidator } from "../validators/CreateLnurlWithdrawValidator"; +import { LnurlWithdrawRequest } from "../entity/LnurlWithdrawRequest"; +import IRespLnserviceWithdrawRequest from "../types/IRespLnserviceWithdrawRequest"; +import IRespLnserviceStatus from "../types/IRespLnserviceStatus"; +import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; + +class LnurlWithdraw { + private _lnurlConfig: LnurlConfig; + private _cyphernodeClient: CyphernodeClient; + private _lnurlDB: LnurlDB; + + constructor(lnurlConfig: LnurlConfig) { + this._lnurlConfig = lnurlConfig; + this._cyphernodeClient = new CyphernodeClient(this._lnurlConfig); + this._lnurlDB = new LnurlDB(this._lnurlConfig); + } + + configureLnurl(lnurlConfig: LnurlConfig): void { + this._lnurlConfig = lnurlConfig; + this._lnurlDB.configureDB(this._lnurlConfig).then(() => { + this._cyphernodeClient.configureCyphernode(this._lnurlConfig); + }); + } + + async createLnurlWithdraw( + reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw + ): Promise { + logger.info( + "LnurlWithdraw.createLnurlWithdraw, reqCreateLnurlWithdraw:", + reqCreateLnurlWithdraw + ); + + const response: IRespCreateLnurlWithdraw = {}; + + if (CreateLnurlWithdrawValidator.validateRequest(reqCreateLnurlWithdraw)) { + // Inputs are valid. + logger.debug("LnurlWithdraw.createLnurlWithdraw, Inputs are valid."); + + let lnurlWithdrawRequest: LnurlWithdrawRequest; + let lnurl = this._lnurlConfig.LN_SERVICE_SERVER + ":" + this._lnurlConfig.LN_SERVICE_PORT + this._lnurlConfig.LN_SERVICE_CTX + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + "?s=" + reqCreateLnurlWithdraw.secretToken; + + lnurlWithdrawRequest = await this._lnurlDB.saveLnurlWithdrawRequest( + Object.assign(reqCreateLnurlWithdraw as LnurlWithdrawRequest, { lnurl: lnurl }) + ); + + if (lnurlWithdrawRequest) { + logger.debug("LnurlWithdraw.createLnurlWithdraw, lnurlWithdrawRequest created."); + + response.result = lnurlWithdrawRequest; + } else { + // LnurlWithdrawRequest not created + logger.debug("LnurlWithdraw.createLnurlWithdraw, LnurlWithdrawRequest not created."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdrawRequest not created", + }; + } + } else { + // There is an error with inputs + logger.debug("LnurlWithdraw.createLnurlWithdraw, there is an error with inputs."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + + async processLnurlWithdrawRequest(secretToken: string): Promise { + logger.info("LnurlWithdraw.processLnurlWithdrawRequest:", secretToken); + + let result: IRespLnserviceWithdrawRequest; + const lnurlWithdrawRequest = await this._lnurlDB.getLnurlWithdrawRequestBySecret(secretToken); + logger.debug("lnurlWithdrawRequest:", lnurlWithdrawRequest); + + if (lnurlWithdrawRequest != null && lnurlWithdrawRequest.active) { + result = { + tag: "withdrawRequest", + callback: this._lnurlConfig.LN_SERVICE_SERVER + ":" + this._lnurlConfig.LN_SERVICE_PORT + this._lnurlConfig.LN_SERVICE_CTX + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, + k1: lnurlWithdrawRequest.secretToken, + defaultDescription: lnurlWithdrawRequest.description, + minWithdrawable: lnurlWithdrawRequest.amount, + maxWithdrawable: lnurlWithdrawRequest.amount + } + } else { + result = { status: "ERROR", reason: "Invalid k1 value" }; + } + + return result; + } + + async processLnurlWithdraw(params: IReqLnurlWithdraw): Promise { + logger.info("LnurlWithdraw.processLnurlWithdraw:", params); + + let result: IRespLnserviceStatus; + const lnurlWithdrawRequest = await this._lnurlDB.getLnurlWithdrawRequestBySecret(params.k1); + + if (lnurlWithdrawRequest != null && lnurlWithdrawRequest.active) { + result = { status: "OK" }; + } else { + result = { status: "ERROR", reason: "Invalid k1 value" }; + } + + return result; + } +} + +export { LnurlWithdraw }; diff --git a/src/lib/Log2File.ts b/src/lib/Log2File.ts new file mode 100644 index 0000000..76ca6be --- /dev/null +++ b/src/lib/Log2File.ts @@ -0,0 +1,22 @@ +import { ILogObject, Logger } from "tslog"; +import { appendFileSync } from "fs"; + +function logToTransport(logObject: ILogObject) { + appendFileSync("logs/lnurl.log", JSON.stringify(logObject) + "\n"); +} + +const logger = new Logger(); +logger.attachTransport( + { + silly: logToTransport, + debug: logToTransport, + trace: logToTransport, + info: logToTransport, + warn: logToTransport, + error: logToTransport, + fatal: logToTransport, + }, + "debug" +); + +export default logger; diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts new file mode 100644 index 0000000..eb52294 --- /dev/null +++ b/src/lib/Utils.ts @@ -0,0 +1,100 @@ +import logger from "./Log2File"; +import axios, { AxiosRequestConfig } from "axios"; +import { bech32 } from "bech32" + +class Utils { + static async post( + url: string, + postdata: unknown, + addedOptions?: unknown + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + logger.info( + "Utils.post %s %s %s", + url, + JSON.stringify(postdata), + addedOptions + ); + + let configs: AxiosRequestConfig = { + baseURL: url, + method: "post", + data: postdata, + }; + if (addedOptions) { + configs = Object.assign(configs, addedOptions); + } + + try { + const response = await axios.request(configs); + logger.debug( + "Utils.post :: response.data = %s", + JSON.stringify(response.data) + ); + + return { status: response.status, data: response.data }; + } catch (error) { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.info( + "Utils.post :: error.response.data = %s", + JSON.stringify(error.response.data) + ); + logger.info( + "Utils.post :: error.response.status = %d", + error.response.status + ); + logger.info( + "Utils.post :: error.response.headers = %s", + error.response.headers + ); + + return { status: error.response.status, data: error.response.data }; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.info("Utils.post :: error.message = %s", error.message); + + return { status: -1, data: error.message }; + } else { + // Something happened in setting up the request that triggered an Error + logger.info("Utils.post :: Error: %s", error.message); + + return { status: -2, data: error.message }; + } + } + } + + static async encodeBech32( + str: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + logger.info( + "Utils.encodeBech32:", + str + ); + + let lnurlBech32 = bech32.encode("LNURL", bech32.toWords(Buffer.from(str, 'utf8')), 2000); + logger.debug("lnurlBech32:", lnurlBech32); + + return lnurlBech32.toUpperCase(); + } + + static async decodeBech32( + str: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + logger.info( + "Utils.decodeBech32:", + str + ); + + let lnurl = Buffer.from(bech32.fromWords(bech32.decode(str, 2000).words)).toString(); + + return lnurl; + } +} + +export { Utils }; diff --git a/src/types/IReqCreateLnurlWithdraw.ts b/src/types/IReqCreateLnurlWithdraw.ts new file mode 100644 index 0000000..fe47129 --- /dev/null +++ b/src/types/IReqCreateLnurlWithdraw.ts @@ -0,0 +1,7 @@ +export default interface IReqCreateLnurlWithdraw { + amount: number; + description?: string; + expiration?: Date; + secretToken: string; + webhookUrl?: string; +} diff --git a/src/types/IReqLnurlWithdraw.ts b/src/types/IReqLnurlWithdraw.ts new file mode 100644 index 0000000..9959b96 --- /dev/null +++ b/src/types/IReqLnurlWithdraw.ts @@ -0,0 +1,5 @@ +export default interface IReqLnurlWithdraw { + k1: string; + pr: string; + balanceNotify?: string; +} diff --git a/src/types/IRespCreateLnurlWithdraw.ts b/src/types/IRespCreateLnurlWithdraw.ts new file mode 100644 index 0000000..c137aa2 --- /dev/null +++ b/src/types/IRespCreateLnurlWithdraw.ts @@ -0,0 +1,7 @@ +import { IResponseError } from "./jsonrpc/IResponseMessage"; +import { LnurlWithdrawRequest } from "../entity/LnurlWithdrawRequest"; + +export default interface IRespCreateLnurlWithdraw { + result?: LnurlWithdrawRequest; + error?: IResponseError; +} diff --git a/src/types/IRespLnserviceStatus.ts b/src/types/IRespLnserviceStatus.ts new file mode 100644 index 0000000..8524cac --- /dev/null +++ b/src/types/IRespLnserviceStatus.ts @@ -0,0 +1,4 @@ +export default interface IRespLnserviceStatus { + status?: string; + reason?: string; +} diff --git a/src/types/IRespLnserviceWithdrawRequest.ts b/src/types/IRespLnserviceWithdrawRequest.ts new file mode 100644 index 0000000..e329142 --- /dev/null +++ b/src/types/IRespLnserviceWithdrawRequest.ts @@ -0,0 +1,11 @@ +import IRespLnserviceStatus from "./IRespLnserviceStatus"; + +export default interface IRespLnserviceWithdrawRequest extends IRespLnserviceStatus { + tag?: string; + callback?: string; + k1?: string; + defaultDescription?: string; + minWithdrawable?: number; + maxWithdrawable?: number; + balanceCheck?: string; +} diff --git a/src/types/cyphernode/IAddToBatchResult.ts b/src/types/cyphernode/IAddToBatchResult.ts new file mode 100644 index 0000000..f914f77 --- /dev/null +++ b/src/types/cyphernode/IAddToBatchResult.ts @@ -0,0 +1,14 @@ +import IBatcherIdent from "./IBatcherIdent"; +import IOutput from "./IOutput"; +import IBatchState from "./IBatchState"; + +export default interface IAddToBatchResult + extends IBatcherIdent, + IOutput, + IBatchState { + // - batcherId, the id of the batcher + // - outputId, the id of the added output + // - nbOutputs, the number of outputs currently in the batch + // - oldest, the timestamp of the oldest output in the batch + // - total, the current sum of the batch's output amounts +} diff --git a/src/types/cyphernode/IBatchDetails.ts b/src/types/cyphernode/IBatchDetails.ts new file mode 100644 index 0000000..76c9702 --- /dev/null +++ b/src/types/cyphernode/IBatchDetails.ts @@ -0,0 +1,26 @@ +import IBatcher from "./IBatcher"; +import IBatchTx from "./IBatchTx"; + +export default interface IBatchDetails extends IBatcher, IBatchTx { + // "batcherId":34, + // "batcherLabel":"Special batcher for a special client", + // "confTarget":6, + // "nbOutputs":83, + // "oldest":123123, + // "total":10.86990143, + // "txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648", + // "hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648", + // "details":{ + // "firstseen":123123, + // "size":424, + // "vsize":371, + // "replaceable":true, + // "fee":0.00004112 + // }, + // "outputs":[ + // "1abc":0.12, + // "3abc":0.66, + // "bc1abc":2.848, + // ... + // ] +} diff --git a/src/types/cyphernode/IBatchState.ts b/src/types/cyphernode/IBatchState.ts new file mode 100644 index 0000000..c7719fe --- /dev/null +++ b/src/types/cyphernode/IBatchState.ts @@ -0,0 +1,5 @@ +export default interface IBatchState { + nbOutputs?: number; + oldest?: Date; + total?: number; +} diff --git a/src/types/cyphernode/IBatchTx.ts b/src/types/cyphernode/IBatchTx.ts new file mode 100644 index 0000000..de7a276 --- /dev/null +++ b/src/types/cyphernode/IBatchTx.ts @@ -0,0 +1,12 @@ +export default interface IBatchTx { + txid?: string; + hash?: string; + details?: { + firstseen: Date; + size: number; + vsize: number; + replaceable: boolean; + fee: number; + }; + outputs?: []; +} diff --git a/src/types/cyphernode/IBatcher.ts b/src/types/cyphernode/IBatcher.ts new file mode 100644 index 0000000..4179e8b --- /dev/null +++ b/src/types/cyphernode/IBatcher.ts @@ -0,0 +1,13 @@ +import IBatcherIdent from "./IBatcherIdent"; +import IBatchState from "./IBatchState"; + +export default interface IBatcher extends IBatcherIdent, IBatchState { + // "batcherId":1, + // "batcherLabel":"default", + // "confTarget":6, + // "nbOutputs":12, + // "oldest":123123, + // "total":0.86990143 + + confTarget?: number; +} diff --git a/src/types/cyphernode/IBatcherIdent.ts b/src/types/cyphernode/IBatcherIdent.ts new file mode 100644 index 0000000..0531813 --- /dev/null +++ b/src/types/cyphernode/IBatcherIdent.ts @@ -0,0 +1,4 @@ +export default interface IBatcherIdent { + batcherId?: number; + batcherLabel?: string; +} diff --git a/src/types/cyphernode/IOutput.ts b/src/types/cyphernode/IOutput.ts new file mode 100644 index 0000000..05caed2 --- /dev/null +++ b/src/types/cyphernode/IOutput.ts @@ -0,0 +1,3 @@ +export default interface IOutput { + outputId?: number; +} diff --git a/src/types/cyphernode/IReqAddToBatch.ts b/src/types/cyphernode/IReqAddToBatch.ts new file mode 100644 index 0000000..20a6915 --- /dev/null +++ b/src/types/cyphernode/IReqAddToBatch.ts @@ -0,0 +1,15 @@ +import IBatcherIdent from "./IBatcherIdent"; + +export default interface IReqAddToBatch extends IBatcherIdent { + // - address, required, desination address + // - amount, required, amount to send to the destination address + // - outputLabel, optional, if you want to reference this output + // - batcherId, optional, the id of the batcher to which the output will be added, default batcher if not supplied, overrides batcherLabel + // - batcherLabel, optional, the label of the batcher to which the output will be added, default batcher if not supplied + // - webhookUrl, optional, the webhook to call when the batch is broadcast + + address: string; + amount: number; + outputLabel?: string; + webhookUrl?: string; +} diff --git a/src/types/cyphernode/IReqBatchSpend.ts b/src/types/cyphernode/IReqBatchSpend.ts new file mode 100644 index 0000000..19c7221 --- /dev/null +++ b/src/types/cyphernode/IReqBatchSpend.ts @@ -0,0 +1,9 @@ +import IBatcherIdent from "./IBatcherIdent"; + +export default interface IReqBatchSpend extends IBatcherIdent { + // - batcherId, optional, id of the batcher to execute, overrides batcherLabel, default batcher will be spent if not supplied + // - batcherLabel, optional, label of the batcher to execute, default batcher will be executed if not supplied + // - confTarget, optional, overrides default value of createbatcher, default to value of createbatcher, default Bitcoin Core conf_target will be used if not supplied + + confTarget?: number; +} diff --git a/src/types/cyphernode/IReqGetBatchDetails.ts b/src/types/cyphernode/IReqGetBatchDetails.ts new file mode 100644 index 0000000..b168829 --- /dev/null +++ b/src/types/cyphernode/IReqGetBatchDetails.ts @@ -0,0 +1,10 @@ +import IBatcherIdent from "./IBatcherIdent"; + +export default interface IReqGetBatchDetails extends IBatcherIdent { + // - batcherId, optional, id of the batcher, overrides batcherLabel, default batcher will be spent if not supplied + // - batcherLabel, optional, label of the batcher, default batcher will be used if not supplied + // - txid, optional, if you want the details of an executed batch, supply the batch txid, will return current pending batch + // if not supplied + + txid?: string; +} diff --git a/src/types/cyphernode/IReqSpend.ts b/src/types/cyphernode/IReqSpend.ts new file mode 100644 index 0000000..13f0a92 --- /dev/null +++ b/src/types/cyphernode/IReqSpend.ts @@ -0,0 +1,10 @@ +export default interface IReqSpend { + // - address, required, desination address + // - amount, required, amount to send to the destination address + + address: string; + amount: number; + confTarget?: number; + replaceable?: boolean; + subtractfeefromamount?: boolean; +} diff --git a/src/types/cyphernode/IRespAddToBatch.ts b/src/types/cyphernode/IRespAddToBatch.ts new file mode 100644 index 0000000..f84e63a --- /dev/null +++ b/src/types/cyphernode/IRespAddToBatch.ts @@ -0,0 +1,7 @@ +import { IResponseError } from "../jsonrpc/IResponseMessage"; +import IAddToBatchResult from "./IAddToBatchResult"; + +export default interface IRespAddToBatch { + result?: IAddToBatchResult; + error?: IResponseError; +} diff --git a/src/types/cyphernode/IRespBatchSpend.ts b/src/types/cyphernode/IRespBatchSpend.ts new file mode 100644 index 0000000..2eaba17 --- /dev/null +++ b/src/types/cyphernode/IRespBatchSpend.ts @@ -0,0 +1,7 @@ +import IBatchDetails from "./IBatchDetails"; +import { IResponseError } from "../jsonrpc/IResponseMessage"; + +export default interface IRespBatchSpend { + result?: IBatchDetails; + error?: IResponseError; +} diff --git a/src/types/cyphernode/IRespGetBatchDetails.ts b/src/types/cyphernode/IRespGetBatchDetails.ts new file mode 100644 index 0000000..0009f2d --- /dev/null +++ b/src/types/cyphernode/IRespGetBatchDetails.ts @@ -0,0 +1,7 @@ +import IBatchDetails from "./IBatchDetails"; +import { IResponseError } from "../jsonrpc/IResponseMessage"; + +export default interface IRespGetBatchDetails { + result?: IBatchDetails; + error?: IResponseError; +} diff --git a/src/types/cyphernode/IRespGetBatcher.ts b/src/types/cyphernode/IRespGetBatcher.ts new file mode 100644 index 0000000..dfaf215 --- /dev/null +++ b/src/types/cyphernode/IRespGetBatcher.ts @@ -0,0 +1,7 @@ +import IBatcher from "./IBatcher"; +import { IResponseError } from "../jsonrpc/IResponseMessage"; + +export default interface IRespGetBatcher { + result?: IBatcher; + error?: IResponseError; +} diff --git a/src/types/cyphernode/IRespSpend.ts b/src/types/cyphernode/IRespSpend.ts new file mode 100644 index 0000000..ab14010 --- /dev/null +++ b/src/types/cyphernode/IRespSpend.ts @@ -0,0 +1,7 @@ +import { IResponseError } from "../jsonrpc/IResponseMessage"; +import ITx from "./ITx"; + +export default interface IRespSpend { + result?: ITx; + error?: IResponseError; +} diff --git a/src/types/cyphernode/ITx.ts b/src/types/cyphernode/ITx.ts new file mode 100644 index 0000000..a93d77b --- /dev/null +++ b/src/types/cyphernode/ITx.ts @@ -0,0 +1,14 @@ +export default interface ITx { + txid?: string; + hash?: string; + details?: { + address: string; + amount: number; + firstseen: Date; + size: number; + vsize: number; + replaceable: boolean; + fee: number; + subtractfeefromamount: boolean; + }; +} diff --git a/src/types/jsonrpc/IMessage.ts b/src/types/jsonrpc/IMessage.ts new file mode 100644 index 0000000..fd65f19 --- /dev/null +++ b/src/types/jsonrpc/IMessage.ts @@ -0,0 +1,3 @@ +export default interface IMessage { + jsonrpc: string; +} diff --git a/src/types/jsonrpc/IRequestMessage.ts b/src/types/jsonrpc/IRequestMessage.ts new file mode 100644 index 0000000..5499517 --- /dev/null +++ b/src/types/jsonrpc/IRequestMessage.ts @@ -0,0 +1,18 @@ +import IMessage from "./IMessage"; + +export interface IRequestMessage extends IMessage { + /** + * The request id. + */ + id: number | string; + + /** + * The method to be invoked. + */ + method: string; + + /** + * The method's params. + */ + params?: Array | object; +} diff --git a/src/types/jsonrpc/IResponseMessage.ts b/src/types/jsonrpc/IResponseMessage.ts new file mode 100644 index 0000000..3183d8b --- /dev/null +++ b/src/types/jsonrpc/IResponseMessage.ts @@ -0,0 +1,55 @@ +import IMessage from "./IMessage"; + +export interface IResponseMessage extends IMessage { + /** + * The request id. + */ + id: number | string | null; + + /** + * The result of a request. This member is REQUIRED on success. + * This member MUST NOT exist if there was an error invoking the method. + */ + result?: string | number | boolean | object | null; + + /** + * The error object in case a request fails. + */ + error?: IResponseError; +} + +export interface IResponseError { + /** + * A number indicating the error type that occurred. + */ + code: number; + + /** + * A string providing a short description of the error. + */ + message: string; + + /** + * A Primitive or Structured value that contains additional + * information about the error. Can be omitted. + */ + data?: D; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace ErrorCodes { + // Defined by JSON RPC + export const ParseError = -32700; + export const InvalidRequest = -32600; + export const MethodNotFound = -32601; + export const InvalidParams = -32602; + export const InternalError = -32603; + export const serverErrorStart = -32099; + export const serverErrorEnd = -32000; + export const ServerNotInitialized = -32002; + export const UnknownErrorCode = -32001; + + // Defined by the protocol. + export const RequestCancelled = -32800; + export const ContentModified = -32801; +} diff --git a/src/validators/CreateLnurlWithdrawValidator.ts b/src/validators/CreateLnurlWithdrawValidator.ts new file mode 100644 index 0000000..7a3276c --- /dev/null +++ b/src/validators/CreateLnurlWithdrawValidator.ts @@ -0,0 +1,13 @@ +import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; + +class CreateLnurlWithdrawValidator { + static validateRequest(request: IReqCreateLnurlWithdraw): boolean { + if (request.amount && request.secretToken) { + return true; + } else { + return false; + } + } +} + +export { CreateLnurlWithdrawValidator }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f1e8f53 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,70 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": ["ES6"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./build", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "resolveJsonModule": true, + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} From 3b6012a8343fa4795dc7864badc7f58de211e8a6 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 16 Jul 2021 23:31:36 -0400 Subject: [PATCH 02/52] First successful end2end withdraw, tests --- .gitignore | 1 + README.md | 24 ++- cypherapps/data/config.json | 2 +- src/config/lnurl.sql | 4 +- ...hdrawRequest.ts => LnurlWithdrawEntity.ts} | 14 +- src/lib/CyphernodeClient.ts | 121 ++++++++++----- src/lib/HttpServer.ts | 47 ++++-- src/lib/LnurlDB.ts | 25 ++-- src/lib/LnurlWithdraw.ts | 140 +++++++++++++----- src/lib/Log2File.ts | 4 +- src/lib/Utils.ts | 32 ++-- src/types/IRespCreateLnurlWithdraw.ts | 7 - src/types/IRespLnserviceStatus.ts | 2 +- src/types/IRespLnserviceWithdrawRequest.ts | 5 +- src/types/IRespLnurlWithdraw.ts | 7 + src/types/cyphernode/IReqLnPay.ts | 9 ++ src/types/cyphernode/IRespLnPay.ts | 6 + src/validators/LnServiceWithdrawValidator.ts | 13 ++ tests/README.md | 101 +++++++++++++ tests/colors.sh | 74 +++++++++ tests/ln_reconnect.sh | 3 + tests/ln_setup.sh | 49 ++++++ tests/lnurl_withdraw.sh | 113 ++++++++++++++ tests/mine.sh | 12 ++ tests/run_tests.sh | 5 + tests/startcallbackserver.sh | 9 ++ 26 files changed, 687 insertions(+), 142 deletions(-) rename src/entity/{LnurlWithdrawRequest.ts => LnurlWithdrawEntity.ts} (85%) delete mode 100644 src/types/IRespCreateLnurlWithdraw.ts create mode 100644 src/types/IRespLnurlWithdraw.ts create mode 100644 src/types/cyphernode/IReqLnPay.ts create mode 100644 src/types/cyphernode/IRespLnPay.ts create mode 100644 src/validators/LnServiceWithdrawValidator.ts create mode 100644 tests/README.md create mode 100644 tests/colors.sh create mode 100755 tests/ln_reconnect.sh create mode 100755 tests/ln_setup.sh create mode 100755 tests/lnurl_withdraw.sh create mode 100755 tests/mine.sh create mode 100755 tests/run_tests.sh create mode 100755 tests/startcallbackserver.sh diff --git a/.gitignore b/.gitignore index bda903c..2bd8cd3 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ build data !cypherapps/data *.sqlite +*.pem diff --git a/README.md b/README.md index 5418782..83770b6 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ kexkey@AlcodaZ-2 ~ % docker exec -it lnurl ash -sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw_request" +sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" amount: number; description?: string; @@ -93,10 +93,22 @@ sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw_request" secretToken: string; webhookUrl?: string; -curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"amount":0.01,"description":"desc01","expiration":"2021-07-15 12:12","secretToken":"abc01","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api -{"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15 12:12","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"http://.onion/lnurlWithdraw?s=abc01","withdrawnDetails":null,"withdrawnTimestamp":null,"lnurlWithdrawId":1,"createdAt":"2021-07-15 15:42:43","updatedAt":"2021-07-15 15:42:43"}} +curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"amount":0.01,"description":"desc02","expiration":"2021-07-15 12:12","secretToken":"abc02","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15 12:12","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} -sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw_request" -id|amount|description|expiration|secret_token|webhook_url|lnurl|withdrawn_details|withdrawn_ts|created_ts|updated_ts -1|0.01|desc01|2021-07-15 12:12|abc01|https://webhookUrl01|http://.onion/lnurlWithdraw?s=abc01|||2021-07-15 15:42:43|2021-07-15 15:42:43 +sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" +id|amount|description|expiration|secret_token|webhook_url|lnurl|withdrawn_details|withdrawn_ts|active|created_ts|updated_ts +1|0.01|desc01|2021-07-15 12:12|abc01|https://webhookUrl01|LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K|||1|2021-07-15 19:42:06|2021-07-15 19:42:06 +curl -d '{"id":0,"method":"decodeBech32","params":{"s":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXGXE8Q93"}}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":"http://.onion:80/lnurl/withdrawRequest?s=abc01"} + +curl localhost:8000/withdrawRequest?s=abc02 +{"tag":"withdrawRequest","callback":"http://.onion:80/lnurl/withdraw","k1":"abc01","defaultDescription":"desc01","minWithdrawable":0.01,"maxWithdrawable":0.01} + +curl localhost:8000/withdraw?k1=abc03\&pr=lnbcrt123456780p1ps0pf5ypp5lfskvgsdef4hpx0lndqe69ypu0rxl5msndkcnlm8v6l5p75xzd6sdq2v3jhxcesxvxqyjw5qcqp2sp5f42mrc40eh4ntmqhgxvk74m2w3q25fx9m8d9wn6d20ahtfy6ju8q9qy9qsqw4khcr86dlg66nz3ds6nhxpsw9z0ugxfkequtyf8qv7q6gdvztdhsfp36uazsz35xp37lfmt0tqsssrew0wr0htfdkhjpwdagnzvc6qp2ynxvd +{"status":"OK"} + +================== + +docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" --network cyphernodeappsnet --entrypoint ash lnurl diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json index 2ed7615..41ad9ab 100644 --- a/cypherapps/data/config.json +++ b/cypherapps/data/config.json @@ -9,7 +9,7 @@ "SESSION_TIMEOUT": 600, "CN_URL": "https://gatekeeper:2009/v0", "CN_API_ID": "003", - "CN_API_KEY": "39b83c35972aeb81a242bfe189dc0a22da5ac6cbb64072b492f2d46519a97618", + "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", "LN_SERVICE_SERVER": "http://.onion", "LN_SERVICE_PORT": 80, "LN_SERVICE_CTX": "/lnurl", diff --git a/src/config/lnurl.sql b/src/config/lnurl.sql index 6749e10..d45a575 100644 --- a/src/config/lnurl.sql +++ b/src/config/lnurl.sql @@ -1,6 +1,6 @@ PRAGMA foreign_keys = ON; -CREATE TABLE lnurl_withdraw_request ( +CREATE TABLE lnurl_withdraw ( id INTEGER PRIMARY KEY AUTOINCREMENT, amount REAL, description TEXT, @@ -8,6 +8,7 @@ CREATE TABLE lnurl_withdraw_request ( secret_token TEXT UNIQUE, webhook_url TEXT, lnurl TEXT, + bolt11 TEXT, withdrawn_details TEXT, withdrawn_ts INTEGER, active INTEGER DEFAULT TRUE, @@ -16,3 +17,4 @@ CREATE TABLE lnurl_withdraw_request ( ); CREATE INDEX idx_lnurl_withdraw_description ON lnurl_withdraw (description); CREATE INDEX idx_lnurl_withdraw_lnurl ON lnurl_withdraw (lnurl); +CREATE INDEX idx_lnurl_withdraw_bolt11 ON lnurl_withdraw (bolt11); diff --git a/src/entity/LnurlWithdrawRequest.ts b/src/entity/LnurlWithdrawEntity.ts similarity index 85% rename from src/entity/LnurlWithdrawRequest.ts rename to src/entity/LnurlWithdrawEntity.ts index 872c2ef..2f54c60 100644 --- a/src/entity/LnurlWithdrawRequest.ts +++ b/src/entity/LnurlWithdrawEntity.ts @@ -2,14 +2,12 @@ import { Entity, Column, PrimaryGeneratedColumn, - OneToMany, Index, CreateDateColumn, UpdateDateColumn, - Unique, } from "typeorm"; -// CREATE TABLE lnurl_withdraw_request ( +// CREATE TABLE lnurl_withdraw ( // id INTEGER PRIMARY KEY AUTOINCREMENT, // amount REAL, // description TEXT, @@ -17,6 +15,7 @@ import { // secret_token TEXT UNIQUE, // webhook_url TEXT, // lnurl TEXT, +// bolt11 TEXT, // withdrawn_details TEXT, // withdrawn_ts INTEGER, // active INTEGER, @@ -25,9 +24,10 @@ import { // ); // CREATE INDEX idx_lnurl_withdraw_description ON lnurl_withdraw (description); // CREATE INDEX idx_lnurl_withdraw_lnurl ON lnurl_withdraw (lnurl); +// CREATE INDEX idx_lnurl_withdraw_bolt11 ON lnurl_withdraw (bolt11); -@Entity() -export class LnurlWithdrawRequest { +@Entity("lnurl_withdraw") +export class LnurlWithdrawEntity { @PrimaryGeneratedColumn({ name: "id" }) lnurlWithdrawId!: number; @@ -51,6 +51,10 @@ export class LnurlWithdrawRequest { @Column({ type: "text", name: "lnurl", nullable: true }) lnurl?: string; + @Index("idx_lnurl_withdraw_bolt11") + @Column({ type: "text", name: "bolt11", nullable: true }) + bolt11?: string; + @Column({ type: "text", name: "withdrawn_details", nullable: true }) withdrawnDetails?: string; diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 822ff86..146a4ad 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -14,6 +14,8 @@ import IReqAddToBatch from "../types/cyphernode/IReqAddToBatch"; import { IResponseError, ErrorCodes } from "../types/jsonrpc/IResponseMessage"; import IReqSpend from "../types/cyphernode/IReqSpend"; import IRespSpend from "../types/cyphernode/IRespSpend"; +import IReqLnPay from "../types/cyphernode/IReqLnPay"; +import IRespLnPay from "../types/cyphernode/IRespLnPay"; class CyphernodeClient { private baseURL: string; @@ -60,7 +62,7 @@ class CyphernodeClient { addedOptions?: unknown // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { - logger.info("CyphernodeClient._post %s %s %s", url, postdata, addedOptions); + logger.info("CyphernodeClient._post:", url, postdata, addedOptions); let configs: AxiosRequestConfig = { url: url, @@ -86,10 +88,7 @@ class CyphernodeClient { try { const response = await axios.request(configs); - logger.debug( - "CyphernodeClient._post :: response.data = %s", - JSON.stringify(response.data) - ); + logger.debug("CyphernodeClient._post :: response.data:", response.data); return { status: response.status, data: response.data }; } catch (error) { @@ -97,15 +96,15 @@ class CyphernodeClient { // The request was made and the server responded with a status code // that falls out of the range of 2xx logger.info( - "CyphernodeClient._post :: error.response.data = %s", - JSON.stringify(error.response.data) + "CyphernodeClient._post :: error.response.data:", + error.response.data ); logger.info( - "CyphernodeClient._post :: error.response.status = %d", + "CyphernodeClient._post :: error.response.status:", error.response.status ); logger.info( - "CyphernodeClient._post :: error.response.headers = %s", + "CyphernodeClient._post :: error.response.headers:", error.response.headers ); @@ -114,15 +113,12 @@ class CyphernodeClient { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - logger.info( - "CyphernodeClient._post :: error.message = %s", - error.message - ); + logger.info("CyphernodeClient._post :: error.message:", error.message); return { status: -1, data: error.message }; } else { // Something happened in setting up the request that triggered an Error - logger.info("CyphernodeClient._post :: Error: %s", error.message); + logger.info("CyphernodeClient._post :: Error:", error.message); return { status: -2, data: error.message }; } @@ -131,7 +127,7 @@ class CyphernodeClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any async _get(url: string, addedOptions?: unknown): Promise { - logger.info("CyphernodeClient._get %s %s", url, addedOptions); + logger.info("CyphernodeClient._get:", url, addedOptions); let configs: AxiosRequestConfig = { url: url, @@ -151,10 +147,7 @@ class CyphernodeClient { try { const response = await axios.request(configs); - logger.debug( - "CyphernodeClient._get :: response.data = %s", - JSON.stringify(response.data) - ); + logger.debug("CyphernodeClient._get :: response.data:", response.data); return { status: response.status, data: response.data }; } catch (error) { @@ -162,15 +155,15 @@ class CyphernodeClient { // The request was made and the server responded with a status code // that falls out of the range of 2xx logger.info( - "CyphernodeClient._get :: error.response.data = %s", - JSON.stringify(error.response.data) + "CyphernodeClient._get :: error.response.data:", + error.response.data ); logger.info( - "CyphernodeClient._get :: error.response.status = %d", + "CyphernodeClient._get :: error.response.status:", error.response.status ); logger.info( - "CyphernodeClient._get :: error.response.headers = %s", + "CyphernodeClient._get :: error.response.headers:", error.response.headers ); @@ -179,15 +172,12 @@ class CyphernodeClient { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - logger.info( - "CyphernodeClient._get :: error.message = %s", - error.message - ); + logger.info("CyphernodeClient._get :: error.message:", error.message); return { status: -1, data: error.message }; } else { // Something happened in setting up the request that triggered an Error - logger.info("CyphernodeClient._get :: Error: %s", error.message); + logger.info("CyphernodeClient._get :: Error:", error.message); return { status: -2, data: error.message }; } @@ -216,7 +206,7 @@ class CyphernodeClient { // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batchLabel":"lowfees","webhookUrl":"https://myCypherApp:3000/batchExecuted"} // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batchId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"} - logger.info("CyphernodeClient.addToBatch: %s", batchRequestTO); + logger.info("CyphernodeClient.addToBatch:", batchRequestTO); let result: IRespAddToBatch; const response = await this._post("/addtobatch", batchRequestTO); @@ -250,7 +240,7 @@ class CyphernodeClient { // // BODY {"id":72} - logger.info("CyphernodeClient.removeFromBatch: %d", outputId); + logger.info("CyphernodeClient.removeFromBatch:", outputId); let result: IRespAddToBatch; const response = await this._post("/removefrombatch", { @@ -311,7 +301,7 @@ class CyphernodeClient { // BODY {} // BODY {"lnurlId":34} - logger.info("CyphernodeClient.getBatchDetails: %s", batchIdent); + logger.info("CyphernodeClient.getBatchDetails:", batchIdent); let result: IRespGetBatchDetails; const response = await this._post("/getbatchdetails", batchIdent); @@ -378,7 +368,7 @@ class CyphernodeClient { // NOTYET BODY {"lnurlLabel":"highfees","feeRate":233.7} // BODY {"lnurlId":411,"confTarget":6} - logger.info("CyphernodeClient.batchSpend: %s", batchSpendTO); + logger.info("CyphernodeClient.batchSpend:", batchSpendTO); let result: IRespBatchSpend; const response = await this._post("/batchspend", batchSpendTO); @@ -431,7 +421,7 @@ class CyphernodeClient { // // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} - logger.info("CyphernodeClient.spend: %s", spendTO); + logger.info("CyphernodeClient.spend:", spendTO); let result: IRespSpend; const response = await this._post("/spend", spendTO); @@ -448,6 +438,71 @@ class CyphernodeClient { } return result; } + + async lnPay(lnPayTO: IReqLnPay): Promise { + // POST http://192.168.111.152:8080/ln_pay + // BODY {"bolt11":"lntb1pdca82tpp5g[...]9wafq9n4w28amnmwzujgqpmapcr3", + // "expected_msatoshi":"10000","expected_description":"Bitcoin Outlet order #7082"} + + // args: + // - bolt11, required, lightning network bolt11 invoice + // - expected_msatoshi, optional, amount we want to send, expected to be the same amount as the one encoded in the bolt11 invoice + // - expected_description, optional, expected description encoded in the bolt11 invoice + // + // Example of error result: + // + // { "code" : 204, "message" : "failed: WIRE_TEMPORARY_CHANNEL_FAILURE (Outgoing subdaemon died)", "data" : + // { + // "erring_index": 0, + // "failcode": 4103, + // "erring_node": "031b867d9d6631a1352cc0f37bcea94bd5587a8d4f40416c4ce1a12511b1e68f56", + // "erring_channel": "1452982:62:0" + // } } + // + // + // Example of successful result: + // + // { + // "id": 44, + // "payment_hash": "de648062da7117903291dab2075881e49ddd78efbf82438e4a2f486a7ebe0f3a", + // "destination": "02be93d1dad1ccae7beea7b42f8dbcfbdafb4d342335c603125ef518200290b450", + // "msatoshi": 207000, + // "msatoshi_sent": 207747, + // "created_at": 1548380406, + // "status": "complete", + // "payment_preimage": "a7ef27e9a94d63e4028f35ca4213fd9008227ad86815cd40d3413287d819b145", + // "description": "Order 43012 - Satoshi Larrivee", + // "getroute_tries": 1, + // "sendpay_tries": 1, + // "route": [ + // { + // "id": "02be93d1dad1ccae7beea7b42f8dbcfbdafb4d342335c603125ef518200290b450", + // "channel": "1452749:174:0", + // "msatoshi": 207747, + // "delay": 10 + // } + // ], + // "failures": [ + // ] + // } + + logger.info("CyphernodeClient.lnPay:", lnPayTO); + + let result: IRespLnPay; + const response = await this._post("/ln_pay", lnPayTO); + if (response.status >= 200 && response.status < 400) { + result = { result: response.data }; + } else { + result = { + error: { + code: ErrorCodes.InternalError, + message: response.data.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespLnPay; + } + return result; + } } export { CyphernodeClient }; diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 32369b4..05d1f51 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -11,7 +11,7 @@ import { } from "../types/jsonrpc/IResponseMessage"; import { IRequestMessage } from "../types/jsonrpc/IRequestMessage"; import { Utils } from "./Utils"; -import IRespCreateLnurlWithdraw from "../types/IRespCreateLnurlWithdraw"; +import IRespCreateLnurlWithdraw from "../types/IRespLnurlWithdraw"; import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; @@ -32,9 +32,7 @@ class HttpServer { async loadConfig(): Promise { logger.debug("loadConfig"); - this._lnurlConfig = JSON.parse( - fs.readFileSync("data/config.json", "utf8") - ); + this._lnurlConfig = JSON.parse(fs.readFileSync("data/config.json", "utf8")); this._lnurlWithdraw.configureLnurl(this._lnurlConfig); } @@ -46,7 +44,9 @@ class HttpServer { const reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw = params as IReqCreateLnurlWithdraw; - return await this._lnurlWithdraw.createLnurlWithdraw(reqCreateLnurlWithdraw); + return await this._lnurlWithdraw.createLnurlWithdraw( + reqCreateLnurlWithdraw + ); } async start(): Promise { @@ -67,7 +67,6 @@ class HttpServer { // Check the method and call the corresponding function switch (reqMessage.method) { - case "createLnurlWithdraw": { const result: IRespCreateLnurlWithdraw = await this.createLnurlWithdraw( reqMessage.params || {} @@ -77,13 +76,28 @@ class HttpServer { break; } + case "getLnurlWithdraw": { + const result: IRespCreateLnurlWithdraw = await this.createLnurlWithdraw( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + case "encodeBech32": { - response.result = await Utils.encodeBech32((reqMessage.params as any).s); + response.result = await Utils.encodeBech32( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (reqMessage.params as any).s + ); break; } case "decodeBech32": { - response.result = await Utils.decodeBech32((reqMessage.params as any).s); + response.result = await Utils.decodeBech32( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (reqMessage.params as any).s + ); break; } @@ -120,7 +134,9 @@ class HttpServer { req.query ); - const response = await this._lnurlWithdraw.processLnurlWithdrawRequest(req.query.s as string); + const response = await this._lnurlWithdraw.lnServiceWithdrawRequest( + req.query.s as string + ); if (response.status) { res.status(400).json(response); @@ -134,12 +150,13 @@ class HttpServer { this._httpServer.get( this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, async (req, res) => { - logger.info( - this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX + ":", - req.query - ); + logger.info(this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX + ":", req.query); - const response = await this._lnurlWithdraw.processLnurlWithdraw({ k1: req.query.k1, pr: req.query.pr, balanceNotify: req.query.balanceNotify } as IReqLnurlWithdraw); + const response = await this._lnurlWithdraw.lnServiceWithdraw({ + k1: req.query.k1, + pr: req.query.pr, + balanceNotify: req.query.balanceNotify, + } as IReqLnurlWithdraw); if (response.status === "ERROR") { res.status(400).json(response); @@ -148,7 +165,7 @@ class HttpServer { } } ); - + this._httpServer.listen(this._lnurlConfig.URL_API_PORT, () => { logger.info( "Express HTTP server listening on port:", diff --git a/src/lib/LnurlDB.ts b/src/lib/LnurlDB.ts index 8783b0d..f048b5e 100644 --- a/src/lib/LnurlDB.ts +++ b/src/lib/LnurlDB.ts @@ -1,8 +1,8 @@ import logger from "./Log2File"; import path from "path"; import LnurlConfig from "../config/LnurlConfig"; -import { Connection, createConnection, IsNull } from "typeorm"; -import { LnurlWithdrawRequest } from "../entity/LnurlWithdrawRequest"; +import { Connection, createConnection } from "typeorm"; +import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; class LnurlDB { private _db?: Connection; @@ -32,26 +32,31 @@ class LnurlDB { return await createConnection({ type: "sqlite", database: dbName, - entities: [LnurlWithdrawRequest], + entities: [LnurlWithdrawEntity], synchronize: true, logging: true, }); } - async saveLnurlWithdrawRequest(lnurlWithdrawRequest: LnurlWithdrawRequest): Promise { - const lwr = await this._db?.manager.getRepository(LnurlWithdrawRequest).save(lnurlWithdrawRequest); + async saveLnurlWithdraw( + lnurlWithdraw: LnurlWithdrawEntity + ): Promise { + const lwr = await this._db?.manager + .getRepository(LnurlWithdrawEntity) + .save(lnurlWithdraw); - return lwr as LnurlWithdrawRequest; + return lwr as LnurlWithdrawEntity; } - async getLnurlWithdrawRequestBySecret(secretToken: string): Promise { + async getLnurlWithdrawBySecret( + secretToken: string + ): Promise { const wr = await this._db?.manager - .getRepository(LnurlWithdrawRequest) + .getRepository(LnurlWithdrawEntity) .findOne({ where: { secretToken } }); - return wr as LnurlWithdrawRequest; + return wr as LnurlWithdrawEntity; } - } export { LnurlDB }; diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 74c94b3..16b30e1 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -2,17 +2,17 @@ import logger from "./Log2File"; import LnurlConfig from "../config/LnurlConfig"; import { CyphernodeClient } from "./CyphernodeClient"; import { LnurlDB } from "./LnurlDB"; -import { - ErrorCodes, - IResponseMessage, -} from "../types/jsonrpc/IResponseMessage"; +import { ErrorCodes } from "../types/jsonrpc/IResponseMessage"; import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; -import IRespCreateLnurlWithdraw from "../types/IRespCreateLnurlWithdraw"; +import IRespGetLnurlWithdraw from "../types/IRespLnurlWithdraw"; import { CreateLnurlWithdrawValidator } from "../validators/CreateLnurlWithdrawValidator"; -import { LnurlWithdrawRequest } from "../entity/LnurlWithdrawRequest"; -import IRespLnserviceWithdrawRequest from "../types/IRespLnserviceWithdrawRequest"; -import IRespLnserviceStatus from "../types/IRespLnserviceStatus"; +import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; +import IRespLnServiceWithdrawRequest from "../types/IRespLnServiceWithdrawRequest"; +import IRespLnServiceStatus from "../types/IRespLnServiceStatus"; import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; +import { Utils } from "./Utils"; +import IRespLnPay from "../types/cyphernode/IRespLnPay"; +import { LnServiceWithdrawValidator } from "../validators/LnServiceWithdrawValidator"; class LnurlWithdraw { private _lnurlConfig: LnurlConfig; @@ -34,41 +34,56 @@ class LnurlWithdraw { async createLnurlWithdraw( reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw - ): Promise { + ): Promise { logger.info( "LnurlWithdraw.createLnurlWithdraw, reqCreateLnurlWithdraw:", reqCreateLnurlWithdraw ); - const response: IRespCreateLnurlWithdraw = {}; + const response: IRespGetLnurlWithdraw = {}; if (CreateLnurlWithdrawValidator.validateRequest(reqCreateLnurlWithdraw)) { // Inputs are valid. logger.debug("LnurlWithdraw.createLnurlWithdraw, Inputs are valid."); - let lnurlWithdrawRequest: LnurlWithdrawRequest; - let lnurl = this._lnurlConfig.LN_SERVICE_SERVER + ":" + this._lnurlConfig.LN_SERVICE_PORT + this._lnurlConfig.LN_SERVICE_CTX + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + "?s=" + reqCreateLnurlWithdraw.secretToken; + const lnurl = await Utils.encodeBech32( + this._lnurlConfig.LN_SERVICE_SERVER + + ":" + + this._lnurlConfig.LN_SERVICE_PORT + + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + + "?s=" + + reqCreateLnurlWithdraw.secretToken + ); - lnurlWithdrawRequest = await this._lnurlDB.saveLnurlWithdrawRequest( - Object.assign(reqCreateLnurlWithdraw as LnurlWithdrawRequest, { lnurl: lnurl }) + const lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + Object.assign(reqCreateLnurlWithdraw as LnurlWithdrawEntity, { + lnurl: lnurl, + }) ); - if (lnurlWithdrawRequest) { - logger.debug("LnurlWithdraw.createLnurlWithdraw, lnurlWithdrawRequest created."); + if (lnurlWithdrawEntity) { + logger.debug( + "LnurlWithdraw.createLnurlWithdraw, lnurlWithdraw created." + ); - response.result = lnurlWithdrawRequest; + response.result = lnurlWithdrawEntity; } else { - // LnurlWithdrawRequest not created - logger.debug("LnurlWithdraw.createLnurlWithdraw, LnurlWithdrawRequest not created."); + // LnurlWithdraw not created + logger.debug( + "LnurlWithdraw.createLnurlWithdraw, LnurlWithdraw not created." + ); response.error = { code: ErrorCodes.InvalidRequest, - message: "LnurlWithdrawRequest not created", + message: "LnurlWithdraw not created", }; } } else { // There is an error with inputs - logger.debug("LnurlWithdraw.createLnurlWithdraw, there is an error with inputs."); + logger.debug( + "LnurlWithdraw.createLnurlWithdraw, there is an error with inputs." + ); response.error = { code: ErrorCodes.InvalidRequest, @@ -79,22 +94,31 @@ class LnurlWithdraw { return response; } - async processLnurlWithdrawRequest(secretToken: string): Promise { - logger.info("LnurlWithdraw.processLnurlWithdrawRequest:", secretToken); + async lnServiceWithdrawRequest( + secretToken: string + ): Promise { + logger.info("LnurlWithdraw.lnServiceWithdrawRequest:", secretToken); - let result: IRespLnserviceWithdrawRequest; - const lnurlWithdrawRequest = await this._lnurlDB.getLnurlWithdrawRequestBySecret(secretToken); - logger.debug("lnurlWithdrawRequest:", lnurlWithdrawRequest); + let result: IRespLnServiceWithdrawRequest; + const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( + secretToken + ); + logger.debug("lnurlWithdrawEntity:", lnurlWithdrawEntity); - if (lnurlWithdrawRequest != null && lnurlWithdrawRequest.active) { + if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { result = { tag: "withdrawRequest", - callback: this._lnurlConfig.LN_SERVICE_SERVER + ":" + this._lnurlConfig.LN_SERVICE_PORT + this._lnurlConfig.LN_SERVICE_CTX + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, - k1: lnurlWithdrawRequest.secretToken, - defaultDescription: lnurlWithdrawRequest.description, - minWithdrawable: lnurlWithdrawRequest.amount, - maxWithdrawable: lnurlWithdrawRequest.amount - } + callback: + this._lnurlConfig.LN_SERVICE_SERVER + + ":" + + this._lnurlConfig.LN_SERVICE_PORT + + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, + k1: lnurlWithdrawEntity.secretToken, + defaultDescription: lnurlWithdrawEntity.description, + minWithdrawable: lnurlWithdrawEntity.amount, + maxWithdrawable: lnurlWithdrawEntity.amount, + }; } else { result = { status: "ERROR", reason: "Invalid k1 value" }; } @@ -102,16 +126,52 @@ class LnurlWithdraw { return result; } - async processLnurlWithdraw(params: IReqLnurlWithdraw): Promise { - logger.info("LnurlWithdraw.processLnurlWithdraw:", params); + async lnServiceWithdraw( + params: IReqLnurlWithdraw + ): Promise { + logger.info("LnurlWithdraw.lnServiceWithdraw:", params); - let result: IRespLnserviceStatus; - const lnurlWithdrawRequest = await this._lnurlDB.getLnurlWithdrawRequestBySecret(params.k1); + let result: IRespLnServiceStatus; + + if (LnServiceWithdrawValidator.validateRequest(params)) { + // Inputs are valid. + logger.debug("LnurlWithdraw.lnServiceWithdraw, Inputs are valid."); + + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( + params.k1 + ); + lnurlWithdrawEntity.bolt11 = params.pr; + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); - if (lnurlWithdrawRequest != null && lnurlWithdrawRequest.active) { - result = { status: "OK" }; + if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + const resp: IRespLnPay = await this._cyphernodeClient.lnPay({ + bolt11: params.pr, + expectedMsatoshi: lnurlWithdrawEntity.amount, + expectedDescription: lnurlWithdrawEntity.description, + }); + if (resp.error) { + result = { status: "ERROR", reason: resp.error.message }; + } else { + result = { status: "OK" }; + } + } else { + result = { + status: "ERROR", + reason: "Invalid k1 value or inactive lnurlWithdrawRequest", + }; + } } else { - result = { status: "ERROR", reason: "Invalid k1 value" }; + // There is an error with inputs + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, there is an error with inputs." + ); + + result = { + status: "ERROR", + reason: "Invalid arguments", + }; } return result; diff --git a/src/lib/Log2File.ts b/src/lib/Log2File.ts index 76ca6be..9eaeecb 100644 --- a/src/lib/Log2File.ts +++ b/src/lib/Log2File.ts @@ -1,7 +1,7 @@ import { ILogObject, Logger } from "tslog"; -import { appendFileSync } from "fs"; +import { appendFileSync } from "fs"; -function logToTransport(logObject: ILogObject) { +function logToTransport(logObject: ILogObject): void { appendFileSync("logs/lnurl.log", JSON.stringify(logObject) + "\n"); } diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts index eb52294..87e589f 100644 --- a/src/lib/Utils.ts +++ b/src/lib/Utils.ts @@ -1,6 +1,6 @@ import logger from "./Log2File"; import axios, { AxiosRequestConfig } from "axios"; -import { bech32 } from "bech32" +import { bech32 } from "bech32"; class Utils { static async post( @@ -67,31 +67,25 @@ class Utils { } } - static async encodeBech32( - str: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { - logger.info( - "Utils.encodeBech32:", - str - ); + static async encodeBech32(str: string): Promise { + logger.info("Utils.encodeBech32:", str); - let lnurlBech32 = bech32.encode("LNURL", bech32.toWords(Buffer.from(str, 'utf8')), 2000); + const lnurlBech32 = bech32.encode( + "LNURL", + bech32.toWords(Buffer.from(str, "utf8")), + 2000 + ); logger.debug("lnurlBech32:", lnurlBech32); return lnurlBech32.toUpperCase(); } - static async decodeBech32( - str: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { - logger.info( - "Utils.decodeBech32:", - str - ); + static async decodeBech32(str: string): Promise { + logger.info("Utils.decodeBech32:", str); - let lnurl = Buffer.from(bech32.fromWords(bech32.decode(str, 2000).words)).toString(); + const lnurl = Buffer.from( + bech32.fromWords(bech32.decode(str, 2000).words) + ).toString(); return lnurl; } diff --git a/src/types/IRespCreateLnurlWithdraw.ts b/src/types/IRespCreateLnurlWithdraw.ts deleted file mode 100644 index c137aa2..0000000 --- a/src/types/IRespCreateLnurlWithdraw.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IResponseError } from "./jsonrpc/IResponseMessage"; -import { LnurlWithdrawRequest } from "../entity/LnurlWithdrawRequest"; - -export default interface IRespCreateLnurlWithdraw { - result?: LnurlWithdrawRequest; - error?: IResponseError; -} diff --git a/src/types/IRespLnserviceStatus.ts b/src/types/IRespLnserviceStatus.ts index 8524cac..5928ea2 100644 --- a/src/types/IRespLnserviceStatus.ts +++ b/src/types/IRespLnserviceStatus.ts @@ -1,4 +1,4 @@ -export default interface IRespLnserviceStatus { +export default interface IRespLnServiceStatus { status?: string; reason?: string; } diff --git a/src/types/IRespLnserviceWithdrawRequest.ts b/src/types/IRespLnserviceWithdrawRequest.ts index e329142..0001f7c 100644 --- a/src/types/IRespLnserviceWithdrawRequest.ts +++ b/src/types/IRespLnserviceWithdrawRequest.ts @@ -1,6 +1,7 @@ -import IRespLnserviceStatus from "./IRespLnserviceStatus"; +import IRespLnServiceStatus from "./IRespLnServiceStatus"; -export default interface IRespLnserviceWithdrawRequest extends IRespLnserviceStatus { +export default interface IRespLnServiceWithdrawRequest + extends IRespLnServiceStatus { tag?: string; callback?: string; k1?: string; diff --git a/src/types/IRespLnurlWithdraw.ts b/src/types/IRespLnurlWithdraw.ts new file mode 100644 index 0000000..9e4d0a6 --- /dev/null +++ b/src/types/IRespLnurlWithdraw.ts @@ -0,0 +1,7 @@ +import { IResponseError } from "./jsonrpc/IResponseMessage"; +import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; + +export default interface IRespLnurlWithdraw { + result?: LnurlWithdrawEntity; + error?: IResponseError; +} diff --git a/src/types/cyphernode/IReqLnPay.ts b/src/types/cyphernode/IReqLnPay.ts new file mode 100644 index 0000000..d00b922 --- /dev/null +++ b/src/types/cyphernode/IReqLnPay.ts @@ -0,0 +1,9 @@ +export default interface IReqLnPay { + // - bolt11, required, lightning network bolt11 invoice + // - expected_msatoshi, optional, amount we want to send, expected to be the same amount as the one encoded in the bolt11 invoice + // - expected_description, optional, expected description encoded in the bolt11 invoice + + bolt11: string; + expectedMsatoshi?: number; + expectedDescription?: string; +} diff --git a/src/types/cyphernode/IRespLnPay.ts b/src/types/cyphernode/IRespLnPay.ts new file mode 100644 index 0000000..42c22bf --- /dev/null +++ b/src/types/cyphernode/IRespLnPay.ts @@ -0,0 +1,6 @@ +import { IResponseError } from "../jsonrpc/IResponseMessage"; + +export default interface IRespLnPay { + result?: unknown; + error?: IResponseError; +} diff --git a/src/validators/LnServiceWithdrawValidator.ts b/src/validators/LnServiceWithdrawValidator.ts new file mode 100644 index 0000000..ad0cc01 --- /dev/null +++ b/src/validators/LnServiceWithdrawValidator.ts @@ -0,0 +1,13 @@ +import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; + +class LnServiceWithdrawValidator { + static validateRequest(request: IReqLnurlWithdraw): boolean { + if (request.pr && request.k1) { + return true; + } else { + return false; + } + } +} + +export { LnServiceWithdrawValidator }; diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..1335ecb --- /dev/null +++ b/tests/README.md @@ -0,0 +1,101 @@ +# Test scripts + +## Setup + +1. Have a Cyphernode instance setup in regtest with LN active. +2. Let's double everything LN-related: + * Duplicate the `lightning` block in `dist/docker-compose.yaml` appending a `2` to the names (see below) + * Copy `apps/sparkwallet` to `apps/sparkwallet2` and change `apps/sparkwallet2/docker-compose.yaml` appending a `2` to the names (see below) +3. Mine enough blocks to have regtest coins to open channels between the two LN nodes (use `ln_setup.sh`) +4. Open a channel between the two nodes (use `ln_setup.sh`) +5. If connection is lost between the two nodes (eg. after a restart of Cyphernode), reconnect the two (use `ln_reconnect.sh`) + +## Changes to files for lightning2... + +dist/docker-compose.yaml: + +```yaml + lightning2: + image: cyphernode/clightning:v0.10.0 + command: $USER /.lightning/bitcoin/entrypoint.sh + volumes: + - "/yourCyphernodePath/dist/cyphernode/lightning2:/.lightning" + - "/yourCyphernodePath/dist/cyphernode/bitcoin/bitcoin-client.conf:/.bitcoin/bitcoin.conf:ro" + - container_monitor:/container_monitor + healthcheck: + test: chown $USER /container_monitor && su-exec $USER sh -c 'lightning-cli getinfo && touch /container_monitor/lightning_ready && chown $USER /container_monitor/lightning_ready || rm -f /container_monitor/lightning_ready' + interval: 20s + timeout: 10s + retries: 10 + stop_grace_period: 30s + networks: + - cyphernodenet + depends_on: + - tor + deploy: + replicas: 1 + placement: + constraints: + - node.labels.io.cyphernode == true + restart_policy: + condition: "any" + delay: 1s + update_config: + parallelism: 1 +``` + +dist/apps/sparkwallet2/docker-compose.yaml: + +```yaml + cyphernode_sparkwallet2: + command: --no-tls ${TOR_PARAMS} + image: cyphernode/sparkwallet:v0.2.17 + environment: + - "NETWORK=${NETWORK}" + volumes: + - "/yourCyphernodePath/dist/cyphernode/lightning2:/etc/lightning" + - "$APP_SCRIPT_PATH/cookie:/data/spark/cookie" + - "$GATEKEEPER_DATAPATH/htpasswd:/htpasswd/htpasswd" + labels: + - "traefik.docker.network=cyphernodeappsnet" + - "traefik.frontend.entryPoints=https" + - "traefik.frontend.redirect.regex=^(.*)/sparkwallet2$$" + - "traefik.frontend.redirect.replacement=$$1/sparkwallet2/" + - "traefik.frontend.rule=PathPrefix:/sparkwallet2;ReplacePathRegex: ^/sparkwallet2/(.*) /$$1" + - "traefik.frontend.passHostHeader=true" + - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" + - "traefik.frontend.headers.customRequestHeaders=X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" + - "traefik.enable=true" + - "traefik.port=9737" + networks: + - cyphernodeappsnet + restart: always + deploy: + labels: + - "traefik.docker.network=cyphernodeappsnet" + - "traefik.frontend.entryPoints=https" + - "traefik.frontend.redirect.regex=^(.*)/sparkwallet2$$" + - "traefik.frontend.redirect.replacement=$$1/sparkwallet2/" + - "traefik.frontend.rule=PathPrefix:/sparkwallet2;ReplacePathRegex: ^/sparkwallet2/(.*) /$$1" + - "traefik.frontend.passHostHeader=true" + - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" + - "traefik.frontend.headers.customRequestHeaders=X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" + - "traefik.enable=true" + - "traefik.port=9737" + replicas: 1 + placement: + constraints: + - node.labels.io.cyphernode == true + restart_policy: + condition: "any" + delay: 1s + update_config: + parallelism: 1 +``` + +## Test LNURL-withdraw + +Container `lightning` is used by Cyphernode and `lightning2` will be our user. + +### Create a LNURL for withdraw + diff --git a/tests/colors.sh b/tests/colors.sh new file mode 100644 index 0000000..fd78e7d --- /dev/null +++ b/tests/colors.sh @@ -0,0 +1,74 @@ +#!/bin/sh + +# Reset +Color_Off='\033[0m' # Text Reset + +# Regular Colors +Black='\033[0;30m' # Black +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +Yellow='\033[0;33m' # Yellow +Blue='\033[0;34m' # Blue +Purple='\033[0;35m' # Purple +Cyan='\033[0;36m' # Cyan +White='\033[0;37m' # White + +# Bold +BBlack='\033[1;30m' # Black +BRed='\033[1;31m' # Red +BGreen='\033[1;32m' # Green +BYellow='\033[1;33m' # Yellow +BBlue='\033[1;34m' # Blue +BPurple='\033[1;35m' # Purple +BCyan='\033[1;36m' # Cyan +BWhite='\033[1;37m' # White + +# Underline +UBlack='\033[4;30m' # Black +URed='\033[4;31m' # Red +UGreen='\033[4;32m' # Green +UYellow='\033[4;33m' # Yellow +UBlue='\033[4;34m' # Blue +UPurple='\033[4;35m' # Purple +UCyan='\033[4;36m' # Cyan +UWhite='\033[4;37m' # White + +# Background +On_Black='\033[40m' # Black +On_Red='\033[41m' # Red +On_Green='\033[42m' # Green +On_Yellow='\033[43m' # Yellow +On_Blue='\033[44m' # Blue +On_Purple='\033[45m' # Purple +On_Cyan='\033[46m' # Cyan +On_White='\033[47m' # White + +# High Intensity +IBlack='\033[0;90m' # Black +IRed='\033[0;91m' # Red +IGreen='\033[0;92m' # Green +IYellow='\033[0;93m' # Yellow +IBlue='\033[0;94m' # Blue +IPurple='\033[0;95m' # Purple +ICyan='\033[0;96m' # Cyan +IWhite='\033[0;97m' # White + +# Bold High Intensity +BIBlack='\033[1;90m' # Black +BIRed='\033[1;91m' # Red +BIGreen='\033[1;92m' # Green +BIYellow='\033[1;93m' # Yellow +BIBlue='\033[1;94m' # Blue +BIPurple='\033[1;95m' # Purple +BICyan='\033[1;96m' # Cyan +BIWhite='\033[1;97m' # White + +# High Intensity backgrounds +On_IBlack='\033[0;100m' # Black +On_IRed='\033[0;101m' # Red +On_IGreen='\033[0;102m' # Green +On_IYellow='\033[0;103m' # Yellow +On_IBlue='\033[0;104m' # Blue +On_IPurple='\033[0;105m' # Purple +On_ICyan='\033[0;106m' # Cyan +On_IWhite='\033[0;107m' # White diff --git a/tests/ln_reconnect.sh b/tests/ln_reconnect.sh new file mode 100755 index 0000000..88c6893 --- /dev/null +++ b/tests/ln_reconnect.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning") diff --git a/tests/ln_setup.sh b/tests/ln_setup.sh new file mode 100755 index 0000000..7957cfe --- /dev/null +++ b/tests/ln_setup.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +. ./mine.sh + +date + +# Get node2 connection string +connectstring2=$(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning2") +echo ; echo "connectstring2=${connectstring2}" + +# Mine enough blocks +mine 105 +channelmsats=8000000000 + +# Create a channel between the two nodes +data='{"peer":"'${connectstring2}'","msatoshi":'${channelmsats}'}' +echo ; echo "data=${data}" +docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/ln_connectfund + +echo ; echo "Sleeping 5 seconds..." +sleep 5 + +# Make the channel ready +mine 6 + +echo ; echo "Sleeping 30 seconds..." +sleep 30 + +# Balance the channel +invoicenumber=$RANDOM +msats=$((${channelmsats}/2)) +label="invoice${invoicenumber}" +desc="Invoice number ${invoicenumber}" +echo ; echo "msats=${msats}" +echo "label=${label}" +echo "desc=${desc}" +invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning invoice ${msats} "${label}" "${desc}") +echo ; echo "invoice=${invoice}" + +# Pay to rebalance channel +bolt11=$(echo ${invoice} | jq ".bolt11") +echo ; echo "bolt11=${bolt11}" +data='{"bolt11":'${bolt11}',"expected_msatoshi":'${msats}',"expected_description":"'${desc}'"}' +echo ; echo "data=${data}" +docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/ln_pay + +echo ; echo "That's all folks!" + +date diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh new file mode 100755 index 0000000..fd319e6 --- /dev/null +++ b/tests/lnurl_withdraw.sh @@ -0,0 +1,113 @@ +#!/bin/sh + +. ./colors.sh + +echo -e "${Color_Off}" +date + +# Install needed packages +echo -e "\n${BCyan}Installing needed packages...${Color_Off}" +apk add curl jq + +# Initializing test variables +echo -e "\n${BCyan}Initializing test variables...${Color_Off}" +callbackservername="cb" +callbackserverport="1111" +callbackurl="http://${callbackservername}:${callbackserverport}" + +invoicenumber=$RANDOM +amount=$((10000+${invoicenumber})) +expiration=$(date -d @$(($(date +"%s")+3600)) +"%F %T") +#echo "invoicenumber=${invoicenumber}" +#echo "amount=${amount}" +#echo "expiration=${expiration}" + +# Get config from lnurl cypherapp +echo -e "\n${BCyan}Getting configuration from lnurl cypherapp...${Color_Off}" +data='{"id":0,"method":"getConfig","params":[]}' +lnurlConfig=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) +#echo "lnurlConfig=${lnurlConfig}" +lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER):\(.LN_SERVICE_PORT)\(.LN_SERVICE_CTX)"') +#echo "lnServicePrefix=${lnServicePrefix}" + +# Service creates LNURL Withdraw +echo -e "\n${BCyan}Service creates LNURL Withdraw...${Color_Off}" +data='{"id":0,"method":"createLnurlWithdraw","params":{"amount":'${amount}',"description":"desc'${invoicenumber}'","expiration":"'${expiration}'","secretToken":"secret'${invoicenumber}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'"}}' +#echo "data=${data}" +#echo "Calling createLnurlWithdraw..." +createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) +#echo "createLnurlWithdraw=${createLnurlWithdraw}" + +# {"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15 12:12","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} +lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") +echo "lnurl=${lnurl}" + +# Decode LNURL +echo -e "\n${BCyan}Decoding LNURL...${Color_Off}" +data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' +#echo ; echo "data=${data}" +decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) +echo "decodedLnurl=${decodedLnurl}" +urlSuffix=$(echo "${decodedLnurl}" | jq -r ".result" | sed 's|'${lnServicePrefix}'||g') +#echo "urlSuffix=${urlSuffix}" + +# User calls LN Service LNURL Withdraw Request +echo -e "\n${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}" +withdrawRequestResponse=$(curl -s lnurl:8000${urlSuffix}) +echo "withdrawRequestResponse=${withdrawRequestResponse}" + +# User calls LN Service LNURL Withdraw +echo -e "\n${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}" +callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") +#echo "callback=${callback}" +urlSuffix=$(echo "${callback}" | sed 's|'${lnServicePrefix}'||g') +#echo "urlSuffix=${urlSuffix}" +k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") +#echo "k1=${k1}" + +# Create bolt11 for LN Service LNURL Withdraw +echo -e "\n${BCyan}User creates bolt11 for the payment...${Color_Off}" +label="invoice${invoicenumber}" +desc="Invoice number ${invoicenumber}" +data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${amount}',"label":"'${label}'","description":"'${desc}'"}}' +#echo ; echo "data=${data}" +invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) +#echo "invoice=${invoice}" +bolt11=$(echo ${invoice} | jq -r ".bolt11") +echo "bolt11=${bolt11}" + +# We want to see that that invoice is unpaid first... +echo -e "\n${BCyan}Let's make sure the invoice is unpaid first...${Color_Off}" +payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") +#echo "payment_hash=${payment_hash}" +data='{"id":1,"jsonrpc": "2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' +#echo "data=${data}" +invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) +#echo "invoices=${invoices}" +status=$(echo "${invoices}" | jq -r ".invoices[0].status") +echo "status=${status}" + +# Actual call to LN Service LNURL Withdraw +echo -e "\n${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" +withdrawResponse=$(curl -s lnurl:8000${urlSuffix}?k1=${k1}\&pr=${bolt11}) +echo "withdrawResponse=${withdrawResponse}" + +# We want to see if payment received (invoice status paid) +echo -e "\n${BCyan}Sleeping 5 seconds...${Color_Off}" +sleep 5 +echo -e "\n${BCyan}Let's see if invoice is paid...${Color_Off}" +invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) +#echo "invoices=${invoices}" +status=$(echo "${invoices}" | jq -r ".invoices[0].status") +echo "status=${status}" + +if [ "${status}" = "paid" ]; then + echo -e "\n${BGreen}SUCCESS!${Color_Off}\n" + date + return 0 +else + echo -e "\n${BRed}FAILURE!${Color_Off}\n" + date + return 1 +fi + diff --git a/tests/mine.sh b/tests/mine.sh new file mode 100755 index 0000000..27351b2 --- /dev/null +++ b/tests/mine.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Mine +mine() { + local nbblocks=${1:-1} + local minedaddr + + echo ; echo "About to mine ${nbblocks} block(s)..." + minedaddr=$(docker exec -it $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat getnewaddress | tr -d '\r') + echo ; echo "minedaddr=${minedaddr}" + docker exec -it $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat generatetoaddress ${nbblocks} "${minedaddr}" +} diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..846692c --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +#./startcallbackserver.sh & + +docker run --rm -it -v "$PWD:/tests" --network=cyphernodeappsnet alpine /tests/lnurl_withdraw.sh diff --git a/tests/startcallbackserver.sh b/tests/startcallbackserver.sh new file mode 100755 index 0000000..9e8c364 --- /dev/null +++ b/tests/startcallbackserver.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +date + +callbackservername="cb" +callbackserverport="1111" + +docker run --rm -it -p ${callbackserverport}:${callbackserverport} --network cyphernodeappsnet --name ${callbackservername} alpine sh -c "nc -vlkp${callbackserverport} -e sh -c 'echo -en \"HTTP/1.1 200 OK\\\\r\\\\n\\\\r\\\\n\" ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo 1>&2'" + From 56cb25513a41e60dc3b8479eac1c1a8832e1b6a6 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 22 Jul 2021 11:36:01 -0400 Subject: [PATCH 03/52] Webhooks and e2e test --- README.md | 2 + cypherapps/data/config.json | 3 +- package-lock.json | 6 +- package.json | 2 +- src/config/LnurlConfig.ts | 1 + src/config/lnurl.sql | 2 + src/entity/LnurlWithdrawEntity.ts | 13 ++++ src/lib/LnurlDB.ts | 32 +++++++- src/lib/LnurlWithdraw.ts | 123 +++++++++++++++++++++++++++++- src/lib/Scheduler.ts | 28 +++++++ src/lib/Utils.ts | 19 ++--- tests/README.md | 16 +++- tests/ln_reconnect.sh | 1 + tests/lnurl_withdraw.sh | 2 +- tests/mine.sh | 2 + 15 files changed, 227 insertions(+), 25 deletions(-) create mode 100644 src/lib/Scheduler.ts diff --git a/README.md b/README.md index 83770b6..fd1e7af 100644 --- a/README.md +++ b/README.md @@ -112,3 +112,5 @@ curl localhost:8000/withdraw?k1=abc03\&pr=lnbcrt123456780p1ps0pf5ypp5lfskvgsdef4 ================== docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" --network cyphernodeappsnet --entrypoint ash lnurl +npm run build +npm run start diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json index 41ad9ab..c0d91e0 100644 --- a/cypherapps/data/config.json +++ b/cypherapps/data/config.json @@ -14,5 +14,6 @@ "LN_SERVICE_PORT": 80, "LN_SERVICE_CTX": "/lnurl", "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", - "LN_SERVICE_WITHDRAW_CTX": "/withdraw" + "LN_SERVICE_WITHDRAW_CTX": "/withdraw", + "RETRY_WEBHOOKS_TIMEOUT": 1 } diff --git a/package-lock.json b/package-lock.json index b0f9512..5dff5c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,9 +153,9 @@ "dev": true }, "@types/node": { - "version": "7.10.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.14.tgz", - "integrity": "sha512-29GS75BE8asnTno3yB6ubOJOO0FboExEqNJy4bpz0GSmW/8wPTNL4h9h63c6s1uTrOopCmJYe/4yJLh5r92ZUA==" + "version": "13.13.52", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", + "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==" }, "@types/qs": { "version": "6.9.7", diff --git a/package.json b/package.json index eb94d76..7f706e3 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "homepage": "https://github.com/SatoshiPortal/lnurl_cypherapp#readme", "dependencies": { - "@types/node": "^7.0.5", + "@types/node": "^13.13.12", "@types/async-lock": "^1.1.2", "async-lock": "^1.2.4", "axios": "^0.21.1", diff --git a/src/config/LnurlConfig.ts b/src/config/LnurlConfig.ts index 936b437..b8a1ae9 100644 --- a/src/config/LnurlConfig.ts +++ b/src/config/LnurlConfig.ts @@ -15,4 +15,5 @@ export default interface LnurlConfig { LN_SERVICE_CTX: string; LN_SERVICE_WITHDRAW_REQUEST_CTX: string; LN_SERVICE_WITHDRAW_CTX: string; + RETRY_WEBHOOKS_TIMEOUT: number; } diff --git a/src/config/lnurl.sql b/src/config/lnurl.sql index d45a575..9fb219a 100644 --- a/src/config/lnurl.sql +++ b/src/config/lnurl.sql @@ -7,6 +7,8 @@ CREATE TABLE lnurl_withdraw ( expiration INTEGER, secret_token TEXT UNIQUE, webhook_url TEXT, + calledback INTEGER DEFAULT false, + calledback_ts INTEGER, lnurl TEXT, bolt11 TEXT, withdrawn_details TEXT, diff --git a/src/entity/LnurlWithdrawEntity.ts b/src/entity/LnurlWithdrawEntity.ts index 2f54c60..af1ae89 100644 --- a/src/entity/LnurlWithdrawEntity.ts +++ b/src/entity/LnurlWithdrawEntity.ts @@ -14,6 +14,8 @@ import { // expiration INTEGER, // secret_token TEXT UNIQUE, // webhook_url TEXT, +// calledback INTEGER DEFAULT false, +// calledback_ts INTEGER, // lnurl TEXT, // bolt11 TEXT, // withdrawn_details TEXT, @@ -47,6 +49,17 @@ export class LnurlWithdrawEntity { @Column({ type: "text", name: "webhook_url", nullable: true }) webhookUrl?: string; + @Column({ + type: "integer", + name: "calledback", + nullable: true, + default: false, + }) + calledback?: boolean; + + @Column({ type: "integer", name: "calledback_ts", nullable: true }) + calledbackTimestamp?: Date; + @Index("idx_lnurl_withdraw_lnurl") @Column({ type: "text", name: "lnurl", nullable: true }) lnurl?: string; diff --git a/src/lib/LnurlDB.ts b/src/lib/LnurlDB.ts index f048b5e..a84f5f5 100644 --- a/src/lib/LnurlDB.ts +++ b/src/lib/LnurlDB.ts @@ -1,7 +1,7 @@ import logger from "./Log2File"; import path from "path"; import LnurlConfig from "../config/LnurlConfig"; -import { Connection, createConnection } from "typeorm"; +import { Connection, createConnection, IsNull, Not } from "typeorm"; import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; class LnurlDB { @@ -57,6 +57,36 @@ class LnurlDB { return wr as LnurlWithdrawEntity; } + + async getLnurlWithdraw( + lnurlWithdrawEntity: LnurlWithdrawEntity + ): Promise { + const wr = await this._db?.manager + .getRepository(LnurlWithdrawEntity) + .findOne(lnurlWithdrawEntity); + + return wr as LnurlWithdrawEntity; + } + + async getLnurlWithdrawById( + lnurlWithdrawId: number + ): Promise { + const lw = await this._db?.manager + .getRepository(LnurlWithdrawEntity) + .findOne(lnurlWithdrawId); + + return lw as LnurlWithdrawEntity; + } + + async getNonCalledbackLnurlWithdraws(): Promise { + const lws = await this._db?.manager + .getRepository(LnurlWithdrawEntity) + .find({ + where: { active: false, calledback: false, webhookUrl: Not(IsNull()) }, + }); + + return lws as LnurlWithdrawEntity[]; + } } export { LnurlDB }; diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 16b30e1..8477673 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -13,25 +13,45 @@ import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; import { Utils } from "./Utils"; import IRespLnPay from "../types/cyphernode/IRespLnPay"; import { LnServiceWithdrawValidator } from "../validators/LnServiceWithdrawValidator"; +import { Scheduler } from "./Scheduler"; class LnurlWithdraw { private _lnurlConfig: LnurlConfig; private _cyphernodeClient: CyphernodeClient; private _lnurlDB: LnurlDB; + private _scheduler: Scheduler; + private _intervalTimeout?: NodeJS.Timeout; constructor(lnurlConfig: LnurlConfig) { this._lnurlConfig = lnurlConfig; this._cyphernodeClient = new CyphernodeClient(this._lnurlConfig); this._lnurlDB = new LnurlDB(this._lnurlConfig); + this._scheduler = new Scheduler(this._lnurlConfig); + this.startIntervals(); } configureLnurl(lnurlConfig: LnurlConfig): void { this._lnurlConfig = lnurlConfig; this._lnurlDB.configureDB(this._lnurlConfig).then(() => { this._cyphernodeClient.configureCyphernode(this._lnurlConfig); + this._scheduler.configureScheduler(this._lnurlConfig).then(() => { + this.startIntervals(); + }); }); } + startIntervals(): void { + if (this._intervalTimeout) { + clearInterval(this._intervalTimeout); + } + this._intervalTimeout = setInterval( + this._scheduler.timeout, + this._lnurlConfig.RETRY_WEBHOOKS_TIMEOUT * 60000, + this._scheduler, + this + ); + } + async createLnurlWithdraw( reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw ): Promise { @@ -140,23 +160,76 @@ class LnurlWithdraw { let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( params.k1 ); - lnurlWithdrawEntity.bolt11 = params.pr; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, active lnurlWithdrawEntity found for this k1!" + ); + + lnurlWithdrawEntity.bolt11 = params.pr; + const resp: IRespLnPay = await this._cyphernodeClient.lnPay({ bolt11: params.pr, expectedMsatoshi: lnurlWithdrawEntity.amount, expectedDescription: lnurlWithdrawEntity.description, }); + if (resp.error) { + logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay error!"); + result = { status: "ERROR", reason: resp.error.message }; + + lnurlWithdrawEntity.withdrawnDetails = resp.error.message; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay success!"); + result = { status: "OK" }; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.result); + lnurlWithdrawEntity.withdrawnTimestamp = new Date(); + lnurlWithdrawEntity.active = false; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + if (lnurlWithdrawEntity.webhookUrl) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." + ); + + this.processCallbacks(lnurlWithdrawEntity); + // const postdata = { + // lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + // lnPayResponse: resp.result, + // }; + // Utils.post(lnurlWithdrawEntity.webhookUrl, postdata).then( + // async (response) => { + // if (response.status >= 200 && response.status < 400) { + // logger.debug( + // "LnurlWithdraw.lnServiceWithdraw, webhook called back" + // ); + + // lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdraw( + // lnurlWithdrawEntity + // ); + // lnurlWithdrawEntity.calledback = true; + // lnurlWithdrawEntity.calledbackTimestamp = new Date(); + // await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + // } + // } + // ); + } } } else { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, active lnurlWithdrawEntity NOT found for this k1!" + ); + result = { status: "ERROR", reason: "Invalid k1 value or inactive lnurlWithdrawRequest", @@ -176,6 +249,48 @@ class LnurlWithdraw { return result; } + + async processCallbacks( + lnurlWithdrawEntity?: LnurlWithdrawEntity + ): Promise { + logger.info( + "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", + lnurlWithdrawEntity + ); + + let lnurlWithdrawEntitys; + if (lnurlWithdrawEntity) { + lnurlWithdrawEntitys = [lnurlWithdrawEntity]; + } else { + lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); + } + let response; + + lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { + logger.debug( + "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", + lnurlWithdrawEntity + ); + + const postdata = { + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + lnPayResponse: JSON.parse(lnurlWithdrawEntity.withdrawnDetails || ""), + }; + logger.debug("LnurlWithdraw.processCallbacks, postdata=", postdata); + + response = await Utils.post( + lnurlWithdrawEntity.webhookUrl || "", + postdata + ); + if (response.status >= 200 && response.status < 400) { + logger.debug("LnurlWithdraw.processCallbacks, webhook called back"); + + lnurlWithdrawEntity.calledback = true; + lnurlWithdrawEntity.calledbackTimestamp = new Date(); + await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + } + }); + } } export { LnurlWithdraw }; diff --git a/src/lib/Scheduler.ts b/src/lib/Scheduler.ts new file mode 100644 index 0000000..d0b2522 --- /dev/null +++ b/src/lib/Scheduler.ts @@ -0,0 +1,28 @@ +import logger from "./Log2File"; +import LnurlConfig from "../config/LnurlConfig"; +import { LnurlWithdraw } from "./LnurlWithdraw"; + +class Scheduler { + private _lnurlConfig: LnurlConfig; + private _startedAt = new Date().getTime(); + + constructor(lnurlWithdrawConfig: LnurlConfig) { + this._lnurlConfig = lnurlWithdrawConfig; + } + + async configureScheduler(lnurlWithdrawConfig: LnurlConfig): Promise { + this._lnurlConfig = lnurlWithdrawConfig; + this._startedAt = new Date().getTime(); + } + + timeout(scheduler: Scheduler, lnurlWithdraw: LnurlWithdraw): void { + logger.info("Scheduler.timeout"); + + scheduler._startedAt = new Date().getTime(); + logger.debug("Scheduler.timeout this._startedAt =", scheduler._startedAt); + + lnurlWithdraw.processCallbacks(undefined); + } +} + +export { Scheduler }; diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts index 87e589f..5d85a49 100644 --- a/src/lib/Utils.ts +++ b/src/lib/Utils.ts @@ -9,12 +9,7 @@ class Utils { addedOptions?: unknown // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { - logger.info( - "Utils.post %s %s %s", - url, - JSON.stringify(postdata), - addedOptions - ); + logger.info("Utils.post", url, JSON.stringify(postdata), addedOptions); let configs: AxiosRequestConfig = { baseURL: url, @@ -28,7 +23,7 @@ class Utils { try { const response = await axios.request(configs); logger.debug( - "Utils.post :: response.data = %s", + "Utils.post :: response.data =", JSON.stringify(response.data) ); @@ -38,15 +33,15 @@ class Utils { // The request was made and the server responded with a status code // that falls out of the range of 2xx logger.info( - "Utils.post :: error.response.data = %s", + "Utils.post :: error.response.data =", JSON.stringify(error.response.data) ); logger.info( - "Utils.post :: error.response.status = %d", + "Utils.post :: error.response.status =", error.response.status ); logger.info( - "Utils.post :: error.response.headers = %s", + "Utils.post :: error.response.headers =", error.response.headers ); @@ -55,12 +50,12 @@ class Utils { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - logger.info("Utils.post :: error.message = %s", error.message); + logger.info("Utils.post :: error.message =", error.message); return { status: -1, data: error.message }; } else { // Something happened in setting up the request that triggered an Error - logger.info("Utils.post :: Error: %s", error.message); + logger.info("Utils.post :: Error:", error.message); return { status: -2, data: error.message }; } diff --git a/tests/README.md b/tests/README.md index 1335ecb..a1300df 100644 --- a/tests/README.md +++ b/tests/README.md @@ -10,7 +10,7 @@ 4. Open a channel between the two nodes (use `ln_setup.sh`) 5. If connection is lost between the two nodes (eg. after a restart of Cyphernode), reconnect the two (use `ln_reconnect.sh`) -## Changes to files for lightning2... +## Changes to files for lightning2 dist/docker-compose.yaml: @@ -97,5 +97,17 @@ dist/apps/sparkwallet2/docker-compose.yaml: Container `lightning` is used by Cyphernode and `lightning2` will be our user. -### Create a LNURL for withdraw +Run ./run_tests.sh or +```bash +docker run --rm -it -v "$PWD:/tests" --network=cyphernodeappsnet alpine /tests/lnurl_withdraw.sh +``` + +lnurl_withdraw.sh will simulate a real-world use case: + +1. Payer creates a LNURL Withdraw URL +2. Payee calls LNURL URL (Withdraw Request) +3. Payee creates a BOLT11 invoice +4. Payee calls callback URL (Withdraw) +5. LNURL Service (this cypherapp) pays BOLT11 +6. LNURL Service (this cypherapp) calls webhook diff --git a/tests/ln_reconnect.sh b/tests/ln_reconnect.sh index 88c6893..3985f46 100755 --- a/tests/ln_reconnect.sh +++ b/tests/ln_reconnect.sh @@ -1,3 +1,4 @@ #!/bin/sh docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning") +docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning2") diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index fd319e6..7c17429 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -1,6 +1,6 @@ #!/bin/sh -. ./colors.sh +. ./tests/colors.sh echo -e "${Color_Off}" date diff --git a/tests/mine.sh b/tests/mine.sh index 27351b2..0276b5f 100755 --- a/tests/mine.sh +++ b/tests/mine.sh @@ -10,3 +10,5 @@ mine() { echo ; echo "minedaddr=${minedaddr}" docker exec -it $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat generatetoaddress ${nbblocks} "${minedaddr}" } + +case "${0}" in *mine.sh) mine $@;; esac From 08f99d741181b742f6be4afa7ff095f6b05e1f40 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 23 Jul 2021 22:08:18 -0400 Subject: [PATCH 04/52] Return bolt11 and decoded LNURL --- src/lib/LnurlWithdraw.ts | 43 +++++++++++---------------------- src/types/ILnurlWithdraw.ts | 5 ++++ src/types/IRespLnurlWithdraw.ts | 4 +-- 3 files changed, 21 insertions(+), 31 deletions(-) create mode 100644 src/types/ILnurlWithdraw.ts diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 8477673..2f1b999 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -14,6 +14,7 @@ import { Utils } from "./Utils"; import IRespLnPay from "../types/cyphernode/IRespLnPay"; import { LnServiceWithdrawValidator } from "../validators/LnServiceWithdrawValidator"; import { Scheduler } from "./Scheduler"; +import ILnurlWithdraw from "../types/ILnurlWithdraw"; class LnurlWithdraw { private _lnurlConfig: LnurlConfig; @@ -66,15 +67,16 @@ class LnurlWithdraw { // Inputs are valid. logger.debug("LnurlWithdraw.createLnurlWithdraw, Inputs are valid."); - const lnurl = await Utils.encodeBech32( + const lnurlDecoded = this._lnurlConfig.LN_SERVICE_SERVER + - ":" + - this._lnurlConfig.LN_SERVICE_PORT + - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + - "?s=" + - reqCreateLnurlWithdraw.secretToken - ); + ":" + + this._lnurlConfig.LN_SERVICE_PORT + + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + + "?s=" + + reqCreateLnurlWithdraw.secretToken; + + const lnurl = await Utils.encodeBech32(lnurlDecoded); const lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( Object.assign(reqCreateLnurlWithdraw as LnurlWithdrawEntity, { @@ -87,7 +89,9 @@ class LnurlWithdraw { "LnurlWithdraw.createLnurlWithdraw, lnurlWithdraw created." ); - response.result = lnurlWithdrawEntity; + response.result = Object.assign(lnurlWithdrawEntity, { + lnurlDecoded, + }); } else { // LnurlWithdraw not created logger.debug( @@ -203,26 +207,6 @@ class LnurlWithdraw { ); this.processCallbacks(lnurlWithdrawEntity); - // const postdata = { - // lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, - // lnPayResponse: resp.result, - // }; - // Utils.post(lnurlWithdrawEntity.webhookUrl, postdata).then( - // async (response) => { - // if (response.status >= 200 && response.status < 400) { - // logger.debug( - // "LnurlWithdraw.lnServiceWithdraw, webhook called back" - // ); - - // lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdraw( - // lnurlWithdrawEntity - // ); - // lnurlWithdrawEntity.calledback = true; - // lnurlWithdrawEntity.calledbackTimestamp = new Date(); - // await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); - // } - // } - // ); } } } else { @@ -274,6 +258,7 @@ class LnurlWithdraw { const postdata = { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + bolt11: lnurlWithdrawEntity.bolt11, lnPayResponse: JSON.parse(lnurlWithdrawEntity.withdrawnDetails || ""), }; logger.debug("LnurlWithdraw.processCallbacks, postdata=", postdata); diff --git a/src/types/ILnurlWithdraw.ts b/src/types/ILnurlWithdraw.ts new file mode 100644 index 0000000..50724a2 --- /dev/null +++ b/src/types/ILnurlWithdraw.ts @@ -0,0 +1,5 @@ +import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; + +export default interface ILnurlWithdraw extends LnurlWithdrawEntity { + lnurlDecoded: string; +} diff --git a/src/types/IRespLnurlWithdraw.ts b/src/types/IRespLnurlWithdraw.ts index 9e4d0a6..e33f7f0 100644 --- a/src/types/IRespLnurlWithdraw.ts +++ b/src/types/IRespLnurlWithdraw.ts @@ -1,7 +1,7 @@ import { IResponseError } from "./jsonrpc/IResponseMessage"; -import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; +import ILnurlWithdraw from "./ILnurlWithdraw"; export default interface IRespLnurlWithdraw { - result?: LnurlWithdrawEntity; + result?: ILnurlWithdraw; error?: IResponseError; } From adc237c0a93fd42183dd081164f4a030c10ec764 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 23 Jul 2021 22:51:10 -0400 Subject: [PATCH 05/52] case fix 1 --- cypherapps/docker-compose.yaml | 19 ++++++++++++++++++- src/lib/LnurlWithdraw.ts | 1 - ...espLnserviceStatus.ts => IRespLnStatus.ts} | 0 ...awRequest.ts => IRespLnWithdrawRequest.ts} | 0 4 files changed, 18 insertions(+), 2 deletions(-) rename src/types/{IRespLnserviceStatus.ts => IRespLnStatus.ts} (100%) rename src/types/{IRespLnserviceWithdrawRequest.ts => IRespLnWithdrawRequest.ts} (100%) diff --git a/cypherapps/docker-compose.yaml b/cypherapps/docker-compose.yaml index 34146df..1682ba3 100644 --- a/cypherapps/docker-compose.yaml +++ b/cypherapps/docker-compose.yaml @@ -20,7 +20,24 @@ services: - "traefik.frontend.passHostHeader=true" - "traefik.enable=true" - "traefik.port=8000" - - "traefik.frontend.auth.basic.users=:$$2y$$05$$LFKGjKBkmWbI5RUFBqwonOWEcen4Yu.mU139fvD3flWcP8gUqLLaC" + - "traefik.frontend.auth.basic.users=" + deploy: + labels: + - "traefik.docker.network=cyphernodeappsnet" + - "traefik.frontend.rule=PathPrefixStrip:/lnurl" + - "traefik.frontend.passHostHeader=true" + - "traefik.enable=true" + - "traefik.port=8000" + - "traefik.frontend.auth.basic.users=" + replicas: 1 + placement: + constraints: + - node.labels.io.cyphernode == true + restart_policy: + condition: "any" + delay: 1s + update_config: + parallelism: 1 networks: cyphernodeappsnet: external: true diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 2f1b999..15cf4e2 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -14,7 +14,6 @@ import { Utils } from "./Utils"; import IRespLnPay from "../types/cyphernode/IRespLnPay"; import { LnServiceWithdrawValidator } from "../validators/LnServiceWithdrawValidator"; import { Scheduler } from "./Scheduler"; -import ILnurlWithdraw from "../types/ILnurlWithdraw"; class LnurlWithdraw { private _lnurlConfig: LnurlConfig; diff --git a/src/types/IRespLnserviceStatus.ts b/src/types/IRespLnStatus.ts similarity index 100% rename from src/types/IRespLnserviceStatus.ts rename to src/types/IRespLnStatus.ts diff --git a/src/types/IRespLnserviceWithdrawRequest.ts b/src/types/IRespLnWithdrawRequest.ts similarity index 100% rename from src/types/IRespLnserviceWithdrawRequest.ts rename to src/types/IRespLnWithdrawRequest.ts From e58d5707054a5c713af7ddce05d831302bf0ace5 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 23 Jul 2021 22:52:40 -0400 Subject: [PATCH 06/52] case fix 2 --- src/types/{IRespLnStatus.ts => IRespLnServiceStatus.ts} | 0 ...IRespLnWithdrawRequest.ts => IRespLnServiceWithdrawRequest.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/types/{IRespLnStatus.ts => IRespLnServiceStatus.ts} (100%) rename src/types/{IRespLnWithdrawRequest.ts => IRespLnServiceWithdrawRequest.ts} (100%) diff --git a/src/types/IRespLnStatus.ts b/src/types/IRespLnServiceStatus.ts similarity index 100% rename from src/types/IRespLnStatus.ts rename to src/types/IRespLnServiceStatus.ts diff --git a/src/types/IRespLnWithdrawRequest.ts b/src/types/IRespLnServiceWithdrawRequest.ts similarity index 100% rename from src/types/IRespLnWithdrawRequest.ts rename to src/types/IRespLnServiceWithdrawRequest.ts From 395b0e844869a3a7d4b6c44bcf94ae13d840d4ae Mon Sep 17 00:00:00 2001 From: kexkey Date: Sat, 24 Jul 2021 22:59:34 -0400 Subject: [PATCH 07/52] e2e test with https on preprod --- cypherapps/data/config.json | 4 ++-- cypherapps/docker-compose.yaml | 12 ++++++++---- src/lib/HttpServer.ts | 6 ++++-- src/lib/LnurlWithdraw.ts | 9 ++++++++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json index c0d91e0..e0af926 100644 --- a/cypherapps/data/config.json +++ b/cypherapps/data/config.json @@ -10,8 +10,8 @@ "CN_URL": "https://gatekeeper:2009/v0", "CN_API_ID": "003", "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", - "LN_SERVICE_SERVER": "http://.onion", - "LN_SERVICE_PORT": 80, + "LN_SERVICE_SERVER": "https://yourdomain", + "LN_SERVICE_PORT": 443, "LN_SERVICE_CTX": "/lnurl", "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", "LN_SERVICE_WITHDRAW_CTX": "/withdraw", diff --git a/cypherapps/docker-compose.yaml b/cypherapps/docker-compose.yaml index 1682ba3..e7a7b68 100644 --- a/cypherapps/docker-compose.yaml +++ b/cypherapps/docker-compose.yaml @@ -16,19 +16,23 @@ services: restart: always labels: - "traefik.docker.network=cyphernodeappsnet" - - "traefik.frontend.rule=PathPrefixStrip:/lnurl" +# PathPrefix won't be stripped because we don't want outsiders to access /api, only /lnurl/... + - "traefik.frontend.rule=PathPrefix:/lnurl" - "traefik.frontend.passHostHeader=true" - "traefik.enable=true" - "traefik.port=8000" - - "traefik.frontend.auth.basic.users=" +# Don't secure the PathPrefix /lnurl +# - "traefik.frontend.auth.basic.users=" deploy: labels: - "traefik.docker.network=cyphernodeappsnet" - - "traefik.frontend.rule=PathPrefixStrip:/lnurl" +# PathPrefix won't be stripped because we don't want outsiders to access /api, only /lnurl/... + - "traefik.frontend.rule=PathPrefix:/lnurl" - "traefik.frontend.passHostHeader=true" - "traefik.enable=true" - "traefik.port=8000" - - "traefik.frontend.auth.basic.users=" +# Don't secure the PathPrefix /lnurl +# - "traefik.frontend.auth.basic.users=" replicas: 1 placement: constraints: diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 05d1f51..6df7cfa 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -127,7 +127,8 @@ class HttpServer { // LN Service LNURL Withdraw Request this._httpServer.get( - this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX, + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX, async (req, res) => { logger.info( this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + ":", @@ -148,7 +149,8 @@ class HttpServer { // LN Service LNURL Withdraw this._httpServer.get( - this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, async (req, res) => { logger.info(this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX + ":", req.query); diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 15cf4e2..81cf885 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -126,7 +126,10 @@ class LnurlWithdraw { const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( secretToken ); - logger.debug("lnurlWithdrawEntity:", lnurlWithdrawEntity); + logger.debug( + "LnurlWithdraw.lnServiceWithdrawRequest, lnurlWithdrawEntity:", + lnurlWithdrawEntity + ); if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { result = { @@ -146,6 +149,8 @@ class LnurlWithdraw { result = { status: "ERROR", reason: "Invalid k1 value" }; } + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, responding:", result); + return result; } @@ -230,6 +235,8 @@ class LnurlWithdraw { }; } + logger.debug("LnurlWithdraw.lnServiceWithdraw, responding:", result); + return result; } From 3651b3d55bc37901d0da862ce46896e73ceaf896 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 26 Jul 2021 23:04:55 -0400 Subject: [PATCH 08/52] Expiration, delete, get, more tests --- Dockerfile | 31 +- README.md | 4 +- src/lib/HttpServer.ts | 33 +- src/lib/LnurlDB.ts | 61 +- src/lib/LnurlWithdraw.ts | 243 +++++-- .../CreateLnurlWithdrawValidator.ts | 14 +- tests/lnurl_withdraw.sh | 619 +++++++++++++++--- 7 files changed, 824 insertions(+), 181 deletions(-) diff --git a/Dockerfile b/Dockerfile index 02f3b26..a87c84f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,5 @@ -#FROM node:14.11.0-alpine3.11 - -#WORKDIR /lnurl - -#COPY package.json /lnurl - -#RUN apk add --update --no-cache --virtual .gyp \ -# python \ -# make \ -# g++ -#RUN npm install -#RUN apk del .gyp - -#COPY tsconfig.json /lnurl -#COPY src /lnurl/src - -#RUN npm run build - -#EXPOSE 9229 3000 - -#ENTRYPOINT [ "npm", "run", "start" ] - -#--------------------------------------------------- - FROM node:14.11.0-alpine3.11 as build-base + WORKDIR /lnurl COPY package.json /lnurl @@ -45,10 +22,10 @@ RUN apk del .gyp FROM base-slim WORKDIR /lnurl -#COPY tsconfig.json /lnurl -#COPY src /lnurl/src +COPY tsconfig.json /lnurl +COPY src /lnurl/src -#RUN npm run build +RUN npm run build EXPOSE 9229 3000 diff --git a/README.md b/README.md index fd1e7af..279f6f6 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,8 @@ sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" secretToken: string; webhookUrl?: string; -curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"amount":0.01,"description":"desc02","expiration":"2021-07-15 12:12","secretToken":"abc02","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api -{"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15 12:12","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} +curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"amount":0.01,"description":"desc02","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc02","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" id|amount|description|expiration|secret_token|webhook_url|lnurl|withdrawn_details|withdrawn_ts|active|created_ts|updated_ts diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 6df7cfa..7857f7a 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -49,6 +49,28 @@ class HttpServer { ); } + async deleteLnurlWithdraw( + params: object | undefined + ): Promise { + logger.debug("/deleteLnurlWithdraw params:", params); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lnurlWithdrawId = parseInt((params as any).lnurlWithdrawId); + + return await this._lnurlWithdraw.deleteLnurlWithdraw(lnurlWithdrawId); + } + + async getLnurlWithdraw( + params: object | undefined + ): Promise { + logger.debug("/getLnurlWithdraw params:", params); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lnurlWithdrawId = parseInt((params as any).lnurlWithdrawId); + + return await this._lnurlWithdraw.getLnurlWithdraw(lnurlWithdrawId); + } + async start(): Promise { logger.info("Starting incredible service"); @@ -77,7 +99,16 @@ class HttpServer { } case "getLnurlWithdraw": { - const result: IRespCreateLnurlWithdraw = await this.createLnurlWithdraw( + const result: IRespCreateLnurlWithdraw = await this.getLnurlWithdraw( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + + case "deleteLnurlWithdraw": { + const result: IRespCreateLnurlWithdraw = await this.deleteLnurlWithdraw( reqMessage.params || {} ); response.result = result.result; diff --git a/src/lib/LnurlDB.ts b/src/lib/LnurlDB.ts index a84f5f5..18ffda9 100644 --- a/src/lib/LnurlDB.ts +++ b/src/lib/LnurlDB.ts @@ -41,31 +41,55 @@ class LnurlDB { async saveLnurlWithdraw( lnurlWithdraw: LnurlWithdrawEntity ): Promise { - const lwr = await this._db?.manager + const lw = await this._db?.manager .getRepository(LnurlWithdrawEntity) .save(lnurlWithdraw); - return lwr as LnurlWithdrawEntity; + // We need to instantiate a new Date with expiration: + // https://github.com/typeorm/typeorm/issues/4320 + if (lw) { + if (lw.expiration) lw.expiration = new Date(lw.expiration); + lw.active = ((lw.active as unknown) as number) == 1; + lw.calledback = ((lw.calledback as unknown) as number) == 1; + } + + return lw as LnurlWithdrawEntity; } async getLnurlWithdrawBySecret( secretToken: string ): Promise { - const wr = await this._db?.manager + const lw = await this._db?.manager .getRepository(LnurlWithdrawEntity) .findOne({ where: { secretToken } }); - return wr as LnurlWithdrawEntity; + // We need to instantiate a new Date with expiration: + // https://github.com/typeorm/typeorm/issues/4320 + if (lw) { + if (lw.expiration) lw.expiration = new Date(lw.expiration); + lw.active = ((lw.active as unknown) as number) == 1; + lw.calledback = ((lw.calledback as unknown) as number) == 1; + } + + return lw as LnurlWithdrawEntity; } async getLnurlWithdraw( lnurlWithdrawEntity: LnurlWithdrawEntity ): Promise { - const wr = await this._db?.manager + const lw = await this._db?.manager .getRepository(LnurlWithdrawEntity) .findOne(lnurlWithdrawEntity); - return wr as LnurlWithdrawEntity; + // We need to instantiate a new Date with expiration: + // https://github.com/typeorm/typeorm/issues/4320 + if (lw) { + if (lw.expiration) lw.expiration = new Date(lw.expiration); + lw.active = ((lw.active as unknown) as number) == 1; + lw.calledback = ((lw.calledback as unknown) as number) == 1; + } + + return lw as LnurlWithdrawEntity; } async getLnurlWithdrawById( @@ -75,6 +99,14 @@ class LnurlDB { .getRepository(LnurlWithdrawEntity) .findOne(lnurlWithdrawId); + // We need to instantiate a new Date with expiration: + // https://github.com/typeorm/typeorm/issues/4320 + if (lw) { + if (lw.expiration) lw.expiration = new Date(lw.expiration); + lw.active = ((lw.active as unknown) as number) == 1; + lw.calledback = ((lw.calledback as unknown) as number) == 1; + } + return lw as LnurlWithdrawEntity; } @@ -82,8 +114,23 @@ class LnurlDB { const lws = await this._db?.manager .getRepository(LnurlWithdrawEntity) .find({ - where: { active: false, calledback: false, webhookUrl: Not(IsNull()) }, + where: { + active: false, + calledback: false, + webhookUrl: Not(IsNull()), + withdrawnDetails: Not(IsNull()), + }, + }); + + // We need to instantiate a new Date with expiration: + // https://github.com/typeorm/typeorm/issues/4320 + if (lws && lws.length > 0) { + lws.forEach((lw) => { + if (lw.expiration) lw.expiration = new Date(lw.expiration); + lw.active = ((lw.active as unknown) as number) == 1; + lw.calledback = ((lw.calledback as unknown) as number) == 1; }); + } return lws as LnurlWithdrawEntity[]; } diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 81cf885..2a713f6 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -117,6 +117,123 @@ class LnurlWithdraw { return response; } + async deleteLnurlWithdraw( + lnurlWithdrawId: number + ): Promise { + logger.info( + "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdrawId:", + lnurlWithdrawId + ); + + const response: IRespGetLnurlWithdraw = {}; + + if (lnurlWithdrawId) { + // Inputs are valid. + logger.debug("LnurlWithdraw.deleteLnurlWithdraw, Inputs are valid."); + + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById( + lnurlWithdrawId + ); + + if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, active lnurlWithdrawEntity found for this lnurlWithdrawId!" + ); + + lnurlWithdrawEntity.active = false; + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + const lnurlDecoded = await Utils.decodeBech32( + lnurlWithdrawEntity?.lnurl || "" + ); + + response.result = Object.assign(lnurlWithdrawEntity, { + lnurlDecoded, + }); + } else { + // Active LnurlWithdraw not found + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw not found or already deactivated." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw not found or already deactivated", + }; + } + } else { + // There is an error with inputs + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, there is an error with inputs." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + + async getLnurlWithdraw( + lnurlWithdrawId: number + ): Promise { + logger.info( + "LnurlWithdraw.getLnurlWithdraw, lnurlWithdrawId:", + lnurlWithdrawId + ); + + const response: IRespGetLnurlWithdraw = {}; + + if (lnurlWithdrawId) { + // Inputs are valid. + logger.debug("LnurlWithdraw.getLnurlWithdraw, Inputs are valid."); + + const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById( + lnurlWithdrawId + ); + + if (lnurlWithdrawEntity != null) { + logger.debug( + "LnurlWithdraw.getLnurlWithdraw, lnurlWithdrawEntity found for this lnurlWithdrawId!" + ); + + const lnurlDecoded = await Utils.decodeBech32( + lnurlWithdrawEntity.lnurl || "" + ); + + response.result = Object.assign(lnurlWithdrawEntity, { + lnurlDecoded, + }); + } else { + // Active LnurlWithdraw not found + logger.debug( + "LnurlWithdraw.getLnurlWithdraw, LnurlWithdraw not found." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw not found", + }; + } + } else { + // There is an error with inputs + logger.debug( + "LnurlWithdraw.getLnurlWithdraw, there is an error with inputs." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + async lnServiceWithdrawRequest( secretToken: string ): Promise { @@ -132,21 +249,39 @@ class LnurlWithdraw { ); if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { - result = { - tag: "withdrawRequest", - callback: - this._lnurlConfig.LN_SERVICE_SERVER + - ":" + - this._lnurlConfig.LN_SERVICE_PORT + - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, - k1: lnurlWithdrawEntity.secretToken, - defaultDescription: lnurlWithdrawEntity.description, - minWithdrawable: lnurlWithdrawEntity.amount, - maxWithdrawable: lnurlWithdrawEntity.amount, - }; + // Check expiration + + if ( + lnurlWithdrawEntity.expiration && + lnurlWithdrawEntity.expiration < new Date() + ) { + //Expired LNURL + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest: expired!"); + + result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); + + result = { + tag: "withdrawRequest", + callback: + this._lnurlConfig.LN_SERVICE_SERVER + + ":" + + this._lnurlConfig.LN_SERVICE_PORT + + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, + k1: lnurlWithdrawEntity.secretToken, + defaultDescription: lnurlWithdrawEntity.description, + minWithdrawable: lnurlWithdrawEntity.amount, + maxWithdrawable: lnurlWithdrawEntity.amount, + }; + } } else { - result = { status: "ERROR", reason: "Invalid k1 value" }; + if (lnurlWithdrawEntity.active) { + result = { status: "ERROR", reason: "Invalid k1 value" }; + } else { + result = { status: "ERROR", reason: "Deleted LNURL" }; + } } logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, responding:", result); @@ -174,54 +309,68 @@ class LnurlWithdraw { "LnurlWithdraw.lnServiceWithdraw, active lnurlWithdrawEntity found for this k1!" ); - lnurlWithdrawEntity.bolt11 = params.pr; + // Check expiration - const resp: IRespLnPay = await this._cyphernodeClient.lnPay({ - bolt11: params.pr, - expectedMsatoshi: lnurlWithdrawEntity.amount, - expectedDescription: lnurlWithdrawEntity.description, - }); + if ( + lnurlWithdrawEntity.expiration && + lnurlWithdrawEntity.expiration < new Date() + ) { + // Expired LNURL + logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); - if (resp.error) { - logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay error!"); + result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); - result = { status: "ERROR", reason: resp.error.message }; + lnurlWithdrawEntity.bolt11 = params.pr; - lnurlWithdrawEntity.withdrawnDetails = resp.error.message; + const resp: IRespLnPay = await this._cyphernodeClient.lnPay({ + bolt11: params.pr, + expectedMsatoshi: lnurlWithdrawEntity.amount, + expectedDescription: lnurlWithdrawEntity.description, + }); - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); - } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay success!"); + if (resp.error) { + logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay error!"); - result = { status: "OK" }; + result = { status: "ERROR", reason: resp.error.message }; + + lnurlWithdrawEntity.withdrawnDetails = resp.error.message; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay success!"); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.result); - lnurlWithdrawEntity.withdrawnTimestamp = new Date(); - lnurlWithdrawEntity.active = false; + result = { status: "OK" }; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.result); + lnurlWithdrawEntity.withdrawnTimestamp = new Date(); + lnurlWithdrawEntity.active = false; - if (lnurlWithdrawEntity.webhookUrl) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity ); - this.processCallbacks(lnurlWithdrawEntity); + if (lnurlWithdrawEntity.webhookUrl) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." + ); + + this.processCallbacks(lnurlWithdrawEntity); + } } } } else { + if (lnurlWithdrawEntity.active) { + result = { status: "ERROR", reason: "Invalid k1 value" }; + } else { + result = { status: "ERROR", reason: "Deleted LNURL" }; + } logger.debug( "LnurlWithdraw.lnServiceWithdraw, active lnurlWithdrawEntity NOT found for this k1!" ); - - result = { - status: "ERROR", - reason: "Invalid k1 value or inactive lnurlWithdrawRequest", - }; } } else { // There is an error with inputs @@ -265,7 +414,9 @@ class LnurlWithdraw { const postdata = { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, bolt11: lnurlWithdrawEntity.bolt11, - lnPayResponse: JSON.parse(lnurlWithdrawEntity.withdrawnDetails || ""), + lnPayResponse: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, }; logger.debug("LnurlWithdraw.processCallbacks, postdata=", postdata); diff --git a/src/validators/CreateLnurlWithdrawValidator.ts b/src/validators/CreateLnurlWithdrawValidator.ts index 7a3276c..a95fa70 100644 --- a/src/validators/CreateLnurlWithdrawValidator.ts +++ b/src/validators/CreateLnurlWithdrawValidator.ts @@ -3,10 +3,18 @@ import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; class CreateLnurlWithdrawValidator { static validateRequest(request: IReqCreateLnurlWithdraw): boolean { if (request.amount && request.secretToken) { - return true; - } else { - return false; + // Mandatory amount and secretToken found + if (request.expiration) { + if (!isNaN(new Date(request.expiration).valueOf())) { + // Expiration date is valid + return true; + } + } else { + // No expiration date + return true; + } } + return false; } } diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index 7c17429..2a28df1 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -1,113 +1,542 @@ #!/bin/sh +# Happy path: +# +# 1. Create a LNURL Withdraw +# 2. Get it and compare +# 3. User calls LNServiceWithdrawRequest with wrong k1 -> Error, wrong k1! +# 3. User calls LNServiceWithdrawRequest +# 4. User calls LNServiceWithdraw with wrong k1 -> Error, wrong k1! +# 4. User calls LNServiceWithdraw + +# Expired 1: +# +# 1. Create a LNURL Withdraw with expiration=now +# 2. Get it and compare +# 3. User calls LNServiceWithdrawRequest -> Error, expired! + +# Expired 2: +# +# 1. Create a LNURL Withdraw with expiration=now + 5 seconds +# 2. Get it and compare +# 3. User calls LNServiceWithdrawRequest +# 4. Sleep 5 seconds +# 5. User calls LNServiceWithdraw -> Error, expired! + +# Deleted 1: +# +# 1. Create a LNURL Withdraw with expiration=now +# 2. Get it and compare +# 3. Delete it +# 4. Get it and compare +# 5. User calls LNServiceWithdrawRequest -> Error, deleted! + +# Deleted 2: +# +# 1. Create a LNURL Withdraw with expiration=now + 5 seconds +# 2. Get it and compare +# 5. User calls LNServiceWithdrawRequest +# 3. Delete it +# 5. User calls LNServiceWithdraw -> Error, deleted! + . ./tests/colors.sh -echo -e "${Color_Off}" +trace() { + if [ "${1}" -le "${TRACING}" ]; then + local str="$(date -Is) $$ ${2}" + echo -e "${str}" 1>&2 + fi +} + +create_lnurl_withdraw() { + trace 1 "\n[create_lnurl_withdraw] ${BCyan}Service creates LNURL Withdraw...${Color_Off}" + + local callbackurl=${1} + + local invoicenumber=$RANDOM + trace 2 "[create_lnurl_withdraw] invoicenumber=${invoicenumber}" + local amount=$((10000+${invoicenumber})) + trace 2 "[create_lnurl_withdraw] amount=${amount}" + local expiration_offset=${2:-0} + local expiration=$(date -d @$(($(date -u +"%s")+${expiration_offset})) +"%Y-%m-%dT%H:%M:%SZ") + trace 2 "[create_lnurl_withdraw] expiration=${expiration}" + + # Service creates LNURL Withdraw + data='{"id":0,"method":"createLnurlWithdraw","params":{"amount":'${amount}',"description":"desc'${invoicenumber}'","expiration":"'${expiration}'","secretToken":"secret'${invoicenumber}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'"}}' + trace 2 "[create_lnurl_withdraw] data=${data}" + trace 2 "[create_lnurl_withdraw] Calling createLnurlWithdraw..." + local createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 2 "[create_lnurl_withdraw] createLnurlWithdraw=${createLnurlWithdraw}" + + # {"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + trace 2 "[create_lnurl_withdraw] lnurl=${lnurl}" + + echo "${createLnurlWithdraw}" +} + +get_lnurl_withdraw() { + trace 1 "\n[get_lnurl_withdraw] ${BCyan}Get LNURL Withdraw...${Color_Off}" + + local lnurl_withdraw_id=${1} + trace 2 "[get_lnurl_withdraw] lnurl_withdraw_id=${lnurl_withdraw_id}" + + # Service creates LNURL Withdraw + data='{"id":0,"method":"getLnurlWithdraw","params":{"lnurlWithdrawId":'${lnurl_withdraw_id}'}}' + trace 2 "[get_lnurl_withdraw] data=${data}" + trace 2 "[get_lnurl_withdraw] Calling getLnurlWithdraw..." + local getLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 2 "[get_lnurl_withdraw] getLnurlWithdraw=${getLnurlWithdraw}" + + echo "${getLnurlWithdraw}" +} + +delete_lnurl_withdraw() { + trace 1 "\n[delete_lnurl_withdraw] ${BCyan}Delete LNURL Withdraw...${Color_Off}" + + local lnurl_withdraw_id=${1} + trace 2 "[delete_lnurl_withdraw] lnurl_withdraw_id=${lnurl_withdraw_id}" + + # Service deletes LNURL Withdraw + data='{"id":0,"method":"deleteLnurlWithdraw","params":{"lnurlWithdrawId":'${lnurl_withdraw_id}'}}' + trace 2 "[delete_lnurl_withdraw] data=${data}" + trace 2 "[delete_lnurl_withdraw] Calling deleteLnurlWithdraw..." + local deleteLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 2 "[delete_lnurl_withdraw] deleteLnurlWithdraw=${deleteLnurlWithdraw}" + + local active=$(echo "${deleteLnurlWithdraw}" | jq ".result.active") + if [ "${active}" = "true" ]; then + trace 2 "[delete_lnurl_withdraw] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" + return 1 + fi + + echo "${deleteLnurlWithdraw}" +} + +decode_lnurl() { + trace 1 "\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}" + + local lnurl=${1} + local lnServicePrefix=${2} + + local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' + trace 2 "[decode_lnurl] data=${data}" + local decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 2 "[decode_lnurl] decodedLnurl=${decodedLnurl}" + local urlSuffix=$(echo "${decodedLnurl}" | jq -r ".result" | sed 's|'${lnServicePrefix}'||g') + trace 2 "[decode_lnurl] urlSuffix=${urlSuffix}" + + echo "${urlSuffix}" +} + +call_lnservice_withdraw_request() { + trace 1 "\n[call_lnservice_withdraw_request] ${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}" + + local urlSuffix=${1} + + local withdrawRequestResponse=$(curl -s lnurl:8000${urlSuffix}) + trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" + + echo "${withdrawRequestResponse}" +} + +create_bolt11() { + trace 1 "\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}" + + local amount=${1} + trace 2 "[create_bolt11] amount=${amount}" + local desc=${2} + trace 2 "[create_bolt11] desc=${desc}" + + local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${amount}',"label":"'${desc}'","description":"'${desc}'"}}' + trace 2 "[create_bolt11] data=${data}" + local invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 2 "[create_bolt11] invoice=${invoice}" + + echo "${invoice}" +} + +get_invoice_status() { + trace 1 "\n[get_invoice_status] ${BCyan}Let's make sure the invoice is unpaid first...${Color_Off}" + + local invoice=${1} + trace 2 "[get_invoice_status] invoice=${invoice}" + + local payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") + trace 2 "[get_invoice_status] payment_hash=${payment_hash}" + local data='{"id":1,"jsonrpc": "2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' + trace 2 "[get_invoice_status] data=${data}" + local invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 2 "[get_invoice_status] invoices=${invoices}" + local status=$(echo "${invoices}" | jq -r ".invoices[0].status") + trace 2 "[get_invoice_status] status=${status}" + + echo "${status}" +} + +call_lnservice_withdraw() { + trace 1 "\n[call_lnservice_withdraw] ${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}" + + local withdrawRequestResponse=${1} + local lnServicePrefix=${2} + local bolt11=${3} + + callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") + trace 2 "[call_lnservice_withdraw] callback=${callback}" + urlSuffix=$(echo "${callback}" | sed 's|'${lnServicePrefix}'||g') + trace 2 "[call_lnservice_withdraw] urlSuffix=${urlSuffix}" + k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") + trace 2 "[call_lnservice_withdraw] k1=${k1}" + + trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" + withdrawResponse=$(curl -s lnurl:8000${urlSuffix}?k1=${k1}\&pr=${bolt11}) + trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" + + echo "${withdrawResponse}" +} + +happy_path() { + # Happy path: + # + # 1. Create a LNURL Withdraw + # 2. Get it and compare + # 3. User calls LNServiceWithdrawRequest with wrong k1 -> Error, wrong k1! + # 3. User calls LNServiceWithdrawRequest + # 4. User calls LNServiceWithdraw with wrong k1 -> Error, wrong k1! + # 4. User calls LNServiceWithdraw + + trace 1 "\n[happy_path] ${On_Yellow}${BBlack} Happy path! ${Color_Off}" + + local callbackurl=${1} + local lnServicePrefix=${2} + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 15) + trace 2 "[happy_path] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + #trace 2 "lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 2 "[happy_path] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 2 "[happy_path] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[happy_path] EQUALS!" + else + trace 1 "[happy_path] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + return 1 + fi + + # Decode LNURL + local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") + trace 2 "[happy_path] urlSuffix=${urlSuffix}" + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + trace 2 "[happy_path] withdrawRequestResponse=${withdrawRequestResponse}" + + # Create bolt11 for LN Service LNURL Withdraw + local amount=$(echo "${createLnurlWithdraw}" | jq -r '.result.amount') + local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') + local invoice=$(create_bolt11 "${amount}" "${description}") + trace 2 "[happy_path] invoice=${invoice}" + local bolt11=$(echo ${invoice} | jq -r ".bolt11") + trace 2 "[happy_path] bolt11=${bolt11}" + + # We want to see that that invoice is unpaid first... + local status=$(get_invoice_status "${invoice}") + trace 2 "[happy_path] status=${status}" + + # User calls LN Service LNURL Withdraw + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") + trace 2 "[happy_path] withdrawResponse=${withdrawResponse}" + + trace 2 "[happy_path] Sleeping 5 seconds..." + sleep 5 + + # We want to see if payment received (invoice status paid) + status=$(get_invoice_status "${invoice}") + trace 2 "[happy_path] status=${status}" + + if [ "${status}" = "paid" ]; then + trace 1 "\n[happy_path] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + date + return 0 + else + trace 1 "\n[happy_path] ${On_Red}${BBlack} FAILURE! ${Color_Off}" + date + return 1 + fi +} + +expired1() { + # Expired 1: + # + # 1. Create a LNURL Withdraw with expiration=now + # 2. Get it and compare + # 3. User calls LNServiceWithdrawRequest -> Error, expired! + + trace 1 "\n[expired1] ${On_Yellow}${BBlack} Expired 1! ${Color_Off}" + + local callbackurl=${1} + local lnServicePrefix=${2} + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) + trace 2 "[expired1] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + #trace 2 "lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 2 "[expired1] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 2 "[expired1] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[expired1] EQUALS!" + else + trace 1 "[expired1] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + return 1 + fi + + # Decode LNURL + local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") + trace 2 "[expired1] urlSuffix=${urlSuffix}" + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + trace 2 "[expired1] withdrawRequestResponse=${withdrawRequestResponse}" + + echo "${withdrawRequestResponse}" | grep -qi "expired" + if [ "$?" -ne "0" ]; then + trace 1 "[expired1] ${On_Red}${BBlack} NOT EXPIRED! ${Color_Off}" + return 1 + else + trace 1 "\n[expired1] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + fi +} + +expired2() { + # Expired 2: + # + # 1. Create a LNURL Withdraw with expiration=now + 5 seconds + # 2. Get it and compare + # 3. User calls LNServiceWithdrawRequest + # 4. Sleep 5 seconds + # 5. User calls LNServiceWithdraw -> Error, expired! + + trace 1 "\n[expired2] ${On_Yellow}${BBlack} Expired 2! ${Color_Off}" + + local callbackurl=${1} + local lnServicePrefix=${2} + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) + trace 2 "[expired2] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + #trace 2 "lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 2 "[expired2] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 2 "[expired2] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[expired2] EQUALS!" + else + trace 1 "[expired2] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + return 1 + fi + + # Decode LNURL + local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") + trace 2 "[expired2] urlSuffix=${urlSuffix}" + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + trace 2 "[expired2] withdrawRequestResponse=${withdrawRequestResponse}" + + # Create bolt11 for LN Service LNURL Withdraw + local amount=$(echo "${createLnurlWithdraw}" | jq -r '.result.amount') + local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') + local invoice=$(create_bolt11 "${amount}" "${description}") + trace 2 "[expired2] invoice=${invoice}" + local bolt11=$(echo ${invoice} | jq -r ".bolt11") + trace 2 "[expired2] bolt11=${bolt11}" + + trace 2 "[expired2] Sleeping 5 seconds..." + sleep 5 + + # User calls LN Service LNURL Withdraw + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") + trace 2 "[expired2] withdrawResponse=${withdrawResponse}" + + echo "${withdrawResponse}" | grep -qi "expired" + if [ "$?" -ne "0" ]; then + trace 1 "[expired2] ${On_Red}${BBlack} NOT EXPIRED! ${Color_Off}" + return 1 + else + trace 1 "\n[expired2] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + fi +} + +deleted1() { + # Deleted 1: + # + # 1. Create a LNURL Withdraw with expiration=now + # 2. Get it and compare + # 3. Delete it + # 4. Get it and compare + # 5. User calls LNServiceWithdrawRequest -> Error, deleted! + + trace 1 "\n[deleted1] ${On_Yellow}${BBlack} Deleted 1! ${Color_Off}" + + local callbackurl=${1} + local lnServicePrefix=${2} + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) + trace 2 "[deleted1] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + #trace 2 "lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 2 "[deleted1] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 2 "[deleted1] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[deleted1] EQUALS!" + else + trace 1 "[deleted1] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + return 1 + fi + + local delete_lnurl_withdraw=$(delete_lnurl_withdraw ${lnurl_withdraw_id}) + trace 2 "[deleted1] delete_lnurl_withdraw=${delete_lnurl_withdraw}" + local deleted=$(echo "${get_lnurl_withdraw}" | jq '.result.active = false | del(.result.updatedAt)') + trace 2 "[deleted1] deleted=${deleted}" + + get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id} | jq 'del(.result.updatedAt)') + trace 2 "[deleted1] get_lnurl_withdraw=${get_lnurl_withdraw}" + equals=$(jq --argjson a "${deleted}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 2 "[deleted1] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[deleted1] EQUALS!" + else + trace 1 "[deleted1] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + return 1 + fi + + # Decode LNURL + local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") + trace 2 "[deleted1] urlSuffix=${urlSuffix}" + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + trace 2 "[deleted1] withdrawRequestResponse=${withdrawRequestResponse}" + + echo "${withdrawRequestResponse}" | grep -qi "deleted" + if [ "$?" -ne "0" ]; then + trace 1 "[deleted1] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" + return 1 + else + trace 1 "\n[deleted1] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + fi +} + +deleted2() { + # Deleted 2: + # + # 1. Create a LNURL Withdraw with expiration=now + 5 seconds + # 2. Get it and compare + # 5. User calls LNServiceWithdrawRequest + # 3. Delete it + # 5. User calls LNServiceWithdraw -> Error, deleted! + + trace 1 "\n[deleted2] ${On_Yellow}${BBlack} Deleted 2! ${Color_Off}" + + local callbackurl=${1} + local lnServicePrefix=${2} + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) + trace 2 "[deleted2] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + #trace 2 "lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 2 "[deleted2] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 2 "[deleted2] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[deleted2] EQUALS!" + else + trace 1 "[deleted2] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + return 1 + fi + + # Decode LNURL + local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") + trace 2 "[deleted2] urlSuffix=${urlSuffix}" + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + trace 2 "[deleted2] withdrawRequestResponse=${withdrawRequestResponse}" + + # Create bolt11 for LN Service LNURL Withdraw + local amount=$(echo "${createLnurlWithdraw}" | jq -r '.result.amount') + local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') + local invoice=$(create_bolt11 "${amount}" "${description}") + trace 2 "[deleted2] invoice=${invoice}" + local bolt11=$(echo ${invoice} | jq -r ".bolt11") + trace 2 "[deleted2] bolt11=${bolt11}" + + local delete_lnurl_withdraw=$(delete_lnurl_withdraw ${lnurl_withdraw_id}) + trace 2 "[deleted2] delete_lnurl_withdraw=${delete_lnurl_withdraw}" + local deleted=$(echo "${get_lnurl_withdraw}" | jq '.result.active = false') + trace 2 "[deleted2] deleted=${deleted}" + + # User calls LN Service LNURL Withdraw + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") + trace 2 "[deleted2] withdrawResponse=${withdrawResponse}" + + echo "${withdrawResponse}" | grep -qi "deleted" + if [ "$?" -ne "0" ]; then + trace 1 "[deleted2] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" + return 1 + else + trace 1 "\n[deleted2] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + fi +} + +TRACING=1 + +trace 2 "${Color_Off}" date # Install needed packages -echo -e "\n${BCyan}Installing needed packages...${Color_Off}" +trace 2 "\n${BCyan}Installing needed packages...${Color_Off}" apk add curl jq # Initializing test variables -echo -e "\n${BCyan}Initializing test variables...${Color_Off}" +trace 2 "\n${BCyan}Initializing test variables...${Color_Off}" callbackservername="cb" callbackserverport="1111" callbackurl="http://${callbackservername}:${callbackserverport}" -invoicenumber=$RANDOM -amount=$((10000+${invoicenumber})) -expiration=$(date -d @$(($(date +"%s")+3600)) +"%F %T") -#echo "invoicenumber=${invoicenumber}" -#echo "amount=${amount}" -#echo "expiration=${expiration}" - # Get config from lnurl cypherapp -echo -e "\n${BCyan}Getting configuration from lnurl cypherapp...${Color_Off}" +trace 2 "\n${BCyan}Getting configuration from lnurl cypherapp...${Color_Off}" data='{"id":0,"method":"getConfig","params":[]}' lnurlConfig=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) -#echo "lnurlConfig=${lnurlConfig}" -lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER):\(.LN_SERVICE_PORT)\(.LN_SERVICE_CTX)"') -#echo "lnServicePrefix=${lnServicePrefix}" - -# Service creates LNURL Withdraw -echo -e "\n${BCyan}Service creates LNURL Withdraw...${Color_Off}" -data='{"id":0,"method":"createLnurlWithdraw","params":{"amount":'${amount}',"description":"desc'${invoicenumber}'","expiration":"'${expiration}'","secretToken":"secret'${invoicenumber}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'"}}' -#echo "data=${data}" -#echo "Calling createLnurlWithdraw..." -createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) -#echo "createLnurlWithdraw=${createLnurlWithdraw}" - -# {"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15 12:12","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} -lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") -echo "lnurl=${lnurl}" - -# Decode LNURL -echo -e "\n${BCyan}Decoding LNURL...${Color_Off}" -data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' -#echo ; echo "data=${data}" -decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) -echo "decodedLnurl=${decodedLnurl}" -urlSuffix=$(echo "${decodedLnurl}" | jq -r ".result" | sed 's|'${lnServicePrefix}'||g') -#echo "urlSuffix=${urlSuffix}" - -# User calls LN Service LNURL Withdraw Request -echo -e "\n${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}" -withdrawRequestResponse=$(curl -s lnurl:8000${urlSuffix}) -echo "withdrawRequestResponse=${withdrawRequestResponse}" - -# User calls LN Service LNURL Withdraw -echo -e "\n${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}" -callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") -#echo "callback=${callback}" -urlSuffix=$(echo "${callback}" | sed 's|'${lnServicePrefix}'||g') -#echo "urlSuffix=${urlSuffix}" -k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") -#echo "k1=${k1}" - -# Create bolt11 for LN Service LNURL Withdraw -echo -e "\n${BCyan}User creates bolt11 for the payment...${Color_Off}" -label="invoice${invoicenumber}" -desc="Invoice number ${invoicenumber}" -data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${amount}',"label":"'${label}'","description":"'${desc}'"}}' -#echo ; echo "data=${data}" -invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) -#echo "invoice=${invoice}" -bolt11=$(echo ${invoice} | jq -r ".bolt11") -echo "bolt11=${bolt11}" - -# We want to see that that invoice is unpaid first... -echo -e "\n${BCyan}Let's make sure the invoice is unpaid first...${Color_Off}" -payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") -#echo "payment_hash=${payment_hash}" -data='{"id":1,"jsonrpc": "2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' -#echo "data=${data}" -invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) -#echo "invoices=${invoices}" -status=$(echo "${invoices}" | jq -r ".invoices[0].status") -echo "status=${status}" - -# Actual call to LN Service LNURL Withdraw -echo -e "\n${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" -withdrawResponse=$(curl -s lnurl:8000${urlSuffix}?k1=${k1}\&pr=${bolt11}) -echo "withdrawResponse=${withdrawResponse}" - -# We want to see if payment received (invoice status paid) -echo -e "\n${BCyan}Sleeping 5 seconds...${Color_Off}" -sleep 5 -echo -e "\n${BCyan}Let's see if invoice is paid...${Color_Off}" -invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) -#echo "invoices=${invoices}" -status=$(echo "${invoices}" | jq -r ".invoices[0].status") -echo "status=${status}" - -if [ "${status}" = "paid" ]; then - echo -e "\n${BGreen}SUCCESS!${Color_Off}\n" - date - return 0 -else - echo -e "\n${BRed}FAILURE!${Color_Off}\n" - date - return 1 -fi +trace 2 "lnurlConfig=${lnurlConfig}" +lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER):\(.LN_SERVICE_PORT)"') +trace 2 "lnServicePrefix=${lnServicePrefix}" +happy_path "${callbackurl}" "${lnServicePrefix}" \ +&& expired1 "${callbackurl}" "${lnServicePrefix}" \ +&& expired2 "${callbackurl}" "${lnServicePrefix}" \ +&& deleted1 "${callbackurl}" "${lnServicePrefix}" \ +&& deleted2 "${callbackurl}" "${lnServicePrefix}" From 4d6c6e4b7f7119a8ebd2193dc69ca27c4ef9e801 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 30 Jul 2021 10:46:44 -0400 Subject: [PATCH 09/52] Locks on delete lnurlwithdraw to avoid doublepay --- src/lib/HttpServer.ts | 58 +++++++++++++++++++++++++++++++++------- src/lib/LnurlWithdraw.ts | 32 +++++++++++----------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 7857f7a..4f33a0e 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -14,6 +14,8 @@ import { Utils } from "./Utils"; import IRespCreateLnurlWithdraw from "../types/IRespLnurlWithdraw"; import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; +import IRespLnServiceWithdrawRequest from "../types/IRespLnServiceWithdrawRequest"; +import IRespLnServiceStatus from "../types/IRespLnServiceStatus"; class HttpServer { // Create a new express application instance @@ -108,9 +110,21 @@ class HttpServer { } case "deleteLnurlWithdraw": { - const result: IRespCreateLnurlWithdraw = await this.deleteLnurlWithdraw( - reqMessage.params || {} + let result: IRespCreateLnurlWithdraw = {}; + + result = await this._lock.acquire( + "deleteLnurlWithdraw", + async (): Promise => { + logger.debug( + "acquired lock deleteLnurlWithdraw in deleteLnurlWithdraw" + ); + return await this.deleteLnurlWithdraw(reqMessage.params || {}); + } + ); + logger.debug( + "released lock deleteLnurlWithdraw in deleteLnurlWithdraw" ); + response.result = result.result; response.error = result.error; break; @@ -166,8 +180,21 @@ class HttpServer { req.query ); - const response = await this._lnurlWithdraw.lnServiceWithdrawRequest( - req.query.s as string + let response: IRespLnServiceWithdrawRequest = {}; + + response = await this._lock.acquire( + "deleteLnurlWithdraw", + async (): Promise => { + logger.debug( + "acquired lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" + ); + return await this._lnurlWithdraw.lnServiceWithdrawRequest( + req.query.s as string + ); + } + ); + logger.debug( + "released lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" ); if (response.status) { @@ -185,11 +212,24 @@ class HttpServer { async (req, res) => { logger.info(this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX + ":", req.query); - const response = await this._lnurlWithdraw.lnServiceWithdraw({ - k1: req.query.k1, - pr: req.query.pr, - balanceNotify: req.query.balanceNotify, - } as IReqLnurlWithdraw); + let response: IRespLnServiceStatus = {}; + + response = await this._lock.acquire( + "deleteLnurlWithdraw", + async (): Promise => { + logger.debug( + "acquired lock deleteLnurlWithdraw in LN Service LNURL Withdraw" + ); + return await this._lnurlWithdraw.lnServiceWithdraw({ + k1: req.query.k1, + pr: req.query.pr, + balanceNotify: req.query.balanceNotify, + } as IReqLnurlWithdraw); + } + ); + logger.debug( + "released lock deleteLnurlWithdraw in LN Service LNURL Withdraw" + ); if (response.status === "ERROR") { res.status(400).json(response); diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 2a713f6..2d0590b 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -248,7 +248,11 @@ class LnurlWithdraw { lnurlWithdrawEntity ); - if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + if (lnurlWithdrawEntity == null) { + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, invalid k1 value:"); + + result = { status: "ERROR", reason: "Invalid k1 value" }; + } else if (lnurlWithdrawEntity.active) { // Check expiration if ( @@ -277,11 +281,9 @@ class LnurlWithdraw { }; } } else { - if (lnurlWithdrawEntity.active) { - result = { status: "ERROR", reason: "Invalid k1 value" }; - } else { - result = { status: "ERROR", reason: "Deleted LNURL" }; - } + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, deactivated LNURL"); + + result = { status: "ERROR", reason: "Deactivated LNURL" }; } logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, responding:", result); @@ -304,13 +306,16 @@ class LnurlWithdraw { params.k1 ); - if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + if (lnurlWithdrawEntity == null) { + logger.debug("LnurlWithdraw.lnServiceWithdraw, invalid k1 value!"); + + result = { status: "ERROR", reason: "Invalid k1 value" }; + } else if (lnurlWithdrawEntity.active) { logger.debug( "LnurlWithdraw.lnServiceWithdraw, active lnurlWithdrawEntity found for this k1!" ); // Check expiration - if ( lnurlWithdrawEntity.expiration && lnurlWithdrawEntity.expiration < new Date() @@ -363,14 +368,9 @@ class LnurlWithdraw { } } } else { - if (lnurlWithdrawEntity.active) { - result = { status: "ERROR", reason: "Invalid k1 value" }; - } else { - result = { status: "ERROR", reason: "Deleted LNURL" }; - } - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, active lnurlWithdrawEntity NOT found for this k1!" - ); + logger.debug("LnurlWithdraw.lnServiceWithdraw, deactivated LNURL!"); + + result = { status: "ERROR", reason: "Deactivated LNURL" }; } } else { // There is an error with inputs From cad6653e83a774014b5759d7491f598869fb1080 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 30 Jul 2021 14:27:53 -0400 Subject: [PATCH 10/52] Tests for delete lnurlwithdraw --- src/lib/LnurlWithdraw.ts | 18 ++++++++++++++---- tests/lnurl_withdraw.sh | 18 +++++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 2d0590b..012ae35 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -135,7 +135,17 @@ class LnurlWithdraw { lnurlWithdrawId ); - if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + // if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + if (lnurlWithdrawEntity == null) { + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdraw not found" + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw not found", + }; + } else if (lnurlWithdrawEntity.active) { logger.debug( "LnurlWithdraw.deleteLnurlWithdraw, active lnurlWithdrawEntity found for this lnurlWithdrawId!" ); @@ -153,14 +163,14 @@ class LnurlWithdraw { lnurlDecoded, }); } else { - // Active LnurlWithdraw not found + // LnurlWithdraw already deactivated logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw not found or already deactivated." + "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already deactivated." ); response.error = { code: ErrorCodes.InvalidRequest, - message: "LnurlWithdraw not found or already deactivated", + message: "LnurlWithdraw already deactivated", }; } } else { diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index 2a28df1..832c9a0 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -429,6 +429,18 @@ deleted1() { return 1 fi + # Delete it twice... + trace 2 "[deleted1] Let's delete it again..." + delete_lnurl_withdraw=$(delete_lnurl_withdraw ${lnurl_withdraw_id}) + trace 2 "[deleted1] delete_lnurl_withdraw=${delete_lnurl_withdraw}" + echo "${delete_lnurl_withdraw}" | grep -qi "already deactivated" + if [ "$?" -ne "0" ]; then + trace 1 "[deleted1] ${On_Red}${BBlack} Should return an error because already deactivated! ${Color_Off}" + return 1 + else + trace 1 "\n[deleted1] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + fi + # Decode LNURL local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") trace 2 "[deleted1] urlSuffix=${urlSuffix}" @@ -437,7 +449,7 @@ deleted1() { local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") trace 2 "[deleted1] withdrawRequestResponse=${withdrawRequestResponse}" - echo "${withdrawRequestResponse}" | grep -qi "deleted" + echo "${withdrawRequestResponse}" | grep -qi "Deactivated" if [ "$?" -ne "0" ]; then trace 1 "[deleted1] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" return 1 @@ -503,7 +515,7 @@ deleted2() { local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") trace 2 "[deleted2] withdrawResponse=${withdrawResponse}" - echo "${withdrawResponse}" | grep -qi "deleted" + echo "${withdrawResponse}" | grep -qi "Deactivated" if [ "$?" -ne "0" ]; then trace 1 "[deleted2] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" return 1 @@ -512,7 +524,7 @@ deleted2() { fi } -TRACING=1 +TRACING=2 trace 2 "${Color_Off}" date From 32b757fb9be9dbd2ee262bc815274c372e701764 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 30 Jul 2021 16:15:43 -0400 Subject: [PATCH 11/52] secret_token unicity catch --- src/lib/LnurlWithdraw.ts | 21 +++++++++++++----- tests/lnurl_withdraw.sh | 46 ++++++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 012ae35..ed992b6 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -77,11 +77,22 @@ class LnurlWithdraw { const lnurl = await Utils.encodeBech32(lnurlDecoded); - const lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - Object.assign(reqCreateLnurlWithdraw as LnurlWithdrawEntity, { - lnurl: lnurl, - }) - ); + let lnurlWithdrawEntity: LnurlWithdrawEntity; + try { + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + Object.assign(reqCreateLnurlWithdraw as LnurlWithdrawEntity, { + lnurl: lnurl, + }) + ); + } catch (ex) { + logger.debug("ex:", ex); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: ex.message, + }; + return response; + } if (lnurlWithdrawEntity) { logger.debug( diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index 832c9a0..3771205 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -53,7 +53,7 @@ create_lnurl_withdraw() { local callbackurl=${1} - local invoicenumber=$RANDOM + local invoicenumber=${3:-$RANDOM} trace 2 "[create_lnurl_withdraw] invoicenumber=${invoicenumber}" local amount=$((10000+${invoicenumber})) trace 2 "[create_lnurl_withdraw] amount=${amount}" @@ -524,6 +524,38 @@ deleted2() { fi } +duplicate_token() { + # duplicate_token: + # + # 1. Create a LNURL Withdraw with expiration=now + 5 seconds + # 2. Create another LNURL Withdraw with the same secret_token + + trace 1 "\n[duplicate_token] ${On_Yellow}${BBlack} Duplicate token! ${Color_Off}" + + local callbackurl=${1} + local lnServicePrefix=${2} + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) + trace 2 "[duplicate_token] createLnurlWithdraw=${createLnurlWithdraw}" + local secret_token=$(echo "${createLnurlWithdraw}" | jq -r ".result.secretToken") + trace 2 "secret_token=${secret_token}" + local invoicenumber=$(echo "${secret_token}" | sed 's/secret//g') + trace 2 "invoicenumber=${invoicenumber}" + + # Service creates another LNURL Withdraw with same secret token + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5 "${invoicenumber}") + trace 2 "[duplicate_token] createLnurlWithdraw=${createLnurlWithdraw}" + + echo "${createLnurlWithdraw}" | jq ".error.message" | grep -iq "unique" + if [ "$?" -ne "0" ]; then + trace 1 "[duplicate_token] ${On_Red}${BBlack} Should have unicity error message! ${Color_Off}" + return 1 + else + trace 1 "\n[duplicate_token] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + fi +} + TRACING=2 trace 2 "${Color_Off}" @@ -547,8 +579,10 @@ trace 2 "lnurlConfig=${lnurlConfig}" lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER):\(.LN_SERVICE_PORT)"') trace 2 "lnServicePrefix=${lnServicePrefix}" -happy_path "${callbackurl}" "${lnServicePrefix}" \ -&& expired1 "${callbackurl}" "${lnServicePrefix}" \ -&& expired2 "${callbackurl}" "${lnServicePrefix}" \ -&& deleted1 "${callbackurl}" "${lnServicePrefix}" \ -&& deleted2 "${callbackurl}" "${lnServicePrefix}" +# happy_path "${callbackurl}" "${lnServicePrefix}" \ +# && expired1 "${callbackurl}" "${lnServicePrefix}" \ +# && expired2 "${callbackurl}" "${lnServicePrefix}" \ +# && deleted1 "${callbackurl}" "${lnServicePrefix}" \ +# && deleted2 "${callbackurl}" "${lnServicePrefix}" \ +duplicate_token "${callbackurl}" "${lnServicePrefix}" + From 6a21f60e71cdd1fb489194f20d443feb5216064f Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 3 Aug 2021 12:48:42 -0400 Subject: [PATCH 12/52] Added script LNURL wallet --- tests/README.md | 2 +- tests/lnurl_withdraw.sh | 12 +-- tests/lnurl_withdraw_wallet.sh | 124 +++++++++++++++++++++++++++++ tests/run_lnurl_withdraw_wallet.sh | 5 ++ 4 files changed, 136 insertions(+), 7 deletions(-) create mode 100755 tests/lnurl_withdraw_wallet.sh create mode 100755 tests/run_lnurl_withdraw_wallet.sh diff --git a/tests/README.md b/tests/README.md index a1300df..c997583 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,7 +5,7 @@ 1. Have a Cyphernode instance setup in regtest with LN active. 2. Let's double everything LN-related: * Duplicate the `lightning` block in `dist/docker-compose.yaml` appending a `2` to the names (see below) - * Copy `apps/sparkwallet` to `apps/sparkwallet2` and change `apps/sparkwallet2/docker-compose.yaml` appending a `2` to the names (see below) + * Copy `apps/sparkwallet` to `apps/sparkwallet2` and change `apps/sparkwallet2/docker-compose.yaml` appending a `2` to the names and different port (see below) 3. Mine enough blocks to have regtest coins to open channels between the two LN nodes (use `ln_setup.sh`) 4. Open a channel between the two nodes (use `ln_setup.sh`) 5. If connection is lost between the two nodes (eg. after a restart of Cyphernode), reconnect the two (use `ln_reconnect.sh`) diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index 3771205..20bc16f 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -579,10 +579,10 @@ trace 2 "lnurlConfig=${lnurlConfig}" lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER):\(.LN_SERVICE_PORT)"') trace 2 "lnServicePrefix=${lnServicePrefix}" -# happy_path "${callbackurl}" "${lnServicePrefix}" \ -# && expired1 "${callbackurl}" "${lnServicePrefix}" \ -# && expired2 "${callbackurl}" "${lnServicePrefix}" \ -# && deleted1 "${callbackurl}" "${lnServicePrefix}" \ -# && deleted2 "${callbackurl}" "${lnServicePrefix}" \ -duplicate_token "${callbackurl}" "${lnServicePrefix}" +happy_path "${callbackurl}" "${lnServicePrefix}" \ +&& expired1 "${callbackurl}" "${lnServicePrefix}" \ +&& expired2 "${callbackurl}" "${lnServicePrefix}" \ +&& deleted1 "${callbackurl}" "${lnServicePrefix}" \ +&& deleted2 "${callbackurl}" "${lnServicePrefix}" \ +&& duplicate_token "${callbackurl}" "${lnServicePrefix}" diff --git a/tests/lnurl_withdraw_wallet.sh b/tests/lnurl_withdraw_wallet.sh new file mode 100755 index 0000000..a9ebb8a --- /dev/null +++ b/tests/lnurl_withdraw_wallet.sh @@ -0,0 +1,124 @@ +#!/bin/sh + +. /tests/colors.sh + +trace() { + if [ "${1}" -le "${TRACING}" ]; then + local str="$(date -Is) $$ ${2}" + echo -e "${str}" 1>&2 + fi +} + +decode_lnurl() { + trace 1 "\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}" + + local lnurl=${1} + trace 2 "[decode_lnurl] lnurl=${lnurl}" + + local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' + trace 2 "[decode_lnurl] data=${data}" + local decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 2 "[decode_lnurl] decodedLnurl=${decodedLnurl}" + + echo "${decodedLnurl}" +} + +call_lnservice_withdraw_request() { + trace 1 "\n[call_lnservice_withdraw_request] ${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}" + + local url=${1} + trace 2 "[decode_lnurl] url=${url}" + + local withdrawRequestResponse=$(curl -s ${url}) + trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" + + echo "${withdrawRequestResponse}" +} + +create_bolt11() { + trace 1 "\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}" + + local amount=${1} + trace 2 "[create_bolt11] amount=${amount}" + local label=${2} + trace 2 "[create_bolt11] label=${label}" + local desc=${3} + trace 2 "[create_bolt11] desc=${desc}" + + local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${amount}',"label":"'${label}'","description":"'${desc}'"}}' + trace 2 "[create_bolt11] data=${data}" + local invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 2 "[create_bolt11] invoice=${invoice}" + + echo "${invoice}" +} + +get_invoice_status() { + trace 1 "\n[get_invoice_status] ${BCyan}Let's make sure the invoice is unpaid first...${Color_Off}" + + local invoice=${1} + trace 2 "[get_invoice_status] invoice=${invoice}" + + local payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") + trace 2 "[get_invoice_status] payment_hash=${payment_hash}" + local data='{"id":1,"jsonrpc": "2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' + trace 2 "[get_invoice_status] data=${data}" + local invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 2 "[get_invoice_status] invoices=${invoices}" + local status=$(echo "${invoices}" | jq -r ".invoices[0].status") + trace 2 "[get_invoice_status] status=${status}" + + echo "${status}" +} + +call_lnservice_withdraw() { + trace 1 "\n[call_lnservice_withdraw] ${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}" + + local withdrawRequestResponse=${1} + trace 2 "[call_lnservice_withdraw] withdrawRequestResponse=${withdrawRequestResponse}" + local bolt11=${2} + trace 2 "[call_lnservice_withdraw] bolt11=${bolt11}" + + callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") + trace 2 "[call_lnservice_withdraw] callback=${callback}" + k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") + trace 2 "[call_lnservice_withdraw] k1=${k1}" + + trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" + trace 2 "url=${callback}?k1=${k1}\&pr=${bolt11}" + withdrawResponse=$(curl -s ${callback}?k1=${k1}\&pr=${bolt11}) + trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" + + echo "${withdrawResponse}" +} + +TRACING=2 + +trace 2 "${Color_Off}" +date + +# Install needed packages +trace 2 "\n${BCyan}Installing needed packages...${Color_Off}" +apk add curl jq + +lnurl=${1} +trace 2 "lnurl=${lnurl}" + +decoded_lnurl=$(decode_lnurl "${lnurl}") +trace 2 "decoded_lnurl=${decoded_lnurl}" +url=$(echo "${decoded_lnurl}" | jq -r ".result") +trace 2 "url=${url}" + +withdrawRequestResponse=$(call_lnservice_withdraw_request "${url}") +trace 2 "withdrawRequestResponse=${withdrawRequestResponse}" +amount=$(echo "${withdrawRequestResponse}" | jq -r ".maxWithdrawable") +trace 2 "amount=${amount}" +desc=$(echo "${withdrawRequestResponse}" | jq -r ".defaultDescription") +trace 2 "desc=${desc}" + +invoice=$(create_bolt11 ${amount} "$RANDOM" "${desc}") +trace 2 "invoice=${invoice}" +bolt11=$(echo "${invoice}" | jq -r ".bolt11") + +withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") +trace 2 "withdrawResponse=${withdrawResponse}" diff --git a/tests/run_lnurl_withdraw_wallet.sh b/tests/run_lnurl_withdraw_wallet.sh new file mode 100755 index 0000000..ae67694 --- /dev/null +++ b/tests/run_lnurl_withdraw_wallet.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +#./startcallbackserver.sh & + +docker run --rm -it -v "$PWD:/tests" --network=cyphernodeappsnet alpine /tests/lnurl_withdraw_wallet.sh $1 From aa863bb5352dc72afd8d044c797e37ff629f36a4 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 4 Aug 2021 14:21:25 -0400 Subject: [PATCH 13/52] Changed amount to msatoshi --- README.md | 8 +++--- src/config/lnurl.sql | 2 +- src/entity/LnurlWithdrawEntity.ts | 6 ++--- src/lib/LnurlWithdraw.ts | 6 ++--- src/types/IReqCreateLnurlWithdraw.ts | 2 +- .../CreateLnurlWithdrawValidator.ts | 4 +-- tests/lnurl_withdraw.sh | 26 +++++++++---------- tests/lnurl_withdraw_wallet.sh | 12 ++++----- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 279f6f6..9869598 100644 --- a/README.md +++ b/README.md @@ -87,17 +87,17 @@ kexkey@AlcodaZ-2 ~ % docker exec -it lnurl ash sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" - amount: number; + msatoshi: number; description?: string; expiration?: Date; secretToken: string; webhookUrl?: string; -curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"amount":0.01,"description":"desc02","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc02","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api -{"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} +curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":0.01,"description":"desc02","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc02","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":{"msatoshi":0.01,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" -id|amount|description|expiration|secret_token|webhook_url|lnurl|withdrawn_details|withdrawn_ts|active|created_ts|updated_ts +id|msatoshi|description|expiration|secret_token|webhook_url|lnurl|withdrawn_details|withdrawn_ts|active|created_ts|updated_ts 1|0.01|desc01|2021-07-15 12:12|abc01|https://webhookUrl01|LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K|||1|2021-07-15 19:42:06|2021-07-15 19:42:06 curl -d '{"id":0,"method":"decodeBech32","params":{"s":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXGXE8Q93"}}' -H "Content-Type: application/json" localhost:8000/api diff --git a/src/config/lnurl.sql b/src/config/lnurl.sql index 9fb219a..0c26d9b 100644 --- a/src/config/lnurl.sql +++ b/src/config/lnurl.sql @@ -2,7 +2,7 @@ PRAGMA foreign_keys = ON; CREATE TABLE lnurl_withdraw ( id INTEGER PRIMARY KEY AUTOINCREMENT, - amount REAL, + msatoshi INTEGER, description TEXT, expiration INTEGER, secret_token TEXT UNIQUE, diff --git a/src/entity/LnurlWithdrawEntity.ts b/src/entity/LnurlWithdrawEntity.ts index af1ae89..97be96e 100644 --- a/src/entity/LnurlWithdrawEntity.ts +++ b/src/entity/LnurlWithdrawEntity.ts @@ -9,7 +9,7 @@ import { // CREATE TABLE lnurl_withdraw ( // id INTEGER PRIMARY KEY AUTOINCREMENT, -// amount REAL, +// msatoshi INTEGER, // description TEXT, // expiration INTEGER, // secret_token TEXT UNIQUE, @@ -33,8 +33,8 @@ export class LnurlWithdrawEntity { @PrimaryGeneratedColumn({ name: "id" }) lnurlWithdrawId!: number; - @Column({ type: "real", name: "amount" }) - amount!: number; + @Column({ type: "integer", name: "msatoshi" }) + msatoshi!: number; @Index("idx_lnurl_withdraw_description") @Column({ type: "text", name: "description", nullable: true }) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index ed992b6..eba196e 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -297,8 +297,8 @@ class LnurlWithdraw { this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, k1: lnurlWithdrawEntity.secretToken, defaultDescription: lnurlWithdrawEntity.description, - minWithdrawable: lnurlWithdrawEntity.amount, - maxWithdrawable: lnurlWithdrawEntity.amount, + minWithdrawable: lnurlWithdrawEntity.msatoshi, + maxWithdrawable: lnurlWithdrawEntity.msatoshi, }; } } else { @@ -352,7 +352,7 @@ class LnurlWithdraw { const resp: IRespLnPay = await this._cyphernodeClient.lnPay({ bolt11: params.pr, - expectedMsatoshi: lnurlWithdrawEntity.amount, + expectedMsatoshi: lnurlWithdrawEntity.msatoshi, expectedDescription: lnurlWithdrawEntity.description, }); diff --git a/src/types/IReqCreateLnurlWithdraw.ts b/src/types/IReqCreateLnurlWithdraw.ts index fe47129..a834996 100644 --- a/src/types/IReqCreateLnurlWithdraw.ts +++ b/src/types/IReqCreateLnurlWithdraw.ts @@ -1,5 +1,5 @@ export default interface IReqCreateLnurlWithdraw { - amount: number; + msatoshi: number; description?: string; expiration?: Date; secretToken: string; diff --git a/src/validators/CreateLnurlWithdrawValidator.ts b/src/validators/CreateLnurlWithdrawValidator.ts index a95fa70..657e73e 100644 --- a/src/validators/CreateLnurlWithdrawValidator.ts +++ b/src/validators/CreateLnurlWithdrawValidator.ts @@ -2,8 +2,8 @@ import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; class CreateLnurlWithdrawValidator { static validateRequest(request: IReqCreateLnurlWithdraw): boolean { - if (request.amount && request.secretToken) { - // Mandatory amount and secretToken found + if (request.msatoshi && request.secretToken) { + // Mandatory msatoshi and secretToken found if (request.expiration) { if (!isNaN(new Date(request.expiration).valueOf())) { // Expiration date is valid diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index 20bc16f..1c44d67 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -55,20 +55,20 @@ create_lnurl_withdraw() { local invoicenumber=${3:-$RANDOM} trace 2 "[create_lnurl_withdraw] invoicenumber=${invoicenumber}" - local amount=$((10000+${invoicenumber})) - trace 2 "[create_lnurl_withdraw] amount=${amount}" + local msatoshi=$((10000+${invoicenumber})) + trace 2 "[create_lnurl_withdraw] msatoshi=${msatoshi}" local expiration_offset=${2:-0} local expiration=$(date -d @$(($(date -u +"%s")+${expiration_offset})) +"%Y-%m-%dT%H:%M:%SZ") trace 2 "[create_lnurl_withdraw] expiration=${expiration}" # Service creates LNURL Withdraw - data='{"id":0,"method":"createLnurlWithdraw","params":{"amount":'${amount}',"description":"desc'${invoicenumber}'","expiration":"'${expiration}'","secretToken":"secret'${invoicenumber}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'"}}' + data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiration":"'${expiration}'","secretToken":"secret'${invoicenumber}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'"}}' trace 2 "[create_lnurl_withdraw] data=${data}" trace 2 "[create_lnurl_withdraw] Calling createLnurlWithdraw..." local createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 2 "[create_lnurl_withdraw] createLnurlWithdraw=${createLnurlWithdraw}" - # {"id":0,"result":{"amount":0.01,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} + # {"id":0,"result":{"msatoshi":100000000,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 2 "[create_lnurl_withdraw] lnurl=${lnurl}" @@ -143,12 +143,12 @@ call_lnservice_withdraw_request() { create_bolt11() { trace 1 "\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}" - local amount=${1} - trace 2 "[create_bolt11] amount=${amount}" + local msatoshi=${1} + trace 2 "[create_bolt11] msatoshi=${msatoshi}" local desc=${2} trace 2 "[create_bolt11] desc=${desc}" - local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${amount}',"label":"'${desc}'","description":"'${desc}'"}}' + local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${msatoshi}',"label":"'${desc}'","description":"'${desc}'"}}' trace 2 "[create_bolt11] data=${data}" local invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) trace 2 "[create_bolt11] invoice=${invoice}" @@ -237,9 +237,9 @@ happy_path() { trace 2 "[happy_path] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw - local amount=$(echo "${createLnurlWithdraw}" | jq -r '.result.amount') + local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') - local invoice=$(create_bolt11 "${amount}" "${description}") + local invoice=$(create_bolt11 "${msatoshi}" "${description}") trace 2 "[happy_path] invoice=${invoice}" local bolt11=$(echo ${invoice} | jq -r ".bolt11") trace 2 "[happy_path] bolt11=${bolt11}" @@ -358,9 +358,9 @@ expired2() { trace 2 "[expired2] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw - local amount=$(echo "${createLnurlWithdraw}" | jq -r '.result.amount') + local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') - local invoice=$(create_bolt11 "${amount}" "${description}") + local invoice=$(create_bolt11 "${msatoshi}" "${description}") trace 2 "[expired2] invoice=${invoice}" local bolt11=$(echo ${invoice} | jq -r ".bolt11") trace 2 "[expired2] bolt11=${bolt11}" @@ -499,9 +499,9 @@ deleted2() { trace 2 "[deleted2] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw - local amount=$(echo "${createLnurlWithdraw}" | jq -r '.result.amount') + local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') - local invoice=$(create_bolt11 "${amount}" "${description}") + local invoice=$(create_bolt11 "${msatoshi}" "${description}") trace 2 "[deleted2] invoice=${invoice}" local bolt11=$(echo ${invoice} | jq -r ".bolt11") trace 2 "[deleted2] bolt11=${bolt11}" diff --git a/tests/lnurl_withdraw_wallet.sh b/tests/lnurl_withdraw_wallet.sh index a9ebb8a..f7ca416 100755 --- a/tests/lnurl_withdraw_wallet.sh +++ b/tests/lnurl_withdraw_wallet.sh @@ -38,14 +38,14 @@ call_lnservice_withdraw_request() { create_bolt11() { trace 1 "\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}" - local amount=${1} - trace 2 "[create_bolt11] amount=${amount}" + local msatoshi=${1} + trace 2 "[create_bolt11] msatoshi=${msatoshi}" local label=${2} trace 2 "[create_bolt11] label=${label}" local desc=${3} trace 2 "[create_bolt11] desc=${desc}" - local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${amount}',"label":"'${label}'","description":"'${desc}'"}}' + local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${msatoshi}',"label":"'${label}'","description":"'${desc}'"}}' trace 2 "[create_bolt11] data=${data}" local invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) trace 2 "[create_bolt11] invoice=${invoice}" @@ -111,12 +111,12 @@ trace 2 "url=${url}" withdrawRequestResponse=$(call_lnservice_withdraw_request "${url}") trace 2 "withdrawRequestResponse=${withdrawRequestResponse}" -amount=$(echo "${withdrawRequestResponse}" | jq -r ".maxWithdrawable") -trace 2 "amount=${amount}" +msatoshi=$(echo "${withdrawRequestResponse}" | jq -r ".maxWithdrawable") +trace 2 "msatoshi=${msatoshi}" desc=$(echo "${withdrawRequestResponse}" | jq -r ".defaultDescription") trace 2 "desc=${desc}" -invoice=$(create_bolt11 ${amount} "$RANDOM" "${desc}") +invoice=$(create_bolt11 ${msatoshi} "$RANDOM" "${desc}") trace 2 "invoice=${invoice}" bolt11=$(echo "${invoice}" | jq -r ".bolt11") From 0db696a5163c4334319a427767d010ca50df95ac Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 4 Aug 2021 14:39:47 -0400 Subject: [PATCH 14/52] Fixed withdrawnDetails error in db --- src/lib/LnurlWithdraw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index eba196e..99b80bc 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -361,7 +361,7 @@ class LnurlWithdraw { result = { status: "ERROR", reason: resp.error.message }; - lnurlWithdrawEntity.withdrawnDetails = resp.error.message; + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( lnurlWithdrawEntity From e74e2bdde6abfeafb6ce0134f0e0833afced7df2 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 12 Aug 2021 16:04:10 -0400 Subject: [PATCH 15/52] Call webhook only when withdraw successful --- src/lib/LnurlWithdraw.ts | 64 ++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 99b80bc..6f09774 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -68,8 +68,8 @@ class LnurlWithdraw { const lnurlDecoded = this._lnurlConfig.LN_SERVICE_SERVER + - ":" + - this._lnurlConfig.LN_SERVICE_PORT + + // ":" + + // this._lnurlConfig.LN_SERVICE_PORT + this._lnurlConfig.LN_SERVICE_CTX + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + "?s=" + @@ -291,8 +291,8 @@ class LnurlWithdraw { tag: "withdrawRequest", callback: this._lnurlConfig.LN_SERVICE_SERVER + - ":" + - this._lnurlConfig.LN_SERVICE_PORT + + // ":" + + // this._lnurlConfig.LN_SERVICE_PORT + this._lnurlConfig.LN_SERVICE_CTX + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, k1: lnurlWithdrawEntity.secretToken, @@ -349,12 +349,22 @@ class LnurlWithdraw { logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); lnurlWithdrawEntity.bolt11 = params.pr; - - const resp: IRespLnPay = await this._cyphernodeClient.lnPay({ + const lnPayParams = { bolt11: params.pr, expectedMsatoshi: lnurlWithdrawEntity.msatoshi, expectedDescription: lnurlWithdrawEntity.description, - }); + }; + let resp: IRespLnPay = await this._cyphernodeClient.lnPay( + lnPayParams + ); + + if (resp.error) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry!" + ); + + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } if (resp.error) { logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay error!"); @@ -424,33 +434,37 @@ class LnurlWithdraw { } else { lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); } - let response; + let response; + let postdata; lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { logger.debug( "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", lnurlWithdrawEntity ); - const postdata = { - lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, - bolt11: lnurlWithdrawEntity.bolt11, - lnPayResponse: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, - }; - logger.debug("LnurlWithdraw.processCallbacks, postdata=", postdata); + if (lnurlWithdrawEntity.withdrawnTimestamp) { + // Call webhook only if withdraw done + postdata = { + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + bolt11: lnurlWithdrawEntity.bolt11, + lnPayResponse: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, + }; + logger.debug("LnurlWithdraw.processCallbacks, postdata=", postdata); - response = await Utils.post( - lnurlWithdrawEntity.webhookUrl || "", - postdata - ); - if (response.status >= 200 && response.status < 400) { - logger.debug("LnurlWithdraw.processCallbacks, webhook called back"); + response = await Utils.post( + lnurlWithdrawEntity.webhookUrl || "", + postdata + ); + if (response.status >= 200 && response.status < 400) { + logger.debug("LnurlWithdraw.processCallbacks, webhook called back"); - lnurlWithdrawEntity.calledback = true; - lnurlWithdrawEntity.calledbackTimestamp = new Date(); - await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + lnurlWithdrawEntity.calledback = true; + lnurlWithdrawEntity.calledbackTimestamp = new Date(); + await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + } } }); } From b56fcc467152e1bbc034e96bb6fbbc5a052f7321 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 13 Aug 2021 17:18:19 -0400 Subject: [PATCH 16/52] Added a 30sec timeout to CN --- src/lib/CyphernodeClient.ts | 2 ++ src/lib/LnurlDB.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 146a4ad..e458cc2 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -68,6 +68,7 @@ class CyphernodeClient { url: url, method: "post", baseURL: this.baseURL, + timeout: 30000, headers: { Authorization: "Bearer " + this._generateToken(), }, @@ -133,6 +134,7 @@ class CyphernodeClient { url: url, method: "get", baseURL: this.baseURL, + timeout: 30000, headers: { Authorization: "Bearer " + this._generateToken(), }, diff --git a/src/lib/LnurlDB.ts b/src/lib/LnurlDB.ts index 18ffda9..c4d28ba 100644 --- a/src/lib/LnurlDB.ts +++ b/src/lib/LnurlDB.ts @@ -119,6 +119,7 @@ class LnurlDB { calledback: false, webhookUrl: Not(IsNull()), withdrawnDetails: Not(IsNull()), + withdrawnTimestamp: Not(IsNull()), }, }); From 311c776c109c927d8d92b804c7c72ef63dc3476b Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 31 Aug 2021 16:59:45 -0400 Subject: [PATCH 17/52] Switched to Prisma ORM, added fallback, batch, webhooks --- tests/lnurl_withdraw.sh | 493 +++++++++++++++++++++++++++------------- 1 file changed, 330 insertions(+), 163 deletions(-) diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index 1c44d67..d4d50f5 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -1,22 +1,22 @@ #!/bin/sh # Happy path: -# + # 1. Create a LNURL Withdraw # 2. Get it and compare # 3. User calls LNServiceWithdrawRequest with wrong k1 -> Error, wrong k1! -# 3. User calls LNServiceWithdrawRequest -# 4. User calls LNServiceWithdraw with wrong k1 -> Error, wrong k1! -# 4. User calls LNServiceWithdraw +# 4. User calls LNServiceWithdrawRequest +# 5. User calls LNServiceWithdraw with wrong k1 -> Error, wrong k1! +# 6. User calls LNServiceWithdraw # Expired 1: -# + # 1. Create a LNURL Withdraw with expiration=now # 2. Get it and compare # 3. User calls LNServiceWithdrawRequest -> Error, expired! # Expired 2: -# + # 1. Create a LNURL Withdraw with expiration=now + 5 seconds # 2. Get it and compare # 3. User calls LNServiceWithdrawRequest @@ -24,7 +24,7 @@ # 5. User calls LNServiceWithdraw -> Error, expired! # Deleted 1: -# + # 1. Create a LNURL Withdraw with expiration=now # 2. Get it and compare # 3. Delete it @@ -32,80 +32,105 @@ # 5. User calls LNServiceWithdrawRequest -> Error, deleted! # Deleted 2: -# + # 1. Create a LNURL Withdraw with expiration=now + 5 seconds # 2. Get it and compare -# 5. User calls LNServiceWithdrawRequest -# 3. Delete it +# 3. User calls LNServiceWithdrawRequest +# 4. Delete it # 5. User calls LNServiceWithdraw -> Error, deleted! +# fallback 1, use of Bitcoin fallback address: + +# 1. Cyphernode.getnewaddress -> btcfallbackaddr +# 2. Cyphernode.watch btcfallbackaddr +# 3. Listen to watch webhook +# 4. Create a LNURL Withdraw with expiration=now and a btcfallbackaddr +# 5. Get it and compare +# 6. User calls LNServiceWithdrawRequest -> Error, expired! +# 7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) +# 8. Mined block and Cyphernode's confirmed watch callback called (port 1113) + +# fallback 2, use of Bitcoin fallback address in a batched spend: + +# 1. Cyphernode.getnewaddress -> btcfallbackaddr +# 2. Cyphernode.watch btcfallbackaddr +# 3. Listen to watch webhook +# 4. Create a LNURL Withdraw with expiration=now and a btcfallbackaddr +# 5. Get it and compare +# 6. User calls LNServiceWithdrawRequest -> Error, expired! +# 7. Fallback should be triggered, added to current batch using the Batcher +# 8. Wait for the batch to execute, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112), Batcher's execute callback called (port 1113) +# 9. Mined block and Cyphernode's confirmed watch callback called (port 1114) + . ./tests/colors.sh trace() { if [ "${1}" -le "${TRACING}" ]; then local str="$(date -Is) $$ ${2}" - echo -e "${str}" 1>&2 + echo -e "${str}" >&2 fi } create_lnurl_withdraw() { - trace 1 "\n[create_lnurl_withdraw] ${BCyan}Service creates LNURL Withdraw...${Color_Off}" + trace 2 "\n\n[create_lnurl_withdraw] ${BCyan}Service creates LNURL Withdraw...${Color_Off}\n" local callbackurl=${1} local invoicenumber=${3:-$RANDOM} - trace 2 "[create_lnurl_withdraw] invoicenumber=${invoicenumber}" - local msatoshi=$((10000+${invoicenumber})) - trace 2 "[create_lnurl_withdraw] msatoshi=${msatoshi}" + trace 3 "[create_lnurl_withdraw] invoicenumber=${invoicenumber}" + local msatoshi=$((500000+${invoicenumber})) + trace 3 "[create_lnurl_withdraw] msatoshi=${msatoshi}" local expiration_offset=${2:-0} local expiration=$(date -d @$(($(date -u +"%s")+${expiration_offset})) +"%Y-%m-%dT%H:%M:%SZ") - trace 2 "[create_lnurl_withdraw] expiration=${expiration}" + trace 3 "[create_lnurl_withdraw] expiration=${expiration}" + local fallback_addr=${4:-""} + local fallback_batched=${5:-"false"} # Service creates LNURL Withdraw - data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiration":"'${expiration}'","secretToken":"secret'${invoicenumber}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'"}}' - trace 2 "[create_lnurl_withdraw] data=${data}" - trace 2 "[create_lnurl_withdraw] Calling createLnurlWithdraw..." + data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiration":"'${expiration}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'","btcFallbackAddress":"'${fallback_addr}'","batchFallback":'${fallback_batched}'}}' + trace 3 "[create_lnurl_withdraw] data=${data}" + trace 3 "[create_lnurl_withdraw] Calling createLnurlWithdraw..." local createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) - trace 2 "[create_lnurl_withdraw] createLnurlWithdraw=${createLnurlWithdraw}" + trace 3 "[create_lnurl_withdraw] createLnurlWithdraw=${createLnurlWithdraw}" # {"id":0,"result":{"msatoshi":100000000,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") - trace 2 "[create_lnurl_withdraw] lnurl=${lnurl}" + trace 3 "[create_lnurl_withdraw] lnurl=${lnurl}" echo "${createLnurlWithdraw}" } get_lnurl_withdraw() { - trace 1 "\n[get_lnurl_withdraw] ${BCyan}Get LNURL Withdraw...${Color_Off}" + trace 2 "\n\n[get_lnurl_withdraw] ${BCyan}Get LNURL Withdraw...${Color_Off}\n" local lnurl_withdraw_id=${1} - trace 2 "[get_lnurl_withdraw] lnurl_withdraw_id=${lnurl_withdraw_id}" + trace 3 "[get_lnurl_withdraw] lnurl_withdraw_id=${lnurl_withdraw_id}" # Service creates LNURL Withdraw data='{"id":0,"method":"getLnurlWithdraw","params":{"lnurlWithdrawId":'${lnurl_withdraw_id}'}}' - trace 2 "[get_lnurl_withdraw] data=${data}" - trace 2 "[get_lnurl_withdraw] Calling getLnurlWithdraw..." + trace 3 "[get_lnurl_withdraw] data=${data}" + trace 3 "[get_lnurl_withdraw] Calling getLnurlWithdraw..." local getLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) - trace 2 "[get_lnurl_withdraw] getLnurlWithdraw=${getLnurlWithdraw}" + trace 3 "[get_lnurl_withdraw] getLnurlWithdraw=${getLnurlWithdraw}" echo "${getLnurlWithdraw}" } delete_lnurl_withdraw() { - trace 1 "\n[delete_lnurl_withdraw] ${BCyan}Delete LNURL Withdraw...${Color_Off}" + trace 2 "\n\n[delete_lnurl_withdraw] ${BCyan}Delete LNURL Withdraw...${Color_Off}\n" local lnurl_withdraw_id=${1} - trace 2 "[delete_lnurl_withdraw] lnurl_withdraw_id=${lnurl_withdraw_id}" + trace 3 "[delete_lnurl_withdraw] lnurl_withdraw_id=${lnurl_withdraw_id}" # Service deletes LNURL Withdraw data='{"id":0,"method":"deleteLnurlWithdraw","params":{"lnurlWithdrawId":'${lnurl_withdraw_id}'}}' - trace 2 "[delete_lnurl_withdraw] data=${data}" - trace 2 "[delete_lnurl_withdraw] Calling deleteLnurlWithdraw..." + trace 3 "[delete_lnurl_withdraw] data=${data}" + trace 3 "[delete_lnurl_withdraw] Calling deleteLnurlWithdraw..." local deleteLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) - trace 2 "[delete_lnurl_withdraw] deleteLnurlWithdraw=${deleteLnurlWithdraw}" + trace 3 "[delete_lnurl_withdraw] deleteLnurlWithdraw=${deleteLnurlWithdraw}" - local active=$(echo "${deleteLnurlWithdraw}" | jq ".result.active") - if [ "${active}" = "true" ]; then + local deleted=$(echo "${deleteLnurlWithdraw}" | jq ".result.deleted") + if [ "${deleted}" = "false" ]; then trace 2 "[delete_lnurl_withdraw] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" return 1 fi @@ -114,83 +139,83 @@ delete_lnurl_withdraw() { } decode_lnurl() { - trace 1 "\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}" + trace 2 "\n\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}\n" local lnurl=${1} local lnServicePrefix=${2} local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' - trace 2 "[decode_lnurl] data=${data}" + trace 3 "[decode_lnurl] data=${data}" local decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) - trace 2 "[decode_lnurl] decodedLnurl=${decodedLnurl}" + trace 3 "[decode_lnurl] decodedLnurl=${decodedLnurl}" local urlSuffix=$(echo "${decodedLnurl}" | jq -r ".result" | sed 's|'${lnServicePrefix}'||g') - trace 2 "[decode_lnurl] urlSuffix=${urlSuffix}" + trace 3 "[decode_lnurl] urlSuffix=${urlSuffix}" echo "${urlSuffix}" } call_lnservice_withdraw_request() { - trace 1 "\n[call_lnservice_withdraw_request] ${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}" + trace 2 "\n\n[call_lnservice_withdraw_request] ${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}\n" local urlSuffix=${1} local withdrawRequestResponse=$(curl -s lnurl:8000${urlSuffix}) - trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" + trace 3 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" } create_bolt11() { - trace 1 "\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}" + trace 2 "\n\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}\n" local msatoshi=${1} - trace 2 "[create_bolt11] msatoshi=${msatoshi}" + trace 3 "[create_bolt11] msatoshi=${msatoshi}" local desc=${2} - trace 2 "[create_bolt11] desc=${desc}" + trace 3 "[create_bolt11] desc=${desc}" local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${msatoshi}',"label":"'${desc}'","description":"'${desc}'"}}' - trace 2 "[create_bolt11] data=${data}" + trace 3 "[create_bolt11] data=${data}" local invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) - trace 2 "[create_bolt11] invoice=${invoice}" + trace 3 "[create_bolt11] invoice=${invoice}" echo "${invoice}" } get_invoice_status() { - trace 1 "\n[get_invoice_status] ${BCyan}Let's make sure the invoice is unpaid first...${Color_Off}" + trace 2 "\n\n[get_invoice_status] ${BCyan}Let's make sure the invoice is unpaid first...${Color_Off}\n" local invoice=${1} - trace 2 "[get_invoice_status] invoice=${invoice}" + trace 3 "[get_invoice_status] invoice=${invoice}" local payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") - trace 2 "[get_invoice_status] payment_hash=${payment_hash}" + trace 3 "[get_invoice_status] payment_hash=${payment_hash}" local data='{"id":1,"jsonrpc": "2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' - trace 2 "[get_invoice_status] data=${data}" + trace 3 "[get_invoice_status] data=${data}" local invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) - trace 2 "[get_invoice_status] invoices=${invoices}" + trace 3 "[get_invoice_status] invoices=${invoices}" local status=$(echo "${invoices}" | jq -r ".invoices[0].status") - trace 2 "[get_invoice_status] status=${status}" + trace 3 "[get_invoice_status] status=${status}" echo "${status}" } call_lnservice_withdraw() { - trace 1 "\n[call_lnservice_withdraw] ${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}" + trace 2 "\n\n[call_lnservice_withdraw] ${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}\n" local withdrawRequestResponse=${1} local lnServicePrefix=${2} local bolt11=${3} callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") - trace 2 "[call_lnservice_withdraw] callback=${callback}" + trace 3 "[call_lnservice_withdraw] callback=${callback}" urlSuffix=$(echo "${callback}" | sed 's|'${lnServicePrefix}'||g') - trace 2 "[call_lnservice_withdraw] urlSuffix=${urlSuffix}" + trace 3 "[call_lnservice_withdraw] urlSuffix=${urlSuffix}" k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") - trace 2 "[call_lnservice_withdraw] k1=${k1}" + trace 3 "[call_lnservice_withdraw] k1=${k1}" - trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" + trace 3 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" withdrawResponse=$(curl -s lnurl:8000${urlSuffix}?k1=${k1}\&pr=${bolt11}) - trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" + trace 3 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" } @@ -205,66 +230,67 @@ happy_path() { # 4. User calls LNServiceWithdraw with wrong k1 -> Error, wrong k1! # 4. User calls LNServiceWithdraw - trace 1 "\n[happy_path] ${On_Yellow}${BBlack} Happy path! ${Color_Off}" + trace 1 "\n\n[happy_path] ${On_Yellow}${BBlack} Happy path: ${Color_Off}\n" local callbackurl=${1} local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 15) - trace 2 "[happy_path] createLnurlWithdraw=${createLnurlWithdraw}" + trace 3 "[happy_path] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") - #trace 2 "lnurl=${lnurl}" + trace 3 "lnurl=${lnurl}" local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) - trace 2 "[happy_path] get_lnurl_withdraw=${get_lnurl_withdraw}" + trace 3 "[happy_path] get_lnurl_withdraw=${get_lnurl_withdraw}" local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') - trace 2 "[happy_path] equals=${equals}" + trace 3 "[happy_path] equals=${equals}" if [ "${equals}" = "true" ]; then trace 2 "[happy_path] EQUALS!" else - trace 1 "[happy_path] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + trace 1 "\n[happy_path] ${On_Red}${BBlack} Happy path: NOT EQUALS! ${Color_Off}\n" return 1 fi # Decode LNURL local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 2 "[happy_path] urlSuffix=${urlSuffix}" + trace 3 "[happy_path] urlSuffix=${urlSuffix}" # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") - trace 2 "[happy_path] withdrawRequestResponse=${withdrawRequestResponse}" + trace 3 "[happy_path] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') local invoice=$(create_bolt11 "${msatoshi}" "${description}") - trace 2 "[happy_path] invoice=${invoice}" + trace 3 "[happy_path] invoice=${invoice}" local bolt11=$(echo ${invoice} | jq -r ".bolt11") - trace 2 "[happy_path] bolt11=${bolt11}" + trace 3 "[happy_path] bolt11=${bolt11}" # We want to see that that invoice is unpaid first... local status=$(get_invoice_status "${invoice}") - trace 2 "[happy_path] status=${status}" + trace 3 "[happy_path] status=${status}" + + start_callback_server # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") - trace 2 "[happy_path] withdrawResponse=${withdrawResponse}" + trace 3 "[happy_path] withdrawResponse=${withdrawResponse}" - trace 2 "[happy_path] Sleeping 5 seconds..." - sleep 5 + wait # We want to see if payment received (invoice status paid) status=$(get_invoice_status "${invoice}") - trace 2 "[happy_path] status=${status}" + trace 3 "[happy_path] status=${status}" if [ "${status}" = "paid" ]; then - trace 1 "\n[happy_path] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + trace 1 "\n\n[happy_path] ${On_IGreen}${BBlack} Happy path: SUCCESS! ${Color_Off}\n" date return 0 else - trace 1 "\n[happy_path] ${On_Red}${BBlack} FAILURE! ${Color_Off}" + trace 1 "\n\n[happy_path] ${On_Red}${BBlack} Happy path: FAILURE! ${Color_Off}\n" date return 1 fi @@ -277,43 +303,43 @@ expired1() { # 2. Get it and compare # 3. User calls LNServiceWithdrawRequest -> Error, expired! - trace 1 "\n[expired1] ${On_Yellow}${BBlack} Expired 1! ${Color_Off}" + trace 1 "\n\n[expired1] ${On_Yellow}${BBlack} Expired 1: ${Color_Off}\n" local callbackurl=${1} local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) - trace 2 "[expired1] createLnurlWithdraw=${createLnurlWithdraw}" + trace 3 "[expired1] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") - #trace 2 "lnurl=${lnurl}" + trace 3 "lnurl=${lnurl}" local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) - trace 2 "[expired1] get_lnurl_withdraw=${get_lnurl_withdraw}" + trace 3 "[expired1] get_lnurl_withdraw=${get_lnurl_withdraw}" local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') - trace 2 "[expired1] equals=${equals}" + trace 3 "[expired1] equals=${equals}" if [ "${equals}" = "true" ]; then trace 2 "[expired1] EQUALS!" else - trace 1 "[expired1] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + trace 1 "\n\n[expired1] ${On_Red}${BBlack} Expired 1: NOT EQUALS! ${Color_Off}\n" return 1 fi # Decode LNURL local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 2 "[expired1] urlSuffix=${urlSuffix}" + trace 3 "[expired1] urlSuffix=${urlSuffix}" # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") - trace 2 "[expired1] withdrawRequestResponse=${withdrawRequestResponse}" + trace 3 "[expired1] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" | grep -qi "expired" if [ "$?" -ne "0" ]; then - trace 1 "[expired1] ${On_Red}${BBlack} NOT EXPIRED! ${Color_Off}" + trace 1 "\n\n[expired1] ${On_Red}${BBlack} Expired 1: NOT EXPIRED! ${Color_Off}\n" return 1 else - trace 1 "\n[expired1] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + trace 1 "\n\n[expired1] ${On_IGreen}${BBlack} Expired 1: SUCCESS! ${Color_Off}\n" fi } @@ -326,58 +352,58 @@ expired2() { # 4. Sleep 5 seconds # 5. User calls LNServiceWithdraw -> Error, expired! - trace 1 "\n[expired2] ${On_Yellow}${BBlack} Expired 2! ${Color_Off}" + trace 1 "\n\n[expired2] ${On_Yellow}${BBlack} Expired 2: ${Color_Off}\n" local callbackurl=${1} local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) - trace 2 "[expired2] createLnurlWithdraw=${createLnurlWithdraw}" + trace 3 "[expired2] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") - #trace 2 "lnurl=${lnurl}" + trace 3 "lnurl=${lnurl}" local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) - trace 2 "[expired2] get_lnurl_withdraw=${get_lnurl_withdraw}" + trace 3 "[expired2] get_lnurl_withdraw=${get_lnurl_withdraw}" local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') - trace 2 "[expired2] equals=${equals}" + trace 3 "[expired2] equals=${equals}" if [ "${equals}" = "true" ]; then trace 2 "[expired2] EQUALS!" else - trace 1 "[expired2] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + trace 1 "\n\n[expired2] ${On_Red}${BBlack} Expired 2: NOT EQUALS! ${Color_Off}\n" return 1 fi # Decode LNURL local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 2 "[expired2] urlSuffix=${urlSuffix}" + trace 3 "[expired2] urlSuffix=${urlSuffix}" # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") - trace 2 "[expired2] withdrawRequestResponse=${withdrawRequestResponse}" + trace 3 "[expired2] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') local invoice=$(create_bolt11 "${msatoshi}" "${description}") - trace 2 "[expired2] invoice=${invoice}" + trace 3 "[expired2] invoice=${invoice}" local bolt11=$(echo ${invoice} | jq -r ".bolt11") - trace 2 "[expired2] bolt11=${bolt11}" + trace 3 "[expired2] bolt11=${bolt11}" - trace 2 "[expired2] Sleeping 5 seconds..." + trace 3 "[expired2] Sleeping 5 seconds..." sleep 5 # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") - trace 2 "[expired2] withdrawResponse=${withdrawResponse}" + trace 3 "[expired2] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" | grep -qi "expired" if [ "$?" -ne "0" ]; then - trace 1 "[expired2] ${On_Red}${BBlack} NOT EXPIRED! ${Color_Off}" + trace 1 "\n\n[expired2] ${On_Red}${BBlack} Expired 2: NOT EXPIRED! ${Color_Off}\n" return 1 else - trace 1 "\n[expired2] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + trace 1 "\n\n[expired2] ${On_IGreen}${BBlack} Expired 2: SUCCESS! ${Color_Off}\n" fi } @@ -390,71 +416,71 @@ deleted1() { # 4. Get it and compare # 5. User calls LNServiceWithdrawRequest -> Error, deleted! - trace 1 "\n[deleted1] ${On_Yellow}${BBlack} Deleted 1! ${Color_Off}" + trace 1 "\n\n[deleted1] ${On_Yellow}${BBlack} Deleted 1: ${Color_Off}\n" local callbackurl=${1} local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) - trace 2 "[deleted1] createLnurlWithdraw=${createLnurlWithdraw}" + trace 3 "[deleted1] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") - #trace 2 "lnurl=${lnurl}" + trace 3 "lnurl=${lnurl}" local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) - trace 2 "[deleted1] get_lnurl_withdraw=${get_lnurl_withdraw}" + trace 3 "[deleted1] get_lnurl_withdraw=${get_lnurl_withdraw}" local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') - trace 2 "[deleted1] equals=${equals}" + trace 3 "[deleted1] equals=${equals}" if [ "${equals}" = "true" ]; then trace 2 "[deleted1] EQUALS!" else - trace 1 "[deleted1] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + trace 1 "\n\n[deleted1] ${On_Red}${BBlack} Deleted 1: NOT EQUALS! ${Color_Off}\n" return 1 fi local delete_lnurl_withdraw=$(delete_lnurl_withdraw ${lnurl_withdraw_id}) - trace 2 "[deleted1] delete_lnurl_withdraw=${delete_lnurl_withdraw}" - local deleted=$(echo "${get_lnurl_withdraw}" | jq '.result.active = false | del(.result.updatedAt)') - trace 2 "[deleted1] deleted=${deleted}" + trace 3 "[deleted1] delete_lnurl_withdraw=${delete_lnurl_withdraw}" + local deleted=$(echo "${get_lnurl_withdraw}" | jq '.result.deleted = true | del(.result.updatedAt)') + trace 3 "[deleted1] deleted=${deleted}" get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id} | jq 'del(.result.updatedAt)') - trace 2 "[deleted1] get_lnurl_withdraw=${get_lnurl_withdraw}" + trace 3 "[deleted1] get_lnurl_withdraw=${get_lnurl_withdraw}" equals=$(jq --argjson a "${deleted}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') - trace 2 "[deleted1] equals=${equals}" + trace 3 "[deleted1] equals=${equals}" if [ "${equals}" = "true" ]; then trace 2 "[deleted1] EQUALS!" else - trace 1 "[deleted1] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + trace 1 "\n\n[deleted1] ${On_Red}${BBlack} Deleted 1: NOT EQUALS! ${Color_Off}\n" return 1 fi # Delete it twice... - trace 2 "[deleted1] Let's delete it again..." + trace 3 "[deleted1] Let's delete it again..." delete_lnurl_withdraw=$(delete_lnurl_withdraw ${lnurl_withdraw_id}) - trace 2 "[deleted1] delete_lnurl_withdraw=${delete_lnurl_withdraw}" + trace 3 "[deleted1] delete_lnurl_withdraw=${delete_lnurl_withdraw}" echo "${delete_lnurl_withdraw}" | grep -qi "already deactivated" if [ "$?" -ne "0" ]; then - trace 1 "[deleted1] ${On_Red}${BBlack} Should return an error because already deactivated! ${Color_Off}" + trace 1 "\n\n[deleted1] ${On_Red}${BBlack} Deleted 1: Should return an error because already deactivated! ${Color_Off}\n" return 1 else - trace 1 "\n[deleted1] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + trace 1 "\n\n[deleted1] ${On_IGreen}${BBlack} Deleted 1: SUCCESS! ${Color_Off}\n" fi # Decode LNURL local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 2 "[deleted1] urlSuffix=${urlSuffix}" + trace 3 "[deleted1] urlSuffix=${urlSuffix}" # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") - trace 2 "[deleted1] withdrawRequestResponse=${withdrawRequestResponse}" + trace 3 "[deleted1] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" | grep -qi "Deactivated" if [ "$?" -ne "0" ]; then - trace 1 "[deleted1] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" + trace 1 "\n\n[deleted1] ${On_Red}${BBlack} Deleted 1: NOT DELETED! ${Color_Off}\n" return 1 else - trace 1 "\n[deleted1] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + trace 1 "\n\n[deleted1] ${On_IGreen}${BBlack} Deleted 1: SUCCESS! ${Color_Off}\n" fi } @@ -467,122 +493,263 @@ deleted2() { # 3. Delete it # 5. User calls LNServiceWithdraw -> Error, deleted! - trace 1 "\n[deleted2] ${On_Yellow}${BBlack} Deleted 2! ${Color_Off}" + trace 1 "\n\n[deleted2] ${On_Yellow}${BBlack} Deleted 2: ${Color_Off}\n" local callbackurl=${1} local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) - trace 2 "[deleted2] createLnurlWithdraw=${createLnurlWithdraw}" + trace 3 "[deleted2] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") - #trace 2 "lnurl=${lnurl}" + trace 3 "lnurl=${lnurl}" local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) - trace 2 "[deleted2] get_lnurl_withdraw=${get_lnurl_withdraw}" + trace 3 "[deleted2] get_lnurl_withdraw=${get_lnurl_withdraw}" local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') - trace 2 "[deleted2] equals=${equals}" + trace 3 "[deleted2] equals=${equals}" if [ "${equals}" = "true" ]; then trace 2 "[deleted2] EQUALS!" else - trace 1 "[deleted2] ${On_Red}${BBlack} NOT EQUALS! ${Color_Off}" + trace 1 "\n\n[deleted2] ${On_Red}${BBlack} Deleted 2: NOT EQUALS! ${Color_Off}\n" return 1 fi # Decode LNURL local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 2 "[deleted2] urlSuffix=${urlSuffix}" + trace 3 "[deleted2] urlSuffix=${urlSuffix}" # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") - trace 2 "[deleted2] withdrawRequestResponse=${withdrawRequestResponse}" + trace 3 "[deleted2] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') local invoice=$(create_bolt11 "${msatoshi}" "${description}") - trace 2 "[deleted2] invoice=${invoice}" + trace 3 "[deleted2] invoice=${invoice}" local bolt11=$(echo ${invoice} | jq -r ".bolt11") - trace 2 "[deleted2] bolt11=${bolt11}" + trace 3 "[deleted2] bolt11=${bolt11}" local delete_lnurl_withdraw=$(delete_lnurl_withdraw ${lnurl_withdraw_id}) - trace 2 "[deleted2] delete_lnurl_withdraw=${delete_lnurl_withdraw}" - local deleted=$(echo "${get_lnurl_withdraw}" | jq '.result.active = false') - trace 2 "[deleted2] deleted=${deleted}" + trace 3 "[deleted2] delete_lnurl_withdraw=${delete_lnurl_withdraw}" + local deleted=$(echo "${get_lnurl_withdraw}" | jq '.result.deleted = true') + trace 3 "[deleted2] deleted=${deleted}" # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") - trace 2 "[deleted2] withdrawResponse=${withdrawResponse}" + trace 3 "[deleted2] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" | grep -qi "Deactivated" if [ "$?" -ne "0" ]; then - trace 1 "[deleted2] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" + trace 1 "\n\n[deleted2] ${On_Red}${BBlack} Deleted 2: NOT DELETED! ${Color_Off}\n" return 1 else - trace 1 "\n[deleted2] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + trace 1 "\n\n[deleted2] ${On_IGreen}${BBlack} Deleted 2: SUCCESS! ${Color_Off}\n" fi } -duplicate_token() { - # duplicate_token: +fallback1() { + # fallback 1, use of Bitcoin fallback address: # - # 1. Create a LNURL Withdraw with expiration=now + 5 seconds - # 2. Create another LNURL Withdraw with the same secret_token + # 1. Cyphernode.getnewaddress -> btcfallbackaddr + # 2. Cyphernode.watch btcfallbackaddr + # 3. Listen to watch webhook + # 4. Create a LNURL Withdraw with expiration=now and a btcfallbackaddr + # 5. Get it and compare + # 6. User calls LNServiceWithdrawRequest -> Error, expired! + # 7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) + # 8. Mined block and Cyphernode's confirmed watch callback called (port 1113) + + trace 1 "\n\n[fallback1] ${On_Yellow}${BBlack} Fallback 1: ${Color_Off}\n" + + local callbackserver=${1} + local callbackport=${2} + local lnServicePrefix=${3} + + local zeroconfport=$((${callbackserverport}+1)) + local oneconfport=$((${callbackserverport}+2)) + local callbackurlCnWatch0conf="http://${callbackservername}:${zeroconfport}" + local callbackurlCnWatch1conf="http://${callbackservername}:${oneconfport}" + local callbackurl="http://${callbackservername}:${callbackserverport}" + + # Get new address + local data='{"label":"lnurl_fallback_test"}' + local btcfallbackaddr=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) + btcfallbackaddr=$(echo "${btcfallbackaddr}" | jq -r ".address") + trace 3 "[fallback1] btcfallbackaddr=${btcfallbackaddr}" + + # Watch the address + data='{"address":"'${btcfallbackaddr}'","unconfirmedCallbackURL":"'${callbackurlCnWatch0conf}'/callback0conf","confirmedCallbackURL":"'${callbackurlCnWatch1conf}'/callback1conf"}' + local watchresponse=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) + trace 3 "[fallback1] watchresponse=${watchresponse}" - trace 1 "\n[duplicate_token] ${On_Yellow}${BBlack} Duplicate token! ${Color_Off}" + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0 "" "${btcfallbackaddr}") + trace 3 "[fallback1] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + trace 3 "[fallback1] lnurl=${lnurl}" - local callbackurl=${1} - local lnServicePrefix=${2} + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 3 "[fallback1] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 3 "[fallback1] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[fallback1] EQUALS!" + else + trace 1 "\n\n[fallback1] ${On_Red}${BBlack} Fallback 1: NOT EQUALS! ${Color_Off}\n" + return 1 + fi - # Service creates LNURL Withdraw - local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) - trace 2 "[duplicate_token] createLnurlWithdraw=${createLnurlWithdraw}" - local secret_token=$(echo "${createLnurlWithdraw}" | jq -r ".result.secretToken") - trace 2 "secret_token=${secret_token}" - local invoicenumber=$(echo "${secret_token}" | sed 's/secret//g') - trace 2 "invoicenumber=${invoicenumber}" + # Decode LNURL + local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") + trace 3 "[fallback1] urlSuffix=${urlSuffix}" + + start_callback_server + start_callback_server ${zeroconfport} + start_callback_server ${oneconfport} + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + trace 3 "[fallback1] withdrawRequestResponse=${withdrawRequestResponse}" + + echo "${withdrawRequestResponse}" | grep -qi "expired" + if [ "$?" -ne "0" ]; then + trace 1 "\n\n[fallback1] ${On_Red}${BBlack} Fallback 1: NOT EXPIRED! ${Color_Off}\n" + return 1 + else + trace 2 "[fallback1] EXPIRED!" + fi + + trace 3 "[fallback1] Waiting for fallback execution and a block mined..." - # Service creates another LNURL Withdraw with same secret token - local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5 "${invoicenumber}") - trace 2 "[duplicate_token] createLnurlWithdraw=${createLnurlWithdraw}" + wait - echo "${createLnurlWithdraw}" | jq ".error.message" | grep -iq "unique" + trace 1 "\n\n[fallback1] ${On_IGreen}${BBlack} Fallback 1: SUCCESS! ${Color_Off}\n" +} + +fallback2() { + # fallback 2, use of Bitcoin fallback address in a batched spend: + # + # 1. Cyphernode.getnewaddress -> btcfallbackaddr + # 2. Cyphernode.watch btcfallbackaddr + # 3. Listen to watch webhook + # 4. Create a LNURL Withdraw with expiration=now and a btcfallbackaddr + # 5. Get it and compare + # 6. User calls LNServiceWithdrawRequest -> Error, expired! + # 7. Fallback should be triggered, added to current batch using the Batcher + # 8. Wait for the batch to execute, Batcher's execute callback called=>LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) + # 9. Mined block and Cyphernode's confirmed watch callback called (port 1113) + + trace 1 "\n\n[fallback2] ${On_Yellow}${BBlack} Fallback 2: ${Color_Off}\n" + + local callbackserver=${1} + local callbackport=${2} + local lnServicePrefix=${3} + + local zeroconfport=$((${callbackserverport}+1)) + local oneconfport=$((${callbackserverport}+2)) + local callbackurlCnWatch0conf="http://${callbackservername}:${zeroconfport}" + local callbackurlCnWatch1conf="http://${callbackservername}:${oneconfport}" + local callbackurl="http://${callbackservername}:${callbackserverport}" + + # Get new address + local data='{"label":"lnurl_fallback_test"}' + local btcfallbackaddr=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) + btcfallbackaddr=$(echo "${btcfallbackaddr}" | jq -r ".address") + trace 3 "[fallback2] btcfallbackaddr=${btcfallbackaddr}" + + # Watch the address + data='{"address":"'${btcfallbackaddr}'","unconfirmedCallbackURL":"'${callbackurlCnWatch0conf}'/callback0conf","confirmedCallbackURL":"'${callbackurlCnWatch1conf}'/callback1conf"}' + local watchresponse=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) + trace 3 "[fallback2] watchresponse=${watchresponse}" + + # Service creates LNURL Withdraw with batching true + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0 "" "${btcfallbackaddr}" "true") + trace 3 "[fallback2] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + trace 3 "[fallback2] lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 3 "[fallback2] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 3 "[fallback2] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[fallback2] EQUALS!" + else + trace 1 "\n\n[fallback2] ${On_Red}${BBlack} Fallback 2: NOT EQUALS! ${Color_Off}\n" + return 1 + fi + + # Decode LNURL + local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") + trace 3 "[fallback2] urlSuffix=${urlSuffix}" + + start_callback_server + start_callback_server ${zeroconfport} + start_callback_server ${oneconfport} + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + trace 3 "[fallback2] withdrawRequestResponse=${withdrawRequestResponse}" + + echo "${withdrawRequestResponse}" | grep -qi "expired" if [ "$?" -ne "0" ]; then - trace 1 "[duplicate_token] ${On_Red}${BBlack} Should have unicity error message! ${Color_Off}" + trace 1 "\n\n[fallback2] ${On_Red}${BBlack} Fallback 2: NOT EXPIRED! ${Color_Off}\n" return 1 else - trace 1 "\n[duplicate_token] ${On_IGreen}${BBlack} SUCCESS! ${Color_Off}" + trace 2 "[fallback2] EXPIRED!" fi + + trace 3 "[fallback2] Waiting for fallback execution and a block mined..." + + wait + + trace 1 "\n\n[fallback2] ${On_IGreen}${BBlack} Fallback 2: SUCCESS! ${Color_Off}\n" } -TRACING=2 +start_callback_server() { + trace 1 "\n\n[start_callback_server] ${BCyan}Let's start a callback server!...${Color_Off}\n" -trace 2 "${Color_Off}" + port=${1:-${callbackserverport}} + nc -vlp${port} -e sh -c 'echo -en "HTTP/1.1 200 OK\\r\\n\\r\\n" ; echo -en "'${On_Black}${White}'" >&2 ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo -e "'${Color_Off}'" >&2' & +} + +TRACING=3 + +trace 1 "${Color_Off}" date # Install needed packages -trace 2 "\n${BCyan}Installing needed packages...${Color_Off}" +trace 2 "\n\n${BCyan}Installing needed packages...${Color_Off}\n" apk add curl jq # Initializing test variables -trace 2 "\n${BCyan}Initializing test variables...${Color_Off}" -callbackservername="cb" +trace 2 "\n\n${BCyan}Initializing test variables...${Color_Off}\n" +callbackservername="lnurl_withdraw_test" callbackserverport="1111" callbackurl="http://${callbackservername}:${callbackserverport}" +# wait_for_callbacks + # Get config from lnurl cypherapp -trace 2 "\n${BCyan}Getting configuration from lnurl cypherapp...${Color_Off}" +trace 2 "\n\n${BCyan}Getting configuration from lnurl cypherapp...${Color_Off}\n" data='{"id":0,"method":"getConfig","params":[]}' lnurlConfig=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) -trace 2 "lnurlConfig=${lnurlConfig}" -lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER):\(.LN_SERVICE_PORT)"') -trace 2 "lnServicePrefix=${lnServicePrefix}" +trace 3 "lnurlConfig=${lnurlConfig}" +# lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER):\(.LN_SERVICE_PORT)"') +lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER)"') +trace 3 "lnServicePrefix=${lnServicePrefix}" happy_path "${callbackurl}" "${lnServicePrefix}" \ && expired1 "${callbackurl}" "${lnServicePrefix}" \ && expired2 "${callbackurl}" "${lnServicePrefix}" \ && deleted1 "${callbackurl}" "${lnServicePrefix}" \ && deleted2 "${callbackurl}" "${lnServicePrefix}" \ -&& duplicate_token "${callbackurl}" "${lnServicePrefix}" +&& fallback1 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" \ +&& fallback2 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" +trace 1 "\n\n${BCyan}Finished, deleting this test container...${Color_Off}\n" From e29dadcc173ea5ad9cd73bfe28d90975472aa01a Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 31 Aug 2021 17:01:52 -0400 Subject: [PATCH 18/52] Last commit didn't commit everything --- .gitignore | 112 +-- .vscode/launch.json | 22 + Dockerfile | 15 +- README.md | 348 ++++++- cypherapps/data/config.json | 5 +- package-lock.json | 905 ++---------------- package.json | 19 +- .../migrations/20210831171126_/migration.sql | 39 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 39 + src/config/LnurlConfig.ts | 3 + src/config/lnurl.sql | 48 +- ....ts => LnurlWithdrawEntity_TypeORM.ts.tmp} | 46 +- src/lib/BatcherClient.ts | 199 ++++ src/lib/CyphernodeClient.ts | 135 +-- src/lib/HttpServer.ts | 17 + src/lib/LnurlDBPrisma.ts | 252 +++++ src/lib/{LnurlDB.ts => LnurlDBTypeORM.ts.tmp} | 91 +- src/lib/LnurlWithdraw.ts | 467 ++++++--- src/lib/Scheduler.ts | 35 +- src/lib/Utils.ts | 63 +- src/types/ILnurlWithdraw.ts | 4 +- src/types/IReqCreateLnurlWithdraw.ts | 4 +- src/types/batcher/IBatchRequestResult.ts | 10 + src/types/batcher/IReqBatchRequest.ts | 9 + src/types/batcher/IRespBatchRequest.ts | 7 + .../CreateLnurlWithdrawValidator.ts | 4 +- tests/README.md | 80 +- tests/mine.sh | 8 +- tests/run_tests.sh | 10 +- 30 files changed, 1717 insertions(+), 1282 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 prisma/migrations/20210831171126_/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma rename src/entity/{LnurlWithdrawEntity.ts => LnurlWithdrawEntity_TypeORM.ts.tmp} (54%) create mode 100644 src/lib/BatcherClient.ts create mode 100644 src/lib/LnurlDBPrisma.ts rename src/lib/{LnurlDB.ts => LnurlDBTypeORM.ts.tmp} (58%) create mode 100644 src/types/batcher/IBatchRequestResult.ts create mode 100644 src/types/batcher/IReqBatchRequest.ts create mode 100644 src/types/batcher/IRespBatchRequest.ts diff --git a/.gitignore b/.gitignore index 2bd8cd3..a8003f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,111 +1,7 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file +node_modules +# Keep environment variables out of version control .env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -logs build -data -!cypherapps/data -*.sqlite +logs +*sqlite* *.pem diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a135da6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach to remote", + "protocol": "inspector", + "address": "localhost", + "port": 9229, + "sourceMaps": true, + "localRoot": "${workspaceRoot}", + "remoteRoot": "/lnurl", + "sourceMapPathOverrides": { + "/usr/src/app/*": "${workspaceRoot}/*" + } + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a87c84f..c0b75bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,21 +10,18 @@ RUN apk add --update --no-cache --virtual .gyp \ g++ RUN npm install -#--------------------------------------------------- +#-------------------------------------------------------------- -FROM build-base as base-slim -WORKDIR /lnurl - -RUN apk del .gyp - -#--------------------------------------------------- - -FROM base-slim +FROM node:14.11.0-alpine3.11 WORKDIR /lnurl +COPY --from=build-base /lnurl/node_modules/ /lnurl/node_modules/ +COPY package.json /lnurl COPY tsconfig.json /lnurl +COPY prisma /lnurl/prisma COPY src /lnurl/src +RUN npx prisma generate RUN npm run build EXPOSE 9229 3000 diff --git a/README.md b/README.md index 9869598..709f0ba 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,317 @@ -# lnurl_cypherapp +# LNURL Cypherapp + LNURL cypherapp for cyphernode -======================== +## LNURL-withdraw happy path + +1. Service (your web app) calls createLnurlWithdraw endpoint, receives a LNURL string +2. Service displays the corresponding QR code +3. User scans the QR code using his LNURL compatible wallet +4. User's wallet calls LNURL-withdraw-request, receives withdraw data +5. User's wallet calls LNURL-withdraw, receives payment status +6. LNURL app uses Cyphernode's ln_pay to send LN payment to user + +## LNURL-withdraw restrictions + +1. If there's an expiration on the LNURL-withdraw, withdraw will fail after the expiration +2. If Service deleted the LNURL, withdraw will fail +3. If there's a fallback Bitcoin address on the LNURL, when expired, LNURL app will send amount on-chain +4. If batching is activated on fallback, the fallback will be sent to the Batcher -LNURL-withdraw +## LNURL-withdraw API endpoints -1. Web app calls lnurl.createLnurl +### createLnurlWithdraw -params: { +Request: +```TypeScript +{ + externalId?: string; + msatoshi: number; + description?: string; + expiration?: Date; + webhookUrl?: string; + btcFallbackAddress?: string; + batchFallback?: boolean; } +``` -========================= -Web app +Response: -### createLNURL - amount = msats - description - expiration = timestamp - secretToken = k1 - webhookUrl = called back when invoice paid +```TypeScript +{ + result?: { + lnurlWithdrawId: number; + externalId: string | null; + msatoshi: number; + description: string | null; + expiration: Date | null; + secretToken: string; + webhookUrl: string | null; + calledback: boolean; + calledbackTs: Date | null; + lnurl: string; + bolt11: string | null; + btcFallbackAddress: string | null; + batchFallback: boolean; + batchRequestId: number | null; + fallbackDone: boolean; + withdrawnDetails: string | null; + withdrawnTs: Date | null; + paid: boolean; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` -returns: - id - LNURL string +### getLnurlWithdraw -https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df -LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS +Request: +```TypeScript +{ + lnurlWithdrawId: number; +} +``` -### deleteLNURL - id +Response: -### getLNURL - id +```TypeScript +{ + result?: { + lnurlWithdrawId: number; + externalId: string | null; + msatoshi: number; + description: string | null; + expiration: Date | null; + secretToken: string; + webhookUrl: string | null; + calledback: boolean; + calledbackTs: Date | null; + lnurl: string; + bolt11: string | null; + btcFallbackAddress: string | null; + batchFallback: boolean; + batchRequestId: number | null; + fallbackDone: boolean; + withdrawnDetails: string | null; + withdrawnTs: Date | null; + paid: boolean; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` -returns: - id - LNURL string +### deleteLnurlWithdraw +Request: -========================= -User/LN Wallet +```TypeScript +{ + lnurlWithdrawId: number; +} +``` -### GET LNURL modalities +Response: -returns: +```TypeScript { - tag: "withdrawRequest", // type of LNURL - callback: String, // the URL which LN SERVICE would accept a withdrawal Lightning invoice as query parameter - k1: String, // random or non-random string to identify the user's LN WALLET when using the callback URL - defaultDescription: String, // A default withdrawal invoice description - minWithdrawable: Integer, // Min amount (in millisatoshis) the user can withdraw from LN SERVICE, or 0 - maxWithdrawable: Integer, // Max amount (in millisatoshis) the user can withdraw from LN SERVICE, or equal to minWithdrawable if the user has no choice over the amounts - balanceCheck: String, // Optional, an URL that can be called next time the wallet wants to perform a balance check, the call will be the same as performed in this step and the expected response is the same + result?: { + lnurlWithdrawId: number; + externalId: string | null; + msatoshi: number; + description: string | null; + expiration: Date | null; + secretToken: string; + webhookUrl: string | null; + calledback: boolean; + calledbackTs: Date | null; + lnurl: string; + bolt11: string | null; + btcFallbackAddress: string | null; + batchFallback: boolean; + batchRequestId: number | null; + fallbackDone: boolean; + withdrawnDetails: string | null; + withdrawnTs: Date | null; + paid: boolean; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } } +``` -### GET payment request +### reloadConfig, getConfig - - // either '?' or '&' depending on whether there is a query string already in the callback - k1= // the k1 specified in the response above - &pr= // the payment request generated by the wallet - &balanceNotify= // optional, see note below +Request: N/A +Response: + +```TypeScript +{ + result?: { + LOG: string; + BASE_DIR: string; + DATA_DIR: string; + DB_NAME: string; + URL_API_SERVER: string; + URL_API_PORT: number; + URL_API_CTX: string; + URL_CTX_WEBHOOKS: string; + SESSION_TIMEOUT: number; + CN_URL: string; + CN_API_ID: string; + CN_API_KEY: string; + BATCHER_URL: string; + LN_SERVICE_SERVER: string; + LN_SERVICE_PORT: number; + LN_SERVICE_CTX: string; + LN_SERVICE_WITHDRAW_REQUEST_CTX: string; + LN_SERVICE_WITHDRAW_CTX: string; + RETRY_WEBHOOKS_TIMEOUT: number; + CHECK_EXPIRATION_TIMEOUT: number; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + +## LNURL-withdraw User/Wallet endpoints + +### /withdrawRequest?s=[secretToken] + +Response: + +```TypeScript +{ + status?: string; + reason?: string; + tag?: string; + callback?: string; + k1?: string; + defaultDescription?: string; + minWithdrawable?: number; + maxWithdrawable?: number; + balanceCheck?: string; +} +``` + +### /withdraw?k1=[secretToken]&pr=[bolt11] + +Response: + +```TypeScript +{ + status?: string; + reason?: string; +} +``` + +## LNURL-withdraw webhooks + +- LNURL Withdrawn using LN + +```json +{ + "lnurlWithdrawId": 1, + "bolt11": "lnbcrt5019590p1psjuc7upp5vzp443fueactllywqp9cm66ewreka2gt37t6su2tcq73hj363cmsdqdv3jhxce38y6njxqyjw5qcqp2sp5cextwkrkepuacr2san20epkgfqfxjukaffd806dgz8z2txrm730s9qy9qsqzuw2a2gempuz78sxa06djguslx0xs8p54656e0m2p82yzzr40rthqkxkpzxk7jhce6lz5m6eyre4jnraz3kfpyd69280qy8k4a6hwrsqxwns9y", + "lnPayResponse": { + "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + "payment_hash": "60835ac53ccf70bffc8e004b8deb5970f36ea90b8f97a8714bc03d1bca3a8e37", + "created_at": 1630430175.298, + "parts": 1, + "msatoshi": 501959, + "amount_msat": "501959msat", + "msatoshi_sent": 501959, + "amount_sent_msat": "501959msat", + "payment_preimage": "337ce72718d3523de121276ef65176d2620959704e442756e81069c34213671f", + "status": "complete" + } +} +``` + +- LNURL Withdraw paid using Bitcoin fallback + +```json +{ + "lnurlWithdrawId": 6, + "btcFallbackAddress": "bcrt1q8hthhmdf9d7v2zrgpdf5ywt3crl25875em649d", + "details": { + "status": "accepted", + "txid": "12a3f45dbec7ddc9f809560560e64bcca40117b1cbba2d6eb9d40b4663066016", + "hash": "9e66b684872fd628974235156e08f189a7de7eb1c084e8022775def0faf059a9", + "details": { + "address": "bcrt1q8hthhmdf9d7v2zrgpdf5ywt3crl25875em649d", + "amount": 5.1e-06, + "firstseen": 1630430188, + "size": 222, + "vsize": 141, + "replaceable": true, + "fee": 2.82e-05, + "subtractfeefromamount": null + } + } +} +``` + +- LNURL Withdraw paid using a batched Bitcoin fallback + +```json +{ + "lnurlWithdrawId": 7, + "btcFallbackAddress": "bcrt1qm2qs6a20k6cv6c8azvu75xsccea65ak65fu3z2", + "details": { + "batchRequestId": 25, + "batchId": 5, + "cnBatcherId": 1, + "requestCountInBatch": 1, + "status": "accepted", + "txid": "a1fcb30493c9bf695e7d8e226c59ec99df0a6f3739f37e8ab122010b7b8d7545", + "hash": "85e6fad9ebcc29d5895de2b9aaa0bdb35a356254a8fa8c1e312e762af3835f6e", + "details": { + "firstseen": 1630430310, + "size": 222, + "vsize": 141, + "replaceable": true, + "fee": 2.82e-05, + "address": "bcrt1qm2qs6a20k6cv6c8azvu75xsccea65ak65fu3z2", + "amount": 5.11e-06 + } + } +} +``` -========================= ========================= +## Temp dev notes + +```bash DOCKER_BUILDKIT=0 docker build -t lnurl . docker run --rm -it -v "$PWD:/lnurl" --entrypoint ash bff4412e444c npm install @@ -78,21 +322,15 @@ npm run start -- -kexkey@AlcodaZ-2 ~ % docker exec -it lnurl ash +docker exec -it lnurl ash /lnurl # apk add curl /lnurl # curl -d '{"id":0,"method":"getConfig","params":[]}' -H "Content-Type: application/json" localhost:8000/api {"id":0,"result":{"LOG":"DEBUG","BASE_DIR":"/lnurl","DATA_DIR":"data","DB_NAME":"lnurl.sqlite","URL_SERVER":"http://lnurl","URL_PORT":8000,"URL_CTX_WEBHOOKS":"webhooks","SESSION_TIMEOUT":600,"CN_URL":"https://gatekeeper:2009/v0","CN_API_ID":"003","CN_API_KEY":"39b83c35972aeb81a242bfe189dc0a22da5ac6cbb64072b492f2d46519a97618"}} - +-- sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" - msatoshi: number; - description?: string; - expiration?: Date; - secretToken: string; - webhookUrl?: string; - curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":0.01,"description":"desc02","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc02","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api {"id":0,"result":{"msatoshi":0.01,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} @@ -114,3 +352,11 @@ curl localhost:8000/withdraw?k1=abc03\&pr=lnbcrt123456780p1ps0pf5ypp5lfskvgsdef4 docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" --network cyphernodeappsnet --entrypoint ash lnurl npm run build npm run start + +DEBUG: +docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" -p 9229:9229 -p 8000:8000 --network cyphernodeappsnet --entrypoint ash lnurl + +npx prisma migrate reset +npx prisma generate +npx prisma migrate dev +``` diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json index e0af926..401f249 100644 --- a/cypherapps/data/config.json +++ b/cypherapps/data/config.json @@ -6,14 +6,17 @@ "URL_API_SERVER": "http://lnurl", "URL_API_PORT": 8000, "URL_API_CTX": "/api", + "URL_CTX_WEBHOOKS": "/webhooks", "SESSION_TIMEOUT": 600, "CN_URL": "https://gatekeeper:2009/v0", "CN_API_ID": "003", "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", + "BATCHER_URL": "http://batcher:8000", "LN_SERVICE_SERVER": "https://yourdomain", "LN_SERVICE_PORT": 443, "LN_SERVICE_CTX": "/lnurl", "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", "LN_SERVICE_WITHDRAW_CTX": "/withdraw", - "RETRY_WEBHOOKS_TIMEOUT": 1 + "RETRY_WEBHOOKS_TIMEOUT": 1, + "CHECK_EXPIRATION_TIMEOUT": 1 } diff --git a/package-lock.json b/package-lock.json index 5dff5c6..e508d13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,10 +82,23 @@ } } }, - "@sqltools/formatter": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz", - "integrity": "sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==" + "@prisma/client": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-2.30.0.tgz", + "integrity": "sha512-tjJNHVfgyNOwS2F+AkjMMCJGPnXzHuUCrOnAMJyidAu4aNzxbJ8jWwjt96rRMpyrg9Hwen3xqqQ2oA+ikK7nhQ==", + "requires": { + "@prisma/engines-version": "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb" + } + }, + "@prisma/engines": { + "version": "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb.tgz", + "integrity": "sha512-LPKq88lIbYezvX0OOc1PU42hHdTsSMPJWmK8lusaHK7DaLHyXjDp/551LbsVapypbjW6N3Jx/If6GoMDASSMSw==" + }, + "@prisma/engines-version": { + "version": "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb.tgz", + "integrity": "sha512-oThNpx7HtJ0eEmnvrWARYcNCs6dqFdAK3Smt2bJVDD6Go4HLuuhjx028osP+rHaFrGOTx7OslLZYtvvFlAXRDA==" }, "@types/async-lock": { "version": "1.1.3", @@ -155,7 +168,8 @@ "@types/node": { "version": "13.13.52", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", - "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==" + "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==", + "dev": true }, "@types/qs": { "version": "6.9.7", @@ -179,20 +193,6 @@ "@types/node": "*" } }, - "@types/sqlite3": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.7.tgz", - "integrity": "sha512-8FHV/8Uzd7IwdHm5mvmF2Aif4aC/gjrt4axWD9SmfaxITnOjtOhCbOSTuqv/VbH1uq0QrwlaTj9aTz3gmR6u4w==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/zen-observable": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", - "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==" - }, "@typescript-eslint/eslint-plugin": { "version": "2.34.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", @@ -270,11 +270,6 @@ } } }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -325,54 +320,15 @@ } } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" - }, - "app-root-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", - "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -400,12 +356,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "bech32": { "version": "2.0.0", @@ -433,20 +385,12 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -467,6 +411,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -478,11 +423,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -492,74 +432,17 @@ "restore-cursor": "^3.1.0" } }, - "cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "requires": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - } - }, "cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -567,17 +450,14 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "content-disposition": { "version": "0.5.3", @@ -602,11 +482,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -620,6 +495,11 @@ "which": "^1.2.9" } }, + "date-fns": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", + "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -628,22 +508,12 @@ "ms": "2.0.0" } }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -654,17 +524,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -674,11 +533,6 @@ "esutils": "^2.0.2" } }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" - }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -687,18 +541,14 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -707,7 +557,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "eslint": { "version": "6.8.0", @@ -1065,11 +916,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "figlet": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.0.tgz", - "integrity": "sha512-ZQJM4aifMpz6H19AW1VqvZ7l4pOE9p7i/3LyxgO2kp+PO/VcDYNqIHEMtkccqIhTXMKci4kjueJr/iCQEaT/Ww==" - }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -1145,18 +991,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "functional-red-black-tree": { "version": "1.0.1", @@ -1164,26 +1003,6 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, "get-stdin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", @@ -1194,6 +1013,7 @@ "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1221,28 +1041,11 @@ "type-fest": "^0.8.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "http-errors": { "version": "1.7.2", @@ -1269,25 +1072,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "ignore-walk": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", - "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", - "requires": { - "minimatch": "^3.0.4" - } - }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -1308,6 +1098,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -1318,11 +1109,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, "inquirer": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", @@ -1389,14 +1175,6 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -1406,11 +1184,6 @@ "is-extglob": "^2.1.1" } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1423,14 +1196,6 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1476,12 +1241,6 @@ } } }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1525,6 +1284,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1532,29 +1292,14 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, "requires": { "minimist": "^1.2.5" } @@ -1570,52 +1315,12 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "needle": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.8.0.tgz", - "integrity": "sha512-ZTq6WYkN/3782H1393me3utVYdq2XyqNUFBsprEE3VMAT0+hP/cItpnITpqsY6ep2yeFE4Tqtqwc74VqUlUYtw==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -1627,86 +1332,6 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", - "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -1719,6 +1344,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -1746,24 +1372,11 @@ "word-wrap": "~1.2.3" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true }, "parent-module": { "version": "1.0.1", @@ -1774,31 +1387,6 @@ "callsites": "^3.0.0" } }, - "parent-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", - "integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc=" - }, - "parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" - }, - "parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "requires": { - "parse5": "^6.0.1" - }, - "dependencies": { - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - } - } - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1807,7 +1395,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "2.0.1", @@ -1841,10 +1430,13 @@ "fast-diff": "^1.1.2" } }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "prisma": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-2.30.0.tgz", + "integrity": "sha512-2XYpSibcVpMd1JDxYypGDU/JKq0W2f/HI1itdddr4Pfg+q6qxt/ItWKcftv4/lqN6u/BVlQ2gDzXVEjpHeO5kQ==", + "requires": { + "@prisma/engines": "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb" + } }, "progress": { "version": "2.0.3", @@ -1888,31 +1480,6 @@ "unpipe": "1.0.0" } }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -1924,11 +1491,6 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1987,15 +1549,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true }, "send": { "version": "0.17.1", @@ -2035,25 +1593,11 @@ "send": "0.17.1" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -2072,7 +1616,8 @@ "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true }, "slice-ansi": { "version": "2.1.0", @@ -2137,55 +1682,16 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sqlite3": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz", - "integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==", - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.11.0" - } - }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -2242,42 +1748,12 @@ } } }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -2298,24 +1774,6 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, - "ts-node": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", - "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", - "dev": true, - "requires": { - "arg": "^4.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.17", - "yn": "3.1.1" - } - }, - "tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" - }, "tslog": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.2.0.tgz", @@ -2365,54 +1823,10 @@ "mime-types": "~2.1.24" } }, - "typeorm": { - "version": "0.2.34", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.34.tgz", - "integrity": "sha512-FZAeEGGdSGq7uTH3FWRQq67JjKu0mgANsSZ04j3kvDYNgy9KwBl/6RFgMVgiSgjf7Rqd7NrhC2KxVT7I80qf7w==", - "requires": { - "@sqltools/formatter": "^1.2.2", - "app-root-path": "^3.0.0", - "buffer": "^6.0.3", - "chalk": "^4.1.0", - "cli-highlight": "^2.1.10", - "debug": "^4.3.1", - "dotenv": "^8.2.0", - "glob": "^7.1.6", - "js-yaml": "^4.0.0", - "mkdirp": "^1.0.4", - "reflect-metadata": "^0.1.13", - "sha.js": "^2.4.11", - "tslib": "^2.1.0", - "xml2js": "^0.4.23", - "yargonaut": "^1.1.4", - "yargs": "^16.2.0", - "zen-observable-ts": "^1.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", "dev": true }, "unpipe": { @@ -2429,11 +1843,6 @@ "punycode": "^2.1.0" } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2459,64 +1868,17 @@ "isexe": "^2.0.0" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write": { "version": "1.0.3", @@ -2526,133 +1888,6 @@ "requires": { "mkdirp": "^0.5.1" } - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "yargonaut": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz", - "integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==", - "requires": { - "chalk": "^1.1.1", - "figlet": "^1.1.1", - "parent-require": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - }, - "zen-observable": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" - }, - "zen-observable-ts": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.0.0.tgz", - "integrity": "sha512-KmWcbz+9kKUeAQ8btY8m1SsEFgBcp7h/Uf3V5quhan7ZWdjGsf0JcGLULQiwOZibbFWnHkYq8Nn2AZbJabovQg==", - "requires": { - "@types/zen-observable": "^0.8.2", - "zen-observable": "^0.8.15" - } } } } diff --git a/package.json b/package.json index 7f706e3..3493725 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rimraf ./build && tsc", - "start:dev": "node --inspect=0.0.0.0:9229 --require ts-node/register ./src/index.ts", - "start": "npm run build && node build/index.js", + "start:dev": "npx prisma migrate dev && node --inspect=0.0.0.0:9229 --require ts-node/register ./src/index.ts", + "start": "npx prisma migrate deploy && npm run build && node build/index.js", "lint": "eslint . --ext .ts", "lintfix": "eslint . --ext .ts --fix" }, @@ -22,22 +22,21 @@ }, "homepage": "https://github.com/SatoshiPortal/lnurl_cypherapp#readme", "dependencies": { - "@types/node": "^13.13.12", + "@prisma/client": "^2.30.0", "@types/async-lock": "^1.1.2", "async-lock": "^1.2.4", "axios": "^0.21.1", + "bech32": "^2.0.0", + "date-fns": "^2.23.0", "express": "^4.17.1", "http-status-codes": "^1.4.0", + "prisma": "^2.30.0", "reflect-metadata": "^0.1.13", - "sqlite3": "^4.2.0", - "typeorm": "^0.2.25", - "tslog": "^3.2.0", - "bech32": "^2.0.0" + "tslog": "^3.2.0" }, "devDependencies": { "@types/express": "^4.17.6", - "@types/node": "^13.13.12", - "@types/sqlite3": "^3.1.6", + "@types/node": "^13.13.52", "@typescript-eslint/eslint-plugin": "^2.24.0", "@typescript-eslint/parser": "^2.24.0", "eslint": "^6.8.0", @@ -46,6 +45,6 @@ "prettier": "2.0.5", "rimraf": "^3.0.2", "ts-node": "^8.10.2", - "typescript": "^3.9.5" + "typescript": "^4.1.0" } } diff --git a/prisma/migrations/20210831171126_/migration.sql b/prisma/migrations/20210831171126_/migration.sql new file mode 100644 index 0000000..0309d2c --- /dev/null +++ b/prisma/migrations/20210831171126_/migration.sql @@ -0,0 +1,39 @@ +-- CreateTable +CREATE TABLE "LnurlWithdrawEntity" ( + "lnurlWithdrawId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "externalId" TEXT, + "msatoshi" INTEGER NOT NULL, + "description" TEXT, + "expiration" DATETIME, + "secretToken" TEXT NOT NULL, + "webhookUrl" TEXT, + "calledback" BOOLEAN NOT NULL DEFAULT false, + "calledbackTs" DATETIME, + "lnurl" TEXT NOT NULL, + "bolt11" TEXT, + "btcFallbackAddress" TEXT, + "batchFallback" BOOLEAN NOT NULL DEFAULT false, + "batchRequestId" INTEGER, + "fallbackDone" BOOLEAN NOT NULL DEFAULT false, + "withdrawnDetails" TEXT, + "withdrawnTs" DATETIME, + "paid" BOOLEAN NOT NULL DEFAULT false, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "createdTs" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedTs" DATETIME NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "LnurlWithdrawEntity.secretToken_unique" ON "LnurlWithdrawEntity"("secretToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "LnurlWithdrawEntity.batchRequestId_unique" ON "LnurlWithdrawEntity"("batchRequestId"); + +-- CreateIndex +CREATE INDEX "LnurlWithdrawEntity.externalId_index" ON "LnurlWithdrawEntity"("externalId"); + +-- CreateIndex +CREATE INDEX "LnurlWithdrawEntity.bolt11_index" ON "LnurlWithdrawEntity"("bolt11"); + +-- CreateIndex +CREATE INDEX "LnurlWithdrawEntity.btcFallbackAddress_index" ON "LnurlWithdrawEntity"("btcFallbackAddress"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..b148174 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,39 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource db { + provider = "sqlite" + url = "file:/lnurl/data/lnurl.sqlite" +} + +generator client { + provider = "prisma-client-js" +} + +model LnurlWithdrawEntity { + lnurlWithdrawId Int @id @default(autoincrement()) + externalId String? + msatoshi Int + description String? + expiration DateTime? + secretToken String @unique + webhookUrl String? + calledback Boolean @default(false) + calledbackTs DateTime? + lnurl String + bolt11 String? + btcFallbackAddress String? + batchFallback Boolean @default(false) + batchRequestId Int? @unique + fallbackDone Boolean @default(false) + withdrawnDetails String? + withdrawnTs DateTime? + paid Boolean @default(false) + deleted Boolean @default(false) + createdTs DateTime @default(now()) + updatedTs DateTime @updatedAt + + @@index([externalId]) + @@index([bolt11]) + @@index([btcFallbackAddress]) +} diff --git a/src/config/LnurlConfig.ts b/src/config/LnurlConfig.ts index b8a1ae9..88aeffe 100644 --- a/src/config/LnurlConfig.ts +++ b/src/config/LnurlConfig.ts @@ -6,14 +6,17 @@ export default interface LnurlConfig { URL_API_SERVER: string; URL_API_PORT: number; URL_API_CTX: string; + URL_CTX_WEBHOOKS: string; SESSION_TIMEOUT: number; CN_URL: string; CN_API_ID: string; CN_API_KEY: string; + BATCHER_URL: string; LN_SERVICE_SERVER: string; LN_SERVICE_PORT: number; LN_SERVICE_CTX: string; LN_SERVICE_WITHDRAW_REQUEST_CTX: string; LN_SERVICE_WITHDRAW_CTX: string; RETRY_WEBHOOKS_TIMEOUT: number; + CHECK_EXPIRATION_TIMEOUT: number; } diff --git a/src/config/lnurl.sql b/src/config/lnurl.sql index 0c26d9b..6fae1db 100644 --- a/src/config/lnurl.sql +++ b/src/config/lnurl.sql @@ -1,22 +1,28 @@ -PRAGMA foreign_keys = ON; - -CREATE TABLE lnurl_withdraw ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - msatoshi INTEGER, - description TEXT, - expiration INTEGER, - secret_token TEXT UNIQUE, - webhook_url TEXT, - calledback INTEGER DEFAULT false, - calledback_ts INTEGER, - lnurl TEXT, - bolt11 TEXT, - withdrawn_details TEXT, - withdrawn_ts INTEGER, - active INTEGER DEFAULT TRUE, - created_ts INTEGER DEFAULT CURRENT_TIMESTAMP, - updated_ts INTEGER DEFAULT CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS "LnurlWithdrawEntity" ( + "lnurlWithdrawId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "externalId" TEXT, + "msatoshi" INTEGER NOT NULL, + "description" TEXT, + "expiration" DATETIME, + "secretToken" TEXT NOT NULL, + "webhookUrl" TEXT, + "calledback" BOOLEAN NOT NULL DEFAULT false, + "calledbackTs" DATETIME, + "lnurl" TEXT NOT NULL, + "bolt11" TEXT, + "btcFallbackAddress" TEXT, + "batchFallback" BOOLEAN NOT NULL DEFAULT false, + "batchRequestId" INTEGER, + "fallbackDone" BOOLEAN NOT NULL DEFAULT false, + "withdrawnDetails" TEXT, + "withdrawnTs" DATETIME, + "paid" BOOLEAN NOT NULL DEFAULT false, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "createdTs" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedTs" DATETIME NOT NULL ); -CREATE INDEX idx_lnurl_withdraw_description ON lnurl_withdraw (description); -CREATE INDEX idx_lnurl_withdraw_lnurl ON lnurl_withdraw (lnurl); -CREATE INDEX idx_lnurl_withdraw_bolt11 ON lnurl_withdraw (bolt11); +CREATE UNIQUE INDEX "LnurlWithdrawEntity.secretToken_unique" ON "LnurlWithdrawEntity"("secretToken"); +CREATE UNIQUE INDEX "LnurlWithdrawEntity.batchRequestId_unique" ON "LnurlWithdrawEntity"("batchRequestId"); +CREATE INDEX "LnurlWithdrawEntity.externalId_index" ON "LnurlWithdrawEntity"("externalId"); +CREATE INDEX "LnurlWithdrawEntity.bolt11_index" ON "LnurlWithdrawEntity"("bolt11"); +CREATE INDEX "LnurlWithdrawEntity.btcFallbackAddress_index" ON "LnurlWithdrawEntity"("btcFallbackAddress"); diff --git a/src/entity/LnurlWithdrawEntity.ts b/src/entity/LnurlWithdrawEntity_TypeORM.ts.tmp similarity index 54% rename from src/entity/LnurlWithdrawEntity.ts rename to src/entity/LnurlWithdrawEntity_TypeORM.ts.tmp index 97be96e..5fb2529 100644 --- a/src/entity/LnurlWithdrawEntity.ts +++ b/src/entity/LnurlWithdrawEntity_TypeORM.ts.tmp @@ -9,30 +9,42 @@ import { // CREATE TABLE lnurl_withdraw ( // id INTEGER PRIMARY KEY AUTOINCREMENT, +// external_id TEXT, // msatoshi INTEGER, // description TEXT, -// expiration INTEGER, +// expiration TEXT, // secret_token TEXT UNIQUE, // webhook_url TEXT, // calledback INTEGER DEFAULT false, -// calledback_ts INTEGER, +// calledback_ts TEXT, // lnurl TEXT, // bolt11 TEXT, +// btc_fallback_addr TEXT, +// batch_fallback INTEGER DEFAULT false, +// batch_request_id INTEGER, +// fallback INTEGER DEFAULT false, // withdrawn_details TEXT, -// withdrawn_ts INTEGER, +// withdrawn_ts TEXT, // active INTEGER, -// created_ts INTEGER DEFAULT CURRENT_TIMESTAMP, -// updated_ts INTEGER DEFAULT CURRENT_TIMESTAMP +// created_ts TEXT DEFAULT CURRENT_TIMESTAMP, +// updated_ts TEXT DEFAULT CURRENT_TIMESTAMP // ); // CREATE INDEX idx_lnurl_withdraw_description ON lnurl_withdraw (description); // CREATE INDEX idx_lnurl_withdraw_lnurl ON lnurl_withdraw (lnurl); // CREATE INDEX idx_lnurl_withdraw_bolt11 ON lnurl_withdraw (bolt11); +// CREATE INDEX idx_lnurl_withdraw_btc_fallback_addr ON lnurl_withdraw (btc_fallback_addr); +// CREATE INDEX idx_lnurl_withdraw_external_id ON lnurl_withdraw (external_id); +// CREATE INDEX idx_lnurl_withdraw_batch_request_id ON lnurl_withdraw (batch_request_id); @Entity("lnurl_withdraw") export class LnurlWithdrawEntity { @PrimaryGeneratedColumn({ name: "id" }) lnurlWithdrawId!: number; + @Index("idx_lnurl_withdraw_external_id") + @Column({ type: "text", name: "external_id", nullable: true }) + externalId?: string; + @Column({ type: "integer", name: "msatoshi" }) msatoshi!: number; @@ -40,7 +52,7 @@ export class LnurlWithdrawEntity { @Column({ type: "text", name: "description", nullable: true }) description?: string; - @Column({ type: "integer", name: "expiration", nullable: true }) + @Column({ type: "text", name: "expiration", nullable: true }) expiration?: Date; @Column({ type: "text", name: "secret_token", unique: true }) @@ -57,7 +69,7 @@ export class LnurlWithdrawEntity { }) calledback?: boolean; - @Column({ type: "integer", name: "calledback_ts", nullable: true }) + @Column({ type: "text", name: "calledback_ts", nullable: true }) calledbackTimestamp?: Date; @Index("idx_lnurl_withdraw_lnurl") @@ -68,18 +80,32 @@ export class LnurlWithdrawEntity { @Column({ type: "text", name: "bolt11", nullable: true }) bolt11?: string; + @Index("idx_lnurl_withdraw_btc_fallback_addr") + @Column({ type: "text", name: "btc_fallback_addr", nullable: true }) + btcFallbackAddress?: string; + + @Column({ type: "integer", name: "batch_fallback", default: false }) + batchFallback?: boolean; + + @Index("idx_lnurl_withdraw_batch_request_id") + @Column({ type: "integer", name: "batch_request_id", nullable: true }) + batchRequestId?: number; + + @Column({ type: "integer", name: "fallback", default: false }) + fallback?: boolean; + @Column({ type: "text", name: "withdrawn_details", nullable: true }) withdrawnDetails?: string; - @Column({ type: "integer", name: "withdrawn_ts", nullable: true }) + @Column({ type: "text", name: "withdrawn_ts", nullable: true }) withdrawnTimestamp?: Date; @Column({ type: "integer", name: "active", nullable: true, default: true }) active?: boolean; - @CreateDateColumn({ type: "integer", name: "created_ts" }) + @CreateDateColumn({ type: "text", name: "created_ts" }) createdAt?: Date; - @UpdateDateColumn({ type: "integer", name: "updated_ts" }) + @UpdateDateColumn({ type: "text", name: "updated_ts" }) updatedAt?: Date; } diff --git a/src/lib/BatcherClient.ts b/src/lib/BatcherClient.ts new file mode 100644 index 0000000..7559b99 --- /dev/null +++ b/src/lib/BatcherClient.ts @@ -0,0 +1,199 @@ +import logger from "./Log2File"; +import axios, { AxiosError, AxiosRequestConfig } from "axios"; +import LnurlConfig from "../config/LnurlConfig"; +import { IResponseError } from "../types/jsonrpc/IResponseMessage"; +import IReqBatchRequest from "../types/batcher/IReqBatchRequest"; +import IRespBatchRequest from "../types/batcher/IRespBatchRequest"; + +class BatcherClient { + private baseURL: string; + + constructor(lnurlConfig: LnurlConfig) { + this.baseURL = lnurlConfig.BATCHER_URL; + } + + configureBatcher(lnurlConfig: LnurlConfig): void { + this.baseURL = lnurlConfig.BATCHER_URL; + } + + async _post( + url: string, + postdata: unknown, + addedOptions?: unknown + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + logger.info("BatcherClient._post:", url, postdata, addedOptions); + + let configs: AxiosRequestConfig = { + url: url, + method: "post", + baseURL: this.baseURL, + timeout: 30000, + data: postdata, + }; + if (addedOptions) { + configs = Object.assign(configs, addedOptions); + } + + // logger.debug( + // "BatcherClient._post :: configs: %s", + // JSON.stringify(configs) + // ); + + try { + const response = await axios.request(configs); + logger.debug("BatcherClient._post :: response.data:", response.data); + + return { status: response.status, data: response.data }; + } catch (err) { + // logger.debug("BatcherClient._post, err:", err); + if (axios.isAxiosError(err)) { + const error: AxiosError = err; + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.info( + "BatcherClient._post :: error.response.data:", + error.response.data + ); + logger.info( + "BatcherClient._post :: error.response.status:", + error.response.status + ); + logger.info( + "BatcherClient._post :: error.response.headers:", + error.response.headers + ); + + return { status: error.response.status, data: error.response.data }; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.info("BatcherClient._post :: error.message:", error.message); + + return { + status: -1, + data: { code: error.code, message: error.message }, + }; + } else { + // Something happened in setting up the request that triggered an Error + logger.info("BatcherClient._post :: Error:", error.message); + + return { + status: -2, + data: { code: error.code, message: error.message }, + }; + } + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { status: -2, data: (err as any).message }; + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async _get(url: string, addedOptions?: unknown): Promise { + logger.info("BatcherClient._get:", url, addedOptions); + + let configs: AxiosRequestConfig = { + url: url, + method: "get", + baseURL: this.baseURL, + timeout: 30000, + }; + if (addedOptions) { + configs = Object.assign(configs, addedOptions); + } + + try { + const response = await axios.request(configs); + logger.debug("BatcherClient._get :: response.data:", response.data); + + return { status: response.status, data: response.data }; + } catch (err) { + // logger.debug("BatcherClient._post, err:", err); + if (axios.isAxiosError(err)) { + const error: AxiosError = err; + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.info( + "BatcherClient._get :: error.response.data:", + error.response.data + ); + logger.info( + "BatcherClient._get :: error.response.status:", + error.response.status + ); + logger.info( + "BatcherClient._get :: error.response.headers:", + error.response.headers + ); + + return { status: error.response.status, data: error.response.data }; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.info("BatcherClient._get :: error.message:", error.message); + + return { + status: -1, + data: { code: error.code, message: error.message }, + }; + } else { + // Something happened in setting up the request that triggered an Error + logger.info("BatcherClient._get :: Error:", error.message); + + return { + status: -2, + data: { code: error.code, message: error.message }, + }; + } + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { status: -2, data: (err as any).message }; + } + } + } + + async queueForNextBatch( + batchRequestTO: IReqBatchRequest + ): Promise { + // { + // batcherId?: number; + // batcherLabel?: string; + // externalId?: number; + // description?: string; + // address: string; + // amount: number; + // webhookUrl?: string; + // } + + logger.info("BatcherClient.queueForNextBatch:", batchRequestTO); + + let result: IRespBatchRequest; + const data = { id: 0, method: "queueForNextBatch", params: batchRequestTO }; + const response = await this._post("/api", data); + + logger.debug("BatcherClient.queueForNextBatch, response:", response); + + if (response.status >= 200 && response.status < 400) { + result = { result: response.data.result }; + } else { + result = { + error: { + code: response.data.error.code, + message: response.data.error.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespBatchRequest; + } + return result; + } +} + +export { BatcherClient }; diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index e458cc2..3b28709 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -1,6 +1,6 @@ import logger from "./Log2File"; import crypto from "crypto"; -import axios, { AxiosRequestConfig } from "axios"; +import axios, { AxiosError, AxiosRequestConfig } from "axios"; import https from "https"; import path from "path"; import fs from "fs"; @@ -92,36 +92,46 @@ class CyphernodeClient { logger.debug("CyphernodeClient._post :: response.data:", response.data); return { status: response.status, data: response.data }; - } catch (error) { - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - logger.info( - "CyphernodeClient._post :: error.response.data:", - error.response.data - ); - logger.info( - "CyphernodeClient._post :: error.response.status:", - error.response.status - ); - logger.info( - "CyphernodeClient._post :: error.response.headers:", - error.response.headers - ); - - return { status: error.response.status, data: error.response.data }; - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - logger.info("CyphernodeClient._post :: error.message:", error.message); - - return { status: -1, data: error.message }; + } catch (err) { + if (axios.isAxiosError(err)) { + const error: AxiosError = err; + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.info( + "CyphernodeClient._post :: error.response.data:", + error.response.data + ); + logger.info( + "CyphernodeClient._post :: error.response.status:", + error.response.status + ); + logger.info( + "CyphernodeClient._post :: error.response.headers:", + error.response.headers + ); + + return { status: error.response.status, data: error.response.data }; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.info( + "CyphernodeClient._post :: error.message:", + error.message + ); + + return { status: -1, data: error.message }; + } else { + // Something happened in setting up the request that triggered an Error + logger.info("CyphernodeClient._post :: Error:", error.message); + + return { status: -2, data: error.message }; + } } else { - // Something happened in setting up the request that triggered an Error - logger.info("CyphernodeClient._post :: Error:", error.message); - - return { status: -2, data: error.message }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { status: -2, data: (err as any).message }; } } } @@ -152,36 +162,43 @@ class CyphernodeClient { logger.debug("CyphernodeClient._get :: response.data:", response.data); return { status: response.status, data: response.data }; - } catch (error) { - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - logger.info( - "CyphernodeClient._get :: error.response.data:", - error.response.data - ); - logger.info( - "CyphernodeClient._get :: error.response.status:", - error.response.status - ); - logger.info( - "CyphernodeClient._get :: error.response.headers:", - error.response.headers - ); - - return { status: error.response.status, data: error.response.data }; - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - logger.info("CyphernodeClient._get :: error.message:", error.message); - - return { status: -1, data: error.message }; + } catch (err) { + if (axios.isAxiosError(err)) { + const error: AxiosError = err; + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.info( + "CyphernodeClient._get :: error.response.data:", + error.response.data + ); + logger.info( + "CyphernodeClient._get :: error.response.status:", + error.response.status + ); + logger.info( + "CyphernodeClient._get :: error.response.headers:", + error.response.headers + ); + + return { status: error.response.status, data: error.response.data }; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.info("CyphernodeClient._get :: error.message:", error.message); + + return { status: -1, data: error.message }; + } else { + // Something happened in setting up the request that triggered an Error + logger.info("CyphernodeClient._get :: Error:", error.message); + + return { status: -2, data: error.message }; + } } else { - // Something happened in setting up the request that triggered an Error - logger.info("CyphernodeClient._get :: Error:", error.message); - - return { status: -2, data: error.message }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { status: -2, data: (err as any).message }; } } } diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 4f33a0e..b093b03 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -239,6 +239,23 @@ class HttpServer { } ); + this._httpServer.post( + this._lnurlConfig.URL_CTX_WEBHOOKS, + async (req, res) => { + logger.info(this._lnurlConfig.URL_CTX_WEBHOOKS + ":", req.body); + + const response = await this._lnurlWithdraw.processBatchWebhook( + req.body + ); + + if (response.error) { + res.status(400).json(response); + } else { + res.status(200).json(response); + } + } + ); + this._httpServer.listen(this._lnurlConfig.URL_API_PORT, () => { logger.info( "Express HTTP server listening on port:", diff --git a/src/lib/LnurlDBPrisma.ts b/src/lib/LnurlDBPrisma.ts new file mode 100644 index 0000000..57934bf --- /dev/null +++ b/src/lib/LnurlDBPrisma.ts @@ -0,0 +1,252 @@ +import logger from "./Log2File"; +import path from "path"; +import LnurlConfig from "../config/LnurlConfig"; +import { LnurlWithdrawEntity, PrismaClient } from "@prisma/client"; + +class LnurlDBPrisma { + private _db?: PrismaClient; + + constructor(lnurlConfig: LnurlConfig) { + this.configureDB(lnurlConfig); + } + + async configureDB(lnurlConfig: LnurlConfig): Promise { + logger.info("LnurlDBPrisma.configureDB", lnurlConfig); + + await this._db?.$disconnect(); + this._db = await this.initDatabase( + path.resolve( + lnurlConfig.BASE_DIR, + lnurlConfig.DATA_DIR, + lnurlConfig.DB_NAME + ) + ); + } + + async initDatabase(dbName: string): Promise { + logger.info("LnurlDBPrisma.initDatabase", dbName); + + return new PrismaClient({ + datasources: { + db: { + // url: "file:" + dbName + "?connection_limit=1&socket_timeout=20", + url: "file:" + dbName + "?socket_timeout=20", + }, + }, + log: ["query", "info", "warn", "error"], + }); + } + + async saveLnurlWithdraw( + lnurlWithdrawEntity: LnurlWithdrawEntity + ): Promise { + // let lw; + // if (lnurlWithdrawEntity.lnurlWithdrawId) { + // lw = await this._db?.lnurlWithdrawEntity.update({ + // where: { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId }, + // data: lnurlWithdrawEntity, + // }); + // } else { + // lw = await this._db?.lnurlWithdrawEntity.create({ + // data: lnurlWithdrawEntity, + // }); + // } + const lw = await this._db?.lnurlWithdrawEntity.upsert({ + where: { secretToken: lnurlWithdrawEntity.secretToken }, + update: lnurlWithdrawEntity, + create: lnurlWithdrawEntity, + }); + + return lw as LnurlWithdrawEntity; + } + + async getLnurlWithdrawBySecret( + secretToken: string + ): Promise { + const lw = await this._db?.lnurlWithdrawEntity.findUnique({ + where: { secretToken }, + }); + + // .f..findUnique( + + // ).ff manager + // .getRepository(LnurlWithdrawEntity) + // .findOne({ where: { secretToken } }); + + // // We need to instantiate a new Date with expiration: + // // https://github.com/typeorm/typeorm/issues/4320 + // if (lw) { + // if (lw.expiration) lw.expiration = new Date(lw.expiration); + // lw.active = ((lw.active as unknown) as number) == 1; + // lw.calledback = ((lw.calledback as unknown) as number) == 1; + // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; + // } + + return lw as LnurlWithdrawEntity; + } + + async getLnurlWithdrawByBatchRequestId( + batchRequestId: number + ): Promise { + const lw = await this._db?.lnurlWithdrawEntity.findUnique({ + where: { batchRequestId }, + }); + + // .manager + // .getRepository(LnurlWithdrawEntity) + // .findOne({ where: { batchRequestId } }); + + // // We need to instantiate a new Date with expiration: + // // https://github.com/typeorm/typeorm/issues/4320 + // if (lw) { + // if (lw.expiration) lw.expiration = new Date(lw.expiration); + // lw.active = ((lw.active as unknown) as number) == 1; + // lw.calledback = ((lw.calledback as unknown) as number) == 1; + // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; + // } + + return lw as LnurlWithdrawEntity; + } + + async getLnurlWithdraw( + lnurlWithdrawEntity: LnurlWithdrawEntity + ): Promise { + const lw = await this._db?.lnurlWithdrawEntity.findUnique({ + where: { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId }, + }); + + // .manager + // .getRepository(LnurlWithdrawEntity) + // .findOne(lnurlWithdrawEntity); + + // // We need to instantiate a new Date with expiration: + // // https://github.com/typeorm/typeorm/issues/4320 + // if (lw) { + // if (lw.expiration) lw.expiration = new Date(lw.expiration); + // lw.active = ((lw.active as unknown) as number) == 1; + // lw.calledback = ((lw.calledback as unknown) as number) == 1; + // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; + // } + + return lw as LnurlWithdrawEntity; + } + + async getLnurlWithdrawById( + lnurlWithdrawId: number + ): Promise { + const lw = await this._db?.lnurlWithdrawEntity.findUnique({ + where: { lnurlWithdrawId: lnurlWithdrawId }, + }); + + // .manager + // .getRepository(LnurlWithdrawEntity) + // .findOne(lnurlWithdrawId); + + // // We need to instantiate a new Date with expiration: + // // https://github.com/typeorm/typeorm/issues/4320 + // if (lw) { + // if (lw.expiration) lw.expiration = new Date(lw.expiration); + // lw.active = ((lw.active as unknown) as number) == 1; + // lw.calledback = ((lw.calledback as unknown) as number) == 1; + // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; + // } + + return lw as LnurlWithdrawEntity; + } + + async getNonCalledbackLnurlWithdraws(): Promise { + const lws = await this._db?.lnurlWithdrawEntity.findMany({ + where: { + deleted: false, + paid: true, + calledback: false, + webhookUrl: { not: null }, + withdrawnDetails: { not: null }, + withdrawnTs: { not: null }, + }, + }); + // const lws = await this._db?.manager + // .getRepository(LnurlWithdrawEntity) + // .find({ + // where: { + // active: false, + // calledback: false, + // webhookUrl: Not(IsNull()), + // withdrawnDetails: Not(IsNull()), + // withdrawnTimestamp: Not(IsNull()), + // }, + // }); + + // // We need to instantiate a new Date with expiration: + // // https://github.com/typeorm/typeorm/issues/4320 + // if (lws && lws.length > 0) { + // lws.forEach((lw) => { + // if (lw.expiration) lw.expiration = new Date(lw.expiration); + // lw.active = ((lw.active as unknown) as number) == 1; + // lw.calledback = ((lw.calledback as unknown) as number) == 1; + // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; + // }); + // } + + return lws as LnurlWithdrawEntity[]; + // return lws; + } + + async getFallbackLnurlWithdraws(): Promise { + const lws = await this._db?.lnurlWithdrawEntity.findMany({ + where: { + deleted: false, + paid: false, + expiration: { lt: new Date() }, + fallbackDone: false, + AND: [ + { NOT: { btcFallbackAddress: null } }, + { NOT: { btcFallbackAddress: "" } }, + ], + }, + }); + // const lws = await this._db?.manager + // .getRepository(LnurlWithdrawEntity) + // .find({ + // where: [ + // { + // active: true, + // // expiration: LessThan(Math.round(new Date().valueOf() / 1000)), + // expiration: LessThan(new Date().toISOString()), + // btcFallbackAddress: Not(IsNull()), + // }, + // { + // active: true, + // expiration: LessThan(new Date().toISOString()), + // btcFallbackAddress: Not(Equal("")), + // }, + // ], + // // where: { + // // { + // // active: true, + // // expiration: LessThan(new Date()), + // // },{ + // // [ + // // { btcFallbackAddress: Not(IsNull()) }, + // // { btcFallbackAddress: Not(Equal("")) }, + // // ]}, + // // } + // }); + + // // We need to instantiate a new Date with expiration: + // // https://github.com/typeorm/typeorm/issues/4320 + // if (lws && lws.length > 0) { + // lws.forEach((lw) => { + // if (lw.expiration) lw.expiration = new Date(lw.expiration); + // lw.active = ((lw.active as unknown) as number) == 1; + // lw.calledback = ((lw.calledback as unknown) as number) == 1; + // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; + // }); + // } + + return lws as LnurlWithdrawEntity[]; + // return []; + } +} + +export { LnurlDBPrisma as LnurlDB }; diff --git a/src/lib/LnurlDB.ts b/src/lib/LnurlDBTypeORM.ts.tmp similarity index 58% rename from src/lib/LnurlDB.ts rename to src/lib/LnurlDBTypeORM.ts.tmp index c4d28ba..e41eb55 100644 --- a/src/lib/LnurlDB.ts +++ b/src/lib/LnurlDBTypeORM.ts.tmp @@ -1,8 +1,15 @@ import logger from "./Log2File"; import path from "path"; import LnurlConfig from "../config/LnurlConfig"; -import { Connection, createConnection, IsNull, Not } from "typeorm"; -import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; +import { + Connection, + createConnection, + Equal, + IsNull, + LessThan, + Not, +} from "typeorm"; +import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity_TypeORM"; class LnurlDB { private _db?: Connection; @@ -29,13 +36,18 @@ class LnurlDB { async initDatabase(dbName: string): Promise { logger.info("LnurlDB.initDatabase", dbName); - return await createConnection({ - type: "sqlite", + const conn = await createConnection({ + type: "better-sqlite3", database: dbName, entities: [LnurlWithdrawEntity], - synchronize: true, - logging: true, + synchronize: false, + logging: "all", + // busyErrorRetry: 1000, }); + await conn.query("PRAGMA foreign_keys=OFF"); + await conn.synchronize(); + await conn.query("PRAGMA foreign_keys=ON"); + return conn; } async saveLnurlWithdraw( @@ -51,6 +63,7 @@ class LnurlDB { if (lw.expiration) lw.expiration = new Date(lw.expiration); lw.active = ((lw.active as unknown) as number) == 1; lw.calledback = ((lw.calledback as unknown) as number) == 1; + lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; } return lw as LnurlWithdrawEntity; @@ -69,6 +82,26 @@ class LnurlDB { if (lw.expiration) lw.expiration = new Date(lw.expiration); lw.active = ((lw.active as unknown) as number) == 1; lw.calledback = ((lw.calledback as unknown) as number) == 1; + lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; + } + + return lw as LnurlWithdrawEntity; + } + + async getLnurlWithdrawByBatchRequestId( + batchRequestId: number + ): Promise { + const lw = await this._db?.manager + .getRepository(LnurlWithdrawEntity) + .findOne({ where: { batchRequestId } }); + + // We need to instantiate a new Date with expiration: + // https://github.com/typeorm/typeorm/issues/4320 + if (lw) { + if (lw.expiration) lw.expiration = new Date(lw.expiration); + lw.active = ((lw.active as unknown) as number) == 1; + lw.calledback = ((lw.calledback as unknown) as number) == 1; + lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; } return lw as LnurlWithdrawEntity; @@ -87,6 +120,7 @@ class LnurlDB { if (lw.expiration) lw.expiration = new Date(lw.expiration); lw.active = ((lw.active as unknown) as number) == 1; lw.calledback = ((lw.calledback as unknown) as number) == 1; + lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; } return lw as LnurlWithdrawEntity; @@ -105,6 +139,7 @@ class LnurlDB { if (lw.expiration) lw.expiration = new Date(lw.expiration); lw.active = ((lw.active as unknown) as number) == 1; lw.calledback = ((lw.calledback as unknown) as number) == 1; + lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; } return lw as LnurlWithdrawEntity; @@ -130,6 +165,50 @@ class LnurlDB { if (lw.expiration) lw.expiration = new Date(lw.expiration); lw.active = ((lw.active as unknown) as number) == 1; lw.calledback = ((lw.calledback as unknown) as number) == 1; + lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; + }); + } + + return lws as LnurlWithdrawEntity[]; + } + + async getFallbackLnurlWithdraws(): Promise { + const lws = await this._db?.manager + .getRepository(LnurlWithdrawEntity) + .find({ + where: [ + { + active: true, + // expiration: LessThan(Math.round(new Date().valueOf() / 1000)), + expiration: LessThan(new Date().toISOString()), + btcFallbackAddress: Not(IsNull()), + }, + { + active: true, + expiration: LessThan(new Date().toISOString()), + btcFallbackAddress: Not(Equal("")), + }, + ], + // where: { + // { + // active: true, + // expiration: LessThan(new Date()), + // },{ + // [ + // { btcFallbackAddress: Not(IsNull()) }, + // { btcFallbackAddress: Not(Equal("")) }, + // ]}, + // } + }); + + // We need to instantiate a new Date with expiration: + // https://github.com/typeorm/typeorm/issues/4320 + if (lws && lws.length > 0) { + lws.forEach((lw) => { + if (lw.expiration) lw.expiration = new Date(lw.expiration); + lw.active = ((lw.active as unknown) as number) == 1; + lw.calledback = ((lw.calledback as unknown) as number) == 1; + lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; }); } diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 6f09774..dabc40d 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -1,12 +1,15 @@ import logger from "./Log2File"; import LnurlConfig from "../config/LnurlConfig"; import { CyphernodeClient } from "./CyphernodeClient"; -import { LnurlDB } from "./LnurlDB"; -import { ErrorCodes } from "../types/jsonrpc/IResponseMessage"; +import { LnurlDB } from "./LnurlDBPrisma"; +import { + ErrorCodes, + IResponseMessage, +} from "../types/jsonrpc/IResponseMessage"; import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; -import IRespGetLnurlWithdraw from "../types/IRespLnurlWithdraw"; +import IRespLnurlWithdraw from "../types/IRespLnurlWithdraw"; import { CreateLnurlWithdrawValidator } from "../validators/CreateLnurlWithdrawValidator"; -import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; +// import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; import IRespLnServiceWithdrawRequest from "../types/IRespLnServiceWithdrawRequest"; import IRespLnServiceStatus from "../types/IRespLnServiceStatus"; import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; @@ -14,17 +17,27 @@ import { Utils } from "./Utils"; import IRespLnPay from "../types/cyphernode/IRespLnPay"; import { LnServiceWithdrawValidator } from "../validators/LnServiceWithdrawValidator"; import { Scheduler } from "./Scheduler"; +import { randomBytes } from "crypto"; +import { BatcherClient } from "./BatcherClient"; +import IReqBatchRequest from "../types/batcher/IReqBatchRequest"; +import IRespBatchRequest from "../types/batcher/IRespBatchRequest"; +import IReqSpend from "../types/cyphernode/IReqSpend"; +import IRespSpend from "../types/cyphernode/IRespSpend"; +import { LnurlWithdrawEntity } from "@prisma/client"; class LnurlWithdraw { private _lnurlConfig: LnurlConfig; private _cyphernodeClient: CyphernodeClient; + private _batcherClient: BatcherClient; private _lnurlDB: LnurlDB; private _scheduler: Scheduler; - private _intervalTimeout?: NodeJS.Timeout; + private _intervalCallbacksTimeout?: NodeJS.Timeout; + private _intervalFallbacksTimeout?: NodeJS.Timeout; constructor(lnurlConfig: LnurlConfig) { this._lnurlConfig = lnurlConfig; this._cyphernodeClient = new CyphernodeClient(this._lnurlConfig); + this._batcherClient = new BatcherClient(this._lnurlConfig); this._lnurlDB = new LnurlDB(this._lnurlConfig); this._scheduler = new Scheduler(this._lnurlConfig); this.startIntervals(); @@ -34,6 +47,7 @@ class LnurlWithdraw { this._lnurlConfig = lnurlConfig; this._lnurlDB.configureDB(this._lnurlConfig).then(() => { this._cyphernodeClient.configureCyphernode(this._lnurlConfig); + this._batcherClient.configureBatcher(this._lnurlConfig); this._scheduler.configureScheduler(this._lnurlConfig).then(() => { this.startIntervals(); }); @@ -41,39 +55,52 @@ class LnurlWithdraw { } startIntervals(): void { - if (this._intervalTimeout) { - clearInterval(this._intervalTimeout); + if (this._intervalCallbacksTimeout) { + clearInterval(this._intervalCallbacksTimeout); } - this._intervalTimeout = setInterval( - this._scheduler.timeout, + this._intervalCallbacksTimeout = setInterval( + this._scheduler.checkCallbacksTimeout, this._lnurlConfig.RETRY_WEBHOOKS_TIMEOUT * 60000, this._scheduler, this ); + + if (this._intervalFallbacksTimeout) { + clearInterval(this._intervalFallbacksTimeout); + } + this._intervalFallbacksTimeout = setInterval( + this._scheduler.checkFallbacksTimeout, + this._lnurlConfig.CHECK_EXPIRATION_TIMEOUT * 60000, + this._scheduler, + this + ); } async createLnurlWithdraw( reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw - ): Promise { + ): Promise { logger.info( "LnurlWithdraw.createLnurlWithdraw, reqCreateLnurlWithdraw:", reqCreateLnurlWithdraw ); - const response: IRespGetLnurlWithdraw = {}; + const response: IRespLnurlWithdraw = {}; if (CreateLnurlWithdrawValidator.validateRequest(reqCreateLnurlWithdraw)) { // Inputs are valid. logger.debug("LnurlWithdraw.createLnurlWithdraw, Inputs are valid."); + const secretToken = randomBytes(16).toString("hex"); + const lnurlDecoded = this._lnurlConfig.LN_SERVICE_SERVER + - // ":" + - // this._lnurlConfig.LN_SERVICE_PORT + + (this._lnurlConfig.LN_SERVICE_PORT === 443 + ? "" + : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + "?s=" + - reqCreateLnurlWithdraw.secretToken; + secretToken; const lnurl = await Utils.encodeBech32(lnurlDecoded); @@ -82,6 +109,7 @@ class LnurlWithdraw { lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( Object.assign(reqCreateLnurlWithdraw as LnurlWithdrawEntity, { lnurl: lnurl, + secretToken: secretToken, }) ); } catch (ex) { @@ -89,14 +117,16 @@ class LnurlWithdraw { response.error = { code: ErrorCodes.InvalidRequest, - message: ex.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message: (ex as any).message, }; return response; } if (lnurlWithdrawEntity) { logger.debug( - "LnurlWithdraw.createLnurlWithdraw, lnurlWithdraw created." + "LnurlWithdraw.createLnurlWithdraw, lnurlWithdraw created:", + lnurlWithdrawEntity ); response.result = Object.assign(lnurlWithdrawEntity, { @@ -130,13 +160,13 @@ class LnurlWithdraw { async deleteLnurlWithdraw( lnurlWithdrawId: number - ): Promise { + ): Promise { logger.info( "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdrawId:", lnurlWithdrawId ); - const response: IRespGetLnurlWithdraw = {}; + const response: IRespLnurlWithdraw = {}; if (lnurlWithdrawId) { // Inputs are valid. @@ -156,23 +186,35 @@ class LnurlWithdraw { code: ErrorCodes.InvalidRequest, message: "LnurlWithdraw not found", }; - } else if (lnurlWithdrawEntity.active) { - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, active lnurlWithdrawEntity found for this lnurlWithdrawId!" - ); + } else if (!lnurlWithdrawEntity.deleted) { + if (!lnurlWithdrawEntity.paid) { + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!" + ); - lnurlWithdrawEntity.active = false; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity.deleted = true; + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); - const lnurlDecoded = await Utils.decodeBech32( - lnurlWithdrawEntity?.lnurl || "" - ); + const lnurlDecoded = await Utils.decodeBech32( + lnurlWithdrawEntity?.lnurl || "" + ); - response.result = Object.assign(lnurlWithdrawEntity, { - lnurlDecoded, - }); + response.result = Object.assign(lnurlWithdrawEntity, { + lnurlDecoded, + }); + } else { + // LnurlWithdraw already paid + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already paid." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw already paid", + }; + } } else { // LnurlWithdraw already deactivated logger.debug( @@ -199,15 +241,13 @@ class LnurlWithdraw { return response; } - async getLnurlWithdraw( - lnurlWithdrawId: number - ): Promise { + async getLnurlWithdraw(lnurlWithdrawId: number): Promise { logger.info( "LnurlWithdraw.getLnurlWithdraw, lnurlWithdrawId:", lnurlWithdrawId ); - const response: IRespGetLnurlWithdraw = {}; + const response: IRespLnurlWithdraw = {}; if (lnurlWithdrawId) { // Inputs are valid. @@ -273,33 +313,42 @@ class LnurlWithdraw { logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, invalid k1 value:"); result = { status: "ERROR", reason: "Invalid k1 value" }; - } else if (lnurlWithdrawEntity.active) { - // Check expiration + } else if (!lnurlWithdrawEntity.deleted) { + if (!lnurlWithdrawEntity.paid) { + // Check expiration - if ( - lnurlWithdrawEntity.expiration && - lnurlWithdrawEntity.expiration < new Date() - ) { - //Expired LNURL - logger.debug("LnurlWithdraw.lnServiceWithdrawRequest: expired!"); + if ( + lnurlWithdrawEntity.expiration && + lnurlWithdrawEntity.expiration < new Date() + ) { + //Expired LNURL + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest: expired!"); - result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; + result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); + + result = { + tag: "withdrawRequest", + callback: + this._lnurlConfig.LN_SERVICE_SERVER + + (this._lnurlConfig.LN_SERVICE_PORT === 443 + ? "" + : ":" + this._lnurlConfig.LN_SERVICE_PORT) + + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, + k1: lnurlWithdrawEntity.secretToken, + defaultDescription: lnurlWithdrawEntity.description || undefined, + minWithdrawable: lnurlWithdrawEntity.msatoshi || undefined, + maxWithdrawable: lnurlWithdrawEntity.msatoshi || undefined, + }; + } } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); - - result = { - tag: "withdrawRequest", - callback: - this._lnurlConfig.LN_SERVICE_SERVER + - // ":" + - // this._lnurlConfig.LN_SERVICE_PORT + - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, - k1: lnurlWithdrawEntity.secretToken, - defaultDescription: lnurlWithdrawEntity.description, - minWithdrawable: lnurlWithdrawEntity.msatoshi, - maxWithdrawable: lnurlWithdrawEntity.msatoshi, - }; + logger.debug( + "LnurlWithdraw.lnServiceWithdrawRequest, LnurlWithdraw already paid" + ); + + result = { status: "ERROR", reason: "LnurlWithdraw already paid" }; } } else { logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, deactivated LNURL"); @@ -331,72 +380,80 @@ class LnurlWithdraw { logger.debug("LnurlWithdraw.lnServiceWithdraw, invalid k1 value!"); result = { status: "ERROR", reason: "Invalid k1 value" }; - } else if (lnurlWithdrawEntity.active) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, active lnurlWithdrawEntity found for this k1!" - ); - - // Check expiration - if ( - lnurlWithdrawEntity.expiration && - lnurlWithdrawEntity.expiration < new Date() - ) { - // Expired LNURL - logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); - - result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; - } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); - - lnurlWithdrawEntity.bolt11 = params.pr; - const lnPayParams = { - bolt11: params.pr, - expectedMsatoshi: lnurlWithdrawEntity.msatoshi, - expectedDescription: lnurlWithdrawEntity.description, - }; - let resp: IRespLnPay = await this._cyphernodeClient.lnPay( - lnPayParams + } else if (!lnurlWithdrawEntity.deleted) { + if (!lnurlWithdrawEntity.paid) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, unpaid lnurlWithdrawEntity found for this k1!" ); - if (resp.error) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry!" + // Check expiration + if ( + lnurlWithdrawEntity.expiration && + lnurlWithdrawEntity.expiration < new Date() + ) { + // Expired LNURL + logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); + + result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); + + lnurlWithdrawEntity.bolt11 = params.pr; + const lnPayParams = { + bolt11: params.pr, + expectedMsatoshi: lnurlWithdrawEntity.msatoshi || undefined, + expectedDescription: lnurlWithdrawEntity.description || undefined, + }; + let resp: IRespLnPay = await this._cyphernodeClient.lnPay( + lnPayParams ); - resp = await this._cyphernodeClient.lnPay(lnPayParams); - } + if (resp.error) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry!" + ); - if (resp.error) { - logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay error!"); + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } - result = { status: "ERROR", reason: resp.error.message }; + if (resp.error) { + logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay error!"); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); + result = { status: "ERROR", reason: resp.error.message }; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); - } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay success!"); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); - result = { status: "OK" }; + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay success!"); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.result); - lnurlWithdrawEntity.withdrawnTimestamp = new Date(); - lnurlWithdrawEntity.active = false; + result = { status: "OK" }; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.result + ); + lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; - if (lnurlWithdrawEntity.webhookUrl) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity ); - this.processCallbacks(lnurlWithdrawEntity); + if (lnurlWithdrawEntity.webhookUrl) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." + ); + + this.processCallbacks(lnurlWithdrawEntity); + } } } + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw, already paid LNURL!"); + + result = { status: "ERROR", reason: "Already paid LNURL" }; } } else { logger.debug("LnurlWithdraw.lnServiceWithdraw, deactivated LNURL!"); @@ -428,9 +485,11 @@ class LnurlWithdraw { lnurlWithdrawEntity ); - let lnurlWithdrawEntitys; + let lnurlWithdrawEntitys: LnurlWithdrawEntity[] = []; if (lnurlWithdrawEntity) { - lnurlWithdrawEntitys = [lnurlWithdrawEntity]; + if (lnurlWithdrawEntity.webhookUrl && !lnurlWithdrawEntity.calledback) { + lnurlWithdrawEntitys = [lnurlWithdrawEntity]; + } } else { lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); } @@ -443,15 +502,28 @@ class LnurlWithdraw { lnurlWithdrawEntity ); - if (lnurlWithdrawEntity.withdrawnTimestamp) { + if (lnurlWithdrawEntity.withdrawnTs) { // Call webhook only if withdraw done - postdata = { - lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, - bolt11: lnurlWithdrawEntity.bolt11, - lnPayResponse: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, - }; + + if (lnurlWithdrawEntity.fallbackDone) { + // If paid through fallback... + postdata = { + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, + details: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, + }; + } else { + // If paid through LN... + postdata = { + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + bolt11: lnurlWithdrawEntity.bolt11, + lnPayResponse: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, + }; + } logger.debug("LnurlWithdraw.processCallbacks, postdata=", postdata); response = await Utils.post( @@ -462,12 +534,165 @@ class LnurlWithdraw { logger.debug("LnurlWithdraw.processCallbacks, webhook called back"); lnurlWithdrawEntity.calledback = true; - lnurlWithdrawEntity.calledbackTimestamp = new Date(); + lnurlWithdrawEntity.calledbackTs = new Date(); await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); } } }); } + + async processFallbacks(): Promise { + logger.info("LnurlWithdraw.processFallbacks"); + + const lnurlWithdrawEntitys = await this._lnurlDB.getFallbackLnurlWithdraws(); + logger.debug( + "LnurlWithdraw.processFallbacks, lnurlWithdrawEntitys=", + lnurlWithdrawEntitys + ); + + lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { + logger.debug( + "LnurlWithdraw.processFallbacks, lnurlWithdrawEntity=", + lnurlWithdrawEntity + ); + + if (lnurlWithdrawEntity.batchFallback) { + logger.debug("LnurlWithdraw.processFallbacks, batched fallback"); + + if (lnurlWithdrawEntity.batchRequestId) { + logger.debug("LnurlWithdraw.processFallbacks, already batched!"); + } else { + // externalId?: string; + // description?: string; + // address: string; + // amount: number; + // webhookUrl?: string; + + const batchRequestTO: IReqBatchRequest = { + externalId: lnurlWithdrawEntity.externalId || undefined, + description: lnurlWithdrawEntity.description || undefined, + address: lnurlWithdrawEntity.btcFallbackAddress || "", + amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, + webhookUrl: + this._lnurlConfig.URL_API_SERVER + + ":" + + this._lnurlConfig.URL_API_PORT + + this._lnurlConfig.URL_CTX_WEBHOOKS, + }; + + const resp: IRespBatchRequest = await this._batcherClient.queueForNextBatch( + batchRequestTO + ); + + if (resp.error) { + logger.debug( + "LnurlWithdraw.processFallbacks, queueForNextBatch error!" + ); + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); + } else { + logger.debug( + "LnurlWithdraw.processFallbacks, queueForNextBatch success!" + ); + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.result); + lnurlWithdrawEntity.batchRequestId = + resp.result?.batchRequestId || null; + } + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + } + } else { + logger.debug("LnurlWithdraw.processFallbacks, not batched fallback"); + + const spendRequestTO: IReqSpend = { + address: lnurlWithdrawEntity.btcFallbackAddress || "", + amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, + }; + + const spendResp: IRespSpend = await this._cyphernodeClient.spend( + spendRequestTO + ); + + if (spendResp?.error) { + // There was an error on Cyphernode end, return that. + logger.debug( + "LnurlWithdraw.processFallbacks: There was an error on Cyphernode spend." + ); + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + spendResp.error + ); + } else if (spendResp?.result) { + logger.debug( + "LnurlWithdraw.processFallbacks: Cyphernode spent: ", + spendResp.result + ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + spendResp.result + ); + lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + lnurlWithdrawEntity.fallbackDone = true; + } + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + if (lnurlWithdrawEntity.fallbackDone) { + this.processCallbacks(lnurlWithdrawEntity); + } + } + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async processBatchWebhook(webhookBody: any): Promise { + logger.info("LnurlWithdraw.processBatchWebhook,", webhookBody); + + // { + // batchRequestId: 48, + // batchId: 8, + // cnBatcherId: 1, + // requestCountInBatch: 12, + // status: "accepted", + // txid: "fc02518e32c22574158b96a513be92739ecb02d0caa463bb273e28d2efead8be", + // hash: "fc02518e32c22574158b96a513be92739ecb02d0caa463bb273e28d2efead8be", + // details: { + // address: "2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp", + // amount: 0.0001, + // firstseen: 1584568841, + // size: 222, + // vsize: 141, + // replaceable: 0, + // fee: 0.00000141, + // } + // } + + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawByBatchRequestId( + webhookBody.batchRequestId + ); + + const result: IResponseMessage = { + id: webhookBody.id, + result: "Merci bonsouère!", + } as IResponseMessage; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(webhookBody); + lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + lnurlWithdrawEntity.fallbackDone = true; + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + this.processCallbacks(lnurlWithdrawEntity); + + return result; + } } export { LnurlWithdraw }; diff --git a/src/lib/Scheduler.ts b/src/lib/Scheduler.ts index d0b2522..fa02735 100644 --- a/src/lib/Scheduler.ts +++ b/src/lib/Scheduler.ts @@ -4,7 +4,8 @@ import { LnurlWithdraw } from "./LnurlWithdraw"; class Scheduler { private _lnurlConfig: LnurlConfig; - private _startedAt = new Date().getTime(); + private _cbStartedAt = new Date().getTime(); + private _fbStartedAt = new Date().getTime(); constructor(lnurlWithdrawConfig: LnurlConfig) { this._lnurlConfig = lnurlWithdrawConfig; @@ -12,17 +13,39 @@ class Scheduler { async configureScheduler(lnurlWithdrawConfig: LnurlConfig): Promise { this._lnurlConfig = lnurlWithdrawConfig; - this._startedAt = new Date().getTime(); + this._cbStartedAt = new Date().getTime(); + this._fbStartedAt = new Date().getTime(); } - timeout(scheduler: Scheduler, lnurlWithdraw: LnurlWithdraw): void { - logger.info("Scheduler.timeout"); + checkCallbacksTimeout( + scheduler: Scheduler, + lnurlWithdraw: LnurlWithdraw + ): void { + logger.info("Scheduler.checkCallbacksTimeout"); - scheduler._startedAt = new Date().getTime(); - logger.debug("Scheduler.timeout this._startedAt =", scheduler._startedAt); + scheduler._cbStartedAt = new Date().getTime(); + logger.debug( + "Scheduler.checkCallbacksTimeout this._cbStartedAt =", + scheduler._cbStartedAt + ); lnurlWithdraw.processCallbacks(undefined); } + + checkFallbacksTimeout( + scheduler: Scheduler, + lnurlWithdraw: LnurlWithdraw + ): void { + logger.info("Scheduler.checkFallbacksTimeout"); + + scheduler._fbStartedAt = new Date().getTime(); + logger.debug( + "Scheduler.checkFallbacksTimeout this._fbStartedAt =", + scheduler._fbStartedAt + ); + + lnurlWithdraw.processFallbacks(); + } } export { Scheduler }; diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts index 5d85a49..dc78d54 100644 --- a/src/lib/Utils.ts +++ b/src/lib/Utils.ts @@ -1,5 +1,5 @@ import logger from "./Log2File"; -import axios, { AxiosRequestConfig } from "axios"; +import axios, { AxiosError, AxiosRequestConfig } from "axios"; import { bech32 } from "bech32"; class Utils { @@ -28,36 +28,43 @@ class Utils { ); return { status: response.status, data: response.data }; - } catch (error) { - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - logger.info( - "Utils.post :: error.response.data =", - JSON.stringify(error.response.data) - ); - logger.info( - "Utils.post :: error.response.status =", - error.response.status - ); - logger.info( - "Utils.post :: error.response.headers =", - error.response.headers - ); + } catch (err) { + if (axios.isAxiosError(err)) { + const error: AxiosError = err; - return { status: error.response.status, data: error.response.data }; - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - logger.info("Utils.post :: error.message =", error.message); + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.info( + "Utils.post :: error.response.data =", + JSON.stringify(error.response.data) + ); + logger.info( + "Utils.post :: error.response.status =", + error.response.status + ); + logger.info( + "Utils.post :: error.response.headers =", + error.response.headers + ); - return { status: -1, data: error.message }; - } else { - // Something happened in setting up the request that triggered an Error - logger.info("Utils.post :: Error:", error.message); + return { status: error.response.status, data: error.response.data }; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.info("Utils.post :: error.message =", error.message); + + return { status: -1, data: error.message }; + } else { + // Something happened in setting up the request that triggered an Error + logger.info("Utils.post :: Error:", error.message); - return { status: -2, data: error.message }; + return { status: -2, data: error.message }; + } + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { status: -2, data: (err as any).message }; } } } diff --git a/src/types/ILnurlWithdraw.ts b/src/types/ILnurlWithdraw.ts index 50724a2..5da9f21 100644 --- a/src/types/ILnurlWithdraw.ts +++ b/src/types/ILnurlWithdraw.ts @@ -1,4 +1,6 @@ -import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; +// import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; + +import { LnurlWithdrawEntity } from "@prisma/client"; export default interface ILnurlWithdraw extends LnurlWithdrawEntity { lnurlDecoded: string; diff --git a/src/types/IReqCreateLnurlWithdraw.ts b/src/types/IReqCreateLnurlWithdraw.ts index a834996..d8653b9 100644 --- a/src/types/IReqCreateLnurlWithdraw.ts +++ b/src/types/IReqCreateLnurlWithdraw.ts @@ -1,7 +1,9 @@ export default interface IReqCreateLnurlWithdraw { + externalId?: string; msatoshi: number; description?: string; expiration?: Date; - secretToken: string; webhookUrl?: string; + btcFallbackAddress?: string; + batchFallback?: boolean; } diff --git a/src/types/batcher/IBatchRequestResult.ts b/src/types/batcher/IBatchRequestResult.ts new file mode 100644 index 0000000..6b84645 --- /dev/null +++ b/src/types/batcher/IBatchRequestResult.ts @@ -0,0 +1,10 @@ +import IAddToBatchResult from "../cyphernode/IAddToBatchResult"; + +export default interface IBatchRequestResult { + batchRequestId: number; + batchId: number; + etaSeconds: number; + cnResult: IAddToBatchResult; + address: string; + amount: number; +} diff --git a/src/types/batcher/IReqBatchRequest.ts b/src/types/batcher/IReqBatchRequest.ts new file mode 100644 index 0000000..0733572 --- /dev/null +++ b/src/types/batcher/IReqBatchRequest.ts @@ -0,0 +1,9 @@ +import IBatcherIdent from "../cyphernode/IBatcherIdent"; + +export default interface IReqBatchRequest extends IBatcherIdent { + externalId?: string; + description?: string; + address: string; + amount: number; + webhookUrl?: string; +} diff --git a/src/types/batcher/IRespBatchRequest.ts b/src/types/batcher/IRespBatchRequest.ts new file mode 100644 index 0000000..684b581 --- /dev/null +++ b/src/types/batcher/IRespBatchRequest.ts @@ -0,0 +1,7 @@ +import IBatchRequestResult from "./IBatchRequestResult"; +import { IResponseError } from "../jsonrpc/IResponseMessage"; + +export default interface IRespBatchRequest { + result?: IBatchRequestResult; + error?: IResponseError; +} diff --git a/src/validators/CreateLnurlWithdrawValidator.ts b/src/validators/CreateLnurlWithdrawValidator.ts index 657e73e..c75e501 100644 --- a/src/validators/CreateLnurlWithdrawValidator.ts +++ b/src/validators/CreateLnurlWithdrawValidator.ts @@ -2,8 +2,8 @@ import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; class CreateLnurlWithdrawValidator { static validateRequest(request: IReqCreateLnurlWithdraw): boolean { - if (request.msatoshi && request.secretToken) { - // Mandatory msatoshi and secretToken found + if (request.msatoshi) { + // Mandatory msatoshi found if (request.expiration) { if (!isNaN(new Date(request.expiration).valueOf())) { // Expiration date is valid diff --git a/tests/README.md b/tests/README.md index c997583..eb2951d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -2,10 +2,17 @@ ## Setup +For these tests to be successful: + +- Set the CHECK_EXPIRATION_TIMEOUT config to 1 +- In the Batcher, set the CHECK_THRESHOLD_MINUTES to 1 and the BATCH_THRESHOLD_AMOUNT to 0.00000500 + +On the Cyphernode ecosystem side: + 1. Have a Cyphernode instance setup in regtest with LN active. 2. Let's double everything LN-related: - * Duplicate the `lightning` block in `dist/docker-compose.yaml` appending a `2` to the names (see below) - * Copy `apps/sparkwallet` to `apps/sparkwallet2` and change `apps/sparkwallet2/docker-compose.yaml` appending a `2` to the names and different port (see below) + - Duplicate the `lightning` block in `dist/docker-compose.yaml` appending a `2` to the names (see below) + - Copy `apps/sparkwallet` to `apps/sparkwallet2` and change `apps/sparkwallet2/docker-compose.yaml` appending a `2` to the names and different port (see below) 3. Mine enough blocks to have regtest coins to open channels between the two LN nodes (use `ln_setup.sh`) 4. Open a channel between the two nodes (use `ln_setup.sh`) 5. If connection is lost between the two nodes (eg. after a restart of Cyphernode), reconnect the two (use `ln_reconnect.sh`) @@ -103,11 +110,66 @@ Run ./run_tests.sh or docker run --rm -it -v "$PWD:/tests" --network=cyphernodeappsnet alpine /tests/lnurl_withdraw.sh ``` -lnurl_withdraw.sh will simulate a real-world use case: +lnurl_withdraw.sh will simulate real-world use cases: + +### Happy path + +1. Create a LNURL Withdraw +2. Get it and compare +3. User calls LNServiceWithdrawRequest with wrong k1 -> Error, wrong k1! +4. User calls LNServiceWithdrawRequest +5. User calls LNServiceWithdraw with wrong k1 -> Error, wrong k1! +6. User calls LNServiceWithdraw + +### Expired 1 + +1. Create a LNURL Withdraw with expiration=now +2. Get it and compare +3. User calls LNServiceWithdrawRequest -> Error, expired! + +### Expired 2 + +1. Create a LNURL Withdraw with expiration=now + 5 seconds +2. Get it and compare +3. User calls LNServiceWithdrawRequest +4. Sleep 5 seconds +5. User calls LNServiceWithdraw -> Error, expired! + +### Deleted 1 + +1. Create a LNURL Withdraw with expiration=now +2. Get it and compare +3. Delete it +4. Get it and compare +5. User calls LNServiceWithdrawRequest -> Error, deleted! + +### Deleted 2 + +1. Create a LNURL Withdraw with expiration=now + 5 seconds +2. Get it and compare +3. User calls LNServiceWithdrawRequest +4. Delete it +5. User calls LNServiceWithdraw -> Error, deleted! + +### fallback 1, use of Bitcoin fallback address + +1. Cyphernode.getnewaddress -> btcfallbackaddr +2. Cyphernode.watch btcfallbackaddr +3. Listen to watch webhook +4. Create a LNURL Withdraw with expiration=now and a btcfallbackaddr +5. Get it and compare +6. User calls LNServiceWithdrawRequest -> Error, expired! +7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) +8. Mined block and Cyphernode's confirmed watch callback called (port 1113) + +### fallback 2, use of Bitcoin fallback address in a batched spend -1. Payer creates a LNURL Withdraw URL -2. Payee calls LNURL URL (Withdraw Request) -3. Payee creates a BOLT11 invoice -4. Payee calls callback URL (Withdraw) -5. LNURL Service (this cypherapp) pays BOLT11 -6. LNURL Service (this cypherapp) calls webhook +1. Cyphernode.getnewaddress -> btcfallbackaddr +2. Cyphernode.watch btcfallbackaddr +3. Listen to watch webhook +4. Create a LNURL Withdraw with expiration=now and a btcfallbackaddr +5. Get it and compare +6. User calls LNServiceWithdrawRequest -> Error, expired! +7. Fallback should be triggered, added to current batch using the Batcher +8. Wait for the batch to execute, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112), Batcher's execute callback called (port 1113) +9. Mined block and Cyphernode's confirmed watch callback called (port 1114) diff --git a/tests/mine.sh b/tests/mine.sh index 0276b5f..fc51b92 100755 --- a/tests/mine.sh +++ b/tests/mine.sh @@ -3,12 +3,16 @@ # Mine mine() { local nbblocks=${1:-1} + local interactive=${2:-1} local minedaddr + if [ "${interactive}" = "1" ]; then + interactivearg="-it" + fi echo ; echo "About to mine ${nbblocks} block(s)..." - minedaddr=$(docker exec -it $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat getnewaddress | tr -d '\r') + minedaddr=$(docker exec ${interactivearg} $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat getnewaddress | tr -d '\r') echo ; echo "minedaddr=${minedaddr}" - docker exec -it $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat generatetoaddress ${nbblocks} "${minedaddr}" + docker exec ${interactivearg} $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat generatetoaddress ${nbblocks} "${minedaddr}" } case "${0}" in *mine.sh) mine $@;; esac diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 846692c..ff4e8a8 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -1,5 +1,11 @@ #!/bin/sh -#./startcallbackserver.sh & +# Set the CHECK_EXPIRATION_TIMEOUT config to 1 for this test to be successful -docker run --rm -it -v "$PWD:/tests" --network=cyphernodeappsnet alpine /tests/lnurl_withdraw.sh +./ln_reconnect.sh + +docker run --rm -d --name lnurl_withdraw_test -v "$PWD:/tests" --network=cyphernodeappsnet alpine sh -c 'while true; do sleep 10; done' +docker network connect cyphernodenet lnurl_withdraw_test +(sleep 90 ; ./mine.sh 1 0 ; sleep 90 ; ./mine.sh 1 0) & +docker exec -it lnurl_withdraw_test /tests/lnurl_withdraw.sh +docker stop lnurl_withdraw_test From 265468df43d9bd19f0eba1e4fe33a2e94179686e Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 2 Sep 2021 15:03:45 -0400 Subject: [PATCH 19/52] expiresAt and batched webhook --- .../migration.sql | 8 +- prisma/schema.prisma | 8 +- src/lib/LnurlDBPrisma.ts | 137 +----------------- src/lib/LnurlWithdraw.ts | 120 ++++++++++----- src/types/IReqCreateLnurlWithdraw.ts | 2 +- .../CreateLnurlWithdrawValidator.ts | 8 +- tests/lnurl_withdraw.sh | 18 ++- tests/startcallbackserver.sh | 4 +- 8 files changed, 117 insertions(+), 188 deletions(-) rename prisma/migrations/{20210831171126_ => 20210902163646_}/migration.sql (86%) diff --git a/prisma/migrations/20210831171126_/migration.sql b/prisma/migrations/20210902163646_/migration.sql similarity index 86% rename from prisma/migrations/20210831171126_/migration.sql rename to prisma/migrations/20210902163646_/migration.sql index 0309d2c..f071fd4 100644 --- a/prisma/migrations/20210831171126_/migration.sql +++ b/prisma/migrations/20210902163646_/migration.sql @@ -4,11 +4,13 @@ CREATE TABLE "LnurlWithdrawEntity" ( "externalId" TEXT, "msatoshi" INTEGER NOT NULL, "description" TEXT, - "expiration" DATETIME, + "expiresAt" DATETIME, "secretToken" TEXT NOT NULL, "webhookUrl" TEXT, - "calledback" BOOLEAN NOT NULL DEFAULT false, - "calledbackTs" DATETIME, + "paidCalledback" BOOLEAN NOT NULL DEFAULT false, + "paidCalledbackTs" DATETIME, + "batchedCalledback" BOOLEAN NOT NULL DEFAULT false, + "batchedCalledbackTs" DATETIME, "lnurl" TEXT NOT NULL, "bolt11" TEXT, "btcFallbackAddress" TEXT, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b148174..896f6d9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,11 +15,13 @@ model LnurlWithdrawEntity { externalId String? msatoshi Int description String? - expiration DateTime? + expiresAt DateTime? secretToken String @unique webhookUrl String? - calledback Boolean @default(false) - calledbackTs DateTime? + paidCalledback Boolean @default(false) + paidCalledbackTs DateTime? + batchedCalledback Boolean @default(false) + batchedCalledbackTs DateTime? lnurl String bolt11 String? btcFallbackAddress String? diff --git a/src/lib/LnurlDBPrisma.ts b/src/lib/LnurlDBPrisma.ts index 57934bf..6f0dff5 100644 --- a/src/lib/LnurlDBPrisma.ts +++ b/src/lib/LnurlDBPrisma.ts @@ -40,17 +40,6 @@ class LnurlDBPrisma { async saveLnurlWithdraw( lnurlWithdrawEntity: LnurlWithdrawEntity ): Promise { - // let lw; - // if (lnurlWithdrawEntity.lnurlWithdrawId) { - // lw = await this._db?.lnurlWithdrawEntity.update({ - // where: { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId }, - // data: lnurlWithdrawEntity, - // }); - // } else { - // lw = await this._db?.lnurlWithdrawEntity.create({ - // data: lnurlWithdrawEntity, - // }); - // } const lw = await this._db?.lnurlWithdrawEntity.upsert({ where: { secretToken: lnurlWithdrawEntity.secretToken }, update: lnurlWithdrawEntity, @@ -67,21 +56,6 @@ class LnurlDBPrisma { where: { secretToken }, }); - // .f..findUnique( - - // ).ff manager - // .getRepository(LnurlWithdrawEntity) - // .findOne({ where: { secretToken } }); - - // // We need to instantiate a new Date with expiration: - // // https://github.com/typeorm/typeorm/issues/4320 - // if (lw) { - // if (lw.expiration) lw.expiration = new Date(lw.expiration); - // lw.active = ((lw.active as unknown) as number) == 1; - // lw.calledback = ((lw.calledback as unknown) as number) == 1; - // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; - // } - return lw as LnurlWithdrawEntity; } @@ -92,19 +66,6 @@ class LnurlDBPrisma { where: { batchRequestId }, }); - // .manager - // .getRepository(LnurlWithdrawEntity) - // .findOne({ where: { batchRequestId } }); - - // // We need to instantiate a new Date with expiration: - // // https://github.com/typeorm/typeorm/issues/4320 - // if (lw) { - // if (lw.expiration) lw.expiration = new Date(lw.expiration); - // lw.active = ((lw.active as unknown) as number) == 1; - // lw.calledback = ((lw.calledback as unknown) as number) == 1; - // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; - // } - return lw as LnurlWithdrawEntity; } @@ -115,19 +76,6 @@ class LnurlDBPrisma { where: { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId }, }); - // .manager - // .getRepository(LnurlWithdrawEntity) - // .findOne(lnurlWithdrawEntity); - - // // We need to instantiate a new Date with expiration: - // // https://github.com/typeorm/typeorm/issues/4320 - // if (lw) { - // if (lw.expiration) lw.expiration = new Date(lw.expiration); - // lw.active = ((lw.active as unknown) as number) == 1; - // lw.calledback = ((lw.calledback as unknown) as number) == 1; - // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; - // } - return lw as LnurlWithdrawEntity; } @@ -138,19 +86,6 @@ class LnurlDBPrisma { where: { lnurlWithdrawId: lnurlWithdrawId }, }); - // .manager - // .getRepository(LnurlWithdrawEntity) - // .findOne(lnurlWithdrawId); - - // // We need to instantiate a new Date with expiration: - // // https://github.com/typeorm/typeorm/issues/4320 - // if (lw) { - // if (lw.expiration) lw.expiration = new Date(lw.expiration); - // lw.active = ((lw.active as unknown) as number) == 1; - // lw.calledback = ((lw.calledback as unknown) as number) == 1; - // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; - // } - return lw as LnurlWithdrawEntity; } @@ -158,38 +93,17 @@ class LnurlDBPrisma { const lws = await this._db?.lnurlWithdrawEntity.findMany({ where: { deleted: false, - paid: true, - calledback: false, webhookUrl: { not: null }, withdrawnDetails: { not: null }, - withdrawnTs: { not: null }, + // withdrawnTs: { not: null }, + AND: [ + { OR: [{ paid: true }, { batchRequestId: { not: null } }] }, + { OR: [{ paidCalledback: false }, { batchedCalledback: false }] }, + ], }, }); - // const lws = await this._db?.manager - // .getRepository(LnurlWithdrawEntity) - // .find({ - // where: { - // active: false, - // calledback: false, - // webhookUrl: Not(IsNull()), - // withdrawnDetails: Not(IsNull()), - // withdrawnTimestamp: Not(IsNull()), - // }, - // }); - - // // We need to instantiate a new Date with expiration: - // // https://github.com/typeorm/typeorm/issues/4320 - // if (lws && lws.length > 0) { - // lws.forEach((lw) => { - // if (lw.expiration) lw.expiration = new Date(lw.expiration); - // lw.active = ((lw.active as unknown) as number) == 1; - // lw.calledback = ((lw.calledback as unknown) as number) == 1; - // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; - // }); - // } return lws as LnurlWithdrawEntity[]; - // return lws; } async getFallbackLnurlWithdraws(): Promise { @@ -197,7 +111,7 @@ class LnurlDBPrisma { where: { deleted: false, paid: false, - expiration: { lt: new Date() }, + expiresAt: { lt: new Date() }, fallbackDone: false, AND: [ { NOT: { btcFallbackAddress: null } }, @@ -205,47 +119,8 @@ class LnurlDBPrisma { ], }, }); - // const lws = await this._db?.manager - // .getRepository(LnurlWithdrawEntity) - // .find({ - // where: [ - // { - // active: true, - // // expiration: LessThan(Math.round(new Date().valueOf() / 1000)), - // expiration: LessThan(new Date().toISOString()), - // btcFallbackAddress: Not(IsNull()), - // }, - // { - // active: true, - // expiration: LessThan(new Date().toISOString()), - // btcFallbackAddress: Not(Equal("")), - // }, - // ], - // // where: { - // // { - // // active: true, - // // expiration: LessThan(new Date()), - // // },{ - // // [ - // // { btcFallbackAddress: Not(IsNull()) }, - // // { btcFallbackAddress: Not(Equal("")) }, - // // ]}, - // // } - // }); - - // // We need to instantiate a new Date with expiration: - // // https://github.com/typeorm/typeorm/issues/4320 - // if (lws && lws.length > 0) { - // lws.forEach((lw) => { - // if (lw.expiration) lw.expiration = new Date(lw.expiration); - // lw.active = ((lw.active as unknown) as number) == 1; - // lw.calledback = ((lw.calledback as unknown) as number) == 1; - // lw.batchFallback = ((lw.batchFallback as unknown) as number) == 1; - // }); - // } return lws as LnurlWithdrawEntity[]; - // return []; } } diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index dabc40d..97d1b46 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -9,7 +9,6 @@ import { import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; import IRespLnurlWithdraw from "../types/IRespLnurlWithdraw"; import { CreateLnurlWithdrawValidator } from "../validators/CreateLnurlWithdrawValidator"; -// import { LnurlWithdrawEntity } from "../entity/LnurlWithdrawEntity"; import IRespLnServiceWithdrawRequest from "../types/IRespLnServiceWithdrawRequest"; import IRespLnServiceStatus from "../types/IRespLnServiceStatus"; import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; @@ -318,10 +317,10 @@ class LnurlWithdraw { // Check expiration if ( - lnurlWithdrawEntity.expiration && - lnurlWithdrawEntity.expiration < new Date() + lnurlWithdrawEntity.expiresAt && + lnurlWithdrawEntity.expiresAt < new Date() ) { - //Expired LNURL + // Expired LNURL logger.debug("LnurlWithdraw.lnServiceWithdrawRequest: expired!"); result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; @@ -388,8 +387,8 @@ class LnurlWithdraw { // Check expiration if ( - lnurlWithdrawEntity.expiration && - lnurlWithdrawEntity.expiration < new Date() + lnurlWithdrawEntity.expiresAt && + lnurlWithdrawEntity.expiresAt < new Date() ) { // Expired LNURL logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); @@ -487,55 +486,98 @@ class LnurlWithdraw { let lnurlWithdrawEntitys: LnurlWithdrawEntity[] = []; if (lnurlWithdrawEntity) { - if (lnurlWithdrawEntity.webhookUrl && !lnurlWithdrawEntity.calledback) { - lnurlWithdrawEntitys = [lnurlWithdrawEntity]; - } + lnurlWithdrawEntitys = [lnurlWithdrawEntity]; } else { lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); } let response; - let postdata; + let postdata = {}; lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { logger.debug( "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", lnurlWithdrawEntity ); - if (lnurlWithdrawEntity.withdrawnTs) { - // Call webhook only if withdraw done - - if (lnurlWithdrawEntity.fallbackDone) { - // If paid through fallback... + if ( + !lnurlWithdrawEntity.deleted && + lnurlWithdrawEntity.webhookUrl && + lnurlWithdrawEntity.webhookUrl.length > 0 && + lnurlWithdrawEntity.withdrawnDetails && + lnurlWithdrawEntity.withdrawnDetails.length > 0 + ) { + if ( + !lnurlWithdrawEntity.batchedCalledback && + lnurlWithdrawEntity.batchRequestId + ) { + // Payment has been batched, not yet paid postdata = { + action: "fallbackBatched", lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, details: lnurlWithdrawEntity.withdrawnDetails ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) : null, }; - } else { - // If paid through LN... - postdata = { - lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, - bolt11: lnurlWithdrawEntity.bolt11, - lnPayResponse: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, - }; + logger.debug( + "LnurlWithdraw.processCallbacks, batched, postdata=", + postdata + ); + + response = await Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); + + if (response.status >= 200 && response.status < 400) { + logger.debug( + "LnurlWithdraw.processCallbacks, batched, webhook called back" + ); + + lnurlWithdrawEntity.batchedCalledback = true; + lnurlWithdrawEntity.batchedCalledbackTs = new Date(); + await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + } } - logger.debug("LnurlWithdraw.processCallbacks, postdata=", postdata); - response = await Utils.post( - lnurlWithdrawEntity.webhookUrl || "", - postdata - ); - if (response.status >= 200 && response.status < 400) { - logger.debug("LnurlWithdraw.processCallbacks, webhook called back"); + if (!lnurlWithdrawEntity.paidCalledback && lnurlWithdrawEntity.paid) { + // Payment has been sent + + if (lnurlWithdrawEntity.fallbackDone) { + // If paid through fallback... + postdata = { + action: "fallbackPaid", + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, + details: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, + }; + } else { + // If paid through LN... + postdata = { + action: "lnPaid", + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + bolt11: lnurlWithdrawEntity.bolt11, + lnPayResponse: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, + }; + } + + logger.debug( + "LnurlWithdraw.processCallbacks, paid, postdata=", + postdata + ); - lnurlWithdrawEntity.calledback = true; - lnurlWithdrawEntity.calledbackTs = new Date(); - await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + response = await Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); + + if (response.status >= 200 && response.status < 400) { + logger.debug( + "LnurlWithdraw.processCallbacks, paid, webhook called back" + ); + + lnurlWithdrawEntity.paidCalledback = true; + lnurlWithdrawEntity.paidCalledbackTs = new Date(); + await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + } } } }); @@ -562,12 +604,6 @@ class LnurlWithdraw { if (lnurlWithdrawEntity.batchRequestId) { logger.debug("LnurlWithdraw.processFallbacks, already batched!"); } else { - // externalId?: string; - // description?: string; - // address: string; - // amount: number; - // webhookUrl?: string; - const batchRequestTO: IReqBatchRequest = { externalId: lnurlWithdrawEntity.externalId || undefined, description: lnurlWithdrawEntity.description || undefined, @@ -603,6 +639,10 @@ class LnurlWithdraw { lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( lnurlWithdrawEntity ); + + if (lnurlWithdrawEntity.batchRequestId) { + this.processCallbacks(lnurlWithdrawEntity); + } } } else { logger.debug("LnurlWithdraw.processFallbacks, not batched fallback"); diff --git a/src/types/IReqCreateLnurlWithdraw.ts b/src/types/IReqCreateLnurlWithdraw.ts index d8653b9..d8a499d 100644 --- a/src/types/IReqCreateLnurlWithdraw.ts +++ b/src/types/IReqCreateLnurlWithdraw.ts @@ -2,7 +2,7 @@ export default interface IReqCreateLnurlWithdraw { externalId?: string; msatoshi: number; description?: string; - expiration?: Date; + expiresAt?: Date; webhookUrl?: string; btcFallbackAddress?: string; batchFallback?: boolean; diff --git a/src/validators/CreateLnurlWithdrawValidator.ts b/src/validators/CreateLnurlWithdrawValidator.ts index c75e501..d68bfa5 100644 --- a/src/validators/CreateLnurlWithdrawValidator.ts +++ b/src/validators/CreateLnurlWithdrawValidator.ts @@ -4,13 +4,13 @@ class CreateLnurlWithdrawValidator { static validateRequest(request: IReqCreateLnurlWithdraw): boolean { if (request.msatoshi) { // Mandatory msatoshi found - if (request.expiration) { - if (!isNaN(new Date(request.expiration).valueOf())) { - // Expiration date is valid + if (request.expiresAt) { + if (!isNaN(new Date(request.expiresAt).valueOf())) { + // expiresAt date is valid return true; } } else { - // No expiration date + // No expiresAt date return true; } } diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index d4d50f5..9c6bbad 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -87,13 +87,13 @@ create_lnurl_withdraw() { local fallback_batched=${5:-"false"} # Service creates LNURL Withdraw - data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiration":"'${expiration}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'","btcFallbackAddress":"'${fallback_addr}'","batchFallback":'${fallback_batched}'}}' + data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiresAt":"'${expiration}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'","btcFallbackAddress":"'${fallback_addr}'","batchFallback":'${fallback_batched}'}}' trace 3 "[create_lnurl_withdraw] data=${data}" trace 3 "[create_lnurl_withdraw] Calling createLnurlWithdraw..." local createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[create_lnurl_withdraw] createLnurlWithdraw=${createLnurlWithdraw}" - # {"id":0,"result":{"msatoshi":100000000,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} + # {"id":0,"result":{"msatoshi":100000000,"description":"desc01","expiresAt":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "[create_lnurl_withdraw] lnurl=${lnurl}" @@ -688,9 +688,8 @@ fallback2() { local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") trace 3 "[fallback2] urlSuffix=${urlSuffix}" + # fallback batched callback start_callback_server - start_callback_server ${zeroconfport} - start_callback_server ${oneconfport} # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") @@ -704,6 +703,17 @@ fallback2() { trace 2 "[fallback2] EXPIRED!" fi + trace 3 "[fallback2] Waiting for fallback batched callback..." + + wait + + # fallback paid callback + start_callback_server + # 0-conf callback + start_callback_server ${zeroconfport} + # 1-conf callback + start_callback_server ${oneconfport} + trace 3 "[fallback2] Waiting for fallback execution and a block mined..." wait diff --git a/tests/startcallbackserver.sh b/tests/startcallbackserver.sh index 9e8c364..9155cd8 100755 --- a/tests/startcallbackserver.sh +++ b/tests/startcallbackserver.sh @@ -2,8 +2,8 @@ date -callbackservername="cb" +callbackservername="lnurl_withdraw_test" callbackserverport="1111" -docker run --rm -it -p ${callbackserverport}:${callbackserverport} --network cyphernodeappsnet --name ${callbackservername} alpine sh -c "nc -vlkp${callbackserverport} -e sh -c 'echo -en \"HTTP/1.1 200 OK\\\\r\\\\n\\\\r\\\\n\" ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo 1>&2'" +docker run --rm -it --network cyphernodeappsnet --name ${callbackservername} alpine sh -c "nc -vlkp${callbackserverport} -e sh -c 'echo -en \"HTTP/1.1 200 OK\\\\r\\\\n\\\\r\\\\n\" ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo 1>&2'" From ccde15dd50d59813fc88da92664876348383e127 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 2 Sep 2021 16:26:45 -0400 Subject: [PATCH 20/52] Fixed timeout management and locks --- src/lib/HttpServer.ts | 35 +++++++++++++++++++++++----- src/lib/LnurlDBPrisma.ts | 12 +++++++++- src/lib/LnurlWithdraw.ts | 23 ++++++++++++------- src/lib/Scheduler.ts | 44 +++++++++++++++++++++++++++--------- tests/startcallbackserver.sh | 2 +- 5 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index b093b03..0f7def7 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -113,16 +113,16 @@ class HttpServer { let result: IRespCreateLnurlWithdraw = {}; result = await this._lock.acquire( - "deleteLnurlWithdraw", + "modifLnurlWithdraw", async (): Promise => { logger.debug( - "acquired lock deleteLnurlWithdraw in deleteLnurlWithdraw" + "acquired lock modifLnurlWithdraw in deleteLnurlWithdraw" ); return await this.deleteLnurlWithdraw(reqMessage.params || {}); } ); logger.debug( - "released lock deleteLnurlWithdraw in deleteLnurlWithdraw" + "released lock modifLnurlWithdraw in deleteLnurlWithdraw" ); response.result = result.result; @@ -130,6 +130,29 @@ class HttpServer { break; } + case "processCallbacks": { + this._lnurlWithdraw.processCallbacks(); + + response.result = {}; + break; + } + + case "processFallbacks": { + await this._lock.acquire( + "modifLnurlWithdraw", + async (): Promise => { + logger.debug( + "acquired lock modifLnurlWithdraw in processFallbacks" + ); + await this._lnurlWithdraw.processFallbacks(); + } + ); + logger.debug("released lock modifLnurlWithdraw in processFallbacks"); + + response.result = {}; + break; + } + case "encodeBech32": { response.result = await Utils.encodeBech32( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -183,7 +206,7 @@ class HttpServer { let response: IRespLnServiceWithdrawRequest = {}; response = await this._lock.acquire( - "deleteLnurlWithdraw", + "modifLnurlWithdraw", async (): Promise => { logger.debug( "acquired lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" @@ -218,7 +241,7 @@ class HttpServer { "deleteLnurlWithdraw", async (): Promise => { logger.debug( - "acquired lock deleteLnurlWithdraw in LN Service LNURL Withdraw" + "acquired lock modifLnurlWithdraw in LN Service LNURL Withdraw" ); return await this._lnurlWithdraw.lnServiceWithdraw({ k1: req.query.k1, @@ -228,7 +251,7 @@ class HttpServer { } ); logger.debug( - "released lock deleteLnurlWithdraw in LN Service LNURL Withdraw" + "released lock modifLnurlWithdraw in LN Service LNURL Withdraw" ); if (response.status === "ERROR") { diff --git a/src/lib/LnurlDBPrisma.ts b/src/lib/LnurlDBPrisma.ts index 6f0dff5..4103720 100644 --- a/src/lib/LnurlDBPrisma.ts +++ b/src/lib/LnurlDBPrisma.ts @@ -98,7 +98,17 @@ class LnurlDBPrisma { // withdrawnTs: { not: null }, AND: [ { OR: [{ paid: true }, { batchRequestId: { not: null } }] }, - { OR: [{ paidCalledback: false }, { batchedCalledback: false }] }, + { + OR: [ + { paidCalledback: false }, + { + AND: [ + { batchedCalledback: false }, + { batchRequestId: { not: null } }, + ], + }, + ], + }, ], }, }); diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 97d1b46..9cc1912 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -60,8 +60,7 @@ class LnurlWithdraw { this._intervalCallbacksTimeout = setInterval( this._scheduler.checkCallbacksTimeout, this._lnurlConfig.RETRY_WEBHOOKS_TIMEOUT * 60000, - this._scheduler, - this + this._scheduler ); if (this._intervalFallbacksTimeout) { @@ -313,7 +312,7 @@ class LnurlWithdraw { result = { status: "ERROR", reason: "Invalid k1 value" }; } else if (!lnurlWithdrawEntity.deleted) { - if (!lnurlWithdrawEntity.paid) { + if (!lnurlWithdrawEntity.paid && !lnurlWithdrawEntity.batchRequestId) { // Check expiration if ( @@ -344,10 +343,13 @@ class LnurlWithdraw { } } else { logger.debug( - "LnurlWithdraw.lnServiceWithdrawRequest, LnurlWithdraw already paid" + "LnurlWithdraw.lnServiceWithdrawRequest, LnurlWithdraw already paid or batched" ); - result = { status: "ERROR", reason: "LnurlWithdraw already paid" }; + result = { + status: "ERROR", + reason: "LnurlWithdraw already paid or batched", + }; } } else { logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, deactivated LNURL"); @@ -380,7 +382,7 @@ class LnurlWithdraw { result = { status: "ERROR", reason: "Invalid k1 value" }; } else if (!lnurlWithdrawEntity.deleted) { - if (!lnurlWithdrawEntity.paid) { + if (!lnurlWithdrawEntity.paid && !lnurlWithdrawEntity.batchRequestId) { logger.debug( "LnurlWithdraw.lnServiceWithdraw, unpaid lnurlWithdrawEntity found for this k1!" ); @@ -450,9 +452,14 @@ class LnurlWithdraw { } } } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw, already paid LNURL!"); + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, already paid or batched!" + ); - result = { status: "ERROR", reason: "Already paid LNURL" }; + result = { + status: "ERROR", + reason: "LnurlWithdraw already paid or batched", + }; } } else { logger.debug("LnurlWithdraw.lnServiceWithdraw, deactivated LNURL!"); diff --git a/src/lib/Scheduler.ts b/src/lib/Scheduler.ts index fa02735..701c6f9 100644 --- a/src/lib/Scheduler.ts +++ b/src/lib/Scheduler.ts @@ -1,6 +1,6 @@ import logger from "./Log2File"; import LnurlConfig from "../config/LnurlConfig"; -import { LnurlWithdraw } from "./LnurlWithdraw"; +import { Utils } from "./Utils"; class Scheduler { private _lnurlConfig: LnurlConfig; @@ -17,10 +17,7 @@ class Scheduler { this._fbStartedAt = new Date().getTime(); } - checkCallbacksTimeout( - scheduler: Scheduler, - lnurlWithdraw: LnurlWithdraw - ): void { + checkCallbacksTimeout(scheduler: Scheduler): void { logger.info("Scheduler.checkCallbacksTimeout"); scheduler._cbStartedAt = new Date().getTime(); @@ -29,13 +26,24 @@ class Scheduler { scheduler._cbStartedAt ); - lnurlWithdraw.processCallbacks(undefined); + // lnurlWithdraw.processCallbacks(undefined); + const postdata = { + id: 0, + method: "processCallbacks", + }; + + Utils.post( + scheduler._lnurlConfig.URL_API_SERVER + + ":" + + scheduler._lnurlConfig.URL_API_PORT + + scheduler._lnurlConfig.URL_API_CTX, + postdata + ).then((res) => { + logger.debug("Scheduler.checkCallbacksTimeout, res=", res); + }); } - checkFallbacksTimeout( - scheduler: Scheduler, - lnurlWithdraw: LnurlWithdraw - ): void { + checkFallbacksTimeout(scheduler: Scheduler): void { logger.info("Scheduler.checkFallbacksTimeout"); scheduler._fbStartedAt = new Date().getTime(); @@ -44,7 +52,21 @@ class Scheduler { scheduler._fbStartedAt ); - lnurlWithdraw.processFallbacks(); + // lnurlWithdraw.processFallbacks(); + const postdata = { + id: 0, + method: "processFallbacks", + }; + + Utils.post( + scheduler._lnurlConfig.URL_API_SERVER + + ":" + + scheduler._lnurlConfig.URL_API_PORT + + scheduler._lnurlConfig.URL_API_CTX, + postdata + ).then((res) => { + logger.debug("Scheduler.checkFallbacksTimeout, res=", res); + }); } } diff --git a/tests/startcallbackserver.sh b/tests/startcallbackserver.sh index 9155cd8..cdcd1fe 100755 --- a/tests/startcallbackserver.sh +++ b/tests/startcallbackserver.sh @@ -3,7 +3,7 @@ date callbackservername="lnurl_withdraw_test" -callbackserverport="1111" +callbackserverport=${1:-"1111"} docker run --rm -it --network cyphernodeappsnet --name ${callbackservername} alpine sh -c "nc -vlkp${callbackserverport} -e sh -c 'echo -en \"HTTP/1.1 200 OK\\\\r\\\\n\\\\r\\\\n\" ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo 1>&2'" From 63776673b902b4c2f449601008df3e0522d53e99 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 2 Sep 2021 16:55:58 -0400 Subject: [PATCH 21/52] Added new stuff in README --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 709f0ba..3e21ac3 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,26 @@ Response: } ``` +### processCallbacks + +Request: N/A + +Response: + +```json +{} +``` + +### processFallbacks + +Request: N/A + +Response: + +```json +{} +``` + ## LNURL-withdraw User/Wallet endpoints ### /withdrawRequest?s=[secretToken] @@ -239,18 +259,19 @@ Response: ```json { - "lnurlWithdrawId": 1, - "bolt11": "lnbcrt5019590p1psjuc7upp5vzp443fueactllywqp9cm66ewreka2gt37t6su2tcq73hj363cmsdqdv3jhxce38y6njxqyjw5qcqp2sp5cextwkrkepuacr2san20epkgfqfxjukaffd806dgz8z2txrm730s9qy9qsqzuw2a2gempuz78sxa06djguslx0xs8p54656e0m2p82yzzr40rthqkxkpzxk7jhce6lz5m6eyre4jnraz3kfpyd69280qy8k4a6hwrsqxwns9y", + "action": "lnPaid", + "lnurlWithdrawId": 45, + "bolt11": "lnbcrt5048150p1psnzvkjpp5fxf3zk4yalqeh3kzxusn7hpqx4f7ya6tundmdst6nvxem6eskw3qdqdv3jhxce58qcn2xqyjw5qcqp2sp5zmddwmhrellsj5nauwvcdyvvze9hg8k7wkhes04ha49htnfuawnq9qy9qsqh6ecxxqv568javdgenr5r4mm3ut82t683pe8yexql7rrwa8l5euq7ffh329rlgzsufj5s7x4n4pj2lcq0j9kqzn7gyt9zhg847pg6csqmuxdfh", "lnPayResponse": { "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", - "payment_hash": "60835ac53ccf70bffc8e004b8deb5970f36ea90b8f97a8714bc03d1bca3a8e37", - "created_at": 1630430175.298, + "payment_hash": "4993115aa4efc19bc6c237213f5c203553e2774be4dbb6c17a9b0d9deb30b3a2", + "created_at": 1630614227.193, "parts": 1, - "msatoshi": 501959, - "amount_msat": "501959msat", - "msatoshi_sent": 501959, - "amount_sent_msat": "501959msat", - "payment_preimage": "337ce72718d3523de121276ef65176d2620959704e442756e81069c34213671f", + "msatoshi": 504815, + "amount_msat": "504815msat", + "msatoshi_sent": 504815, + "amount_sent_msat": "504815msat", + "payment_preimage": "d4ecd85e662ce1f6c6f14d134755240efd6dcb2217a0f511ca29fd694942b532", "status": "complete" } } @@ -260,16 +281,17 @@ Response: ```json { - "lnurlWithdrawId": 6, - "btcFallbackAddress": "bcrt1q8hthhmdf9d7v2zrgpdf5ywt3crl25875em649d", + "action": "fallbackPaid", + "lnurlWithdrawId": 50, + "btcFallbackAddress": "bcrt1qgs0axulr8s2wp69n5cf805ck4xv6crelndustu", "details": { "status": "accepted", - "txid": "12a3f45dbec7ddc9f809560560e64bcca40117b1cbba2d6eb9d40b4663066016", - "hash": "9e66b684872fd628974235156e08f189a7de7eb1c084e8022775def0faf059a9", + "txid": "5683ecabaf7b4bd1d90ee23de74655495945af41f6fc783876a841598e4041f3", + "hash": "49ba4efa1ed7a19016b623686c34a2e287d7ca3c8f58593ce07ce08ff8a12f7c", "details": { - "address": "bcrt1q8hthhmdf9d7v2zrgpdf5ywt3crl25875em649d", - "amount": 5.1e-06, - "firstseen": 1630430188, + "address": "bcrt1qgs0axulr8s2wp69n5cf805ck4xv6crelndustu", + "amount": 5.33e-06, + "firstseen": 1630614256, "size": 222, "vsize": 141, "replaceable": true, @@ -280,28 +302,53 @@ Response: } ``` +- LNURL Withdraw batched using Bitcoin fallback + +```json +{ + "action": "fallbackBatched", + "lnurlWithdrawId": 51, + "btcFallbackAddress": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", + "details": { + "batchId": 16, + "batchRequestId": 36, + "etaSeconds": 7, + "cnResult": { + "batcherId": 1, + "outputId": 155, + "nbOutputs": 1, + "oldest": "2021-09-02 20:25:16", + "total": 5.26e-06 + }, + "address": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", + "amount": 5.26e-06 + } +} +``` + - LNURL Withdraw paid using a batched Bitcoin fallback ```json { - "lnurlWithdrawId": 7, - "btcFallbackAddress": "bcrt1qm2qs6a20k6cv6c8azvu75xsccea65ak65fu3z2", + "action": "fallbackPaid", + "lnurlWithdrawId": 51, + "btcFallbackAddress": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", "details": { - "batchRequestId": 25, - "batchId": 5, + "batchRequestId": 36, + "batchId": 16, "cnBatcherId": 1, "requestCountInBatch": 1, "status": "accepted", - "txid": "a1fcb30493c9bf695e7d8e226c59ec99df0a6f3739f37e8ab122010b7b8d7545", - "hash": "85e6fad9ebcc29d5895de2b9aaa0bdb35a356254a8fa8c1e312e762af3835f6e", + "txid": "a6a03ea728718bccf42f53b1b833fb405f06243d1ce3aad2e5a4522eb3ab3231", + "hash": "8aafb505abfffa6bb91b3e1c59445091c785685b9fabd6a3188e088b4c3210f0", "details": { - "firstseen": 1630430310, + "firstseen": 1630614325, "size": 222, "vsize": 141, "replaceable": true, "fee": 2.82e-05, - "address": "bcrt1qm2qs6a20k6cv6c8azvu75xsccea65ak65fu3z2", - "amount": 5.11e-06 + "address": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", + "amount": 5.26e-06 } } } From a3981df62d141bf5e6fa7cdb936eeddb3c8928f3 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 3 Sep 2021 12:44:30 -0400 Subject: [PATCH 22/52] Secured with more locks --- src/lib/HttpServer.ts | 69 +--- src/lib/LnurlWithdraw.ts | 835 ++++++++++++++++++++++----------------- 2 files changed, 472 insertions(+), 432 deletions(-) diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 0f7def7..c61c19e 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -1,7 +1,6 @@ // lib/HttpServer.ts import express from "express"; import logger from "./Log2File"; -import AsyncLock from "async-lock"; import LnurlConfig from "../config/LnurlConfig"; import fs from "fs"; import { LnurlWithdraw } from "./LnurlWithdraw"; @@ -15,12 +14,10 @@ import IRespCreateLnurlWithdraw from "../types/IRespLnurlWithdraw"; import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; import IRespLnServiceWithdrawRequest from "../types/IRespLnServiceWithdrawRequest"; -import IRespLnServiceStatus from "../types/IRespLnServiceStatus"; class HttpServer { // Create a new express application instance private readonly _httpServer: express.Application = express(); - private readonly _lock = new AsyncLock(); private _lnurlConfig: LnurlConfig = JSON.parse( fs.readFileSync("data/config.json", "utf8") ); @@ -110,19 +107,8 @@ class HttpServer { } case "deleteLnurlWithdraw": { - let result: IRespCreateLnurlWithdraw = {}; - - result = await this._lock.acquire( - "modifLnurlWithdraw", - async (): Promise => { - logger.debug( - "acquired lock modifLnurlWithdraw in deleteLnurlWithdraw" - ); - return await this.deleteLnurlWithdraw(reqMessage.params || {}); - } - ); - logger.debug( - "released lock modifLnurlWithdraw in deleteLnurlWithdraw" + const result: IRespCreateLnurlWithdraw = await this.deleteLnurlWithdraw( + reqMessage.params || {} ); response.result = result.result; @@ -138,16 +124,7 @@ class HttpServer { } case "processFallbacks": { - await this._lock.acquire( - "modifLnurlWithdraw", - async (): Promise => { - logger.debug( - "acquired lock modifLnurlWithdraw in processFallbacks" - ); - await this._lnurlWithdraw.processFallbacks(); - } - ); - logger.debug("released lock modifLnurlWithdraw in processFallbacks"); + this._lnurlWithdraw.processFallbacks(); response.result = {}; break; @@ -203,21 +180,8 @@ class HttpServer { req.query ); - let response: IRespLnServiceWithdrawRequest = {}; - - response = await this._lock.acquire( - "modifLnurlWithdraw", - async (): Promise => { - logger.debug( - "acquired lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" - ); - return await this._lnurlWithdraw.lnServiceWithdrawRequest( - req.query.s as string - ); - } - ); - logger.debug( - "released lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" + const response: IRespLnServiceWithdrawRequest = await this._lnurlWithdraw.lnServiceWithdrawRequest( + req.query.s as string ); if (response.status) { @@ -235,24 +199,11 @@ class HttpServer { async (req, res) => { logger.info(this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX + ":", req.query); - let response: IRespLnServiceStatus = {}; - - response = await this._lock.acquire( - "deleteLnurlWithdraw", - async (): Promise => { - logger.debug( - "acquired lock modifLnurlWithdraw in LN Service LNURL Withdraw" - ); - return await this._lnurlWithdraw.lnServiceWithdraw({ - k1: req.query.k1, - pr: req.query.pr, - balanceNotify: req.query.balanceNotify, - } as IReqLnurlWithdraw); - } - ); - logger.debug( - "released lock modifLnurlWithdraw in LN Service LNURL Withdraw" - ); + const response = await this._lnurlWithdraw.lnServiceWithdraw({ + k1: req.query.k1, + pr: req.query.pr, + balanceNotify: req.query.balanceNotify, + } as IReqLnurlWithdraw); if (response.status === "ERROR") { res.status(400).json(response); diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 9cc1912..e0ff6bc 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -23,6 +23,7 @@ import IRespBatchRequest from "../types/batcher/IRespBatchRequest"; import IReqSpend from "../types/cyphernode/IReqSpend"; import IRespSpend from "../types/cyphernode/IRespSpend"; import { LnurlWithdrawEntity } from "@prisma/client"; +import AsyncLock from "async-lock"; class LnurlWithdraw { private _lnurlConfig: LnurlConfig; @@ -32,6 +33,7 @@ class LnurlWithdraw { private _scheduler: Scheduler; private _intervalCallbacksTimeout?: NodeJS.Timeout; private _intervalFallbacksTimeout?: NodeJS.Timeout; + private readonly _lock = new AsyncLock(); constructor(lnurlConfig: LnurlConfig) { this._lnurlConfig = lnurlConfig; @@ -159,84 +161,93 @@ class LnurlWithdraw { async deleteLnurlWithdraw( lnurlWithdrawId: number ): Promise { - logger.info( - "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdrawId:", - lnurlWithdrawId - ); + const result: IRespLnurlWithdraw = await this._lock.acquire( + "modifLnurlWithdraw", + async (): Promise => { + logger.debug("acquired lock modifLnurlWithdraw in deleteLnurlWithdraw"); + + logger.info( + "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdrawId:", + lnurlWithdrawId + ); - const response: IRespLnurlWithdraw = {}; + const response: IRespLnurlWithdraw = {}; - if (lnurlWithdrawId) { - // Inputs are valid. - logger.debug("LnurlWithdraw.deleteLnurlWithdraw, Inputs are valid."); + if (lnurlWithdrawId) { + // Inputs are valid. + logger.debug("LnurlWithdraw.deleteLnurlWithdraw, Inputs are valid."); - let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById( - lnurlWithdrawId - ); + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById( + lnurlWithdrawId + ); - // if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { - if (lnurlWithdrawEntity == null) { - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdraw not found" - ); + // if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + if (lnurlWithdrawEntity == null) { + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdraw not found" + ); - response.error = { - code: ErrorCodes.InvalidRequest, - message: "LnurlWithdraw not found", - }; - } else if (!lnurlWithdrawEntity.deleted) { - if (!lnurlWithdrawEntity.paid) { - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!" - ); + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw not found", + }; + } else if (!lnurlWithdrawEntity.deleted) { + if (!lnurlWithdrawEntity.paid) { + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!" + ); - lnurlWithdrawEntity.deleted = true; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity.deleted = true; + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); - const lnurlDecoded = await Utils.decodeBech32( - lnurlWithdrawEntity?.lnurl || "" - ); + const lnurlDecoded = await Utils.decodeBech32( + lnurlWithdrawEntity?.lnurl || "" + ); + + response.result = Object.assign(lnurlWithdrawEntity, { + lnurlDecoded, + }); + } else { + // LnurlWithdraw already paid + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already paid." + ); - response.result = Object.assign(lnurlWithdrawEntity, { - lnurlDecoded, - }); + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw already paid", + }; + } + } else { + // LnurlWithdraw already deactivated + logger.debug( + "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already deactivated." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw already deactivated", + }; + } } else { - // LnurlWithdraw already paid + // There is an error with inputs logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already paid." + "LnurlWithdraw.deleteLnurlWithdraw, there is an error with inputs." ); response.error = { code: ErrorCodes.InvalidRequest, - message: "LnurlWithdraw already paid", + message: "Invalid arguments", }; } - } else { - // LnurlWithdraw already deactivated - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already deactivated." - ); - response.error = { - code: ErrorCodes.InvalidRequest, - message: "LnurlWithdraw already deactivated", - }; + return response; } - } else { - // There is an error with inputs - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, there is an error with inputs." - ); - - response.error = { - code: ErrorCodes.InvalidRequest, - message: "Invalid arguments", - }; - } - - return response; + ); + logger.debug("released lock modifLnurlWithdraw in deleteLnurlWithdraw"); + return result; } async getLnurlWithdraw(lnurlWithdrawId: number): Promise { @@ -296,404 +307,482 @@ class LnurlWithdraw { async lnServiceWithdrawRequest( secretToken: string ): Promise { - logger.info("LnurlWithdraw.lnServiceWithdrawRequest:", secretToken); + const result: IRespLnServiceWithdrawRequest = await this._lock.acquire( + "modifLnurlWithdraw", + async (): Promise => { + logger.debug( + "acquired lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" + ); - let result: IRespLnServiceWithdrawRequest; - const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( - secretToken - ); - logger.debug( - "LnurlWithdraw.lnServiceWithdrawRequest, lnurlWithdrawEntity:", - lnurlWithdrawEntity - ); + logger.info("LnurlWithdraw.lnServiceWithdrawRequest:", secretToken); - if (lnurlWithdrawEntity == null) { - logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, invalid k1 value:"); + let result: IRespLnServiceWithdrawRequest; + const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( + secretToken + ); + logger.debug( + "LnurlWithdraw.lnServiceWithdrawRequest, lnurlWithdrawEntity:", + lnurlWithdrawEntity + ); - result = { status: "ERROR", reason: "Invalid k1 value" }; - } else if (!lnurlWithdrawEntity.deleted) { - if (!lnurlWithdrawEntity.paid && !lnurlWithdrawEntity.batchRequestId) { - // Check expiration + if (lnurlWithdrawEntity == null) { + logger.debug( + "LnurlWithdraw.lnServiceWithdrawRequest, invalid k1 value:" + ); - if ( - lnurlWithdrawEntity.expiresAt && - lnurlWithdrawEntity.expiresAt < new Date() - ) { - // Expired LNURL - logger.debug("LnurlWithdraw.lnServiceWithdrawRequest: expired!"); + result = { status: "ERROR", reason: "Invalid k1 value" }; + } else if (!lnurlWithdrawEntity.deleted) { + if ( + !lnurlWithdrawEntity.paid && + !lnurlWithdrawEntity.batchRequestId + ) { + // Check expiration - result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; + if ( + lnurlWithdrawEntity.expiresAt && + lnurlWithdrawEntity.expiresAt < new Date() + ) { + // Expired LNURL + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest: expired!"); + + result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); + + result = { + tag: "withdrawRequest", + callback: + this._lnurlConfig.LN_SERVICE_SERVER + + (this._lnurlConfig.LN_SERVICE_PORT === 443 + ? "" + : ":" + this._lnurlConfig.LN_SERVICE_PORT) + + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, + k1: lnurlWithdrawEntity.secretToken, + defaultDescription: + lnurlWithdrawEntity.description || undefined, + minWithdrawable: lnurlWithdrawEntity.msatoshi || undefined, + maxWithdrawable: lnurlWithdrawEntity.msatoshi || undefined, + }; + } + } else { + logger.debug( + "LnurlWithdraw.lnServiceWithdrawRequest, LnurlWithdraw already paid or batched" + ); + + result = { + status: "ERROR", + reason: "LnurlWithdraw already paid or batched", + }; + } } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); + logger.debug( + "LnurlWithdraw.lnServiceWithdrawRequest, deactivated LNURL" + ); - result = { - tag: "withdrawRequest", - callback: - this._lnurlConfig.LN_SERVICE_SERVER + - (this._lnurlConfig.LN_SERVICE_PORT === 443 - ? "" - : ":" + this._lnurlConfig.LN_SERVICE_PORT) + - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, - k1: lnurlWithdrawEntity.secretToken, - defaultDescription: lnurlWithdrawEntity.description || undefined, - minWithdrawable: lnurlWithdrawEntity.msatoshi || undefined, - maxWithdrawable: lnurlWithdrawEntity.msatoshi || undefined, - }; + result = { status: "ERROR", reason: "Deactivated LNURL" }; } - } else { + logger.debug( - "LnurlWithdraw.lnServiceWithdrawRequest, LnurlWithdraw already paid or batched" + "LnurlWithdraw.lnServiceWithdrawRequest, responding:", + result ); - result = { - status: "ERROR", - reason: "LnurlWithdraw already paid or batched", - }; + return result; } - } else { - logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, deactivated LNURL"); - - result = { status: "ERROR", reason: "Deactivated LNURL" }; - } - - logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, responding:", result); - + ); + logger.debug( + "released lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" + ); return result; } async lnServiceWithdraw( params: IReqLnurlWithdraw ): Promise { - logger.info("LnurlWithdraw.lnServiceWithdraw:", params); - - let result: IRespLnServiceStatus; + const result = await this._lock.acquire( + "deleteLnurlWithdraw", + async (): Promise => { + logger.debug( + "acquired lock modifLnurlWithdraw in LN Service LNURL Withdraw" + ); - if (LnServiceWithdrawValidator.validateRequest(params)) { - // Inputs are valid. - logger.debug("LnurlWithdraw.lnServiceWithdraw, Inputs are valid."); + logger.info("LnurlWithdraw.lnServiceWithdraw:", params); - let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( - params.k1 - ); + let result: IRespLnServiceStatus; - if (lnurlWithdrawEntity == null) { - logger.debug("LnurlWithdraw.lnServiceWithdraw, invalid k1 value!"); + if (LnServiceWithdrawValidator.validateRequest(params)) { + // Inputs are valid. + logger.debug("LnurlWithdraw.lnServiceWithdraw, Inputs are valid."); - result = { status: "ERROR", reason: "Invalid k1 value" }; - } else if (!lnurlWithdrawEntity.deleted) { - if (!lnurlWithdrawEntity.paid && !lnurlWithdrawEntity.batchRequestId) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, unpaid lnurlWithdrawEntity found for this k1!" + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( + params.k1 ); - // Check expiration - if ( - lnurlWithdrawEntity.expiresAt && - lnurlWithdrawEntity.expiresAt < new Date() - ) { - // Expired LNURL - logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); + if (lnurlWithdrawEntity == null) { + logger.debug("LnurlWithdraw.lnServiceWithdraw, invalid k1 value!"); - result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; - } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); - - lnurlWithdrawEntity.bolt11 = params.pr; - const lnPayParams = { - bolt11: params.pr, - expectedMsatoshi: lnurlWithdrawEntity.msatoshi || undefined, - expectedDescription: lnurlWithdrawEntity.description || undefined, - }; - let resp: IRespLnPay = await this._cyphernodeClient.lnPay( - lnPayParams - ); - - if (resp.error) { + result = { status: "ERROR", reason: "Invalid k1 value" }; + } else if (!lnurlWithdrawEntity.deleted) { + if ( + !lnurlWithdrawEntity.paid && + !lnurlWithdrawEntity.batchRequestId + ) { logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry!" + "LnurlWithdraw.lnServiceWithdraw, unpaid lnurlWithdrawEntity found for this k1!" ); - resp = await this._cyphernodeClient.lnPay(lnPayParams); - } - - if (resp.error) { - logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay error!"); - - result = { status: "ERROR", reason: resp.error.message }; - - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); + // Check expiration + if ( + lnurlWithdrawEntity.expiresAt && + lnurlWithdrawEntity.expiresAt < new Date() + ) { + // Expired LNURL + logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); + + result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); + + lnurlWithdrawEntity.bolt11 = params.pr; + const lnPayParams = { + bolt11: params.pr, + expectedMsatoshi: lnurlWithdrawEntity.msatoshi || undefined, + expectedDescription: + lnurlWithdrawEntity.description || undefined, + }; + let resp: IRespLnPay = await this._cyphernodeClient.lnPay( + lnPayParams + ); - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + if (resp.error) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry!" + ); + + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } + + if (resp.error) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, ln_pay error!" + ); + + result = { status: "ERROR", reason: resp.error.message }; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.error + ); + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + } else { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, ln_pay success!" + ); + + result = { status: "OK" }; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.result + ); + lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + if (lnurlWithdrawEntity.webhookUrl) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." + ); + + this.processCallbacks(lnurlWithdrawEntity); + } + } + } } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay success!"); - - result = { status: "OK" }; - - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.result - ); - lnurlWithdrawEntity.withdrawnTs = new Date(); - lnurlWithdrawEntity.paid = true; - - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, already paid or batched!" ); - if (lnurlWithdrawEntity.webhookUrl) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." - ); - - this.processCallbacks(lnurlWithdrawEntity); - } + result = { + status: "ERROR", + reason: "LnurlWithdraw already paid or batched", + }; } + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw, deactivated LNURL!"); + + result = { status: "ERROR", reason: "Deactivated LNURL" }; } } else { + // There is an error with inputs logger.debug( - "LnurlWithdraw.lnServiceWithdraw, already paid or batched!" + "LnurlWithdraw.lnServiceWithdraw, there is an error with inputs." ); result = { status: "ERROR", - reason: "LnurlWithdraw already paid or batched", + reason: "Invalid arguments", }; } - } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw, deactivated LNURL!"); - result = { status: "ERROR", reason: "Deactivated LNURL" }; - } - } else { - // There is an error with inputs - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, there is an error with inputs." - ); - - result = { - status: "ERROR", - reason: "Invalid arguments", - }; - } - - logger.debug("LnurlWithdraw.lnServiceWithdraw, responding:", result); + logger.debug("LnurlWithdraw.lnServiceWithdraw, responding:", result); + return result; + } + ); + logger.debug( + "released lock modifLnurlWithdraw in LN Service LNURL Withdraw" + ); return result; } async processCallbacks( lnurlWithdrawEntity?: LnurlWithdrawEntity ): Promise { - logger.info( - "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", - lnurlWithdrawEntity - ); + await this._lock.acquire( + "processCallbacks", + async (): Promise => { + logger.debug("acquired lock processCallbacks in processCallbacks"); - let lnurlWithdrawEntitys: LnurlWithdrawEntity[] = []; - if (lnurlWithdrawEntity) { - lnurlWithdrawEntitys = [lnurlWithdrawEntity]; - } else { - lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); - } + logger.info( + "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", + lnurlWithdrawEntity + ); - let response; - let postdata = {}; - lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { - logger.debug( - "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", - lnurlWithdrawEntity - ); + let lnurlWithdrawEntitys; + if (lnurlWithdrawEntity) { + lnurlWithdrawEntitys = [lnurlWithdrawEntity]; + } else { + lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); + } - if ( - !lnurlWithdrawEntity.deleted && - lnurlWithdrawEntity.webhookUrl && - lnurlWithdrawEntity.webhookUrl.length > 0 && - lnurlWithdrawEntity.withdrawnDetails && - lnurlWithdrawEntity.withdrawnDetails.length > 0 - ) { - if ( - !lnurlWithdrawEntity.batchedCalledback && - lnurlWithdrawEntity.batchRequestId - ) { - // Payment has been batched, not yet paid - postdata = { - action: "fallbackBatched", - lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, - btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, - details: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, - }; + let response; + let postdata = {}; + lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { logger.debug( - "LnurlWithdraw.processCallbacks, batched, postdata=", - postdata + "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", + lnurlWithdrawEntity ); - response = await Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); + if ( + !lnurlWithdrawEntity.deleted && + lnurlWithdrawEntity.webhookUrl && + lnurlWithdrawEntity.webhookUrl.length > 0 && + lnurlWithdrawEntity.withdrawnDetails && + lnurlWithdrawEntity.withdrawnDetails.length > 0 + ) { + if ( + !lnurlWithdrawEntity.batchedCalledback && + lnurlWithdrawEntity.batchRequestId + ) { + // Payment has been batched, not yet paid + postdata = { + action: "fallbackBatched", + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, + details: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, + }; + logger.debug( + "LnurlWithdraw.processCallbacks, batched, postdata=", + postdata + ); - if (response.status >= 200 && response.status < 400) { - logger.debug( - "LnurlWithdraw.processCallbacks, batched, webhook called back" - ); + response = await Utils.post( + lnurlWithdrawEntity.webhookUrl, + postdata + ); - lnurlWithdrawEntity.batchedCalledback = true; - lnurlWithdrawEntity.batchedCalledbackTs = new Date(); - await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); - } - } + if (response.status >= 200 && response.status < 400) { + logger.debug( + "LnurlWithdraw.processCallbacks, batched, webhook called back" + ); - if (!lnurlWithdrawEntity.paidCalledback && lnurlWithdrawEntity.paid) { - // Payment has been sent - - if (lnurlWithdrawEntity.fallbackDone) { - // If paid through fallback... - postdata = { - action: "fallbackPaid", - lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, - btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, - details: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, - }; - } else { - // If paid through LN... - postdata = { - action: "lnPaid", - lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, - bolt11: lnurlWithdrawEntity.bolt11, - lnPayResponse: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, - }; - } + lnurlWithdrawEntity.batchedCalledback = true; + lnurlWithdrawEntity.batchedCalledbackTs = new Date(); + await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + } + } - logger.debug( - "LnurlWithdraw.processCallbacks, paid, postdata=", - postdata - ); + if ( + !lnurlWithdrawEntity.paidCalledback && + lnurlWithdrawEntity.paid + ) { + // Payment has been sent + + if (lnurlWithdrawEntity.fallbackDone) { + // If paid through fallback... + postdata = { + action: "fallbackPaid", + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, + details: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, + }; + } else { + // If paid through LN... + postdata = { + action: "lnPaid", + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + bolt11: lnurlWithdrawEntity.bolt11, + lnPayResponse: lnurlWithdrawEntity.withdrawnDetails + ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) + : null, + }; + } + + logger.debug( + "LnurlWithdraw.processCallbacks, paid, postdata=", + postdata + ); - response = await Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); + response = await Utils.post( + lnurlWithdrawEntity.webhookUrl, + postdata + ); - if (response.status >= 200 && response.status < 400) { - logger.debug( - "LnurlWithdraw.processCallbacks, paid, webhook called back" - ); + if (response.status >= 200 && response.status < 400) { + logger.debug( + "LnurlWithdraw.processCallbacks, paid, webhook called back" + ); - lnurlWithdrawEntity.paidCalledback = true; - lnurlWithdrawEntity.paidCalledbackTs = new Date(); - await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + lnurlWithdrawEntity.paidCalledback = true; + lnurlWithdrawEntity.paidCalledbackTs = new Date(); + await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + } + } } - } + }); } - }); + ); + logger.debug("released lock processCallbacks in processCallbacks"); } async processFallbacks(): Promise { - logger.info("LnurlWithdraw.processFallbacks"); + await this._lock.acquire( + "processFallbacks", + async (): Promise => { + logger.debug("acquired lock processFallbacks in processFallbacks"); - const lnurlWithdrawEntitys = await this._lnurlDB.getFallbackLnurlWithdraws(); - logger.debug( - "LnurlWithdraw.processFallbacks, lnurlWithdrawEntitys=", - lnurlWithdrawEntitys - ); + logger.info("LnurlWithdraw.processFallbacks"); - lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { - logger.debug( - "LnurlWithdraw.processFallbacks, lnurlWithdrawEntity=", - lnurlWithdrawEntity - ); + const lnurlWithdrawEntitys = await this._lnurlDB.getFallbackLnurlWithdraws(); + logger.debug( + "LnurlWithdraw.processFallbacks, lnurlWithdrawEntitys=", + lnurlWithdrawEntitys + ); - if (lnurlWithdrawEntity.batchFallback) { - logger.debug("LnurlWithdraw.processFallbacks, batched fallback"); + lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { + logger.debug( + "LnurlWithdraw.processFallbacks, lnurlWithdrawEntity=", + lnurlWithdrawEntity + ); - if (lnurlWithdrawEntity.batchRequestId) { - logger.debug("LnurlWithdraw.processFallbacks, already batched!"); - } else { - const batchRequestTO: IReqBatchRequest = { - externalId: lnurlWithdrawEntity.externalId || undefined, - description: lnurlWithdrawEntity.description || undefined, - address: lnurlWithdrawEntity.btcFallbackAddress || "", - amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, - webhookUrl: - this._lnurlConfig.URL_API_SERVER + - ":" + - this._lnurlConfig.URL_API_PORT + - this._lnurlConfig.URL_CTX_WEBHOOKS, - }; + if (lnurlWithdrawEntity.batchFallback) { + logger.debug("LnurlWithdraw.processFallbacks, batched fallback"); - const resp: IRespBatchRequest = await this._batcherClient.queueForNextBatch( - batchRequestTO - ); + if (lnurlWithdrawEntity.batchRequestId) { + logger.debug("LnurlWithdraw.processFallbacks, already batched!"); + } else { + const batchRequestTO: IReqBatchRequest = { + externalId: lnurlWithdrawEntity.externalId || undefined, + description: lnurlWithdrawEntity.description || undefined, + address: lnurlWithdrawEntity.btcFallbackAddress || "", + amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, + webhookUrl: + this._lnurlConfig.URL_API_SERVER + + ":" + + this._lnurlConfig.URL_API_PORT + + this._lnurlConfig.URL_CTX_WEBHOOKS, + }; + + const resp: IRespBatchRequest = await this._batcherClient.queueForNextBatch( + batchRequestTO + ); - if (resp.error) { - logger.debug( - "LnurlWithdraw.processFallbacks, queueForNextBatch error!" - ); + if (resp.error) { + logger.debug( + "LnurlWithdraw.processFallbacks, queueForNextBatch error!" + ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); - } else { - logger.debug( - "LnurlWithdraw.processFallbacks, queueForNextBatch success!" - ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.error + ); + } else { + logger.debug( + "LnurlWithdraw.processFallbacks, queueForNextBatch success!" + ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.result); - lnurlWithdrawEntity.batchRequestId = - resp.result?.batchRequestId || null; - } + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.result + ); + lnurlWithdrawEntity.batchRequestId = + resp.result?.batchRequestId || null; + } - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); - if (lnurlWithdrawEntity.batchRequestId) { - this.processCallbacks(lnurlWithdrawEntity); - } - } - } else { - logger.debug("LnurlWithdraw.processFallbacks, not batched fallback"); + if (lnurlWithdrawEntity.batchRequestId) { + this.processCallbacks(lnurlWithdrawEntity); + } + } + } else { + logger.debug( + "LnurlWithdraw.processFallbacks, not batched fallback" + ); - const spendRequestTO: IReqSpend = { - address: lnurlWithdrawEntity.btcFallbackAddress || "", - amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, - }; + const spendRequestTO: IReqSpend = { + address: lnurlWithdrawEntity.btcFallbackAddress || "", + amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, + }; - const spendResp: IRespSpend = await this._cyphernodeClient.spend( - spendRequestTO - ); + const spendResp: IRespSpend = await this._cyphernodeClient.spend( + spendRequestTO + ); - if (spendResp?.error) { - // There was an error on Cyphernode end, return that. - logger.debug( - "LnurlWithdraw.processFallbacks: There was an error on Cyphernode spend." - ); + if (spendResp?.error) { + // There was an error on Cyphernode end, return that. + logger.debug( + "LnurlWithdraw.processFallbacks: There was an error on Cyphernode spend." + ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - spendResp.error - ); - } else if (spendResp?.result) { - logger.debug( - "LnurlWithdraw.processFallbacks: Cyphernode spent: ", - spendResp.result - ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - spendResp.result - ); - lnurlWithdrawEntity.withdrawnTs = new Date(); - lnurlWithdrawEntity.paid = true; - lnurlWithdrawEntity.fallbackDone = true; - } + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + spendResp.error + ); + } else if (spendResp?.result) { + logger.debug( + "LnurlWithdraw.processFallbacks: Cyphernode spent: ", + spendResp.result + ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + spendResp.result + ); + lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + lnurlWithdrawEntity.fallbackDone = true; + } - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); - if (lnurlWithdrawEntity.fallbackDone) { - this.processCallbacks(lnurlWithdrawEntity); - } + if (lnurlWithdrawEntity.fallbackDone) { + this.processCallbacks(lnurlWithdrawEntity); + } + } + }); } - }); + ); + logger.debug("released lock processFallbacks in processFallbacks"); } // eslint-disable-next-line @typescript-eslint/no-explicit-any From 236ff44f65ab5fd78586f81a96f2570102c30dbe Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 7 Sep 2021 00:00:35 -0400 Subject: [PATCH 23/52] pay retry twice and validate previous bolt11 --- src/lib/CyphernodeClient.ts | 2 +- src/lib/LnurlWithdraw.ts | 110 ++++++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 3b28709..e64f020 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -68,7 +68,7 @@ class CyphernodeClient { url: url, method: "post", baseURL: this.baseURL, - timeout: 30000, + timeout: 60000, headers: { Authorization: "Bearer " + this._generateToken(), }, diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index e0ff6bc..9a2710d 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -444,63 +444,89 @@ class LnurlWithdraw { } else { logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); - lnurlWithdrawEntity.bolt11 = params.pr; - const lnPayParams = { - bolt11: params.pr, - expectedMsatoshi: lnurlWithdrawEntity.msatoshi || undefined, - expectedDescription: - lnurlWithdrawEntity.description || undefined, - }; - let resp: IRespLnPay = await this._cyphernodeClient.lnPay( - lnPayParams - ); - - if (resp.error) { + if ( + !lnurlWithdrawEntity.bolt11 || + lnurlWithdrawEntity.bolt11 === params.pr + ) { logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry!" + "LnurlWithdraw.lnServiceWithdraw: new bolt11 or same as previous!" ); - resp = await this._cyphernodeClient.lnPay(lnPayParams); - } - - if (resp.error) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay error!" + lnurlWithdrawEntity.bolt11 = params.pr; + const lnPayParams = { + bolt11: params.pr, + expectedMsatoshi: lnurlWithdrawEntity.msatoshi || undefined, + expectedDescription: + lnurlWithdrawEntity.description || undefined, + }; + let resp: IRespLnPay = await this._cyphernodeClient.lnPay( + lnPayParams ); - result = { status: "ERROR", reason: resp.error.message }; + if (resp.error) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry #1!" + ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.error - ); + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); - } else { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay success!" - ); + if (resp.error) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry #2!" + ); + + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } - result = { status: "OK" }; + if (resp.error) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, ln_pay error!" + ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.result - ); - lnurlWithdrawEntity.withdrawnTs = new Date(); - lnurlWithdrawEntity.paid = true; + result = { status: "ERROR", reason: resp.error.message }; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.error + ); - if (lnurlWithdrawEntity.webhookUrl) { + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + } else { logger.debug( - "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." + "LnurlWithdraw.lnServiceWithdraw, ln_pay success!" ); - this.processCallbacks(lnurlWithdrawEntity); + result = { status: "OK" }; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.result + ); + lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + if (lnurlWithdrawEntity.webhookUrl) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." + ); + + this.processCallbacks(lnurlWithdrawEntity); + } } + } else { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, trying to redeem twice with different bolt11!" + ); + + result = { + status: "ERROR", + reason: "Trying to redeem twice with different bolt11", + }; } } } else { From 1bb86830039f1cb070499fafd2477d58124d8cf8 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 7 Sep 2021 15:30:49 -0400 Subject: [PATCH 24/52] Added forceFallback --- README.md | 49 ++++++++++++++++ src/lib/HttpServer.ts | 21 +++++++ src/lib/LnurlWithdraw.ts | 92 +++++++++++++++++++++++++++++ tests/lnurl_withdraw.sh | 122 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 277 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3e21ac3..540758d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ LNURL cypherapp for cyphernode 2. If Service deleted the LNURL, withdraw will fail 3. If there's a fallback Bitcoin address on the LNURL, when expired, LNURL app will send amount on-chain 4. If batching is activated on fallback, the fallback will be sent to the Batcher +5. Because LN payments can be "stuck" and may eventually be successful, we reject subsequent withdraw requests that is using different bolt11 than the first request. ## LNURL-withdraw API endpoints @@ -202,6 +203,54 @@ Response: } ``` +### forceFallback + +This will rewing the LNURL expiration in the past to make it elligible to fallback on next check. + +Request: + +```TypeScript +{ + lnurlWithdrawId: number; +} +``` + +Response: + +```TypeScript +{ + result?: { + lnurlWithdrawId: number; + externalId: string | null; + msatoshi: number; + description: string | null; + expiration: Date | null; + secretToken: string; + webhookUrl: string | null; + calledback: boolean; + calledbackTs: Date | null; + lnurl: string; + bolt11: string | null; + btcFallbackAddress: string | null; + batchFallback: boolean; + batchRequestId: number | null; + fallbackDone: boolean; + withdrawnDetails: string | null; + withdrawnTs: Date | null; + paid: boolean; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + ### processCallbacks Request: N/A diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index c61c19e..8f2b370 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -59,6 +59,17 @@ class HttpServer { return await this._lnurlWithdraw.deleteLnurlWithdraw(lnurlWithdrawId); } + async forceFallback( + params: object | undefined + ): Promise { + logger.debug("/forceFallback params:", params); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lnurlWithdrawId = parseInt((params as any).lnurlWithdrawId); + + return await this._lnurlWithdraw.forceFallback(lnurlWithdrawId); + } + async getLnurlWithdraw( params: object | undefined ): Promise { @@ -130,6 +141,16 @@ class HttpServer { break; } + case "forceFallback": { + const result: IRespCreateLnurlWithdraw = await this.forceFallback( + reqMessage.params || {} + ); + + response.result = result.result; + response.error = result.error; + break; + } + case "encodeBech32": { response.result = await Utils.encodeBech32( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 9a2710d..5535c07 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -811,6 +811,98 @@ class LnurlWithdraw { logger.debug("released lock processFallbacks in processFallbacks"); } + async forceFallback(lnurlWithdrawId: number): Promise { + const result: IRespLnurlWithdraw = await this._lock.acquire( + "processFallbacks", + async (): Promise => { + logger.debug("acquired lock processFallbacks in forceFallback"); + + logger.info( + "LnurlWithdraw.forceFallback, lnurlWithdrawId:", + lnurlWithdrawId + ); + + const response: IRespLnurlWithdraw = {}; + + if (lnurlWithdrawId) { + // Inputs are valid. + logger.debug("LnurlWithdraw.forceFallback, Inputs are valid."); + + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById( + lnurlWithdrawId + ); + + // if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { + if (lnurlWithdrawEntity == null) { + logger.debug( + "LnurlWithdraw.forceFallback, lnurlWithdraw not found" + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw not found", + }; + } else if (!lnurlWithdrawEntity.deleted) { + if (!lnurlWithdrawEntity.paid) { + logger.debug( + "LnurlWithdraw.forceFallback, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!" + ); + + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + lnurlWithdrawEntity.expiresAt = yesterday; + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + const lnurlDecoded = await Utils.decodeBech32( + lnurlWithdrawEntity?.lnurl || "" + ); + + response.result = Object.assign(lnurlWithdrawEntity, { + lnurlDecoded, + }); + } else { + // LnurlWithdraw already paid + logger.debug( + "LnurlWithdraw.forceFallback, LnurlWithdraw already paid." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw already paid", + }; + } + } else { + // LnurlWithdraw already deactivated + logger.debug( + "LnurlWithdraw.forceFallback, LnurlWithdraw already deactivated." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlWithdraw already deactivated", + }; + } + } else { + // There is an error with inputs + logger.debug( + "LnurlWithdraw.forceFallback, there is an error with inputs." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + ); + logger.debug("released lock processFallbacks in forceFallback"); + return result; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any async processBatchWebhook(webhookBody: any): Promise { logger.info("LnurlWithdraw.processBatchWebhook,", webhookBody); diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index 9c6bbad..e457aa4 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -138,6 +138,28 @@ delete_lnurl_withdraw() { echo "${deleteLnurlWithdraw}" } +force_lnurl_fallback() { + trace 2 "\n\n[force_lnurl_fallback] ${BCyan}Force LNURL Fallback...${Color_Off}\n" + + local lnurl_withdraw_id=${1} + trace 3 "[force_lnurl_fallback] lnurl_withdraw_id=${lnurl_withdraw_id}" + + # Service forces LNURL Fallback + data='{"id":0,"method":"forceFallback","params":{"lnurlWithdrawId":'${lnurl_withdraw_id}'}}' + trace 3 "[force_lnurl_fallback] data=${data}" + trace 3 "[force_lnurl_fallback] Calling forceFallback..." + local forceFallback=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[force_lnurl_fallback] forceFallback=${forceFallback}" + + local expiresAt=$(echo "${forceFallback}" | jq ".result.expiresAt") + if [ "${expiresAt}" -ge "$(date -u +"%s")" ]; then + trace 2 "[force_lnurl_fallback] ${On_Red}${BBlack} NOT EXPIRED! ${Color_Off}" + return 1 + fi + + echo "${forceFallback}" +} + decode_lnurl() { trace 2 "\n\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}\n" @@ -721,6 +743,91 @@ fallback2() { trace 1 "\n\n[fallback2] ${On_IGreen}${BBlack} Fallback 2: SUCCESS! ${Color_Off}\n" } +fallback3() { + # fallback 3, force fallback + # + # 1. Cyphernode.getnewaddress -> btcfallbackaddr + # 2. Cyphernode.watch btcfallbackaddr + # 3. Listen to watch webhook + # 4. Create a LNURL Withdraw with expiration=tomorrow and a btcfallbackaddr + # 5. Get it and compare + # 6. User calls LNServiceWithdrawRequest -> works, not expired! + # 7. Call forceFallback + # 7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) + # 8. Mined block and Cyphernode's confirmed watch callback called (port 1113) + + trace 1 "\n\n[fallback3] ${On_Yellow}${BBlack} Fallback 3: ${Color_Off}\n" + + local callbackserver=${1} + local callbackport=${2} + local lnServicePrefix=${3} + + local zeroconfport=$((${callbackserverport}+1)) + local oneconfport=$((${callbackserverport}+2)) + local callbackurlCnWatch0conf="http://${callbackservername}:${zeroconfport}" + local callbackurlCnWatch1conf="http://${callbackservername}:${oneconfport}" + local callbackurl="http://${callbackservername}:${callbackserverport}" + + # Get new address + local data='{"label":"lnurl_fallback_test"}' + local btcfallbackaddr=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) + btcfallbackaddr=$(echo "${btcfallbackaddr}" | jq -r ".address") + trace 3 "[fallback3] btcfallbackaddr=${btcfallbackaddr}" + + # Watch the address + data='{"address":"'${btcfallbackaddr}'","unconfirmedCallbackURL":"'${callbackurlCnWatch0conf}'/callback0conf","confirmedCallbackURL":"'${callbackurlCnWatch1conf}'/callback1conf"}' + local watchresponse=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) + trace 3 "[fallback3] watchresponse=${watchresponse}" + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 86400 "" "${btcfallbackaddr}") + trace 3 "[fallback3] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + trace 3 "[fallback3] lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 3 "[fallback3] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 3 "[fallback3] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[fallback3] EQUALS!" + else + trace 1 "\n\n[fallback3] ${On_Red}${BBlack} Fallback 3: NOT EQUALS! ${Color_Off}\n" + return 1 + fi + + # Decode LNURL + local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") + trace 3 "[fallback3] urlSuffix=${urlSuffix}" + + start_callback_server + start_callback_server ${zeroconfport} + start_callback_server ${oneconfport} + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + trace 3 "[fallback3] withdrawRequestResponse=${withdrawRequestResponse}" + + echo "${withdrawRequestResponse}" | grep -qi "expired" + if [ "$?" -eq "0" ]; then + trace 1 "\n\n[fallback3] ${On_Red}${BBlack} Fallback 3: EXPIRED! ${Color_Off}\n" + return 1 + else + trace 2 "[fallback3] NOT EXPIRED, good!" + fi + + trace 3 "[fallback3] Forcing fallback..." + local force_lnurl_fallback=$(force_lnurl_fallback ${lnurl_withdraw_id}) + trace 3 "[fallback3] force_lnurl_fallback=${force_lnurl_fallback}" + + trace 3 "[fallback3] Waiting for fallback execution and a block mined..." + + wait + + trace 1 "\n\n[fallback3] ${On_IGreen}${BBlack} Fallback 3: SUCCESS! ${Color_Off}\n" +} + start_callback_server() { trace 1 "\n\n[start_callback_server] ${BCyan}Let's start a callback server!...${Color_Off}\n" @@ -754,12 +861,13 @@ trace 3 "lnurlConfig=${lnurlConfig}" lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER)"') trace 3 "lnServicePrefix=${lnServicePrefix}" -happy_path "${callbackurl}" "${lnServicePrefix}" \ -&& expired1 "${callbackurl}" "${lnServicePrefix}" \ -&& expired2 "${callbackurl}" "${lnServicePrefix}" \ -&& deleted1 "${callbackurl}" "${lnServicePrefix}" \ -&& deleted2 "${callbackurl}" "${lnServicePrefix}" \ -&& fallback1 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" \ -&& fallback2 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" +# happy_path "${callbackurl}" "${lnServicePrefix}" \ +# && expired1 "${callbackurl}" "${lnServicePrefix}" \ +# && expired2 "${callbackurl}" "${lnServicePrefix}" \ +# && deleted1 "${callbackurl}" "${lnServicePrefix}" \ +# && deleted2 "${callbackurl}" "${lnServicePrefix}" \ +# && fallback1 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" \ +# && fallback2 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" \ +fallback3 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" trace 1 "\n\n${BCyan}Finished, deleting this test container...${Color_Off}\n" From f2747691fbe778a2756c02fc5e5f6749c23d6431 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 10 Sep 2021 02:06:35 -0400 Subject: [PATCH 25/52] Made LN pay more robust --- src/lib/CyphernodeClient.ts | 220 +++++++++++++++ src/lib/LnurlWithdraw.ts | 323 ++++++++++++++++++----- src/types/cyphernode/IBatchTx.ts | 2 +- src/types/cyphernode/ILnListPays.ts | 3 + src/types/cyphernode/ILnPayStatus.ts | 3 + src/types/cyphernode/IReqLnListPays.ts | 5 + src/types/cyphernode/IRespLnListPays.ts | 7 + src/types/cyphernode/IRespLnPayStatus.ts | 7 + tests/lnurl_withdraw.sh | 131 ++++----- tests/lnurl_withdraw_wallet.sh | 113 ++++++-- tests/run_lnurl_withdraw_wallet.sh | 2 +- 11 files changed, 647 insertions(+), 169 deletions(-) create mode 100644 src/types/cyphernode/ILnListPays.ts create mode 100644 src/types/cyphernode/ILnPayStatus.ts create mode 100644 src/types/cyphernode/IReqLnListPays.ts create mode 100644 src/types/cyphernode/IRespLnListPays.ts create mode 100644 src/types/cyphernode/IRespLnPayStatus.ts diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index e64f020..0c1b5ee 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -16,6 +16,9 @@ import IReqSpend from "../types/cyphernode/IReqSpend"; import IRespSpend from "../types/cyphernode/IRespSpend"; import IReqLnPay from "../types/cyphernode/IReqLnPay"; import IRespLnPay from "../types/cyphernode/IRespLnPay"; +import IRespLnListPays from "../types/cyphernode/IRespLnListPays"; +import IReqLnListPays from "../types/cyphernode/IReqLnListPays"; +import IRespLnPayStatus from "../types/cyphernode/IRespLnPayStatus"; class CyphernodeClient { private baseURL: string; @@ -522,6 +525,223 @@ class CyphernodeClient { } return result; } + + async lnListPays(lnListPaysTO: IReqLnListPays): Promise { + // POST http://192.168.111.152:8080/ln_listpays + // BODY {"bolt11":"lntb1pdca82tpp5g[...]9wafq9n4w28amnmwzujgqpmapcr3"} + // + // args: + // - bolt11, optional, lightning network bolt11 invoice + // + // Example of error result: + // + // { + // "code": -32602, + // "message": "Invalid invstring: invalid bech32 string" + // } + // + // + // Example of successful result when a bolt11 is supplied: + // + // { + // "pays": [ + // { + // "bolt11": "lnbcrt10m1psv5fu0pp5x239mf9m5p6grz4muzv202pdye3atrzp9nzm7nqjt5vfz4mp2vqqdqdv3ekxvfnxy6njxqzuycqp2sp5f9rhgvpy7j5l3ka2yhxazd2kf8mx4h7sjcwncfy3s7vrq7wt2v6q9qy9qsqngws5fwc56uagscw8pqwupt7hqkrj7nl60x9yv3c4gp3xl8tpd6hzp8f4rtk3k7r2c30sgwjyedtq5xxqvqnljt3ymz5thlrw367ccsparekqw", + // "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + // "payment_hash": "32a25da4bba074818abbe098a7a82d2663d58c412cc5bf4c125d189157615300", + // "status": "complete", + // "created_at": 1623861137, + // "preimage": "6a2b15478bd661cac9ed03b808b3f21e27ed0a2abe392e953dfc3f801a9d1829", + // "amount_msat": "1000000000msat", + // "amount_sent_msat": "1000000000msat" + // } + // ] + // } + // + // + // Example of successful result when a non-existing bolt11 is supplied or none supplied but empty list: + // + // { + // "pays": [ + // ] + // } + // + // + // Example of successful result when no bolt11 is supplied (lists all): + // + // { + // "pays": [ + // { + // "bolt11": "lnbcrt174410p1ps07kf8pp500w6fzdfgzse5wf59l36ktqhtqzmzla7ypa2uagx796nlzlzm8jqdqdv3jhxcehxs6rzxqyjw5qcqp2sp5rkknr49qf3empm9shcvayvtcwuv2pkfz04yf38rxpnacnvx382ms9qy9qsqlfqvztg2hlrzu0vad5gwhmh8rnd6t2ph27shq7gm36y24mce9k6n45cq4kmexkandrg5463luw4rduj3uu4cy9qxrnmukn8g29azhaqprngzww", + // "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + // "payment_hash": "7bdda489a940a19a39342fe3ab2c175805b17fbe207aae7506f1753f8be2d9e4", + // "status": "complete", + // "created_at": 1627347240, + // "preimage": "8ba5bfd92f363656633232b94c25551b236b0a4cf343503bf705a2d4951c8ac8", + // "amount_msat": "17441msat", + // "amount_sent_msat": "17441msat" + // }, + // ... + // { + // "bolt11": "lnbcrt10m1psv5fu0pp5x239mf9m5p6grz4muzv202pdye3atrzp9nzm7nqjt5vfz4mp2vqqdqdv3ekxvfnxy6njxqzuycqp2sp5f9rhgvpy7j5l3ka2yhxazd2kf8mx4h7sjcwncfy3s7vrq7wt2v6q9qy9qsqngws5fwc56uagscw8pqwupt7hqkrj7nl60x9yv3c4gp3xl8tpd6hzp8f4rtk3k7r2c30sgwjyedtq5xxqvqnljt3ymz5thlrw367ccsparekqw", + // "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + // "payment_hash": "32a25da4bba074818abbe098a7a82d2663d58c412cc5bf4c125d189157615300", + // "status": "complete", + // "created_at": 1623861137, + // "preimage": "6a2b15478bd661cac9ed03b808b3f21e27ed0a2abe392e953dfc3f801a9d1829", + // "amount_msat": "1000000000msat", + // "amount_sent_msat": "1000000000msat" + // } + // ] + // } + // + + logger.info("CyphernodeClient.lnListPays:", lnListPaysTO); + + let result: IRespLnListPays; + const response = await this._post("/ln_listpays", lnListPaysTO); + if (response.status >= 200 && response.status < 400) { + result = { result: response.data }; + } else { + result = { + error: { + code: ErrorCodes.InternalError, + message: response.data.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespLnListPays; + } + return result; + } + + async lnPayStatus(lnPayStatusTO: IReqLnListPays): Promise { + // POST http://192.168.111.152:8080/ln_listpays + // BODY {"bolt11":"lntb1pdca82tpp5g[...]9wafq9n4w28amnmwzujgqpmapcr3"} + // + // args: + // - bolt11, optional, lightning network bolt11 invoice + // + // Example of error result: + // + // { + // "pay": [] + // } + // + // + // Example of successful result when a bolt11 is supplied: + // + // { + // "pay": [ + // { + // "bolt11": "lnbcrt123450p1psn5ud2pp5q4rfk3qgxcejp30uqrakauva03e28xrjxy48cpha8ngceyk62pxqdq9vscrjxqyjw5qcqp2sp5cen5rxu72vcktz7mu3uaq84gqulcc0a5yvekmdfady8v5dr5xkjq9qyyssqs7ff458qr6atp2k8lj0t5l8n722mtv3qnetrclzep33mdp48smgrl4phqz89wq07wmhlug5ezztr2yxh8uzwpda5pzfsdtvz3d5qdvgpt56wqt", + // "amount_msat": "12345msat", + // "amount_msat": "12345msat", + // "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + // "attempts": [ + // { + // "strategy": "Initial attempt", + // "start_time": "2021-09-09T20:42:54.779Z", + // "age_in_seconds": 652, + // "end_time": "2021-09-09T20:42:54.826Z", + // "state": "completed", + // "failure": { + // "code": 205, + // "message": "Call to getroute: Could not find a route" + // } + // } + // ] + // }, + // { + // "bolt11": "lnbcrt123450p1psn5ud2pp5q4rfk3qgxcejp30uqrakauva03e28xrjxy48cpha8ngceyk62pxqdq9vscrjxqyjw5qcqp2sp5cen5rxu72vcktz7mu3uaq84gqulcc0a5yvekmdfady8v5dr5xkjq9qyyssqs7ff458qr6atp2k8lj0t5l8n722mtv3qnetrclzep33mdp48smgrl4phqz89wq07wmhlug5ezztr2yxh8uzwpda5pzfsdtvz3d5qdvgpt56wqt", + // "amount_msat": "12345msat", + // "amount_msat": "12345msat", + // "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + // "attempts": [ + // { + // "strategy": "Initial attempt", + // "start_time": "2021-09-09T20:53:37.384Z", + // "age_in_seconds": 9, + // "end_time": "2021-09-09T20:53:37.719Z", + // "state": "completed", + // "success": { + // "id": 225, + // "payment_preimage": "0fe69b61ee05fa966b612a9398057730f69459a8a7cf5a1f518203be92ce962e" + // } + // } + // ] + // } + // ] + // } + // + // + // + // Example of successful result when no bolt11 is supplied (lists all): + // + // { + // "pay": [ + // { + // "bolt11": "lnbcrt5190610p1psn5urqpp5l5mxhx8u0wfck2ha96ke0hvqn22rlp34zkqfllysmj0m3unctsgqdq0v3jhxce38ycrvvgxqyjw5qcqp2sp5rcqxz7qpnckvjkpfacc5er8vl4s2es6ved4zqsge4zaxgkww72ls9qyyssq7yr4ulda5xl3t5hc3u7rykq64nfh7v66zehtl37cn7ehepzlg2fqwy2px5msqtpf96caqrhmtcclk6j4qu6pe5r0rayvqmw3wxcl7pgpk20fsl", + // "msatoshi": 519061, + // "amount_msat": "519061msat", + // "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + // "local_exclusions": "Excluded channel 294x1x0/1 (3881837942msat, disconnected). ", + // "attempts": [ + // { + // "strategy": "Initial attempt", + // "start_time": "2021-09-09T20:36:48.925Z", + // "age_in_seconds": 2968, + // "end_time": "2021-09-09T20:36:48.927Z", + // "duration_in_seconds": 0, + // "excluded_nodes_or_channels": [ + // "294x1x0/1" + // ], + // "failure": { + // "code": 205, + // "message": "Call to getroute: Could not find a route" + // } + // } + // ] + // }, + // ... + // { + // "bolt11": "lnbcrt123450p1psn5ud2pp5q4rfk3qgxcejp30uqrakauva03e28xrjxy48cpha8ngceyk62pxqdq9vscrjxqyjw5qcqp2sp5cen5rxu72vcktz7mu3uaq84gqulcc0a5yvekmdfady8v5dr5xkjq9qyyssqs7ff458qr6atp2k8lj0t5l8n722mtv3qnetrclzep33mdp48smgrl4phqz89wq07wmhlug5ezztr2yxh8uzwpda5pzfsdtvz3d5qdvgpt56wqt", + // "amount_msat": "12345msat", + // "amount_msat": "12345msat", + // "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + // "attempts": [ + // { + // "strategy": "Initial attempt", + // "start_time": "2021-09-09T20:53:37.384Z", + // "age_in_seconds": 1960, + // "end_time": "2021-09-09T20:53:37.719Z", + // "state": "completed", + // "success": { + // "id": 225, + // "payment_preimage": "0fe69b61ee05fa966b612a9398057730f69459a8a7cf5a1f518203be92ce962e" + // } + // } + // ] + // } + // ] + // } + + logger.info("CyphernodeClient.lnPayStatus:", lnPayStatusTO); + + let result: IRespLnPayStatus; + const response = await this._post("/ln_paystatus", lnPayStatusTO); + if (response.status >= 200 && response.status < 400) { + result = { result: response.data }; + } else { + result = { + error: { + code: ErrorCodes.InternalError, + message: response.data.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespLnPayStatus; + } + return result; + } } export { CyphernodeClient }; diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 5535c07..e29b07c 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -24,6 +24,7 @@ import IReqSpend from "../types/cyphernode/IReqSpend"; import IRespSpend from "../types/cyphernode/IRespSpend"; import { LnurlWithdrawEntity } from "@prisma/client"; import AsyncLock from "async-lock"; +import IReqLnListPays from "../types/cyphernode/IReqLnListPays"; class LnurlWithdraw { private _lnurlConfig: LnurlConfig; @@ -397,6 +398,139 @@ class LnurlWithdraw { return result; } + async processLnPayment( + lnurlWithdrawEntity: LnurlWithdrawEntity, + bolt11: string + ): Promise> { + logger.debug( + "LnurlWithdraw.processLnPayment: lnurlWithdrawEntity:", + lnurlWithdrawEntity + ); + logger.debug("LnurlWithdraw.processLnPayment: bolt11:", bolt11); + + let result; + + lnurlWithdrawEntity.bolt11 = bolt11; + const lnPayParams = { + bolt11: bolt11, + expectedMsatoshi: lnurlWithdrawEntity.msatoshi || undefined, + expectedDescription: lnurlWithdrawEntity.description || undefined, + }; + let resp: IRespLnPay = await this._cyphernodeClient.lnPay(lnPayParams); + + if (resp.error) { + logger.debug( + "LnurlWithdraw.processLnPayment, ln_pay error, let's retry #1!" + ); + + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } + + if (resp.error) { + logger.debug("LnurlWithdraw.processLnPayment, ln_pay error!"); + + result = { status: "ERROR", reason: resp.error.message }; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + } else { + logger.debug("LnurlWithdraw.processLnPayment, ln_pay success!"); + + result = { status: "OK" }; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.result); + lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + this.checkWebhook(lnurlWithdrawEntity); + } + + return result; + } + + async checkWebhook(lnurlWithdrawEntity: LnurlWithdrawEntity): Promise { + if (lnurlWithdrawEntity.webhookUrl) { + logger.debug( + "LnurlWithdraw.checkWebhook, about to call back the webhookUrl..." + ); + + this.processCallbacks(lnurlWithdrawEntity); + } else { + logger.debug("LnurlWithdraw.checkWebhook, skipping, no webhookUrl..."); + } + } + + async processLnStatus( + paymentStatus: string, + lnurlWithdrawEntity: LnurlWithdrawEntity, + bolt11: string, + statusResult: unknown + ): Promise { + let result: IRespLnServiceStatus; + + if (paymentStatus === "pending") { + logger.debug("LnurlWithdraw.lnServiceWithdraw, payment pending..."); + + result = { + status: "ERROR", + reason: "LnurlWithdraw payment pending", + }; + } else if (paymentStatus === "complete") { + logger.debug("LnurlWithdraw.lnServiceWithdraw, payment complete..."); + result = { + status: "ERROR", + reason: "LnurlWithdraw payment already done", + }; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(statusResult); + // lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + this.checkWebhook(lnurlWithdrawEntity); + } else if (paymentStatus === "failed") { + logger.debug("LnurlWithdraw.lnServiceWithdraw, payment failed..."); + + if ( + lnurlWithdrawEntity.expiresAt && + lnurlWithdrawEntity.expiresAt < new Date() + ) { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, previous pay failed, now expired..." + ); + result = { + status: "ERROR", + reason: "Expired LNURL-Withdraw", + }; + } else { + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, previous payment failed but not expired, retry..." + ); + + result = await this.processLnPayment(lnurlWithdrawEntity, bolt11); + } + } else { + // Error, invalid paymentStatus + logger.debug("LnurlWithdraw.lnServiceWithdraw, invalid paymentStatus..."); + result = { + status: "ERROR", + reason: "Something unexpected happened", + }; + } + + return result; + } + async lnServiceWithdraw( params: IReqLnurlWithdraw ): Promise { @@ -415,10 +549,19 @@ class LnurlWithdraw { // Inputs are valid. logger.debug("LnurlWithdraw.lnServiceWithdraw, Inputs are valid."); - let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( + const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( params.k1 ); + // If a payment request has already been made, we need to check that payment + // status first. + // If status is failed, we can accept retrying with supplied bolt11 even if + // it is different from the previous one. + // If status is complete, we need to update our payment status and tell the + // user it's already been paid. + // If status is pending, we need to tell the user the payment is pending and + // not allow a payment to a different bolt11. + if (lnurlWithdrawEntity == null) { logger.debug("LnurlWithdraw.lnServiceWithdraw, invalid k1 value!"); @@ -432,101 +575,138 @@ class LnurlWithdraw { "LnurlWithdraw.lnServiceWithdraw, unpaid lnurlWithdrawEntity found for this k1!" ); - // Check expiration - if ( - lnurlWithdrawEntity.expiresAt && - lnurlWithdrawEntity.expiresAt < new Date() - ) { - // Expired LNURL - logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); - - result = { status: "ERROR", reason: "Expired LNURL-Withdraw" }; - } else { - logger.debug("LnurlWithdraw.lnServiceWithdraw: not expired!"); + if (lnurlWithdrawEntity.bolt11) { + // Payment request has been made before, check payment status + const resp = await this._cyphernodeClient.lnListPays({ + bolt11: lnurlWithdrawEntity.bolt11, + } as IReqLnListPays); - if ( - !lnurlWithdrawEntity.bolt11 || - lnurlWithdrawEntity.bolt11 === params.pr - ) { + if (resp.error) { + // Error, should not happen, something's wrong, let's get out of here logger.debug( - "LnurlWithdraw.lnServiceWithdraw: new bolt11 or same as previous!" + "LnurlWithdraw.lnServiceWithdraw, lnListPays errored..." ); - - lnurlWithdrawEntity.bolt11 = params.pr; - const lnPayParams = { - bolt11: params.pr, - expectedMsatoshi: lnurlWithdrawEntity.msatoshi || undefined, - expectedDescription: - lnurlWithdrawEntity.description || undefined, + result = { + status: "ERROR", + reason: "Something unexpected happened", }; - let resp: IRespLnPay = await this._cyphernodeClient.lnPay( - lnPayParams + } else if ( + resp.result && + resp.result.pays && + resp.result.pays.length > 0 + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const paymentStatus = (resp.result.pays[0] as any).status; + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, paymentStatus =", + paymentStatus ); - if (resp.error) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry #1!" - ); + result = await this.processLnStatus( + paymentStatus, + lnurlWithdrawEntity, + params.pr, + resp.result + ); + } else { + // Error, should not happen, something's wrong, let's try with paystatus... + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, no previous listpays for this bolt11..." + ); - resp = await this._cyphernodeClient.lnPay(lnPayParams); - } + const paystatus = await this._cyphernodeClient.lnPayStatus({ + bolt11: lnurlWithdrawEntity.bolt11, + } as IReqLnListPays); - if (resp.error) { + if (paystatus.error) { + // Error, should not happen, something's wrong, let's get out of here logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay error, let's retry #2!" + "LnurlWithdraw.lnServiceWithdraw, lnPayStatus errored..." ); - - resp = await this._cyphernodeClient.lnPay(lnPayParams); - } - - if (resp.error) { + result = { + status: "ERROR", + reason: "Something unexpected happened", + }; + } else if (paystatus.result) { logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay error!" + "LnurlWithdraw.lnServiceWithdraw, lnPayStatus success..." ); - result = { status: "ERROR", reason: resp.error.message }; + // We parse paystatus result + // pay[] is an array of payments + // attempts[] is an array of attempts for each payment + // As soon as there's a "success" field in attemps, payment succeeded! + // If the last attempt doesn't have a "failure" field, it means there's a pending attempt + // If the last attempt has a "failure" field, it means payment failed. + let success = false; + let failure = null; + paystatus.result.pay.forEach((pay) => { + pay.attempts.forEach((attempt) => { + if (attempt.success) { + success = true; + return; + } else if (attempt.failure) { + failure = true; + } else { + failure = false; + } + }); + }); + + let paymentStatus; + if (success) { + paymentStatus = "complete"; + } else if (failure === false) { + paymentStatus = "pending"; + } else { + paymentStatus = "failed"; + } - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.error + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, paymentStatus =", + paymentStatus ); - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity + result = await this.processLnStatus( + paymentStatus, + lnurlWithdrawEntity, + params.pr, + paystatus.result ); } else { + // Error, should not happen, something's wrong, let's get out of here logger.debug( - "LnurlWithdraw.lnServiceWithdraw, ln_pay success!" - ); - - result = { status: "OK" }; - - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.result + "LnurlWithdraw.lnServiceWithdraw, lnPayStatus errored..." ); - lnurlWithdrawEntity.withdrawnTs = new Date(); - lnurlWithdrawEntity.paid = true; - - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); - - if (lnurlWithdrawEntity.webhookUrl) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, about to call back the webhookUrl..." - ); - - this.processCallbacks(lnurlWithdrawEntity); - } + result = { + status: "ERROR", + reason: "Something unexpected happened", + }; } - } else { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, trying to redeem twice with different bolt11!" - ); + } + } else { + // Not previously claimed LNURL + logger.debug( + "LnurlWithdraw.lnServiceWithdraw, Not previously claimed LNURL..." + ); + + // Check expiration + if ( + lnurlWithdrawEntity.expiresAt && + lnurlWithdrawEntity.expiresAt < new Date() + ) { + // Expired LNURL + logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); result = { status: "ERROR", - reason: "Trying to redeem twice with different bolt11", + reason: "Expired LNURL-Withdraw", }; + } else { + result = await this.processLnPayment( + lnurlWithdrawEntity, + params.pr + ); } } } else { @@ -561,6 +741,7 @@ class LnurlWithdraw { return result; } ); + logger.debug( "released lock modifLnurlWithdraw in LN Service LNURL Withdraw" ); diff --git a/src/types/cyphernode/IBatchTx.ts b/src/types/cyphernode/IBatchTx.ts index de7a276..c7a2975 100644 --- a/src/types/cyphernode/IBatchTx.ts +++ b/src/types/cyphernode/IBatchTx.ts @@ -8,5 +8,5 @@ export default interface IBatchTx { replaceable: boolean; fee: number; }; - outputs?: []; + outputs?: unknown[]; } diff --git a/src/types/cyphernode/ILnListPays.ts b/src/types/cyphernode/ILnListPays.ts new file mode 100644 index 0000000..b041d7e --- /dev/null +++ b/src/types/cyphernode/ILnListPays.ts @@ -0,0 +1,3 @@ +export default interface ILnListPays { + pays: unknown[]; +} diff --git a/src/types/cyphernode/ILnPayStatus.ts b/src/types/cyphernode/ILnPayStatus.ts new file mode 100644 index 0000000..b879b23 --- /dev/null +++ b/src/types/cyphernode/ILnPayStatus.ts @@ -0,0 +1,3 @@ +export default interface ILnPayStatus { + pay: { attempts: { success?: unknown; failure?: unknown }[] }[]; +} diff --git a/src/types/cyphernode/IReqLnListPays.ts b/src/types/cyphernode/IReqLnListPays.ts new file mode 100644 index 0000000..59068a3 --- /dev/null +++ b/src/types/cyphernode/IReqLnListPays.ts @@ -0,0 +1,5 @@ +export default interface IReqLnListPays { + // - bolt11, optional, lightning network bolt11 invoice + + bolt11?: string; +} diff --git a/src/types/cyphernode/IRespLnListPays.ts b/src/types/cyphernode/IRespLnListPays.ts new file mode 100644 index 0000000..ede529f --- /dev/null +++ b/src/types/cyphernode/IRespLnListPays.ts @@ -0,0 +1,7 @@ +import { IResponseError } from "../jsonrpc/IResponseMessage"; +import ILnListPays from "./ILnListPays"; + +export default interface IRespLnListPays { + result?: ILnListPays; + error?: IResponseError; +} diff --git a/src/types/cyphernode/IRespLnPayStatus.ts b/src/types/cyphernode/IRespLnPayStatus.ts new file mode 100644 index 0000000..a0feb62 --- /dev/null +++ b/src/types/cyphernode/IRespLnPayStatus.ts @@ -0,0 +1,7 @@ +import { IResponseError } from "../jsonrpc/IResponseMessage"; +import ILnPayStatus from "./ILnPayStatus"; + +export default interface IRespLnPayStatus { + result?: ILnPayStatus; + error?: IResponseError; +} diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index e457aa4..85a619b 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -62,6 +62,8 @@ # 8. Wait for the batch to execute, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112), Batcher's execute callback called (port 1113) # 9. Mined block and Cyphernode's confirmed watch callback called (port 1114) +# + . ./tests/colors.sh trace() { @@ -164,25 +166,25 @@ decode_lnurl() { trace 2 "\n\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}\n" local lnurl=${1} - local lnServicePrefix=${2} local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' trace 3 "[decode_lnurl] data=${data}" local decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[decode_lnurl] decodedLnurl=${decodedLnurl}" - local urlSuffix=$(echo "${decodedLnurl}" | jq -r ".result" | sed 's|'${lnServicePrefix}'||g') - trace 3 "[decode_lnurl] urlSuffix=${urlSuffix}" + local url=$(echo "${decodedLnurl}" | jq -r ".result") + trace 3 "[decode_lnurl] url=${url}" - echo "${urlSuffix}" + echo "${url}" } call_lnservice_withdraw_request() { - trace 2 "\n\n[call_lnservice_withdraw_request] ${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}\n" + trace 1 "\n[call_lnservice_withdraw_request] ${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}" - local urlSuffix=${1} + local url=${1} + trace 2 "[call_lnservice_withdraw_request] url=${url}" - local withdrawRequestResponse=$(curl -s lnurl:8000${urlSuffix}) - trace 3 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" + local withdrawRequestResponse=$(curl -s ${url}) + trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" } @@ -204,7 +206,7 @@ create_bolt11() { } get_invoice_status() { - trace 2 "\n\n[get_invoice_status] ${BCyan}Let's make sure the invoice is unpaid first...${Color_Off}\n" + trace 2 "\n\n[get_invoice_status] ${BCyan}Getting invoice status...${Color_Off}\n" local invoice=${1} trace 3 "[get_invoice_status] invoice=${invoice}" @@ -222,22 +224,22 @@ get_invoice_status() { } call_lnservice_withdraw() { - trace 2 "\n\n[call_lnservice_withdraw] ${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}\n" + trace 1 "\n[call_lnservice_withdraw] ${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}" local withdrawRequestResponse=${1} - local lnServicePrefix=${2} - local bolt11=${3} + trace 2 "[call_lnservice_withdraw] withdrawRequestResponse=${withdrawRequestResponse}" + local bolt11=${2} + trace 2 "[call_lnservice_withdraw] bolt11=${bolt11}" callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") - trace 3 "[call_lnservice_withdraw] callback=${callback}" - urlSuffix=$(echo "${callback}" | sed 's|'${lnServicePrefix}'||g') - trace 3 "[call_lnservice_withdraw] urlSuffix=${urlSuffix}" + trace 2 "[call_lnservice_withdraw] callback=${callback}" k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") - trace 3 "[call_lnservice_withdraw] k1=${k1}" + trace 2 "[call_lnservice_withdraw] k1=${k1}" - trace 3 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" - withdrawResponse=$(curl -s lnurl:8000${urlSuffix}?k1=${k1}\&pr=${bolt11}) - trace 3 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" + trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" + trace 2 "url=${callback}?k1=${k1}\&pr=${bolt11}" + withdrawResponse=$(curl -s ${callback}?k1=${k1}\&pr=${bolt11}) + trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" } @@ -255,7 +257,6 @@ happy_path() { trace 1 "\n\n[happy_path] ${On_Yellow}${BBlack} Happy path: ${Color_Off}\n" local callbackurl=${1} - local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 15) @@ -276,11 +277,11 @@ happy_path() { fi # Decode LNURL - local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 3 "[happy_path] urlSuffix=${urlSuffix}" + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[happy_path] serviceUrl=${serviceUrl}" # User calls LN Service LNURL Withdraw Request - local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[happy_path] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw @@ -298,7 +299,7 @@ happy_path() { start_callback_server # User calls LN Service LNURL Withdraw - local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[happy_path] withdrawResponse=${withdrawResponse}" wait @@ -328,7 +329,6 @@ expired1() { trace 1 "\n\n[expired1] ${On_Yellow}${BBlack} Expired 1: ${Color_Off}\n" local callbackurl=${1} - local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) @@ -349,11 +349,11 @@ expired1() { fi # Decode LNURL - local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 3 "[expired1] urlSuffix=${urlSuffix}" + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[expired1] serviceUrl=${serviceUrl}" # User calls LN Service LNURL Withdraw Request - local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[expired1] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" | grep -qi "expired" @@ -377,7 +377,6 @@ expired2() { trace 1 "\n\n[expired2] ${On_Yellow}${BBlack} Expired 2: ${Color_Off}\n" local callbackurl=${1} - local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) @@ -398,11 +397,11 @@ expired2() { fi # Decode LNURL - local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 3 "[expired2] urlSuffix=${urlSuffix}" + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[expired2] serviceUrl=${serviceUrl}" # User calls LN Service LNURL Withdraw Request - local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[expired2] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw @@ -417,7 +416,7 @@ expired2() { sleep 5 # User calls LN Service LNURL Withdraw - local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[expired2] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" | grep -qi "expired" @@ -441,7 +440,6 @@ deleted1() { trace 1 "\n\n[deleted1] ${On_Yellow}${BBlack} Deleted 1: ${Color_Off}\n" local callbackurl=${1} - local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) @@ -486,15 +484,15 @@ deleted1() { trace 1 "\n\n[deleted1] ${On_Red}${BBlack} Deleted 1: Should return an error because already deactivated! ${Color_Off}\n" return 1 else - trace 1 "\n\n[deleted1] ${On_IGreen}${BBlack} Deleted 1: SUCCESS! ${Color_Off}\n" + trace 1 "[deleted1] DELETED! Good!..." fi # Decode LNURL - local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 3 "[deleted1] urlSuffix=${urlSuffix}" + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[deleted1] serviceUrl=${serviceUrl}" # User calls LN Service LNURL Withdraw Request - local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[deleted1] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" | grep -qi "Deactivated" @@ -518,7 +516,6 @@ deleted2() { trace 1 "\n\n[deleted2] ${On_Yellow}${BBlack} Deleted 2: ${Color_Off}\n" local callbackurl=${1} - local lnServicePrefix=${2} # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) @@ -539,11 +536,11 @@ deleted2() { fi # Decode LNURL - local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 3 "[deleted2] urlSuffix=${urlSuffix}" + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[deleted2] serviceUrl=${serviceUrl}" # User calls LN Service LNURL Withdraw Request - local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[deleted2] withdrawRequestResponse=${withdrawRequestResponse}" # Create bolt11 for LN Service LNURL Withdraw @@ -560,7 +557,7 @@ deleted2() { trace 3 "[deleted2] deleted=${deleted}" # User calls LN Service LNURL Withdraw - local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${lnServicePrefix}" "${bolt11}") + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[deleted2] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" | grep -qi "Deactivated" @@ -588,7 +585,6 @@ fallback1() { local callbackserver=${1} local callbackport=${2} - local lnServicePrefix=${3} local zeroconfport=$((${callbackserverport}+1)) local oneconfport=$((${callbackserverport}+2)) @@ -626,15 +622,15 @@ fallback1() { fi # Decode LNURL - local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 3 "[fallback1] urlSuffix=${urlSuffix}" + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[fallback1] serviceUrl=${serviceUrl}" start_callback_server start_callback_server ${zeroconfport} start_callback_server ${oneconfport} # User calls LN Service LNURL Withdraw Request - local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[fallback1] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" | grep -qi "expired" @@ -669,7 +665,6 @@ fallback2() { local callbackserver=${1} local callbackport=${2} - local lnServicePrefix=${3} local zeroconfport=$((${callbackserverport}+1)) local oneconfport=$((${callbackserverport}+2)) @@ -707,14 +702,14 @@ fallback2() { fi # Decode LNURL - local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 3 "[fallback2] urlSuffix=${urlSuffix}" + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[fallback2] serviceUrl=${serviceUrl}" # fallback batched callback start_callback_server # User calls LN Service LNURL Withdraw Request - local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[fallback2] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" | grep -qi "expired" @@ -760,7 +755,6 @@ fallback3() { local callbackserver=${1} local callbackport=${2} - local lnServicePrefix=${3} local zeroconfport=$((${callbackserverport}+1)) local oneconfport=$((${callbackserverport}+2)) @@ -798,15 +792,15 @@ fallback3() { fi # Decode LNURL - local urlSuffix=$(decode_lnurl "${lnurl}" "${lnServicePrefix}") - trace 3 "[fallback3] urlSuffix=${urlSuffix}" + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[fallback3] serviceUrl=${serviceUrl}" start_callback_server start_callback_server ${zeroconfport} start_callback_server ${oneconfport} # User calls LN Service LNURL Withdraw Request - local withdrawRequestResponse=$(call_lnservice_withdraw_request "${urlSuffix}") + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[fallback3] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" | grep -qi "expired" @@ -850,24 +844,13 @@ callbackservername="lnurl_withdraw_test" callbackserverport="1111" callbackurl="http://${callbackservername}:${callbackserverport}" -# wait_for_callbacks - -# Get config from lnurl cypherapp -trace 2 "\n\n${BCyan}Getting configuration from lnurl cypherapp...${Color_Off}\n" -data='{"id":0,"method":"getConfig","params":[]}' -lnurlConfig=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) -trace 3 "lnurlConfig=${lnurlConfig}" -# lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER):\(.LN_SERVICE_PORT)"') -lnServicePrefix=$(echo "${lnurlConfig}" | jq -r '.result | "\(.LN_SERVICE_SERVER)"') -trace 3 "lnServicePrefix=${lnServicePrefix}" - -# happy_path "${callbackurl}" "${lnServicePrefix}" \ -# && expired1 "${callbackurl}" "${lnServicePrefix}" \ -# && expired2 "${callbackurl}" "${lnServicePrefix}" \ -# && deleted1 "${callbackurl}" "${lnServicePrefix}" \ -# && deleted2 "${callbackurl}" "${lnServicePrefix}" \ -# && fallback1 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" \ -# && fallback2 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" \ -fallback3 "${callbackservername}" "${callbackserverport}" "${lnServicePrefix}" +happy_path "${callbackurl}" \ +&& expired1 "${callbackurl}" \ +&& expired2 "${callbackurl}" \ +&& deleted1 "${callbackurl}" \ +&& deleted2 "${callbackurl}" \ +&& fallback1 "${callbackservername}" "${callbackserverport}" \ +&& fallback2 "${callbackservername}" "${callbackserverport}" \ +&& fallback3 "${callbackservername}" "${callbackserverport}" trace 1 "\n\n${BCyan}Finished, deleting this test container...${Color_Off}\n" diff --git a/tests/lnurl_withdraw_wallet.sh b/tests/lnurl_withdraw_wallet.sh index f7ca416..c91f958 100755 --- a/tests/lnurl_withdraw_wallet.sh +++ b/tests/lnurl_withdraw_wallet.sh @@ -9,25 +9,60 @@ trace() { fi } +create_lnurl_withdraw() { + trace 2 "\n\n[create_lnurl_withdraw] ${BCyan}Service creates LNURL Withdraw...${Color_Off}\n" + + local callbackurl=${1} + trace 3 "[create_lnurl_withdraw] callbackurl=${callbackurl}" + + local invoicenumber=${3:-$RANDOM} + trace 3 "[create_lnurl_withdraw] invoicenumber=${invoicenumber}" + local msatoshi=$((500000+${invoicenumber})) + trace 3 "[create_lnurl_withdraw] msatoshi=${msatoshi}" + local expiration_offset=${2:-0} + local expiration=$(date -d @$(($(date -u +"%s")+${expiration_offset})) +"%Y-%m-%dT%H:%M:%SZ") + trace 3 "[create_lnurl_withdraw] expiration=${expiration}" + local fallback_addr=${4:-""} + local fallback_batched=${5:-"false"} + + if [ -n "${callbackurl}" ]; then + callbackurl=',"webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'"' + fi + + # Service creates LNURL Withdraw + data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiresAt":"'${expiration}'"'${callbackurl}',"btcFallbackAddress":"'${fallback_addr}'","batchFallback":'${fallback_batched}'}}' + trace 3 "[create_lnurl_withdraw] data=${data}" + trace 3 "[create_lnurl_withdraw] Calling createLnurlWithdraw..." + local createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[create_lnurl_withdraw] createLnurlWithdraw=${createLnurlWithdraw}" + + # {"id":0,"result":{"msatoshi":100000000,"description":"desc01","expiresAt":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + trace 3 "[create_lnurl_withdraw] lnurl=${lnurl}" + + echo "${createLnurlWithdraw}" +} + decode_lnurl() { - trace 1 "\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}" + trace 2 "\n\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}\n" local lnurl=${1} - trace 2 "[decode_lnurl] lnurl=${lnurl}" local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' - trace 2 "[decode_lnurl] data=${data}" + trace 3 "[decode_lnurl] data=${data}" local decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) - trace 2 "[decode_lnurl] decodedLnurl=${decodedLnurl}" + trace 3 "[decode_lnurl] decodedLnurl=${decodedLnurl}" + local url=$(echo "${decodedLnurl}" | jq -r ".result") + trace 3 "[decode_lnurl] url=${url}" - echo "${decodedLnurl}" + echo "${url}" } call_lnservice_withdraw_request() { trace 1 "\n[call_lnservice_withdraw_request] ${BCyan}User calls LN Service LNURL Withdraw Request...${Color_Off}" local url=${1} - trace 2 "[decode_lnurl] url=${url}" + trace 2 "[call_lnservice_withdraw_request] url=${url}" local withdrawRequestResponse=$(curl -s ${url}) trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" @@ -92,7 +127,7 @@ call_lnservice_withdraw() { echo "${withdrawResponse}" } -TRACING=2 +TRACING=3 trace 2 "${Color_Off}" date @@ -103,22 +138,56 @@ apk add curl jq lnurl=${1} trace 2 "lnurl=${lnurl}" +bolt11=${2} +trace 2 "bolt11=${bolt11}" + +if [ "${lnurl}" = "createlnurl" ]; then + # Initializing test variables + trace 2 "\n\n${BCyan}Initializing test variables...${Color_Off}\n" + # callbackservername="lnurl_withdraw_test" + # callbackserverport="1111" + # callbackurl="http://${callbackservername}:${callbackserverport}" + trace 3 "callbackurl=${callbackurl}" + + createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 600) + trace 3 "[fallback3] createLnurlWithdraw=${createLnurlWithdraw}" + lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + trace 3 "[fallback3] lnurl=${lnurl}" +else + url=$(decode_lnurl "${lnurl}") + trace 2 "url=${url}" + + withdrawRequestResponse=$(call_lnservice_withdraw_request "${url}") + trace 2 "withdrawRequestResponse=${withdrawRequestResponse}" + + # {"status":"ERROR","reason":"Expired LNURL-Withdraw"} + reason=$(echo "${withdrawRequestResponse}" | jq -r ".reason // empty") + + if [ -n "${reason}" ]; then + trace 1 "\n\nERROR! Reason: ${reason}\n\n" + return 1 + fi + + msatoshi=$(echo "${withdrawRequestResponse}" | jq -r ".maxWithdrawable") + trace 2 "msatoshi=${msatoshi}" + desc=$(echo "${withdrawRequestResponse}" | jq -r ".defaultDescription") + trace 2 "desc=${desc}" -decoded_lnurl=$(decode_lnurl "${lnurl}") -trace 2 "decoded_lnurl=${decoded_lnurl}" -url=$(echo "${decoded_lnurl}" | jq -r ".result") -trace 2 "url=${url}" + if [ -z "${bolt11}" ]; then + invoice=$(create_bolt11 ${msatoshi} "$RANDOM" "${desc}") + trace 2 "invoice=${invoice}" + bolt11=$(echo "${invoice}" | jq -r ".bolt11") -withdrawRequestResponse=$(call_lnservice_withdraw_request "${url}") -trace 2 "withdrawRequestResponse=${withdrawRequestResponse}" -msatoshi=$(echo "${withdrawRequestResponse}" | jq -r ".maxWithdrawable") -trace 2 "msatoshi=${msatoshi}" -desc=$(echo "${withdrawRequestResponse}" | jq -r ".defaultDescription") -trace 2 "desc=${desc}" + trace 2 "bolt11=${bolt11}" + fi -invoice=$(create_bolt11 ${msatoshi} "$RANDOM" "${desc}") -trace 2 "invoice=${invoice}" -bolt11=$(echo "${invoice}" | jq -r ".bolt11") + withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") + trace 2 "withdrawResponse=${withdrawResponse}" -withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") -trace 2 "withdrawResponse=${withdrawResponse}" + reason=$(echo "${withdrawResponse}" | jq -r ".reason // empty") + + if [ -n "${reason}" ]; then + trace 1 "\n\nERROR! Reason: ${reason}\n\n" + return 1 + fi +fi diff --git a/tests/run_lnurl_withdraw_wallet.sh b/tests/run_lnurl_withdraw_wallet.sh index ae67694..7ea69eb 100755 --- a/tests/run_lnurl_withdraw_wallet.sh +++ b/tests/run_lnurl_withdraw_wallet.sh @@ -2,4 +2,4 @@ #./startcallbackserver.sh & -docker run --rm -it -v "$PWD:/tests" --network=cyphernodeappsnet alpine /tests/lnurl_withdraw_wallet.sh $1 +docker run --rm -it -v "$PWD:/tests" --network=cyphernodeappsnet alpine /tests/lnurl_withdraw_wallet.sh $@ From 60479afa3d6a0db077faeffd8802e93623da0ec9 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 17 Sep 2021 21:49:43 -0400 Subject: [PATCH 26/52] Fixed some comments and tests --- src/lib/CyphernodeClient.ts | 32 ++++++++++++++++---------------- tests/lnurl_withdraw.sh | 2 +- tests/run_tests.sh | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 0c1b5ee..a6ebbc2 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -217,7 +217,7 @@ class CyphernodeClient { // - webhookUrl, optional, the webhook to call when the batch is broadcast // response: - // - lnurlId, the id of the lnurl + // - batcherId, the id of the batcher // - outputId, the id of the added output // - nbOutputs, the number of outputs currently in the batch // - oldest, the timestamp of the oldest output in the batch @@ -254,7 +254,7 @@ class CyphernodeClient { // - outputId, required, id of the output to remove // // response: - // - lnurlId, the id of the lnurl + // - batcherId, the id of the batcher // - outputId, the id of the removed output if found // - nbOutputs, the number of outputs currently in the batch // - oldest, the timestamp of the oldest output in the batch @@ -289,15 +289,15 @@ class CyphernodeClient { // POST (GET) http://192.168.111.152:8080/getbatchdetails // // args: - // - lnurlId, optional, id of the lnurl, overrides lnurlLabel, default lnurl will be spent if not supplied - // - lnurlLabel, optional, label of the lnurl, default lnurl will be used if not supplied + // - batcherId, optional, id of the batcher, overrides batcherLabel, default batcher will be spent if not supplied + // - batcherLabel, optional, label of the batcher, default batcher will be used if not supplied // - txid, optional, if you want the details of an executed batch, supply the batch txid, will return current pending batch // if not supplied // // response: // {"result":{ - // "lnurlId":34, - // "lnurlLabel":"Special lnurl for a special client", + // "batcherId":34, + // "batcherLabel":"Special batcher for a special client", // "confTarget":6, // "nbOutputs":83, // "oldest":123123, @@ -321,7 +321,7 @@ class CyphernodeClient { // },"error":null} // // BODY {} - // BODY {"lnurlId":34} + // BODY {"batcherId":34} logger.info("CyphernodeClient.getBatchDetails:", batchIdent); @@ -346,10 +346,10 @@ class CyphernodeClient { // POST http://192.168.111.152:8080/batchspend // // args: - // - lnurlId, optional, id of the lnurl to execute, overrides lnurlLabel, default lnurl will be spent if not supplied - // - lnurlLabel, optional, label of the lnurl to execute, default lnurl will be executed if not supplied - // - confTarget, optional, overrides default value of createlnurl, default to value of createlnurl, default Bitcoin Core conf_target will be used if not supplied - // NOTYET - feeRate, optional, overrides confTarget if supplied, overrides default value of createlnurl, default to value of createlnurl, default Bitcoin Core value will be used if not supplied + // - batcherId, optional, id of the batcher to execute, overrides batcherLabel, default batcher will be spent if not supplied + // - batcherLabel, optional, label of the batcher to execute, default batcher will be executed if not supplied + // - confTarget, optional, overrides default value of createbatcher, default to value of createbatcher, default Bitcoin Core conf_target will be used if not supplied + // NOTYET - feeRate, optional, overrides confTarget if supplied, overrides default value of createbatcher, default to value of createbatcher, default Bitcoin Core value will be used if not supplied // // response: // - txid, the transaction txid @@ -361,8 +361,8 @@ class CyphernodeClient { // - outputs // // {"result":{ - // "lnurlId":34, - // "lnurlLabel":"Special lnurl for a special client", + // "batcherId":34, + // "batcherLabel":"Special batcher for a special client", // "confTarget":6, // "nbOutputs":83, // "oldest":123123, @@ -386,9 +386,9 @@ class CyphernodeClient { // },"error":null} // // BODY {} - // BODY {"lnurlId":34,"confTarget":12} - // NOTYET BODY {"lnurlLabel":"highfees","feeRate":233.7} - // BODY {"lnurlId":411,"confTarget":6} + // BODY {"batcherId":34,"confTarget":12} + // NOTYET BODY {"batcherLabel":"highfees","feeRate":233.7} + // BODY {"batcherId":411,"confTarget":6} logger.info("CyphernodeClient.batchSpend:", batchSpendTO); diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index 85a619b..c761f0b 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -231,7 +231,7 @@ call_lnservice_withdraw() { local bolt11=${2} trace 2 "[call_lnservice_withdraw] bolt11=${bolt11}" - callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") + local callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") trace 2 "[call_lnservice_withdraw] callback=${callback}" k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") trace 2 "[call_lnservice_withdraw] k1=${k1}" diff --git a/tests/run_tests.sh b/tests/run_tests.sh index ff4e8a8..bcc0a13 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -6,6 +6,6 @@ docker run --rm -d --name lnurl_withdraw_test -v "$PWD:/tests" --network=cyphernodeappsnet alpine sh -c 'while true; do sleep 10; done' docker network connect cyphernodenet lnurl_withdraw_test -(sleep 90 ; ./mine.sh 1 0 ; sleep 90 ; ./mine.sh 1 0) & +(sleep 90 ; ./mine.sh 1 0 ; sleep 90 ; ./mine.sh 1 0 ; sleep 30 ; ./mine.sh 1 0) & docker exec -it lnurl_withdraw_test /tests/lnurl_withdraw.sh docker stop lnurl_withdraw_test From 682ac73962273614f393da2c1ad33b0216428fe6 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 7 Oct 2021 00:48:01 -0400 Subject: [PATCH 27/52] Don't fallback if payment done in background --- src/lib/LnurlWithdraw.ts | 429 +++++++++++++++++++++++---------------- tests/ln_reconnect.sh | 8 +- tests/lnurl_withdraw.sh | 173 +++++++++++++++- tests/run_tests.sh | 5 +- 4 files changed, 430 insertions(+), 185 deletions(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index e29b07c..89e9069 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -531,6 +531,89 @@ class LnurlWithdraw { return result; } + async lnFetchPaymentStatus( + bolt11: string + ): Promise<{ paymentStatus?: string; result?: unknown }> { + let paymentStatus; + let result; + + const resp = await this._cyphernodeClient.lnListPays({ + bolt11, + } as IReqLnListPays); + + if (resp.error) { + // Error, should not happen, something's wrong, let's get out of here + logger.debug("LnurlWithdraw.lnFetchPaymentStatus, lnListPays errored..."); + } else if (resp.result && resp.result.pays && resp.result.pays.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + paymentStatus = (resp.result.pays[0] as any).status; + logger.debug( + "LnurlWithdraw.lnFetchPaymentStatus, paymentStatus =", + paymentStatus + ); + + result = resp.result; + } else { + // Error, should not happen, something's wrong, let's try with paystatus... + logger.debug( + "LnurlWithdraw.lnFetchPaymentStatus, no previous listpays for this bolt11..." + ); + + const paystatus = await this._cyphernodeClient.lnPayStatus({ + bolt11, + } as IReqLnListPays); + + if (paystatus.error) { + // Error, should not happen, something's wrong, let's get out of here + logger.debug( + "LnurlWithdraw.lnFetchPaymentStatus, lnPayStatus errored..." + ); + } else if (paystatus.result) { + logger.debug( + "LnurlWithdraw.lnFetchPaymentStatus, lnPayStatus success..." + ); + + // We parse paystatus result + // pay[] is an array of payments + // attempts[] is an array of attempts for each payment + // As soon as there's a "success" field in attemps, payment succeeded! + // If the last attempt doesn't have a "failure" field, it means there's a pending attempt + // If the last attempt has a "failure" field, it means payment failed. + let success = false; + let failure = null; + paystatus.result.pay.forEach((pay) => { + pay.attempts.forEach((attempt) => { + if (attempt.success) { + success = true; + return; + } else if (attempt.failure) { + failure = true; + } else { + failure = false; + } + }); + }); + + if (success) { + paymentStatus = "complete"; + } else if (failure === false) { + paymentStatus = "pending"; + } else { + paymentStatus = "failed"; + } + + logger.debug( + "LnurlWithdraw.lnFetchPaymentStatus, paymentStatus =", + paymentStatus + ); + + result = paystatus.result; + } + } + + return { paymentStatus, result }; + } + async lnServiceWithdraw( params: IReqLnurlWithdraw ): Promise { @@ -577,112 +660,22 @@ class LnurlWithdraw { if (lnurlWithdrawEntity.bolt11) { // Payment request has been made before, check payment status - const resp = await this._cyphernodeClient.lnListPays({ - bolt11: lnurlWithdrawEntity.bolt11, - } as IReqLnListPays); + const paymentStatus = await this.lnFetchPaymentStatus( + lnurlWithdrawEntity.bolt11 + ); - if (resp.error) { - // Error, should not happen, something's wrong, let's get out of here - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, lnListPays errored..." - ); + if (paymentStatus.paymentStatus === undefined) { result = { status: "ERROR", reason: "Something unexpected happened", }; - } else if ( - resp.result && - resp.result.pays && - resp.result.pays.length > 0 - ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const paymentStatus = (resp.result.pays[0] as any).status; - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, paymentStatus =", - paymentStatus - ); - + } else { result = await this.processLnStatus( - paymentStatus, + paymentStatus.paymentStatus, lnurlWithdrawEntity, params.pr, - resp.result - ); - } else { - // Error, should not happen, something's wrong, let's try with paystatus... - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, no previous listpays for this bolt11..." + paymentStatus.result ); - - const paystatus = await this._cyphernodeClient.lnPayStatus({ - bolt11: lnurlWithdrawEntity.bolt11, - } as IReqLnListPays); - - if (paystatus.error) { - // Error, should not happen, something's wrong, let's get out of here - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, lnPayStatus errored..." - ); - result = { - status: "ERROR", - reason: "Something unexpected happened", - }; - } else if (paystatus.result) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, lnPayStatus success..." - ); - - // We parse paystatus result - // pay[] is an array of payments - // attempts[] is an array of attempts for each payment - // As soon as there's a "success" field in attemps, payment succeeded! - // If the last attempt doesn't have a "failure" field, it means there's a pending attempt - // If the last attempt has a "failure" field, it means payment failed. - let success = false; - let failure = null; - paystatus.result.pay.forEach((pay) => { - pay.attempts.forEach((attempt) => { - if (attempt.success) { - success = true; - return; - } else if (attempt.failure) { - failure = true; - } else { - failure = false; - } - }); - }); - - let paymentStatus; - if (success) { - paymentStatus = "complete"; - } else if (failure === false) { - paymentStatus = "pending"; - } else { - paymentStatus = "failed"; - } - - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, paymentStatus =", - paymentStatus - ); - - result = await this.processLnStatus( - paymentStatus, - lnurlWithdrawEntity, - params.pr, - paystatus.result - ); - } else { - // Error, should not happen, something's wrong, let's get out of here - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, lnPayStatus errored..." - ); - result = { - status: "ERROR", - reason: "Something unexpected happened", - }; - } } } else { // Not previously claimed LNURL @@ -892,99 +885,136 @@ class LnurlWithdraw { lnurlWithdrawEntity ); - if (lnurlWithdrawEntity.batchFallback) { - logger.debug("LnurlWithdraw.processFallbacks, batched fallback"); + let proceedToFallback = true; + if (lnurlWithdrawEntity.bolt11) { + // Before falling back on-chain, let's make really sure the payment has not been done... + const paymentStatus = await this.lnFetchPaymentStatus( + lnurlWithdrawEntity.bolt11 + ); + + if (paymentStatus.paymentStatus === undefined) { + logger.debug( + "LnurlWithdraw.processFallbacks: Can't get LnurlWithdraw previously paid status." + ); + proceedToFallback = false; + } else if (paymentStatus.paymentStatus !== "failed") { + logger.debug( + "LnurlWithdraw.processFallbacks: LnurlWithdraw payment already " + + paymentStatus.paymentStatus + ); + proceedToFallback = false; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + paymentStatus.result + ); + // lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + this.checkWebhook(lnurlWithdrawEntity); + } + } + + if (proceedToFallback) { + if (lnurlWithdrawEntity.batchFallback) { + logger.debug("LnurlWithdraw.processFallbacks, batched fallback"); + + if (lnurlWithdrawEntity.batchRequestId) { + logger.debug( + "LnurlWithdraw.processFallbacks, already batched!" + ); + } else { + const batchRequestTO: IReqBatchRequest = { + externalId: lnurlWithdrawEntity.externalId || undefined, + description: lnurlWithdrawEntity.description || undefined, + address: lnurlWithdrawEntity.btcFallbackAddress || "", + amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, + webhookUrl: + this._lnurlConfig.URL_API_SERVER + + ":" + + this._lnurlConfig.URL_API_PORT + + this._lnurlConfig.URL_CTX_WEBHOOKS, + }; + + const resp: IRespBatchRequest = await this._batcherClient.queueForNextBatch( + batchRequestTO + ); + + if (resp.error) { + logger.debug( + "LnurlWithdraw.processFallbacks, queueForNextBatch error!" + ); + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.error + ); + } else { + logger.debug( + "LnurlWithdraw.processFallbacks, queueForNextBatch success!" + ); + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + resp.result + ); + lnurlWithdrawEntity.batchRequestId = + resp.result?.batchRequestId || null; + } + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); - if (lnurlWithdrawEntity.batchRequestId) { - logger.debug("LnurlWithdraw.processFallbacks, already batched!"); + if (lnurlWithdrawEntity.batchRequestId) { + this.processCallbacks(lnurlWithdrawEntity); + } + } } else { - const batchRequestTO: IReqBatchRequest = { - externalId: lnurlWithdrawEntity.externalId || undefined, - description: lnurlWithdrawEntity.description || undefined, + logger.debug( + "LnurlWithdraw.processFallbacks, not batched fallback" + ); + + const spendRequestTO: IReqSpend = { address: lnurlWithdrawEntity.btcFallbackAddress || "", amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, - webhookUrl: - this._lnurlConfig.URL_API_SERVER + - ":" + - this._lnurlConfig.URL_API_PORT + - this._lnurlConfig.URL_CTX_WEBHOOKS, }; - const resp: IRespBatchRequest = await this._batcherClient.queueForNextBatch( - batchRequestTO + const spendResp: IRespSpend = await this._cyphernodeClient.spend( + spendRequestTO ); - if (resp.error) { + if (spendResp?.error) { + // There was an error on Cyphernode end, return that. logger.debug( - "LnurlWithdraw.processFallbacks, queueForNextBatch error!" + "LnurlWithdraw.processFallbacks: There was an error on Cyphernode spend." ); lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.error + spendResp.error ); - } else { + } else if (spendResp?.result) { logger.debug( - "LnurlWithdraw.processFallbacks, queueForNextBatch success!" + "LnurlWithdraw.processFallbacks: Cyphernode spent: ", + spendResp.result ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.result + spendResp.result ); - lnurlWithdrawEntity.batchRequestId = - resp.result?.batchRequestId || null; + lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + lnurlWithdrawEntity.fallbackDone = true; } lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( lnurlWithdrawEntity ); - if (lnurlWithdrawEntity.batchRequestId) { + if (lnurlWithdrawEntity.fallbackDone) { this.processCallbacks(lnurlWithdrawEntity); } } - } else { - logger.debug( - "LnurlWithdraw.processFallbacks, not batched fallback" - ); - - const spendRequestTO: IReqSpend = { - address: lnurlWithdrawEntity.btcFallbackAddress || "", - amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, - }; - - const spendResp: IRespSpend = await this._cyphernodeClient.spend( - spendRequestTO - ); - - if (spendResp?.error) { - // There was an error on Cyphernode end, return that. - logger.debug( - "LnurlWithdraw.processFallbacks: There was an error on Cyphernode spend." - ); - - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - spendResp.error - ); - } else if (spendResp?.result) { - logger.debug( - "LnurlWithdraw.processFallbacks: Cyphernode spent: ", - spendResp.result - ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - spendResp.result - ); - lnurlWithdrawEntity.withdrawnTs = new Date(); - lnurlWithdrawEntity.paid = true; - lnurlWithdrawEntity.fallbackDone = true; - } - - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); - - if (lnurlWithdrawEntity.fallbackDone) { - this.processCallbacks(lnurlWithdrawEntity); - } } }); } @@ -1029,20 +1059,67 @@ class LnurlWithdraw { "LnurlWithdraw.forceFallback, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!" ); - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - lnurlWithdrawEntity.expiresAt = yesterday; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + if (lnurlWithdrawEntity.bolt11) { + // Payment request has been made before... + // Before falling back on-chain, let's make really sure the payment has not been done... + const paymentStatus = await this.lnFetchPaymentStatus( + lnurlWithdrawEntity.bolt11 + ); - const lnurlDecoded = await Utils.decodeBech32( - lnurlWithdrawEntity?.lnurl || "" - ); + if (paymentStatus.paymentStatus === undefined) { + logger.debug( + "LnurlWithdraw.forceFallback, can't get LnurlWithdraw previously paid status!" + ); - response.result = Object.assign(lnurlWithdrawEntity, { - lnurlDecoded, - }); + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Can't get LnurlWithdraw previously paid status", + }; + } else { + if (paymentStatus.paymentStatus !== "failed") { + logger.debug( + "LnurlWithdraw.forceFallback, LnurlWithdraw payment already " + + paymentStatus.paymentStatus + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: + "LnurlWithdraw payment already " + + paymentStatus.paymentStatus, + }; + + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( + paymentStatus.result + ); + // lnurlWithdrawEntity.withdrawnTs = new Date(); + lnurlWithdrawEntity.paid = true; + + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + this.checkWebhook(lnurlWithdrawEntity); + } + } + } + + if (!response.error) { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + lnurlWithdrawEntity.expiresAt = yesterday; + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( + lnurlWithdrawEntity + ); + + const lnurlDecoded = await Utils.decodeBech32( + lnurlWithdrawEntity?.lnurl || "" + ); + + response.result = Object.assign(lnurlWithdrawEntity, { + lnurlDecoded, + }); + } } else { // LnurlWithdraw already paid logger.debug( diff --git a/tests/ln_reconnect.sh b/tests/ln_reconnect.sh index 3985f46..36c6ae5 100755 --- a/tests/ln_reconnect.sh +++ b/tests/ln_reconnect.sh @@ -1,4 +1,8 @@ #!/bin/sh -docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning") -docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning2") +ln_reconnect() { + docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning") + docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning2") +} + +case "${0}" in *ln_reconnect.sh) ln_reconnect $@;; esac diff --git a/tests/lnurl_withdraw.sh b/tests/lnurl_withdraw.sh index c761f0b..1e001a4 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/lnurl_withdraw.sh @@ -62,7 +62,29 @@ # 8. Wait for the batch to execute, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112), Batcher's execute callback called (port 1113) # 9. Mined block and Cyphernode's confirmed watch callback called (port 1114) -# +# fallback 3, force fallback + +# 1. Cyphernode.getnewaddress -> btcfallbackaddr +# 2. Cyphernode.watch btcfallbackaddr +# 3. Listen to watch webhook +# 4. Create a LNURL Withdraw with expiration=tomorrow and a btcfallbackaddr +# 5. Get it and compare +# 6. User calls LNServiceWithdrawRequest -> works, not expired! +# 7. Call forceFallback +# 7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) +# 8. Mined block and Cyphernode's confirmed watch callback called (port 1113) + +# fallback 4, execute fallback on a bolt11 paid in background (lnurl app doesn't know it's been paid) + +# 1. Cyphernode.getnewaddress -> btcfallbackaddr +# 2. Create a LNURL Withdraw with expiration=tomorrow and a btcfallbackaddr +# 3. Get it and compare +# 4. User calls LNServiceWithdrawRequest -> works, not expired! +# 5. Shut down LN02 +# 6. User calls LNServiceWithdraw -> fails because LN02 is down +# 7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it +# 8. Call forceFallback -> should check payment status and say it's already paid! + . ./tests/colors.sh @@ -73,6 +95,38 @@ trace() { fi } +ln_reconnect() { + trace 2 "\n\n[ln_reconnect] ${BCyan}Reconnecting the two LN instances...${Color_Off}\n" + + (while true ; do ping -c 1 cyphernode_lightning ; [ "$?" -eq "0" ] && break ; sleep 5; done) & + (while true ; do ping -c 1 cyphernode_lightning2 ; [ "$?" -eq "0" ] && break ; sleep 5; done) & + wait + + local data='{"id":1,"jsonrpc":"2.0","method":"getinfo"}' + trace 3 "[ln_reconnect] data=${data}" + local getinfo1=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet:9737/rpc) + trace 3 "[ln_reconnect] getinfo1=${getinfo1}" + local id1=$(echo "${getinfo1}" | jq -r ".id") + trace 3 "[ln_reconnect] id1=${id1}" + + data='{"id":1,"jsonrpc":"2.0","method":"getinfo"}' + trace 3 "[ln_reconnect] data=${data}" + local getinfo2=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 3 "[ln_reconnect] getinfo2=${getinfo2}" + local id2=$(echo "${getinfo2}" | jq -r ".id") + trace 3 "[ln_reconnect] id2=${id2}" + + data='{"id":1,"jsonrpc":"2.0","method":"connect","params":{"id":"'${id2}'@lightning2"}}' + trace 3 "[ln_reconnect] data=${data}" + local connect=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet:9737/rpc) + trace 3 "[create_bolt11] connect=${connect}" + + data='{"id":1,"jsonrpc":"2.0","method":"connect","params":{"id":"'${id1}'@lightning"}}' + trace 3 "[ln_reconnect] data=${data}" + local connect2=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 3 "[create_bolt11] connect2=${connect2}" +} + create_lnurl_withdraw() { trace 2 "\n\n[create_lnurl_withdraw] ${BCyan}Service creates LNURL Withdraw...${Color_Off}\n" @@ -197,7 +251,7 @@ create_bolt11() { local desc=${2} trace 3 "[create_bolt11] desc=${desc}" - local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${msatoshi}',"label":"'${desc}'","description":"'${desc}'"}}' + local data='{"id":1,"jsonrpc":"2.0","method":"invoice","params":{"msatoshi":'${msatoshi}',"label":"'${desc}'","description":"'${desc}'"}}' trace 3 "[create_bolt11] data=${data}" local invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) trace 3 "[create_bolt11] invoice=${invoice}" @@ -213,7 +267,7 @@ get_invoice_status() { local payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") trace 3 "[get_invoice_status] payment_hash=${payment_hash}" - local data='{"id":1,"jsonrpc": "2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' + local data='{"id":1,"jsonrpc":"2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' trace 3 "[get_invoice_status] data=${data}" local invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) trace 3 "[get_invoice_status] invoices=${invoices}" @@ -822,6 +876,113 @@ fallback3() { trace 1 "\n\n[fallback3] ${On_IGreen}${BBlack} Fallback 3: SUCCESS! ${Color_Off}\n" } +fallback4() { + # 1. Cyphernode.getnewaddress -> btcfallbackaddr + # 2. Create a LNURL Withdraw with expiration=tomorrow and a btcfallbackaddr + # 3. Get it and compare + # 4. User calls LNServiceWithdrawRequest -> works, not expired! + # 5. Shut down LN02 + # 6. User calls LNServiceWithdraw -> fails because LN02 is down + # 7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it + # 8. Call forceFallback -> should check payment status and say it's already paid! + + trace 1 "\n\n[fallback4] ${On_Yellow}${BBlack} Fallback 4: ${Color_Off}\n" + + local callbackurl="http://${callbackservername}:${callbackserverport}" + + # Get new address + local data='{"label":"lnurl_fallback_test"}' + local btcfallbackaddr=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) + btcfallbackaddr=$(echo "${btcfallbackaddr}" | jq -r ".address") + trace 3 "[fallback4] btcfallbackaddr=${btcfallbackaddr}" + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 86400 "" "${btcfallbackaddr}") + trace 3 "[fallback4] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + trace 3 "[fallback4] lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 3 "[fallback4] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 3 "[fallback4] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[fallback4] EQUALS!" + else + trace 1 "\n\n[fallback4] ${On_Red}${BBlack} Fallback 3: NOT EQUALS! ${Color_Off}\n" + return 1 + fi + + # Decode LNURL + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[fallback4] serviceUrl=${serviceUrl}" + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") + trace 3 "[fallback4] withdrawRequestResponse=${withdrawRequestResponse}" + + echo "${withdrawRequestResponse}" | grep -qi "expired" + if [ "$?" -eq "0" ]; then + trace 1 "\n\n[fallback4] ${On_Red}${BBlack} Fallback 3: EXPIRED! ${Color_Off}\n" + return 1 + else + trace 2 "[fallback4] NOT EXPIRED, good!" + fi + + # Create bolt11 for LN Service LNURL Withdraw + local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') + local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') + local invoice=$(create_bolt11 "${msatoshi}" "${description}") + trace 3 "[fallback4] invoice=${invoice}" + local bolt11=$(echo ${invoice} | jq -r ".bolt11") + trace 3 "[fallback4] bolt11=${bolt11}" + + # We want to see that that invoice is unpaid first... + local status=$(get_invoice_status "${invoice}") + trace 3 "[fallback4] status=${status}" + + # 6. User calls LNServiceWithdraw -> fails because LN02 is down + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") + trace 3 "[fallback4] withdrawResponse=${withdrawResponse}" + + echo "${withdrawResponse}" | grep -qi "OK" + if [ "$?" -ne "0" ]; then + trace 1 "\n\n[fallback4] ${On_IGreen}${BBlack} Fallback 4: failed, good! ${Color_Off}\n" + else + trace 1 "\n\n[fallback4] ${On_Red}${BBlack} Fallback 4: Should have failed! ${Color_Off}\n" + return 1 + fi + + # Reconnecting the two LN instances... + ln_reconnect + + # 7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it + trace 2 "[fallback4] Calling ln_pay directly on Cyphernode..." + local data='{"bolt11":"'${bolt11}'"}' + local lnpay=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/ln_pay) + trace 3 "[fallback4] lnpay=${lnpay}" + lnpaystatus=$(echo "${lnpay}" | jq -r ".status") + trace 3 "[fallback4] lnpaystatus=${lnpaystatus}" + + start_callback_server + + # 8. Call forceFallback -> should check payment status and say it's already paid! + trace 3 "[fallback4] Forcing fallback..." + local force_lnurl_fallback=$(force_lnurl_fallback ${lnurl_withdraw_id}) + trace 3 "[fallback4] force_lnurl_fallback=${force_lnurl_fallback}" + + echo "${withdrawResponse}" | grep -qi "error" + if [ "$?" -eq "0" ]; then + trace 1 "\n\n[fallback4] ${On_IGreen}${BBlack} Fallback 4: failed, good! ${Color_Off}\n" + else + trace 1 "\n\n[fallback4] ${On_Red}${BBlack} Fallback 4: Should have failed! ${Color_Off}\n" + return 1 + fi + + wait +} + start_callback_server() { trace 1 "\n\n[start_callback_server] ${BCyan}Let's start a callback server!...${Color_Off}\n" @@ -844,7 +1005,11 @@ callbackservername="lnurl_withdraw_test" callbackserverport="1111" callbackurl="http://${callbackservername}:${callbackserverport}" -happy_path "${callbackurl}" \ +ln_reconnect + +# Let's start with fallback4 so that the LN02 shutdown from run_tests.sh runs and the timing fits... +fallback4 "${callbackservername}" "${callbackserverport}" \ +&& happy_path "${callbackurl}" \ && expired1 "${callbackurl}" \ && expired2 "${callbackurl}" \ && deleted1 "${callbackurl}" \ diff --git a/tests/run_tests.sh b/tests/run_tests.sh index bcc0a13..a3168c9 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -2,10 +2,9 @@ # Set the CHECK_EXPIRATION_TIMEOUT config to 1 for this test to be successful -./ln_reconnect.sh - docker run --rm -d --name lnurl_withdraw_test -v "$PWD:/tests" --network=cyphernodeappsnet alpine sh -c 'while true; do sleep 10; done' docker network connect cyphernodenet lnurl_withdraw_test -(sleep 90 ; ./mine.sh 1 0 ; sleep 90 ; ./mine.sh 1 0 ; sleep 30 ; ./mine.sh 1 0) & +(sleep 3 ; echo "** STOPPING LN02 **" ; docker stop $(docker ps -q -f "name=cyphernode_lightning2")) & +(sleep 90 ; ./mine.sh 1 0 ; sleep 90 ; ./mine.sh 1 0 ; sleep 90 ; ./mine.sh 1 0) & docker exec -it lnurl_withdraw_test /tests/lnurl_withdraw.sh docker stop lnurl_withdraw_test From 6aaf80b836dcda158a11bcf4440f4f317928e9aa Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 7 Oct 2021 01:24:11 -0400 Subject: [PATCH 28/52] Call webhook only when paid --- src/lib/LnurlWithdraw.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 89e9069..80e7e88 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -908,13 +908,16 @@ class LnurlWithdraw { paymentStatus.result ); // lnurlWithdrawEntity.withdrawnTs = new Date(); - lnurlWithdrawEntity.paid = true; + lnurlWithdrawEntity.paid = + paymentStatus.paymentStatus === "complete"; lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( lnurlWithdrawEntity ); - this.checkWebhook(lnurlWithdrawEntity); + if (paymentStatus.paymentStatus === "complete") { + this.checkWebhook(lnurlWithdrawEntity); + } } } From cd93757bf166ec1494afd75da7806e08631240a6 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 9 Nov 2021 14:28:32 -0500 Subject: [PATCH 29/52] Better tests --- tests/README.md | 37 ++- tests/ln_reconnect.sh | 12 + tests/ln_setup.sh | 2 + tests/lnurl_withdraw_wallet.sh | 23 ++ tests/mine.sh | 12 +- tests/run_tests.sh | 10 - ...url_withdraw.sh => test-lnurl-withdraw.sh} | 233 ++++++++++++------ 7 files changed, 225 insertions(+), 104 deletions(-) delete mode 100755 tests/run_tests.sh rename tests/{lnurl_withdraw.sh => test-lnurl-withdraw.sh} (81%) diff --git a/tests/README.md b/tests/README.md index eb2951d..27e9ffc 100644 --- a/tests/README.md +++ b/tests/README.md @@ -56,11 +56,11 @@ dist/apps/sparkwallet2/docker-compose.yaml: ```yaml cyphernode_sparkwallet2: command: --no-tls ${TOR_PARAMS} - image: cyphernode/sparkwallet:v0.2.17 + image: cyphernode/sparkwallet:v0.3.0 environment: - "NETWORK=${NETWORK}" volumes: - - "/yourCyphernodePath/dist/cyphernode/lightning2:/etc/lightning" + - "/yourCyphernodePath/dist/cyphernode/lightning2/${NETWORK}:/etc/lightning" - "$APP_SCRIPT_PATH/cookie:/data/spark/cookie" - "$GATEKEEPER_DATAPATH/htpasswd:/htpasswd/htpasswd" labels: @@ -102,15 +102,13 @@ dist/apps/sparkwallet2/docker-compose.yaml: ## Test LNURL-withdraw -Container `lightning` is used by Cyphernode and `lightning2` will be our user. +Make sure you're in regtest. -Run ./run_tests.sh or +Container `lightning` is used by Cyphernode and `lightning2` will be our user. -```bash -docker run --rm -it -v "$PWD:/tests" --network=cyphernodeappsnet alpine /tests/lnurl_withdraw.sh -``` +Run ./test-lnurl-withdraw.sh -lnurl_withdraw.sh will simulate real-world use cases: +test-lnurl-withdraw.sh will simulate real-world use cases: ### Happy path @@ -173,3 +171,26 @@ lnurl_withdraw.sh will simulate real-world use cases: 7. Fallback should be triggered, added to current batch using the Batcher 8. Wait for the batch to execute, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112), Batcher's execute callback called (port 1113) 9. Mined block and Cyphernode's confirmed watch callback called (port 1114) + +### fallback 3, force fallback + +1. Cyphernode.getnewaddress -> btcfallbackaddr +2. Cyphernode.watch btcfallbackaddr +3. Listen to watch webhook +4. Create a LNURL Withdraw with expiration=tomorrow and a btcfallbackaddr +5. Get it and compare +6. User calls LNServiceWithdrawRequest -> works, not expired! +7. Call forceFallback +8. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) +9. Mined block and Cyphernode's confirmed watch callback called (port 1113) + +### fallback 4, execute fallback on a bolt11 paid in background (lnurl app doesn't know it's been paid) + +1. Cyphernode.getnewaddress -> btcfallbackaddr +2. Create a LNURL Withdraw with expiration=tomorrow and a btcfallbackaddr +3. Get it and compare +4. User calls LNServiceWithdrawRequest -> works, not expired! +5. Shut down LN02 +6. User calls LNServiceWithdraw -> fails because LN02 is down +7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it +8. Call forceFallback -> should check payment status and say it's already paid! diff --git a/tests/ln_reconnect.sh b/tests/ln_reconnect.sh index 36c6ae5..94b673d 100755 --- a/tests/ln_reconnect.sh +++ b/tests/ln_reconnect.sh @@ -1,6 +1,18 @@ #!/bin/sh ln_reconnect() { + + # First ping the containers to make sure they're up... + docker run --rm -it --name ln-reconnecter --network cyphernodenet alpine sh -c ' + while true ; do ping -c 1 cyphernode_lightning ; [ "$?" -eq "0" ] && break ; sleep 5; done + while true ; do ping -c 1 cyphernode_lightning2 ; [ "$?" -eq "0" ] && break ; sleep 5; done + ' + + # Now check if the lightning nodes are ready to accept requests... + while true ; do docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo ; [ "$?" -eq "0" ] && break ; sleep 5; done + while true ; do docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo ; [ "$?" -eq "0" ] && break ; sleep 5; done + + # Ok, let's reconnect them! docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning") docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning2") } diff --git a/tests/ln_setup.sh b/tests/ln_setup.sh index 7957cfe..ac0fee8 100755 --- a/tests/ln_setup.sh +++ b/tests/ln_setup.sh @@ -1,5 +1,7 @@ #!/bin/sh +# This is a helper script to create a balanced channel between lightning and lightning2 + . ./mine.sh date diff --git a/tests/lnurl_withdraw_wallet.sh b/tests/lnurl_withdraw_wallet.sh index c91f958..500c7fd 100755 --- a/tests/lnurl_withdraw_wallet.sh +++ b/tests/lnurl_withdraw_wallet.sh @@ -1,5 +1,28 @@ #!/bin/sh +# +# This is a super basic LNURL-compatible wallet, command-line +# Useful to test LNURL on a regtest or testnet environment. +# + +# +# This script assumes you have lightning and lightning2 running. +# It will use lightning2 as the destination wallet (user's wallet) +# It will use lightning as the source wallet (service's wallet) +# +# If you don't have a lnurl and want to create one to call withdraw: +# +# ./lnurl_withdraw_wallet.sh createlnurl +# +# If you have a lnurl string and want to withdraw it to your lightning2 node: +# +# ./lnurl_withdraw_wallet.sh +# +# If you have a lnurl string and want to withdraw it to a specific bolt11: +# +# ./lnurl_withdraw_wallet.sh +# + . /tests/colors.sh trace() { diff --git a/tests/mine.sh b/tests/mine.sh index fc51b92..cf13b57 100755 --- a/tests/mine.sh +++ b/tests/mine.sh @@ -1,18 +1,18 @@ #!/bin/sh +# This needs to be run in regtest + +# This will mine n blocks. If n is not supplied, will mine 1 block. + # Mine mine() { local nbblocks=${1:-1} - local interactive=${2:-1} local minedaddr - if [ "${interactive}" = "1" ]; then - interactivearg="-it" - fi echo ; echo "About to mine ${nbblocks} block(s)..." - minedaddr=$(docker exec ${interactivearg} $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat getnewaddress | tr -d '\r') + minedaddr=$(docker exec -t $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat getnewaddress | tr -d '\r') echo ; echo "minedaddr=${minedaddr}" - docker exec ${interactivearg} $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat generatetoaddress ${nbblocks} "${minedaddr}" + docker exec -it $(docker ps -q -f "name=cyphernode_bitcoin") bitcoin-cli -rpcwallet=spending01.dat generatetoaddress ${nbblocks} "${minedaddr}" } case "${0}" in *mine.sh) mine $@;; esac diff --git a/tests/run_tests.sh b/tests/run_tests.sh deleted file mode 100755 index a3168c9..0000000 --- a/tests/run_tests.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -# Set the CHECK_EXPIRATION_TIMEOUT config to 1 for this test to be successful - -docker run --rm -d --name lnurl_withdraw_test -v "$PWD:/tests" --network=cyphernodeappsnet alpine sh -c 'while true; do sleep 10; done' -docker network connect cyphernodenet lnurl_withdraw_test -(sleep 3 ; echo "** STOPPING LN02 **" ; docker stop $(docker ps -q -f "name=cyphernode_lightning2")) & -(sleep 90 ; ./mine.sh 1 0 ; sleep 90 ; ./mine.sh 1 0 ; sleep 90 ; ./mine.sh 1 0) & -docker exec -it lnurl_withdraw_test /tests/lnurl_withdraw.sh -docker stop lnurl_withdraw_test diff --git a/tests/lnurl_withdraw.sh b/tests/test-lnurl-withdraw.sh similarity index 81% rename from tests/lnurl_withdraw.sh rename to tests/test-lnurl-withdraw.sh index 1e001a4..fe8918c 100755 --- a/tests/lnurl_withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -1,6 +1,9 @@ #!/bin/sh -# Happy path: +# This needs to be run in regtest +# You need jq installed for these tests to run correctly + +# Happy path # 1. Create a LNURL Withdraw # 2. Get it and compare @@ -9,13 +12,13 @@ # 5. User calls LNServiceWithdraw with wrong k1 -> Error, wrong k1! # 6. User calls LNServiceWithdraw -# Expired 1: +# Expired 1 # 1. Create a LNURL Withdraw with expiration=now # 2. Get it and compare # 3. User calls LNServiceWithdrawRequest -> Error, expired! -# Expired 2: +# Expired 2 # 1. Create a LNURL Withdraw with expiration=now + 5 seconds # 2. Get it and compare @@ -23,7 +26,7 @@ # 4. Sleep 5 seconds # 5. User calls LNServiceWithdraw -> Error, expired! -# Deleted 1: +# Deleted 1 # 1. Create a LNURL Withdraw with expiration=now # 2. Get it and compare @@ -31,7 +34,7 @@ # 4. Get it and compare # 5. User calls LNServiceWithdrawRequest -> Error, deleted! -# Deleted 2: +# Deleted 2 # 1. Create a LNURL Withdraw with expiration=now + 5 seconds # 2. Get it and compare @@ -39,7 +42,7 @@ # 4. Delete it # 5. User calls LNServiceWithdraw -> Error, deleted! -# fallback 1, use of Bitcoin fallback address: +# fallback 1, use of Bitcoin fallback address # 1. Cyphernode.getnewaddress -> btcfallbackaddr # 2. Cyphernode.watch btcfallbackaddr @@ -50,7 +53,7 @@ # 7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) # 8. Mined block and Cyphernode's confirmed watch callback called (port 1113) -# fallback 2, use of Bitcoin fallback address in a batched spend: +# fallback 2, use of Bitcoin fallback address in a batched spend # 1. Cyphernode.getnewaddress -> btcfallbackaddr # 2. Cyphernode.watch btcfallbackaddr @@ -71,8 +74,8 @@ # 5. Get it and compare # 6. User calls LNServiceWithdrawRequest -> works, not expired! # 7. Call forceFallback -# 7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) -# 8. Mined block and Cyphernode's confirmed watch callback called (port 1113) +# 8. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) +# 9. Mined block and Cyphernode's confirmed watch callback called (port 1113) # fallback 4, execute fallback on a bolt11 paid in background (lnurl app doesn't know it's been paid) @@ -86,45 +89,67 @@ # 8. Call forceFallback -> should check payment status and say it's already paid! -. ./tests/colors.sh +. ./colors.sh +. ./mine.sh +. ./ln_reconnect.sh trace() { if [ "${1}" -le "${TRACING}" ]; then - local str="$(date -Is) $$ ${2}" - echo -e "${str}" >&2 + echo "$(date -u +%FT%TZ) ${2}" 1>&2 fi } -ln_reconnect() { +start_test_container() { + docker run -d --rm -it --name tests-lnurl-withdraw --network=cyphernodeappsnet alpine + docker network connect cyphernodenet tests-lnurl-withdraw +} + +stop_test_container() { + trace 1 "\n\n[stop_test_container] ${BCyan}Stopping existing containers if they are running...${Color_Off}\n" + + local containers=$(docker ps -q -f "name=tests-lnurl-withdraw") + if [ -n "${containers}" ]; then + docker stop $(docker ps -q -f "name=tests-lnurl-withdraw") + fi +} + +exec_in_test_container() { + docker exec -it tests-lnurl-withdraw "$@" | tr -d '\r\n' +} + +exec_in_test_container_leave_lf() { + docker exec -it tests-lnurl-withdraw "$@" +} + +ln_reconnect_with_sparkwallet() { trace 2 "\n\n[ln_reconnect] ${BCyan}Reconnecting the two LN instances...${Color_Off}\n" - (while true ; do ping -c 1 cyphernode_lightning ; [ "$?" -eq "0" ] && break ; sleep 5; done) & - (while true ; do ping -c 1 cyphernode_lightning2 ; [ "$?" -eq "0" ] && break ; sleep 5; done) & - wait + exec_in_test_container_leave_lf sh -c 'while true ; do ping -c 1 cyphernode_lightning ; [ "$?" -eq "0" ] && break ; sleep 5; done' + exec_in_test_container_leave_lf sh -c 'while true ; do ping -c 1 cyphernode_lightning2 ; [ "$?" -eq "0" ] && break ; sleep 5; done' - local data='{"id":1,"jsonrpc":"2.0","method":"getinfo"}' + local data='{"id":1,"jsonrpc":"2.0","method":"getinfo","params":[]}' trace 3 "[ln_reconnect] data=${data}" - local getinfo1=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet:9737/rpc) + local getinfo1=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet:9737/rpc) trace 3 "[ln_reconnect] getinfo1=${getinfo1}" local id1=$(echo "${getinfo1}" | jq -r ".id") trace 3 "[ln_reconnect] id1=${id1}" - data='{"id":1,"jsonrpc":"2.0","method":"getinfo"}' + data='{"id":1,"jsonrpc":"2.0","method":"getinfo","params":[]}' trace 3 "[ln_reconnect] data=${data}" - local getinfo2=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + local getinfo2=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) trace 3 "[ln_reconnect] getinfo2=${getinfo2}" local id2=$(echo "${getinfo2}" | jq -r ".id") trace 3 "[ln_reconnect] id2=${id2}" - data='{"id":1,"jsonrpc":"2.0","method":"connect","params":{"id":"'${id2}'@lightning2"}}' + data='{"id":1,"jsonrpc":"2.0","method":"connect","params":["'${id2}'@lightning2"]}' trace 3 "[ln_reconnect] data=${data}" - local connect=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet:9737/rpc) - trace 3 "[create_bolt11] connect=${connect}" + local connect=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet:9737/rpc) + trace 3 "[ln_reconnect] connect=${connect}" - data='{"id":1,"jsonrpc":"2.0","method":"connect","params":{"id":"'${id1}'@lightning"}}' + data='{"id":1,"jsonrpc":"2.0","method":"connect","params":["'${id1}'@lightning"]}' trace 3 "[ln_reconnect] data=${data}" - local connect2=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) - trace 3 "[create_bolt11] connect2=${connect2}" + local connect2=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 3 "[ln_reconnect] connect2=${connect2}" } create_lnurl_withdraw() { @@ -137,7 +162,7 @@ create_lnurl_withdraw() { local msatoshi=$((500000+${invoicenumber})) trace 3 "[create_lnurl_withdraw] msatoshi=${msatoshi}" local expiration_offset=${2:-0} - local expiration=$(date -d @$(($(date -u +"%s")+${expiration_offset})) +"%Y-%m-%dT%H:%M:%SZ") + local expiration=$(exec_in_test_container date -d @$(($(date -u +"%s")+${expiration_offset})) +"%Y-%m-%dT%H:%M:%SZ") trace 3 "[create_lnurl_withdraw] expiration=${expiration}" local fallback_addr=${4:-""} local fallback_batched=${5:-"false"} @@ -146,7 +171,7 @@ create_lnurl_withdraw() { data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiresAt":"'${expiration}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'","btcFallbackAddress":"'${fallback_addr}'","batchFallback":'${fallback_batched}'}}' trace 3 "[create_lnurl_withdraw] data=${data}" trace 3 "[create_lnurl_withdraw] Calling createLnurlWithdraw..." - local createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + local createLnurlWithdraw=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[create_lnurl_withdraw] createLnurlWithdraw=${createLnurlWithdraw}" # {"id":0,"result":{"msatoshi":100000000,"description":"desc01","expiresAt":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} @@ -166,7 +191,7 @@ get_lnurl_withdraw() { data='{"id":0,"method":"getLnurlWithdraw","params":{"lnurlWithdrawId":'${lnurl_withdraw_id}'}}' trace 3 "[get_lnurl_withdraw] data=${data}" trace 3 "[get_lnurl_withdraw] Calling getLnurlWithdraw..." - local getLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + local getLnurlWithdraw=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[get_lnurl_withdraw] getLnurlWithdraw=${getLnurlWithdraw}" echo "${getLnurlWithdraw}" @@ -182,7 +207,7 @@ delete_lnurl_withdraw() { data='{"id":0,"method":"deleteLnurlWithdraw","params":{"lnurlWithdrawId":'${lnurl_withdraw_id}'}}' trace 3 "[delete_lnurl_withdraw] data=${data}" trace 3 "[delete_lnurl_withdraw] Calling deleteLnurlWithdraw..." - local deleteLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + local deleteLnurlWithdraw=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[delete_lnurl_withdraw] deleteLnurlWithdraw=${deleteLnurlWithdraw}" local deleted=$(echo "${deleteLnurlWithdraw}" | jq ".result.deleted") @@ -204,11 +229,17 @@ force_lnurl_fallback() { data='{"id":0,"method":"forceFallback","params":{"lnurlWithdrawId":'${lnurl_withdraw_id}'}}' trace 3 "[force_lnurl_fallback] data=${data}" trace 3 "[force_lnurl_fallback] Calling forceFallback..." - local forceFallback=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + local forceFallback=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[force_lnurl_fallback] forceFallback=${forceFallback}" - local expiresAt=$(echo "${forceFallback}" | jq ".result.expiresAt") - if [ "${expiresAt}" -ge "$(date -u +"%s")" ]; then + local expiresAt=$(echo "${forceFallback}" | jq -r ".result.expiresAt") + trace 3 "[force_lnurl_fallback] expiresAt=${expiresAt}" + # 2021-11-04T20:55:13.720Z + local expiresAtEpoch=$(exec_in_test_container date -d "${expiresAt}" -D "%Y-%m-%dT%H:%M:%S" +"%s") + trace 3 "[force_lnurl_fallback] expiresAtEpoch=${expiresAtEpoch}" + local currentEpoch=$(exec_in_test_container date -u +"%s") + trace 3 "[force_lnurl_fallback] currentEpoch=${currentEpoch}" + if [ "${expiresAtEpoch}" -ge "${currentEpoch}" ]; then trace 2 "[force_lnurl_fallback] ${On_Red}${BBlack} NOT EXPIRED! ${Color_Off}" return 1 fi @@ -223,7 +254,7 @@ decode_lnurl() { local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' trace 3 "[decode_lnurl] data=${data}" - local decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + local decodedLnurl=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[decode_lnurl] decodedLnurl=${decodedLnurl}" local url=$(echo "${decodedLnurl}" | jq -r ".result") trace 3 "[decode_lnurl] url=${url}" @@ -237,7 +268,7 @@ call_lnservice_withdraw_request() { local url=${1} trace 2 "[call_lnservice_withdraw_request] url=${url}" - local withdrawRequestResponse=$(curl -s ${url}) + local withdrawRequestResponse=$(exec_in_test_container curl -s ${url}) trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" @@ -253,7 +284,7 @@ create_bolt11() { local data='{"id":1,"jsonrpc":"2.0","method":"invoice","params":{"msatoshi":'${msatoshi}',"label":"'${desc}'","description":"'${desc}'"}}' trace 3 "[create_bolt11] data=${data}" - local invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + local invoice=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) trace 3 "[create_bolt11] invoice=${invoice}" echo "${invoice}" @@ -269,7 +300,7 @@ get_invoice_status() { trace 3 "[get_invoice_status] payment_hash=${payment_hash}" local data='{"id":1,"jsonrpc":"2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' trace 3 "[get_invoice_status] data=${data}" - local invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + local invoices=$(exec_in_test_container curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) trace 3 "[get_invoice_status] invoices=${invoices}" local status=$(echo "${invoices}" | jq -r ".invoices[0].status") trace 3 "[get_invoice_status] status=${status}" @@ -291,8 +322,8 @@ call_lnservice_withdraw() { trace 2 "[call_lnservice_withdraw] k1=${k1}" trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" - trace 2 "url=${callback}?k1=${k1}\&pr=${bolt11}" - withdrawResponse=$(curl -s ${callback}?k1=${k1}\&pr=${bolt11}) + trace 2 "[call_lnservice_withdraw] url=${callback}?k1=${k1}\&pr=${bolt11}" + withdrawResponse=$(exec_in_test_container curl -s ${callback}?k1=${k1}\&pr=${bolt11}) trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" @@ -356,6 +387,8 @@ happy_path() { local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[happy_path] withdrawResponse=${withdrawResponse}" + trace 2 "\n\n[fallback2] ${BPurple}Waiting for the LNURL payment callback...\n${Color_Off}" + wait # We want to see if payment received (invoice status paid) @@ -642,19 +675,19 @@ fallback1() { local zeroconfport=$((${callbackserverport}+1)) local oneconfport=$((${callbackserverport}+2)) - local callbackurlCnWatch0conf="http://${callbackservername}:${zeroconfport}" - local callbackurlCnWatch1conf="http://${callbackservername}:${oneconfport}" + local callbackurlCnWatch0conf="http://${callbackservername}2:${zeroconfport}" + local callbackurlCnWatch1conf="http://${callbackservername}3:${oneconfport}" local callbackurl="http://${callbackservername}:${callbackserverport}" # Get new address local data='{"label":"lnurl_fallback_test"}' - local btcfallbackaddr=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) + local btcfallbackaddr=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) btcfallbackaddr=$(echo "${btcfallbackaddr}" | jq -r ".address") trace 3 "[fallback1] btcfallbackaddr=${btcfallbackaddr}" # Watch the address data='{"address":"'${btcfallbackaddr}'","unconfirmedCallbackURL":"'${callbackurlCnWatch0conf}'/callback0conf","confirmedCallbackURL":"'${callbackurlCnWatch1conf}'/callback1conf"}' - local watchresponse=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) + local watchresponse=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) trace 3 "[fallback1] watchresponse=${watchresponse}" # Service creates LNURL Withdraw @@ -680,8 +713,7 @@ fallback1() { trace 3 "[fallback1] serviceUrl=${serviceUrl}" start_callback_server - start_callback_server ${zeroconfport} - start_callback_server ${oneconfport} + start_callback_server ${zeroconfport} 2 # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") @@ -695,8 +727,15 @@ fallback1() { trace 2 "[fallback1] EXPIRED!" fi - trace 3 "[fallback1] Waiting for fallback execution and a block mined..." + trace 2 "\n\n[fallback1] ${BPurple}Waiting for fallback execution, fallback callback and the 0-conf callback...${Color_Off}\n" + + wait + + start_callback_server ${oneconfport} 3 + + trace 2 "\n\n[fallback1] ${BPurple}Waiting for the 1-conf callback...${Color_Off}\n" + mine wait trace 1 "\n\n[fallback1] ${On_IGreen}${BBlack} Fallback 1: SUCCESS! ${Color_Off}\n" @@ -722,19 +761,19 @@ fallback2() { local zeroconfport=$((${callbackserverport}+1)) local oneconfport=$((${callbackserverport}+2)) - local callbackurlCnWatch0conf="http://${callbackservername}:${zeroconfport}" - local callbackurlCnWatch1conf="http://${callbackservername}:${oneconfport}" + local callbackurlCnWatch0conf="http://${callbackservername}2:${zeroconfport}" + local callbackurlCnWatch1conf="http://${callbackservername}3:${oneconfport}" local callbackurl="http://${callbackservername}:${callbackserverport}" # Get new address local data='{"label":"lnurl_fallback_test"}' - local btcfallbackaddr=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) + local btcfallbackaddr=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) btcfallbackaddr=$(echo "${btcfallbackaddr}" | jq -r ".address") trace 3 "[fallback2] btcfallbackaddr=${btcfallbackaddr}" # Watch the address data='{"address":"'${btcfallbackaddr}'","unconfirmedCallbackURL":"'${callbackurlCnWatch0conf}'/callback0conf","confirmedCallbackURL":"'${callbackurlCnWatch1conf}'/callback1conf"}' - local watchresponse=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) + local watchresponse=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) trace 3 "[fallback2] watchresponse=${watchresponse}" # Service creates LNURL Withdraw with batching true @@ -774,19 +813,25 @@ fallback2() { trace 2 "[fallback2] EXPIRED!" fi - trace 3 "[fallback2] Waiting for fallback batched callback..." + trace 2 "\n\n[fallback2] ${BPurple}Waiting for fallback batched callback...\n${Color_Off}" wait # fallback paid callback start_callback_server # 0-conf callback - start_callback_server ${zeroconfport} + start_callback_server ${zeroconfport} 2 + + trace 2 "\n\n[fallback2] ${BPurple}Waiting for fallback execution and the 0-conf callback...\n${Color_Off}" + + wait + # 1-conf callback - start_callback_server ${oneconfport} + start_callback_server ${oneconfport} 3 - trace 3 "[fallback2] Waiting for fallback execution and a block mined..." + trace 2 "\n\n[fallback2] ${BPurple}Waiting for the 1-conf callback...\n${Color_Off}" + mine wait trace 1 "\n\n[fallback2] ${On_IGreen}${BBlack} Fallback 2: SUCCESS! ${Color_Off}\n" @@ -802,8 +847,8 @@ fallback3() { # 5. Get it and compare # 6. User calls LNServiceWithdrawRequest -> works, not expired! # 7. Call forceFallback - # 7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) - # 8. Mined block and Cyphernode's confirmed watch callback called (port 1113) + # 8. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) + # 9. Mined block and Cyphernode's confirmed watch callback called (port 1113) trace 1 "\n\n[fallback3] ${On_Yellow}${BBlack} Fallback 3: ${Color_Off}\n" @@ -812,19 +857,19 @@ fallback3() { local zeroconfport=$((${callbackserverport}+1)) local oneconfport=$((${callbackserverport}+2)) - local callbackurlCnWatch0conf="http://${callbackservername}:${zeroconfport}" - local callbackurlCnWatch1conf="http://${callbackservername}:${oneconfport}" + local callbackurlCnWatch0conf="http://${callbackservername}2:${zeroconfport}" + local callbackurlCnWatch1conf="http://${callbackservername}3:${oneconfport}" local callbackurl="http://${callbackservername}:${callbackserverport}" # Get new address local data='{"label":"lnurl_fallback_test"}' - local btcfallbackaddr=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) + local btcfallbackaddr=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) btcfallbackaddr=$(echo "${btcfallbackaddr}" | jq -r ".address") trace 3 "[fallback3] btcfallbackaddr=${btcfallbackaddr}" # Watch the address data='{"address":"'${btcfallbackaddr}'","unconfirmedCallbackURL":"'${callbackurlCnWatch0conf}'/callback0conf","confirmedCallbackURL":"'${callbackurlCnWatch1conf}'/callback1conf"}' - local watchresponse=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) + local watchresponse=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/watch) trace 3 "[fallback3] watchresponse=${watchresponse}" # Service creates LNURL Withdraw @@ -850,8 +895,7 @@ fallback3() { trace 3 "[fallback3] serviceUrl=${serviceUrl}" start_callback_server - start_callback_server ${zeroconfport} - start_callback_server ${oneconfport} + start_callback_server ${zeroconfport} 2 # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") @@ -869,10 +913,17 @@ fallback3() { local force_lnurl_fallback=$(force_lnurl_fallback ${lnurl_withdraw_id}) trace 3 "[fallback3] force_lnurl_fallback=${force_lnurl_fallback}" - trace 3 "[fallback3] Waiting for fallback execution and a block mined..." + trace 2 "\n\n[fallback3] ${BPurple}Waiting for fallback execution and the 0-conf callback...\n${Color_Off}" wait + start_callback_server ${oneconfport} 3 + + trace 2 "\n\n[fallback3] ${BPurple}Waiting for the 1-conf callback...\n${Color_Off}" + + mine + wait + trace 1 "\n\n[fallback3] ${On_IGreen}${BBlack} Fallback 3: SUCCESS! ${Color_Off}\n" } @@ -883,7 +934,7 @@ fallback4() { # 4. User calls LNServiceWithdrawRequest -> works, not expired! # 5. Shut down LN02 # 6. User calls LNServiceWithdraw -> fails because LN02 is down - # 7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it + # 7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it (simulate delayed LN payment) # 8. Call forceFallback -> should check payment status and say it's already paid! trace 1 "\n\n[fallback4] ${On_Yellow}${BBlack} Fallback 4: ${Color_Off}\n" @@ -892,7 +943,7 @@ fallback4() { # Get new address local data='{"label":"lnurl_fallback_test"}' - local btcfallbackaddr=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) + local btcfallbackaddr=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/getnewaddress) btcfallbackaddr=$(echo "${btcfallbackaddr}" | jq -r ".address") trace 3 "[fallback4] btcfallbackaddr=${btcfallbackaddr}" @@ -941,8 +992,18 @@ fallback4() { # We want to see that that invoice is unpaid first... local status=$(get_invoice_status "${invoice}") trace 3 "[fallback4] status=${status}" + if [ "${status}" != "unpaid" ]; then + trace 1 "\n\n[fallback4] ${On_Red}${BBlack} fallback4: Invoice status should be unpaid! ${Color_Off}\n" + date + return 1 + else + trace 1 "\n\n[fallback4] ${On_IGreen}${BBlack} fallback4: Status unpaid! Good! ${Color_Off}\n" + fi # 6. User calls LNServiceWithdraw -> fails because LN02 is down + trace 3 "[fallback4] Shutting down lightning2..." + docker stop $(docker ps -q -f "name=lightning2") + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[fallback4] withdrawResponse=${withdrawResponse}" @@ -957,10 +1018,10 @@ fallback4() { # Reconnecting the two LN instances... ln_reconnect - # 7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it + # 7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it (simulate delayed LN payment) trace 2 "[fallback4] Calling ln_pay directly on Cyphernode..." local data='{"bolt11":"'${bolt11}'"}' - local lnpay=$(curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/ln_pay) + local lnpay=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" proxy:8888/ln_pay) trace 3 "[fallback4] lnpay=${lnpay}" lnpaystatus=$(echo "${lnpay}" | jq -r ".status") trace 3 "[fallback4] lnpaystatus=${lnpaystatus}" @@ -980,6 +1041,8 @@ fallback4() { return 1 fi + trace 2 "\n\n[fallback4] ${BPurple}Waiting for fallback execution callback...\n${Color_Off}" + wait } @@ -987,35 +1050,45 @@ start_callback_server() { trace 1 "\n\n[start_callback_server] ${BCyan}Let's start a callback server!...${Color_Off}\n" port=${1:-${callbackserverport}} - nc -vlp${port} -e sh -c 'echo -en "HTTP/1.1 200 OK\\r\\n\\r\\n" ; echo -en "'${On_Black}${White}'" >&2 ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo -e "'${Color_Off}'" >&2' & + conainerseq=${2} + + docker run --rm -t --name tests-lnurl-withdraw-cb${conainerseq} --network=cyphernodeappsnet alpine sh -c \ + "nc -vlp${port} -e sh -c 'echo -en \"HTTP/1.1 200 OK\\\\r\\\\n\\\\r\\\\n\" ; echo -en \"\\033[40m\\033[0;37m\" >&2 ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo -e \"\033[0m\" >&2'" & + sleep 2 + # docker network connect cyphernodenet tests-lnurl-withdraw-cb${conainerseq} } TRACING=3 -trace 1 "${Color_Off}" date -# Install needed packages -trace 2 "\n\n${BCyan}Installing needed packages...${Color_Off}\n" -apk add curl jq +stop_test_container +start_test_container -# Initializing test variables -trace 2 "\n\n${BCyan}Initializing test variables...${Color_Off}\n" -callbackservername="lnurl_withdraw_test" callbackserverport="1111" +callbackservername="tests-lnurl-withdraw-cb" callbackurl="http://${callbackservername}:${callbackserverport}" +trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}Installing needed packages...${Color_Off}\n" +exec_in_test_container_leave_lf apk add --update curl + ln_reconnect -# Let's start with fallback4 so that the LN02 shutdown from run_tests.sh runs and the timing fits... -fallback4 "${callbackservername}" "${callbackserverport}" \ -&& happy_path "${callbackurl}" \ +happy_path "${callbackurl}" \ && expired1 "${callbackurl}" \ && expired2 "${callbackurl}" \ && deleted1 "${callbackurl}" \ && deleted2 "${callbackurl}" \ && fallback1 "${callbackservername}" "${callbackserverport}" \ && fallback2 "${callbackservername}" "${callbackserverport}" \ -&& fallback3 "${callbackservername}" "${callbackserverport}" +&& fallback3 "${callbackservername}" "${callbackserverport}" \ +&& fallback4 "${callbackservername}" "${callbackserverport}" + +trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}Tearing down...${Color_Off}\n" +wait + +stop_test_container + +date -trace 1 "\n\n${BCyan}Finished, deleting this test container...${Color_Off}\n" +trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}See ya!${Color_Off}\n" From 2ab684473a7d3f9948d14c505ac1f10914379233 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 10 Nov 2021 21:19:20 -0500 Subject: [PATCH 30/52] Fixed tests --- tests/colors.sh | 1 - tests/ln_reconnect.sh | 2 +- tests/ln_setup.sh | 2 +- tests/lnurl_withdraw_wallet.sh | 2 ++ tests/mine.sh | 2 +- tests/run_lnurl_withdraw_wallet.sh | 2 +- tests/startcallbackserver.sh | 2 +- tests/test-lnurl-withdraw.sh | 49 +++++++++++++++++++++--------- 8 files changed, 41 insertions(+), 21 deletions(-) diff --git a/tests/colors.sh b/tests/colors.sh index fd78e7d..9978103 100644 --- a/tests/colors.sh +++ b/tests/colors.sh @@ -1,4 +1,3 @@ -#!/bin/sh # Reset Color_Off='\033[0m' # Text Reset diff --git a/tests/ln_reconnect.sh b/tests/ln_reconnect.sh index 94b673d..d3a7143 100755 --- a/tests/ln_reconnect.sh +++ b/tests/ln_reconnect.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash ln_reconnect() { diff --git a/tests/ln_setup.sh b/tests/ln_setup.sh index ac0fee8..379d7c7 100755 --- a/tests/ln_setup.sh +++ b/tests/ln_setup.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # This is a helper script to create a balanced channel between lightning and lightning2 diff --git a/tests/lnurl_withdraw_wallet.sh b/tests/lnurl_withdraw_wallet.sh index 500c7fd..5d9ce5b 100755 --- a/tests/lnurl_withdraw_wallet.sh +++ b/tests/lnurl_withdraw_wallet.sh @@ -1,5 +1,7 @@ #!/bin/sh +# +# Must be run using run_lnurl_withdraw_wallet.sh # # This is a super basic LNURL-compatible wallet, command-line # Useful to test LNURL on a regtest or testnet environment. diff --git a/tests/mine.sh b/tests/mine.sh index cf13b57..90e1fae 100755 --- a/tests/mine.sh +++ b/tests/mine.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # This needs to be run in regtest diff --git a/tests/run_lnurl_withdraw_wallet.sh b/tests/run_lnurl_withdraw_wallet.sh index 7ea69eb..b86ccb4 100755 --- a/tests/run_lnurl_withdraw_wallet.sh +++ b/tests/run_lnurl_withdraw_wallet.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash #./startcallbackserver.sh & diff --git a/tests/startcallbackserver.sh b/tests/startcallbackserver.sh index cdcd1fe..3526641 100755 --- a/tests/startcallbackserver.sh +++ b/tests/startcallbackserver.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash date diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index fe8918c..509f044 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # This needs to be run in regtest # You need jq installed for these tests to run correctly @@ -95,7 +95,7 @@ trace() { if [ "${1}" -le "${TRACING}" ]; then - echo "$(date -u +%FT%TZ) ${2}" 1>&2 + echo -e "$(date -u +%FT%TZ) ${2}" 1>&2 fi } @@ -234,6 +234,12 @@ force_lnurl_fallback() { local expiresAt=$(echo "${forceFallback}" | jq -r ".result.expiresAt") trace 3 "[force_lnurl_fallback] expiresAt=${expiresAt}" + if [ "${expiresAt}" = "null" ]; then + trace 2 "[force_lnurl_fallback] No expiresAt field, could be normal but forceFallback failed..." + echo "${forceFallback}" + return 1 + fi + # 2021-11-04T20:55:13.720Z local expiresAtEpoch=$(exec_in_test_container date -d "${expiresAt}" -D "%Y-%m-%dT%H:%M:%S" +"%s") trace 3 "[force_lnurl_fallback] expiresAtEpoch=${expiresAtEpoch}" @@ -383,12 +389,12 @@ happy_path() { start_callback_server + trace 2 "\n\n[fallback2] ${BPurple}Waiting for the LNURL payment callback...\n${Color_Off}" + # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[happy_path] withdrawResponse=${withdrawResponse}" - trace 2 "\n\n[fallback2] ${BPurple}Waiting for the LNURL payment callback...\n${Color_Off}" - wait # We want to see if payment received (invoice status paid) @@ -715,6 +721,8 @@ fallback1() { start_callback_server start_callback_server ${zeroconfport} 2 + trace 2 "\n\n[fallback1] ${BPurple}Waiting for fallback execution, fallback callback and the 0-conf callback...${Color_Off}\n" + # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[fallback1] withdrawRequestResponse=${withdrawRequestResponse}" @@ -727,8 +735,6 @@ fallback1() { trace 2 "[fallback1] EXPIRED!" fi - trace 2 "\n\n[fallback1] ${BPurple}Waiting for fallback execution, fallback callback and the 0-conf callback...${Color_Off}\n" - wait start_callback_server ${oneconfport} 3 @@ -801,6 +807,8 @@ fallback2() { # fallback batched callback start_callback_server + trace 2 "\n\n[fallback2] ${BPurple}Waiting for fallback batched callback...\n${Color_Off}" + # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[fallback2] withdrawRequestResponse=${withdrawRequestResponse}" @@ -813,8 +821,6 @@ fallback2() { trace 2 "[fallback2] EXPIRED!" fi - trace 2 "\n\n[fallback2] ${BPurple}Waiting for fallback batched callback...\n${Color_Off}" - wait # fallback paid callback @@ -909,11 +915,16 @@ fallback3() { trace 2 "[fallback3] NOT EXPIRED, good!" fi + trace 2 "\n\n[fallback3] ${BPurple}Waiting for fallback execution and the 0-conf callback...\n${Color_Off}" + trace 3 "[fallback3] Forcing fallback..." - local force_lnurl_fallback=$(force_lnurl_fallback ${lnurl_withdraw_id}) + local force_lnurl_fallback + force_lnurl_fallback=$(force_lnurl_fallback ${lnurl_withdraw_id}) + if [ "$?" -ne "0" ]; then + trace 1 "\n\n[fallback3] ${On_Red}${BBlack} forceFallback failed! ${Color_Off}\n" + return 1 + fi trace 3 "[fallback3] force_lnurl_fallback=${force_lnurl_fallback}" - - trace 2 "\n\n[fallback3] ${BPurple}Waiting for fallback execution and the 0-conf callback...\n${Color_Off}" wait @@ -1028,10 +1039,20 @@ fallback4() { start_callback_server + trace 2 "\n\n[fallback4] ${BPurple}Waiting for fallback execution callback...\n${Color_Off}" + # 8. Call forceFallback -> should check payment status and say it's already paid! trace 3 "[fallback4] Forcing fallback..." - local force_lnurl_fallback=$(force_lnurl_fallback ${lnurl_withdraw_id}) - trace 3 "[fallback4] force_lnurl_fallback=${force_lnurl_fallback}" + local force_lnurl_fallback + force_lnurl_fallback=$(force_lnurl_fallback ${lnurl_withdraw_id}) + if [ "$?" -ne "0" ]; then + trace 3 "[fallback4] force_lnurl_fallback=${force_lnurl_fallback}" + trace 1 "\n\n[fallback4] ${On_IGreen}${BBlack} forceFallback failed, good! ${Color_Off}\n" + else + trace 3 "[fallback4] force_lnurl_fallback=${force_lnurl_fallback}" + trace 1 "\n\n[fallback4] ${On_Red}${BBlack} forceFallback should have failed! ${Color_Off}\n" + return 1 + fi echo "${withdrawResponse}" | grep -qi "error" if [ "$?" -eq "0" ]; then @@ -1041,8 +1062,6 @@ fallback4() { return 1 fi - trace 2 "\n\n[fallback4] ${BPurple}Waiting for fallback execution callback...\n${Color_Off}" - wait } From 80010e37510628956119de4deb1ae5ef601cabec Mon Sep 17 00:00:00 2001 From: kexkey Date: Sun, 14 Nov 2021 22:24:53 -0500 Subject: [PATCH 31/52] prisma on arm32, small fixes in tests --- Dockerfile | 54 +++++++++++++++++++++++++++---- docker-build.sh | 63 ++++++++++++++++++++++++++++++++++++ package.json | 4 +-- prisma/schema.prisma | 1 + tests/ln_setup.sh | 17 ++++++++-- tests/test-lnurl-withdraw.sh | 37 ++------------------- 6 files changed, 129 insertions(+), 47 deletions(-) create mode 100755 docker-build.sh diff --git a/Dockerfile b/Dockerfile index c0b75bd..c377bf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,61 @@ -FROM node:14.11.0-alpine3.11 as build-base +# arm32 for arm32 +ARG ARCH="all" + +#-------------------------------------------------------------- + +FROM node:17.1-bullseye-slim as build-base-all WORKDIR /lnurl +RUN apt-get update && apt-get install -y openssl + COPY package.json /lnurl -RUN apk add --update --no-cache --virtual .gyp \ - python \ - make \ - g++ RUN npm install #-------------------------------------------------------------- -FROM node:14.11.0-alpine3.11 +# Prisma team officially doesn't support arm32 engines binaries. +# pantharshit00 put together what's needed to build them: +# https://github.com/prisma/prisma/issues/5379#issuecomment-843961332 +# https://github.com/pantharshit00/prisma-rpi-builds + +# We will be using those. + +# This stage will only be used when building the image with build-arg ARCH=arm32 + +FROM node:17.1-bullseye-slim as build-base-arm32 + WORKDIR /lnurl -COPY --from=build-base /lnurl/node_modules/ /lnurl/node_modules/ +RUN apt-get update && apt-get install -y openssl wget + +COPY --from=build-base-all /lnurl/node_modules/ /lnurl/node_modules/ + +RUN wget --quiet -O /lnurl/node_modules/@prisma/engines/introspection-engine https://github.com/pantharshit00/prisma-rpi-builds/releases/download/3.2.1/introspection-engine \ + && wget --quiet -O /lnurl/node_modules/@prisma/engines/libquery_engine.so https://github.com/pantharshit00/prisma-rpi-builds/releases/download/3.2.1/libquery_engine.so \ + && wget --quiet -O /lnurl/node_modules/@prisma/engines/migration-engine https://github.com/pantharshit00/prisma-rpi-builds/releases/download/3.2.1/migration-engine \ + && wget --quiet -O /lnurl/node_modules/@prisma/engines/prisma-fmt https://github.com/pantharshit00/prisma-rpi-builds/releases/download/3.2.1/prisma-fmt \ + && wget --quiet -O /lnurl/node_modules/@prisma/engines/query-engine https://github.com/pantharshit00/prisma-rpi-builds/releases/download/3.2.1/query-engine + +RUN cd /lnurl/node_modules/@prisma/engines/ \ + && chmod +x introspection-engine migration-engine prisma-fmt query-engine + +ENV PRISMA_QUERY_ENGINE_BINARY=/lnurl/node_modules/@prisma/engines/query-engine +ENV PRISMA_MIGRATION_ENGINE_BINARY=/lnurl/node_modules/@prisma/engines/migration-engine +ENV PRISMA_INTROSPECTION_ENGINE_BINARY=/lnurl/node_modules/@prisma/engines/introspection-engine +ENV PRISMA_FMT_BINARY=/lnurl/node_modules/@prisma/engines/prisma-fmt +ENV PRISMA_QUERY_ENGINE_LIBRARY=/lnurl/node_modules/@prisma/engines/libquery_engine.so +ENV PRISMA_CLI_QUERY_ENGINE_TYPE=binary +ENV PRISMA_QUERY_ENGINE_TYPE=binary + +#-------------------------------------------------------------- + +FROM build-base-${ARCH} + +ENV PRISMA_CLI_QUERY_ENGINE_TYPE=binary +ENV PRISMA_QUERY_ENGINE_TYPE=binary + COPY package.json /lnurl COPY tsconfig.json /lnurl COPY prisma /lnurl/prisma diff --git a/docker-build.sh b/docker-build.sh new file mode 100755 index 0000000..6fe139f --- /dev/null +++ b/docker-build.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# Must be logged to docker hub: +# docker login -u cyphernode + +# Must enable experimental cli features +# "experimental": "enabled" in ~/.docker/config.json + +image() { + local arch=$1 + local arch2=$2 + + echo "Building and pushing cyphernode/lnurl for ${arch} tagging as ${version} alpine arch ${arch2}..." + + docker build --no-cache -t cyphernode/lnurl:${arch}-${version} --build-arg ARCH=${arch2} . \ + && docker push cyphernode/lnurl:${arch}-${version} + + return $? +} + +manifest() { + echo "Creating and pushing manifest for cyphernode/lnurl for version ${version}..." + + docker manifest create cyphernode/lnurl:${version} \ + cyphernode/lnurl:${x86_docker}-${version} \ + cyphernode/lnurl:${arm_docker}-${version} \ + cyphernode/lnurl:${aarch64_docker}-${version} \ + && docker manifest annotate cyphernode/lnurl:${version} cyphernode/lnurl:${arm_docker}-${version} --os linux --arch ${arm_docker} \ + && docker manifest annotate cyphernode/lnurl:${version} cyphernode/lnurl:${x86_docker}-${version} --os linux --arch ${x86_docker} \ + && docker manifest annotate cyphernode/lnurl:${version} cyphernode/lnurl:${aarch64_docker}-${version} --os linux --arch ${aarch64_docker} \ + && docker manifest push -p cyphernode/lnurl:${version} + + return $? +} + +x86_docker="amd64" +x86_alpine="" +arm_docker="arm" +arm_alpine="arm32" +aarch64_docker="arm64" +aarch64_alpine="" + +# Build amd64 and arm64 first, building for arm will trigger the manifest creation and push on hub + +#arch_docker=${arm_docker} ; arch_alpine=${arm_alpine} +#arch_docker=${aarch64_docker} ; arch_alpine=${aarch64_alpine} +arch_docker=${x86_docker} ; arch_alpine=${x86_alpine} + +version="v0.1.0-rc.1" + +echo "arch_docker=$arch_docker, arch_alpine=$arch_alpine" + +image ${arch_docker} ${arch_alpine} + +[ $? -ne 0 ] && echo "Error" && exit 1 + +[ "${arch_docker}" = "${x86_docker}" ] && echo "Built and pushed ${arch_docker} only" && exit 0 +[ "${arch_docker}" = "${aarch64_docker}" ] && echo "Built and pushed ${arch_docker} only" && exit 0 +[ "${arch_docker}" = "${arm_docker}" ] && echo "Built and pushed images, now building and pushing manifest for all archs..." + +manifest + +[ $? -ne 0 ] && echo "Error" && exit 1 diff --git a/package.json b/package.json index 3493725..4ee4ae7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "homepage": "https://github.com/SatoshiPortal/lnurl_cypherapp#readme", "dependencies": { - "@prisma/client": "^2.30.0", + "@prisma/client": "^3.2.1", "@types/async-lock": "^1.1.2", "async-lock": "^1.2.4", "axios": "^0.21.1", @@ -30,7 +30,7 @@ "date-fns": "^2.23.0", "express": "^4.17.1", "http-status-codes": "^1.4.0", - "prisma": "^2.30.0", + "prisma": "^3.2.1", "reflect-metadata": "^0.1.13", "tslog": "^3.2.0" }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 896f6d9..8be2503 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,6 +8,7 @@ datasource db { generator client { provider = "prisma-client-js" + engineType = "binary" } model LnurlWithdrawEntity { diff --git a/tests/ln_setup.sh b/tests/ln_setup.sh index 379d7c7..1636065 100755 --- a/tests/ln_setup.sh +++ b/tests/ln_setup.sh @@ -11,9 +11,20 @@ connectstring2=$(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` l echo ; echo "connectstring2=${connectstring2}" # Mine enough blocks -mine 105 +#mine 105 channelmsats=8000000000 +# Fund LN node +address=$(docker exec -it `docker ps -q -f "name=proxy\."` curl localhost:8888/ln_newaddr | jq -r ".bech32") +echo ; echo "address=${address}" +data='{"address":"'${address}'","amount":1}' +docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/spend + +mine 6 + +echo ; echo "Sleeping 5 seconds..." +sleep 5 + # Create a channel between the two nodes data='{"peer":"'${connectstring2}'","msatoshi":'${channelmsats}'}' echo ; echo "data=${data}" @@ -25,8 +36,8 @@ sleep 5 # Make the channel ready mine 6 -echo ; echo "Sleeping 30 seconds..." -sleep 30 +echo ; echo "Sleeping 15 seconds..." +sleep 15 # Balance the channel invoicenumber=$RANDOM diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index 509f044..198d0a1 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -121,37 +121,6 @@ exec_in_test_container_leave_lf() { docker exec -it tests-lnurl-withdraw "$@" } -ln_reconnect_with_sparkwallet() { - trace 2 "\n\n[ln_reconnect] ${BCyan}Reconnecting the two LN instances...${Color_Off}\n" - - exec_in_test_container_leave_lf sh -c 'while true ; do ping -c 1 cyphernode_lightning ; [ "$?" -eq "0" ] && break ; sleep 5; done' - exec_in_test_container_leave_lf sh -c 'while true ; do ping -c 1 cyphernode_lightning2 ; [ "$?" -eq "0" ] && break ; sleep 5; done' - - local data='{"id":1,"jsonrpc":"2.0","method":"getinfo","params":[]}' - trace 3 "[ln_reconnect] data=${data}" - local getinfo1=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet:9737/rpc) - trace 3 "[ln_reconnect] getinfo1=${getinfo1}" - local id1=$(echo "${getinfo1}" | jq -r ".id") - trace 3 "[ln_reconnect] id1=${id1}" - - data='{"id":1,"jsonrpc":"2.0","method":"getinfo","params":[]}' - trace 3 "[ln_reconnect] data=${data}" - local getinfo2=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) - trace 3 "[ln_reconnect] getinfo2=${getinfo2}" - local id2=$(echo "${getinfo2}" | jq -r ".id") - trace 3 "[ln_reconnect] id2=${id2}" - - data='{"id":1,"jsonrpc":"2.0","method":"connect","params":["'${id2}'@lightning2"]}' - trace 3 "[ln_reconnect] data=${data}" - local connect=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet:9737/rpc) - trace 3 "[ln_reconnect] connect=${connect}" - - data='{"id":1,"jsonrpc":"2.0","method":"connect","params":["'${id1}'@lightning"]}' - trace 3 "[ln_reconnect] data=${data}" - local connect2=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) - trace 3 "[ln_reconnect] connect2=${connect2}" -} - create_lnurl_withdraw() { trace 2 "\n\n[create_lnurl_withdraw] ${BCyan}Service creates LNURL Withdraw...${Color_Off}\n" @@ -288,9 +257,7 @@ create_bolt11() { local desc=${2} trace 3 "[create_bolt11] desc=${desc}" - local data='{"id":1,"jsonrpc":"2.0","method":"invoice","params":{"msatoshi":'${msatoshi}',"label":"'${desc}'","description":"'${desc}'"}}' - trace 3 "[create_bolt11] data=${data}" - local invoice=$(exec_in_test_container curl -sd "${data}" -H "X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + local invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning invoice ${msatoshi} "${desc}" "${desc}") trace 3 "[create_bolt11] invoice=${invoice}" echo "${invoice}" @@ -306,7 +273,7 @@ get_invoice_status() { trace 3 "[get_invoice_status] payment_hash=${payment_hash}" local data='{"id":1,"jsonrpc":"2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' trace 3 "[get_invoice_status] data=${data}" - local invoices=$(exec_in_test_container curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + local invoices=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning listinvoices -k payment_hash=${payment_hash}) trace 3 "[get_invoice_status] invoices=${invoices}" local status=$(echo "${invoices}" | jq -r ".invoices[0].status") trace 3 "[get_invoice_status] status=${status}" From f98df059806f8dbe87cbacdd016a36598c819ba2 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 15 Nov 2021 19:58:19 -0500 Subject: [PATCH 32/52] Fixed forceFallback when payment pending --- src/lib/LnurlWithdraw.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 80e7e88..b1127bf 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -1096,7 +1096,10 @@ class LnurlWithdraw { paymentStatus.result ); // lnurlWithdrawEntity.withdrawnTs = new Date(); - lnurlWithdrawEntity.paid = true; + if (paymentStatus.paymentStatus === "complete") { + // We set status to paid only if completed... not when pending! + lnurlWithdrawEntity.paid = true; + } lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( lnurlWithdrawEntity From 03635c2645f25a191e7dc58ee4ed46e33b383c95 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 25 Nov 2021 13:06:45 -0500 Subject: [PATCH 33/52] JWT compliant token --- src/lib/BatcherClient.ts | 16 ++++++++++++++-- src/lib/CyphernodeClient.ts | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/lib/BatcherClient.ts b/src/lib/BatcherClient.ts index 7559b99..f489fcc 100644 --- a/src/lib/BatcherClient.ts +++ b/src/lib/BatcherClient.ts @@ -180,14 +180,26 @@ class BatcherClient { const response = await this._post("/api", data); logger.debug("BatcherClient.queueForNextBatch, response:", response); + // Errors may include local ones like this: + // { + // status: -1, + // data: { + // code: 'ECONNABORTED', + // message: 'timeout of 30000ms exceeded' + // } + // } if (response.status >= 200 && response.status < 400) { result = { result: response.data.result }; } else { result = { error: { - code: response.data.error.code, - message: response.data.error.message, + code: response.data.error + ? response.data.error.code + : response.data.code, + message: response.data.error + ? response.data.error.message + : response.data.message, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as IResponseError, } as IRespBatchRequest; diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index a6ebbc2..0415ea0 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -22,7 +22,8 @@ import IRespLnPayStatus from "../types/cyphernode/IRespLnPayStatus"; class CyphernodeClient { private baseURL: string; - private readonly h64: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg=="; + // echo -n '{"alg":"HS256","typ":"JWT"}' | basenc --base64url | tr -d '=' + private readonly h64: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; private apiId: string; private apiKey: string; private caFile: string; @@ -46,12 +47,21 @@ class CyphernodeClient { const current = Math.round(new Date().getTime() / 1000) + 10; const p = '{"id":"' + this.apiId + '","exp":' + current + "}"; - const p64 = Buffer.from(p).toString("base64"); + const re1 = /\+/g; + const re2 = /\//g; + const p64 = Buffer.from(p) + .toString("base64") + .replace(re1, "-") + .replace(re2, "_") + .split("=")[0]; const msg = this.h64 + "." + p64; const s = crypto .createHmac("sha256", this.apiKey) .update(msg) - .digest("hex"); + .digest("base64") + .replace(re1, "-") + .replace(re2, "_") + .split("=")[0]; const token = msg + "." + s; logger.debug("CyphernodeClient._generateToken :: token=" + token); From b2af9e238aad5841cdb9072510ff7c99ec1b4382 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 3 Dec 2021 12:06:17 -0500 Subject: [PATCH 34/52] dependabot directives --- .github/dependabot.yml | 7 +++++++ tests/ln_reconnect.sh | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7b8353a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + target-branch: "dev" diff --git a/tests/ln_reconnect.sh b/tests/ln_reconnect.sh index d3a7143..b264c86 100755 --- a/tests/ln_reconnect.sh +++ b/tests/ln_reconnect.sh @@ -4,8 +4,8 @@ ln_reconnect() { # First ping the containers to make sure they're up... docker run --rm -it --name ln-reconnecter --network cyphernodenet alpine sh -c ' - while true ; do ping -c 1 cyphernode_lightning ; [ "$?" -eq "0" ] && break ; sleep 5; done - while true ; do ping -c 1 cyphernode_lightning2 ; [ "$?" -eq "0" ] && break ; sleep 5; done + while true ; do ping -c 1 lightning ; [ "$?" -eq "0" ] && break ; sleep 5; done + while true ; do ping -c 1 lightning2 ; [ "$?" -eq "0" ] && break ; sleep 5; done ' # Now check if the lightning nodes are ready to accept requests... From d2d72b2b802361287d0c4ef67640fb354bf4741c Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 27 Apr 2022 22:32:47 -0400 Subject: [PATCH 35/52] Lots of changes including tests and traefik v2 --- README.md | 64 ++------------ cypherapps/data/config.json | 42 ++++----- cypherapps/docker-compose.yaml | 106 +++++++++++++++++++---- cypherapps/test.sh | 40 +++++++++ doc/dev-notes | 55 ++++++++++++ src/lib/CyphernodeClient.ts | 13 ++- src/lib/HttpServer.ts | 8 +- src/lib/LnurlDBPrisma.ts | 18 +++- src/lib/LnurlWithdraw.ts | 23 ++++- tests/lnurl_withdraw_wallet.sh | 154 ++++++++++++++++++--------------- tests/test-lnurl-withdraw.sh | 14 +-- 11 files changed, 351 insertions(+), 186 deletions(-) create mode 100755 cypherapps/test.sh create mode 100644 doc/dev-notes diff --git a/README.md b/README.md index 540758d..2a9fcca 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Request: externalId?: string; msatoshi: number; description?: string; - expiration?: Date; + expiresAt?: Date; webhookUrl?: string; btcFallbackAddress?: string; batchFallback?: boolean; @@ -46,7 +46,7 @@ Response: externalId: string | null; msatoshi: number; description: string | null; - expiration: Date | null; + expiresAt: Date | null; secretToken: string; webhookUrl: string | null; calledback: boolean; @@ -92,7 +92,7 @@ Response: externalId: string | null; msatoshi: number; description: string | null; - expiration: Date | null; + expiresAt: Date | null; secretToken: string; webhookUrl: string | null; calledback: boolean; @@ -138,7 +138,7 @@ Response: externalId: string | null; msatoshi: number; description: string | null; - expiration: Date | null; + expiresAt: Date | null; secretToken: string; webhookUrl: string | null; calledback: boolean; @@ -224,7 +224,7 @@ Response: externalId: string | null; msatoshi: number; description: string | null; - expiration: Date | null; + expiresAt: Date | null; secretToken: string; webhookUrl: string | null; calledback: boolean; @@ -402,57 +402,3 @@ Response: } } ``` - -========================= - -## Temp dev notes - -```bash -DOCKER_BUILDKIT=0 docker build -t lnurl . -docker run --rm -it -v "$PWD:/lnurl" --entrypoint ash bff4412e444c -npm install - -docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" --entrypoint ash lnurl -npm run build -npm run start - --- - -docker exec -it lnurl ash -/lnurl # apk add curl -/lnurl # curl -d '{"id":0,"method":"getConfig","params":[]}' -H "Content-Type: application/json" localhost:8000/api -{"id":0,"result":{"LOG":"DEBUG","BASE_DIR":"/lnurl","DATA_DIR":"data","DB_NAME":"lnurl.sqlite","URL_SERVER":"http://lnurl","URL_PORT":8000,"URL_CTX_WEBHOOKS":"webhooks","SESSION_TIMEOUT":600,"CN_URL":"https://gatekeeper:2009/v0","CN_API_ID":"003","CN_API_KEY":"39b83c35972aeb81a242bfe189dc0a22da5ac6cbb64072b492f2d46519a97618"}} - --- - -sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" - -curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":0.01,"description":"desc02","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc02","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api -{"id":0,"result":{"msatoshi":0.01,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} - -sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" -id|msatoshi|description|expiration|secret_token|webhook_url|lnurl|withdrawn_details|withdrawn_ts|active|created_ts|updated_ts -1|0.01|desc01|2021-07-15 12:12|abc01|https://webhookUrl01|LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K|||1|2021-07-15 19:42:06|2021-07-15 19:42:06 - -curl -d '{"id":0,"method":"decodeBech32","params":{"s":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXGXE8Q93"}}' -H "Content-Type: application/json" localhost:8000/api -{"id":0,"result":"http://.onion:80/lnurl/withdrawRequest?s=abc01"} - -curl localhost:8000/withdrawRequest?s=abc02 -{"tag":"withdrawRequest","callback":"http://.onion:80/lnurl/withdraw","k1":"abc01","defaultDescription":"desc01","minWithdrawable":0.01,"maxWithdrawable":0.01} - -curl localhost:8000/withdraw?k1=abc03\&pr=lnbcrt123456780p1ps0pf5ypp5lfskvgsdef4hpx0lndqe69ypu0rxl5msndkcnlm8v6l5p75xzd6sdq2v3jhxcesxvxqyjw5qcqp2sp5f42mrc40eh4ntmqhgxvk74m2w3q25fx9m8d9wn6d20ahtfy6ju8q9qy9qsqw4khcr86dlg66nz3ds6nhxpsw9z0ugxfkequtyf8qv7q6gdvztdhsfp36uazsz35xp37lfmt0tqsssrew0wr0htfdkhjpwdagnzvc6qp2ynxvd -{"status":"OK"} - -================== - -docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" --network cyphernodeappsnet --entrypoint ash lnurl -npm run build -npm run start - -DEBUG: -docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" -p 9229:9229 -p 8000:8000 --network cyphernodeappsnet --entrypoint ash lnurl - -npx prisma migrate reset -npx prisma generate -npx prisma migrate dev -``` diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json index 401f249..c34eb3e 100644 --- a/cypherapps/data/config.json +++ b/cypherapps/data/config.json @@ -1,22 +1,22 @@ { - "LOG": "DEBUG", - "BASE_DIR": "/lnurl", - "DATA_DIR": "data", - "DB_NAME": "lnurl.sqlite", - "URL_API_SERVER": "http://lnurl", - "URL_API_PORT": 8000, - "URL_API_CTX": "/api", - "URL_CTX_WEBHOOKS": "/webhooks", - "SESSION_TIMEOUT": 600, - "CN_URL": "https://gatekeeper:2009/v0", - "CN_API_ID": "003", - "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", - "BATCHER_URL": "http://batcher:8000", - "LN_SERVICE_SERVER": "https://yourdomain", - "LN_SERVICE_PORT": 443, - "LN_SERVICE_CTX": "/lnurl", - "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", - "LN_SERVICE_WITHDRAW_CTX": "/withdraw", - "RETRY_WEBHOOKS_TIMEOUT": 1, - "CHECK_EXPIRATION_TIMEOUT": 1 -} + "LOG": "DEBUG", + "BASE_DIR": "/lnurl", + "DATA_DIR": "data", + "DB_NAME": "lnurl.sqlite", + "URL_API_SERVER": "http://lnurl", + "URL_API_PORT": 8000, + "URL_API_CTX": "/api", + "URL_CTX_WEBHOOKS": "/webhooks", + "SESSION_TIMEOUT": 600, + "CN_URL": "https://gatekeeper:2009/v0", + "CN_API_ID": "003", + "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", + "BATCHER_URL": "http://batcher:8000", + "LN_SERVICE_SERVER": "https://yourdomain", + "LN_SERVICE_PORT": 443, + "LN_SERVICE_CTX": "/lnurl", + "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", + "LN_SERVICE_WITHDRAW_CTX": "/withdraw", + "RETRY_WEBHOOKS_TIMEOUT": 1, + "CHECK_EXPIRATION_TIMEOUT": 1 +} \ No newline at end of file diff --git a/cypherapps/docker-compose.yaml b/cypherapps/docker-compose.yaml index e7a7b68..3871de5 100644 --- a/cypherapps/docker-compose.yaml +++ b/cypherapps/docker-compose.yaml @@ -1,12 +1,18 @@ version: "3" +# This is how you can generate a random authentication key: +# server:~$ dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -ps -c 32 +# 82b5313656818b70d900c32c3bf8ea502705048392f96a4c4f65d0e270d576fe +# server:~$ htpasswd -bnB yourapiuser '82b5313656818b70d900c32c3bf8ea502705048392f96a4c4f65d0e270d576fe' | sed 's/\$/\$\$/g' +# yourapiuser:$$2y$$05$$M0vR2UtI.I5JlV9Ha3j/buXwfXuyG6QqnjSBgSkkJomGaynIS1lPO + services: lnurl: environment: - "TRACING=1" - "CYPHERNODE_URL=https://gatekeeper:${GATEKEEPER_PORT}" image: cyphernode/lnurl:v0.1.0 - entrypoint: ["npm", "run", "start:dev"] + entrypoint: [ "npm", "run", "start:dev" ] volumes: - "$APP_SCRIPT_PATH/data:/lnurl/data" - "$GATEKEEPER_DATAPATH/certs/cert.pem:/lnurl/cert.pem:ro" @@ -15,24 +21,88 @@ services: - cyphernodeappsnet restart: always labels: - - "traefik.docker.network=cyphernodeappsnet" -# PathPrefix won't be stripped because we don't want outsiders to access /api, only /lnurl/... - - "traefik.frontend.rule=PathPrefix:/lnurl" - - "traefik.frontend.passHostHeader=true" - - "traefik.enable=true" - - "traefik.port=8000" -# Don't secure the PathPrefix /lnurl -# - "traefik.frontend.auth.basic.users=" + - traefik.enable=true + - traefik.docker.network=cyphernodeappsnet + + - traefik.http.routers.lnurl-api.rule=PathPrefix(`/lnurl/api`) + - traefik.http.routers.lnurl-api.entrypoints=websecure + - traefik.http.routers.lnurl-api.tls=true + - traefik.http.routers.lnurl-api.service=lnurl + - traefik.http.routers.lnurl-api.middlewares=lnurl-api-redirectregex@docker,lnurl-api-auth@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker + + # - traefik.http.routers.lnurl.rule=Host(`lnurl.yourdomain.com`) + # PathPrefix won't be stripped because we don't want outsiders to access /api, only /lnurl/... + - traefik.http.routers.lnurl.rule=PathPrefix(`/lnurl`) + - traefik.http.routers.lnurl.entrypoints=websecure + - traefik.http.routers.lnurl.tls=true + - traefik.http.routers.lnurl.service=lnurl + - traefik.http.routers.lnurl.middlewares=lnurl-redirectregex@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker + # - traefik.http.routers.lnurl.middlewares=lnurl-auth@docker,lnurl-allowedhosts@docker + + # PathPrefix won't be stripped because we don't want outsiders to access /api, only /lnurl/... + - traefik.http.routers.lnurl-onion.rule=PathPrefix(`/lnurl`) + - traefik.http.routers.lnurl-onion.entrypoints=onion + - traefik.http.routers.lnurl-onion.service=lnurl + - traefik.http.routers.lnurl-onion.middlewares=lnurl-redirectregex@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker + + - traefik.http.services.lnurl.loadbalancer.server.port=8000 + - traefik.http.services.lnurl.loadbalancer.passHostHeader=true + + # yourapiuser:82b5313656818b70d900c32c3bf8ea502705048392f96a4c4f65d0e270d576fe + - traefik.http.middlewares.lnurl-api-auth.basicauth.users=yourapiuser:$$2y$$05$$M0vR2UtI.I5JlV9Ha3j/buXwfXuyG6QqnjSBgSkkJomGaynIS1lPO" + # - traefik.http.middlewares.lnurl-allowedhosts.headers.allowedHosts=lnurl.yourdomain.com + - traefik.http.middlewares.lnurl-stripprefix.stripprefix.prefixes=/lnurl,/lnurl/ + - traefik.http.middlewares.lnurl-redirectregex.redirectregex.regex=^(.*)/lnurl$$ + - traefik.http.middlewares.lnurl-redirectregex.redirectregex.replacement=$$1/lnurl/ + - traefik.http.middlewares.lnurl-redirectregex.redirectregex.permanent=true + - traefik.http.middlewares.lnurl-api-redirectregex.redirectregex.regex=^(.*)/lnurl/api$$ + - traefik.http.middlewares.lnurl-api-redirectregex.redirectregex.replacement=$$1/lnurl/api/ + - traefik.http.middlewares.lnurl-api-redirectregex.redirectregex.permanent=true + - traefik.http.middlewares.lnurl-ratelimit.ratelimit.sourcecriterion.requesthost=true + - traefik.http.middlewares.lnurl-ratelimit.ratelimit.period=1s + - traefik.http.middlewares.lnurl-ratelimit.ratelimit.average=1 + - traefik.http.middlewares.lnurl-ratelimit.ratelimit.burst=2 deploy: labels: - - "traefik.docker.network=cyphernodeappsnet" -# PathPrefix won't be stripped because we don't want outsiders to access /api, only /lnurl/... - - "traefik.frontend.rule=PathPrefix:/lnurl" - - "traefik.frontend.passHostHeader=true" - - "traefik.enable=true" - - "traefik.port=8000" -# Don't secure the PathPrefix /lnurl -# - "traefik.frontend.auth.basic.users=" + - traefik.enable=true + - traefik.docker.network=cyphernodeappsnet + + - traefik.http.routers.lnurl-api.rule=PathPrefix(`/lnurl/api`) + - traefik.http.routers.lnurl-api.entrypoints=websecure + - traefik.http.routers.lnurl-api.tls=true + - traefik.http.routers.lnurl-api.service=lnurl + - traefik.http.routers.lnurl-api.middlewares=lnurl-api-redirectregex@docker,lnurl-api-auth@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker + + # - traefik.http.routers.lnurl.rule=Host(`lnurl.yourdomain.com`) + - traefik.http.routers.lnurl.rule=PathPrefix(`/lnurl`) + - traefik.http.routers.lnurl.entrypoints=websecure + - traefik.http.routers.lnurl.tls=true + - traefik.http.routers.lnurl.service=lnurl + - traefik.http.routers.lnurl.middlewares=lnurl-redirectregex@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker + # - traefik.http.routers.lnurl.middlewares=lnurl-auth@docker,lnurl-allowedhosts@docker + + - traefik.http.routers.lnurl-onion.rule=PathPrefix(`/lnurl`) + - traefik.http.routers.lnurl-onion.entrypoints=onion + - traefik.http.routers.lnurl-onion.service=lnurl + - traefik.http.routers.lnurl-onion.middlewares=lnurl-redirectregex@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker + + - traefik.http.services.lnurl.loadbalancer.server.port=8000 + - traefik.http.services.lnurl.loadbalancer.passHostHeader=true + + # yourapiuser:82b5313656818b70d900c32c3bf8ea502705048392f96a4c4f65d0e270d576fe + - traefik.http.middlewares.lnurl-api-auth.basicauth.users=yourapiuser:$$2y$$05$$M0vR2UtI.I5JlV9Ha3j/buXwfXuyG6QqnjSBgSkkJomGaynIS1lPO" + # - traefik.http.middlewares.lnurl-allowedhosts.headers.allowedHosts=lnurl.yourdomain.com + - traefik.http.middlewares.lnurl-stripprefix.stripprefix.prefixes=/lnurl,/lnurl/ + - traefik.http.middlewares.lnurl-redirectregex.redirectregex.regex=^(.*)/lnurl$$ + - traefik.http.middlewares.lnurl-redirectregex.redirectregex.replacement=$$1/lnurl/ + - traefik.http.middlewares.lnurl-redirectregex.redirectregex.permanent=true + - traefik.http.middlewares.lnurl-api-redirectregex.redirectregex.regex=^(.*)/lnurl/api$$ + - traefik.http.middlewares.lnurl-api-redirectregex.redirectregex.replacement=$$1/lnurl/api/ + - traefik.http.middlewares.lnurl-api-redirectregex.redirectregex.permanent=true + - traefik.http.middlewares.lnurl-ratelimit.ratelimit.sourcecriterion.requesthost=true + - traefik.http.middlewares.lnurl-ratelimit.ratelimit.period=1s + - traefik.http.middlewares.lnurl-ratelimit.ratelimit.average=1 + - traefik.http.middlewares.lnurl-ratelimit.ratelimit.burst=2 replicas: 1 placement: constraints: @@ -41,7 +111,7 @@ services: condition: "any" delay: 1s update_config: - parallelism: 1 + parallelism: 1 networks: cyphernodeappsnet: external: true diff --git a/cypherapps/test.sh b/cypherapps/test.sh new file mode 100755 index 0000000..a27be1e --- /dev/null +++ b/cypherapps/test.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +timeout_feature() { + local interval=10 + local totaltime=60 + local testwhat=${1} + local returncode + local endtime=$(($(date +%s) + ${totaltime})) + + while : + do + eval ${testwhat} + returncode=$? + + # If no error or 2 minutes passed, we get out of this loop + ([ "${returncode}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break + + printf "\e[1;31mMaybe it's too early, I'll retry every ${interval} seconds for $((${totaltime} / 60)) minutes ($((${endtime} - $(date +%s))) seconds left).\e[1;0m\r\n" + + sleep ${interval} + done + + return ${returncode} +} + +do_test() { + local rc + rc=$(curl -k -s -o /dev/null -w "%{http_code}" https://127.0.0.1:${TRAEFIK_HTTPS_PORT}/lnurl/api/) + [ "${rc}" -ne "200" ] && return 400 + return 0 +} + +export TRAEFIK_HTTPS_PORT + +timeout_feature do_test +returncode=$? + +# return 0: tests cool +# return 1: tests failed +return $returncode diff --git a/doc/dev-notes b/doc/dev-notes new file mode 100644 index 0000000..82b8f34 --- /dev/null +++ b/doc/dev-notes @@ -0,0 +1,55 @@ +If running locally in regtest, LN_SERVICE_SERVER should be https://traefik in config.json. + +========================= + +## Temp dev notes + +```bash +DOCKER_BUILDKIT=0 docker build -t lnurl . +docker run --rm -it -v "$PWD:/lnurl" --entrypoint ash bff4412e444c +npm install + +docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" --entrypoint ash lnurl +npm run build +npm run start + +-- + +docker exec -it lnurl ash +/lnurl # apk add curl +/lnurl # curl -d '{"id":0,"method":"getConfig","params":[]}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":{"LOG":"DEBUG","BASE_DIR":"/lnurl","DATA_DIR":"data","DB_NAME":"lnurl.sqlite","URL_SERVER":"http://lnurl","URL_PORT":8000,"URL_CTX_WEBHOOKS":"webhooks","SESSION_TIMEOUT":600,"CN_URL":"https://gatekeeper:2009/v0","CN_API_ID":"003","CN_API_KEY":"39b83c35972aeb81a242bfe189dc0a22da5ac6cbb64072b492f2d46519a97618"}} + +-- + +sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" + +curl -d '{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":0.01,"description":"desc02","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc02","webhookUrl":"https://webhookUrl01"}}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":{"msatoshi":0.01,"description":"desc01","expiration":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} + +sqlite3 data/lnurl.sqlite -header "select * from lnurl_withdraw" +id|msatoshi|description|expiration|secret_token|webhook_url|lnurl|withdrawn_details|withdrawn_ts|active|created_ts|updated_ts +1|0.01|desc01|2021-07-15 12:12|abc01|https://webhookUrl01|LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K|||1|2021-07-15 19:42:06|2021-07-15 19:42:06 + +curl -d '{"id":0,"method":"decodeBech32","params":{"s":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXGXE8Q93"}}' -H "Content-Type: application/json" localhost:8000/api +{"id":0,"result":"http://.onion:80/lnurl/withdrawRequest?s=abc01"} + +curl localhost:8000/withdrawRequest?s=abc02 +{"tag":"withdrawRequest","callback":"http://.onion:80/lnurl/withdraw","k1":"abc01","defaultDescription":"desc01","minWithdrawable":0.01,"maxWithdrawable":0.01} + +curl localhost:8000/withdraw?k1=abc03\&pr=lnbcrt123456780p1ps0pf5ypp5lfskvgsdef4hpx0lndqe69ypu0rxl5msndkcnlm8v6l5p75xzd6sdq2v3jhxcesxvxqyjw5qcqp2sp5f42mrc40eh4ntmqhgxvk74m2w3q25fx9m8d9wn6d20ahtfy6ju8q9qy9qsqw4khcr86dlg66nz3ds6nhxpsw9z0ugxfkequtyf8qv7q6gdvztdhsfp36uazsz35xp37lfmt0tqsssrew0wr0htfdkhjpwdagnzvc6qp2ynxvd +{"status":"OK"} + +================== + +docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" --network cyphernodeappsnet --entrypoint ash lnurl +npm run build +npm run start + +DEBUG: +docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" -p 9229:9229 -p 8000:8000 --network cyphernodeappsnet --entrypoint ash lnurl + +npx prisma migrate reset +npx prisma generate +npx prisma migrate dev +``` diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 0415ea0..78d25d3 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -96,16 +96,21 @@ class CyphernodeClient { } // logger.debug( - // "CyphernodeClient._post :: configs: %s", + // "CyphernodeClient._post :: configs:", // JSON.stringify(configs) // ); try { const response = await axios.request(configs); - logger.debug("CyphernodeClient._post :: response.data:", response.data); + // logger.debug("CyphernodeClient._post :: response:", response); + // response.data used to be a string, looks like it's now an object... taking no chance. + const str = typeof response.data === 'string' ? response.data : JSON.stringify(response.data); + logger.debug("CyphernodeClient._post :: response.data:", str.substring(0, 1000)); return { status: response.status, data: response.data }; } catch (err) { + // logger.debug("CyphernodeClient._post :: catch, err:", err); + if (axios.isAxiosError(err)) { const error: AxiosError = err; @@ -172,7 +177,9 @@ class CyphernodeClient { try { const response = await axios.request(configs); - logger.debug("CyphernodeClient._get :: response.data:", response.data); + // response.data used to be a string, looks like it's now an object... taking no chance. + const str = typeof response.data === 'string' ? response.data : JSON.stringify(response.data); + logger.debug("CyphernodeClient._get :: response.data:", str.substring(0, 1000)); return { status: response.status, data: response.data }; } catch (err) { diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 8f2b370..8d3c7e4 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -193,8 +193,8 @@ class HttpServer { // LN Service LNURL Withdraw Request this._httpServer.get( - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX, + // this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX, async (req, res) => { logger.info( this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + ":", @@ -215,8 +215,8 @@ class HttpServer { // LN Service LNURL Withdraw this._httpServer.get( - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, + // this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, async (req, res) => { logger.info(this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX + ":", req.query); diff --git a/src/lib/LnurlDBPrisma.ts b/src/lib/LnurlDBPrisma.ts index 4103720..d19bb8b 100644 --- a/src/lib/LnurlDBPrisma.ts +++ b/src/lib/LnurlDBPrisma.ts @@ -89,8 +89,12 @@ class LnurlDBPrisma { return lw as LnurlWithdrawEntity; } - async getNonCalledbackLnurlWithdraws(): Promise { - const lws = await this._db?.lnurlWithdrawEntity.findMany({ + async getNonCalledbackLnurlWithdraws(lnurlWithdrawId?: number): Promise { + + // If there's a lnurlWithdrawId as arg, let's add it to the where clause! + + let lws; + let whereClause = { where: { deleted: false, webhookUrl: { not: null }, @@ -111,7 +115,15 @@ class LnurlDBPrisma { }, ], }, - }); + } + + if (lnurlWithdrawId) { + whereClause.where = Object.assign(whereClause.where, { lnurlWithdrawId }); + } + + // logger.debug("LnurlDBPrisma.getNonCalledbackLnurlWithdraws, whereClause=", whereClause); + + lws = await this._db?.lnurlWithdrawEntity.findMany(whereClause); return lws as LnurlWithdrawEntity[]; } diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index b1127bf..3e96d5b 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -579,9 +579,11 @@ class LnurlWithdraw { // As soon as there's a "success" field in attemps, payment succeeded! // If the last attempt doesn't have a "failure" field, it means there's a pending attempt // If the last attempt has a "failure" field, it means payment failed. + let nbAttempts; let success = false; let failure = null; paystatus.result.pay.forEach((pay) => { + nbAttempts = 0; pay.attempts.forEach((attempt) => { if (attempt.success) { success = true; @@ -592,6 +594,20 @@ class LnurlWithdraw { failure = false; } }); + + // The result of paystatus can get quite big when trying + // to find a route (several MB). Let's save paystatus result only + // if not too big to avoid filling up disk space with + // database + logging of the row. + nbAttempts += pay.attempts.length; + if (nbAttempts > 1000) { + logger.debug( + "LnurlWithdraw.lnFetchPaymentStatus, paystatus.result is too large, truncating content..." + ); + // Let's keep two attempts, and put a message in the second one... + pay.attempts.splice(2); + pay.attempts[1].failure = { message: "attempts array truncated by lnurl cypherapp" }; + } }); if (success) { @@ -756,7 +772,8 @@ class LnurlWithdraw { let lnurlWithdrawEntitys; if (lnurlWithdrawEntity) { - lnurlWithdrawEntitys = [lnurlWithdrawEntity]; + // Let's take the latest on from database, just in case passed object has stale data + lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(lnurlWithdrawEntity.lnurlWithdrawId); } else { lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); } @@ -900,7 +917,7 @@ class LnurlWithdraw { } else if (paymentStatus.paymentStatus !== "failed") { logger.debug( "LnurlWithdraw.processFallbacks: LnurlWithdraw payment already " + - paymentStatus.paymentStatus + paymentStatus.paymentStatus ); proceedToFallback = false; @@ -1082,7 +1099,7 @@ class LnurlWithdraw { if (paymentStatus.paymentStatus !== "failed") { logger.debug( "LnurlWithdraw.forceFallback, LnurlWithdraw payment already " + - paymentStatus.paymentStatus + paymentStatus.paymentStatus ); response.error = { diff --git a/tests/lnurl_withdraw_wallet.sh b/tests/lnurl_withdraw_wallet.sh index 5d9ce5b..55fc604 100755 --- a/tests/lnurl_withdraw_wallet.sh +++ b/tests/lnurl_withdraw_wallet.sh @@ -1,7 +1,5 @@ -#!/bin/sh +#!/bin/bash -# -# Must be run using run_lnurl_withdraw_wallet.sh # # This is a super basic LNURL-compatible wallet, command-line # Useful to test LNURL on a regtest or testnet environment. @@ -25,15 +23,36 @@ # ./lnurl_withdraw_wallet.sh # -. /tests/colors.sh +. ./colors.sh trace() { if [ "${1}" -le "${TRACING}" ]; then - local str="$(date -Is) $$ ${2}" - echo -e "${str}" 1>&2 + echo -e "$(date -u +%FT%TZ) ${2}" 1>&2 + fi +} + +start_test_container() { + docker run -d --rm -it --name lnurl-withdraw-wallet --network=cyphernodeappsnet alpine + docker network connect cyphernodenet lnurl-withdraw-wallet +} + +stop_test_container() { + trace 1 "\n\n[stop_test_container] ${BCyan}Stopping existing containers if they are running...${Color_Off}\n" + + local containers=$(docker ps -q -f "name=lnurl-withdraw-wallet") + if [ -n "${containers}" ]; then + docker stop $(docker ps -q -f "name=lnurl-withdraw-wallet") fi } +exec_in_test_container() { + docker exec -it lnurl-withdraw-wallet "$@" | tr -d '\r\n' +} + +exec_in_test_container_leave_lf() { + docker exec -it lnurl-withdraw-wallet "$@" +} + create_lnurl_withdraw() { trace 2 "\n\n[create_lnurl_withdraw] ${BCyan}Service creates LNURL Withdraw...${Color_Off}\n" @@ -45,20 +64,16 @@ create_lnurl_withdraw() { local msatoshi=$((500000+${invoicenumber})) trace 3 "[create_lnurl_withdraw] msatoshi=${msatoshi}" local expiration_offset=${2:-0} - local expiration=$(date -d @$(($(date -u +"%s")+${expiration_offset})) +"%Y-%m-%dT%H:%M:%SZ") + local expiration=$(exec_in_test_container date -d @$(($(date -u +"%s")+${expiration_offset})) +"%Y-%m-%dT%H:%M:%SZ") trace 3 "[create_lnurl_withdraw] expiration=${expiration}" local fallback_addr=${4:-""} local fallback_batched=${5:-"false"} - if [ -n "${callbackurl}" ]; then - callbackurl=',"webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'"' - fi - # Service creates LNURL Withdraw - data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiresAt":"'${expiration}'"'${callbackurl}',"btcFallbackAddress":"'${fallback_addr}'","batchFallback":'${fallback_batched}'}}' + data='{"id":0,"method":"createLnurlWithdraw","params":{"msatoshi":'${msatoshi}',"description":"desc'${invoicenumber}'","expiresAt":"'${expiration}'","webhookUrl":"'${callbackurl}'/lnurl/inv'${invoicenumber}'","btcFallbackAddress":"'${fallback_addr}'","batchFallback":'${fallback_batched}'}}' trace 3 "[create_lnurl_withdraw] data=${data}" trace 3 "[create_lnurl_withdraw] Calling createLnurlWithdraw..." - local createLnurlWithdraw=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + local createLnurlWithdraw=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[create_lnurl_withdraw] createLnurlWithdraw=${createLnurlWithdraw}" # {"id":0,"result":{"msatoshi":100000000,"description":"desc01","expiresAt":"2021-07-15T12:12:23.112Z","secretToken":"abc01","webhookUrl":"https://webhookUrl01","lnurl":"LNURL1DP68GUP69UHJUMMWD9HKUW3CXQHKCMN4WFKZ7AMFW35XGUNPWAFX2UT4V4EHG0MN84SKYCESXYH8P25K","withdrawnDetails":null,"withdrawnTimestamp":null,"active":1,"lnurlWithdrawId":1,"createdAt":"2021-07-15 19:42:06","updatedAt":"2021-07-15 19:42:06"}} @@ -75,7 +90,7 @@ decode_lnurl() { local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' trace 3 "[decode_lnurl] data=${data}" - local decodedLnurl=$(curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + local decodedLnurl=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) trace 3 "[decode_lnurl] decodedLnurl=${decodedLnurl}" local url=$(echo "${decodedLnurl}" | jq -r ".result") trace 3 "[decode_lnurl] url=${url}" @@ -89,44 +104,40 @@ call_lnservice_withdraw_request() { local url=${1} trace 2 "[call_lnservice_withdraw_request] url=${url}" - local withdrawRequestResponse=$(curl -s ${url}) + local withdrawRequestResponse=$(exec_in_test_container curl -s ${url}) trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" } create_bolt11() { - trace 1 "\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}" + trace 2 "\n\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}\n" local msatoshi=${1} - trace 2 "[create_bolt11] msatoshi=${msatoshi}" - local label=${2} - trace 2 "[create_bolt11] label=${label}" - local desc=${3} - trace 2 "[create_bolt11] desc=${desc}" + trace 3 "[create_bolt11] msatoshi=${msatoshi}" + local desc=${2} + trace 3 "[create_bolt11] desc=${desc}" - local data='{"id":1,"jsonrpc": "2.0","method":"invoice","params":{"msatoshi":'${msatoshi}',"label":"'${label}'","description":"'${desc}'"}}' - trace 2 "[create_bolt11] data=${data}" - local invoice=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) - trace 2 "[create_bolt11] invoice=${invoice}" + local invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning invoice ${msatoshi} "${desc}" "${desc}") + trace 3 "[create_bolt11] invoice=${invoice}" echo "${invoice}" } get_invoice_status() { - trace 1 "\n[get_invoice_status] ${BCyan}Let's make sure the invoice is unpaid first...${Color_Off}" + trace 2 "\n\n[get_invoice_status] ${BCyan}Getting invoice status...${Color_Off}\n" local invoice=${1} - trace 2 "[get_invoice_status] invoice=${invoice}" + trace 3 "[get_invoice_status] invoice=${invoice}" local payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") - trace 2 "[get_invoice_status] payment_hash=${payment_hash}" - local data='{"id":1,"jsonrpc": "2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' - trace 2 "[get_invoice_status] data=${data}" - local invoices=$(curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) - trace 2 "[get_invoice_status] invoices=${invoices}" + trace 3 "[get_invoice_status] payment_hash=${payment_hash}" + local data='{"id":1,"jsonrpc":"2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' + trace 3 "[get_invoice_status] data=${data}" + local invoices=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning listinvoices -k payment_hash=${payment_hash}) + trace 3 "[get_invoice_status] invoices=${invoices}" local status=$(echo "${invoices}" | jq -r ".invoices[0].status") - trace 2 "[get_invoice_status] status=${status}" + trace 3 "[get_invoice_status] status=${status}" echo "${status}" } @@ -139,14 +150,14 @@ call_lnservice_withdraw() { local bolt11=${2} trace 2 "[call_lnservice_withdraw] bolt11=${bolt11}" - callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") + local callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") trace 2 "[call_lnservice_withdraw] callback=${callback}" k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") trace 2 "[call_lnservice_withdraw] k1=${k1}" trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" - trace 2 "url=${callback}?k1=${k1}\&pr=${bolt11}" - withdrawResponse=$(curl -s ${callback}?k1=${k1}\&pr=${bolt11}) + trace 2 "[call_lnservice_withdraw] url=${callback}?k1=${k1}\&pr=${bolt11}" + withdrawResponse=$(exec_in_test_container curl -s ${callback}?k1=${k1}\&pr=${bolt11}) trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" @@ -154,65 +165,72 @@ call_lnservice_withdraw() { TRACING=3 -trace 2 "${Color_Off}" date -# Install needed packages -trace 2 "\n${BCyan}Installing needed packages...${Color_Off}" -apk add curl jq +stop_test_container +start_test_container + +trace 1 "\n\n[lnurl-withdraw-wallet] ${BCyan}Installing needed packages...${Color_Off}\n" +exec_in_test_container_leave_lf apk add --update curl lnurl=${1} -trace 2 "lnurl=${lnurl}" +trace 2 "[lnurl-withdraw-wallet] lnurl=${lnurl}" bolt11=${2} -trace 2 "bolt11=${bolt11}" +trace 2 "[lnurl-withdraw-wallet] bolt11=${bolt11}" if [ "${lnurl}" = "createlnurl" ]; then # Initializing test variables - trace 2 "\n\n${BCyan}Initializing test variables...${Color_Off}\n" + trace 2 "\n\n[lnurl-withdraw-wallet] ${BCyan}Initializing test variables...${Color_Off}\n" # callbackservername="lnurl_withdraw_test" # callbackserverport="1111" # callbackurl="http://${callbackservername}:${callbackserverport}" - trace 3 "callbackurl=${callbackurl}" + trace 3 "[lnurl-withdraw-wallet] callbackurl=${callbackurl}" createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 600) - trace 3 "[fallback3] createLnurlWithdraw=${createLnurlWithdraw}" + trace 3 "[lnurl-withdraw-wallet] createLnurlWithdraw=${createLnurlWithdraw}" lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") - trace 3 "[fallback3] lnurl=${lnurl}" + trace 3 "[lnurl-withdraw-wallet] lnurl=${lnurl}" else url=$(decode_lnurl "${lnurl}") - trace 2 "url=${url}" + trace 2 "[lnurl-withdraw-wallet] url=${url}" withdrawRequestResponse=$(call_lnservice_withdraw_request "${url}") - trace 2 "withdrawRequestResponse=${withdrawRequestResponse}" + trace 2 "[lnurl-withdraw-wallet] withdrawRequestResponse=${withdrawRequestResponse}" # {"status":"ERROR","reason":"Expired LNURL-Withdraw"} reason=$(echo "${withdrawRequestResponse}" | jq -r ".reason // empty") if [ -n "${reason}" ]; then - trace 1 "\n\nERROR! Reason: ${reason}\n\n" - return 1 - fi + trace 1 "\n\n[lnurl-withdraw-wallet] ERROR! Reason: ${reason}\n\n" + else + msatoshi=$(echo "${withdrawRequestResponse}" | jq -r ".maxWithdrawable") + trace 2 "[lnurl-withdraw-wallet] msatoshi=${msatoshi}" + desc=$(echo "${withdrawRequestResponse}" | jq -r ".defaultDescription") + trace 2 "[lnurl-withdraw-wallet] desc=${desc}" - msatoshi=$(echo "${withdrawRequestResponse}" | jq -r ".maxWithdrawable") - trace 2 "msatoshi=${msatoshi}" - desc=$(echo "${withdrawRequestResponse}" | jq -r ".defaultDescription") - trace 2 "desc=${desc}" + if [ -z "${bolt11}" ]; then + invoice=$(create_bolt11 ${msatoshi} "${desc}") + trace 2 "[lnurl-withdraw-wallet] invoice=${invoice}" + bolt11=$(echo "${invoice}" | jq -r ".bolt11") - if [ -z "${bolt11}" ]; then - invoice=$(create_bolt11 ${msatoshi} "$RANDOM" "${desc}") - trace 2 "invoice=${invoice}" - bolt11=$(echo "${invoice}" | jq -r ".bolt11") + trace 2 "[lnurl-withdraw-wallet] bolt11=${bolt11}" + fi - trace 2 "bolt11=${bolt11}" - fi - - withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") - trace 2 "withdrawResponse=${withdrawResponse}" + withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") + trace 2 "[lnurl-withdraw-wallet] withdrawResponse=${withdrawResponse}" - reason=$(echo "${withdrawResponse}" | jq -r ".reason // empty") + reason=$(echo "${withdrawResponse}" | jq -r ".reason // empty") - if [ -n "${reason}" ]; then - trace 1 "\n\nERROR! Reason: ${reason}\n\n" - return 1 + if [ -n "${reason}" ]; then + trace 1 "\n\n[lnurl-withdraw-wallet] ERROR! Reason: ${reason}\n\n" + fi fi fi + +trace 1 "\n\n[lnurl-withdraw-wallet] ${BCyan}Tearing down...${Color_Off}\n" + +stop_test_container + +date + +trace 1 "\n\n[lnurl-withdraw-wallet] ${BCyan}See ya!${Color_Off}\n" diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index 198d0a1..2e90ded 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -243,7 +243,7 @@ call_lnservice_withdraw_request() { local url=${1} trace 2 "[call_lnservice_withdraw_request] url=${url}" - local withdrawRequestResponse=$(exec_in_test_container curl -s ${url}) + local withdrawRequestResponse=$(exec_in_test_container curl -sk ${url}) trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" @@ -296,7 +296,7 @@ call_lnservice_withdraw() { trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" trace 2 "[call_lnservice_withdraw] url=${callback}?k1=${k1}\&pr=${bolt11}" - withdrawResponse=$(exec_in_test_container curl -s ${callback}?k1=${k1}\&pr=${bolt11}) + withdrawResponse=$(exec_in_test_container curl -sk ${callback}?k1=${k1}\&pr=${bolt11}) trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" @@ -439,7 +439,7 @@ expired2() { local callbackurl=${1} # Service creates LNURL Withdraw - local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 10) trace 3 "[expired2] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "lnurl=${lnurl}" @@ -472,8 +472,8 @@ expired2() { local bolt11=$(echo ${invoice} | jq -r ".bolt11") trace 3 "[expired2] bolt11=${bolt11}" - trace 3 "[expired2] Sleeping 5 seconds..." - sleep 5 + trace 3 "[expired2] Sleeping 10 seconds..." + sleep 10 # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") @@ -502,7 +502,7 @@ deleted1() { local callbackurl=${1} # Service creates LNURL Withdraw - local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 10) trace 3 "[deleted1] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "lnurl=${lnurl}" @@ -578,7 +578,7 @@ deleted2() { local callbackurl=${1} # Service creates LNURL Withdraw - local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 5) + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 10) trace 3 "[deleted2] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "lnurl=${lnurl}" From 178723ee138401acfbda656e947956dc0bd97c8c Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 10 Aug 2022 14:07:29 -0400 Subject: [PATCH 36/52] Fix on detecting payment status --- src/lib/LnurlWithdraw.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 3e96d5b..6037eac 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -545,12 +545,16 @@ class LnurlWithdraw { // Error, should not happen, something's wrong, let's get out of here logger.debug("LnurlWithdraw.lnFetchPaymentStatus, lnListPays errored..."); } else if (resp.result && resp.result.pays && resp.result.pays.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - paymentStatus = (resp.result.pays[0] as any).status; - logger.debug( - "LnurlWithdraw.lnFetchPaymentStatus, paymentStatus =", - paymentStatus - ); + const nonfailedpay = resp.result.pays.find((obj) => { + return ((obj as any).status === "complete" || (obj as any).status === "pending"); + }) + logger.debug("LnurlWithdraw.lnFetchPaymentStatus, nonfailedpay =", nonfailedpay); + + if (nonfailedpay !== undefined) { + paymentStatus = (nonfailedpay as any).status; + } else { + paymentStatus = "failed"; + } result = resp.result; } else { From cfa064cbde93f430bbde0fb2e487a0e8ce9f4e77 Mon Sep 17 00:00:00 2001 From: kexkey Date: Sun, 8 Jan 2023 13:16:59 -0500 Subject: [PATCH 37/52] Small fixes in tests --- tests/ln_reconnect.sh | 15 +++++++++++++-- tests/ln_setup.sh | 14 +++++++------- tests/test-lnurl-withdraw.sh | 6 +++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/tests/ln_reconnect.sh b/tests/ln_reconnect.sh index b264c86..6249c24 100755 --- a/tests/ln_reconnect.sh +++ b/tests/ln_reconnect.sh @@ -8,13 +8,24 @@ ln_reconnect() { while true ; do ping -c 1 lightning2 ; [ "$?" -eq "0" ] && break ; sleep 5; done ' + local ln_getinfo + local ln2_getinfo + # Now check if the lightning nodes are ready to accept requests... while true ; do docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo ; [ "$?" -eq "0" ] && break ; sleep 5; done + ln_getinfo=$(docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo) while true ; do docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo ; [ "$?" -eq "0" ] && break ; sleep 5; done + ln2_getinfo=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo) # Ok, let's reconnect them! - docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning") - docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning connect $(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning2") + local id id2 port port2 + id=$(echo "$ln_getinfo" | jq -r ".id") + id2=$(echo "$ln2_getinfo" | jq -r ".id") + port=$(echo "$ln_getinfo" | jq -r ".binding[0].port") + port2=$(echo "$ln2_getinfo" | jq -r ".binding[0].port") + + docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning connect $id@lightning:$port + docker exec -it `docker ps -q -f "name=lightning\."` lightning-cli --lightning-dir=/.lightning connect $id2@lightning2:$port2 } case "${0}" in *ln_reconnect.sh) ln_reconnect $@;; esac diff --git a/tests/ln_setup.sh b/tests/ln_setup.sh index 1636065..cd7040a 100755 --- a/tests/ln_setup.sh +++ b/tests/ln_setup.sh @@ -7,7 +7,7 @@ date # Get node2 connection string -connectstring2=$(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning getinfo | jq -r ".id")@lightning2") +connectstring2=$(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli getinfo | jq -r ".id")@lightning2") echo ; echo "connectstring2=${connectstring2}" # Mine enough blocks @@ -15,12 +15,12 @@ echo ; echo "connectstring2=${connectstring2}" channelmsats=8000000000 # Fund LN node -address=$(docker exec -it `docker ps -q -f "name=proxy\."` curl localhost:8888/ln_newaddr | jq -r ".bech32") -echo ; echo "address=${address}" -data='{"address":"'${address}'","amount":1}' -docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/spend +#address=$(docker exec -it `docker ps -q -f "name=proxy\."` curl localhost:8888/ln_newaddr | jq -r ".bech32") +#echo ; echo "address=${address}" +#data='{"address":"'${address}'","amount":1}' +#docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/spend -mine 6 +#mine 6 echo ; echo "Sleeping 5 seconds..." sleep 5 @@ -47,7 +47,7 @@ desc="Invoice number ${invoicenumber}" echo ; echo "msats=${msats}" echo "label=${label}" echo "desc=${desc}" -invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning invoice ${msats} "${label}" "${desc}") +invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli invoice ${msats} "${label}" "${desc}") echo ; echo "invoice=${invoice}" # Pay to rebalance channel diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index 2e90ded..737dd91 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -257,7 +257,7 @@ create_bolt11() { local desc=${2} trace 3 "[create_bolt11] desc=${desc}" - local invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning invoice ${msatoshi} "${desc}" "${desc}") + local invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli invoice ${msatoshi} "${desc}" "${desc}") trace 3 "[create_bolt11] invoice=${invoice}" echo "${invoice}" @@ -273,7 +273,7 @@ get_invoice_status() { trace 3 "[get_invoice_status] payment_hash=${payment_hash}" local data='{"id":1,"jsonrpc":"2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' trace 3 "[get_invoice_status] data=${data}" - local invoices=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning listinvoices -k payment_hash=${payment_hash}) + local invoices=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli listinvoices -k payment_hash=${payment_hash}) trace 3 "[get_invoice_status] invoices=${invoices}" local status=$(echo "${invoices}" | jq -r ".invoices[0].status") trace 3 "[get_invoice_status] status=${status}" @@ -356,7 +356,7 @@ happy_path() { start_callback_server - trace 2 "\n\n[fallback2] ${BPurple}Waiting for the LNURL payment callback...\n${Color_Off}" + trace 2 "\n\n[happy_path] ${BPurple}Waiting for the LNURL payment callback...\n${Color_Off}" # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") From aeb62727aa844c3a5695e7c7973f1e6e95a8d53e Mon Sep 17 00:00:00 2001 From: Michael Little Date: Thu, 26 Jan 2023 18:11:29 +1100 Subject: [PATCH 38/52] initial LNURL-pay commit --- cypherapps/data/config.json | 45 +- .../20230126070420_lnurl/migration.sql | 41 + prisma/schema.prisma | 33 + src/config/LnurlConfig.ts | 3 + src/lib/CyphernodeClient.ts | 39 + src/lib/HttpServer.ts | 224 ++++++ src/lib/LnAddress.ts | 63 ++ src/lib/LnurlDBPrisma.ts | 83 ++- src/lib/LnurlPay.ts | 702 ++++++++++++++++++ src/lib/Utils.ts | 43 +- src/types/ILnurlPay.ts | 5 + src/types/IReqCreateLnurlPay.ts | 7 + src/types/IReqCreateLnurlPayRequest.ts | 4 + src/types/IReqLnurlPayRequestCallback.ts | 3 + src/types/IReqPayLnAddress.ts | 4 + src/types/IReqUpdateLnurlPay.ts | 6 + src/types/IReqViewLnurlPay.ts | 3 + src/types/IRespLnurlPay.ts | 7 + src/types/IRespLnurlPayRequest.ts | 15 + src/types/IRespLnurlPayRequestCallback.ts | 6 + src/types/IRespPayLnAddress.ts | 6 + src/types/SaveLnurlPayRequestWhere.ts | 6 + src/types/cyphernode/IReqLnCreate.ts | 13 + src/types/cyphernode/IRespLnCreate.ts | 12 + .../CreateLnurlPayRequestValidator.ts | 20 + src/validators/CreateLnurlPayValidator.ts | 17 + src/validators/UpdateLnurlPayValidator.ts | 18 + 27 files changed, 1395 insertions(+), 33 deletions(-) create mode 100644 prisma/migrations/20230126070420_lnurl/migration.sql create mode 100644 src/lib/LnAddress.ts create mode 100644 src/lib/LnurlPay.ts create mode 100644 src/types/ILnurlPay.ts create mode 100644 src/types/IReqCreateLnurlPay.ts create mode 100644 src/types/IReqCreateLnurlPayRequest.ts create mode 100644 src/types/IReqLnurlPayRequestCallback.ts create mode 100644 src/types/IReqPayLnAddress.ts create mode 100644 src/types/IReqUpdateLnurlPay.ts create mode 100644 src/types/IReqViewLnurlPay.ts create mode 100644 src/types/IRespLnurlPay.ts create mode 100644 src/types/IRespLnurlPayRequest.ts create mode 100644 src/types/IRespLnurlPayRequestCallback.ts create mode 100644 src/types/IRespPayLnAddress.ts create mode 100644 src/types/SaveLnurlPayRequestWhere.ts create mode 100644 src/types/cyphernode/IReqLnCreate.ts create mode 100644 src/types/cyphernode/IRespLnCreate.ts create mode 100644 src/validators/CreateLnurlPayRequestValidator.ts create mode 100644 src/validators/CreateLnurlPayValidator.ts create mode 100644 src/validators/UpdateLnurlPayValidator.ts diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json index c34eb3e..659a513 100644 --- a/cypherapps/data/config.json +++ b/cypherapps/data/config.json @@ -1,22 +1,25 @@ { - "LOG": "DEBUG", - "BASE_DIR": "/lnurl", - "DATA_DIR": "data", - "DB_NAME": "lnurl.sqlite", - "URL_API_SERVER": "http://lnurl", - "URL_API_PORT": 8000, - "URL_API_CTX": "/api", - "URL_CTX_WEBHOOKS": "/webhooks", - "SESSION_TIMEOUT": 600, - "CN_URL": "https://gatekeeper:2009/v0", - "CN_API_ID": "003", - "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", - "BATCHER_URL": "http://batcher:8000", - "LN_SERVICE_SERVER": "https://yourdomain", - "LN_SERVICE_PORT": 443, - "LN_SERVICE_CTX": "/lnurl", - "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", - "LN_SERVICE_WITHDRAW_CTX": "/withdraw", - "RETRY_WEBHOOKS_TIMEOUT": 1, - "CHECK_EXPIRATION_TIMEOUT": 1 -} \ No newline at end of file + "LOG": "DEBUG", + "BASE_DIR": "/lnurl", + "DATA_DIR": "data", + "DB_NAME": "lnurl.sqlite", + "URL_API_SERVER": "http://lnurl", + "URL_API_PORT": 8000, + "URL_API_CTX": "/api", + "URL_CTX_WEBHOOKS": "/webhooks", + "SESSION_TIMEOUT": 600, + "CN_URL": "https://gatekeeper:2009/v0", + "CN_API_ID": "003", + "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", + "BATCHER_URL": "http://batcher:8000", + "LN_SERVICE_SERVER": "https://yourdomain", + "LN_SERVICE_PORT": 443, + "LN_SERVICE_CTX": "/lnurl", + "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", + "LN_SERVICE_WITHDRAW_CTX": "/withdraw", + "LN_SERVICE_PAY_CTX": "/pay", + "LN_SERVICE_PAY_REQUEST_CTX": "/payRequest", + "LN_SERVICE_PAY_CB_CTX": "/payReqCallback", + "RETRY_WEBHOOKS_TIMEOUT": 1, + "CHECK_EXPIRATION_TIMEOUT": 1 +} diff --git a/prisma/migrations/20230126070420_lnurl/migration.sql b/prisma/migrations/20230126070420_lnurl/migration.sql new file mode 100644 index 0000000..15ff7d9 --- /dev/null +++ b/prisma/migrations/20230126070420_lnurl/migration.sql @@ -0,0 +1,41 @@ +-- CreateTable +CREATE TABLE "LnurlPayEntity" ( + "lnurlPayId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "externalId" TEXT NOT NULL, + "minMsatoshi" INTEGER NOT NULL DEFAULT 1, + "maxMsatoshi" INTEGER NOT NULL DEFAULT 1, + "description" TEXT, + "webhookUrl" TEXT, + "lnurl" TEXT NOT NULL, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "createdTs" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedTs" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "LnurlPayRequestEntity" ( + "lnurlPayRequestId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "lnurlPayEntityId" INTEGER NOT NULL, + "bolt11Label" TEXT NOT NULL, + "msatoshi" INTEGER NOT NULL, + "bolt11" TEXT, + "metadata" TEXT, + "paid" BOOLEAN NOT NULL DEFAULT false, + "paidCalledbackTs" DATETIME, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "createdTs" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedTs" DATETIME NOT NULL, + FOREIGN KEY ("lnurlPayEntityId") REFERENCES "LnurlPayEntity" ("lnurlPayId") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "LnurlPayEntity.externalId_unique" ON "LnurlPayEntity"("externalId"); + +-- CreateIndex +CREATE INDEX "LnurlPayEntity.externalId_index" ON "LnurlPayEntity"("externalId"); + +-- CreateIndex +CREATE UNIQUE INDEX "LnurlPayRequestEntity.bolt11Label_unique" ON "LnurlPayRequestEntity"("bolt11Label"); + +-- CreateIndex +CREATE INDEX "LnurlPayRequestEntity.bolt11_index" ON "LnurlPayRequestEntity"("bolt11"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8be2503..b87d750 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -40,3 +40,36 @@ model LnurlWithdrawEntity { @@index([bolt11]) @@index([btcFallbackAddress]) } + +model LnurlPayEntity { + lnurlPayId Int @id @default(autoincrement()) + externalId String @unique + minMsatoshi Int @default(1) + maxMsatoshi Int @default(1) + description String? + webhookUrl String? + lnurl String + requests LnurlPayRequestEntity[] + deleted Boolean @default(false) + createdTs DateTime @default(now()) + updatedTs DateTime @updatedAt + + @@index([externalId]) +} + +model LnurlPayRequestEntity { + lnurlPayRequestId Int @id @default(autoincrement()) + lnurlPay LnurlPayEntity @relation(fields: [lnurlPayEntityId], references: [lnurlPayId]) + lnurlPayEntityId Int + bolt11Label String @unique + msatoshi Int + bolt11 String? + metadata String? + paid Boolean @default(false) + paidCalledbackTs DateTime? + deleted Boolean @default(false) + createdTs DateTime @default(now()) + updatedTs DateTime @updatedAt + + @@index([bolt11]) +} \ No newline at end of file diff --git a/src/config/LnurlConfig.ts b/src/config/LnurlConfig.ts index 88aeffe..4e4c060 100644 --- a/src/config/LnurlConfig.ts +++ b/src/config/LnurlConfig.ts @@ -17,6 +17,9 @@ export default interface LnurlConfig { LN_SERVICE_CTX: string; LN_SERVICE_WITHDRAW_REQUEST_CTX: string; LN_SERVICE_WITHDRAW_CTX: string; + LN_SERVICE_PAY_CTX: string; + LN_SERVICE_PAY_REQUEST_CTX: string; + LN_SERVICE_PAY_CB_CTX: string; RETRY_WEBHOOKS_TIMEOUT: number; CHECK_EXPIRATION_TIMEOUT: number; } diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 78d25d3..5966788 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -19,6 +19,8 @@ import IRespLnPay from "../types/cyphernode/IRespLnPay"; import IRespLnListPays from "../types/cyphernode/IRespLnListPays"; import IReqLnListPays from "../types/cyphernode/IReqLnListPays"; import IRespLnPayStatus from "../types/cyphernode/IRespLnPayStatus"; +import IReqLnCreate from "../types/cyphernode/IReqLnCreate"; +import IRespLnCreate from "../types/cyphernode/IRespLnCreate"; class CyphernodeClient { private baseURL: string; @@ -759,6 +761,43 @@ class CyphernodeClient { } return result; } + + async lnCreate(lnCreate: IReqLnCreate): Promise { + // POST http://192.168.111.152:8080/ln_create_invoice + // BODY {"msatoshi":10000,"label":"1234", "description":"Bitcoin Outlet order #7082", "expiry":900} + + // args: + // - msatoshi, required, amount we want to recieve + // - label, required, unique label to identify the bolt11 invoice + // - description, required, description to be encoded in the bolt11 invoice + // - expiry, optional, expiry time in seconds + // - callbackUrl, optional, callback for invoice updates / payment + // + // Example of successful result: + // + // { + // "payment_hash": "fd27edf261d4b089c3478dece4f2c92c8c68db7be3999e89d452d39c083ad00f", + // "expires_at": 1536593926, + // "bolt11": "lntb100n1pdedryzpp5l5n7munp6jcgns683hkwfukf9jxx3kmmuwveazw52tfeczp66q8sdqagfukcmrnyphhyer9wgszxvfsxc6rjxqzuycqp2ak5feh7x7wkkt76uc5ptzcv90jhzhs5swzefv9344hnv74c25dvsstx7l24y46sx5tnkenu480pe06wtly2h5lrj63vszzgrxt4grkcqcltquj" + // } + + logger.info("CyphernodeClient.lnCreate:", lnCreate); + + let result: IRespLnCreate; + const response = await this._post("/ln_create_invoice", lnCreate); + if (response.status >= 200 && response.status < 400) { + result = { result: response.data }; + } else { + result = { + error: { + code: ErrorCodes.InternalError, + message: response.data.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespLnCreate; + } + return result; + } } export { CyphernodeClient }; diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 8d3c7e4..09b8bee 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -4,6 +4,7 @@ import logger from "./Log2File"; import LnurlConfig from "../config/LnurlConfig"; import fs from "fs"; import { LnurlWithdraw } from "./LnurlWithdraw"; +import { LnurlPay } from "./LnurlPay"; import { IResponseMessage, ErrorCodes, @@ -14,6 +15,15 @@ import IRespCreateLnurlWithdraw from "../types/IRespLnurlWithdraw"; import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; import IReqLnurlWithdraw from "../types/IReqLnurlWithdraw"; import IRespLnServiceWithdrawRequest from "../types/IRespLnServiceWithdrawRequest"; +import IRespCreateLnurlPay from "../types/IRespLnurlPay"; +import IReqCreateLnurlPay from "../types/IReqCreateLnurlPay"; +import IReqViewLnurlPay from "../types/IReqViewLnurlPay"; +import IReqCreateLnurlPayRequest from "../types/IReqCreateLnurlPayRequest"; +import IReqLnurlPayRequestCallback from "../types/IReqLnurlPayRequestCallback"; +import IRespLnurlPay from "../types/IRespLnurlPay"; +import IRespLnurlPayRequest from "../types/IRespLnurlPayRequest"; +import IReqUpdateLnurlPay from "../types/IReqUpdateLnurlPay"; +import IRespPayLnAddress from "../types/IRespPayLnAddress"; class HttpServer { // Create a new express application instance @@ -22,6 +32,7 @@ class HttpServer { fs.readFileSync("data/config.json", "utf8") ); private _lnurlWithdraw: LnurlWithdraw = new LnurlWithdraw(this._lnurlConfig); + private _lnurlPay: LnurlPay = new LnurlPay(this._lnurlConfig); setup(): void { logger.debug("setup"); @@ -34,6 +45,7 @@ class HttpServer { this._lnurlConfig = JSON.parse(fs.readFileSync("data/config.json", "utf8")); this._lnurlWithdraw.configureLnurl(this._lnurlConfig); + this._lnurlPay.configureLnurl(this._lnurlConfig); } async createLnurlWithdraw( @@ -81,6 +93,72 @@ class HttpServer { return await this._lnurlWithdraw.getLnurlWithdraw(lnurlWithdrawId); } + async createLnurlPay( + params: object | undefined + ): Promise { + logger.debug("/createLnurlPay params:", params); + + const reqCreateLnurlPay: IReqCreateLnurlPay = params as IReqCreateLnurlPay; + + return await this._lnurlPay.createLnurlPay(reqCreateLnurlPay); + } + + async updateLnurlPay(params: object | undefined): Promise { + logger.debug("/updateLnurlPay params:", params); + + const reqUpdateLnurlPay: IReqUpdateLnurlPay = params as IReqUpdateLnurlPay; + + return await this._lnurlPay.updateLnurlPay(reqUpdateLnurlPay); + } + + async deleteLnurlPay(params: object | undefined): Promise { + logger.debug("/deleteLnurlPay params:", params); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lnurlPayId = parseInt((params as any).lnurlPayId); + + return await this._lnurlPay.deleteLnurlPay(lnurlPayId); + } + + async getLnurlPay(params: object | undefined): Promise { + logger.debug("/getLnurlPay params:", params); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lnurlPayId = parseInt((params as any).lnurlPayId); + + return await this._lnurlPay.getLnurlPay(lnurlPayId); + } + + async deleteLnurlPayRequest( + params: object | undefined + ): Promise { + logger.debug("/deleteLnurlPayRequest params:", params); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lnurlPayRequestId = parseInt((params as any).lnurlPayRequestId); + + return await this._lnurlPay.deleteLnurlPayRequest(lnurlPayRequestId); + } + + async getLnurlPayRequest( + params: object | undefined + ): Promise { + logger.debug("/getLnurlPayRequest params:", params); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lnurlPayRequestId = parseInt((params as any).lnurlPayRequestId); + + return await this._lnurlPay.getLnurlPayRequest(lnurlPayRequestId); + } + + async payLnAddress(params: object | undefined): Promise { + logger.debug("/payLnAddress params:", params); + + const reqpayLnAddress: IReqPayLnAddress = params as IReqPayLnAddress; + + return await this._lnurlPay.payLnAddress(reqpayLnAddress); + } + async start(): Promise { logger.info("Starting incredible service"); @@ -127,6 +205,70 @@ class HttpServer { break; } + case "createLnurlPay": { + const result: IRespCreateLnurlPay = await this.createLnurlPay( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + + case "updateLnurlPay": { + const result: IRespLnurlPay = await this.updateLnurlPay( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + + case "getLnurlPay": { + const result: IRespLnurlPay = await this.getLnurlPay( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + + case "deleteLnurlPay": { + const result: IRespLnurlPay = await this.deleteLnurlPay( + reqMessage.params || {} + ); + + response.result = result.result; + response.error = result.error; + break; + } + + case "getLnurlPayRequest": { + const result: IRespLnurlPayRequest = await this.getLnurlPayRequest( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + + case "deleteLnurlPayRequest": { + const result: IRespLnurlPayRequest = await this.getLnurlPayRequest( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + + case "payLnAddress": { + const result: IRespPayLnAddress = await this.payLnAddress( + reqMessage.params || {} + ); + response.result = result.result; + response.error = result.error; + break; + } + case "processCallbacks": { this._lnurlWithdraw.processCallbacks(); @@ -234,6 +376,88 @@ class HttpServer { } ); + // LN Service LNURL Pay request (step 3) + this._httpServer.get( + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_PAY_CTX + + "/:externalId", + async (req, res) => { + logger.info(this._lnurlConfig.LN_SERVICE_PAY_CTX + ":", req.params); + + const response = await this._lnurlPay.viewLnurlPay({ + externalId: req.params.externalId, + } as IReqViewLnurlPay); + + if (response.status === "ERROR") { + res.status(400).json(response); + } else { + res.status(200).json(response); + } + } + ); + + // LN Service LNURL Pay request (step 3) lightning address format + this._httpServer.get( + "/.well-known/lnurlp/:externalId", + async (req, res) => { + logger.info("/.well-known/lnurlp/:", req.params); + + const response = await this._lnurlPay.viewLnurlPay({ + externalId: req.params.externalId, + } as IReqViewLnurlPay); + + if (response.status === "ERROR") { + res.status(400).json(response); + } else { + res.status(200).json(response); + } + } + ); + + // LN Service LNURL Pay request (step 5) + this._httpServer.get( + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + + "/:externalId", + async (req, res) => { + logger.info( + this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + ":", + req.params + ); + + const response = await this._lnurlPay.createLnurlPayRequest({ + externalId: req.params.externalId, + amount: req.query.amount, + } as IReqCreateLnurlPayRequest); + + if (response.status === "ERROR") { + res.status(400).json(response); + } else { + res.status(200).json(response); + } + } + ); + + // LN Service LNURL Pay request callback (called when bolt11 paid) + this._httpServer.post( + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + + "/:label", + async (req, res) => { + logger.info(this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + ":", req.params); + + const response = await this._lnurlPay.lnurlPayRequestCallback({ + bolt11Label: req.params.label, + } as IReqLnurlPayRequestCallback); + + if (response.error) { + res.status(400).json(response); + } else { + res.status(200).json(response); + } + } + ); + this._httpServer.post( this._lnurlConfig.URL_CTX_WEBHOOKS, async (req, res) => { diff --git a/src/lib/LnAddress.ts b/src/lib/LnAddress.ts new file mode 100644 index 0000000..6fd494d --- /dev/null +++ b/src/lib/LnAddress.ts @@ -0,0 +1,63 @@ +import logger from "./Log2File"; +import { URL } from "url"; +import { Utils } from "./Utils"; + +class LnAddress { + static addressToUrl(address: string): string | false { + //const LNAddressRE = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + const LNAddressRE = /([^@]+)@(.+)/; + const m = address.match(LNAddressRE); + logger.debug("matches", m); + //if (m && m.length >= 6) { + if (m && m.length >= 2) { + logger.info("LnAddress: success ", address); + + //return `https://${m[5]}/.well-known/lnurlp/${m[1]}`; + return `http://${m[2]}/.well-known/lnurlp/${m[1]}`; + } + + return false; + } + + static async fetchBolt11( + address: string, + amount: number + ): Promise { + const url = LnAddress.addressToUrl(address); + + if (url) { + logger.debug("calling", url); + const resp = await Utils.get(url); + logger.debug("resp", resp.data.callback); + if (resp.status >= 200 && resp.status < 400) { + logger.debug("lnurl called", resp.data); + if (resp.data.callback && resp.data.tag == "payRequest") { + const cbUrl = new URL(resp.data.callback); + cbUrl.searchParams.set("amount", String(amount)); + logger.debug("calling url ", cbUrl); + const cbResp = await Utils.get(cbUrl.toString()); + logger.debug("cbResp", cbResp); + if (cbResp.status >= 200 && cbResp.status < 400) { + if (cbResp.data && cbResp.data.pr) { + return cbResp.data.pr; + } else { + logger.debug("fetchBolt11: no valid bolt11 invoice provided"); + } + } else { + logger.debug("fetchBolt11: failed calling callback url"); + } + } else { + logger.debug("fetchBolt11: no callback url provided"); + } + } else { + logger.debug("fetchBolt11: failed calling lnurl", url); + } + } else { + logger.debug("fetchBolt11: not a valid lightning address"); + } + + return false; + } +} + +export { LnAddress }; diff --git a/src/lib/LnurlDBPrisma.ts b/src/lib/LnurlDBPrisma.ts index d19bb8b..ff47ff4 100644 --- a/src/lib/LnurlDBPrisma.ts +++ b/src/lib/LnurlDBPrisma.ts @@ -1,7 +1,13 @@ import logger from "./Log2File"; import path from "path"; import LnurlConfig from "../config/LnurlConfig"; -import { LnurlWithdrawEntity, PrismaClient } from "@prisma/client"; +import { + LnurlPayEntity, + LnurlPayRequestEntity, + LnurlWithdrawEntity, + PrismaClient, +} from "@prisma/client"; +import { SaveLnurlPayRequestWhere } from "../types/SaveLnurlPayRequestWhere"; class LnurlDBPrisma { private _db?: PrismaClient; @@ -144,6 +150,81 @@ class LnurlDBPrisma { return lws as LnurlWithdrawEntity[]; } + + async saveLnurlPay(lnurlPayEntity: LnurlPayEntity): Promise { + const lw = await this._db?.lnurlPayEntity.upsert({ + where: { externalId: lnurlPayEntity.externalId }, + update: lnurlPayEntity, + create: lnurlPayEntity, + }); + + return lw as LnurlPayEntity; + } + + async getLnurlPayById(lnurlPayId: number): Promise { + const lw = await this._db?.lnurlPayEntity.findUnique({ + where: { lnurlPayId: lnurlPayId }, + }); + + return lw as LnurlPayEntity; + } + + async getLnurlPayByExternalId(externalId: string): Promise { + const lw = await this._db?.lnurlPayEntity.findUnique({ + where: { externalId }, + }); + + return lw as LnurlPayEntity; + } + + async saveLnurlPayRequest( + lnurlPayRequestEntity: LnurlPayRequestEntity + ): Promise { + const where: SaveLnurlPayRequestWhere = {}; + if (lnurlPayRequestEntity.lnurlPayRequestId) { + where.lnurlPayRequestId = lnurlPayRequestEntity.lnurlPayRequestId; + } else { + where.bolt11Label = lnurlPayRequestEntity.bolt11Label; + } + + const lw = await this._db?.lnurlPayRequestEntity.upsert({ + where, + update: lnurlPayRequestEntity, + create: lnurlPayRequestEntity, + }); + + return lw as LnurlPayRequestEntity; + } + + async getLnurlPayRequestById( + lnurlPayRequestId: number + ): Promise { + const lw = await this._db?.lnurlPayRequestEntity.findUnique({ + where: { lnurlPayRequestId: lnurlPayRequestId }, + }); + + return lw as LnurlPayRequestEntity; + } + + async getLnurlPayRequestByLabel( + bolt11Label: string + ): Promise { + const lw = await this._db?.lnurlPayRequestEntity.findUnique({ + where: { bolt11Label }, + }); + + return lw as LnurlPayRequestEntity; + } + + async getLnurlPayRequestByPayId( + lnurlPayId: number + ): Promise { + const lw = await this._db?.lnurlPayRequestEntity.findMany({ + where: { lnurlPayEntityId: lnurlPayId }, + }); + + return lw as LnurlPayRequestEntity[]; + } } export { LnurlDBPrisma as LnurlDB }; diff --git a/src/lib/LnurlPay.ts b/src/lib/LnurlPay.ts new file mode 100644 index 0000000..7a56e08 --- /dev/null +++ b/src/lib/LnurlPay.ts @@ -0,0 +1,702 @@ +import logger from "./Log2File"; +import crypto from "crypto"; +import LnurlConfig from "../config/LnurlConfig"; +import { CyphernodeClient } from "./CyphernodeClient"; +import { LnurlDB } from "./LnurlDBPrisma"; +import { ErrorCodes } from "../types/jsonrpc/IResponseMessage"; +import IReqCreateLnurlPay from "../types/IReqCreateLnurlPay"; +import IReqViewLnurlPay from "../types/IReqViewLnurlPay"; +import IRespLnurlPay from "../types/IRespLnurlPay"; +import { CreateLnurlPayValidator } from "../validators/CreateLnurlPayValidator"; +import IReqCreateLnurlPayRequest from "../types/IReqCreateLnurlPayRequest"; +import IRespLnurlPayRequest from "../types/IRespLnurlPayRequest"; +import { CreateLnurlPayRequestValidator } from "../validators/CreateLnurlPayRequestValidator"; +import IRespLnCreate from "../types/cyphernode/IRespLnCreate"; +import { Utils } from "./Utils"; +import { LnurlPayEntity, LnurlPayRequestEntity } from "@prisma/client"; +import AsyncLock from "async-lock"; +import IRespLnurlPayRequestCallback from "../types/IRespLnurlPayRequestCallback"; +import IReqLnurlPayRequestCallback from "../types/IReqLnurlPayRequestCallback"; +import IReqUpdateLnurlPay from "../types/IReqUpdateLnurlPay"; +import { UpdateLnurlPayValidator } from "../validators/UpdateLnurlPayValidator"; +import IRespLnPay from "../types/cyphernode/IRespLnPay"; +import { LnAddress } from "./LnAddress"; +import IRespPayLnAddress from "../types/IRespPayLnAddress"; + +class LnurlPay { + private _lnurlConfig: LnurlConfig; + private _cyphernodeClient: CyphernodeClient; + private _lnurlDB: LnurlDB; + private readonly _lock = new AsyncLock(); + + constructor(lnurlConfig: LnurlConfig) { + this._lnurlConfig = lnurlConfig; + this._cyphernodeClient = new CyphernodeClient(this._lnurlConfig); + this._lnurlDB = new LnurlDB(this._lnurlConfig); + } + + configureLnurl(lnurlConfig: LnurlConfig): void { + this._lnurlConfig = lnurlConfig; + this._lnurlDB.configureDB(this._lnurlConfig).then(() => { + this._cyphernodeClient.configureCyphernode(this._lnurlConfig); + }); + } + + lnurlPayUrl(externalId: string, req = false): string { + return ( + this._lnurlConfig.LN_SERVICE_SERVER + + (this._lnurlConfig.LN_SERVICE_PORT === 443 + ? "" + : ":" + this._lnurlConfig.LN_SERVICE_PORT) + + this._lnurlConfig.LN_SERVICE_CTX + + (req + ? this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + : this._lnurlConfig.LN_SERVICE_PAY_CTX) + + "/" + + externalId + ); + } + + async createLnurlPay( + reqCreateLnurlPay: IReqCreateLnurlPay + ): Promise { + logger.info( + "LnurlPay.createLnurlPay, reqCreateLnurlPay:", + reqCreateLnurlPay + ); + + const response: IRespLnurlPay = {}; + + if (CreateLnurlPayValidator.validateRequest(reqCreateLnurlPay)) { + // Inputs are valid. + logger.debug("LnurlPay.createLnurlPay, Inputs are valid."); + + const lnurlDecoded = this.lnurlPayUrl(reqCreateLnurlPay.externalId); + + const lnurl = await Utils.encodeBech32(lnurlDecoded); + + let lnurlPayEntity: LnurlPayEntity; + try { + lnurlPayEntity = await this._lnurlDB.saveLnurlPay( + Object.assign(reqCreateLnurlPay as LnurlPayEntity, { + lnurl: lnurl, + }) + ); + } catch (ex) { + logger.debug("ex:", ex); + + response.error = { + code: ErrorCodes.InvalidRequest, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message: (ex as any).message, + }; + return response; + } + + if (lnurlPayEntity) { + logger.debug( + "LnurlPay.createLnurlPay, lnurlPay created:", + lnurlPayEntity + ); + + response.result = Object.assign(lnurlPayEntity, { + lnurlDecoded, + }); + } else { + // LnurlPay not created + logger.debug("LnurlPay.createLnurlPay, LnurlPay not created."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPay not created", + }; + } + } else { + // There is an error with inputs + logger.debug("LnurlPay.createLnurlPay, there is an error with inputs."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + + async updateLnurlPay( + reqUpdateLnurlPay: IReqUpdateLnurlPay + ): Promise { + logger.info( + "LnurlPay.updateLnurlPay, reqCreateLnurlPay:", + reqUpdateLnurlPay + ); + + const response: IRespLnurlPay = {}; + + if (UpdateLnurlPayValidator.validateRequest(reqUpdateLnurlPay)) { + // Inputs are valid. + logger.debug("LnurlPay.updateLnurlPay, Inputs are valid."); + + let lnurlPayEntity: LnurlPayEntity = await this._lnurlDB.getLnurlPayById( + reqUpdateLnurlPay.lnurlPayId + ); + + if (lnurlPayEntity) { + try { + lnurlPayEntity = await this._lnurlDB.saveLnurlPay( + Object.assign(lnurlPayEntity, reqUpdateLnurlPay) + ); + } catch (ex) { + logger.debug("ex:", ex); + + response.error = { + code: ErrorCodes.InvalidRequest, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message: (ex as any).message, + }; + return response; + } + + if (lnurlPayEntity) { + logger.debug( + "LnurlPay.createLnurlPay, lnurlPay created:", + lnurlPayEntity + ); + + const lnurlDecoded = await Utils.decodeBech32(lnurlPayEntity.lnurl); + + response.result = Object.assign(lnurlPayEntity, { + lnurlDecoded, + }); + } else { + // LnurlPay not updated + logger.debug("LnurlPay.updateLnurlPay, LnurlPay not updated."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPay not updated", + }; + } + } else { + logger.debug("LnurlPay.updateLnurlPay, lnurlPay not found"); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPay not found", + }; + } + } else { + // There is an error with inputs + logger.debug("LnurlPay.updatedLnurlPay, there is an error with inputs."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + + async deleteLnurlPay(lnurlPayId: number): Promise { + const result: IRespLnurlPay = await this._lock.acquire( + "modifLnurlPay", + async (): Promise => { + logger.debug("acquired lock modifLnurlPay in deleteLnurlPay"); + + logger.info("LnurlPay.deleteLnurlPay, lnurlPayId:", lnurlPayId); + + const response: IRespLnurlPay = {}; + + if (lnurlPayId) { + // Inputs are valid. + logger.debug("LnurlPay.deleteLnurlPay, Inputs are valid."); + + let lnurlPayEntity = await this._lnurlDB.getLnurlPayById(lnurlPayId); + + // if (lnurlPayEntity != null && lnurlPayEntity.active) { + if (lnurlPayEntity == null) { + logger.debug("LnurlPay.deleteLnurlPay, lnurlPay not found"); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPay not found", + }; + } else if (!lnurlPayEntity.deleted) { + logger.debug( + "LnurlPay.deleteLnurlPay, unpaid lnurlPayEntity found for this lnurlPayId!" + ); + + lnurlPayEntity.deleted = true; + lnurlPayEntity = await this._lnurlDB.saveLnurlPay(lnurlPayEntity); + + const lnurlDecoded = await Utils.decodeBech32( + lnurlPayEntity?.lnurl || "" + ); + + response.result = Object.assign(lnurlPayEntity, { + lnurlDecoded, + }); + } else { + // LnurlPay already deactivated + logger.debug( + "LnurlPay.deleteLnurlPay, LnurlPay already deactivated." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPay already deactivated", + }; + } + } else { + // There is an error with inputs + logger.debug( + "LnurlPay.deleteLnurlPay, there is an error with inputs." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + ); + logger.debug("released lock modifLnurlPay in deleteLnurlPay"); + return result; + } + + async getLnurlPay(lnurlPayId: number): Promise { + logger.info("LnurlPay.getLnurlPay, lnurlPayId:", lnurlPayId); + + const response: IRespLnurlPay = {}; + + if (lnurlPayId) { + // Inputs are valid. + logger.debug("LnurlPay.getLnurlPay, Inputs are valid."); + + const lnurlPayEntity = await this._lnurlDB.getLnurlPayById(lnurlPayId); + + if (lnurlPayEntity != null) { + logger.debug( + "LnurlPay.getLnurlPay, lnurlPayEntity found for this lnurlPayId!" + ); + + const lnurlDecoded = await Utils.decodeBech32( + lnurlPayEntity.lnurl || "" + ); + + response.result = Object.assign(lnurlPayEntity, { + lnurlDecoded, + }); + } else { + // Active LnurlPay not found + logger.debug("LnurlPay.getLnurlPay, LnurlPay not found."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPay not found", + }; + } + } else { + // There is an error with inputs + logger.debug("LnurlPay.getLnurlPay, there is an error with inputs."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + + //// payRequest !!!! + async viewLnurlPay( + reqViewLnurlPay: IReqViewLnurlPay + ): Promise { + logger.info("LnurlPay.viewLnurlPay, reqViewLnurlPay:", reqViewLnurlPay); + + let response: IRespLnurlPayRequest = {}; + const lnurlPay: LnurlPayEntity = await this._lnurlDB.getLnurlPayByExternalId( + reqViewLnurlPay.externalId + ); + + if (lnurlPay && lnurlPay.externalId) { + const metadata = JSON.stringify([["text/plain", lnurlPay.description]]); + + response = { + callback: this.lnurlPayUrl(lnurlPay.externalId, true), + maxSendable: lnurlPay.maxMsatoshi, + minSendable: lnurlPay.minMsatoshi, + metadata: metadata, + tag: "payRequest", + }; + } else { + // There is an error with inputs + logger.debug( + "LnurlPay.createLnurlPayRequest, there is an error with inputs." + ); + + response = { + status: "ERROR", + reason: "Invalid arguments", + }; + } + + return response; + } + + async createLnurlPayRequest( + reqCreateLnurlPayReq: IReqCreateLnurlPayRequest + ): Promise { + logger.info( + "LnurlPay.createLnurlPayRequest, reqCreateLnurlPayReq:", + reqCreateLnurlPayReq + ); + + let response: IRespLnurlPayRequest = {}; + const lnurlPay: LnurlPayEntity = await this._lnurlDB.getLnurlPayByExternalId( + reqCreateLnurlPayReq.externalId + ); + + if ( + lnurlPay && + CreateLnurlPayRequestValidator.validateRequest( + lnurlPay, + reqCreateLnurlPayReq + ) + ) { + // Inputs are valid. + logger.debug("LnurlPay.createLnurlPayRequest, Inputs are valid."); + + const metadata = JSON.stringify([["text/plain", lnurlPay.description]]); + const hHash = crypto.createHmac("sha256", metadata).digest("hex"); + const label = lnurlPay.lnurlPayId + "-" + Date.now(); + + const lnCreateParams = { + msatoshi: reqCreateLnurlPayReq.amount as number, + label: label, + description: hHash, + callbackUrl: + this._lnurlConfig.LN_SERVICE_SERVER + + (this._lnurlConfig.LN_SERVICE_PORT === 443 + ? "" + : ":" + this._lnurlConfig.LN_SERVICE_PORT) + + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + + "/" + + label, + }; + + logger.debug( + "LnurlPay.createLnurlPayRequest trying to get invoice", + lnCreateParams + ); + + const resp: IRespLnCreate = await this._cyphernodeClient.lnCreate( + lnCreateParams + ); + logger.debug("LnurlPay.createLnurlPayRequest lnCreate invoice", resp); + + if (resp.result) { + const data = { + lnurlPayEntityId: lnurlPay.lnurlPayId, + bolt11Label: label, + msatoshi: parseInt(reqCreateLnurlPayReq.amount as string), + bolt11: resp.result.bolt11, + metadata: metadata, + }; + + let lnurlPayRequestEntity: LnurlPayRequestEntity; + try { + lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest( + data as LnurlPayRequestEntity + ); + } catch (ex) { + logger.debug("ex:", ex); + + response = { + status: "ERROR", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reason: (ex as any).message, + }; + return response; + } + + if (lnurlPayRequestEntity && lnurlPayRequestEntity.bolt11) { + logger.debug( + "LnurlPay.createLnurlPayRequest, lnurlPayRequest created:", + lnurlPayRequestEntity + ); + + response = { + pr: lnurlPayRequestEntity.bolt11, + routes: [], + }; + } else { + // LnurlPayRequest not created + logger.debug( + "LnurlPay.createLnurlPayRequest, LnurlPayRequest not created." + ); + + response = { + status: "ERROR", + reason: "payRequest not created", + }; + } + } else { + response = { + status: "ERROR", + reason: "invoice not created", + }; + } + } else { + // There is an error with inputs + logger.debug( + "LnurlPay.createLnurlPayRequest, there is an error with inputs." + ); + + response = { + status: "ERROR", + reason: "Invalid arguments", + }; + } + + return response; + } + + async deleteLnurlPayRequest( + lnurlPayRequestId: number + ): Promise { + const result: IRespLnurlPayRequest = await this._lock.acquire( + "modifLnurlPayRequest", + async (): Promise => { + logger.debug( + "acquired lock modifLnurlPayRequest in deleteLnurlPayRequest" + ); + + logger.info( + "LnurlPay.deleteLnurlPayRequest, lnurlPayRequestId:", + lnurlPayRequestId + ); + + const response: IRespLnurlPayRequest = {}; + + if (lnurlPayRequestId) { + // Inputs are valid. + logger.debug("LnurlPay.deleteLnurlPayRequest, Inputs are valid."); + + let lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestById( + lnurlPayRequestId + ); + + if (lnurlPayRequestEntity == null) { + logger.debug( + "LnurlPay.deleteLnurlPayRequest, lnurlPayRequest not found" + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPayRequest not found", + }; + } else if ( + !lnurlPayRequestEntity.deleted && + !lnurlPayRequestEntity.paid + ) { + logger.debug( + "LnurlPay.deleteLnurlPayRequest, unpaid lnurlPayRequestEntity found for this lnurlPayRequestId!" + ); + + lnurlPayRequestEntity.deleted = true; + lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest( + lnurlPayRequestEntity + ); + + response.result = lnurlPayRequestEntity; + } else { + // LnurlPayRequest already deactivated + logger.debug( + "LnurlPay.deleteLnurlPayRequest, LnurlPayRequest already deactivated." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPayRequest already deactivated", + }; + } + } else { + // There is an error with inputs + logger.debug( + "LnurlPay.deleteLnurlPayRequest, there is an error with inputs." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + ); + logger.debug("released lock modifLnurlPayRequest in deleteLnurlPayRequest"); + return result; + } + + async getLnurlPayRequest( + lnurlPayRequestId: number + ): Promise { + logger.info( + "LnurlPay.getLnurlPayRequest, lnurlPayRequestId:", + lnurlPayRequestId + ); + + const response: IRespLnurlPayRequest = {}; + + if (lnurlPayRequestId) { + // Inputs are valid. + logger.debug("LnurlPay.getLnurlPayRequest, Inputs are valid."); + + const lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestById( + lnurlPayRequestId + ); + + if (lnurlPayRequestEntity != null) { + logger.debug( + "LnurlPay.getLnurlPayRequest, lnurlPayRequestEntity found for this lnurlPayRequestId!" + ); + + response.result = lnurlPayRequestEntity; + } else { + // Active LnurlPayRequest not found + logger.debug("LnurlPay.getLnurlPayRequest, LnurlPayRequest not found."); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "LnurlPayRequest not found", + }; + } + } else { + // There is an error with inputs + logger.debug( + "LnurlPay.getLnurlPayRequest, there is an error with inputs." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + + async lnurlPayRequestCallback( + reqCallback: IReqLnurlPayRequestCallback + ): Promise { + const result: IRespLnurlPayRequestCallback = await this._lock.acquire( + "modifLnurlPayRequestCallback", + async (): Promise => { + logger.debug( + "acquired lock modifLnurlPayRequestCallback in lnurlPayRequestCallback" + ); + + const response: IRespLnurlPayRequestCallback = {}; + + let lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestByLabel( + reqCallback.bolt11Label + ); + + if (lnurlPayRequestEntity) { + lnurlPayRequestEntity.paid = true; + + lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest( + lnurlPayRequestEntity + ); + + const lnurlPayEntity = await this._lnurlDB.getLnurlPayById( + lnurlPayRequestEntity.lnurlPayEntityId + ); + + if (lnurlPayEntity && lnurlPayEntity.webhookUrl) { + const cbResponse = await Utils.post( + lnurlPayEntity.webhookUrl, + lnurlPayRequestEntity + ); + + if (cbResponse.status >= 200 && cbResponse.status < 400) { + logger.debug( + "LnurlWithdraw.lnurlPayRequestCallback, paid, webhook called back" + ); + + lnurlPayRequestEntity.paidCalledbackTs = new Date(); + await this._lnurlDB.saveLnurlPayRequest(lnurlPayRequestEntity); + } + } + + response.result = "success"; + } else { + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid arguments", + }; + } + + return response; + } + ); + + return result; + } + + async payLnAddress(req: IReqPayLnAddress): Promise { + const bolt11 = await LnAddress.fetchBolt11(req.address, req.amountMsat); + + const response: IRespPayLnAddress = {}; + if (bolt11) { + const lnPayParams = { + bolt11, + expectedMsatoshi: req.amountMsat, + }; + + let resp: IRespLnPay = await this._cyphernodeClient.lnPay(lnPayParams); + + if (resp.error) { + logger.debug("LnurlPay.payLnAddress, ln_pay error, let's retry #1!"); + + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } + + if (resp.error) { + logger.debug("LnurlPay.payLnAddress, ln_pay error, let's retry #2!"); + + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } + + if (resp.error) { + logger.debug("LnurlPay.payLnAddress, ln_pay error!"); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: resp.error.message, + }; + } else { + logger.debug("LnurlWithdraw.lnServiceWithdraw, ln_pay success!"); + + response.result = "OK"; + } + } else { + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Unable to fetch bolt11 invoice", + }; + } + + return response; + } +} + +export { LnurlPay }; diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts index dc78d54..f8de7ab 100644 --- a/src/lib/Utils.ts +++ b/src/lib/Utils.ts @@ -3,19 +3,26 @@ import axios, { AxiosError, AxiosRequestConfig } from "axios"; import { bech32 } from "bech32"; class Utils { - static async post( + static async request( + method: "post" | "get", url: string, - postdata: unknown, + postdata?: unknown, addedOptions?: unknown // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { - logger.info("Utils.post", url, JSON.stringify(postdata), addedOptions); + logger.info( + "Utils.request", + method, + url, + JSON.stringify(postdata), + addedOptions + ); let configs: AxiosRequestConfig = { baseURL: url, - method: "post", - data: postdata, + method, }; + if (postdata) configs.data = postdata; if (addedOptions) { configs = Object.assign(configs, addedOptions); } @@ -23,7 +30,7 @@ class Utils { try { const response = await axios.request(configs); logger.debug( - "Utils.post :: response.data =", + "Utils.request :: response.data =", JSON.stringify(response.data) ); @@ -36,15 +43,15 @@ class Utils { // The request was made and the server responded with a status code // that falls out of the range of 2xx logger.info( - "Utils.post :: error.response.data =", + "Utils.request :: error.response.data =", JSON.stringify(error.response.data) ); logger.info( - "Utils.post :: error.response.status =", + "Utils.request :: error.response.status =", error.response.status ); logger.info( - "Utils.post :: error.response.headers =", + "Utils.request :: error.response.headers =", error.response.headers ); @@ -53,12 +60,12 @@ class Utils { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - logger.info("Utils.post :: error.message =", error.message); + logger.info("Utils.request :: error.message =", error.message); return { status: -1, data: error.message }; } else { // Something happened in setting up the request that triggered an Error - logger.info("Utils.post :: Error:", error.message); + logger.info("Utils.request :: Error:", error.message); return { status: -2, data: error.message }; } @@ -69,6 +76,20 @@ class Utils { } } + static post( + url: string, + postdata?: unknown, + addedOptions?: unknown + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + return Utils.request("post", url, postdata, addedOptions); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static get(url: string, addedOptions?: unknown): Promise { + return Utils.request("get", url, undefined, addedOptions); + } + static async encodeBech32(str: string): Promise { logger.info("Utils.encodeBech32:", str); diff --git a/src/types/ILnurlPay.ts b/src/types/ILnurlPay.ts new file mode 100644 index 0000000..f7fa66f --- /dev/null +++ b/src/types/ILnurlPay.ts @@ -0,0 +1,5 @@ +import { LnurlPayEntity } from "@prisma/client"; + +export default interface ILnurlPay extends LnurlPayEntity { + lnurlDecoded: string; +} diff --git a/src/types/IReqCreateLnurlPay.ts b/src/types/IReqCreateLnurlPay.ts new file mode 100644 index 0000000..54408ca --- /dev/null +++ b/src/types/IReqCreateLnurlPay.ts @@ -0,0 +1,7 @@ +export default interface IReqCreateLnurlPay { + externalId: string; + minMsatoshi: number; + maxMsatoshi: number; + description: string; + webhookUrl?: string; +} diff --git a/src/types/IReqCreateLnurlPayRequest.ts b/src/types/IReqCreateLnurlPayRequest.ts new file mode 100644 index 0000000..2d5b318 --- /dev/null +++ b/src/types/IReqCreateLnurlPayRequest.ts @@ -0,0 +1,4 @@ +export default interface IReqCreateLnurlPayRequest { + externalId: string; + amount: string | number; +} diff --git a/src/types/IReqLnurlPayRequestCallback.ts b/src/types/IReqLnurlPayRequestCallback.ts new file mode 100644 index 0000000..fce6f73 --- /dev/null +++ b/src/types/IReqLnurlPayRequestCallback.ts @@ -0,0 +1,3 @@ +export default interface IReqLnurlPayRequestCallback { + bolt11Label: string; +} diff --git a/src/types/IReqPayLnAddress.ts b/src/types/IReqPayLnAddress.ts new file mode 100644 index 0000000..30bb3d5 --- /dev/null +++ b/src/types/IReqPayLnAddress.ts @@ -0,0 +1,4 @@ +interface IReqPayLnAddress { + address: string; + amountMsat: number; +} diff --git a/src/types/IReqUpdateLnurlPay.ts b/src/types/IReqUpdateLnurlPay.ts new file mode 100644 index 0000000..100d5f3 --- /dev/null +++ b/src/types/IReqUpdateLnurlPay.ts @@ -0,0 +1,6 @@ +export default interface IReqUpdateLnurlPay { + lnurlPayId: number; + minMsatoshi?: number; + maxMsatoshi?: number; + description?: string; +} diff --git a/src/types/IReqViewLnurlPay.ts b/src/types/IReqViewLnurlPay.ts new file mode 100644 index 0000000..f89f1d2 --- /dev/null +++ b/src/types/IReqViewLnurlPay.ts @@ -0,0 +1,3 @@ +export default interface IReqViewLnurlPay { + externalId: string; +} diff --git a/src/types/IRespLnurlPay.ts b/src/types/IRespLnurlPay.ts new file mode 100644 index 0000000..27394d6 --- /dev/null +++ b/src/types/IRespLnurlPay.ts @@ -0,0 +1,7 @@ +import { IResponseError } from "./jsonrpc/IResponseMessage"; +import ILnurlPay from "./ILnurlPay"; + +export default interface IRespLnurlPay { + result?: ILnurlPay; + error?: IResponseError; +} diff --git a/src/types/IRespLnurlPayRequest.ts b/src/types/IRespLnurlPayRequest.ts new file mode 100644 index 0000000..dc2aee4 --- /dev/null +++ b/src/types/IRespLnurlPayRequest.ts @@ -0,0 +1,15 @@ +import IRespLnServiceStatus from "./IRespLnServiceStatus"; +import { IResponseError } from "./jsonrpc/IResponseMessage"; +import { LnurlPayRequestEntity } from ".prisma/client"; + +export default interface IRespLnurlPayRequest extends IRespLnServiceStatus { + tag?: string; + callback?: string; + metadata?: string; + minSendable?: number; + maxSendable?: number; + pr?: string; + routes?: string[]; + result?: LnurlPayRequestEntity; + error?: IResponseError; +} diff --git a/src/types/IRespLnurlPayRequestCallback.ts b/src/types/IRespLnurlPayRequestCallback.ts new file mode 100644 index 0000000..04074ec --- /dev/null +++ b/src/types/IRespLnurlPayRequestCallback.ts @@ -0,0 +1,6 @@ +import { IResponseError } from "./jsonrpc/IResponseMessage"; + +export default interface IRespLnurlPayRequestCallback { + result?: string; + error?: IResponseError; +} diff --git a/src/types/IRespPayLnAddress.ts b/src/types/IRespPayLnAddress.ts new file mode 100644 index 0000000..24d9d77 --- /dev/null +++ b/src/types/IRespPayLnAddress.ts @@ -0,0 +1,6 @@ +import { IResponseError } from "./jsonrpc/IResponseMessage"; + +export default interface IRespPayLnAddress { + result?: string; + error?: IResponseError; +} diff --git a/src/types/SaveLnurlPayRequestWhere.ts b/src/types/SaveLnurlPayRequestWhere.ts new file mode 100644 index 0000000..7e045c7 --- /dev/null +++ b/src/types/SaveLnurlPayRequestWhere.ts @@ -0,0 +1,6 @@ +interface SaveLnurlPayRequestWhere { + lnurlPayRequestId?: number; + bolt11Label?: string; +} + +export { SaveLnurlPayRequestWhere }; diff --git a/src/types/cyphernode/IReqLnCreate.ts b/src/types/cyphernode/IReqLnCreate.ts new file mode 100644 index 0000000..0995362 --- /dev/null +++ b/src/types/cyphernode/IReqLnCreate.ts @@ -0,0 +1,13 @@ +export default interface IReqLnCreate { + // - msatoshi, required, amount we want to recieve + // - label, required, unique label to identify the bolt11 invoice + // - description, required, description to be encoded in the bolt11 invoice + // - expiry, optional, expiry time in seconds + // - callbackUrl, optional, callback for invoice updates / payment + + msatoshi: number; + label: string; + description: string; + expiry?: number; + callbackUrl?: string; +} diff --git a/src/types/cyphernode/IRespLnCreate.ts b/src/types/cyphernode/IRespLnCreate.ts new file mode 100644 index 0000000..7dff1ac --- /dev/null +++ b/src/types/cyphernode/IRespLnCreate.ts @@ -0,0 +1,12 @@ +import { IResponseError } from "../jsonrpc/IResponseMessage"; + +interface IRespLnCreateResult { + payment_hash: string; + expires_at: number; + bolt11: string; +} + +export default interface IRespLnCreate { + result?: IRespLnCreateResult; + error?: IResponseError; +} diff --git a/src/validators/CreateLnurlPayRequestValidator.ts b/src/validators/CreateLnurlPayRequestValidator.ts new file mode 100644 index 0000000..4e1cca8 --- /dev/null +++ b/src/validators/CreateLnurlPayRequestValidator.ts @@ -0,0 +1,20 @@ +import { LnurlPayEntity } from ".prisma/client"; +import IReqCreateLnurlPayRequest from "../types/IReqCreateLnurlPayRequest"; + +class CreateLnurlPayRequestValidator { + static validateRequest( + lnurlPay: LnurlPayEntity, + request: IReqCreateLnurlPayRequest + ): boolean { + if ( + request.amount >= lnurlPay.minMsatoshi && + request.amount <= lnurlPay.minMsatoshi + ) { + // Mandatory maxMsatoshi at least equal to minMsatoshi + return true; + } + return false; + } +} + +export { CreateLnurlPayRequestValidator }; diff --git a/src/validators/CreateLnurlPayValidator.ts b/src/validators/CreateLnurlPayValidator.ts new file mode 100644 index 0000000..aa17909 --- /dev/null +++ b/src/validators/CreateLnurlPayValidator.ts @@ -0,0 +1,17 @@ +import IReqCreateLnurlPay from "../types/IReqCreateLnurlPay"; + +class CreateLnurlPayValidator { + static validateRequest(request: IReqCreateLnurlPay): boolean { + if ( + !!request.minMsatoshi && + !!request.maxMsatoshi && + request.maxMsatoshi >= request.minMsatoshi + ) { + // Mandatory maxMsatoshi at least equal to minMsatoshi + return true; + } + return false; + } +} + +export { CreateLnurlPayValidator }; diff --git a/src/validators/UpdateLnurlPayValidator.ts b/src/validators/UpdateLnurlPayValidator.ts new file mode 100644 index 0000000..a32bac3 --- /dev/null +++ b/src/validators/UpdateLnurlPayValidator.ts @@ -0,0 +1,18 @@ +import IReqUpdateLnurlPay from "../types/IReqUpdateLnurlPay"; + +class UpdateLnurlPayValidator { + static validateRequest(request: IReqUpdateLnurlPay): boolean { + if (request.lnurlPayId) { + if ( + (!!request.minMsatoshi || !!request.maxMsatoshi) && + (request.maxMsatoshi || 0) >= (request.minMsatoshi || 0) + ) { + // Mandatory maxMsatoshi at least equal to minMsatoshi + return true; + } + } + return false; + } +} + +export { UpdateLnurlPayValidator }; From e5a04e2343953ca50265f714673a0d56c4ebead0 Mon Sep 17 00:00:00 2001 From: Michael Little Date: Fri, 27 Jan 2023 12:40:42 +1100 Subject: [PATCH 39/52] cleaned up LnAddress --- src/lib/LnAddress.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/lib/LnAddress.ts b/src/lib/LnAddress.ts index 6fd494d..7344a2f 100644 --- a/src/lib/LnAddress.ts +++ b/src/lib/LnAddress.ts @@ -4,15 +4,9 @@ import { Utils } from "./Utils"; class LnAddress { static addressToUrl(address: string): string | false { - //const LNAddressRE = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const LNAddressRE = /([^@]+)@(.+)/; const m = address.match(LNAddressRE); - logger.debug("matches", m); - //if (m && m.length >= 6) { if (m && m.length >= 2) { - logger.info("LnAddress: success ", address); - - //return `https://${m[5]}/.well-known/lnurlp/${m[1]}`; return `http://${m[2]}/.well-known/lnurlp/${m[1]}`; } @@ -26,17 +20,15 @@ class LnAddress { const url = LnAddress.addressToUrl(address); if (url) { - logger.debug("calling", url); const resp = await Utils.get(url); - logger.debug("resp", resp.data.callback); + if (resp.status >= 200 && resp.status < 400) { - logger.debug("lnurl called", resp.data); if (resp.data.callback && resp.data.tag == "payRequest") { const cbUrl = new URL(resp.data.callback); cbUrl.searchParams.set("amount", String(amount)); - logger.debug("calling url ", cbUrl); + const cbResp = await Utils.get(cbUrl.toString()); - logger.debug("cbResp", cbResp); + if (cbResp.status >= 200 && cbResp.status < 400) { if (cbResp.data && cbResp.data.pr) { return cbResp.data.pr; From 26c87a36bcda532b472964178deea5dcdc9525ae Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 3 Mar 2023 03:25:49 +0000 Subject: [PATCH 40/52] Small fixes, improvements and tests --- README.md | 363 +-- cypherapps/data/config.json | 48 +- doc/LNURL-Pay.md | 255 ++ doc/LNURL-Withdraw.md | 364 +++ package-lock.json | 2651 ++++++++++++++++- src/config/LnurlConfig.ts | 2 +- src/lib/HttpServer.ts | 32 +- src/lib/LnurlPay.ts | 266 +- src/lib/LnurlWithdraw.ts | 20 +- src/types/IRespLnServicePayRequest.ts | 6 + src/types/IRespLnServicePaySpecs.ts | 9 + src/types/IRespLnurlPayRequest.ts | 12 +- src/validators/CreateLnurlPayValidator.ts | 1 + ...tValidator.ts => LnServicePayValidator.ts} | 6 +- tests/test-lnurl-pay.sh | 683 +++++ 15 files changed, 4181 insertions(+), 537 deletions(-) create mode 100644 doc/LNURL-Pay.md create mode 100644 doc/LNURL-Withdraw.md create mode 100644 src/types/IRespLnServicePayRequest.ts create mode 100644 src/types/IRespLnServicePaySpecs.ts rename src/validators/{CreateLnurlPayRequestValidator.ts => LnServicePayValidator.ts} (74%) create mode 100755 tests/test-lnurl-pay.sh diff --git a/README.md b/README.md index 2a9fcca..458b819 100644 --- a/README.md +++ b/README.md @@ -2,168 +2,7 @@ LNURL cypherapp for cyphernode -## LNURL-withdraw happy path - -1. Service (your web app) calls createLnurlWithdraw endpoint, receives a LNURL string -2. Service displays the corresponding QR code -3. User scans the QR code using his LNURL compatible wallet -4. User's wallet calls LNURL-withdraw-request, receives withdraw data -5. User's wallet calls LNURL-withdraw, receives payment status -6. LNURL app uses Cyphernode's ln_pay to send LN payment to user - -## LNURL-withdraw restrictions - -1. If there's an expiration on the LNURL-withdraw, withdraw will fail after the expiration -2. If Service deleted the LNURL, withdraw will fail -3. If there's a fallback Bitcoin address on the LNURL, when expired, LNURL app will send amount on-chain -4. If batching is activated on fallback, the fallback will be sent to the Batcher -5. Because LN payments can be "stuck" and may eventually be successful, we reject subsequent withdraw requests that is using different bolt11 than the first request. - -## LNURL-withdraw API endpoints - -### createLnurlWithdraw - -Request: - -```TypeScript -{ - externalId?: string; - msatoshi: number; - description?: string; - expiresAt?: Date; - webhookUrl?: string; - btcFallbackAddress?: string; - batchFallback?: boolean; -} -``` - -Response: - -```TypeScript -{ - result?: { - lnurlWithdrawId: number; - externalId: string | null; - msatoshi: number; - description: string | null; - expiresAt: Date | null; - secretToken: string; - webhookUrl: string | null; - calledback: boolean; - calledbackTs: Date | null; - lnurl: string; - bolt11: string | null; - btcFallbackAddress: string | null; - batchFallback: boolean; - batchRequestId: number | null; - fallbackDone: boolean; - withdrawnDetails: string | null; - withdrawnTs: Date | null; - paid: boolean; - deleted: boolean; - createdTs: Date; - updatedTs: Date; - lnurlDecoded: string; - }, - error?: { - code: number; - message: string; - data?: D; - } -} -``` - -### getLnurlWithdraw - -Request: - -```TypeScript -{ - lnurlWithdrawId: number; -} -``` - -Response: - -```TypeScript -{ - result?: { - lnurlWithdrawId: number; - externalId: string | null; - msatoshi: number; - description: string | null; - expiresAt: Date | null; - secretToken: string; - webhookUrl: string | null; - calledback: boolean; - calledbackTs: Date | null; - lnurl: string; - bolt11: string | null; - btcFallbackAddress: string | null; - batchFallback: boolean; - batchRequestId: number | null; - fallbackDone: boolean; - withdrawnDetails: string | null; - withdrawnTs: Date | null; - paid: boolean; - deleted: boolean; - createdTs: Date; - updatedTs: Date; - lnurlDecoded: string; - }, - error?: { - code: number; - message: string; - data?: D; - } -} -``` - -### deleteLnurlWithdraw - -Request: - -```TypeScript -{ - lnurlWithdrawId: number; -} -``` - -Response: - -```TypeScript -{ - result?: { - lnurlWithdrawId: number; - externalId: string | null; - msatoshi: number; - description: string | null; - expiresAt: Date | null; - secretToken: string; - webhookUrl: string | null; - calledback: boolean; - calledbackTs: Date | null; - lnurl: string; - bolt11: string | null; - btcFallbackAddress: string | null; - batchFallback: boolean; - batchRequestId: number | null; - fallbackDone: boolean; - withdrawnDetails: string | null; - withdrawnTs: Date | null; - paid: boolean; - deleted: boolean; - createdTs: Date; - updatedTs: Date; - lnurlDecoded: string; - }, - error?: { - code: number; - message: string; - data?: D; - } -} -``` +## General endpoints ### reloadConfig, getConfig @@ -203,202 +42,10 @@ Response: } ``` -### forceFallback +## LNURL-Withdraw specific endpoints -This will rewing the LNURL expiration in the past to make it elligible to fallback on next check. +[LNURL-Withdraw specific documentation can be found here](doc/LNURL-Withdraw.md) -Request: +## LNURL-Pay specific endpoints -```TypeScript -{ - lnurlWithdrawId: number; -} -``` - -Response: - -```TypeScript -{ - result?: { - lnurlWithdrawId: number; - externalId: string | null; - msatoshi: number; - description: string | null; - expiresAt: Date | null; - secretToken: string; - webhookUrl: string | null; - calledback: boolean; - calledbackTs: Date | null; - lnurl: string; - bolt11: string | null; - btcFallbackAddress: string | null; - batchFallback: boolean; - batchRequestId: number | null; - fallbackDone: boolean; - withdrawnDetails: string | null; - withdrawnTs: Date | null; - paid: boolean; - deleted: boolean; - createdTs: Date; - updatedTs: Date; - lnurlDecoded: string; - }, - error?: { - code: number; - message: string; - data?: D; - } -} -``` - -### processCallbacks - -Request: N/A - -Response: - -```json -{} -``` - -### processFallbacks - -Request: N/A - -Response: - -```json -{} -``` - -## LNURL-withdraw User/Wallet endpoints - -### /withdrawRequest?s=[secretToken] - -Response: - -```TypeScript -{ - status?: string; - reason?: string; - tag?: string; - callback?: string; - k1?: string; - defaultDescription?: string; - minWithdrawable?: number; - maxWithdrawable?: number; - balanceCheck?: string; -} -``` - -### /withdraw?k1=[secretToken]&pr=[bolt11] - -Response: - -```TypeScript -{ - status?: string; - reason?: string; -} -``` - -## LNURL-withdraw webhooks - -- LNURL Withdrawn using LN - -```json -{ - "action": "lnPaid", - "lnurlWithdrawId": 45, - "bolt11": "lnbcrt5048150p1psnzvkjpp5fxf3zk4yalqeh3kzxusn7hpqx4f7ya6tundmdst6nvxem6eskw3qdqdv3jhxce58qcn2xqyjw5qcqp2sp5zmddwmhrellsj5nauwvcdyvvze9hg8k7wkhes04ha49htnfuawnq9qy9qsqh6ecxxqv568javdgenr5r4mm3ut82t683pe8yexql7rrwa8l5euq7ffh329rlgzsufj5s7x4n4pj2lcq0j9kqzn7gyt9zhg847pg6csqmuxdfh", - "lnPayResponse": { - "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", - "payment_hash": "4993115aa4efc19bc6c237213f5c203553e2774be4dbb6c17a9b0d9deb30b3a2", - "created_at": 1630614227.193, - "parts": 1, - "msatoshi": 504815, - "amount_msat": "504815msat", - "msatoshi_sent": 504815, - "amount_sent_msat": "504815msat", - "payment_preimage": "d4ecd85e662ce1f6c6f14d134755240efd6dcb2217a0f511ca29fd694942b532", - "status": "complete" - } -} -``` - -- LNURL Withdraw paid using Bitcoin fallback - -```json -{ - "action": "fallbackPaid", - "lnurlWithdrawId": 50, - "btcFallbackAddress": "bcrt1qgs0axulr8s2wp69n5cf805ck4xv6crelndustu", - "details": { - "status": "accepted", - "txid": "5683ecabaf7b4bd1d90ee23de74655495945af41f6fc783876a841598e4041f3", - "hash": "49ba4efa1ed7a19016b623686c34a2e287d7ca3c8f58593ce07ce08ff8a12f7c", - "details": { - "address": "bcrt1qgs0axulr8s2wp69n5cf805ck4xv6crelndustu", - "amount": 5.33e-06, - "firstseen": 1630614256, - "size": 222, - "vsize": 141, - "replaceable": true, - "fee": 2.82e-05, - "subtractfeefromamount": null - } - } -} -``` - -- LNURL Withdraw batched using Bitcoin fallback - -```json -{ - "action": "fallbackBatched", - "lnurlWithdrawId": 51, - "btcFallbackAddress": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", - "details": { - "batchId": 16, - "batchRequestId": 36, - "etaSeconds": 7, - "cnResult": { - "batcherId": 1, - "outputId": 155, - "nbOutputs": 1, - "oldest": "2021-09-02 20:25:16", - "total": 5.26e-06 - }, - "address": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", - "amount": 5.26e-06 - } -} -``` - -- LNURL Withdraw paid using a batched Bitcoin fallback - -```json -{ - "action": "fallbackPaid", - "lnurlWithdrawId": 51, - "btcFallbackAddress": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", - "details": { - "batchRequestId": 36, - "batchId": 16, - "cnBatcherId": 1, - "requestCountInBatch": 1, - "status": "accepted", - "txid": "a6a03ea728718bccf42f53b1b833fb405f06243d1ce3aad2e5a4522eb3ab3231", - "hash": "8aafb505abfffa6bb91b3e1c59445091c785685b9fabd6a3188e088b4c3210f0", - "details": { - "firstseen": 1630614325, - "size": 222, - "vsize": 141, - "replaceable": true, - "fee": 2.82e-05, - "address": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", - "amount": 5.26e-06 - } - } -} -``` +[LNURL-Pay specific documentation can be found here](doc/LNURL-Pay.md) diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json index 659a513..dbe7735 100644 --- a/cypherapps/data/config.json +++ b/cypherapps/data/config.json @@ -1,25 +1,25 @@ { - "LOG": "DEBUG", - "BASE_DIR": "/lnurl", - "DATA_DIR": "data", - "DB_NAME": "lnurl.sqlite", - "URL_API_SERVER": "http://lnurl", - "URL_API_PORT": 8000, - "URL_API_CTX": "/api", - "URL_CTX_WEBHOOKS": "/webhooks", - "SESSION_TIMEOUT": 600, - "CN_URL": "https://gatekeeper:2009/v0", - "CN_API_ID": "003", - "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", - "BATCHER_URL": "http://batcher:8000", - "LN_SERVICE_SERVER": "https://yourdomain", - "LN_SERVICE_PORT": 443, - "LN_SERVICE_CTX": "/lnurl", - "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", - "LN_SERVICE_WITHDRAW_CTX": "/withdraw", - "LN_SERVICE_PAY_CTX": "/pay", - "LN_SERVICE_PAY_REQUEST_CTX": "/payRequest", - "LN_SERVICE_PAY_CB_CTX": "/payReqCallback", - "RETRY_WEBHOOKS_TIMEOUT": 1, - "CHECK_EXPIRATION_TIMEOUT": 1 -} + "LOG": "DEBUG", + "BASE_DIR": "/lnurl", + "DATA_DIR": "data", + "DB_NAME": "lnurl.sqlite", + "URL_API_SERVER": "http://lnurl", + "URL_API_PORT": 8000, + "URL_API_CTX": "/api", + "URL_CTX_WEBHOOKS": "/webhooks", + "SESSION_TIMEOUT": 600, + "CN_URL": "https://gatekeeper:2009/v0", + "CN_API_ID": "003", + "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", + "BATCHER_URL": "http://batcher:8000", + "LN_SERVICE_SERVER": "https://yourdomain", + "LN_SERVICE_PORT": 443, + "LN_SERVICE_CTX": "/lnurl", + "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", + "LN_SERVICE_WITHDRAW_CTX": "/withdraw", + "LN_SERVICE_PAY_SPECS_CTX": "/paySpecs", + "LN_SERVICE_PAY_REQUEST_CTX": "/payRequest", + "LN_SERVICE_PAY_CB_CTX": "/payReqCallback", + "RETRY_WEBHOOKS_TIMEOUT": 1, + "CHECK_EXPIRATION_TIMEOUT": 1 +} \ No newline at end of file diff --git a/doc/LNURL-Pay.md b/doc/LNURL-Pay.md new file mode 100644 index 0000000..9d03d27 --- /dev/null +++ b/doc/LNURL-Pay.md @@ -0,0 +1,255 @@ +# LNURL-Pay + +## LNURL-Pay happy path + +1. Service (your web app) calls createLnurlPay endpoint, receives a LNURL string +2. Service displays the corresponding QR code +3. User scans the QR code using his LNURL compatible wallet +4. User's wallet calls LNURL-pay-request, receives pay data +5. User's wallet calls LNURL-pay, receives bolt11 +6. User's wallet pays to bolt11 +7. LNURL app receives a Cyphernode webhook when the bolt11 is paid + +## LNURL-Pay restrictions + +1. If Service deleted the LNURL-Pay, LNURL-pay-request will fail +2. User has the responsability to have a LNURL-compatible and well-connected LN Wallet + +## LNURL-Pay API endpoints + +### createLnurlPay + +Create a LNURL-Pay address. The `externalId` argument will be used as the alias. + +Request: + +```TypeScript +{ + externalId: string; + minMsatoshi: number; + maxMsatoshi: number; + description: string; + webhookUrl?: string; +} +``` + +Response: + +```TypeScript +{ + result?: { + lnurlPayId: number; + externalId: string; + minMsatoshi: number; + maxMsatoshi: number; + description: string; + webhookUrl: string | null; + lnurl: string; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + +Example: + +Request: + +```json +{ + "id":0, + "method":"createLnurlPay", + "params":{ + "externalId":"kex002", + "minMsatoshi":200000, + "maxMsatoshi":2000000000, + "description":"kex002LNURLPAY", + "webhookUrl":"http://tests-lnurl-pay-cb:1111/lnurl/paid-kex002" + } +} +``` + +Response: + +```json +{ + "id":0, + "result":{ + "lnurlPayId":2, + "externalId":"kex002", + "minMsatoshi":200000, + "maxMsatoshi":2000000000, + "description":"kex002LNURLPAY", + "webhookUrl":"http://tests-lnurl-pay-cb:1111/lnurl/paid-kex002", + "lnurl":"LNURL1DP68GURN8GHJ7ARJV9JKV6TT9AKXUATJDSHHQCTE9A4K27PSXQEQLMG4YP", + "deleted":false, + "createdTs":"2023-03-01T22:22:56.635Z", + "updatedTs":"2023-03-01T22:22:56.635Z", + "lnurlDecoded":"https://traefik/lnurl/pay/kex002" + } +} +``` + + +### getLnurlPay + +Request: + +```TypeScript +{ + lnurlPayId: number; +} +``` + +Response: + +```TypeScript +{ + result?: { + lnurlPayId: number; + externalId: string; + minMsatoshi: number; + maxMsatoshi: number; + description: string; + webhookUrl: string | null; + lnurl: string; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + +Example: + +Request: + +```json +{ + "id":0, + "method":"getLnurlPay", + "params":{ + "lnurlPayId":2, + } +} +``` + +Response: + +```json +{ + "id":0, + "result":{ + "lnurlPayId":2, + "externalId":"kex002", + "minMsatoshi":200000, + "maxMsatoshi":2000000000, + "description":"kex002LNURLPAY", + "webhookUrl":"http://tests-lnurl-pay-cb:1111/lnurl/paid-kex002", + "lnurl":"LNURL1DP68GURN8GHJ7ARJV9JKV6TT9AKXUATJDSHHQCTE9A4K27PSXQEQLMG4YP", + "deleted":false, + "createdTs":"2023-03-01T22:22:56.635Z", + "updatedTs":"2023-03-01T22:22:56.635Z", + "lnurlDecoded":"https://traefik/lnurl/pay/kex002" + } +} +``` + +### deleteLnurlPay + +Request: + +```TypeScript +{ + lnurlPayId: number; +} +``` + +Response: + +```TypeScript +{ + result?: { + lnurlPayId: number; + externalId: string; + minMsatoshi: number; + maxMsatoshi: number; + description: string; + webhookUrl: string | null; + lnurl: string; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + +## LNURL-pay User/Wallet endpoints + +### /paySpecs/:externalId + +Response: + +```TypeScript +{ + callback?: string; + maxSendable?: number; + minSendable?: number; + metadata?: string; + tag?: string; + status?: string; + reason?: string; +} +``` + +### /payRequest/:externalId?amount=[msatoshi] + +Response: + +```TypeScript +{ + pr?: string; + routes?: []; + status?: string; + reason?: string; +} +``` + +## LNURL-pay webhooks + +- Payment sent to a LNURL Pay address + +```json +{ + "lnurlPayRequestId": 7, + "lnurlPayEntityId": 2, + "bolt11Label": "2-1677730727795", + "msatoshi": 30000000, + "bolt11": "lnbcrt300u1pjqqgagsp54gymkgvtasfh5vh784kvaswctwtdzdykjz673zxvzyppsjecyvmspp5nqjearzvzhs9ljen5m7tr3utes5puqkzjjn3trsgtthskryeahxqhp5wg3g2ypfrmyy5wmy3q4cjhm4aq4scfsz36wldq98e3e57q22x4esxqyjw5qcqp29qyysgqr5n7kc5waj2pc5z4g20tw9ccy2g99dnrdpmgmdw93v3ejnmyysvpeuk58z6p46kjn89yqcxekj4jkrhh44fxjl6vy02k5p0mk4n2pgqpjhv3re", + "metadata": "[[\"text/plain\",\"kex002LNURLPAY\"]]", + "paid": true, + "paidCalledbackTs": null, + "deleted": false, + "createdTs": "2023-03-02T04:18:48.343Z", + "updatedTs": "2023-03-02T04:18:48.343Z" +} +``` diff --git a/doc/LNURL-Withdraw.md b/doc/LNURL-Withdraw.md new file mode 100644 index 0000000..7ba781c --- /dev/null +++ b/doc/LNURL-Withdraw.md @@ -0,0 +1,364 @@ +# LNURL-Withdraw + +## LNURL-withdraw happy path + +1. Service (your web app) calls createLnurlWithdraw endpoint, receives a LNURL string +2. Service displays the corresponding QR code +3. User scans the QR code using his LNURL compatible wallet +4. User's wallet calls LNURL-withdraw-request, receives withdraw data +5. User's wallet calls LNURL-withdraw, receives payment status +6. LNURL app uses Cyphernode's ln_pay to send LN payment to user + +## LNURL-withdraw restrictions + +1. If there's an expiration on the LNURL-withdraw, withdraw will fail after the expiration +2. If Service deleted the LNURL, withdraw will fail +3. If there's a fallback Bitcoin address on the LNURL, when expired, LNURL app will send amount on-chain +4. If batching is activated on fallback, the fallback will be sent to the Batcher +5. Because LN payments can be "stuck" and may eventually be successful, we reject subsequent withdraw requests if payment is in "pending" state. + +## LNURL-withdraw API endpoints + +### createLnurlWithdraw + +Request: + +```TypeScript +{ + externalId?: string; + msatoshi: number; + description?: string; + expiresAt?: Date; + webhookUrl?: string; + btcFallbackAddress?: string; + batchFallback?: boolean; +} +``` + +Response: + +```TypeScript +{ + result?: { + lnurlWithdrawId: number; + externalId: string | null; + msatoshi: number; + description: string | null; + expiresAt: Date | null; + secretToken: string; + webhookUrl: string | null; + calledback: boolean; + calledbackTs: Date | null; + lnurl: string; + bolt11: string | null; + btcFallbackAddress: string | null; + batchFallback: boolean; + batchRequestId: number | null; + fallbackDone: boolean; + withdrawnDetails: string | null; + withdrawnTs: Date | null; + paid: boolean; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + +### getLnurlWithdraw + +Request: + +```TypeScript +{ + lnurlWithdrawId: number; +} +``` + +Response: + +```TypeScript +{ + result?: { + lnurlWithdrawId: number; + externalId: string | null; + msatoshi: number; + description: string | null; + expiresAt: Date | null; + secretToken: string; + webhookUrl: string | null; + calledback: boolean; + calledbackTs: Date | null; + lnurl: string; + bolt11: string | null; + btcFallbackAddress: string | null; + batchFallback: boolean; + batchRequestId: number | null; + fallbackDone: boolean; + withdrawnDetails: string | null; + withdrawnTs: Date | null; + paid: boolean; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + +### deleteLnurlWithdraw + +Request: + +```TypeScript +{ + lnurlWithdrawId: number; +} +``` + +Response: + +```TypeScript +{ + result?: { + lnurlWithdrawId: number; + externalId: string | null; + msatoshi: number; + description: string | null; + expiresAt: Date | null; + secretToken: string; + webhookUrl: string | null; + calledback: boolean; + calledbackTs: Date | null; + lnurl: string; + bolt11: string | null; + btcFallbackAddress: string | null; + batchFallback: boolean; + batchRequestId: number | null; + fallbackDone: boolean; + withdrawnDetails: string | null; + withdrawnTs: Date | null; + paid: boolean; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + +### forceFallback + +This will rewing the LNURL expiration in the past to make it elligible to fallback on next check. + +Request: + +```TypeScript +{ + lnurlWithdrawId: number; +} +``` + +Response: + +```TypeScript +{ + result?: { + lnurlWithdrawId: number; + externalId: string | null; + msatoshi: number; + description: string | null; + expiresAt: Date | null; + secretToken: string; + webhookUrl: string | null; + calledback: boolean; + calledbackTs: Date | null; + lnurl: string; + bolt11: string | null; + btcFallbackAddress: string | null; + batchFallback: boolean; + batchRequestId: number | null; + fallbackDone: boolean; + withdrawnDetails: string | null; + withdrawnTs: Date | null; + paid: boolean; + deleted: boolean; + createdTs: Date; + updatedTs: Date; + lnurlDecoded: string; + }, + error?: { + code: number; + message: string; + data?: D; + } +} +``` + +### processCallbacks + +Request: N/A + +Response: + +```json +{} +``` + +### processFallbacks + +Request: N/A + +Response: + +```json +{} +``` + +## LNURL-withdraw User/Wallet endpoints + +### /withdrawRequest?s=[secretToken] + +Response: + +```TypeScript +{ + status?: string; + reason?: string; + tag?: string; + callback?: string; + k1?: string; + defaultDescription?: string; + minWithdrawable?: number; + maxWithdrawable?: number; + balanceCheck?: string; +} +``` + +### /withdraw?k1=[secretToken]&pr=[bolt11] + +Response: + +```TypeScript +{ + status: string; + reason?: string; +} +``` + +## LNURL-withdraw webhooks + +- LNURL Withdrawn using LN + +```json +{ + "action": "lnPaid", + "lnurlWithdrawId": 45, + "bolt11": "lnbcrt5048150p1psnzvkjpp5fxf3zk4yalqeh3kzxusn7hpqx4f7ya6tundmdst6nvxem6eskw3qdqdv3jhxce58qcn2xqyjw5qcqp2sp5zmddwmhrellsj5nauwvcdyvvze9hg8k7wkhes04ha49htnfuawnq9qy9qsqh6ecxxqv568javdgenr5r4mm3ut82t683pe8yexql7rrwa8l5euq7ffh329rlgzsufj5s7x4n4pj2lcq0j9kqzn7gyt9zhg847pg6csqmuxdfh", + "lnPayResponse": { + "destination": "029b26c73b2c19ec9bdddeeec97c313670c96b6414ceacae0fb1b3502e490a6cbb", + "payment_hash": "4993115aa4efc19bc6c237213f5c203553e2774be4dbb6c17a9b0d9deb30b3a2", + "created_at": 1630614227.193, + "parts": 1, + "msatoshi": 504815, + "amount_msat": "504815msat", + "msatoshi_sent": 504815, + "amount_sent_msat": "504815msat", + "payment_preimage": "d4ecd85e662ce1f6c6f14d134755240efd6dcb2217a0f511ca29fd694942b532", + "status": "complete" + } +} +``` + +- LNURL Withdraw paid using Bitcoin fallback + +```json +{ + "action": "fallbackPaid", + "lnurlWithdrawId": 50, + "btcFallbackAddress": "bcrt1qgs0axulr8s2wp69n5cf805ck4xv6crelndustu", + "details": { + "status": "accepted", + "txid": "5683ecabaf7b4bd1d90ee23de74655495945af41f6fc783876a841598e4041f3", + "hash": "49ba4efa1ed7a19016b623686c34a2e287d7ca3c8f58593ce07ce08ff8a12f7c", + "details": { + "address": "bcrt1qgs0axulr8s2wp69n5cf805ck4xv6crelndustu", + "amount": 5.33e-06, + "firstseen": 1630614256, + "size": 222, + "vsize": 141, + "replaceable": true, + "fee": 2.82e-05, + "subtractfeefromamount": null + } + } +} +``` + +- LNURL Withdraw batched using Bitcoin fallback + +```json +{ + "action": "fallbackBatched", + "lnurlWithdrawId": 51, + "btcFallbackAddress": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", + "details": { + "batchId": 16, + "batchRequestId": 36, + "etaSeconds": 7, + "cnResult": { + "batcherId": 1, + "outputId": 155, + "nbOutputs": 1, + "oldest": "2021-09-02 20:25:16", + "total": 5.26e-06 + }, + "address": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", + "amount": 5.26e-06 + } +} +``` + +- LNURL Withdraw paid using a batched Bitcoin fallback + +```json +{ + "action": "fallbackPaid", + "lnurlWithdrawId": 51, + "btcFallbackAddress": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", + "details": { + "batchRequestId": 36, + "batchId": 16, + "cnBatcherId": 1, + "requestCountInBatch": 1, + "status": "accepted", + "txid": "a6a03ea728718bccf42f53b1b833fb405f06243d1ce3aad2e5a4522eb3ab3231", + "hash": "8aafb505abfffa6bb91b3e1c59445091c785685b9fabd6a3188e088b4c3210f0", + "details": { + "firstseen": 1630614325, + "size": 222, + "vsize": 141, + "replaceable": true, + "fee": 2.82e-05, + "address": "bcrt1qh0cpvxan6fzjxzwhwlagrpxgca7h5qrcjq2pm9", + "amount": 5.26e-06 + } + } +} +``` diff --git a/package-lock.json b/package-lock.json index e508d13..8aad969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,2589 @@ { "name": "lnurl", "version": "0.1.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "lnurl", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@prisma/client": "^3.2.1", + "@types/async-lock": "^1.1.2", + "async-lock": "^1.2.4", + "axios": "^0.21.1", + "bech32": "^2.0.0", + "date-fns": "^2.23.0", + "express": "^4.17.1", + "http-status-codes": "^1.4.0", + "prisma": "^3.2.1", + "reflect-metadata": "^0.1.13", + "tslog": "^3.2.0" + }, + "devDependencies": { + "@types/express": "^4.17.6", + "@types/node": "^13.13.52", + "@typescript-eslint/eslint-plugin": "^2.24.0", + "@typescript-eslint/parser": "^2.24.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-prettier": "^3.1.4", + "prettier": "2.0.5", + "rimraf": "^3.0.2", + "ts-node": "^8.10.2", + "typescript": "^4.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@prisma/client": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.15.2.tgz", + "integrity": "sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + }, + "engines": { + "node": ">=12.6" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz", + "integrity": "sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==", + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz", + "integrity": "sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==" + }, + "node_modules/@types/async-lock": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.3.tgz", + "integrity": "sha512-UpeDcjGKsYEQMeqEbfESm8OWJI305I7b9KE4ji3aBjoKWyN5CTdn8izcA1FM1DVDne30R5fNEnIy89vZw5LXJQ==" + }, + "node_modules/@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", + "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "13.13.52", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", + "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "2.34.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^2.0.0", + "eslint": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", + "dev": true, + "dependencies": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-lock": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.0.tgz", + "integrity": "sha512-8A7SkiisnEgME2zEedtDYPxUPzdv3x//E7n5IFktPAtMYSEAV7eNJF0rMwrVyUFj6d/8rgajLantbjcNRQYXIg==" + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/date-fns": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", + "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz", + "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">=5.0.0", + "prettier": ">=1.13.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/eslint/node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/eslint/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-status-codes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-1.4.0.tgz", + "integrity": "sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "dependencies": { + "mime-db": "1.48.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prisma": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.15.2.tgz", + "integrity": "sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=12.6" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/tslog": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.2.0.tgz", + "integrity": "sha512-xOCghepl5w+wcI4qXI7vJy6c53loF8OoC/EuKz1ktAPMtltEDz00yo1poKuyBYIQaq4ZDYKYFPD9PfqVrFXh0A==", + "dependencies": { + "source-map-support": "^0.5.19" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + }, "dependencies": { "@babel/code-frame": { "version": "7.14.5", @@ -83,22 +2664,22 @@ } }, "@prisma/client": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-2.30.0.tgz", - "integrity": "sha512-tjJNHVfgyNOwS2F+AkjMMCJGPnXzHuUCrOnAMJyidAu4aNzxbJ8jWwjt96rRMpyrg9Hwen3xqqQ2oA+ikK7nhQ==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.15.2.tgz", + "integrity": "sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==", "requires": { - "@prisma/engines-version": "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb" + "@prisma/engines-version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" } }, "@prisma/engines": { - "version": "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb.tgz", - "integrity": "sha512-LPKq88lIbYezvX0OOc1PU42hHdTsSMPJWmK8lusaHK7DaLHyXjDp/551LbsVapypbjW6N3Jx/If6GoMDASSMSw==" + "version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz", + "integrity": "sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==" }, "@prisma/engines-version": { - "version": "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb.tgz", - "integrity": "sha512-oThNpx7HtJ0eEmnvrWARYcNCs6dqFdAK3Smt2bJVDD6Go4HLuuhjx028osP+rHaFrGOTx7OslLZYtvvFlAXRDA==" + "version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz", + "integrity": "sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==" }, "@types/async-lock": { "version": "1.1.3", @@ -289,7 +2870,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv": { "version": "6.12.6", @@ -329,6 +2911,12 @@ "color-convert": "^2.0.1" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -524,6 +3112,12 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1241,6 +3835,12 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1431,11 +4031,11 @@ } }, "prisma": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-2.30.0.tgz", - "integrity": "sha512-2XYpSibcVpMd1JDxYypGDU/JKq0W2f/HI1itdddr4Pfg+q6qxt/ItWKcftv4/lqN6u/BVlQ2gDzXVEjpHeO5kQ==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.15.2.tgz", + "integrity": "sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==", "requires": { - "@prisma/engines": "2.30.0-28.60b19f4a1de4fe95741da371b4c44a92f4d1adcb" + "@prisma/engines": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" } }, "progress": { @@ -1774,6 +4374,19 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, "tslog": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.2.0.tgz", @@ -1888,6 +4501,12 @@ "requires": { "mkdirp": "^0.5.1" } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/src/config/LnurlConfig.ts b/src/config/LnurlConfig.ts index 4e4c060..3fbfea6 100644 --- a/src/config/LnurlConfig.ts +++ b/src/config/LnurlConfig.ts @@ -17,7 +17,7 @@ export default interface LnurlConfig { LN_SERVICE_CTX: string; LN_SERVICE_WITHDRAW_REQUEST_CTX: string; LN_SERVICE_WITHDRAW_CTX: string; - LN_SERVICE_PAY_CTX: string; + LN_SERVICE_PAY_SPECS_CTX: string; LN_SERVICE_PAY_REQUEST_CTX: string; LN_SERVICE_PAY_CB_CTX: string; RETRY_WEBHOOKS_TIMEOUT: number; diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 09b8bee..6b1f638 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -376,15 +376,17 @@ class HttpServer { } ); - // LN Service LNURL Pay request (step 3) + // LN Service LNURL Pay specs (step 3) this._httpServer.get( - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_PAY_CTX + - "/:externalId", + // this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX + "/:externalId", async (req, res) => { - logger.info(this._lnurlConfig.LN_SERVICE_PAY_CTX + ":", req.params); + logger.info( + this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX + ":", + req.params + ); - const response = await this._lnurlPay.viewLnurlPay({ + const response = await this._lnurlPay.lnServicePaySpecs({ externalId: req.params.externalId, } as IReqViewLnurlPay); @@ -396,13 +398,13 @@ class HttpServer { } ); - // LN Service LNURL Pay request (step 3) lightning address format + // LN Service LNURL Pay specs (step 3) lightning address format this._httpServer.get( "/.well-known/lnurlp/:externalId", async (req, res) => { logger.info("/.well-known/lnurlp/:", req.params); - const response = await this._lnurlPay.viewLnurlPay({ + const response = await this._lnurlPay.lnServicePaySpecs({ externalId: req.params.externalId, } as IReqViewLnurlPay); @@ -416,16 +418,15 @@ class HttpServer { // LN Service LNURL Pay request (step 5) this._httpServer.get( - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + - "/:externalId", + // this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + "/:externalId", async (req, res) => { logger.info( this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + ":", req.params ); - const response = await this._lnurlPay.createLnurlPayRequest({ + const response = await this._lnurlPay.lnServicePayRequest({ externalId: req.params.externalId, amount: req.query.amount, } as IReqCreateLnurlPayRequest); @@ -438,11 +439,10 @@ class HttpServer { } ); - // LN Service LNURL Pay request callback (called when bolt11 paid) + // LN Service LNURL Pay request callback (called by CN when bolt11 paid) this._httpServer.post( - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + - "/:label", + // this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + "/:label", async (req, res) => { logger.info(this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + ":", req.params); diff --git a/src/lib/LnurlPay.ts b/src/lib/LnurlPay.ts index 7a56e08..cff2189 100644 --- a/src/lib/LnurlPay.ts +++ b/src/lib/LnurlPay.ts @@ -9,8 +9,8 @@ import IReqViewLnurlPay from "../types/IReqViewLnurlPay"; import IRespLnurlPay from "../types/IRespLnurlPay"; import { CreateLnurlPayValidator } from "../validators/CreateLnurlPayValidator"; import IReqCreateLnurlPayRequest from "../types/IReqCreateLnurlPayRequest"; -import IRespLnurlPayRequest from "../types/IRespLnurlPayRequest"; -import { CreateLnurlPayRequestValidator } from "../validators/CreateLnurlPayRequestValidator"; +import IRespLnServicePaySpecs from "../types/IRespLnServicePaySpecs"; +import { CreateLnurlPayRequestValidator } from "../validators/LnServicePayValidator"; import IRespLnCreate from "../types/cyphernode/IRespLnCreate"; import { Utils } from "./Utils"; import { LnurlPayEntity, LnurlPayRequestEntity } from "@prisma/client"; @@ -22,6 +22,8 @@ import { UpdateLnurlPayValidator } from "../validators/UpdateLnurlPayValidator"; import IRespLnPay from "../types/cyphernode/IRespLnPay"; import { LnAddress } from "./LnAddress"; import IRespPayLnAddress from "../types/IRespPayLnAddress"; +import IRespLnServicePayRequest from "../types/IRespLnServicePayRequest"; +import IRespLnurlPayRequest from "../types/IRespLnurlPayRequest"; class LnurlPay { private _lnurlConfig: LnurlConfig; @@ -42,6 +44,9 @@ class LnurlPay { }); } + // https://localhost/lnurl/payRequest/externalId + // https://localhost/lnurl/pay/externalId + // https://lnurl.bullbitcoin.com/ lnurlPayUrl(externalId: string, req = false): string { return ( this._lnurlConfig.LN_SERVICE_SERVER + @@ -51,7 +56,7 @@ class LnurlPay { this._lnurlConfig.LN_SERVICE_CTX + (req ? this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX - : this._lnurlConfig.LN_SERVICE_PAY_CTX) + + : this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX) + "/" + externalId ); @@ -313,161 +318,202 @@ class LnurlPay { return response; } - //// payRequest !!!! - async viewLnurlPay( + /** + * Called by user's wallet to get Payment specs + */ + async lnServicePaySpecs( reqViewLnurlPay: IReqViewLnurlPay - ): Promise { + ): Promise { logger.info("LnurlPay.viewLnurlPay, reqViewLnurlPay:", reqViewLnurlPay); - let response: IRespLnurlPayRequest = {}; + let response: IRespLnServicePaySpecs = {}; const lnurlPay: LnurlPayEntity = await this._lnurlDB.getLnurlPayByExternalId( reqViewLnurlPay.externalId ); - if (lnurlPay && lnurlPay.externalId) { - const metadata = JSON.stringify([["text/plain", lnurlPay.description]]); + if (lnurlPay) { + if (!lnurlPay.deleted) { + if (lnurlPay.externalId) { + const metadata = JSON.stringify([ + ["text/plain", lnurlPay.description], + ]); - response = { - callback: this.lnurlPayUrl(lnurlPay.externalId, true), - maxSendable: lnurlPay.maxMsatoshi, - minSendable: lnurlPay.minMsatoshi, - metadata: metadata, - tag: "payRequest", - }; + response = { + callback: this.lnurlPayUrl(lnurlPay.externalId, true), + maxSendable: lnurlPay.maxMsatoshi, + minSendable: lnurlPay.minMsatoshi, + metadata: metadata, + tag: "payRequest", + }; + } else { + logger.debug("LnurlPay.lnServicePaySpecs, no external id."); + + response = { + status: "ERROR", + reason: "Invalid arguments", + }; + } + } else { + logger.debug("LnurlPay.lnServicePaySpecs, deactivated LNURL"); + + response = { status: "ERROR", reason: "Deactivated LNURL" }; + } } else { // There is an error with inputs - logger.debug( - "LnurlPay.createLnurlPayRequest, there is an error with inputs." - ); + logger.debug("LnurlPay.lnServicePaySpecs, LNURL not found."); response = { status: "ERROR", - reason: "Invalid arguments", + reason: "Not found", }; } return response; } - async createLnurlPayRequest( + /** + * Called by user's wallet to ultimately get the bolt11 invoice + */ + async lnServicePayRequest( reqCreateLnurlPayReq: IReqCreateLnurlPayRequest - ): Promise { + ): Promise { logger.info( "LnurlPay.createLnurlPayRequest, reqCreateLnurlPayReq:", reqCreateLnurlPayReq ); - let response: IRespLnurlPayRequest = {}; + let response: IRespLnServicePayRequest = {}; const lnurlPay: LnurlPayEntity = await this._lnurlDB.getLnurlPayByExternalId( reqCreateLnurlPayReq.externalId ); - if ( - lnurlPay && - CreateLnurlPayRequestValidator.validateRequest( - lnurlPay, - reqCreateLnurlPayReq - ) - ) { - // Inputs are valid. - logger.debug("LnurlPay.createLnurlPayRequest, Inputs are valid."); - - const metadata = JSON.stringify([["text/plain", lnurlPay.description]]); - const hHash = crypto.createHmac("sha256", metadata).digest("hex"); - const label = lnurlPay.lnurlPayId + "-" + Date.now(); - - const lnCreateParams = { - msatoshi: reqCreateLnurlPayReq.amount as number, - label: label, - description: hHash, - callbackUrl: - this._lnurlConfig.LN_SERVICE_SERVER + - (this._lnurlConfig.LN_SERVICE_PORT === 443 - ? "" - : ":" + this._lnurlConfig.LN_SERVICE_PORT) + - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + - "/" + - label, - }; - - logger.debug( - "LnurlPay.createLnurlPayRequest trying to get invoice", - lnCreateParams - ); + if (lnurlPay) { + if (!lnurlPay.deleted) { + if ( + CreateLnurlPayRequestValidator.validateRequest( + lnurlPay, + reqCreateLnurlPayReq + ) + ) { + // Inputs are valid. + logger.debug("LnurlPay.createLnurlPayRequest, Inputs are valid."); + + const metadata = JSON.stringify([ + ["text/plain", lnurlPay.description], + ]); + const hHash = crypto.createHmac("sha256", metadata).digest("hex"); + const label = lnurlPay.lnurlPayId + "-" + Date.now(); + + const lnCreateParams = { + msatoshi: reqCreateLnurlPayReq.amount as number, + label: label, + description: hHash, + callbackUrl: + this._lnurlConfig.LN_SERVICE_SERVER + + (this._lnurlConfig.LN_SERVICE_PORT === 443 + ? "" + : ":" + this._lnurlConfig.LN_SERVICE_PORT) + + this._lnurlConfig.LN_SERVICE_CTX + + this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + + "/" + + label, + deschashonly: true, + }; - const resp: IRespLnCreate = await this._cyphernodeClient.lnCreate( - lnCreateParams - ); - logger.debug("LnurlPay.createLnurlPayRequest lnCreate invoice", resp); - - if (resp.result) { - const data = { - lnurlPayEntityId: lnurlPay.lnurlPayId, - bolt11Label: label, - msatoshi: parseInt(reqCreateLnurlPayReq.amount as string), - bolt11: resp.result.bolt11, - metadata: metadata, - }; + logger.debug( + "LnurlPay.createLnurlPayRequest trying to get invoice", + lnCreateParams + ); - let lnurlPayRequestEntity: LnurlPayRequestEntity; - try { - lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest( - data as LnurlPayRequestEntity + const resp: IRespLnCreate = await this._cyphernodeClient.lnCreate( + lnCreateParams ); - } catch (ex) { - logger.debug("ex:", ex); + logger.debug("LnurlPay.createLnurlPayRequest lnCreate invoice", resp); + + if (resp.result) { + const data = { + lnurlPayEntityId: lnurlPay.lnurlPayId, + bolt11Label: label, + msatoshi: parseInt(reqCreateLnurlPayReq.amount as string), + bolt11: resp.result.bolt11, + metadata: metadata, + }; - response = { - status: "ERROR", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - reason: (ex as any).message, - }; - return response; - } + let lnurlPayRequestEntity: LnurlPayRequestEntity; + try { + lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest( + data as LnurlPayRequestEntity + ); + } catch (ex) { + logger.debug("ex:", ex); + + response = { + status: "ERROR", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reason: (ex as any).message, + }; + return response; + } - if (lnurlPayRequestEntity && lnurlPayRequestEntity.bolt11) { - logger.debug( - "LnurlPay.createLnurlPayRequest, lnurlPayRequest created:", - lnurlPayRequestEntity - ); + if (lnurlPayRequestEntity && lnurlPayRequestEntity.bolt11) { + logger.debug( + "LnurlPay.createLnurlPayRequest, lnurlPayRequest created:", + lnurlPayRequestEntity + ); - response = { - pr: lnurlPayRequestEntity.bolt11, - routes: [], - }; + response = { + pr: lnurlPayRequestEntity.bolt11, + routes: [], + }; + } else { + // LnurlPayRequest not created + logger.debug( + "LnurlPay.createLnurlPayRequest, LnurlPayRequest not created." + ); + + response = { + status: "ERROR", + reason: "payRequest not created", + }; + } + } else { + response = { + status: "ERROR", + reason: "invoice not created", + }; + } } else { - // LnurlPayRequest not created + // There is an error with inputs logger.debug( - "LnurlPay.createLnurlPayRequest, LnurlPayRequest not created." + "LnurlPay.createLnurlPayRequest, there is an error with inputs." ); response = { status: "ERROR", - reason: "payRequest not created", + reason: "Invalid arguments", }; } } else { - response = { - status: "ERROR", - reason: "invoice not created", - }; + logger.debug("LnurlPay.lnServicePaySpecs, deactivated LNURL"); + + response = { status: "ERROR", reason: "Deactivated LNURL" }; } } else { // There is an error with inputs - logger.debug( - "LnurlPay.createLnurlPayRequest, there is an error with inputs." - ); + logger.debug("LnurlPay.lnServicePaySpecs, LNURL not found."); response = { status: "ERROR", - reason: "Invalid arguments", + reason: "Not found", }; } return response; } + /** + * Delete a payRequest, for instance if the LNPay Address is deleted. + */ async deleteLnurlPayRequest( lnurlPayRequestId: number ): Promise { @@ -594,6 +640,9 @@ class LnurlPay { return response; } + /** + * This is called by CN when an LN invoice is paid. + */ async lnurlPayRequestCallback( reqCallback: IReqLnurlPayRequestCallback ): Promise { @@ -634,10 +683,16 @@ class LnurlPay { lnurlPayRequestEntity.paidCalledbackTs = new Date(); await this._lnurlDB.saveLnurlPayRequest(lnurlPayRequestEntity); + + response.result = "success"; + } else { + // This will make Cyphernode redo the callback later + response.error = { + code: ErrorCodes.InternalError, + message: "Downstream callback failed", + }; } } - - response.result = "success"; } else { response.error = { code: ErrorCodes.InvalidRequest, @@ -652,6 +707,9 @@ class LnurlPay { return result; } + /** + * If you want to pay to external LNURL Pay Address + */ async payLnAddress(req: IReqPayLnAddress): Promise { const bolt11 = await LnAddress.fetchBolt11(req.address, req.amountMsat); diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 6037eac..f9b87bf 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -546,9 +546,15 @@ class LnurlWithdraw { logger.debug("LnurlWithdraw.lnFetchPaymentStatus, lnListPays errored..."); } else if (resp.result && resp.result.pays && resp.result.pays.length > 0) { const nonfailedpay = resp.result.pays.find((obj) => { - return ((obj as any).status === "complete" || (obj as any).status === "pending"); - }) - logger.debug("LnurlWithdraw.lnFetchPaymentStatus, nonfailedpay =", nonfailedpay); + return ( + (obj as any).status === "complete" || + (obj as any).status === "pending" + ); + }); + logger.debug( + "LnurlWithdraw.lnFetchPaymentStatus, nonfailedpay =", + nonfailedpay + ); if (nonfailedpay !== undefined) { paymentStatus = (nonfailedpay as any).status; @@ -610,7 +616,9 @@ class LnurlWithdraw { ); // Let's keep two attempts, and put a message in the second one... pay.attempts.splice(2); - pay.attempts[1].failure = { message: "attempts array truncated by lnurl cypherapp" }; + pay.attempts[1].failure = { + message: "attempts array truncated by lnurl cypherapp", + }; } }); @@ -777,7 +785,9 @@ class LnurlWithdraw { let lnurlWithdrawEntitys; if (lnurlWithdrawEntity) { // Let's take the latest on from database, just in case passed object has stale data - lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(lnurlWithdrawEntity.lnurlWithdrawId); + lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws( + lnurlWithdrawEntity.lnurlWithdrawId + ); } else { lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); } diff --git a/src/types/IRespLnServicePayRequest.ts b/src/types/IRespLnServicePayRequest.ts new file mode 100644 index 0000000..be555d9 --- /dev/null +++ b/src/types/IRespLnServicePayRequest.ts @@ -0,0 +1,6 @@ +import IRespLnServiceStatus from "./IRespLnServiceStatus"; + +export default interface IRespLnServicePayRequest extends IRespLnServiceStatus { + pr?: string; + routes?: string[]; +} diff --git a/src/types/IRespLnServicePaySpecs.ts b/src/types/IRespLnServicePaySpecs.ts new file mode 100644 index 0000000..833d235 --- /dev/null +++ b/src/types/IRespLnServicePaySpecs.ts @@ -0,0 +1,9 @@ +import IRespLnServiceStatus from "./IRespLnServiceStatus"; + +export default interface IRespLnServicePaySpecs extends IRespLnServiceStatus { + tag?: string; + callback?: string; + metadata?: string; + minSendable?: number; + maxSendable?: number; +} diff --git a/src/types/IRespLnurlPayRequest.ts b/src/types/IRespLnurlPayRequest.ts index dc2aee4..be4b2bf 100644 --- a/src/types/IRespLnurlPayRequest.ts +++ b/src/types/IRespLnurlPayRequest.ts @@ -1,15 +1,7 @@ -import IRespLnServiceStatus from "./IRespLnServiceStatus"; import { IResponseError } from "./jsonrpc/IResponseMessage"; -import { LnurlPayRequestEntity } from ".prisma/client"; +import { LnurlPayRequestEntity } from "@prisma/client"; -export default interface IRespLnurlPayRequest extends IRespLnServiceStatus { - tag?: string; - callback?: string; - metadata?: string; - minSendable?: number; - maxSendable?: number; - pr?: string; - routes?: string[]; +export default interface IRespLnurlPayRequest { result?: LnurlPayRequestEntity; error?: IResponseError; } diff --git a/src/validators/CreateLnurlPayValidator.ts b/src/validators/CreateLnurlPayValidator.ts index aa17909..c3dd7c1 100644 --- a/src/validators/CreateLnurlPayValidator.ts +++ b/src/validators/CreateLnurlPayValidator.ts @@ -5,6 +5,7 @@ class CreateLnurlPayValidator { if ( !!request.minMsatoshi && !!request.maxMsatoshi && + request.minMsatoshi > 0 && request.maxMsatoshi >= request.minMsatoshi ) { // Mandatory maxMsatoshi at least equal to minMsatoshi diff --git a/src/validators/CreateLnurlPayRequestValidator.ts b/src/validators/LnServicePayValidator.ts similarity index 74% rename from src/validators/CreateLnurlPayRequestValidator.ts rename to src/validators/LnServicePayValidator.ts index 4e1cca8..c94b8b7 100644 --- a/src/validators/CreateLnurlPayRequestValidator.ts +++ b/src/validators/LnServicePayValidator.ts @@ -1,14 +1,14 @@ import { LnurlPayEntity } from ".prisma/client"; import IReqCreateLnurlPayRequest from "../types/IReqCreateLnurlPayRequest"; -class CreateLnurlPayRequestValidator { +class LnServicePayValidator { static validateRequest( lnurlPay: LnurlPayEntity, request: IReqCreateLnurlPayRequest ): boolean { if ( request.amount >= lnurlPay.minMsatoshi && - request.amount <= lnurlPay.minMsatoshi + request.amount <= lnurlPay.maxMsatoshi ) { // Mandatory maxMsatoshi at least equal to minMsatoshi return true; @@ -17,4 +17,4 @@ class CreateLnurlPayRequestValidator { } } -export { CreateLnurlPayRequestValidator }; +export { LnServicePayValidator as CreateLnurlPayRequestValidator }; diff --git a/tests/test-lnurl-pay.sh b/tests/test-lnurl-pay.sh new file mode 100755 index 0000000..b29c78e --- /dev/null +++ b/tests/test-lnurl-pay.sh @@ -0,0 +1,683 @@ +#!/bin/bash + +# This needs to be run in regtest +# You need jq installed for these tests to run correctly + +# Happy path + +# 1. Service creates a LNURL Pay Address +# 2. Get it and compare +# 3. User calls LNServicePay +# 4. User calls LNServicePayRequest +# 5. User pays the received invoice +# 6. Cyphernode ln_pay's callback occurs + +# Invalid LNURL Pay Address Creation + +# 1. Service creates a LNURL Pay Address with invalid min/max +# 2. Service creates a LNURL Pay Address without description + +# Invalid payment to LNURL Pay Address + +# 1. Service creates a LNURL Pay Address +# 2. Get it and compare +# 3. User calls LNServicePay +# 4. User calls LNServicePayRequest with an amount less than min +# 5. User calls LNServicePayRequest with an amount more than max + +# Pay to a deleted LNURL Pay Address + +# 1. Service creates a LNURL Pay Address +# 2. Get it and compare +# 4. User calls LNServicePay +# 5. User calls LNServicePayRequest +# 6. User pays the received invoice +# 7. Cyphernode ln_pay's callback occurs +# 8. Service deletes the LNURL Pay Address +# 9. User calls LNServicePay +# 10. User calls LNServicePayRequest + + + +. ./colors.sh +. ./mine.sh +. ./ln_reconnect.sh + +trace() { + if [ "${1}" -le "${TRACING}" ]; then + echo -e "$(date -u +%FT%TZ) ${2}" 1>&2 + fi +} + +start_test_container() { + docker run -d --rm -it --name tests-lnurl-pay --network=cyphernodeappsnet alpine + docker network connect cyphernodenet tests-lnurl-pay +} + +stop_test_container() { + trace 1 "\n\n[stop_test_container] ${BCyan}Stopping existing containers if they are running...${Color_Off}\n" + + local containers=$(docker ps -q -f "name=tests-lnurl-pay") + if [ -n "${containers}" ]; then + docker stop $(docker ps -q -f "name=tests-lnurl-pay") + fi +} + +exec_in_test_container() { + docker exec -it tests-lnurl-pay "$@" | tr -d '\r\n' +} + +exec_in_test_container_leave_lf() { + docker exec -it tests-lnurl-pay "$@" +} + + +create_lnurl_pay() { + trace 2 "\n\n[create_lnurl_pay] ${BCyan}Service creates a LNURL Pay Address...${Color_Off}\n" + + local webhookUrl=${1} + local externalIdNumber=${2:-$(($RANDOM+${min_max_range}))} + trace 3 "[create_lnurl_pay] externalIdNumber=${externalIdNumber}" + local externalId="lnurlPayAddress-${externalIdNumber}" + trace 3 "[create_lnurl_pay] externalId=${externalId}" + local minMsatoshi=${3:-$((${externalIdNumber}-${min_max_range}))} + trace 3 "[create_lnurl_pay] minMsatoshi=${minMsatoshi}" + local maxMsatoshi=$((${externalIdNumber}+${min_max_range})) + trace 3 "[create_lnurl_pay] maxMsatoshi=${maxMsatoshi}" + local description="lnurlPayAddressDescription-${externalIdNumber}" + trace 3 "[create_lnurl_pay] description=${description}" + + # Service creates LNURL Pay Address + data='{"id":0,"method":"createLnurlPay","params":{"externalId":"'${externalId}'","minMsatoshi":'${minMsatoshi}',"maxMsatoshi":'${maxMsatoshi}',"description":"'${description}'","webhookUrl":"'${webhookUrl}'/lnurl/paid-'${externalId}'"}}' + trace 3 "[create_lnurl_pay] data=${data}" + trace 3 "[create_lnurl_pay] Calling createLnurlPay..." + local createLnurlPay=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[create_lnurl_pay] createLnurlPay=${createLnurlPay}" + + echo "${createLnurlPay}" +} + +get_lnurl_pay() { + trace 2 "\n\n[get_lnurl_pay] ${BCyan}Get LNURL Pay...${Color_Off}\n" + + local lnurl_pay_id=${1} + trace 3 "[get_lnurl_pay] lnurl_pay_id=${lnurl_pay_id}" + + # Service creates LNURL Pay + data='{"id":0,"method":"getLnurlPay","params":{"lnurlPayId":'${lnurl_pay_id}'}}' + trace 3 "[get_lnurl_pay] data=${data}" + trace 3 "[get_lnurl_pay] Calling getLnurlPay..." + local getLnurlPay=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[get_lnurl_pay] getLnurlPay=${getLnurlPay}" + + echo "${getLnurlPay}" +} + +delete_lnurl_pay() { + trace 2 "\n\n[delete_lnurl_pay] ${BCyan}Delete LNURL Pay...${Color_Off}\n" + + local lnurl_pay_id=${1} + trace 3 "[delete_lnurl_pay] lnurl_pay_id=${lnurl_pay_id}" + + # Service deletes LNURL Pay + data='{"id":0,"method":"deleteLnurlPay","params":{"lnurlPayId":'${lnurl_pay_id}'}}' + trace 3 "[delete_lnurl_pay] data=${data}" + trace 3 "[delete_lnurl_pay] Calling deleteLnurlPay..." + local deleteLnurlPay=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[delete_lnurl_pay] deleteLnurlPay=${deleteLnurlPay}" + + local deleted=$(echo "${deleteLnurlPay}" | jq ".result.deleted") + if [ "${deleted}" = "false" ]; then + trace 2 "[delete_lnurl_pay] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" + return 1 + fi + + echo "${deleteLnurlPay}" +} + +call_lnservice_pay_request() { + trace 1 "\n[call_lnservice_pay_request] ${BCyan}User calls LN Service LNURL Pay Request...${Color_Off}" + + local url=${1} + trace 2 "[call_lnservice_pay_request] url=${url}" + + local payRequestResponse=$(exec_in_test_container curl -sk ${url}) + trace 2 "[call_lnservice_pay_request] payRequestResponse=${payRequestResponse}" + + echo "${payRequestResponse}" +} + +call_lnservice_pay() { + trace 1 "\n[call_lnservice_pay] ${BCyan}User prepares call to LN Service LNURL Pay...${Color_Off}" + + local payRequestResponse=${1} + trace 2 "[call_lnservice_pay] payRequestResponse=${payRequestResponse}" + local amount=${2} + trace 2 "[call_lnservice_pay] amount=${amount}" + + local callback=$(echo "${payRequestResponse}" | jq -r ".callback") + trace 2 "[call_lnservice_pay] callback=${callback}" + # k1=$(echo "${payRequestResponse}" | jq -r ".k1") + # trace 2 "[call_lnservice_pay] k1=${k1}" + + trace 2 "\n[call_lnservice_pay] ${BCyan}User finally calls LN Service LNURL Pay...${Color_Off}" + trace 2 "[call_lnservice_pay] url=${callback}?amount=${amount}" + payResponse=$(exec_in_test_container curl -sk ${callback}?amount=${amount}) + trace 2 "[call_lnservice_pay] payResponse=${payResponse}" + + echo "${payResponse}" +} + + +decode_lnurl() { + trace 2 "\n\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}\n" + + local lnurl=${1} + + local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' + trace 3 "[decode_lnurl] data=${data}" + local decodedLnurl=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[decode_lnurl] decodedLnurl=${decodedLnurl}" + local url=$(echo "${decodedLnurl}" | jq -r ".result") + trace 3 "[decode_lnurl] url=${url}" + + echo "${url}" +} + +create_bolt11() { + trace 2 "\n\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}\n" + + local msatoshi=${1} + trace 3 "[create_bolt11] msatoshi=${msatoshi}" + local desc=${2} + trace 3 "[create_bolt11] desc=${desc}" + + local invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli invoice ${msatoshi} "${desc}" "${desc}") + trace 3 "[create_bolt11] invoice=${invoice}" + + echo "${invoice}" +} + +get_invoice_status_ln1() { + local status=$(get_invoice_status "${1}") + trace 3 "[get_invoice_status_ln1] status=${status}" + + echo "${status}" +} + +get_invoice_status_ln2() { + local status=$(get_invoice_status "${1}" 2) + trace 3 "[get_invoice_status_ln2] status=${status}" + + echo "${status}" +} + +get_invoice_status() { + trace 2 "\n\n[get_invoice_status] ${BCyan}Getting invoice status...${Color_Off}\n" + + local payment_hash=${1} + trace 3 "[get_invoice_status] payment_hash=${payment_hash}" + local instance=${2} + trace 3 "[get_invoice_status] instance=${instance}" + + # local payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") + # trace 3 "[get_invoice_status] payment_hash=${payment_hash}" + local data='{"id":1,"jsonrpc":"2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' + trace 3 "[get_invoice_status] data=${data}" + local invoices=$(docker exec -it `docker ps -q -f "name=lightning${instance}\."` lightning-cli listinvoices -k payment_hash=${payment_hash}) + trace 3 "[get_invoice_status] invoices=${invoices}" + local status=$(echo "${invoices}" | jq -r ".invoices[0].status") + trace 3 "[get_invoice_status] status=${status}" + + echo "${status}" +} + +call_lnservice_withdraw() { + trace 1 "\n[call_lnservice_withdraw] ${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}" + + local withdrawRequestResponse=${1} + trace 2 "[call_lnservice_withdraw] withdrawRequestResponse=${withdrawRequestResponse}" + local bolt11=${2} + trace 2 "[call_lnservice_withdraw] bolt11=${bolt11}" + + local callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") + trace 2 "[call_lnservice_withdraw] callback=${callback}" + k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") + trace 2 "[call_lnservice_withdraw] k1=${k1}" + + trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" + trace 2 "[call_lnservice_withdraw] url=${callback}?k1=${k1}\&pr=${bolt11}" + withdrawResponse=$(exec_in_test_container curl -sk ${callback}?k1=${k1}\&pr=${bolt11}) + trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" + + echo "${withdrawResponse}" +} + +happy_path() { + # Happy path + + # 1. Service creates a LNURL Pay Address + # 2. Get it and compare + # 3. User calls LNServicePay + # 4. User calls LNServicePayRequest + # 5. User pays the received invoice + # 6. Cyphernode ln_pay's callback occurs + + trace 1 "\n\n[happy_path] ${On_Yellow}${BBlack} Happy path: ${Color_Off}\n" + + local callbackurl=${1} + + # 1. Service creates a LNURL Pay Address + + local externalIdRandom=$(($RANDOM+${min_max_range})) + trace 3 "[happy_path] externalIdRandom=${externalIdRandom}" + local createLnurlPay=$(create_lnurl_pay "${callbackurl}" ${externalIdRandom}) + trace 3 "[happy_path] createLnurlPay=${createLnurlPay}" + local lnurl_pay_id=$(echo "${createLnurlPay}" | jq -r ".result.lnurlPayId") + + # 2. Get it and compare + + local get_lnurl_pay=$(get_lnurl_pay ${lnurl_pay_id}) + trace 3 "[happy_path] get_lnurl_pay=${get_lnurl_pay}" + local equals=$(jq --argjson a "${createLnurlPay}" --argjson b "${get_lnurl_pay}" -n '$a == $b') + trace 3 "[happy_path] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[happy_path] EQUALS!" + else + trace 1 "\n[happy_path] ${On_Red}${BBlack} Happy path: NOT EQUALS! ${Color_Off}\n" + return 1 + fi + + local lnurl=$(echo "${get_lnurl_pay}" | jq -r ".result.lnurl") + trace 3 "[happy_path] lnurl=${lnurl}" + + # Decode LNURL + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[happy_path] serviceUrl=${serviceUrl}" + + # 3. User calls LNServicePay + + local payRequestResponse=$(call_lnservice_pay_request "${serviceUrl}") + trace 3 "[happy_path] payRequestResponse=${payRequestResponse}" + + # 4. User calls LNServicePayRequest + + local payResponse=$(call_lnservice_pay "${payRequestResponse}" ${externalIdRandom}) + trace 3 "[happy_path] payResponse=${payResponse}" + local bolt11=$(echo ${payResponse} | jq -r ".pr") + trace 3 "[happy_path] bolt11=${bolt11}" + + # Reconnecting the two LN instances... + ln_reconnect + + start_callback_server + + trace 2 "\n\n[happy_path] ${BPurple}Waiting for the LNURL payment callback...\n${Color_Off}" + + # 5. User pays the received invoice + + local data='{"id":1,"jsonrpc":"2.0","method":"pay","params":["'${bolt11}'"]}' + trace 3 "[happy_path] data=${data}" + local lnpay=$(exec_in_test_container curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 3 "[happy_path] lnpay=${lnpay}" + + # 6. Cyphernode ln_pay's callback occurs + + wait + + local payment_hash=$(echo ${lnpay} | jq -r ".payment_hash") + trace 3 "[happy_path] payment_hash=${payment_hash}" + + # We want to see if payment received (invoice status paid) + status=$(get_invoice_status_ln1 "${payment_hash}") + trace 3 "[happy_path] status=${status}" + + if [ "${status}" = "paid" ]; then + trace 1 "\n\n[happy_path] ${On_IGreen}${BBlack} Happy path: SUCCESS! ${Color_Off}\n" + date + return 0 + else + trace 1 "\n\n[happy_path] ${On_Red}${BBlack} Happy path: FAILURE! ${Color_Off}\n" + date + return 1 + fi +} + + +invalid_creation() { + # Invalid LNURL Pay Address Creation + + # 1. Service creates a LNURL Pay Address with invalid min amount + # 2. Service creates a LNURL Pay Address without description + + trace 1 "\n\n[invalid_creation] ${On_Yellow}${BBlack} Invalid LNURL Pay Address creation: ${Color_Off}\n" + + local callbackurl=${1} + + # 1. Service creates a LNURL Pay Address + + local externalIdRandom=$(($RANDOM+${min_max_range})) + trace 3 "[invalid_creation] externalIdRandom=${externalIdRandom}" + # the min amount of 0 should make the creation fail + local createLnurlPay=$(create_lnurl_pay "${callbackurl}" ${externalIdRandom} 0) + trace 3 "[invalid_creation] createLnurlPay=${createLnurlPay}" + + echo "${createLnurlPay}" | grep -qi "error" + if [ "$?" -eq "0" ]; then + trace 1 "\n\n[invalid_creation] ${On_IGreen}${BBlack} invalid_creation: minAmount 0 failed, good! ${Color_Off}\n" + else + trace 1 "\n\n[invalid_creation] ${On_Red}${BBlack} invalid_creation: minAmount 0 should have failed! ${Color_Off}\n" + return 1 + fi + + createLnurlPay=$(create_lnurl_pay "${callbackurl}" ${externalIdRandom} -1) + trace 3 "[invalid_creation] createLnurlPay=${createLnurlPay}" + + echo "${createLnurlPay}" | grep -qi "error" + if [ "$?" -eq "0" ]; then + trace 1 "\n\n[invalid_creation] ${On_IGreen}${BBlack} invalid_creation: minAmount -1 failed, good! ${Color_Off}\n" + else + trace 1 "\n\n[invalid_creation] ${On_Red}${BBlack} invalid_creation: minAmount -1 should have failed! ${Color_Off}\n" + return 1 + fi + + local toolarge=$((${externalIdRandom}+${min_max_range}+1)) + trace 3 "[invalid_creation] toolarge=${toolarge}" + + createLnurlPay=$(create_lnurl_pay "${callbackurl}" ${externalIdRandom} ${toolarge}) + trace 3 "[invalid_creation] createLnurlPay=${createLnurlPay}" + + echo "${createLnurlPay}" | grep -qi "error" + if [ "$?" -eq "0" ]; then + trace 1 "\n\n[invalid_creation] ${On_IGreen}${BBlack} invalid_creation: minAmount larger than maxAmount failed, good! ${Color_Off}\n" + else + trace 1 "\n\n[invalid_creation] ${On_Red}${BBlack} invalid_creation: minAmount larger than maxAmount should have failed! ${Color_Off}\n" + return 1 + fi + + local maxAmount=$((${externalIdRandom}+${min_max_range})) + trace 3 "[invalid_creation] maxAmount=${maxAmount}" + + createLnurlPay=$(create_lnurl_pay "${callbackurl}" ${externalIdRandom} ${maxAmount}) + trace 3 "[invalid_creation] createLnurlPay=${createLnurlPay}" + + echo "${createLnurlPay}" | grep -qi "error" + if [ "$?" -eq "1" ]; then + trace 1 "\n\n[invalid_creation] ${On_IGreen}${BBlack} invalid_creation: minAmount equals maxAmount worked, good! ${Color_Off}\n" + else + trace 1 "\n\n[invalid_creation] ${On_Red}${BBlack} invalid_creation: minAmount equals maxAmount should have worked! NOT good! ${Color_Off}\n" + return 1 + fi +} + + +invalid_payment() { + # Invalid payment to LNURL Pay Address + + # 1. Service creates a LNURL Pay Address + # 2. Get it and compare + # 3. User calls LNServicePay + # 4. User calls LNServicePayRequest with an amount less than min + # 5. User calls LNServicePayRequest with an amount more than max + + trace 1 "\n\n[invalid_payment] ${On_Yellow}${BBlack} Invalid payment: ${Color_Off}\n" + + local callbackurl=${1} + + # 1. Service creates a LNURL Pay Address + + local externalIdRandom=$(($RANDOM+${min_max_range})) + trace 3 "[invalid_payment] externalIdRandom=${externalIdRandom}" + local createLnurlPay=$(create_lnurl_pay "${callbackurl}" ${externalIdRandom}) + trace 3 "[invalid_payment] createLnurlPay=${createLnurlPay}" + local lnurl_pay_id=$(echo "${createLnurlPay}" | jq -r ".result.lnurlPayId") + + # 2. Get it and compare + + local get_lnurl_pay=$(get_lnurl_pay ${lnurl_pay_id}) + trace 3 "[invalid_payment] get_lnurl_pay=${get_lnurl_pay}" + local equals=$(jq --argjson a "${createLnurlPay}" --argjson b "${get_lnurl_pay}" -n '$a == $b') + trace 3 "[invalid_payment] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[invalid_payment] EQUALS!" + else + trace 1 "\n[invalid_payment] ${On_Red}${BBlack} Invalid Payment: NOT EQUALS! ${Color_Off}\n" + return 1 + fi + + local lnurl=$(echo "${get_lnurl_pay}" | jq -r ".result.lnurl") + trace 3 "[invalid_payment] lnurl=${lnurl}" + + # Decode LNURL + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[invalid_payment] serviceUrl=${serviceUrl}" + + # 3. User calls LNServicePay + + local payRequestResponse=$(call_lnservice_pay_request "${serviceUrl}") + trace 3 "[invalid_payment] payRequestResponse=${payRequestResponse}" + + # 4. User calls LNServicePayRequest with an amount less than min + + local toosmall=$((${externalIdRandom}-${min_max_range}-1)) + trace 3 "[invalid_payment] toosmall=${toosmall}" + + local payResponse=$(call_lnservice_pay "${payRequestResponse}" ${toosmall}) + trace 3 "[invalid_payment] payResponse=${payResponse}" + + echo "${payResponse}" | grep -qi "error" + if [ "$?" -eq "0" ]; then + trace 1 "\n\n[invalid_payment] ${On_IGreen}${BBlack} invalid_payment: amount smaller than minAmount failed, good! ${Color_Off}\n" + else + trace 1 "\n\n[invalid_payment] ${On_Red}${BBlack} invalid_payment: amount smaller than minAmount should have failed! ${Color_Off}\n" + return 1 + fi + + sleep 1 + + # 5. User calls LNServicePayRequest with an amount more than max + + local toolarge=$((${externalIdRandom}+${min_max_range}+1)) + trace 3 "[invalid_payment] toolarge=${toolarge}" + + local payResponse=$(call_lnservice_pay "${payRequestResponse}" ${toolarge}) + trace 3 "[invalid_payment] payResponse=${payResponse}" + + echo "${payResponse}" | grep -qi "error" + if [ "$?" -eq "0" ]; then + trace 1 "\n\n[invalid_payment] ${On_IGreen}${BBlack} invalid_payment: amount larger than maxAmount failed, good! ${Color_Off}\n" + else + trace 1 "\n\n[invalid_payment] ${On_Red}${BBlack} invalid_payment: amount larger than maxAmount should have failed! ${Color_Off}\n" + return 1 + fi +} + +pay_to_deleted() { + # Pay to a deleted LNURL Pay Address + + # 1. Service creates a LNURL Pay Address + # 2. Get it and compare + # 4. User calls LNServicePay + # 5. User calls LNServicePayRequest + # 6. User pays the received invoice + # 7. Cyphernode ln_pay's callback occurs + # 8. Service deletes the LNURL Pay Address + # 9. User calls LNServicePay + # 10. User calls LNServicePayRequest + + trace 1 "\n\n[pay_to_deleted] ${On_Yellow}${BBlack} Pay to deleted LNURL Pay Address: ${Color_Off}\n" + + local callbackurl=${1} + + # 1. Service creates a LNURL Pay Address + + local externalIdRandom=$(($RANDOM+${min_max_range})) + trace 3 "[pay_to_deleted] externalIdRandom=${externalIdRandom}" + local createLnurlPay=$(create_lnurl_pay "${callbackurl}" ${externalIdRandom}) + trace 3 "[pay_to_deleted] createLnurlPay=${createLnurlPay}" + local lnurl_pay_id=$(echo "${createLnurlPay}" | jq -r ".result.lnurlPayId") + + # 2. Get it and compare + + local get_lnurl_pay=$(get_lnurl_pay ${lnurl_pay_id}) + trace 3 "[pay_to_deleted] get_lnurl_pay=${get_lnurl_pay}" + local equals=$(jq --argjson a "${createLnurlPay}" --argjson b "${get_lnurl_pay}" -n '$a == $b') + trace 3 "[pay_to_deleted] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[pay_to_deleted] EQUALS!" + else + trace 1 "\n[pay_to_deleted] ${On_Red}${BBlack} Pay to deleted: NOT EQUALS! ${Color_Off}\n" + return 1 + fi + + local lnurl=$(echo "${get_lnurl_pay}" | jq -r ".result.lnurl") + trace 3 "[pay_to_deleted] lnurl=${lnurl}" + + # Decode LNURL + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[pay_to_deleted] serviceUrl=${serviceUrl}" + + # 3. User calls LNServicePay + + local payRequestResponse=$(call_lnservice_pay_request "${serviceUrl}") + trace 3 "[pay_to_deleted] payRequestResponse=${payRequestResponse}" + + sleep 1 + + # 4. User calls LNServicePayRequest + + local payResponse=$(call_lnservice_pay "${payRequestResponse}" ${externalIdRandom}) + trace 3 "[pay_to_deleted] payResponse=${payResponse}" + local bolt11=$(echo ${payResponse} | jq -r ".pr") + trace 3 "[pay_to_deleted] bolt11=${bolt11}" + + # Reconnecting the two LN instances... + ln_reconnect + + start_callback_server + + trace 2 "\n\n[pay_to_deleted] ${BPurple}Waiting for the LNURL payment callback...\n${Color_Off}" + + # 5. User pays the received invoice + + local data='{"id":1,"jsonrpc":"2.0","method":"pay","params":["'${bolt11}'"]}' + trace 3 "[pay_to_deleted] data=${data}" + local lnpay=$(exec_in_test_container curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 3 "[pay_to_deleted] lnpay=${lnpay}" + + # 6. Cyphernode ln_pay's callback occurs + + wait + + local payment_hash=$(echo ${lnpay} | jq -r ".payment_hash") + trace 3 "[pay_to_deleted] payment_hash=${payment_hash}" + + # We want to see if payment received (invoice status paid) + status=$(get_invoice_status_ln1 "${payment_hash}") + trace 3 "[pay_to_deleted] status=${status}" + + if [ "${status}" = "paid" ]; then + trace 1 "\n\n[pay_to_deleted] ${On_IGreen}${BBlack} Pay to deleted: SUCCESS! ${Color_Off}\n" + else + trace 1 "\n\n[pay_to_deleted] ${On_Red}${BBlack} Pay to deleted: FAILURE! ${Color_Off}\n" + date + return 1 + fi + + # 8. Service deletes the LNURL Pay Address + local delete_lnurl_pay=$(delete_lnurl_pay ${lnurl_pay_id}) + trace 3 "[pay_to_deleted] delete_lnurl_pay=${delete_lnurl_pay}" + local deleted=$(echo "${get_lnurl_pay}" | jq '.result.deleted = true | del(.result.updatedAt)') + trace 3 "[pay_to_deleted] deleted=${deleted}" + + get_lnurl_pay=$(get_lnurl_pay ${lnurl_pay_id} | jq 'del(.result.updatedAt)') + trace 3 "[pay_to_deleted] get_lnurl_pay=${get_lnurl_pay}" + equals=$(jq --argjson a "${deleted}" --argjson b "${get_lnurl_pay}" -n '$a == $b') + trace 3 "[pay_to_deleted] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[pay_to_deleted] EQUALS!" + else + trace 1 "\n\n[pay_to_deleted] ${On_Red}${BBlack} Pay to deleted: NOT EQUALS! ${Color_Off}\n" + return 1 + fi + + # Delete it twice... + trace 3 "[pay_to_deleted] Let's delete it again..." + delete_lnurl_pay=$(delete_lnurl_pay ${lnurl_pay_id}) + trace 3 "[pay_to_deleted] delete_lnurl_pay=${delete_lnurl_pay}" + echo "${delete_lnurl_pay}" | grep -qi "already deactivated" + if [ "$?" -ne "0" ]; then + trace 1 "\n\n[pay_to_deleted] ${On_Red}${BBlack} Pay to deleted: Should return an error because already deactivated! ${Color_Off}\n" + return 1 + else + trace 1 "[pay_to_deleted] DELETED! Good!..." + fi + + # 9. User calls LNServicePay + local payRequestResponse2=$(call_lnservice_pay_request "${serviceUrl}") + trace 3 "[pay_to_deleted] payRequestResponse2=${payRequestResponse2}" + + echo "${payRequestResponse2}" | grep -qi "Deactivated" + if [ "$?" -ne "0" ]; then + trace 1 "\n\n[pay_to_deleted] ${On_Red}${BBlack} Pay to deleted: NOT DELETED! ${Color_Off}\n" + return 1 + else + trace 1 "\n\n[pay_to_deleted] ${On_IGreen}${BBlack} Pay to deleted: SUCCESS! ${Color_Off}\n" + fi + + # 10. User calls LNServicePayRequest + payResponse=$(call_lnservice_pay "${payRequestResponse}" ${externalIdRandom}) + trace 3 "[pay_to_deleted] payResponse=${payResponse}" + + echo "${payResponse}" | grep -qi "Deactivated" + if [ "$?" -ne "0" ]; then + trace 1 "\n\n[pay_to_deleted] ${On_Red}${BBlack} Pay to deleted: NOT DELETED! ${Color_Off}\n" + return 1 + else + trace 1 "\n\n[pay_to_deleted] ${On_IGreen}${BBlack} Pay to deleted: SUCCESS! ${Color_Off}\n" + fi +} + +start_callback_server() { + trace 1 "\n\n[start_callback_server] ${BCyan}Let's start a callback server!...${Color_Off}\n" + + port=${1:-${callbackserverport}} + conainerseq=${2} + + docker run --rm -t --name tests-lnurl-pay-cb${conainerseq} --network=cyphernodeappsnet alpine sh -c \ + "nc -vlp${port} -e sh -c 'echo -en \"HTTP/1.1 200 OK\\\\r\\\\n\\\\r\\\\n\" ; echo -en \"\\033[40m\\033[0;37m\" >&2 ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo -e \"\033[0m\" >&2'" & + sleep 2 + # docker network connect cyphernodenet tests-lnurl-withdraw-cb${conainerseq} +} + +TRACING=3 + +date + +stop_test_container +start_test_container + +callbackserverport="1111" +callbackservername="tests-lnurl-pay-cb" +callbackurl="http://${callbackservername}:${callbackserverport}" + +min_max_range=1000 + +trace 1 "\n\n[test-lnurl-pay] ${BCyan}Installing needed packages...${Color_Off}\n" +exec_in_test_container_leave_lf apk add --update curl + +ln_reconnect + +happy_path "${callbackurl}" \ +&& invalid_creation "${callbackurl}" \ +&& invalid_payment "${callbackurl}" \ +&& pay_to_deleted "${callbackurl}" + +trace 1 "\n\n[test-lnurl-pay] ${BCyan}Tearing down...${Color_Off}\n" +wait + +stop_test_container + +date + +trace 1 "\n\n[test-lnurl-pay] ${BCyan}See ya!${Color_Off}\n" From ea30e43a1ae9a22f7e3b9bb6bfebfdfb718b91db Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 7 Mar 2023 19:55:02 +0000 Subject: [PATCH 41/52] Fixes to desc_hash, more tests, lnurlpay-wallet --- cypherapps/data/config.json | 7 +- src/config/LnurlConfig.ts | 7 +- src/lib/HttpServer.ts | 21 +- src/lib/LnurlPay.ts | 40 ++-- src/lib/LnurlWithdraw.ts | 22 +- tests/lnurl_pay_wallet.sh | 358 +++++++++++++++++++++++++++++++++ tests/lnurl_withdraw_wallet.sh | 2 +- tests/test-lnurl-pay.sh | 124 ++++++------ 8 files changed, 490 insertions(+), 91 deletions(-) create mode 100755 tests/lnurl_pay_wallet.sh diff --git a/cypherapps/data/config.json b/cypherapps/data/config.json index dbe7735..b180605 100644 --- a/cypherapps/data/config.json +++ b/cypherapps/data/config.json @@ -6,20 +6,21 @@ "URL_API_SERVER": "http://lnurl", "URL_API_PORT": 8000, "URL_API_CTX": "/api", - "URL_CTX_WEBHOOKS": "/webhooks", + "URL_CTX_WITHDRAW_WEBHOOKS": "/webhooks", + "URL_CTX_PAY_WEBHOOKS": "/payWebhooks", "SESSION_TIMEOUT": 600, "CN_URL": "https://gatekeeper:2009/v0", "CN_API_ID": "003", "CN_API_KEY": "bdd3fc82dff1fb9193a9c15c676e79da10367a540a85cb50b736e77f452f9dc6", "BATCHER_URL": "http://batcher:8000", - "LN_SERVICE_SERVER": "https://yourdomain", + "LN_SERVICE_SCHEME": "https", + "LN_SERVICE_DOMAIN": "yourdomain", "LN_SERVICE_PORT": 443, "LN_SERVICE_CTX": "/lnurl", "LN_SERVICE_WITHDRAW_REQUEST_CTX": "/withdrawRequest", "LN_SERVICE_WITHDRAW_CTX": "/withdraw", "LN_SERVICE_PAY_SPECS_CTX": "/paySpecs", "LN_SERVICE_PAY_REQUEST_CTX": "/payRequest", - "LN_SERVICE_PAY_CB_CTX": "/payReqCallback", "RETRY_WEBHOOKS_TIMEOUT": 1, "CHECK_EXPIRATION_TIMEOUT": 1 } \ No newline at end of file diff --git a/src/config/LnurlConfig.ts b/src/config/LnurlConfig.ts index 3fbfea6..480dacd 100644 --- a/src/config/LnurlConfig.ts +++ b/src/config/LnurlConfig.ts @@ -6,20 +6,21 @@ export default interface LnurlConfig { URL_API_SERVER: string; URL_API_PORT: number; URL_API_CTX: string; - URL_CTX_WEBHOOKS: string; + URL_CTX_WITHDRAW_WEBHOOKS: string; + URL_CTX_PAY_WEBHOOKS: string; SESSION_TIMEOUT: number; CN_URL: string; CN_API_ID: string; CN_API_KEY: string; BATCHER_URL: string; - LN_SERVICE_SERVER: string; + LN_SERVICE_SCHEME: string; + LN_SERVICE_DOMAIN: string; LN_SERVICE_PORT: number; LN_SERVICE_CTX: string; LN_SERVICE_WITHDRAW_REQUEST_CTX: string; LN_SERVICE_WITHDRAW_CTX: string; LN_SERVICE_PAY_SPECS_CTX: string; LN_SERVICE_PAY_REQUEST_CTX: string; - LN_SERVICE_PAY_CB_CTX: string; RETRY_WEBHOOKS_TIMEOUT: number; CHECK_EXPIRATION_TIMEOUT: number; } diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 6b1f638..89cb783 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -335,7 +335,7 @@ class HttpServer { // LN Service LNURL Withdraw Request this._httpServer.get( - // this._lnurlConfig.LN_SERVICE_CTX + + // this._lnurlConfig.LN_SERVICE_CTX + // Stripped by traefik this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX, async (req, res) => { logger.info( @@ -357,7 +357,7 @@ class HttpServer { // LN Service LNURL Withdraw this._httpServer.get( - // this._lnurlConfig.LN_SERVICE_CTX + + // this._lnurlConfig.LN_SERVICE_CTX + // Stripped by traefik this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, async (req, res) => { logger.info(this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX + ":", req.query); @@ -378,7 +378,7 @@ class HttpServer { // LN Service LNURL Pay specs (step 3) this._httpServer.get( - // this._lnurlConfig.LN_SERVICE_CTX + + // this._lnurlConfig.LN_SERVICE_CTX + // Stripped by traefik this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX + "/:externalId", async (req, res) => { logger.info( @@ -418,7 +418,7 @@ class HttpServer { // LN Service LNURL Pay request (step 5) this._httpServer.get( - // this._lnurlConfig.LN_SERVICE_CTX + + // this._lnurlConfig.LN_SERVICE_CTX + // Stripped by traefik this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + "/:externalId", async (req, res) => { logger.info( @@ -441,10 +441,10 @@ class HttpServer { // LN Service LNURL Pay request callback (called by CN when bolt11 paid) this._httpServer.post( - // this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + "/:label", + // this._lnurlConfig.LN_SERVICE_CTX + // Stripped by traefik + this._lnurlConfig.URL_CTX_PAY_WEBHOOKS + "/:label", async (req, res) => { - logger.info(this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + ":", req.params); + logger.info(this._lnurlConfig.URL_CTX_PAY_WEBHOOKS + ":", req.params); const response = await this._lnurlPay.lnurlPayRequestCallback({ bolt11Label: req.params.label, @@ -459,9 +459,12 @@ class HttpServer { ); this._httpServer.post( - this._lnurlConfig.URL_CTX_WEBHOOKS, + this._lnurlConfig.URL_CTX_WITHDRAW_WEBHOOKS, async (req, res) => { - logger.info(this._lnurlConfig.URL_CTX_WEBHOOKS + ":", req.body); + logger.info( + this._lnurlConfig.URL_CTX_WITHDRAW_WEBHOOKS + ":", + req.body + ); const response = await this._lnurlWithdraw.processBatchWebhook( req.body diff --git a/src/lib/LnurlPay.ts b/src/lib/LnurlPay.ts index cff2189..3ae6230 100644 --- a/src/lib/LnurlPay.ts +++ b/src/lib/LnurlPay.ts @@ -1,5 +1,4 @@ import logger from "./Log2File"; -import crypto from "crypto"; import LnurlConfig from "../config/LnurlConfig"; import { CyphernodeClient } from "./CyphernodeClient"; import { LnurlDB } from "./LnurlDBPrisma"; @@ -44,13 +43,19 @@ class LnurlPay { }); } + // https://localhost/lnurl/paySpecs/externalId // https://localhost/lnurl/payRequest/externalId - // https://localhost/lnurl/pay/externalId - // https://lnurl.bullbitcoin.com/ + // https://lnurl.bullbitcoin.com/paySpecs/externalId + // https://lnurl.bullbitcoin.com/payRequest/externalId lnurlPayUrl(externalId: string, req = false): string { return ( - this._lnurlConfig.LN_SERVICE_SERVER + - (this._lnurlConfig.LN_SERVICE_PORT === 443 + this._lnurlConfig.LN_SERVICE_SCHEME + + "://" + + this._lnurlConfig.LN_SERVICE_DOMAIN + + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && + this._lnurlConfig.LN_SERVICE_PORT === 443) || + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && + this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + @@ -336,8 +341,14 @@ class LnurlPay { if (lnurlPay.externalId) { const metadata = JSON.stringify([ ["text/plain", lnurlPay.description], + [ + "text/identifier", + `${lnurlPay.externalId}@${this._lnurlConfig.LN_SERVICE_DOMAIN}`, + ], ]); + logger.info("metadata =", metadata); + response = { callback: this.lnurlPayUrl(lnurlPay.externalId, true), maxSendable: lnurlPay.maxMsatoshi, @@ -400,21 +411,24 @@ class LnurlPay { const metadata = JSON.stringify([ ["text/plain", lnurlPay.description], + [ + "text/identifier", + `${lnurlPay.externalId}@${this._lnurlConfig.LN_SERVICE_DOMAIN}`, + ], ]); - const hHash = crypto.createHmac("sha256", metadata).digest("hex"); + logger.debug("metadata =", metadata); + const label = lnurlPay.lnurlPayId + "-" + Date.now(); const lnCreateParams = { msatoshi: reqCreateLnurlPayReq.amount as number, label: label, - description: hHash, + description: metadata, callbackUrl: - this._lnurlConfig.LN_SERVICE_SERVER + - (this._lnurlConfig.LN_SERVICE_PORT === 443 - ? "" - : ":" + this._lnurlConfig.LN_SERVICE_PORT) + - this._lnurlConfig.LN_SERVICE_CTX + - this._lnurlConfig.LN_SERVICE_PAY_CB_CTX + + this._lnurlConfig.URL_API_SERVER + + ":" + + this._lnurlConfig.URL_API_PORT + + this._lnurlConfig.URL_CTX_PAY_WEBHOOKS + "/" + label, deschashonly: true, diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index f9b87bf..ae31432 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -94,8 +94,13 @@ class LnurlWithdraw { const secretToken = randomBytes(16).toString("hex"); const lnurlDecoded = - this._lnurlConfig.LN_SERVICE_SERVER + - (this._lnurlConfig.LN_SERVICE_PORT === 443 + this._lnurlConfig.LN_SERVICE_SCHEME + + "://" + + this._lnurlConfig.LN_SERVICE_DOMAIN + + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && + this._lnurlConfig.LN_SERVICE_PORT === 443) || + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && + this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + @@ -353,8 +358,15 @@ class LnurlWithdraw { result = { tag: "withdrawRequest", callback: - this._lnurlConfig.LN_SERVICE_SERVER + - (this._lnurlConfig.LN_SERVICE_PORT === 443 + this._lnurlConfig.LN_SERVICE_SCHEME + + "://" + + this._lnurlConfig.LN_SERVICE_DOMAIN + + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === + "https" && + this._lnurlConfig.LN_SERVICE_PORT === 443) || + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === + "http" && + this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + @@ -970,7 +982,7 @@ class LnurlWithdraw { this._lnurlConfig.URL_API_SERVER + ":" + this._lnurlConfig.URL_API_PORT + - this._lnurlConfig.URL_CTX_WEBHOOKS, + this._lnurlConfig.URL_CTX_WITHDRAW_WEBHOOKS, }; const resp: IRespBatchRequest = await this._batcherClient.queueForNextBatch( diff --git a/tests/lnurl_pay_wallet.sh b/tests/lnurl_pay_wallet.sh new file mode 100755 index 0000000..b3d03ce --- /dev/null +++ b/tests/lnurl_pay_wallet.sh @@ -0,0 +1,358 @@ +#!/bin/bash + +# +# This is a super basic LNURL-pay-compatible wallet, command-line +# Useful to test LNURL on a regtest or testnet environment. +# + +# +# This script assumes you have lightning and lightning2 running. +# It will use lightning as the destination wallet (service's wallet) +# It will use lightning2 as the source wallet (user's wallet) +# +# If you don't have a lnurl-pay and want to create one to call pay: +# +# ./lnurl_pay_wallet.sh createlnurl +# ./lnurl_pay_wallet.sh createlnurl "bob" +# +# If you have a lnurl-pay string and want to pay to it from your lightning2 node: +# +# ./lnurl_pay_wallet.sh +# ./lnurl_pay_wallet.sh LNURL1DP68GURN8GHJ7ARJV9JKV6TT9AKXUATJDSHHQCTE2DCX2CMN9A4K27RTV4UNQVC2VDRYE 30000 +# +# If you have a lnurl-pay static address (eg bob@localhost) and want to pay to it: +# +# ./lnurl_pay_wallet.sh paytoaddress
+# ./lnurl_pay_wallet.sh paytoaddress bob@traefik 30100 +# + +. ./colors.sh + +trace() { + if [ "${1}" -le "${TRACING}" ]; then + echo -e "$(date -u +%FT%TZ) ${2}" 1>&2 + fi +} + +start_test_container() { + docker run -d --rm -it --name lnurl-pay-wallet --network=cyphernodeappsnet alpine + docker network connect cyphernodenet lnurl-pay-wallet +} + +stop_test_container() { + trace 1 "\n\n[stop_test_container] ${BCyan}Stopping existing containers if they are running...${Color_Off}\n" + + local containers=$(docker ps -q -f "name=lnurl-pay-wallet") + if [ -n "${containers}" ]; then + docker stop $(docker ps -q -f "name=lnurl-pay-wallet") + fi +} + +exec_in_test_container() { + docker exec -it lnurl-pay-wallet "$@" | tr -d '\r\n' +} + +exec_in_test_container_leave_lf() { + docker exec -it lnurl-pay-wallet "$@" +} + +create_lnurl_pay() { + trace 2 "\n\n[create_lnurl_pay] ${BCyan}Service creates a LNURL Pay Address...${Color_Off}\n" + + local webhookUrl=${1} + + local externalId=${2} + + trace 3 "[create_lnurl_pay] externalId=${externalId}" + local minMsatoshi=$(($RANDOM-${min_max_range})) + trace 3 "[create_lnurl_pay] minMsatoshi=${minMsatoshi}" + local maxMsatoshi=$((${minMsatoshi}+${min_max_range}+${min_max_range})) + trace 3 "[create_lnurl_pay] maxMsatoshi=${maxMsatoshi}" + local description="lnurlPayAddressDescription-${externalId}" + trace 3 "[create_lnurl_pay] description=${description}" + + # Service creates LNURL Pay Address + data='{"id":0,"method":"createLnurlPay","params":{"externalId":"'${externalId}'","minMsatoshi":'${minMsatoshi}',"maxMsatoshi":'${maxMsatoshi}',"description":"'${description}'","webhookUrl":"'${webhookUrl}'/lnurl/paid-'${externalId}'"}}' + trace 3 "[create_lnurl_pay] data=${data}" + trace 3 "[create_lnurl_pay] Calling createLnurlPay..." + local createLnurlPay=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[create_lnurl_pay] createLnurlPay=${createLnurlPay}" + + echo "${createLnurlPay}" +} + +get_lnurl_pay() { + trace 2 "\n\n[get_lnurl_pay] ${BCyan}Get LNURL Pay...${Color_Off}\n" + + local lnurl_pay_id=${1} + trace 3 "[get_lnurl_pay] lnurl_pay_id=${lnurl_pay_id}" + + # Service creates LNURL Pay + data='{"id":0,"method":"getLnurlPay","params":{"lnurlPayId":'${lnurl_pay_id}'}}' + trace 3 "[get_lnurl_pay] data=${data}" + trace 3 "[get_lnurl_pay] Calling getLnurlPay..." + local getLnurlPay=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[get_lnurl_pay] getLnurlPay=${getLnurlPay}" + + echo "${getLnurlPay}" +} + +delete_lnurl_pay() { + trace 2 "\n\n[delete_lnurl_pay] ${BCyan}Delete LNURL Pay...${Color_Off}\n" + + local lnurl_pay_id=${1} + trace 3 "[delete_lnurl_pay] lnurl_pay_id=${lnurl_pay_id}" + + # Service deletes LNURL Pay + data='{"id":0,"method":"deleteLnurlPay","params":{"lnurlPayId":'${lnurl_pay_id}'}}' + trace 3 "[delete_lnurl_pay] data=${data}" + trace 3 "[delete_lnurl_pay] Calling deleteLnurlPay..." + local deleteLnurlPay=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[delete_lnurl_pay] deleteLnurlPay=${deleteLnurlPay}" + + local deleted=$(echo "${deleteLnurlPay}" | jq ".result.deleted") + if [ "${deleted}" = "false" ]; then + trace 2 "[delete_lnurl_pay] ${On_Red}${BBlack} NOT DELETED! ${Color_Off}" + return 1 + fi + + echo "${deleteLnurlPay}" +} + +call_lnservice_pay_specs() { + trace 1 "\n[call_lnservice_pay_specs] ${BCyan}User calls LN Service LNURL Pay Specs...${Color_Off}" + + local url=${1} + trace 2 "[call_lnservice_pay_specs] url=${url}" + + local paySpecsResponse=$(exec_in_test_container curl -sk ${url}) + trace 2 "[call_lnservice_pay_specs] paySpecsResponse=${paySpecsResponse}" + + echo "${paySpecsResponse}" +} + +call_lnservice_pay_request() { + trace 1 "\n[call_lnservice_pay_request] ${BCyan}User prepares call to LN Service LNURL Pay Request...${Color_Off}" + + local paySpecsResponse=${1} + trace 2 "[call_lnservice_pay_request] paySpecsResponse=${paySpecsResponse}" + local amount=${2} + trace 2 "[call_lnservice_pay_request] amount=${amount}" + + local callback=$(echo "${paySpecsResponse}" | jq -r ".callback") + trace 2 "[call_lnservice_pay_request] callback=${callback}" + + trace 2 "\n[call_lnservice_pay_request] ${BCyan}User finally calls LN Service LNURL Pay Request...${Color_Off}" + trace 2 "[call_lnservice_pay_request] url=${callback}?amount=${amount}" + payRequestResponse=$(exec_in_test_container curl -sk ${callback}?amount=${amount}) + trace 2 "[call_lnservice_pay_request] payRequestResponse=${payRequestResponse}" + + echo "${payRequestResponse}" +} + + +decode_lnurl() { + trace 2 "\n\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}\n" + + local lnurl=${1} + + local data='{"id":0,"method":"decodeBech32","params":{"s":"'${lnurl}'"}}' + trace 3 "[decode_lnurl] data=${data}" + local decodedLnurl=$(exec_in_test_container curl -sd "${data}" -H "Content-Type: application/json" lnurl:8000/api) + trace 3 "[decode_lnurl] decodedLnurl=${decodedLnurl}" + local url=$(echo "${decodedLnurl}" | jq -r ".result") + trace 3 "[decode_lnurl] url=${url}" + + echo "${url}" +} + +decode_bolt11() { + trace 2 "\n\n[decode_bolt11] ${BCyan}Decoding bolt11 string...${Color_Off}\n" + + local bolt11=${1} + + local data='{"id":1,"jsonrpc":"2.0","method":"decode","params":["'${bolt11}'"]}' + trace 3 "[decode_bolt11] data=${data}" + local decoded=$(exec_in_test_container curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 3 "[decode_bolt11] decoded=${decoded}" + + echo "${decoded}" +} + +check_desc_hash() { + trace 2 "\n\n[check_desc_hash] ${BCyan}Checking description hash...${Color_Off}\n" + + local bolt11=${1} + local paySpecsResponse=${2} + + local decoded=$(decode_bolt11 "${bolt11}") + trace 3 "[check_desc_hash] decoded=${decoded}" + local description_hash=$(echo "${decoded}" | jq -r ".description_hash") + trace 3 "[check_desc_hash] description_hash=${description_hash}" + + local metadata=$(echo "${paySpecsResponse}" | jq -r ".metadata") + trace 3 "[happy_path] metadata=${metadata}" + local computed_hash=$(echo -n ${metadata} | shasum -a 256 | cut -d' ' -f1) + trace 3 "[check_desc_hash] computed_hash=${computed_hash}" + + if [ "${computed_hash}" = "${description_hash}" ]; then + trace 1 "\n\n[check_desc_hash] ${On_IGreen}${BBlack} check_desc_hash: description hash is good! ${Color_Off}\n" + date + return 0 + else + trace 1 "\n\n[check_desc_hash] ${On_Red}${BBlack} check_desc_hash: description hash not good! FAILURE! ${Color_Off}\n" + date + return 1 + fi +} + +pay_with_ln1() { + local lnPay=$(ln_pay "${1}") + trace 3 "[pay_with_ln1] lnPay=${lnPay}" + + echo "${lnPay}" +} + +pay_with_ln2() { + local lnPay=$(ln_pay "${1}" 2) + trace 3 "[pay_with_ln2] lnPay=${lnPay}" + + echo "${lnPay}" +} + +ln_pay() { + trace 2 "\n\n[ln_pay] ${BCyan}Paying an invoice...${Color_Off}\n" + + local bolt11=${1} + trace 3 "[ln_pay] payment_hash=${payment_hash}" + local instance=${2} + trace 3 "[ln_pay] instance=${instance}" + + local data='{"id":1,"jsonrpc":"2.0","method":"pay","params":["'${bolt11}'"]}' + trace 3 "[ln_pay] data=${data}" + local lnpay=$(exec_in_test_container curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet${instance}:9737/rpc) + trace 3 "[ln_pay] lnpay=${lnpay}" + + echo "${lnpay}" +} + +get_invoice_status() { + trace 2 "\n\n[get_invoice_status] ${BCyan}Getting invoice status...${Color_Off}\n" + + local invoice=${1} + trace 3 "[get_invoice_status] invoice=${invoice}" + + local payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") + trace 3 "[get_invoice_status] payment_hash=${payment_hash}" + local data='{"id":1,"jsonrpc":"2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' + trace 3 "[get_invoice_status] data=${data}" + local invoices=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli --lightning-dir=/.lightning listinvoices -k payment_hash=${payment_hash}) + trace 3 "[get_invoice_status] invoices=${invoices}" + local status=$(echo "${invoices}" | jq -r ".invoices[0].status") + trace 3 "[get_invoice_status] status=${status}" + + echo "${status}" +} + +pay_to() { + local url=${1} + local msatoshi=${2} + + local paySpecsResponse=$(call_lnservice_pay_specs "${url}") + trace 2 "[pay_to] paySpecsResponse=${paySpecsResponse}" + + # {"status":"ERROR","reason":"Expired LNURL-Pay"} + local reason=$(echo "${paySpecsResponse}" | jq -r ".reason // empty") + + if [ -n "${reason}" ]; then + trace 1 "\n\n[pay_to] ERROR! Reason: ${reason}\n\n" + else + local payResponse=$(call_lnservice_pay_request "${paySpecsResponse}" "${msatoshi}") + trace 2 "[pay_to] payResponse=${payResponse}" + + reason=$(echo "${payResponse}" | jq -r ".reason // empty") + + if [ -n "${reason}" ]; then + trace 1 "\n\n[pay_to] ERROR! Reason: ${reason}\n\n" + else + local bolt11=$(echo "${payResponse}" | jq -r ".pr") + trace 2 "[pay_to] bolt11=${bolt11}" + + check_desc_hash "${bolt11}" "${paySpecsResponse}" + if [ "$?" = "1" ]; then + trace 1 "\n\n[pay_to] ERROR! Reason: description_hash doesn't match metadata!\n\n" + return 1 + fi + + local lnPay=$(pay_with_ln2 "${bolt11}") + trace 2 "[pay_to] lnPay=${lnPay}" + fi + fi +} + +TRACING=3 + +date + +stop_test_container +start_test_container + +trace 1 "\n\n[lnurl-pay-wallet] ${BCyan}Installing needed packages...${Color_Off}\n" +exec_in_test_container_leave_lf apk add --update curl + +lnurl=${1} +trace 2 "[lnurl-pay-wallet] lnurl=${lnurl}" + +if [ "${lnurl}" = "createlnurl" ]; then + trace 2 "\n\n[lnurl-pay-wallet] ${BCyan} createlnurl...${Color_Off}\n" + + min_max_range=1000 + + address=${2} + trace 2 "[lnurl-pay-wallet] address=${address}" + + # Initializing test variables + trace 2 "\n\n[lnurl-pay-wallet] ${BCyan}Initializing test variables...${Color_Off}\n" + + trace 3 "[lnurl-pay-wallet] callbackurl=${callbackurl}" + + createLnurlPay=$(create_lnurl_pay "${callbackurl}" "${address}") + trace 3 "[lnurl-pay-wallet] createLnurlPay=${createLnurlPay}" + lnurl=$(echo "${createLnurlPay}" | jq -r ".result.lnurl") + trace 3 "[lnurl-pay-wallet] lnurl=${lnurl}" +elif [ "${lnurl}" = "paytoaddress" ]; then + trace 2 "\n\n[lnurl-pay-wallet] ${BCyan} paytoaddress...${Color_Off}\n" + + # kexkey03@traefik + address=${2} + trace 2 "[lnurl-pay-wallet] address=${address}" + user=$(echo "${address}" | cut -d'@' -f1) + trace 2 "[lnurl-pay-wallet] user=${user}" + domain=$(echo "${address}" | cut -d'@' -f2) + trace 2 "[lnurl-pay-wallet] domain=${domain}" + + msatoshi=${3} + trace 2 "[lnurl-pay-wallet] msatoshi=${msatoshi}" + + url="https://${domain}/lnurl/.well-known/lnurlp/${user}" + + pay_to "${url}" ${msatoshi} + +else + url=$(decode_lnurl "${lnurl}") + trace 2 "[lnurl-pay-wallet] url=${url}" + + msatoshi=${2} + trace 2 "[lnurl-pay-wallet] msatoshi=${msatoshi}" + + pay_to "${url}" ${msatoshi} + +fi + +trace 1 "\n\n[lnurl-pay-wallet] ${BCyan}Tearing down...${Color_Off}\n" + +stop_test_container + +date + +trace 1 "\n\n[lnurl-pay-wallet] ${BCyan}See ya!${Color_Off}\n" diff --git a/tests/lnurl_withdraw_wallet.sh b/tests/lnurl_withdraw_wallet.sh index 55fc604..a47d2f8 100755 --- a/tests/lnurl_withdraw_wallet.sh +++ b/tests/lnurl_withdraw_wallet.sh @@ -1,7 +1,7 @@ #!/bin/bash # -# This is a super basic LNURL-compatible wallet, command-line +# This is a super basic LNURL-withdraw-compatible wallet, command-line # Useful to test LNURL on a regtest or testnet environment. # diff --git a/tests/test-lnurl-pay.sh b/tests/test-lnurl-pay.sh index b29c78e..4c0d8b3 100755 --- a/tests/test-lnurl-pay.sh +++ b/tests/test-lnurl-pay.sh @@ -135,40 +135,37 @@ delete_lnurl_pay() { echo "${deleteLnurlPay}" } -call_lnservice_pay_request() { - trace 1 "\n[call_lnservice_pay_request] ${BCyan}User calls LN Service LNURL Pay Request...${Color_Off}" +call_lnservice_pay_specs() { + trace 1 "\n[call_lnservice_pay_specs] ${BCyan}User calls LN Service LNURL Pay Specs...${Color_Off}" local url=${1} - trace 2 "[call_lnservice_pay_request] url=${url}" + trace 2 "[call_lnservice_pay_specs] url=${url}" - local payRequestResponse=$(exec_in_test_container curl -sk ${url}) - trace 2 "[call_lnservice_pay_request] payRequestResponse=${payRequestResponse}" + local paySpecsResponse=$(exec_in_test_container curl -sk ${url}) + trace 2 "[call_lnservice_pay_specs] paySpecsResponse=${paySpecsResponse}" - echo "${payRequestResponse}" + echo "${paySpecsResponse}" } -call_lnservice_pay() { - trace 1 "\n[call_lnservice_pay] ${BCyan}User prepares call to LN Service LNURL Pay...${Color_Off}" +call_lnservice_pay_request() { + trace 1 "\n[call_lnservice_pay_request] ${BCyan}User calls LN Service LNURL Pay Request...${Color_Off}" local payRequestResponse=${1} - trace 2 "[call_lnservice_pay] payRequestResponse=${payRequestResponse}" + trace 2 "[call_lnservice_pay_request] payRequestResponse=${payRequestResponse}" local amount=${2} - trace 2 "[call_lnservice_pay] amount=${amount}" + trace 2 "[call_lnservice_pay_request] amount=${amount}" local callback=$(echo "${payRequestResponse}" | jq -r ".callback") - trace 2 "[call_lnservice_pay] callback=${callback}" - # k1=$(echo "${payRequestResponse}" | jq -r ".k1") - # trace 2 "[call_lnservice_pay] k1=${k1}" + trace 2 "[call_lnservice_pay_request] callback=${callback}" - trace 2 "\n[call_lnservice_pay] ${BCyan}User finally calls LN Service LNURL Pay...${Color_Off}" - trace 2 "[call_lnservice_pay] url=${callback}?amount=${amount}" + trace 2 "\n[call_lnservice_pay_request] ${BCyan}User finally calls LN Service LNURL Pay Request...${Color_Off}" + trace 2 "[call_lnservice_pay_request] url=${callback}?amount=${amount}" payResponse=$(exec_in_test_container curl -sk ${callback}?amount=${amount}) - trace 2 "[call_lnservice_pay] payResponse=${payResponse}" + trace 2 "[call_lnservice_pay_request] payResponse=${payResponse}" echo "${payResponse}" } - decode_lnurl() { trace 2 "\n\n[decode_lnurl] ${BCyan}Decoding LNURL...${Color_Off}\n" @@ -184,18 +181,17 @@ decode_lnurl() { echo "${url}" } -create_bolt11() { - trace 2 "\n\n[create_bolt11] ${BCyan}User creates bolt11 for the payment...${Color_Off}\n" +decode_bolt11() { + trace 2 "\n\n[decode_bolt11] ${BCyan}Decoding bolt11 string...${Color_Off}\n" - local msatoshi=${1} - trace 3 "[create_bolt11] msatoshi=${msatoshi}" - local desc=${2} - trace 3 "[create_bolt11] desc=${desc}" + local bolt11=${1} - local invoice=$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli invoice ${msatoshi} "${desc}" "${desc}") - trace 3 "[create_bolt11] invoice=${invoice}" + local data='{"id":1,"jsonrpc":"2.0","method":"decode","params":["'${bolt11}'"]}' + trace 3 "[decode_bolt11] data=${data}" + local decoded=$(exec_in_test_container curl -sd "${data}" -H 'X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=' -H "Content-Type: application/json" cyphernode_sparkwallet2:9737/rpc) + trace 3 "[decode_bolt11] decoded=${decoded}" - echo "${invoice}" + echo "${decoded}" } get_invoice_status_ln1() { @@ -220,8 +216,6 @@ get_invoice_status() { local instance=${2} trace 3 "[get_invoice_status] instance=${instance}" - # local payment_hash=$(echo "${invoice}" | jq -r ".payment_hash") - # trace 3 "[get_invoice_status] payment_hash=${payment_hash}" local data='{"id":1,"jsonrpc":"2.0","method":"listinvoices","params":{"payment_hash":"'${payment_hash}'"}}' trace 3 "[get_invoice_status] data=${data}" local invoices=$(docker exec -it `docker ps -q -f "name=lightning${instance}\."` lightning-cli listinvoices -k payment_hash=${payment_hash}) @@ -232,25 +226,31 @@ get_invoice_status() { echo "${status}" } -call_lnservice_withdraw() { - trace 1 "\n[call_lnservice_withdraw] ${BCyan}User prepares call to LN Service LNURL Withdraw...${Color_Off}" +check_desc_hash() { + trace 2 "\n\n[check_desc_hash] ${BCyan}Checking description hash...${Color_Off}\n" - local withdrawRequestResponse=${1} - trace 2 "[call_lnservice_withdraw] withdrawRequestResponse=${withdrawRequestResponse}" - local bolt11=${2} - trace 2 "[call_lnservice_withdraw] bolt11=${bolt11}" + local bolt11=${1} + local paySpecsResponse=${2} - local callback=$(echo "${withdrawRequestResponse}" | jq -r ".callback") - trace 2 "[call_lnservice_withdraw] callback=${callback}" - k1=$(echo "${withdrawRequestResponse}" | jq -r ".k1") - trace 2 "[call_lnservice_withdraw] k1=${k1}" + local decoded=$(decode_bolt11 "${bolt11}") + trace 3 "[check_desc_hash] decoded=${decoded}" + local description_hash=$(echo "${decoded}" | jq -r ".description_hash") + trace 3 "[check_desc_hash] description_hash=${description_hash}" - trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" - trace 2 "[call_lnservice_withdraw] url=${callback}?k1=${k1}\&pr=${bolt11}" - withdrawResponse=$(exec_in_test_container curl -sk ${callback}?k1=${k1}\&pr=${bolt11}) - trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" + local metadata=$(echo "${paySpecsResponse}" | jq -r ".metadata") + trace 3 "[happy_path] metadata=${metadata}" + local computed_hash=$(echo -n ${metadata} | shasum -a 256 | cut -d' ' -f1) + trace 3 "[check_desc_hash] computed_hash=${computed_hash}" - echo "${withdrawResponse}" + if [ "${computed_hash}" = "${description_hash}" ]; then + trace 1 "\n\n[check_desc_hash] ${On_IGreen}${BBlack} check_desc_hash: description hash is good! ${Color_Off}\n" + date + return 0 + else + trace 1 "\n\n[check_desc_hash] ${On_Red}${BBlack} check_desc_hash: description hash not good! FAILURE! ${Color_Off}\n" + date + return 1 + fi } happy_path() { @@ -297,16 +297,21 @@ happy_path() { # 3. User calls LNServicePay - local payRequestResponse=$(call_lnservice_pay_request "${serviceUrl}") - trace 3 "[happy_path] payRequestResponse=${payRequestResponse}" + local paySpecsResponse=$(call_lnservice_pay_specs "${serviceUrl}") + trace 3 "[happy_path] paySpecsResponse=${paySpecsResponse}" # 4. User calls LNServicePayRequest - local payResponse=$(call_lnservice_pay "${payRequestResponse}" ${externalIdRandom}) + local payResponse=$(call_lnservice_pay_request "${paySpecsResponse}" ${externalIdRandom}) trace 3 "[happy_path] payResponse=${payResponse}" local bolt11=$(echo ${payResponse} | jq -r ".pr") trace 3 "[happy_path] bolt11=${bolt11}" + check_desc_hash "${bolt11}" "${paySpecsResponse}" + if [ "$?" = "1" ]; then + return 1 + fi + # Reconnecting the two LN instances... ln_reconnect @@ -454,15 +459,15 @@ invalid_payment() { # 3. User calls LNServicePay - local payRequestResponse=$(call_lnservice_pay_request "${serviceUrl}") - trace 3 "[invalid_payment] payRequestResponse=${payRequestResponse}" + local paySpecsResponse=$(call_lnservice_pay_specs "${serviceUrl}") + trace 3 "[invalid_payment] paySpecsResponse=${paySpecsResponse}" # 4. User calls LNServicePayRequest with an amount less than min local toosmall=$((${externalIdRandom}-${min_max_range}-1)) trace 3 "[invalid_payment] toosmall=${toosmall}" - local payResponse=$(call_lnservice_pay "${payRequestResponse}" ${toosmall}) + local payResponse=$(call_lnservice_pay_request "${paySpecsResponse}" ${toosmall}) trace 3 "[invalid_payment] payResponse=${payResponse}" echo "${payResponse}" | grep -qi "error" @@ -480,7 +485,7 @@ invalid_payment() { local toolarge=$((${externalIdRandom}+${min_max_range}+1)) trace 3 "[invalid_payment] toolarge=${toolarge}" - local payResponse=$(call_lnservice_pay "${payRequestResponse}" ${toolarge}) + local payResponse=$(call_lnservice_pay_request "${paySpecsResponse}" ${toolarge}) trace 3 "[invalid_payment] payResponse=${payResponse}" echo "${payResponse}" | grep -qi "error" @@ -539,18 +544,23 @@ pay_to_deleted() { # 3. User calls LNServicePay - local payRequestResponse=$(call_lnservice_pay_request "${serviceUrl}") - trace 3 "[pay_to_deleted] payRequestResponse=${payRequestResponse}" + local paySpecsResponse=$(call_lnservice_pay_specs "${serviceUrl}") + trace 3 "[pay_to_deleted] paySpecsResponse=${paySpecsResponse}" sleep 1 # 4. User calls LNServicePayRequest - local payResponse=$(call_lnservice_pay "${payRequestResponse}" ${externalIdRandom}) + local payResponse=$(call_lnservice_pay_request "${paySpecsResponse}" ${externalIdRandom}) trace 3 "[pay_to_deleted] payResponse=${payResponse}" local bolt11=$(echo ${payResponse} | jq -r ".pr") trace 3 "[pay_to_deleted] bolt11=${bolt11}" + check_desc_hash "${bolt11}" "${paySpecsResponse}" + if [ "$?" = "1" ]; then + return 1 + fi + # Reconnecting the two LN instances... ln_reconnect @@ -614,10 +624,10 @@ pay_to_deleted() { fi # 9. User calls LNServicePay - local payRequestResponse2=$(call_lnservice_pay_request "${serviceUrl}") - trace 3 "[pay_to_deleted] payRequestResponse2=${payRequestResponse2}" + local paySpecsResponse2=$(call_lnservice_pay_specs "${serviceUrl}") + trace 3 "[pay_to_deleted] paySpecsResponse2=${paySpecsResponse2}" - echo "${payRequestResponse2}" | grep -qi "Deactivated" + echo "${paySpecsResponse2}" | grep -qi "Deactivated" if [ "$?" -ne "0" ]; then trace 1 "\n\n[pay_to_deleted] ${On_Red}${BBlack} Pay to deleted: NOT DELETED! ${Color_Off}\n" return 1 @@ -626,7 +636,7 @@ pay_to_deleted() { fi # 10. User calls LNServicePayRequest - payResponse=$(call_lnservice_pay "${payRequestResponse}" ${externalIdRandom}) + payResponse=$(call_lnservice_pay_request "${paySpecsResponse}" ${externalIdRandom}) trace 3 "[pay_to_deleted] payResponse=${payResponse}" echo "${payResponse}" | grep -qi "Deactivated" From 5b2a6ad024f27ca2450387ddd5bc19ac86f0bb72 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 6 Mar 2024 22:17:10 +0000 Subject: [PATCH 42/52] More tests --- doc/dev-notes | 4 +- src/lib/LnurlPay.ts | 2 +- src/lib/LnurlWithdraw.ts | 23 ++--- src/types/cyphernode/IReqLnPay.ts | 4 +- tests/ln_setup.sh | 18 ++-- tests/lnurl_withdraw_wallet.sh | 4 +- tests/test-lnurl-withdraw.sh | 138 +++++++++++++++++++++++++++++- 7 files changed, 169 insertions(+), 24 deletions(-) diff --git a/doc/dev-notes b/doc/dev-notes index 82b8f34..804ef85 100644 --- a/doc/dev-notes +++ b/doc/dev-notes @@ -47,9 +47,11 @@ npm run build npm run start DEBUG: -docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" -p 9229:9229 -p 8000:8000 --network cyphernodeappsnet --entrypoint ash lnurl +docker run --rm -it --name lnurl -v "$PWD:/lnurl" -v "$PWD/cypherapps/data:/lnurl/data" -v "$PWD/cypherapps/data/logs:/lnurl/logs" -v "/Users/kexkey/dev/cn-dev/dist/cyphernode/gatekeeper/certs/cert.pem:/lnurl/cert.pem:ro" -p 9229:9229 -p 8000:8000 --network cyphernodeappsnet --entrypoint ash lnurl npx prisma migrate reset npx prisma generate npx prisma migrate dev ``` + +curl -d '{"id":0,"method":"createLnurlPay","params":{"externalId":"kexkey","minMsatoshi":1000,"maxMsatoshi":1000000000,"description":"Kexkey'"'"'s jar","webhookUrl":"/lnurl/paid-kexkey"}}' -H "Content-Type: application/json" localhost:8000/api diff --git a/src/lib/LnurlPay.ts b/src/lib/LnurlPay.ts index 3ae6230..ed0dde3 100644 --- a/src/lib/LnurlPay.ts +++ b/src/lib/LnurlPay.ts @@ -731,7 +731,7 @@ class LnurlPay { if (bolt11) { const lnPayParams = { bolt11, - expectedMsatoshi: req.amountMsat, + expected_msatoshi: req.amountMsat, }; let resp: IRespLnPay = await this._cyphernodeClient.lnPay(lnPayParams); diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index ae31432..f640550 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -425,19 +425,13 @@ class LnurlWithdraw { lnurlWithdrawEntity.bolt11 = bolt11; const lnPayParams = { bolt11: bolt11, - expectedMsatoshi: lnurlWithdrawEntity.msatoshi || undefined, - expectedDescription: lnurlWithdrawEntity.description || undefined, + // eslint-disable-next-line @typescript-eslint/camelcase + expected_msatoshi: lnurlWithdrawEntity.msatoshi || undefined, + // eslint-disable-next-line @typescript-eslint/camelcase + expected_description: lnurlWithdrawEntity.description || undefined, }; let resp: IRespLnPay = await this._cyphernodeClient.lnPay(lnPayParams); - if (resp.error) { - logger.debug( - "LnurlWithdraw.processLnPayment, ln_pay error, let's retry #1!" - ); - - resp = await this._cyphernodeClient.lnPay(lnPayParams); - } - if (resp.error) { logger.debug("LnurlWithdraw.processLnPayment, ln_pay error!"); @@ -1242,6 +1236,15 @@ class LnurlWithdraw { result: "Merci bonsouère!", } as IResponseMessage; + if (!lnurlWithdrawEntity) { + result.error = { + code: ErrorCodes.InvalidRequest, + message: "batchRequestId not found", + }; + + return result; + } + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(webhookBody); lnurlWithdrawEntity.withdrawnTs = new Date(); lnurlWithdrawEntity.paid = true; diff --git a/src/types/cyphernode/IReqLnPay.ts b/src/types/cyphernode/IReqLnPay.ts index d00b922..749df40 100644 --- a/src/types/cyphernode/IReqLnPay.ts +++ b/src/types/cyphernode/IReqLnPay.ts @@ -4,6 +4,6 @@ export default interface IReqLnPay { // - expected_description, optional, expected description encoded in the bolt11 invoice bolt11: string; - expectedMsatoshi?: number; - expectedDescription?: string; + expected_msatoshi?: number; + expected_description?: string; } diff --git a/tests/ln_setup.sh b/tests/ln_setup.sh index cd7040a..527cd5e 100755 --- a/tests/ln_setup.sh +++ b/tests/ln_setup.sh @@ -7,7 +7,7 @@ date # Get node2 connection string -connectstring2=$(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli getinfo | jq -r ".id")@lightning2") +connectstring2=$(echo "$(docker exec -it `docker ps -q -f "name=lightning2\."` lightning-cli getinfo | jq -r ".id")@lightning2:9735") echo ; echo "connectstring2=${connectstring2}" # Mine enough blocks @@ -15,12 +15,12 @@ echo ; echo "connectstring2=${connectstring2}" channelmsats=8000000000 # Fund LN node -#address=$(docker exec -it `docker ps -q -f "name=proxy\."` curl localhost:8888/ln_newaddr | jq -r ".bech32") -#echo ; echo "address=${address}" -#data='{"address":"'${address}'","amount":1}' -#docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/spend +address=$(docker exec -it `docker ps -q -f "name=proxy\."` curl localhost:8888/ln_newaddr | jq -r ".bech32") +echo ; echo "address=${address}" +data='{"address":"'${address}'","amount":1}' +docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/spend -#mine 6 +mine 6 echo ; echo "Sleeping 5 seconds..." sleep 5 @@ -34,7 +34,7 @@ echo ; echo "Sleeping 5 seconds..." sleep 5 # Make the channel ready -mine 6 +mine 3 echo ; echo "Sleeping 15 seconds..." sleep 15 @@ -57,6 +57,10 @@ data='{"bolt11":'${bolt11}',"expected_msatoshi":'${msats}',"expected_description echo ; echo "data=${data}" docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/ln_pay +sleep 5 + +mine 1 + echo ; echo "That's all folks!" date diff --git a/tests/lnurl_withdraw_wallet.sh b/tests/lnurl_withdraw_wallet.sh index a47d2f8..7e3d080 100755 --- a/tests/lnurl_withdraw_wallet.sh +++ b/tests/lnurl_withdraw_wallet.sh @@ -104,7 +104,7 @@ call_lnservice_withdraw_request() { local url=${1} trace 2 "[call_lnservice_withdraw_request] url=${url}" - local withdrawRequestResponse=$(exec_in_test_container curl -s ${url}) + local withdrawRequestResponse=$(exec_in_test_container curl -sk ${url}) trace 2 "[call_lnservice_withdraw_request] withdrawRequestResponse=${withdrawRequestResponse}" echo "${withdrawRequestResponse}" @@ -157,7 +157,7 @@ call_lnservice_withdraw() { trace 2 "\n[call_lnservice_withdraw] ${BCyan}User finally calls LN Service LNURL Withdraw...${Color_Off}" trace 2 "[call_lnservice_withdraw] url=${callback}?k1=${k1}\&pr=${bolt11}" - withdrawResponse=$(exec_in_test_container curl -s ${callback}?k1=${k1}\&pr=${bolt11}) + withdrawResponse=$(exec_in_test_container curl -sk ${callback}?k1=${k1}\&pr=${bolt11}) trace 2 "[call_lnservice_withdraw] withdrawResponse=${withdrawResponse}" echo "${withdrawResponse}" diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index 737dd91..2f2c713 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -379,6 +379,137 @@ happy_path() { fi } +wrong_bolt11() { + # wrong_bolt11: + # + # 1. Create a LNURL Withdraw + # 2. Get it and compare + # 3. User calls LNServiceWithdrawRequest + # 4. User calls LNServiceWithdraw with wrong bolt11 + # 5. User calls LNServiceWithdraw with wrong description + + trace 1 "\n\n[wrong_bolt11] ${On_Yellow}${BBlack} wrong_bolt11: ${Color_Off}\n" + + local callbackurl=${1} + + # Service creates LNURL Withdraw + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 15) + trace 3 "[wrong_bolt11] createLnurlWithdraw=${createLnurlWithdraw}" + local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") + trace 3 "lnurl=${lnurl}" + + local lnurl_withdraw_id=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurlWithdrawId") + local get_lnurl_withdraw=$(get_lnurl_withdraw ${lnurl_withdraw_id}) + trace 3 "[wrong_bolt11] get_lnurl_withdraw=${get_lnurl_withdraw}" + local equals=$(jq --argjson a "${createLnurlWithdraw}" --argjson b "${get_lnurl_withdraw}" -n '$a == $b') + trace 3 "[wrong_bolt11] equals=${equals}" + if [ "${equals}" = "true" ]; then + trace 2 "[wrong_bolt11] EQUALS!" + else + trace 1 "\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: NOT EQUALS! ${Color_Off}\n" + return 1 + fi + + # Decode LNURL + local serviceUrl=$(decode_lnurl "${lnurl}") + trace 3 "[wrong_bolt11] serviceUrl=${serviceUrl}" + + # User calls LN Service LNURL Withdraw Request + local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") + trace 3 "[wrong_bolt11] withdrawRequestResponse=${withdrawRequestResponse}" + + # Create bolt11 with wrong amount for LN Service LNURL Withdraw + local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') + msatoshi=$((${msatoshi}+10)) + local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') + local invoice=$(create_bolt11 "${msatoshi}" "${description}") + trace 3 "[wrong_bolt11] invoice=${invoice}" + local bolt11=$(echo ${invoice} | jq -r ".bolt11") + trace 3 "[wrong_bolt11] bolt11=${bolt11}" + + # We want to see that that invoice is unpaid first... + local status=$(get_invoice_status "${invoice}") + trace 3 "[wrong_bolt11] status=${status}" + + trace 2 "\n\n[wrong_bolt11] ${BPurple}User calles LN Service Withdraw with wrong bolt11...\n${Color_Off}" + + # User calls LN Service LNURL Withdraw + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") + trace 3 "[wrong_bolt11] withdrawResponse=${withdrawResponse}" + + status=$(echo ${withdrawResponse} | jq -r ".status") + trace 3 "[wrong_bolt11] status=${status}" + + if [ "${status}" = "ERROR" ]; then + trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} wrong_bolt11: SUCCESS! ${Color_Off}\n" + date + else + trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: FAILURE! ${Color_Off}\n" + date + return 1 + fi + + # We want to see if payment received (invoice status paid) + status=$(get_invoice_status "${invoice}") + trace 3 "[wrong_bolt11] status=${status}" + + if [ "${status}" = "paid" ]; then + trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: FAILURE! ${Color_Off}\n" + date + return 1 + else + trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} wrong_bolt11: SUCCESS! ${Color_Off}\n" + date + # return 0 + fi + + # Create bolt11 with wrong description for LN Service LNURL Withdraw + local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') + local description=$(echo "${createLnurlWithdraw}" | jq -r '.result.description') + description="wrong${description}" + local invoice=$(create_bolt11 "${msatoshi}" "${description}") + trace 3 "[wrong_bolt11] invoice=${invoice}" + local bolt11=$(echo ${invoice} | jq -r ".bolt11") + trace 3 "[wrong_bolt11] bolt11=${bolt11}" + + # We want to see that that invoice is unpaid first... + local status=$(get_invoice_status "${invoice}") + trace 3 "[wrong_bolt11] status=${status}" + + trace 2 "\n\n[wrong_bolt11] ${BPurple}User calles LN Service Withdraw with wrong description...\n${Color_Off}" + + # User calls LN Service LNURL Withdraw + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") + trace 3 "[wrong_bolt11] withdrawResponse=${withdrawResponse}" + + status=$(echo ${withdrawResponse} | jq -r ".status") + trace 3 "[wrong_bolt11] status=${status}" + + if [ "${status}" = "ERROR" ]; then + trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} wrong_bolt11: SUCCESS! ${Color_Off}\n" + date + else + trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: FAILURE! ${Color_Off}\n" + date + return 1 + fi + + # We want to see if payment received (invoice status paid) + status=$(get_invoice_status "${invoice}") + trace 3 "[wrong_bolt11] status=${status}" + + if [ "${status}" = "paid" ]; then + trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: FAILURE! ${Color_Off}\n" + date + return 1 + else + trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} wrong_bolt11: SUCCESS! ${Color_Off}\n" + date + return 0 + fi + +} + expired1() { # Expired 1: # @@ -616,6 +747,9 @@ deleted2() { local deleted=$(echo "${get_lnurl_withdraw}" | jq '.result.deleted = true') trace 3 "[deleted2] deleted=${deleted}" + trace 3 "[deleted2] Sleeping 5 seconds..." + sleep 5 + # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[deleted2] withdrawResponse=${withdrawResponse}" @@ -699,7 +833,7 @@ fallback1() { trace 1 "\n\n[fallback1] ${On_Red}${BBlack} Fallback 1: NOT EXPIRED! ${Color_Off}\n" return 1 else - trace 2 "[fallback1] EXPIRED!" + trace 2 "[fallback1] EXPIRED! Good!" fi wait @@ -1049,6 +1183,7 @@ TRACING=3 date stop_test_container +sleep 5 start_test_container callbackserverport="1111" @@ -1061,6 +1196,7 @@ exec_in_test_container_leave_lf apk add --update curl ln_reconnect happy_path "${callbackurl}" \ +&& wrong_bolt11 "${callbackurl}" \ && expired1 "${callbackurl}" \ && expired2 "${callbackurl}" \ && deleted1 "${callbackurl}" \ From 1efe09118b6ffaab2c66bee819ce0846f9fcac67 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 13 Nov 2024 19:24:17 +0000 Subject: [PATCH 43/52] Fixed mandatory description matching for lnurl-w --- src/lib/LnurlWithdraw.ts | 4 +--- tests/test-lnurl-withdraw.sh | 16 ++++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index f640550..3e4685d 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -427,10 +427,8 @@ class LnurlWithdraw { bolt11: bolt11, // eslint-disable-next-line @typescript-eslint/camelcase expected_msatoshi: lnurlWithdrawEntity.msatoshi || undefined, - // eslint-disable-next-line @typescript-eslint/camelcase - expected_description: lnurlWithdrawEntity.description || undefined, }; - let resp: IRespLnPay = await this._cyphernodeClient.lnPay(lnPayParams); + const resp: IRespLnPay = await this._cyphernodeClient.lnPay(lnPayParams); if (resp.error) { logger.debug("LnurlWithdraw.processLnPayment, ln_pay error!"); diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index 2f2c713..62ef9af 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -386,7 +386,7 @@ wrong_bolt11() { # 2. Get it and compare # 3. User calls LNServiceWithdrawRequest # 4. User calls LNServiceWithdraw with wrong bolt11 - # 5. User calls LNServiceWithdraw with wrong description + # 5. User calls LNServiceWithdraw with wrong description and it should work! trace 1 "\n\n[wrong_bolt11] ${On_Yellow}${BBlack} wrong_bolt11: ${Color_Off}\n" @@ -486,12 +486,12 @@ wrong_bolt11() { trace 3 "[wrong_bolt11] status=${status}" if [ "${status}" = "ERROR" ]; then - trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} wrong_bolt11: SUCCESS! ${Color_Off}\n" - date - else trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: FAILURE! ${Color_Off}\n" date return 1 + else + trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} wrong_bolt11: SUCCESS! ${Color_Off}\n" + date fi # We want to see if payment received (invoice status paid) @@ -499,13 +499,13 @@ wrong_bolt11() { trace 3 "[wrong_bolt11] status=${status}" if [ "${status}" = "paid" ]; then - trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: FAILURE! ${Color_Off}\n" - date - return 1 - else trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} wrong_bolt11: SUCCESS! ${Color_Off}\n" date return 0 + else + trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: FAILURE! ${Color_Off}\n" + date + return 1 fi } From 104e0c51ce1d7e3d451a1107c722ebc256f38b27 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 12 Nov 2024 22:00:13 +0000 Subject: [PATCH 44/52] Validate bolt11 before saving it --- package.json | 2 +- src/lib/CyphernodeClient.ts | 71 +++++++++++++++++++-- src/lib/LnurlWithdraw.ts | 33 +++++++--- src/types/cyphernode/IRespLnDecodeBolt11.ts | 6 ++ tests/test-lnurl-withdraw.sh | 37 +++++++++-- 5 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 src/types/cyphernode/IRespLnDecodeBolt11.ts diff --git a/package.json b/package.json index 4ee4ae7..f78ef6f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@prisma/client": "^3.2.1", "@types/async-lock": "^1.1.2", "async-lock": "^1.2.4", - "axios": "^0.21.1", + "axios": "^1.7.7", "bech32": "^2.0.0", "date-fns": "^2.23.0", "express": "^4.17.1", diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 5966788..ea9e9fe 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -21,6 +21,7 @@ import IReqLnListPays from "../types/cyphernode/IReqLnListPays"; import IRespLnPayStatus from "../types/cyphernode/IRespLnPayStatus"; import IReqLnCreate from "../types/cyphernode/IReqLnCreate"; import IRespLnCreate from "../types/cyphernode/IRespLnCreate"; +import IRespLnDecodeBolt11 from "../types/cyphernode/IRespLnDecodeBolt11"; class CyphernodeClient { private baseURL: string; @@ -106,8 +107,14 @@ class CyphernodeClient { const response = await axios.request(configs); // logger.debug("CyphernodeClient._post :: response:", response); // response.data used to be a string, looks like it's now an object... taking no chance. - const str = typeof response.data === 'string' ? response.data : JSON.stringify(response.data); - logger.debug("CyphernodeClient._post :: response.data:", str.substring(0, 1000)); + const str = + typeof response.data === "string" + ? response.data + : JSON.stringify(response.data); + logger.debug( + "CyphernodeClient._post :: response.data:", + str.substring(0, 1000) + ); return { status: response.status, data: response.data }; } catch (err) { @@ -180,8 +187,14 @@ class CyphernodeClient { try { const response = await axios.request(configs); // response.data used to be a string, looks like it's now an object... taking no chance. - const str = typeof response.data === 'string' ? response.data : JSON.stringify(response.data); - logger.debug("CyphernodeClient._get :: response.data:", str.substring(0, 1000)); + const str = + typeof response.data === "string" + ? response.data + : JSON.stringify(response.data); + logger.debug( + "CyphernodeClient._get :: response.data:", + str.substring(0, 1000) + ); return { status: response.status, data: response.data }; } catch (err) { @@ -537,7 +550,7 @@ class CyphernodeClient { result = { error: { code: ErrorCodes.InternalError, - message: response.data.message, + message: JSON.stringify(response.data), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as IResponseError, } as IRespLnPay; @@ -798,6 +811,54 @@ class CyphernodeClient { } return result; } + + async lnDecodeBolt11(bolt11: string): Promise { + // GET http://192.168.111.152:8080/ln_decodebolt11/bolt11 + // GET http://192.168.111.152:8080/ln_decodebolt11/lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3 + + // args: + // - bolt11, required, lightning network bolt11 invoice + // + // Example of successful result: + // + // { + // "currency": "bcrt", + // "created_at": 1731447599, + // "expiry": 604800, + // "payee": "026ec94ffa595479ccd80fe5f0bdf1481dbb8b75b976a5bfe21f0556c6376d549a", + // "amount_msat": 532204, + // "description": "desc32204", + // "min_final_cltv_expiry": 10, + // "payment_secret": "befdced7b0654a16bc3255ef5edea6d0fc5bcae32f4ba2ccbe848317d66d1c48", + // "features": "02024100", + // "payment_hash": "b5b866e84b9fe2e6cf948a89767a70448d80b82b96e6bdf6f9185ec0a2bdc166", + // "signature": "304402207169439ffd3ea345904b91ecc5f891f9513fcdc787e5ef14ef66aa65959864d80220334fb5b83cc61c3ba1feec3c2b72c3e7b95c27780331fd6f1f3bba641c12da92" + // } + // + // Example of failed result: + // + // { + // code: -1, + // message: 'Invalid bolt11: Bad bech32 string' + // } + + logger.info("CyphernodeClient.lnDecodeBolt11:", bolt11); + + let result: IRespLnCreate; + const response = await this._get("/ln_decodebolt11/" + bolt11); + if (response.status >= 200 && response.status < 400) { + result = { result: response.data }; + } else { + result = { + error: { + code: ErrorCodes.InternalError, + message: response.data.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespLnCreate; + } + return result; + } } export { CyphernodeClient }; diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index f640550..ceb6823 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -25,6 +25,7 @@ import IRespSpend from "../types/cyphernode/IRespSpend"; import { LnurlWithdrawEntity } from "@prisma/client"; import AsyncLock from "async-lock"; import IReqLnListPays from "../types/cyphernode/IReqLnListPays"; +import IRespLnDecodeBolt11 from "../types/cyphernode/IRespLnDecodeBolt11"; class LnurlWithdraw { private _lnurlConfig: LnurlConfig; @@ -422,15 +423,25 @@ class LnurlWithdraw { let result; - lnurlWithdrawEntity.bolt11 = bolt11; - const lnPayParams = { - bolt11: bolt11, - // eslint-disable-next-line @typescript-eslint/camelcase - expected_msatoshi: lnurlWithdrawEntity.msatoshi || undefined, - // eslint-disable-next-line @typescript-eslint/camelcase - expected_description: lnurlWithdrawEntity.description || undefined, - }; - let resp: IRespLnPay = await this._cyphernodeClient.lnPay(lnPayParams); + // Let's check if bolt11 is valid first. + // If it's valid, we'll try to pay and save the data. + // If it's not valid, we'll send an error and won't save the data. + let resp: + | IRespLnDecodeBolt11 + | IRespLnPay = await this._cyphernodeClient.lnDecodeBolt11(bolt11); + + if (resp.result) { + lnurlWithdrawEntity.bolt11 = bolt11; + + const lnPayParams = { + bolt11: bolt11, + // eslint-disable-next-line @typescript-eslint/camelcase + expected_msatoshi: lnurlWithdrawEntity.msatoshi || undefined, + // eslint-disable-next-line @typescript-eslint/camelcase + expected_description: lnurlWithdrawEntity.description || undefined, + }; + resp = await this._cyphernodeClient.lnPay(lnPayParams); + } if (resp.error) { logger.debug("LnurlWithdraw.processLnPayment, ln_pay error!"); @@ -710,6 +721,10 @@ class LnurlWithdraw { params.pr, paymentStatus.result ); + logger.debug( + "this.processLnStatus result =", + JSON.stringify(result) + ); } } else { // Not previously claimed LNURL diff --git a/src/types/cyphernode/IRespLnDecodeBolt11.ts b/src/types/cyphernode/IRespLnDecodeBolt11.ts new file mode 100644 index 0000000..a8b76e3 --- /dev/null +++ b/src/types/cyphernode/IRespLnDecodeBolt11.ts @@ -0,0 +1,6 @@ +import { IResponseError } from "../jsonrpc/IResponseMessage"; + +export default interface IRespLnDecodeBolt11 { + result?: unknown; + error?: IResponseError; +} diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index 2f2c713..e04fc9f 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -385,8 +385,9 @@ wrong_bolt11() { # 1. Create a LNURL Withdraw # 2. Get it and compare # 3. User calls LNServiceWithdrawRequest - # 4. User calls LNServiceWithdraw with wrong bolt11 - # 5. User calls LNServiceWithdraw with wrong description + # 4. User calls LNServiceWithdraw with an invalid bolt11 + # 5. User calls LNServiceWithdraw with wrong amount in bolt11 + # 6. User calls LNServiceWithdraw with wrong description in bolt11 trace 1 "\n\n[wrong_bolt11] ${On_Yellow}${BBlack} wrong_bolt11: ${Color_Off}\n" @@ -418,6 +419,28 @@ wrong_bolt11() { local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[wrong_bolt11] withdrawRequestResponse=${withdrawRequestResponse}" + # 4. User calls LNServiceWithdraw with an invalid bolt11 + local bolt11="invalidbolt11" + trace 3 "[wrong_bolt11] bolt11=${bolt11}" + + trace 2 "\n\n[wrong_bolt11] ${BPurple}User calls LN Service Withdraw with an invalid bolt11...\n${Color_Off}" + + # User calls LN Service LNURL Withdraw + local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") + trace 3 "[wrong_bolt11] withdrawResponse=${withdrawResponse}" + + status=$(echo ${withdrawResponse} | jq -r ".status") + trace 3 "[wrong_bolt11] status=${status}" + + if [ "${status}" = "ERROR" ]; then + trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} wrong_bolt11: SUCCESS! ${Color_Off}\n" + date + else + trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} wrong_bolt11: FAILURE! ${Color_Off}\n" + date + return 1 + fi + # Create bolt11 with wrong amount for LN Service LNURL Withdraw local msatoshi=$(echo "${createLnurlWithdraw}" | jq -r '.result.msatoshi') msatoshi=$((${msatoshi}+10)) @@ -431,7 +454,7 @@ wrong_bolt11() { local status=$(get_invoice_status "${invoice}") trace 3 "[wrong_bolt11] status=${status}" - trace 2 "\n\n[wrong_bolt11] ${BPurple}User calles LN Service Withdraw with wrong bolt11...\n${Color_Off}" + trace 2 "\n\n[wrong_bolt11] ${BPurple}User calls LN Service Withdraw with wrong amount in bolt11...\n${Color_Off}" # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") @@ -469,14 +492,14 @@ wrong_bolt11() { description="wrong${description}" local invoice=$(create_bolt11 "${msatoshi}" "${description}") trace 3 "[wrong_bolt11] invoice=${invoice}" - local bolt11=$(echo ${invoice} | jq -r ".bolt11") + bolt11=$(echo ${invoice} | jq -r ".bolt11") trace 3 "[wrong_bolt11] bolt11=${bolt11}" # We want to see that that invoice is unpaid first... local status=$(get_invoice_status "${invoice}") trace 3 "[wrong_bolt11] status=${status}" - trace 2 "\n\n[wrong_bolt11] ${BPurple}User calles LN Service Withdraw with wrong description...\n${Color_Off}" + trace 2 "\n\n[wrong_bolt11] ${BPurple}User calls LN Service Withdraw with wrong description in bolt11...\n${Color_Off}" # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") @@ -799,6 +822,7 @@ fallback1() { # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0 "" "${btcfallbackaddr}") + # local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) trace 3 "[fallback1] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "[fallback1] lnurl=${lnurl}" @@ -885,6 +909,7 @@ fallback2() { # Service creates LNURL Withdraw with batching true local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0 "" "${btcfallbackaddr}" "true") + # local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0 "" "" "true") trace 3 "[fallback2] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "[fallback2] lnurl=${lnurl}" @@ -981,6 +1006,7 @@ fallback3() { # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 86400 "" "${btcfallbackaddr}") + # local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 86400) trace 3 "[fallback3] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "[fallback3] lnurl=${lnurl}" @@ -1061,6 +1087,7 @@ fallback4() { # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 86400 "" "${btcfallbackaddr}") + # local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 86400) trace 3 "[fallback4] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "[fallback4] lnurl=${lnurl}" From 608ae8297346f22f82e873136775aa2dc0f924e7 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 15 Nov 2024 20:49:27 +0000 Subject: [PATCH 45/52] Added a webhook when a failed claim attempt occurs --- doc/LNURL-Withdraw.md | 15 + src/lib/CyphernodeClient.ts | 4 +- src/lib/LnurlWithdraw.ts | 19 + tests/test-lnurl-withdraw.sh | 33 +- yarn.lock | 1687 ++++++++++++++++++++++++++++++++++ 5 files changed, 1744 insertions(+), 14 deletions(-) create mode 100644 yarn.lock diff --git a/doc/LNURL-Withdraw.md b/doc/LNURL-Withdraw.md index 7ba781c..fc222a8 100644 --- a/doc/LNURL-Withdraw.md +++ b/doc/LNURL-Withdraw.md @@ -362,3 +362,18 @@ Response: } } ``` + +- LNURL Withdraw attempt failed + +```json +{ + "action": "claimAttemptFailed", + "lnurlWithdrawId": 250, + "bolt11": "lnbcrt5201080p1pnn0f2csp5exylqpp349slwnum55z0msx9jr367xkju459nz2gl3vzgsz6zdvspp5gh0vusulhesdrtykt8xhhcvmtvgarwkl30j2ulgt48fx7gr5dqzsdq0v3jhxcejxqcnqwqxqyjw5qcqp29qxpqysgqaf76lktf0up2yhfhmwttkpz84v2y75vsnysh02p2jq0vf7ncgrhnnp5t5d84czzq9av7w0yjhm8trxnqy67hh5nh45usptuktlw8u4qquwjr42", + "lnPayResponse": { + "code": -32603, + "message": "Destination 026ec94ffa595479ccd80fe5f0bdf1481dbb8b75b976a5bfe21f0556c6376d549a is not reachable directly and all routehints were unusable." + }, + "updatedTs": "2024-11-15T19:47:36.217Z" +} +``` diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index ea9e9fe..54c91f5 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -550,7 +550,9 @@ class CyphernodeClient { result = { error: { code: ErrorCodes.InternalError, - message: JSON.stringify(response.data), + message: response.data.message + ? response.data.message + : JSON.stringify(response.data), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as IResponseError, } as IRespLnPay; diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 8f58fb5..8faf10f 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -451,6 +451,25 @@ class LnurlWithdraw { lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( lnurlWithdrawEntity ); + + if (lnurlWithdrawEntity?.webhookUrl && lnurlWithdrawEntity?.webhookUrl.length > 0) { + // Immediately send a webhook to let client know a failed attempt has been made. + const postdata = { + action: "claimAttemptFailed", + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + bolt11: lnurlWithdrawEntity.bolt11, + lnPayResponse: resp.error, + updatedTs: lnurlWithdrawEntity.updatedTs + }; + + logger.debug( + "LnurlWithdraw.processLnPayment, claim attempt failed, calling back with postdata=", + postdata + ); + + Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); + } + } else { logger.debug("LnurlWithdraw.processLnPayment, ln_pay success!"); diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index 3ee114c..e3aa5ee 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -454,6 +454,9 @@ wrong_bolt11() { local status=$(get_invoice_status "${invoice}") trace 3 "[wrong_bolt11] status=${status}" + # "Failed claim attempt" callback + start_callback_server + trace 2 "\n\n[wrong_bolt11] ${BPurple}User calls LN Service Withdraw with wrong amount in bolt11...\n${Color_Off}" # User calls LN Service LNURL Withdraw @@ -1139,6 +1142,9 @@ fallback4() { trace 1 "\n\n[fallback4] ${On_IGreen}${BBlack} fallback4: Status unpaid! Good! ${Color_Off}\n" fi + # "Failed claim attempt" callback + start_callback_server + # 6. User calls LNServiceWithdraw -> fails because LN02 is down trace 3 "[fallback4] Shutting down lightning2..." docker stop $(docker ps -q -f "name=lightning2") @@ -1197,12 +1203,12 @@ start_callback_server() { trace 1 "\n\n[start_callback_server] ${BCyan}Let's start a callback server!...${Color_Off}\n" port=${1:-${callbackserverport}} - conainerseq=${2} + cotnainerseq=${2} - docker run --rm -t --name tests-lnurl-withdraw-cb${conainerseq} --network=cyphernodeappsnet alpine sh -c \ + docker run --rm -t --name tests-lnurl-withdraw-cb${cotnainerseq} --network=cyphernodeappsnet alpine sh -c \ "nc -vlp${port} -e sh -c 'echo -en \"HTTP/1.1 200 OK\\\\r\\\\n\\\\r\\\\n\" ; echo -en \"\\033[40m\\033[0;37m\" >&2 ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo -e \"\033[0m\" >&2'" & sleep 2 - # docker network connect cyphernodenet tests-lnurl-withdraw-cb${conainerseq} + # docker network connect cyphernodenet tests-lnurl-withdraw-cb${cotnainerseq} } TRACING=3 @@ -1222,16 +1228,17 @@ exec_in_test_container_leave_lf apk add --update curl ln_reconnect -happy_path "${callbackurl}" \ -&& wrong_bolt11 "${callbackurl}" \ -&& expired1 "${callbackurl}" \ -&& expired2 "${callbackurl}" \ -&& deleted1 "${callbackurl}" \ -&& deleted2 "${callbackurl}" \ -&& fallback1 "${callbackservername}" "${callbackserverport}" \ -&& fallback2 "${callbackservername}" "${callbackserverport}" \ -&& fallback3 "${callbackservername}" "${callbackserverport}" \ -&& fallback4 "${callbackservername}" "${callbackserverport}" +happy_path "${callbackurl}" && \ +wrong_bolt11 "${callbackurl}" && \ +expired1 "${callbackurl}" && \ +expired2 "${callbackurl}" && \ +deleted1 "${callbackurl}" && \ +deleted2 "${callbackurl}" && \ +fallback1 "${callbackservername}" "${callbackserverport}" && \ +fallback2 "${callbackservername}" "${callbackserverport}" && \ +fallback3 "${callbackservername}" "${callbackserverport}" && \ +fallback4 "${callbackservername}" "${callbackserverport}" && \ +trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}All tests passed!${Color_Off}\n" trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}Tearing down...${Color_Off}\n" wait diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..ef4bc8a --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1687 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/runtime@^7.21.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + +"@prisma/client@^3.2.1": + version "3.15.2" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.15.2.tgz#2181398147afc79bfe0d83c03a88dc45b49bd365" + integrity sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w== + dependencies: + "@prisma/engines-version" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + +"@prisma/engines-version@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e": + version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53" + integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w== + +"@prisma/engines@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e": + version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#f691893df506b93e3cb1ccc15ec6e5ac64e8e570" + integrity sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg== + +"@types/async-lock@^1.1.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.4.2.tgz#c2037ba1d6018de766c2505c3abe3b7b6b244ab4" + integrity sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw== + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.6": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/json-schema@^7.0.3": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node@*": + version "22.9.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365" + integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ== + dependencies: + undici-types "~6.19.8" + +"@types/node@^13.13.52": + version "13.13.52" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" + integrity sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ== + +"@types/qs@*": + version "6.9.17" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.17.tgz#fc560f60946d0aeff2f914eb41679659d3310e1a" + integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@typescript-eslint/eslint-plugin@^2.24.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" + integrity sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ== + dependencies: + "@typescript-eslint/experimental-utils" "2.34.0" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@2.34.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" + integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.34.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/parser@^2.24.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8" + integrity sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.34.0" + "@typescript-eslint/typescript-estree" "2.34.0" + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/typescript-estree@2.34.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" + integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +ajv@^6.10.0, ajv@^6.10.2: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-lock@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f" + integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.1.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +date-fns@^2.23.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.0.1, debug@^4.1.1: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +eslint-config-prettier@^6.11.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9" + integrity sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw== + dependencies: + get-stdin "^6.0.0" + +eslint-plugin-prettier@^3.1.4: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5" + integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-scope@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-utils@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.3" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== + dependencies: + acorn "^7.1.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.1.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^4.17.1: + version "4.21.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" + integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.10" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + +glob-parent@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-status-codes@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-1.4.0.tgz#6e4c15d16ff3a9e2df03b89f3a55e1aae05fb477" + integrity sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ== + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +import-fresh@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inquirer@^7.0.0: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +object-inspect@^1.13.1: + version "1.13.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + +picocolors@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== + +prisma@^3.2.1: + version "3.15.2" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.15.2.tgz#4ebe32fb284da3ac60c49fbc16c75e56ecf32067" + integrity sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA== + dependencies: + "@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +reflect-metadata@^0.1.13: + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +regexpp@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^5.5.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.1.2: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.2: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +source-map-support@^0.5.17, source-map-support@^0.5.21: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-node@^8.10.2: + version "8.10.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + +tslib@^1.8.1, tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslog@^3.2.0: + version "3.3.4" + resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.3.4.tgz#083197a908c97b3b714a0576b9dac293f223f368" + integrity sha512-N0HHuHE0e/o75ALfkioFObknHR5dVchUad4F0XyFf3gXJYB++DewEzwGI/uIOM216E5a43ovnRNEeQIq9qgm4Q== + dependencies: + source-map-support "^0.5.21" + +tsutils@^3.17.1: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^4.1.0: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +undici-types@~6.19.8: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +v8-compile-cache@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" + integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From 323c88069a75c201dffdc180d9ab3c01c7723e19 Mon Sep 17 00:00:00 2001 From: kexkey Date: Sat, 16 Nov 2024 12:27:47 +0000 Subject: [PATCH 46/52] typo --- tests/test-lnurl-withdraw.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index e3aa5ee..e4708f3 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -1203,12 +1203,12 @@ start_callback_server() { trace 1 "\n\n[start_callback_server] ${BCyan}Let's start a callback server!...${Color_Off}\n" port=${1:-${callbackserverport}} - cotnainerseq=${2} + containerseq=${2} - docker run --rm -t --name tests-lnurl-withdraw-cb${cotnainerseq} --network=cyphernodeappsnet alpine sh -c \ + docker run --rm -t --name tests-lnurl-withdraw-cb${containerseq} --network=cyphernodeappsnet alpine sh -c \ "nc -vlp${port} -e sh -c 'echo -en \"HTTP/1.1 200 OK\\\\r\\\\n\\\\r\\\\n\" ; echo -en \"\\033[40m\\033[0;37m\" >&2 ; date >&2 ; timeout 1 tee /dev/tty | cat ; echo -e \"\033[0m\" >&2'" & sleep 2 - # docker network connect cyphernodenet tests-lnurl-withdraw-cb${cotnainerseq} + # docker network connect cyphernodenet tests-lnurl-withdraw-cb${containerseq} } TRACING=3 From d6bddf1dcbcd2db26b2b00f36b1d5680f3a191df Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 19 Nov 2024 15:28:56 +1100 Subject: [PATCH 47/52] IReqPayLnAddress fix (#15) --- src/lib/HttpServer.ts | 1 + src/lib/LnurlPay.ts | 6 ++++-- src/types/IReqPayLnAddress.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 89cb783..8788a3f 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -24,6 +24,7 @@ import IRespLnurlPay from "../types/IRespLnurlPay"; import IRespLnurlPayRequest from "../types/IRespLnurlPayRequest"; import IReqUpdateLnurlPay from "../types/IReqUpdateLnurlPay"; import IRespPayLnAddress from "../types/IRespPayLnAddress"; +import { IReqPayLnAddress } from "../types/IReqPayLnAddress"; class HttpServer { // Create a new express application instance diff --git a/src/lib/LnurlPay.ts b/src/lib/LnurlPay.ts index ed0dde3..1afff49 100644 --- a/src/lib/LnurlPay.ts +++ b/src/lib/LnurlPay.ts @@ -23,6 +23,7 @@ import { LnAddress } from "./LnAddress"; import IRespPayLnAddress from "../types/IRespPayLnAddress"; import IRespLnServicePayRequest from "../types/IRespLnServicePayRequest"; import IRespLnurlPayRequest from "../types/IRespLnurlPayRequest"; +import { IReqPayLnAddress } from "../types/IReqPayLnAddress"; class LnurlPay { private _lnurlConfig: LnurlConfig; @@ -54,8 +55,8 @@ class LnurlPay { this._lnurlConfig.LN_SERVICE_DOMAIN + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && this._lnurlConfig.LN_SERVICE_PORT === 443) || - (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && - this._lnurlConfig.LN_SERVICE_PORT === 80) + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && + this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + @@ -731,6 +732,7 @@ class LnurlPay { if (bolt11) { const lnPayParams = { bolt11, + // eslint-disable-next-line @typescript-eslint/camelcase expected_msatoshi: req.amountMsat, }; diff --git a/src/types/IReqPayLnAddress.ts b/src/types/IReqPayLnAddress.ts index 30bb3d5..cb4885c 100644 --- a/src/types/IReqPayLnAddress.ts +++ b/src/types/IReqPayLnAddress.ts @@ -1,4 +1,4 @@ -interface IReqPayLnAddress { +export interface IReqPayLnAddress { address: string; amountMsat: number; } From 942e78b24819c0f9711fa9fc7500efc02967f979 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 3 Dec 2024 00:11:44 +0000 Subject: [PATCH 48/52] Expired Voucher callbacks --- .eslintrc.json | 19 ++-- doc/LNURL-Withdraw.md | 10 ++ .../20241202231421_expired/migration.sql | 10 ++ prisma/schema.prisma | 6 +- src/lib/LnurlDBPrisma.ts | 44 +++++---- src/lib/LnurlWithdraw.ts | 46 +++++++-- tests/ln_setup.sh | 11 ++- tests/test-lnurl-withdraw.sh | 98 ++++++++++++++----- 8 files changed, 182 insertions(+), 62 deletions(-) create mode 100644 prisma/migrations/20241202231421_expired/migration.sql diff --git a/.eslintrc.json b/.eslintrc.json index 76b7e67..3034029 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,21 +1,26 @@ { "root": true, "parser": "@typescript-eslint/parser", - "parserOptions": { - }, + "parserOptions": {}, "plugins": [ "@typescript-eslint", "prettier" ], "rules": { - "prettier/prettier": "error", - "@typescript-eslint/interface-name-prefix": ["off"] + "prettier/prettier": [ + "error", + { + "printWidth": 80 + } + ], + "@typescript-eslint/interface-name-prefix": [ + "off" + ] }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "prettier/@typescript-eslint" + "plugin:prettier/recommended" ] -} +} \ No newline at end of file diff --git a/doc/LNURL-Withdraw.md b/doc/LNURL-Withdraw.md index fc222a8..3ca42dd 100644 --- a/doc/LNURL-Withdraw.md +++ b/doc/LNURL-Withdraw.md @@ -377,3 +377,13 @@ Response: "updatedTs": "2024-11-15T19:47:36.217Z" } ``` + +- LNURL Withdraw voucher expired + +```json +{ + "action": "lnurlWithdrawExpired", + "lnurlWithdrawId": 253, + "expiresAt": "2024-11-15T19:47:36.217Z", +} +``` diff --git a/prisma/migrations/20241202231421_expired/migration.sql b/prisma/migrations/20241202231421_expired/migration.sql new file mode 100644 index 0000000..36d2cee --- /dev/null +++ b/prisma/migrations/20241202231421_expired/migration.sql @@ -0,0 +1,10 @@ + +-- We want a default value of false +ALTER TABLE "LnurlWithdrawEntity" ADD COLUMN "expiredCalledback" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "LnurlWithdrawEntity" ADD COLUMN "expiredCalledbackTs" DATETIME; + +-- but we want the existing rows to be set to true so we won't callback all of the existing ones +UPDATE "LnurlWithdrawEntity" SET "expiredCalledback" = true; + +CREATE INDEX "LnurlWithdrawEntity_deleted_paid_expiresAt_fallbackDone_btcFallbackAddress_idx" ON "LnurlWithdrawEntity"("deleted", "paid", "expiresAt", "fallbackDone", "btcFallbackAddress"); +CREATE INDEX "LnurlWithdrawEntity_deleted_webhookUrl_withdrawnDetails_paid_batchRequestId_paidCalledback_batchedCalledback_idx" ON "LnurlWithdrawEntity"("deleted", "webhookUrl", "withdrawnDetails", "paid", "batchRequestId", "paidCalledback", "batchedCalledback"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b87d750..63e4334 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,6 +23,8 @@ model LnurlWithdrawEntity { paidCalledbackTs DateTime? batchedCalledback Boolean @default(false) batchedCalledbackTs DateTime? + expiredCalledback Boolean @default(false) + expiredCalledbackTs DateTime? lnurl String bolt11 String? btcFallbackAddress String? @@ -39,6 +41,8 @@ model LnurlWithdrawEntity { @@index([externalId]) @@index([bolt11]) @@index([btcFallbackAddress]) + @@index([deleted, paid, expiresAt, fallbackDone, btcFallbackAddress]) + @@index([deleted, webhookUrl, withdrawnDetails, paid, batchRequestId, paidCalledback, batchedCalledback]) } model LnurlPayEntity { @@ -46,7 +50,7 @@ model LnurlPayEntity { externalId String @unique minMsatoshi Int @default(1) maxMsatoshi Int @default(1) - description String? + description String? webhookUrl String? lnurl String requests LnurlPayRequestEntity[] diff --git a/src/lib/LnurlDBPrisma.ts b/src/lib/LnurlDBPrisma.ts index ff47ff4..c147da0 100644 --- a/src/lib/LnurlDBPrisma.ts +++ b/src/lib/LnurlDBPrisma.ts @@ -99,39 +99,43 @@ class LnurlDBPrisma { // If there's a lnurlWithdrawId as arg, let's add it to the where clause! - let lws; - let whereClause = { + // We want to get all the lnurlWithdraws that: + // - are not deleted, and + // - have a webhookUrl set + // and are either: + // - Paid but paidCalledback is false, or + // - Batched (batchRequestId is not null) but batchedCalledback is false, or + // - Expired (expiresAt is less then current Date) but expiredCalledback is false + // + // If a lnurlWithdrawId is provided, we want to get that specific one but with the same conditions. + + const lws = await this._db?.lnurlWithdrawEntity.findMany({ where: { + lnurlWithdrawId, deleted: false, webhookUrl: { not: null }, - withdrawnDetails: { not: null }, - // withdrawnTs: { not: null }, AND: [ - { OR: [{ paid: true }, { batchRequestId: { not: null } }] }, { OR: [ - { paidCalledback: false }, { - AND: [ - { batchedCalledback: false }, - { batchRequestId: { not: null } }, - ], + paid: true, + paidCalledback: false, + }, + { + batchRequestId: { not: null }, + batchedCalledback: false, + }, + { + expiresAt: { lt: new Date() }, + expiredCalledback: false, }, ], }, ], }, - } - - if (lnurlWithdrawId) { - whereClause.where = Object.assign(whereClause.where, { lnurlWithdrawId }); - } - - // logger.debug("LnurlDBPrisma.getNonCalledbackLnurlWithdraws, whereClause=", whereClause); - - lws = await this._db?.lnurlWithdrawEntity.findMany(whereClause); + }); - return lws as LnurlWithdrawEntity[]; + return lws || []; } async getFallbackLnurlWithdraws(): Promise { diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 8faf10f..9a71691 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -841,9 +841,7 @@ class LnurlWithdraw { if ( !lnurlWithdrawEntity.deleted && lnurlWithdrawEntity.webhookUrl && - lnurlWithdrawEntity.webhookUrl.length > 0 && - lnurlWithdrawEntity.withdrawnDetails && - lnurlWithdrawEntity.withdrawnDetails.length > 0 + lnurlWithdrawEntity.webhookUrl.length > 0 ) { if ( !lnurlWithdrawEntity.batchedCalledback && @@ -927,6 +925,38 @@ class LnurlWithdraw { await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); } } + + if ( + !lnurlWithdrawEntity.expiredCalledback && + lnurlWithdrawEntity.expiresAt && + lnurlWithdrawEntity.expiresAt < new Date() + ) { + // LNURL Withdraw voucher has expired + postdata = { + action: "lnurlWithdrawExpired", + lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, + expiresAt: lnurlWithdrawEntity.expiresAt, + }; + logger.debug( + "LnurlWithdraw.processCallbacks, expired, postdata=", + postdata + ); + + response = await Utils.post( + lnurlWithdrawEntity.webhookUrl, + postdata + ); + + if (response.status >= 200 && response.status < 400) { + logger.debug( + "LnurlWithdraw.processCallbacks, expired, webhook called back" + ); + + lnurlWithdrawEntity.expiredCalledback = true; + lnurlWithdrawEntity.expiredCalledbackTs = new Date(); + await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); + } + } } }); } @@ -1040,7 +1070,7 @@ class LnurlWithdraw { ); if (lnurlWithdrawEntity.batchRequestId) { - this.processCallbacks(lnurlWithdrawEntity); + this.checkWebhook(lnurlWithdrawEntity); } } } else { @@ -1084,7 +1114,7 @@ class LnurlWithdraw { ); if (lnurlWithdrawEntity.fallbackDone) { - this.processCallbacks(lnurlWithdrawEntity); + this.checkWebhook(lnurlWithdrawEntity); } } } @@ -1236,7 +1266,9 @@ class LnurlWithdraw { return result; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + /** + * This is called by Batcher when a batch containing a fallback payment has been executed. + */ async processBatchWebhook(webhookBody: any): Promise { logger.info("LnurlWithdraw.processBatchWebhook,", webhookBody); @@ -1285,7 +1317,7 @@ class LnurlWithdraw { lnurlWithdrawEntity ); - this.processCallbacks(lnurlWithdrawEntity); + this.checkWebhook(lnurlWithdrawEntity); return result; } diff --git a/tests/ln_setup.sh b/tests/ln_setup.sh index 527cd5e..9c7c2eb 100755 --- a/tests/ln_setup.sh +++ b/tests/ln_setup.sh @@ -20,10 +20,13 @@ echo ; echo "address=${address}" data='{"address":"'${address}'","amount":1}' docker exec -it `docker ps -q -f "name=proxy\."` curl -d "${data}" localhost:8888/spend -mine 6 - -echo ; echo "Sleeping 5 seconds..." -sleep 5 +echo ; echo "Mining blocks..." +mine 1 +sleep 2 +mine 1 +sleep 2 +mine 1 +sleep 2 # Create a channel between the two nodes data='{"peer":"'${connectstring2}'","msatoshi":'${channelmsats}'}' diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index e4708f3..5e879e3 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -16,7 +16,8 @@ # 1. Create a LNURL Withdraw with expiration=now # 2. Get it and compare -# 3. User calls LNServiceWithdrawRequest -> Error, expired! +# 3. Expired webhook is called +# 4. User calls LNServiceWithdrawRequest -> Error, expired! # Expired 2 @@ -24,7 +25,8 @@ # 2. Get it and compare # 3. User calls LNServiceWithdrawRequest # 4. Sleep 5 seconds -# 5. User calls LNServiceWithdraw -> Error, expired! +# 5. Expired webhook is called +# 6. User calls LNServiceWithdraw -> Error, expired! # Deleted 1 @@ -49,9 +51,10 @@ # 3. Listen to watch webhook # 4. Create a LNURL Withdraw with expiration=now and a btcfallbackaddr # 5. Get it and compare -# 6. User calls LNServiceWithdrawRequest -> Error, expired! -# 7. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) -# 8. Mined block and Cyphernode's confirmed watch callback called (port 1113) +# 6. Expired webhook is called back +# 7. User calls LNServiceWithdrawRequest -> Error, expired! +# 8. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) +# 9. Mined block and Cyphernode's confirmed watch callback called (port 1113) # fallback 2, use of Bitcoin fallback address in a batched spend @@ -60,10 +63,11 @@ # 3. Listen to watch webhook # 4. Create a LNURL Withdraw with expiration=now and a btcfallbackaddr # 5. Get it and compare -# 6. User calls LNServiceWithdrawRequest -> Error, expired! -# 7. Fallback should be triggered, added to current batch using the Batcher -# 8. Wait for the batch to execute, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112), Batcher's execute callback called (port 1113) -# 9. Mined block and Cyphernode's confirmed watch callback called (port 1114) +# 6. Expired webhook is called back +# 7. User calls LNServiceWithdrawRequest -> Error, expired! +# 8. Fallback should be triggered, added to current batch using the Batcher +# 9. Wait for the batch to execute, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112), Batcher's execute callback called (port 1113) +# 10. Mined block and Cyphernode's confirmed watch callback called (port 1114) # fallback 3, force fallback @@ -72,21 +76,23 @@ # 3. Listen to watch webhook # 4. Create a LNURL Withdraw with expiration=tomorrow and a btcfallbackaddr # 5. Get it and compare -# 6. User calls LNServiceWithdrawRequest -> works, not expired! -# 7. Call forceFallback -# 8. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) -# 9. Mined block and Cyphernode's confirmed watch callback called (port 1113) +# 6. Expired webhook is called back +# 7. User calls LNServiceWithdrawRequest -> works, not expired! +# 8. Call forceFallback +# 9. Fallback should be triggered, LNURL callback called (port 1111), Cyphernode's watch callback called (port 1112) +# 10. Mined block and Cyphernode's confirmed watch callback called (port 1113) # fallback 4, execute fallback on a bolt11 paid in background (lnurl app doesn't know it's been paid) # 1. Cyphernode.getnewaddress -> btcfallbackaddr # 2. Create a LNURL Withdraw with expiration=tomorrow and a btcfallbackaddr # 3. Get it and compare -# 4. User calls LNServiceWithdrawRequest -> works, not expired! -# 5. Shut down LN02 -# 6. User calls LNServiceWithdraw -> fails because LN02 is down -# 7. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it -# 8. Call forceFallback -> should check payment status and say it's already paid! +# 4. Expired webhook is called back +# 5. User calls LNServiceWithdrawRequest -> works, not expired! +# 6. Shut down LN02 +# 7. User calls LNServiceWithdraw -> fails because LN02 is down +# 8. Call ln_pay directly on Cyphernode so that LNURLapp doesn't know about it +# 9. Call forceFallback -> should check payment status and say it's already paid! . ./colors.sh @@ -362,6 +368,7 @@ happy_path() { local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[happy_path] withdrawResponse=${withdrawResponse}" + # Wait for the LNURL Payment callback wait # We want to see if payment received (invoice status paid) @@ -475,6 +482,9 @@ wrong_bolt11() { return 1 fi + # Wait for the "Failed claim attempt" callback + wait + # We want to see if payment received (invoice status paid) status=$(get_invoice_status "${invoice}") trace 3 "[wrong_bolt11] status=${status}" @@ -541,12 +551,16 @@ expired1() { # # 1. Create a LNURL Withdraw with expiration=now # 2. Get it and compare - # 3. User calls LNServiceWithdrawRequest -> Error, expired! + # 3. Expired webhook is called back + # 4. User calls LNServiceWithdrawRequest -> Error, expired! trace 1 "\n\n[expired1] ${On_Yellow}${BBlack} Expired 1: ${Color_Off}\n" local callbackurl=${1} + # "Expired voucher" callback + start_callback_server + # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0) trace 3 "[expired1] createLnurlWithdraw=${createLnurlWithdraw}" @@ -569,6 +583,9 @@ expired1() { local serviceUrl=$(decode_lnurl "${lnurl}") trace 3 "[expired1] serviceUrl=${serviceUrl}" + # Wait for the "Expired voucher" callback + wait + # User calls LN Service LNURL Withdraw Request local withdrawRequestResponse=$(call_lnservice_withdraw_request "${serviceUrl}") trace 3 "[expired1] withdrawRequestResponse=${withdrawRequestResponse}" @@ -595,6 +612,9 @@ expired2() { local callbackurl=${1} + # "Expired voucher" callback + start_callback_server + # Service creates LNURL Withdraw local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 10) trace 3 "[expired2] createLnurlWithdraw=${createLnurlWithdraw}" @@ -632,6 +652,9 @@ expired2() { trace 3 "[expired2] Sleeping 10 seconds..." sleep 10 + # Wait for the "Expired voucher" callback + wait + # User calls LN Service LNURL Withdraw local withdrawResponse=$(call_lnservice_withdraw "${withdrawRequestResponse}" "${bolt11}") trace 3 "[expired2] withdrawResponse=${withdrawResponse}" @@ -803,9 +826,6 @@ fallback1() { trace 1 "\n\n[fallback1] ${On_Yellow}${BBlack} Fallback 1: ${Color_Off}\n" - local callbackserver=${1} - local callbackport=${2} - local zeroconfport=$((${callbackserverport}+1)) local oneconfport=$((${callbackserverport}+2)) local callbackurlCnWatch0conf="http://${callbackservername}2:${zeroconfport}" @@ -846,7 +866,10 @@ fallback1() { local serviceUrl=$(decode_lnurl "${lnurl}") trace 3 "[fallback1] serviceUrl=${serviceUrl}" + # fallback (or expired) callback server start_callback_server + + # 0-conf callback server start_callback_server ${zeroconfport} 2 trace 2 "\n\n[fallback1] ${BPurple}Waiting for fallback execution, fallback callback and the 0-conf callback...${Color_Off}\n" @@ -863,13 +886,20 @@ fallback1() { trace 2 "[fallback1] EXPIRED! Good!" fi + # Wait for the fallback (or expired) and 0-conf callbacks wait + # expired voucher (or fallback) callback server + start_callback_server + + # 1-conf callback server start_callback_server ${oneconfport} 3 trace 2 "\n\n[fallback1] ${BPurple}Waiting for the 1-conf callback...${Color_Off}\n" mine + + # Wait for the 1-conf and expired (or fallback) callbacks wait trace 1 "\n\n[fallback1] ${On_IGreen}${BBlack} Fallback 1: SUCCESS! ${Color_Off}\n" @@ -933,7 +963,7 @@ fallback2() { local serviceUrl=$(decode_lnurl "${lnurl}") trace 3 "[fallback2] serviceUrl=${serviceUrl}" - # fallback batched callback + # fallback batched (or voucher expired) callback start_callback_server trace 2 "\n\n[fallback2] ${BPurple}Waiting for fallback batched callback...\n${Color_Off}" @@ -950,8 +980,12 @@ fallback2() { trace 2 "[fallback2] EXPIRED!" fi + # Wait for the fallback batched (or voucher expired) callback wait + # voucher expired (or fallback batched) callback + start_callback_server + # fallback paid callback start_callback_server # 0-conf callback @@ -959,6 +993,7 @@ fallback2() { trace 2 "\n\n[fallback2] ${BPurple}Waiting for fallback execution and the 0-conf callback...\n${Color_Off}" + # Wait for the fallback paid, 0-conf callbacks and voucher expired (or fallback batched) callbacks wait # 1-conf callback @@ -967,6 +1002,8 @@ fallback2() { trace 2 "\n\n[fallback2] ${BPurple}Waiting for the 1-conf callback...\n${Color_Off}" mine + + # Wait for the 1-conf callback wait trace 1 "\n\n[fallback2] ${On_IGreen}${BBlack} Fallback 2: SUCCESS! ${Color_Off}\n" @@ -1030,7 +1067,10 @@ fallback3() { local serviceUrl=$(decode_lnurl "${lnurl}") trace 3 "[fallback3] serviceUrl=${serviceUrl}" + # Fallback (or voucher expired) callback server start_callback_server + + # 0-conf callback server start_callback_server ${zeroconfport} 2 # User calls LN Service LNURL Withdraw Request @@ -1056,13 +1096,20 @@ fallback3() { fi trace 3 "[fallback3] force_lnurl_fallback=${force_lnurl_fallback}" + # Wait for the fallback (or voucher expired) and 0-conf callbacks to happen wait + # voucher expired (or Fallback) callback server + start_callback_server + + # 1-conf callback server start_callback_server ${oneconfport} 3 trace 2 "\n\n[fallback3] ${BPurple}Waiting for the 1-conf callback...\n${Color_Off}" mine + + # Wait for the 1-conf callback and voucher expired (or fallback) callbacks wait trace 1 "\n\n[fallback3] ${On_IGreen}${BBlack} Fallback 3: SUCCESS! ${Color_Off}\n" @@ -1160,6 +1207,9 @@ fallback4() { return 1 fi + # Wait for the "Failed claim attempt" callback + wait + # Reconnecting the two LN instances... ln_reconnect @@ -1171,6 +1221,7 @@ fallback4() { lnpaystatus=$(echo "${lnpay}" | jq -r ".status") trace 3 "[fallback4] lnpaystatus=${lnpaystatus}" + # Fallback callback server start_callback_server trace 2 "\n\n[fallback4] ${BPurple}Waiting for fallback execution callback...\n${Color_Off}" @@ -1196,6 +1247,7 @@ fallback4() { return 1 fi + # Wait for the fallback callback to happen wait } From 279e5144a3556e6bd361498446927aa9a0032a1f Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 3 Dec 2024 17:03:06 +0000 Subject: [PATCH 49/52] Validate fallback Bitcoin address when creating LNURL-W --- src/lib/CyphernodeClient.ts | 64 ++++++++++++++++++++ src/lib/LnurlWithdraw.ts | 20 ++++++ src/types/cyphernode/IRespValidateAddress.ts | 6 ++ tests/test-lnurl-withdraw.sh | 49 ++++++++++----- 4 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 src/types/cyphernode/IRespValidateAddress.ts diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 54c91f5..21ced2b 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -22,6 +22,7 @@ import IRespLnPayStatus from "../types/cyphernode/IRespLnPayStatus"; import IReqLnCreate from "../types/cyphernode/IReqLnCreate"; import IRespLnCreate from "../types/cyphernode/IRespLnCreate"; import IRespLnDecodeBolt11 from "../types/cyphernode/IRespLnDecodeBolt11"; +import IRespValidateAddress from "../types/cyphernode/IRespValidateAddress"; class CyphernodeClient { private baseURL: string; @@ -861,6 +862,69 @@ class CyphernodeClient { } return result; } + + async validateAddress(address: string): Promise { + // GET http://192.168.111.152:8080/validateaddress/tb1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqp3mvzv + + // args: + // - address, required, Bitcoin address to validate + // + // Example of valid address result: + // + // { + // "result": { + // "isvalid": true, + // "address": "bc1p8k4v4xuz55dv49svzjg43qjxq2whur7ync9tm0xgl5t4wjl9ca9snxgmlt", + // "scriptPubKey": "51203daaca9b82a51aca960c1491588246029d7e0fc49e0abdbcc8fd17574be5c74b", + // "isscript": true, + // "iswitness": true, + // "witness_version": 1, + // "witness_program": "3daaca9b82a51aca960c1491588246029d7e0fc49e0abdbcc8fd17574be5c74b" + // }, + // "error": null, + // "id": null + // } + // + // Example of invalid address result: + // + // { + // "result": { + // "isvalid": false, + // "error_locations": [], + // "error": "Not a valid Bech32 or Base58 encoding" + // }, + // "error": null, + // "id": null + // } + // + // Example of failed result: + // + // { + // "result": null, + // "error": { + // "code": -1, + // "message": "validateaddress \"address\"\n\nReturn information about the given bitcoin address.\n\nArguments:\n1. address (string, required) The bitcoin address to validate\n\nResult:\n{ (json object)\n \"isvalid\" : true|false, (boolean) If the address is valid or not\n \"address\" : \"str\", (string, optional) The bitcoin address validated\n \"scriptPubKey\" : \"hex\", (string, optional) The hex-encoded scriptPubKey generated by the address\n \"isscript\" : true|false, (boolean, optional) If the key is a script\n \"iswitness\" : true|false, (boolean, optional) If the address is a witness address\n \"witness_version\" : n, (numeric, optional) The version number of the witness program\n \"witness_program\" : \"hex\", (string, optional) The hex value of the witness program\n \"error\" : \"str\", (string, optional) Error message, if any\n \"error_locations\" : [ (json array, optional) Indices of likely error locations in address, if known (e.g. Bech32 errors)\n n, (numeric) index of a potential error\n ...\n ]\n}\n\nExamples:\n> bitcoin-cli validateaddress \"bc1q09vm5lfy0j5reeulh4x5752q25uqqvz34hufdl\"\n> curl --user myusername --data-binary '{\"jsonrpc\": \"1.0\", \"id\": \"curltest\", \"method\": \"validateaddress\", \"params\": [\"bc1q09vm5lfy0j5reeulh4x5752q25uqqvz34hufdl\"]}' -H 'content-type: text/plain;' http://127.0.0.1:8332/\n" + // }, + // "id": null + // } + + logger.info("CyphernodeClient.validateAddress:", address); + + let result: IRespValidateAddress; + const response = await this._get("/validateaddress/" + address); + if (response.status >= 200 && response.status < 400) { + result = { result: response.data.result }; + } else { + result = { + error: { + code: ErrorCodes.InvalidRequest, + message: response.data.error, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as IResponseError, + } as IRespValidateAddress; + } + return result; + } } export { CyphernodeClient }; diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 8faf10f..b9f3e95 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -89,6 +89,26 @@ class LnurlWithdraw { const response: IRespLnurlWithdraw = {}; if (CreateLnurlWithdrawValidator.validateRequest(reqCreateLnurlWithdraw)) { + if (reqCreateLnurlWithdraw.btcFallbackAddress) { + const validateAddressResponse = await this._cyphernodeClient.validateAddress( + reqCreateLnurlWithdraw.btcFallbackAddress + ); + + if (!validateAddressResponse.result?.isvalid) { + // There is an error with inputs + logger.debug( + "LnurlWithdraw.createLnurlWithdraw, invalid fallback Bitcoin address." + ); + + response.error = { + code: ErrorCodes.InvalidRequest, + message: "Invalid fallback Bitcoin address", + }; + + return response; + } + } + // Inputs are valid. logger.debug("LnurlWithdraw.createLnurlWithdraw, Inputs are valid."); diff --git a/src/types/cyphernode/IRespValidateAddress.ts b/src/types/cyphernode/IRespValidateAddress.ts new file mode 100644 index 0000000..1d0569a --- /dev/null +++ b/src/types/cyphernode/IRespValidateAddress.ts @@ -0,0 +1,6 @@ +import { IResponseError } from "../jsonrpc/IResponseMessage"; + +export default interface IRespValidateAddress { + result?: { isvalid: boolean }; + error?: IResponseError; +} diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index e4708f3..60f470f 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -382,19 +382,36 @@ happy_path() { wrong_bolt11() { # wrong_bolt11: # - # 1. Create a LNURL Withdraw - # 2. Get it and compare - # 3. User calls LNServiceWithdrawRequest - # 4. User calls LNServiceWithdraw with an invalid bolt11 - # 5. User calls LNServiceWithdraw with wrong amount in bolt11 - # 6. User calls LNServiceWithdraw with wrong description in bolt11 and it should work! + # 1. Create a LNURL Withdraw with an invalid fallback Bitcoin address + # 2. Create a LNURL Withdraw + # 3. Get it and compare + # 4. User calls LNServiceWithdrawRequest + # 5. User calls LNServiceWithdraw with an invalid bolt11 + # 6. User calls LNServiceWithdraw with wrong amount in bolt11 + # 7. User calls LNServiceWithdraw with wrong description in bolt11 and it should work! trace 1 "\n\n[wrong_bolt11] ${On_Yellow}${BBlack} wrong_bolt11: ${Color_Off}\n" local callbackurl=${1} + # Service creates LNURL Withdraw with an invalid fallback Bitcoin address + local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 0 "" "allototo") + trace 3 "[wrong_bolt11] createLnurlWithdraw=${createLnurlWithdraw}" + + local error=$(echo ${createLnurlWithdraw} | jq -r ".error") + trace 3 "[wrong_bolt11] error=${error}" + + if [ -n "${error}" ]; then + trace 1 "\n\n[wrong_bolt11] ${On_IGreen}${BBlack} invalid address error: SUCCESS! ${Color_Off}\n" + date + else + trace 1 "\n\n[wrong_bolt11] ${On_Red}${BBlack} invalid address error: FAILURE! ${Color_Off}\n" + date + return 1 + fi + # Service creates LNURL Withdraw - local createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 15) + createLnurlWithdraw=$(create_lnurl_withdraw "${callbackurl}" 15) trace 3 "[wrong_bolt11] createLnurlWithdraw=${createLnurlWithdraw}" local lnurl=$(echo "${createLnurlWithdraw}" | jq -r ".result.lnurl") trace 3 "lnurl=${lnurl}" @@ -1228,16 +1245,16 @@ exec_in_test_container_leave_lf apk add --update curl ln_reconnect -happy_path "${callbackurl}" && \ +# happy_path "${callbackurl}" && \ wrong_bolt11 "${callbackurl}" && \ -expired1 "${callbackurl}" && \ -expired2 "${callbackurl}" && \ -deleted1 "${callbackurl}" && \ -deleted2 "${callbackurl}" && \ -fallback1 "${callbackservername}" "${callbackserverport}" && \ -fallback2 "${callbackservername}" "${callbackserverport}" && \ -fallback3 "${callbackservername}" "${callbackserverport}" && \ -fallback4 "${callbackservername}" "${callbackserverport}" && \ +# expired1 "${callbackurl}" && \ +# expired2 "${callbackurl}" && \ +# deleted1 "${callbackurl}" && \ +# deleted2 "${callbackurl}" && \ +# fallback1 "${callbackservername}" "${callbackserverport}" && \ +# fallback2 "${callbackservername}" "${callbackserverport}" && \ +# fallback3 "${callbackservername}" "${callbackserverport}" && \ +# fallback4 "${callbackservername}" "${callbackserverport}" && \ trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}All tests passed!${Color_Off}\n" trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}Tearing down...${Color_Off}\n" From d071d19c0d43d31e14f7b8a52c8ec99f46f2958a Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 3 Dec 2024 17:27:41 +0000 Subject: [PATCH 50/52] tt --- tests/test-lnurl-withdraw.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test-lnurl-withdraw.sh b/tests/test-lnurl-withdraw.sh index 60f470f..7f499b3 100755 --- a/tests/test-lnurl-withdraw.sh +++ b/tests/test-lnurl-withdraw.sh @@ -1245,16 +1245,16 @@ exec_in_test_container_leave_lf apk add --update curl ln_reconnect -# happy_path "${callbackurl}" && \ +happy_path "${callbackurl}" && \ wrong_bolt11 "${callbackurl}" && \ -# expired1 "${callbackurl}" && \ -# expired2 "${callbackurl}" && \ -# deleted1 "${callbackurl}" && \ -# deleted2 "${callbackurl}" && \ -# fallback1 "${callbackservername}" "${callbackserverport}" && \ -# fallback2 "${callbackservername}" "${callbackserverport}" && \ -# fallback3 "${callbackservername}" "${callbackserverport}" && \ -# fallback4 "${callbackservername}" "${callbackserverport}" && \ +expired1 "${callbackurl}" && \ +expired2 "${callbackurl}" && \ +deleted1 "${callbackurl}" && \ +deleted2 "${callbackurl}" && \ +fallback1 "${callbackservername}" "${callbackserverport}" && \ +fallback2 "${callbackservername}" "${callbackserverport}" && \ +fallback3 "${callbackservername}" "${callbackserverport}" && \ +fallback4 "${callbackservername}" "${callbackserverport}" && \ trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}All tests passed!${Color_Off}\n" trace 1 "\n\n[test-lnurl-withdraw] ${BCyan}Tearing down...${Color_Off}\n" From e540107b68fe20d011e03c7b66acdfe5ec72127a Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 10 Dec 2024 21:32:28 +0000 Subject: [PATCH 51/52] Just reformating code with 132 char width --- .eslintrc.json | 2 +- src/lib/BatcherClient.ts | 42 +- src/lib/CyphernodeClient.ts | 69 +-- src/lib/HttpServer.ts | 156 ++---- src/lib/LnAddress.ts | 5 +- src/lib/LnurlDBPrisma.ts | 57 +-- src/lib/LnurlPay.ts | 226 ++------- src/lib/LnurlWithdraw.ts | 562 +++++---------------- src/lib/Scheduler.ts | 20 +- src/lib/Utils.ts | 38 +- src/types/IRespLnServiceWithdrawRequest.ts | 3 +- src/types/cyphernode/IAddToBatchResult.ts | 5 +- src/validators/CreateLnurlPayValidator.ts | 7 +- src/validators/LnServicePayValidator.ts | 10 +- src/validators/UpdateLnurlPayValidator.ts | 5 +- 15 files changed, 284 insertions(+), 923 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 3034029..172ebe7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,7 +10,7 @@ "prettier/prettier": [ "error", { - "printWidth": 80 + "printWidth": 132 } ], "@typescript-eslint/interface-name-prefix": [ diff --git a/src/lib/BatcherClient.ts b/src/lib/BatcherClient.ts index f489fcc..b0f83fd 100644 --- a/src/lib/BatcherClient.ts +++ b/src/lib/BatcherClient.ts @@ -53,18 +53,9 @@ class BatcherClient { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx - logger.info( - "BatcherClient._post :: error.response.data:", - error.response.data - ); - logger.info( - "BatcherClient._post :: error.response.status:", - error.response.status - ); - logger.info( - "BatcherClient._post :: error.response.headers:", - error.response.headers - ); + logger.info("BatcherClient._post :: error.response.data:", error.response.data); + logger.info("BatcherClient._post :: error.response.status:", error.response.status); + logger.info("BatcherClient._post :: error.response.headers:", error.response.headers); return { status: error.response.status, data: error.response.data }; } else if (error.request) { @@ -120,18 +111,9 @@ class BatcherClient { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx - logger.info( - "BatcherClient._get :: error.response.data:", - error.response.data - ); - logger.info( - "BatcherClient._get :: error.response.status:", - error.response.status - ); - logger.info( - "BatcherClient._get :: error.response.headers:", - error.response.headers - ); + logger.info("BatcherClient._get :: error.response.data:", error.response.data); + logger.info("BatcherClient._get :: error.response.status:", error.response.status); + logger.info("BatcherClient._get :: error.response.headers:", error.response.headers); return { status: error.response.status, data: error.response.data }; } else if (error.request) { @@ -160,9 +142,7 @@ class BatcherClient { } } - async queueForNextBatch( - batchRequestTO: IReqBatchRequest - ): Promise { + async queueForNextBatch(batchRequestTO: IReqBatchRequest): Promise { // { // batcherId?: number; // batcherLabel?: string; @@ -194,12 +174,8 @@ class BatcherClient { } else { result = { error: { - code: response.data.error - ? response.data.error.code - : response.data.code, - message: response.data.error - ? response.data.error.message - : response.data.message, + code: response.data.error ? response.data.error.code : response.data.code, + message: response.data.error ? response.data.error.message : response.data.message, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as IResponseError, } as IRespBatchRequest; diff --git a/src/lib/CyphernodeClient.ts b/src/lib/CyphernodeClient.ts index 21ced2b..c3c285a 100644 --- a/src/lib/CyphernodeClient.ts +++ b/src/lib/CyphernodeClient.ts @@ -53,11 +53,7 @@ class CyphernodeClient { const p = '{"id":"' + this.apiId + '","exp":' + current + "}"; const re1 = /\+/g; const re2 = /\//g; - const p64 = Buffer.from(p) - .toString("base64") - .replace(re1, "-") - .replace(re2, "_") - .split("=")[0]; + const p64 = Buffer.from(p).toString("base64").replace(re1, "-").replace(re2, "_").split("=")[0]; const msg = this.h64 + "." + p64; const s = crypto .createHmac("sha256", this.apiKey) @@ -108,14 +104,8 @@ class CyphernodeClient { const response = await axios.request(configs); // logger.debug("CyphernodeClient._post :: response:", response); // response.data used to be a string, looks like it's now an object... taking no chance. - const str = - typeof response.data === "string" - ? response.data - : JSON.stringify(response.data); - logger.debug( - "CyphernodeClient._post :: response.data:", - str.substring(0, 1000) - ); + const str = typeof response.data === "string" ? response.data : JSON.stringify(response.data); + logger.debug("CyphernodeClient._post :: response.data:", str.substring(0, 1000)); return { status: response.status, data: response.data }; } catch (err) { @@ -127,28 +117,16 @@ class CyphernodeClient { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx - logger.info( - "CyphernodeClient._post :: error.response.data:", - error.response.data - ); - logger.info( - "CyphernodeClient._post :: error.response.status:", - error.response.status - ); - logger.info( - "CyphernodeClient._post :: error.response.headers:", - error.response.headers - ); + logger.info("CyphernodeClient._post :: error.response.data:", error.response.data); + logger.info("CyphernodeClient._post :: error.response.status:", error.response.status); + logger.info("CyphernodeClient._post :: error.response.headers:", error.response.headers); return { status: error.response.status, data: error.response.data }; } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - logger.info( - "CyphernodeClient._post :: error.message:", - error.message - ); + logger.info("CyphernodeClient._post :: error.message:", error.message); return { status: -1, data: error.message }; } else { @@ -188,14 +166,8 @@ class CyphernodeClient { try { const response = await axios.request(configs); // response.data used to be a string, looks like it's now an object... taking no chance. - const str = - typeof response.data === "string" - ? response.data - : JSON.stringify(response.data); - logger.debug( - "CyphernodeClient._get :: response.data:", - str.substring(0, 1000) - ); + const str = typeof response.data === "string" ? response.data : JSON.stringify(response.data); + logger.debug("CyphernodeClient._get :: response.data:", str.substring(0, 1000)); return { status: response.status, data: response.data }; } catch (err) { @@ -205,18 +177,9 @@ class CyphernodeClient { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx - logger.info( - "CyphernodeClient._get :: error.response.data:", - error.response.data - ); - logger.info( - "CyphernodeClient._get :: error.response.status:", - error.response.status - ); - logger.info( - "CyphernodeClient._get :: error.response.headers:", - error.response.headers - ); + logger.info("CyphernodeClient._get :: error.response.data:", error.response.data); + logger.info("CyphernodeClient._get :: error.response.status:", error.response.status); + logger.info("CyphernodeClient._get :: error.response.headers:", error.response.headers); return { status: error.response.status, data: error.response.data }; } else if (error.request) { @@ -316,9 +279,7 @@ class CyphernodeClient { return result; } - async getBatchDetails( - batchIdent: IReqGetBatchDetails - ): Promise { + async getBatchDetails(batchIdent: IReqGetBatchDetails): Promise { // POST (GET) http://192.168.111.152:8080/getbatchdetails // // args: @@ -551,9 +512,7 @@ class CyphernodeClient { result = { error: { code: ErrorCodes.InternalError, - message: response.data.message - ? response.data.message - : JSON.stringify(response.data), + message: response.data.message ? response.data.message : JSON.stringify(response.data), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as IResponseError, } as IRespLnPay; diff --git a/src/lib/HttpServer.ts b/src/lib/HttpServer.ts index 8788a3f..215f127 100644 --- a/src/lib/HttpServer.ts +++ b/src/lib/HttpServer.ts @@ -5,10 +5,7 @@ import LnurlConfig from "../config/LnurlConfig"; import fs from "fs"; import { LnurlWithdraw } from "./LnurlWithdraw"; import { LnurlPay } from "./LnurlPay"; -import { - IResponseMessage, - ErrorCodes, -} from "../types/jsonrpc/IResponseMessage"; +import { IResponseMessage, ErrorCodes } from "../types/jsonrpc/IResponseMessage"; import { IRequestMessage } from "../types/jsonrpc/IRequestMessage"; import { Utils } from "./Utils"; import IRespCreateLnurlWithdraw from "../types/IRespLnurlWithdraw"; @@ -29,9 +26,7 @@ import { IReqPayLnAddress } from "../types/IReqPayLnAddress"; class HttpServer { // Create a new express application instance private readonly _httpServer: express.Application = express(); - private _lnurlConfig: LnurlConfig = JSON.parse( - fs.readFileSync("data/config.json", "utf8") - ); + private _lnurlConfig: LnurlConfig = JSON.parse(fs.readFileSync("data/config.json", "utf8")); private _lnurlWithdraw: LnurlWithdraw = new LnurlWithdraw(this._lnurlConfig); private _lnurlPay: LnurlPay = new LnurlPay(this._lnurlConfig); @@ -49,21 +44,15 @@ class HttpServer { this._lnurlPay.configureLnurl(this._lnurlConfig); } - async createLnurlWithdraw( - params: object | undefined - ): Promise { + async createLnurlWithdraw(params: object | undefined): Promise { logger.debug("/createLnurlWithdraw params:", params); const reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw = params as IReqCreateLnurlWithdraw; - return await this._lnurlWithdraw.createLnurlWithdraw( - reqCreateLnurlWithdraw - ); + return await this._lnurlWithdraw.createLnurlWithdraw(reqCreateLnurlWithdraw); } - async deleteLnurlWithdraw( - params: object | undefined - ): Promise { + async deleteLnurlWithdraw(params: object | undefined): Promise { logger.debug("/deleteLnurlWithdraw params:", params); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -72,9 +61,7 @@ class HttpServer { return await this._lnurlWithdraw.deleteLnurlWithdraw(lnurlWithdrawId); } - async forceFallback( - params: object | undefined - ): Promise { + async forceFallback(params: object | undefined): Promise { logger.debug("/forceFallback params:", params); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -83,9 +70,7 @@ class HttpServer { return await this._lnurlWithdraw.forceFallback(lnurlWithdrawId); } - async getLnurlWithdraw( - params: object | undefined - ): Promise { + async getLnurlWithdraw(params: object | undefined): Promise { logger.debug("/getLnurlWithdraw params:", params); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -94,9 +79,7 @@ class HttpServer { return await this._lnurlWithdraw.getLnurlWithdraw(lnurlWithdrawId); } - async createLnurlPay( - params: object | undefined - ): Promise { + async createLnurlPay(params: object | undefined): Promise { logger.debug("/createLnurlPay params:", params); const reqCreateLnurlPay: IReqCreateLnurlPay = params as IReqCreateLnurlPay; @@ -130,9 +113,7 @@ class HttpServer { return await this._lnurlPay.getLnurlPay(lnurlPayId); } - async deleteLnurlPayRequest( - params: object | undefined - ): Promise { + async deleteLnurlPayRequest(params: object | undefined): Promise { logger.debug("/deleteLnurlPayRequest params:", params); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -141,9 +122,7 @@ class HttpServer { return await this._lnurlPay.deleteLnurlPayRequest(lnurlPayRequestId); } - async getLnurlPayRequest( - params: object | undefined - ): Promise { + async getLnurlPayRequest(params: object | undefined): Promise { logger.debug("/getLnurlPayRequest params:", params); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -179,27 +158,21 @@ class HttpServer { // Check the method and call the corresponding function switch (reqMessage.method) { case "createLnurlWithdraw": { - const result: IRespCreateLnurlWithdraw = await this.createLnurlWithdraw( - reqMessage.params || {} - ); + const result: IRespCreateLnurlWithdraw = await this.createLnurlWithdraw(reqMessage.params || {}); response.result = result.result; response.error = result.error; break; } case "getLnurlWithdraw": { - const result: IRespCreateLnurlWithdraw = await this.getLnurlWithdraw( - reqMessage.params || {} - ); + const result: IRespCreateLnurlWithdraw = await this.getLnurlWithdraw(reqMessage.params || {}); response.result = result.result; response.error = result.error; break; } case "deleteLnurlWithdraw": { - const result: IRespCreateLnurlWithdraw = await this.deleteLnurlWithdraw( - reqMessage.params || {} - ); + const result: IRespCreateLnurlWithdraw = await this.deleteLnurlWithdraw(reqMessage.params || {}); response.result = result.result; response.error = result.error; @@ -207,36 +180,28 @@ class HttpServer { } case "createLnurlPay": { - const result: IRespCreateLnurlPay = await this.createLnurlPay( - reqMessage.params || {} - ); + const result: IRespCreateLnurlPay = await this.createLnurlPay(reqMessage.params || {}); response.result = result.result; response.error = result.error; break; } case "updateLnurlPay": { - const result: IRespLnurlPay = await this.updateLnurlPay( - reqMessage.params || {} - ); + const result: IRespLnurlPay = await this.updateLnurlPay(reqMessage.params || {}); response.result = result.result; response.error = result.error; break; } case "getLnurlPay": { - const result: IRespLnurlPay = await this.getLnurlPay( - reqMessage.params || {} - ); + const result: IRespLnurlPay = await this.getLnurlPay(reqMessage.params || {}); response.result = result.result; response.error = result.error; break; } case "deleteLnurlPay": { - const result: IRespLnurlPay = await this.deleteLnurlPay( - reqMessage.params || {} - ); + const result: IRespLnurlPay = await this.deleteLnurlPay(reqMessage.params || {}); response.result = result.result; response.error = result.error; @@ -244,27 +209,21 @@ class HttpServer { } case "getLnurlPayRequest": { - const result: IRespLnurlPayRequest = await this.getLnurlPayRequest( - reqMessage.params || {} - ); + const result: IRespLnurlPayRequest = await this.getLnurlPayRequest(reqMessage.params || {}); response.result = result.result; response.error = result.error; break; } case "deleteLnurlPayRequest": { - const result: IRespLnurlPayRequest = await this.getLnurlPayRequest( - reqMessage.params || {} - ); + const result: IRespLnurlPayRequest = await this.getLnurlPayRequest(reqMessage.params || {}); response.result = result.result; response.error = result.error; break; } case "payLnAddress": { - const result: IRespPayLnAddress = await this.payLnAddress( - reqMessage.params || {} - ); + const result: IRespPayLnAddress = await this.payLnAddress(reqMessage.params || {}); response.result = result.result; response.error = result.error; break; @@ -285,9 +244,7 @@ class HttpServer { } case "forceFallback": { - const result: IRespCreateLnurlWithdraw = await this.forceFallback( - reqMessage.params || {} - ); + const result: IRespCreateLnurlWithdraw = await this.forceFallback(reqMessage.params || {}); response.result = result.result; response.error = result.error; @@ -339,14 +296,9 @@ class HttpServer { // this._lnurlConfig.LN_SERVICE_CTX + // Stripped by traefik this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX, async (req, res) => { - logger.info( - this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + ":", - req.query - ); + logger.info(this._lnurlConfig.LN_SERVICE_WITHDRAW_REQUEST_CTX + ":", req.query); - const response: IRespLnServiceWithdrawRequest = await this._lnurlWithdraw.lnServiceWithdrawRequest( - req.query.s as string - ); + const response: IRespLnServiceWithdrawRequest = await this._lnurlWithdraw.lnServiceWithdrawRequest(req.query.s as string); if (response.status) { res.status(400).json(response); @@ -382,10 +334,7 @@ class HttpServer { // this._lnurlConfig.LN_SERVICE_CTX + // Stripped by traefik this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX + "/:externalId", async (req, res) => { - logger.info( - this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX + ":", - req.params - ); + logger.info(this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX + ":", req.params); const response = await this._lnurlPay.lnServicePaySpecs({ externalId: req.params.externalId, @@ -400,32 +349,26 @@ class HttpServer { ); // LN Service LNURL Pay specs (step 3) lightning address format - this._httpServer.get( - "/.well-known/lnurlp/:externalId", - async (req, res) => { - logger.info("/.well-known/lnurlp/:", req.params); + this._httpServer.get("/.well-known/lnurlp/:externalId", async (req, res) => { + logger.info("/.well-known/lnurlp/:", req.params); - const response = await this._lnurlPay.lnServicePaySpecs({ - externalId: req.params.externalId, - } as IReqViewLnurlPay); + const response = await this._lnurlPay.lnServicePaySpecs({ + externalId: req.params.externalId, + } as IReqViewLnurlPay); - if (response.status === "ERROR") { - res.status(400).json(response); - } else { - res.status(200).json(response); - } + if (response.status === "ERROR") { + res.status(400).json(response); + } else { + res.status(200).json(response); } - ); + }); // LN Service LNURL Pay request (step 5) this._httpServer.get( // this._lnurlConfig.LN_SERVICE_CTX + // Stripped by traefik this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + "/:externalId", async (req, res) => { - logger.info( - this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + ":", - req.params - ); + logger.info(this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX + ":", req.params); const response = await this._lnurlPay.lnServicePayRequest({ externalId: req.params.externalId, @@ -459,31 +402,20 @@ class HttpServer { } ); - this._httpServer.post( - this._lnurlConfig.URL_CTX_WITHDRAW_WEBHOOKS, - async (req, res) => { - logger.info( - this._lnurlConfig.URL_CTX_WITHDRAW_WEBHOOKS + ":", - req.body - ); + this._httpServer.post(this._lnurlConfig.URL_CTX_WITHDRAW_WEBHOOKS, async (req, res) => { + logger.info(this._lnurlConfig.URL_CTX_WITHDRAW_WEBHOOKS + ":", req.body); - const response = await this._lnurlWithdraw.processBatchWebhook( - req.body - ); + const response = await this._lnurlWithdraw.processBatchWebhook(req.body); - if (response.error) { - res.status(400).json(response); - } else { - res.status(200).json(response); - } + if (response.error) { + res.status(400).json(response); + } else { + res.status(200).json(response); } - ); + }); this._httpServer.listen(this._lnurlConfig.URL_API_PORT, () => { - logger.info( - "Express HTTP server listening on port:", - this._lnurlConfig.URL_API_PORT - ); + logger.info("Express HTTP server listening on port:", this._lnurlConfig.URL_API_PORT); }); } } diff --git a/src/lib/LnAddress.ts b/src/lib/LnAddress.ts index 7344a2f..7195fca 100644 --- a/src/lib/LnAddress.ts +++ b/src/lib/LnAddress.ts @@ -13,10 +13,7 @@ class LnAddress { return false; } - static async fetchBolt11( - address: string, - amount: number - ): Promise { + static async fetchBolt11(address: string, amount: number): Promise { const url = LnAddress.addressToUrl(address); if (url) { diff --git a/src/lib/LnurlDBPrisma.ts b/src/lib/LnurlDBPrisma.ts index c147da0..78de0a4 100644 --- a/src/lib/LnurlDBPrisma.ts +++ b/src/lib/LnurlDBPrisma.ts @@ -1,12 +1,7 @@ import logger from "./Log2File"; import path from "path"; import LnurlConfig from "../config/LnurlConfig"; -import { - LnurlPayEntity, - LnurlPayRequestEntity, - LnurlWithdrawEntity, - PrismaClient, -} from "@prisma/client"; +import { LnurlPayEntity, LnurlPayRequestEntity, LnurlWithdrawEntity, PrismaClient } from "@prisma/client"; import { SaveLnurlPayRequestWhere } from "../types/SaveLnurlPayRequestWhere"; class LnurlDBPrisma { @@ -20,13 +15,7 @@ class LnurlDBPrisma { logger.info("LnurlDBPrisma.configureDB", lnurlConfig); await this._db?.$disconnect(); - this._db = await this.initDatabase( - path.resolve( - lnurlConfig.BASE_DIR, - lnurlConfig.DATA_DIR, - lnurlConfig.DB_NAME - ) - ); + this._db = await this.initDatabase(path.resolve(lnurlConfig.BASE_DIR, lnurlConfig.DATA_DIR, lnurlConfig.DB_NAME)); } async initDatabase(dbName: string): Promise { @@ -43,9 +32,7 @@ class LnurlDBPrisma { }); } - async saveLnurlWithdraw( - lnurlWithdrawEntity: LnurlWithdrawEntity - ): Promise { + async saveLnurlWithdraw(lnurlWithdrawEntity: LnurlWithdrawEntity): Promise { const lw = await this._db?.lnurlWithdrawEntity.upsert({ where: { secretToken: lnurlWithdrawEntity.secretToken }, update: lnurlWithdrawEntity, @@ -55,9 +42,7 @@ class LnurlDBPrisma { return lw as LnurlWithdrawEntity; } - async getLnurlWithdrawBySecret( - secretToken: string - ): Promise { + async getLnurlWithdrawBySecret(secretToken: string): Promise { const lw = await this._db?.lnurlWithdrawEntity.findUnique({ where: { secretToken }, }); @@ -65,9 +50,7 @@ class LnurlDBPrisma { return lw as LnurlWithdrawEntity; } - async getLnurlWithdrawByBatchRequestId( - batchRequestId: number - ): Promise { + async getLnurlWithdrawByBatchRequestId(batchRequestId: number): Promise { const lw = await this._db?.lnurlWithdrawEntity.findUnique({ where: { batchRequestId }, }); @@ -75,9 +58,7 @@ class LnurlDBPrisma { return lw as LnurlWithdrawEntity; } - async getLnurlWithdraw( - lnurlWithdrawEntity: LnurlWithdrawEntity - ): Promise { + async getLnurlWithdraw(lnurlWithdrawEntity: LnurlWithdrawEntity): Promise { const lw = await this._db?.lnurlWithdrawEntity.findUnique({ where: { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId }, }); @@ -85,9 +66,7 @@ class LnurlDBPrisma { return lw as LnurlWithdrawEntity; } - async getLnurlWithdrawById( - lnurlWithdrawId: number - ): Promise { + async getLnurlWithdrawById(lnurlWithdrawId: number): Promise { const lw = await this._db?.lnurlWithdrawEntity.findUnique({ where: { lnurlWithdrawId: lnurlWithdrawId }, }); @@ -96,7 +75,6 @@ class LnurlDBPrisma { } async getNonCalledbackLnurlWithdraws(lnurlWithdrawId?: number): Promise { - // If there's a lnurlWithdrawId as arg, let's add it to the where clause! // We want to get all the lnurlWithdraws that: @@ -145,10 +123,7 @@ class LnurlDBPrisma { paid: false, expiresAt: { lt: new Date() }, fallbackDone: false, - AND: [ - { NOT: { btcFallbackAddress: null } }, - { NOT: { btcFallbackAddress: "" } }, - ], + AND: [{ NOT: { btcFallbackAddress: null } }, { NOT: { btcFallbackAddress: "" } }], }, }); @@ -181,9 +156,7 @@ class LnurlDBPrisma { return lw as LnurlPayEntity; } - async saveLnurlPayRequest( - lnurlPayRequestEntity: LnurlPayRequestEntity - ): Promise { + async saveLnurlPayRequest(lnurlPayRequestEntity: LnurlPayRequestEntity): Promise { const where: SaveLnurlPayRequestWhere = {}; if (lnurlPayRequestEntity.lnurlPayRequestId) { where.lnurlPayRequestId = lnurlPayRequestEntity.lnurlPayRequestId; @@ -200,9 +173,7 @@ class LnurlDBPrisma { return lw as LnurlPayRequestEntity; } - async getLnurlPayRequestById( - lnurlPayRequestId: number - ): Promise { + async getLnurlPayRequestById(lnurlPayRequestId: number): Promise { const lw = await this._db?.lnurlPayRequestEntity.findUnique({ where: { lnurlPayRequestId: lnurlPayRequestId }, }); @@ -210,9 +181,7 @@ class LnurlDBPrisma { return lw as LnurlPayRequestEntity; } - async getLnurlPayRequestByLabel( - bolt11Label: string - ): Promise { + async getLnurlPayRequestByLabel(bolt11Label: string): Promise { const lw = await this._db?.lnurlPayRequestEntity.findUnique({ where: { bolt11Label }, }); @@ -220,9 +189,7 @@ class LnurlDBPrisma { return lw as LnurlPayRequestEntity; } - async getLnurlPayRequestByPayId( - lnurlPayId: number - ): Promise { + async getLnurlPayRequestByPayId(lnurlPayId: number): Promise { const lw = await this._db?.lnurlPayRequestEntity.findMany({ where: { lnurlPayEntityId: lnurlPayId }, }); diff --git a/src/lib/LnurlPay.ts b/src/lib/LnurlPay.ts index 1afff49..614a607 100644 --- a/src/lib/LnurlPay.ts +++ b/src/lib/LnurlPay.ts @@ -53,28 +53,19 @@ class LnurlPay { this._lnurlConfig.LN_SERVICE_SCHEME + "://" + this._lnurlConfig.LN_SERVICE_DOMAIN + - ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && - this._lnurlConfig.LN_SERVICE_PORT === 443) || - (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && - this._lnurlConfig.LN_SERVICE_PORT === 80) + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && this._lnurlConfig.LN_SERVICE_PORT === 443) || + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + - (req - ? this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX - : this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX) + + (req ? this._lnurlConfig.LN_SERVICE_PAY_REQUEST_CTX : this._lnurlConfig.LN_SERVICE_PAY_SPECS_CTX) + "/" + externalId ); } - async createLnurlPay( - reqCreateLnurlPay: IReqCreateLnurlPay - ): Promise { - logger.info( - "LnurlPay.createLnurlPay, reqCreateLnurlPay:", - reqCreateLnurlPay - ); + async createLnurlPay(reqCreateLnurlPay: IReqCreateLnurlPay): Promise { + logger.info("LnurlPay.createLnurlPay, reqCreateLnurlPay:", reqCreateLnurlPay); const response: IRespLnurlPay = {}; @@ -105,10 +96,7 @@ class LnurlPay { } if (lnurlPayEntity) { - logger.debug( - "LnurlPay.createLnurlPay, lnurlPay created:", - lnurlPayEntity - ); + logger.debug("LnurlPay.createLnurlPay, lnurlPay created:", lnurlPayEntity); response.result = Object.assign(lnurlPayEntity, { lnurlDecoded, @@ -135,13 +123,8 @@ class LnurlPay { return response; } - async updateLnurlPay( - reqUpdateLnurlPay: IReqUpdateLnurlPay - ): Promise { - logger.info( - "LnurlPay.updateLnurlPay, reqCreateLnurlPay:", - reqUpdateLnurlPay - ); + async updateLnurlPay(reqUpdateLnurlPay: IReqUpdateLnurlPay): Promise { + logger.info("LnurlPay.updateLnurlPay, reqCreateLnurlPay:", reqUpdateLnurlPay); const response: IRespLnurlPay = {}; @@ -149,15 +132,11 @@ class LnurlPay { // Inputs are valid. logger.debug("LnurlPay.updateLnurlPay, Inputs are valid."); - let lnurlPayEntity: LnurlPayEntity = await this._lnurlDB.getLnurlPayById( - reqUpdateLnurlPay.lnurlPayId - ); + let lnurlPayEntity: LnurlPayEntity = await this._lnurlDB.getLnurlPayById(reqUpdateLnurlPay.lnurlPayId); if (lnurlPayEntity) { try { - lnurlPayEntity = await this._lnurlDB.saveLnurlPay( - Object.assign(lnurlPayEntity, reqUpdateLnurlPay) - ); + lnurlPayEntity = await this._lnurlDB.saveLnurlPay(Object.assign(lnurlPayEntity, reqUpdateLnurlPay)); } catch (ex) { logger.debug("ex:", ex); @@ -170,10 +149,7 @@ class LnurlPay { } if (lnurlPayEntity) { - logger.debug( - "LnurlPay.createLnurlPay, lnurlPay created:", - lnurlPayEntity - ); + logger.debug("LnurlPay.createLnurlPay, lnurlPay created:", lnurlPayEntity); const lnurlDecoded = await Utils.decodeBech32(lnurlPayEntity.lnurl); @@ -235,25 +211,19 @@ class LnurlPay { message: "LnurlPay not found", }; } else if (!lnurlPayEntity.deleted) { - logger.debug( - "LnurlPay.deleteLnurlPay, unpaid lnurlPayEntity found for this lnurlPayId!" - ); + logger.debug("LnurlPay.deleteLnurlPay, unpaid lnurlPayEntity found for this lnurlPayId!"); lnurlPayEntity.deleted = true; lnurlPayEntity = await this._lnurlDB.saveLnurlPay(lnurlPayEntity); - const lnurlDecoded = await Utils.decodeBech32( - lnurlPayEntity?.lnurl || "" - ); + const lnurlDecoded = await Utils.decodeBech32(lnurlPayEntity?.lnurl || ""); response.result = Object.assign(lnurlPayEntity, { lnurlDecoded, }); } else { // LnurlPay already deactivated - logger.debug( - "LnurlPay.deleteLnurlPay, LnurlPay already deactivated." - ); + logger.debug("LnurlPay.deleteLnurlPay, LnurlPay already deactivated."); response.error = { code: ErrorCodes.InvalidRequest, @@ -262,9 +232,7 @@ class LnurlPay { } } else { // There is an error with inputs - logger.debug( - "LnurlPay.deleteLnurlPay, there is an error with inputs." - ); + logger.debug("LnurlPay.deleteLnurlPay, there is an error with inputs."); response.error = { code: ErrorCodes.InvalidRequest, @@ -291,13 +259,9 @@ class LnurlPay { const lnurlPayEntity = await this._lnurlDB.getLnurlPayById(lnurlPayId); if (lnurlPayEntity != null) { - logger.debug( - "LnurlPay.getLnurlPay, lnurlPayEntity found for this lnurlPayId!" - ); + logger.debug("LnurlPay.getLnurlPay, lnurlPayEntity found for this lnurlPayId!"); - const lnurlDecoded = await Utils.decodeBech32( - lnurlPayEntity.lnurl || "" - ); + const lnurlDecoded = await Utils.decodeBech32(lnurlPayEntity.lnurl || ""); response.result = Object.assign(lnurlPayEntity, { lnurlDecoded, @@ -327,25 +291,18 @@ class LnurlPay { /** * Called by user's wallet to get Payment specs */ - async lnServicePaySpecs( - reqViewLnurlPay: IReqViewLnurlPay - ): Promise { + async lnServicePaySpecs(reqViewLnurlPay: IReqViewLnurlPay): Promise { logger.info("LnurlPay.viewLnurlPay, reqViewLnurlPay:", reqViewLnurlPay); let response: IRespLnServicePaySpecs = {}; - const lnurlPay: LnurlPayEntity = await this._lnurlDB.getLnurlPayByExternalId( - reqViewLnurlPay.externalId - ); + const lnurlPay: LnurlPayEntity = await this._lnurlDB.getLnurlPayByExternalId(reqViewLnurlPay.externalId); if (lnurlPay) { if (!lnurlPay.deleted) { if (lnurlPay.externalId) { const metadata = JSON.stringify([ ["text/plain", lnurlPay.description], - [ - "text/identifier", - `${lnurlPay.externalId}@${this._lnurlConfig.LN_SERVICE_DOMAIN}`, - ], + ["text/identifier", `${lnurlPay.externalId}@${this._lnurlConfig.LN_SERVICE_DOMAIN}`], ]); logger.info("metadata =", metadata); @@ -386,36 +343,21 @@ class LnurlPay { /** * Called by user's wallet to ultimately get the bolt11 invoice */ - async lnServicePayRequest( - reqCreateLnurlPayReq: IReqCreateLnurlPayRequest - ): Promise { - logger.info( - "LnurlPay.createLnurlPayRequest, reqCreateLnurlPayReq:", - reqCreateLnurlPayReq - ); + async lnServicePayRequest(reqCreateLnurlPayReq: IReqCreateLnurlPayRequest): Promise { + logger.info("LnurlPay.createLnurlPayRequest, reqCreateLnurlPayReq:", reqCreateLnurlPayReq); let response: IRespLnServicePayRequest = {}; - const lnurlPay: LnurlPayEntity = await this._lnurlDB.getLnurlPayByExternalId( - reqCreateLnurlPayReq.externalId - ); + const lnurlPay: LnurlPayEntity = await this._lnurlDB.getLnurlPayByExternalId(reqCreateLnurlPayReq.externalId); if (lnurlPay) { if (!lnurlPay.deleted) { - if ( - CreateLnurlPayRequestValidator.validateRequest( - lnurlPay, - reqCreateLnurlPayReq - ) - ) { + if (CreateLnurlPayRequestValidator.validateRequest(lnurlPay, reqCreateLnurlPayReq)) { // Inputs are valid. logger.debug("LnurlPay.createLnurlPayRequest, Inputs are valid."); const metadata = JSON.stringify([ ["text/plain", lnurlPay.description], - [ - "text/identifier", - `${lnurlPay.externalId}@${this._lnurlConfig.LN_SERVICE_DOMAIN}`, - ], + ["text/identifier", `${lnurlPay.externalId}@${this._lnurlConfig.LN_SERVICE_DOMAIN}`], ]); logger.debug("metadata =", metadata); @@ -435,14 +377,9 @@ class LnurlPay { deschashonly: true, }; - logger.debug( - "LnurlPay.createLnurlPayRequest trying to get invoice", - lnCreateParams - ); + logger.debug("LnurlPay.createLnurlPayRequest trying to get invoice", lnCreateParams); - const resp: IRespLnCreate = await this._cyphernodeClient.lnCreate( - lnCreateParams - ); + const resp: IRespLnCreate = await this._cyphernodeClient.lnCreate(lnCreateParams); logger.debug("LnurlPay.createLnurlPayRequest lnCreate invoice", resp); if (resp.result) { @@ -456,9 +393,7 @@ class LnurlPay { let lnurlPayRequestEntity: LnurlPayRequestEntity; try { - lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest( - data as LnurlPayRequestEntity - ); + lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest(data as LnurlPayRequestEntity); } catch (ex) { logger.debug("ex:", ex); @@ -471,10 +406,7 @@ class LnurlPay { } if (lnurlPayRequestEntity && lnurlPayRequestEntity.bolt11) { - logger.debug( - "LnurlPay.createLnurlPayRequest, lnurlPayRequest created:", - lnurlPayRequestEntity - ); + logger.debug("LnurlPay.createLnurlPayRequest, lnurlPayRequest created:", lnurlPayRequestEntity); response = { pr: lnurlPayRequestEntity.bolt11, @@ -482,9 +414,7 @@ class LnurlPay { }; } else { // LnurlPayRequest not created - logger.debug( - "LnurlPay.createLnurlPayRequest, LnurlPayRequest not created." - ); + logger.debug("LnurlPay.createLnurlPayRequest, LnurlPayRequest not created."); response = { status: "ERROR", @@ -499,9 +429,7 @@ class LnurlPay { } } else { // There is an error with inputs - logger.debug( - "LnurlPay.createLnurlPayRequest, there is an error with inputs." - ); + logger.debug("LnurlPay.createLnurlPayRequest, there is an error with inputs."); response = { status: "ERROR", @@ -529,20 +457,13 @@ class LnurlPay { /** * Delete a payRequest, for instance if the LNPay Address is deleted. */ - async deleteLnurlPayRequest( - lnurlPayRequestId: number - ): Promise { + async deleteLnurlPayRequest(lnurlPayRequestId: number): Promise { const result: IRespLnurlPayRequest = await this._lock.acquire( "modifLnurlPayRequest", async (): Promise => { - logger.debug( - "acquired lock modifLnurlPayRequest in deleteLnurlPayRequest" - ); + logger.debug("acquired lock modifLnurlPayRequest in deleteLnurlPayRequest"); - logger.info( - "LnurlPay.deleteLnurlPayRequest, lnurlPayRequestId:", - lnurlPayRequestId - ); + logger.info("LnurlPay.deleteLnurlPayRequest, lnurlPayRequestId:", lnurlPayRequestId); const response: IRespLnurlPayRequest = {}; @@ -550,38 +471,25 @@ class LnurlPay { // Inputs are valid. logger.debug("LnurlPay.deleteLnurlPayRequest, Inputs are valid."); - let lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestById( - lnurlPayRequestId - ); + let lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestById(lnurlPayRequestId); if (lnurlPayRequestEntity == null) { - logger.debug( - "LnurlPay.deleteLnurlPayRequest, lnurlPayRequest not found" - ); + logger.debug("LnurlPay.deleteLnurlPayRequest, lnurlPayRequest not found"); response.error = { code: ErrorCodes.InvalidRequest, message: "LnurlPayRequest not found", }; - } else if ( - !lnurlPayRequestEntity.deleted && - !lnurlPayRequestEntity.paid - ) { - logger.debug( - "LnurlPay.deleteLnurlPayRequest, unpaid lnurlPayRequestEntity found for this lnurlPayRequestId!" - ); + } else if (!lnurlPayRequestEntity.deleted && !lnurlPayRequestEntity.paid) { + logger.debug("LnurlPay.deleteLnurlPayRequest, unpaid lnurlPayRequestEntity found for this lnurlPayRequestId!"); lnurlPayRequestEntity.deleted = true; - lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest( - lnurlPayRequestEntity - ); + lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest(lnurlPayRequestEntity); response.result = lnurlPayRequestEntity; } else { // LnurlPayRequest already deactivated - logger.debug( - "LnurlPay.deleteLnurlPayRequest, LnurlPayRequest already deactivated." - ); + logger.debug("LnurlPay.deleteLnurlPayRequest, LnurlPayRequest already deactivated."); response.error = { code: ErrorCodes.InvalidRequest, @@ -590,9 +498,7 @@ class LnurlPay { } } else { // There is an error with inputs - logger.debug( - "LnurlPay.deleteLnurlPayRequest, there is an error with inputs." - ); + logger.debug("LnurlPay.deleteLnurlPayRequest, there is an error with inputs."); response.error = { code: ErrorCodes.InvalidRequest, @@ -607,13 +513,8 @@ class LnurlPay { return result; } - async getLnurlPayRequest( - lnurlPayRequestId: number - ): Promise { - logger.info( - "LnurlPay.getLnurlPayRequest, lnurlPayRequestId:", - lnurlPayRequestId - ); + async getLnurlPayRequest(lnurlPayRequestId: number): Promise { + logger.info("LnurlPay.getLnurlPayRequest, lnurlPayRequestId:", lnurlPayRequestId); const response: IRespLnurlPayRequest = {}; @@ -621,14 +522,10 @@ class LnurlPay { // Inputs are valid. logger.debug("LnurlPay.getLnurlPayRequest, Inputs are valid."); - const lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestById( - lnurlPayRequestId - ); + const lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestById(lnurlPayRequestId); if (lnurlPayRequestEntity != null) { - logger.debug( - "LnurlPay.getLnurlPayRequest, lnurlPayRequestEntity found for this lnurlPayRequestId!" - ); + logger.debug("LnurlPay.getLnurlPayRequest, lnurlPayRequestEntity found for this lnurlPayRequestId!"); response.result = lnurlPayRequestEntity; } else { @@ -642,9 +539,7 @@ class LnurlPay { } } else { // There is an error with inputs - logger.debug( - "LnurlPay.getLnurlPayRequest, there is an error with inputs." - ); + logger.debug("LnurlPay.getLnurlPayRequest, there is an error with inputs."); response.error = { code: ErrorCodes.InvalidRequest, @@ -658,43 +553,28 @@ class LnurlPay { /** * This is called by CN when an LN invoice is paid. */ - async lnurlPayRequestCallback( - reqCallback: IReqLnurlPayRequestCallback - ): Promise { + async lnurlPayRequestCallback(reqCallback: IReqLnurlPayRequestCallback): Promise { const result: IRespLnurlPayRequestCallback = await this._lock.acquire( "modifLnurlPayRequestCallback", async (): Promise => { - logger.debug( - "acquired lock modifLnurlPayRequestCallback in lnurlPayRequestCallback" - ); + logger.debug("acquired lock modifLnurlPayRequestCallback in lnurlPayRequestCallback"); const response: IRespLnurlPayRequestCallback = {}; - let lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestByLabel( - reqCallback.bolt11Label - ); + let lnurlPayRequestEntity = await this._lnurlDB.getLnurlPayRequestByLabel(reqCallback.bolt11Label); if (lnurlPayRequestEntity) { lnurlPayRequestEntity.paid = true; - lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest( - lnurlPayRequestEntity - ); + lnurlPayRequestEntity = await this._lnurlDB.saveLnurlPayRequest(lnurlPayRequestEntity); - const lnurlPayEntity = await this._lnurlDB.getLnurlPayById( - lnurlPayRequestEntity.lnurlPayEntityId - ); + const lnurlPayEntity = await this._lnurlDB.getLnurlPayById(lnurlPayRequestEntity.lnurlPayEntityId); if (lnurlPayEntity && lnurlPayEntity.webhookUrl) { - const cbResponse = await Utils.post( - lnurlPayEntity.webhookUrl, - lnurlPayRequestEntity - ); + const cbResponse = await Utils.post(lnurlPayEntity.webhookUrl, lnurlPayRequestEntity); if (cbResponse.status >= 200 && cbResponse.status < 400) { - logger.debug( - "LnurlWithdraw.lnurlPayRequestCallback, paid, webhook called back" - ); + logger.debug("LnurlWithdraw.lnurlPayRequestCallback, paid, webhook called back"); lnurlPayRequestEntity.paidCalledbackTs = new Date(); await this._lnurlDB.saveLnurlPayRequest(lnurlPayRequestEntity); diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index d5bd9b0..35d956c 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -2,10 +2,7 @@ import logger from "./Log2File"; import LnurlConfig from "../config/LnurlConfig"; import { CyphernodeClient } from "./CyphernodeClient"; import { LnurlDB } from "./LnurlDBPrisma"; -import { - ErrorCodes, - IResponseMessage, -} from "../types/jsonrpc/IResponseMessage"; +import { ErrorCodes, IResponseMessage } from "../types/jsonrpc/IResponseMessage"; import IReqCreateLnurlWithdraw from "../types/IReqCreateLnurlWithdraw"; import IRespLnurlWithdraw from "../types/IRespLnurlWithdraw"; import { CreateLnurlWithdrawValidator } from "../validators/CreateLnurlWithdrawValidator"; @@ -78,27 +75,18 @@ class LnurlWithdraw { ); } - async createLnurlWithdraw( - reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw - ): Promise { - logger.info( - "LnurlWithdraw.createLnurlWithdraw, reqCreateLnurlWithdraw:", - reqCreateLnurlWithdraw - ); + async createLnurlWithdraw(reqCreateLnurlWithdraw: IReqCreateLnurlWithdraw): Promise { + logger.info("LnurlWithdraw.createLnurlWithdraw, reqCreateLnurlWithdraw:", reqCreateLnurlWithdraw); const response: IRespLnurlWithdraw = {}; if (CreateLnurlWithdrawValidator.validateRequest(reqCreateLnurlWithdraw)) { if (reqCreateLnurlWithdraw.btcFallbackAddress) { - const validateAddressResponse = await this._cyphernodeClient.validateAddress( - reqCreateLnurlWithdraw.btcFallbackAddress - ); + const validateAddressResponse = await this._cyphernodeClient.validateAddress(reqCreateLnurlWithdraw.btcFallbackAddress); if (!validateAddressResponse.result?.isvalid) { // There is an error with inputs - logger.debug( - "LnurlWithdraw.createLnurlWithdraw, invalid fallback Bitcoin address." - ); + logger.debug("LnurlWithdraw.createLnurlWithdraw, invalid fallback Bitcoin address."); response.error = { code: ErrorCodes.InvalidRequest, @@ -118,10 +106,8 @@ class LnurlWithdraw { this._lnurlConfig.LN_SERVICE_SCHEME + "://" + this._lnurlConfig.LN_SERVICE_DOMAIN + - ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && - this._lnurlConfig.LN_SERVICE_PORT === 443) || - (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && - this._lnurlConfig.LN_SERVICE_PORT === 80) + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && this._lnurlConfig.LN_SERVICE_PORT === 443) || + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + @@ -151,19 +137,14 @@ class LnurlWithdraw { } if (lnurlWithdrawEntity) { - logger.debug( - "LnurlWithdraw.createLnurlWithdraw, lnurlWithdraw created:", - lnurlWithdrawEntity - ); + logger.debug("LnurlWithdraw.createLnurlWithdraw, lnurlWithdraw created:", lnurlWithdrawEntity); response.result = Object.assign(lnurlWithdrawEntity, { lnurlDecoded, }); } else { // LnurlWithdraw not created - logger.debug( - "LnurlWithdraw.createLnurlWithdraw, LnurlWithdraw not created." - ); + logger.debug("LnurlWithdraw.createLnurlWithdraw, LnurlWithdraw not created."); response.error = { code: ErrorCodes.InvalidRequest, @@ -172,9 +153,7 @@ class LnurlWithdraw { } } else { // There is an error with inputs - logger.debug( - "LnurlWithdraw.createLnurlWithdraw, there is an error with inputs." - ); + logger.debug("LnurlWithdraw.createLnurlWithdraw, there is an error with inputs."); response.error = { code: ErrorCodes.InvalidRequest, @@ -185,18 +164,13 @@ class LnurlWithdraw { return response; } - async deleteLnurlWithdraw( - lnurlWithdrawId: number - ): Promise { + async deleteLnurlWithdraw(lnurlWithdrawId: number): Promise { const result: IRespLnurlWithdraw = await this._lock.acquire( "modifLnurlWithdraw", async (): Promise => { logger.debug("acquired lock modifLnurlWithdraw in deleteLnurlWithdraw"); - logger.info( - "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdrawId:", - lnurlWithdrawId - ); + logger.info("LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdrawId:", lnurlWithdrawId); const response: IRespLnurlWithdraw = {}; @@ -204,15 +178,11 @@ class LnurlWithdraw { // Inputs are valid. logger.debug("LnurlWithdraw.deleteLnurlWithdraw, Inputs are valid."); - let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById( - lnurlWithdrawId - ); + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById(lnurlWithdrawId); // if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { if (lnurlWithdrawEntity == null) { - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdraw not found" - ); + logger.debug("LnurlWithdraw.deleteLnurlWithdraw, lnurlWithdraw not found"); response.error = { code: ErrorCodes.InvalidRequest, @@ -220,27 +190,19 @@ class LnurlWithdraw { }; } else if (!lnurlWithdrawEntity.deleted) { if (!lnurlWithdrawEntity.paid) { - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!" - ); + logger.debug("LnurlWithdraw.deleteLnurlWithdraw, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!"); lnurlWithdrawEntity.deleted = true; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); - const lnurlDecoded = await Utils.decodeBech32( - lnurlWithdrawEntity?.lnurl || "" - ); + const lnurlDecoded = await Utils.decodeBech32(lnurlWithdrawEntity?.lnurl || ""); response.result = Object.assign(lnurlWithdrawEntity, { lnurlDecoded, }); } else { // LnurlWithdraw already paid - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already paid." - ); + logger.debug("LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already paid."); response.error = { code: ErrorCodes.InvalidRequest, @@ -249,9 +211,7 @@ class LnurlWithdraw { } } else { // LnurlWithdraw already deactivated - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already deactivated." - ); + logger.debug("LnurlWithdraw.deleteLnurlWithdraw, LnurlWithdraw already deactivated."); response.error = { code: ErrorCodes.InvalidRequest, @@ -260,9 +220,7 @@ class LnurlWithdraw { } } else { // There is an error with inputs - logger.debug( - "LnurlWithdraw.deleteLnurlWithdraw, there is an error with inputs." - ); + logger.debug("LnurlWithdraw.deleteLnurlWithdraw, there is an error with inputs."); response.error = { code: ErrorCodes.InvalidRequest, @@ -278,10 +236,7 @@ class LnurlWithdraw { } async getLnurlWithdraw(lnurlWithdrawId: number): Promise { - logger.info( - "LnurlWithdraw.getLnurlWithdraw, lnurlWithdrawId:", - lnurlWithdrawId - ); + logger.info("LnurlWithdraw.getLnurlWithdraw, lnurlWithdrawId:", lnurlWithdrawId); const response: IRespLnurlWithdraw = {}; @@ -289,27 +244,19 @@ class LnurlWithdraw { // Inputs are valid. logger.debug("LnurlWithdraw.getLnurlWithdraw, Inputs are valid."); - const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById( - lnurlWithdrawId - ); + const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById(lnurlWithdrawId); if (lnurlWithdrawEntity != null) { - logger.debug( - "LnurlWithdraw.getLnurlWithdraw, lnurlWithdrawEntity found for this lnurlWithdrawId!" - ); + logger.debug("LnurlWithdraw.getLnurlWithdraw, lnurlWithdrawEntity found for this lnurlWithdrawId!"); - const lnurlDecoded = await Utils.decodeBech32( - lnurlWithdrawEntity.lnurl || "" - ); + const lnurlDecoded = await Utils.decodeBech32(lnurlWithdrawEntity.lnurl || ""); response.result = Object.assign(lnurlWithdrawEntity, { lnurlDecoded, }); } else { // Active LnurlWithdraw not found - logger.debug( - "LnurlWithdraw.getLnurlWithdraw, LnurlWithdraw not found." - ); + logger.debug("LnurlWithdraw.getLnurlWithdraw, LnurlWithdraw not found."); response.error = { code: ErrorCodes.InvalidRequest, @@ -318,9 +265,7 @@ class LnurlWithdraw { } } else { // There is an error with inputs - logger.debug( - "LnurlWithdraw.getLnurlWithdraw, there is an error with inputs." - ); + logger.debug("LnurlWithdraw.getLnurlWithdraw, there is an error with inputs."); response.error = { code: ErrorCodes.InvalidRequest, @@ -331,44 +276,27 @@ class LnurlWithdraw { return response; } - async lnServiceWithdrawRequest( - secretToken: string - ): Promise { + async lnServiceWithdrawRequest(secretToken: string): Promise { const result: IRespLnServiceWithdrawRequest = await this._lock.acquire( "modifLnurlWithdraw", async (): Promise => { - logger.debug( - "acquired lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" - ); + logger.debug("acquired lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request"); logger.info("LnurlWithdraw.lnServiceWithdrawRequest:", secretToken); let result: IRespLnServiceWithdrawRequest; - const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( - secretToken - ); - logger.debug( - "LnurlWithdraw.lnServiceWithdrawRequest, lnurlWithdrawEntity:", - lnurlWithdrawEntity - ); + const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret(secretToken); + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, lnurlWithdrawEntity:", lnurlWithdrawEntity); if (lnurlWithdrawEntity == null) { - logger.debug( - "LnurlWithdraw.lnServiceWithdrawRequest, invalid k1 value:" - ); + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, invalid k1 value:"); result = { status: "ERROR", reason: "Invalid k1 value" }; } else if (!lnurlWithdrawEntity.deleted) { - if ( - !lnurlWithdrawEntity.paid && - !lnurlWithdrawEntity.batchRequestId - ) { + if (!lnurlWithdrawEntity.paid && !lnurlWithdrawEntity.batchRequestId) { // Check expiration - if ( - lnurlWithdrawEntity.expiresAt && - lnurlWithdrawEntity.expiresAt < new Date() - ) { + if (lnurlWithdrawEntity.expiresAt && lnurlWithdrawEntity.expiresAt < new Date()) { // Expired LNURL logger.debug("LnurlWithdraw.lnServiceWithdrawRequest: expired!"); @@ -382,27 +310,20 @@ class LnurlWithdraw { this._lnurlConfig.LN_SERVICE_SCHEME + "://" + this._lnurlConfig.LN_SERVICE_DOMAIN + - ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === - "https" && - this._lnurlConfig.LN_SERVICE_PORT === 443) || - (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === - "http" && - this._lnurlConfig.LN_SERVICE_PORT === 80) + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && this._lnurlConfig.LN_SERVICE_PORT === 443) || + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + this._lnurlConfig.LN_SERVICE_WITHDRAW_CTX, k1: lnurlWithdrawEntity.secretToken, - defaultDescription: - lnurlWithdrawEntity.description || undefined, + defaultDescription: lnurlWithdrawEntity.description || undefined, minWithdrawable: lnurlWithdrawEntity.msatoshi || undefined, maxWithdrawable: lnurlWithdrawEntity.msatoshi || undefined, }; } } else { - logger.debug( - "LnurlWithdraw.lnServiceWithdrawRequest, LnurlWithdraw already paid or batched" - ); + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, LnurlWithdraw already paid or batched"); result = { status: "ERROR", @@ -410,35 +331,22 @@ class LnurlWithdraw { }; } } else { - logger.debug( - "LnurlWithdraw.lnServiceWithdrawRequest, deactivated LNURL" - ); + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, deactivated LNURL"); result = { status: "ERROR", reason: "Deactivated LNURL" }; } - logger.debug( - "LnurlWithdraw.lnServiceWithdrawRequest, responding:", - result - ); + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, responding:", result); return result; } ); - logger.debug( - "released lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request" - ); + logger.debug("released lock deleteLnurlWithdraw in LN Service LNURL Withdraw Request"); return result; } - async processLnPayment( - lnurlWithdrawEntity: LnurlWithdrawEntity, - bolt11: string - ): Promise> { - logger.debug( - "LnurlWithdraw.processLnPayment: lnurlWithdrawEntity:", - lnurlWithdrawEntity - ); + async processLnPayment(lnurlWithdrawEntity: LnurlWithdrawEntity, bolt11: string): Promise> { + logger.debug("LnurlWithdraw.processLnPayment: lnurlWithdrawEntity:", lnurlWithdrawEntity); logger.debug("LnurlWithdraw.processLnPayment: bolt11:", bolt11); let result; @@ -446,9 +354,7 @@ class LnurlWithdraw { // Let's check if bolt11 is valid first. // If it's valid, we'll try to pay and save the data. // If it's not valid, we'll send an error and won't save the data. - let resp: - | IRespLnDecodeBolt11 - | IRespLnPay = await this._cyphernodeClient.lnDecodeBolt11(bolt11); + let resp: IRespLnDecodeBolt11 | IRespLnPay = await this._cyphernodeClient.lnDecodeBolt11(bolt11); if (resp.result) { lnurlWithdrawEntity.bolt11 = bolt11; @@ -468,9 +374,7 @@ class LnurlWithdraw { lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); if (lnurlWithdrawEntity?.webhookUrl && lnurlWithdrawEntity?.webhookUrl.length > 0) { // Immediately send a webhook to let client know a failed attempt has been made. @@ -479,17 +383,13 @@ class LnurlWithdraw { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, bolt11: lnurlWithdrawEntity.bolt11, lnPayResponse: resp.error, - updatedTs: lnurlWithdrawEntity.updatedTs + updatedTs: lnurlWithdrawEntity.updatedTs, }; - logger.debug( - "LnurlWithdraw.processLnPayment, claim attempt failed, calling back with postdata=", - postdata - ); + logger.debug("LnurlWithdraw.processLnPayment, claim attempt failed, calling back with postdata=", postdata); Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); } - } else { logger.debug("LnurlWithdraw.processLnPayment, ln_pay success!"); @@ -499,9 +399,7 @@ class LnurlWithdraw { lnurlWithdrawEntity.withdrawnTs = new Date(); lnurlWithdrawEntity.paid = true; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); this.checkWebhook(lnurlWithdrawEntity); } @@ -511,9 +409,7 @@ class LnurlWithdraw { async checkWebhook(lnurlWithdrawEntity: LnurlWithdrawEntity): Promise { if (lnurlWithdrawEntity.webhookUrl) { - logger.debug( - "LnurlWithdraw.checkWebhook, about to call back the webhookUrl..." - ); + logger.debug("LnurlWithdraw.checkWebhook, about to call back the webhookUrl..."); this.processCallbacks(lnurlWithdrawEntity); } else { @@ -547,29 +443,20 @@ class LnurlWithdraw { // lnurlWithdrawEntity.withdrawnTs = new Date(); lnurlWithdrawEntity.paid = true; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); this.checkWebhook(lnurlWithdrawEntity); } else if (paymentStatus === "failed") { logger.debug("LnurlWithdraw.lnServiceWithdraw, payment failed..."); - if ( - lnurlWithdrawEntity.expiresAt && - lnurlWithdrawEntity.expiresAt < new Date() - ) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, previous pay failed, now expired..." - ); + if (lnurlWithdrawEntity.expiresAt && lnurlWithdrawEntity.expiresAt < new Date()) { + logger.debug("LnurlWithdraw.lnServiceWithdraw, previous pay failed, now expired..."); result = { status: "ERROR", reason: "Expired LNURL-Withdraw", }; } else { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, previous payment failed but not expired, retry..." - ); + logger.debug("LnurlWithdraw.lnServiceWithdraw, previous payment failed but not expired, retry..."); result = await this.processLnPayment(lnurlWithdrawEntity, bolt11); } @@ -585,9 +472,7 @@ class LnurlWithdraw { return result; } - async lnFetchPaymentStatus( - bolt11: string - ): Promise<{ paymentStatus?: string; result?: unknown }> { + async lnFetchPaymentStatus(bolt11: string): Promise<{ paymentStatus?: string; result?: unknown }> { let paymentStatus; let result; @@ -600,15 +485,9 @@ class LnurlWithdraw { logger.debug("LnurlWithdraw.lnFetchPaymentStatus, lnListPays errored..."); } else if (resp.result && resp.result.pays && resp.result.pays.length > 0) { const nonfailedpay = resp.result.pays.find((obj) => { - return ( - (obj as any).status === "complete" || - (obj as any).status === "pending" - ); + return (obj as any).status === "complete" || (obj as any).status === "pending"; }); - logger.debug( - "LnurlWithdraw.lnFetchPaymentStatus, nonfailedpay =", - nonfailedpay - ); + logger.debug("LnurlWithdraw.lnFetchPaymentStatus, nonfailedpay =", nonfailedpay); if (nonfailedpay !== undefined) { paymentStatus = (nonfailedpay as any).status; @@ -619,9 +498,7 @@ class LnurlWithdraw { result = resp.result; } else { // Error, should not happen, something's wrong, let's try with paystatus... - logger.debug( - "LnurlWithdraw.lnFetchPaymentStatus, no previous listpays for this bolt11..." - ); + logger.debug("LnurlWithdraw.lnFetchPaymentStatus, no previous listpays for this bolt11..."); const paystatus = await this._cyphernodeClient.lnPayStatus({ bolt11, @@ -629,13 +506,9 @@ class LnurlWithdraw { if (paystatus.error) { // Error, should not happen, something's wrong, let's get out of here - logger.debug( - "LnurlWithdraw.lnFetchPaymentStatus, lnPayStatus errored..." - ); + logger.debug("LnurlWithdraw.lnFetchPaymentStatus, lnPayStatus errored..."); } else if (paystatus.result) { - logger.debug( - "LnurlWithdraw.lnFetchPaymentStatus, lnPayStatus success..." - ); + logger.debug("LnurlWithdraw.lnFetchPaymentStatus, lnPayStatus success..."); // We parse paystatus result // pay[] is an array of payments @@ -665,9 +538,7 @@ class LnurlWithdraw { // database + logging of the row. nbAttempts += pay.attempts.length; if (nbAttempts > 1000) { - logger.debug( - "LnurlWithdraw.lnFetchPaymentStatus, paystatus.result is too large, truncating content..." - ); + logger.debug("LnurlWithdraw.lnFetchPaymentStatus, paystatus.result is too large, truncating content..."); // Let's keep two attempts, and put a message in the second one... pay.attempts.splice(2); pay.attempts[1].failure = { @@ -684,10 +555,7 @@ class LnurlWithdraw { paymentStatus = "failed"; } - logger.debug( - "LnurlWithdraw.lnFetchPaymentStatus, paymentStatus =", - paymentStatus - ); + logger.debug("LnurlWithdraw.lnFetchPaymentStatus, paymentStatus =", paymentStatus); result = paystatus.result; } @@ -696,15 +564,11 @@ class LnurlWithdraw { return { paymentStatus, result }; } - async lnServiceWithdraw( - params: IReqLnurlWithdraw - ): Promise { + async lnServiceWithdraw(params: IReqLnurlWithdraw): Promise { const result = await this._lock.acquire( "deleteLnurlWithdraw", async (): Promise => { - logger.debug( - "acquired lock modifLnurlWithdraw in LN Service LNURL Withdraw" - ); + logger.debug("acquired lock modifLnurlWithdraw in LN Service LNURL Withdraw"); logger.info("LnurlWithdraw.lnServiceWithdraw:", params); @@ -714,9 +578,7 @@ class LnurlWithdraw { // Inputs are valid. logger.debug("LnurlWithdraw.lnServiceWithdraw, Inputs are valid."); - const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret( - params.k1 - ); + const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret(params.k1); // If a payment request has already been made, we need to check that payment // status first. @@ -732,19 +594,12 @@ class LnurlWithdraw { result = { status: "ERROR", reason: "Invalid k1 value" }; } else if (!lnurlWithdrawEntity.deleted) { - if ( - !lnurlWithdrawEntity.paid && - !lnurlWithdrawEntity.batchRequestId - ) { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, unpaid lnurlWithdrawEntity found for this k1!" - ); + if (!lnurlWithdrawEntity.paid && !lnurlWithdrawEntity.batchRequestId) { + logger.debug("LnurlWithdraw.lnServiceWithdraw, unpaid lnurlWithdrawEntity found for this k1!"); if (lnurlWithdrawEntity.bolt11) { // Payment request has been made before, check payment status - const paymentStatus = await this.lnFetchPaymentStatus( - lnurlWithdrawEntity.bolt11 - ); + const paymentStatus = await this.lnFetchPaymentStatus(lnurlWithdrawEntity.bolt11); if (paymentStatus.paymentStatus === undefined) { result = { @@ -758,22 +613,14 @@ class LnurlWithdraw { params.pr, paymentStatus.result ); - logger.debug( - "this.processLnStatus result =", - JSON.stringify(result) - ); + logger.debug("this.processLnStatus result =", JSON.stringify(result)); } } else { // Not previously claimed LNURL - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, Not previously claimed LNURL..." - ); + logger.debug("LnurlWithdraw.lnServiceWithdraw, Not previously claimed LNURL..."); // Check expiration - if ( - lnurlWithdrawEntity.expiresAt && - lnurlWithdrawEntity.expiresAt < new Date() - ) { + if (lnurlWithdrawEntity.expiresAt && lnurlWithdrawEntity.expiresAt < new Date()) { // Expired LNURL logger.debug("LnurlWithdraw.lnServiceWithdraw: expired!"); @@ -782,16 +629,11 @@ class LnurlWithdraw { reason: "Expired LNURL-Withdraw", }; } else { - result = await this.processLnPayment( - lnurlWithdrawEntity, - params.pr - ); + result = await this.processLnPayment(lnurlWithdrawEntity, params.pr); } } } else { - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, already paid or batched!" - ); + logger.debug("LnurlWithdraw.lnServiceWithdraw, already paid or batched!"); result = { status: "ERROR", @@ -805,9 +647,7 @@ class LnurlWithdraw { } } else { // There is an error with inputs - logger.debug( - "LnurlWithdraw.lnServiceWithdraw, there is an error with inputs." - ); + logger.debug("LnurlWithdraw.lnServiceWithdraw, there is an error with inputs."); result = { status: "ERROR", @@ -821,31 +661,22 @@ class LnurlWithdraw { } ); - logger.debug( - "released lock modifLnurlWithdraw in LN Service LNURL Withdraw" - ); + logger.debug("released lock modifLnurlWithdraw in LN Service LNURL Withdraw"); return result; } - async processCallbacks( - lnurlWithdrawEntity?: LnurlWithdrawEntity - ): Promise { + async processCallbacks(lnurlWithdrawEntity?: LnurlWithdrawEntity): Promise { await this._lock.acquire( "processCallbacks", async (): Promise => { logger.debug("acquired lock processCallbacks in processCallbacks"); - logger.info( - "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", - lnurlWithdrawEntity - ); + logger.info("LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", lnurlWithdrawEntity); let lnurlWithdrawEntitys; if (lnurlWithdrawEntity) { // Let's take the latest on from database, just in case passed object has stale data - lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws( - lnurlWithdrawEntity.lnurlWithdrawId - ); + lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(lnurlWithdrawEntity.lnurlWithdrawId); } else { lnurlWithdrawEntitys = await this._lnurlDB.getNonCalledbackLnurlWithdraws(); } @@ -853,43 +684,23 @@ class LnurlWithdraw { let response; let postdata = {}; lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { - logger.debug( - "LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", - lnurlWithdrawEntity - ); - - if ( - !lnurlWithdrawEntity.deleted && - lnurlWithdrawEntity.webhookUrl && - lnurlWithdrawEntity.webhookUrl.length > 0 - ) { - if ( - !lnurlWithdrawEntity.batchedCalledback && - lnurlWithdrawEntity.batchRequestId - ) { + logger.debug("LnurlWithdraw.processCallbacks, lnurlWithdrawEntity=", lnurlWithdrawEntity); + + if (!lnurlWithdrawEntity.deleted && lnurlWithdrawEntity.webhookUrl && lnurlWithdrawEntity.webhookUrl.length > 0) { + if (!lnurlWithdrawEntity.batchedCalledback && lnurlWithdrawEntity.batchRequestId) { // Payment has been batched, not yet paid postdata = { action: "fallbackBatched", lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, - details: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, + details: lnurlWithdrawEntity.withdrawnDetails ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) : null, }; - logger.debug( - "LnurlWithdraw.processCallbacks, batched, postdata=", - postdata - ); + logger.debug("LnurlWithdraw.processCallbacks, batched, postdata=", postdata); - response = await Utils.post( - lnurlWithdrawEntity.webhookUrl, - postdata - ); + response = await Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); if (response.status >= 200 && response.status < 400) { - logger.debug( - "LnurlWithdraw.processCallbacks, batched, webhook called back" - ); + logger.debug("LnurlWithdraw.processCallbacks, batched, webhook called back"); lnurlWithdrawEntity.batchedCalledback = true; lnurlWithdrawEntity.batchedCalledbackTs = new Date(); @@ -897,10 +708,7 @@ class LnurlWithdraw { } } - if ( - !lnurlWithdrawEntity.paidCalledback && - lnurlWithdrawEntity.paid - ) { + if (!lnurlWithdrawEntity.paidCalledback && lnurlWithdrawEntity.paid) { // Payment has been sent if (lnurlWithdrawEntity.fallbackDone) { @@ -909,9 +717,7 @@ class LnurlWithdraw { action: "fallbackPaid", lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, btcFallbackAddress: lnurlWithdrawEntity.btcFallbackAddress, - details: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, + details: lnurlWithdrawEntity.withdrawnDetails ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) : null, }; } else { // If paid through LN... @@ -919,26 +725,16 @@ class LnurlWithdraw { action: "lnPaid", lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, bolt11: lnurlWithdrawEntity.bolt11, - lnPayResponse: lnurlWithdrawEntity.withdrawnDetails - ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) - : null, + lnPayResponse: lnurlWithdrawEntity.withdrawnDetails ? JSON.parse(lnurlWithdrawEntity.withdrawnDetails) : null, }; } - logger.debug( - "LnurlWithdraw.processCallbacks, paid, postdata=", - postdata - ); + logger.debug("LnurlWithdraw.processCallbacks, paid, postdata=", postdata); - response = await Utils.post( - lnurlWithdrawEntity.webhookUrl, - postdata - ); + response = await Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); if (response.status >= 200 && response.status < 400) { - logger.debug( - "LnurlWithdraw.processCallbacks, paid, webhook called back" - ); + logger.debug("LnurlWithdraw.processCallbacks, paid, webhook called back"); lnurlWithdrawEntity.paidCalledback = true; lnurlWithdrawEntity.paidCalledbackTs = new Date(); @@ -957,20 +753,12 @@ class LnurlWithdraw { lnurlWithdrawId: lnurlWithdrawEntity.lnurlWithdrawId, expiresAt: lnurlWithdrawEntity.expiresAt, }; - logger.debug( - "LnurlWithdraw.processCallbacks, expired, postdata=", - postdata - ); + logger.debug("LnurlWithdraw.processCallbacks, expired, postdata=", postdata); - response = await Utils.post( - lnurlWithdrawEntity.webhookUrl, - postdata - ); + response = await Utils.post(lnurlWithdrawEntity.webhookUrl, postdata); if (response.status >= 200 && response.status < 400) { - logger.debug( - "LnurlWithdraw.processCallbacks, expired, webhook called back" - ); + logger.debug("LnurlWithdraw.processCallbacks, expired, webhook called back"); lnurlWithdrawEntity.expiredCalledback = true; lnurlWithdrawEntity.expiredCalledbackTs = new Date(); @@ -993,46 +781,28 @@ class LnurlWithdraw { logger.info("LnurlWithdraw.processFallbacks"); const lnurlWithdrawEntitys = await this._lnurlDB.getFallbackLnurlWithdraws(); - logger.debug( - "LnurlWithdraw.processFallbacks, lnurlWithdrawEntitys=", - lnurlWithdrawEntitys - ); + logger.debug("LnurlWithdraw.processFallbacks, lnurlWithdrawEntitys=", lnurlWithdrawEntitys); lnurlWithdrawEntitys.forEach(async (lnurlWithdrawEntity) => { - logger.debug( - "LnurlWithdraw.processFallbacks, lnurlWithdrawEntity=", - lnurlWithdrawEntity - ); + logger.debug("LnurlWithdraw.processFallbacks, lnurlWithdrawEntity=", lnurlWithdrawEntity); let proceedToFallback = true; if (lnurlWithdrawEntity.bolt11) { // Before falling back on-chain, let's make really sure the payment has not been done... - const paymentStatus = await this.lnFetchPaymentStatus( - lnurlWithdrawEntity.bolt11 - ); + const paymentStatus = await this.lnFetchPaymentStatus(lnurlWithdrawEntity.bolt11); if (paymentStatus.paymentStatus === undefined) { - logger.debug( - "LnurlWithdraw.processFallbacks: Can't get LnurlWithdraw previously paid status." - ); + logger.debug("LnurlWithdraw.processFallbacks: Can't get LnurlWithdraw previously paid status."); proceedToFallback = false; } else if (paymentStatus.paymentStatus !== "failed") { - logger.debug( - "LnurlWithdraw.processFallbacks: LnurlWithdraw payment already " + - paymentStatus.paymentStatus - ); + logger.debug("LnurlWithdraw.processFallbacks: LnurlWithdraw payment already " + paymentStatus.paymentStatus); proceedToFallback = false; - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - paymentStatus.result - ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(paymentStatus.result); // lnurlWithdrawEntity.withdrawnTs = new Date(); - lnurlWithdrawEntity.paid = - paymentStatus.paymentStatus === "complete"; + lnurlWithdrawEntity.paid = paymentStatus.paymentStatus === "complete"; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); if (paymentStatus.paymentStatus === "complete") { this.checkWebhook(lnurlWithdrawEntity); @@ -1045,9 +815,7 @@ class LnurlWithdraw { logger.debug("LnurlWithdraw.processFallbacks, batched fallback"); if (lnurlWithdrawEntity.batchRequestId) { - logger.debug( - "LnurlWithdraw.processFallbacks, already batched!" - ); + logger.debug("LnurlWithdraw.processFallbacks, already batched!"); } else { const batchRequestTO: IReqBatchRequest = { externalId: lnurlWithdrawEntity.externalId || undefined, @@ -1061,77 +829,49 @@ class LnurlWithdraw { this._lnurlConfig.URL_CTX_WITHDRAW_WEBHOOKS, }; - const resp: IRespBatchRequest = await this._batcherClient.queueForNextBatch( - batchRequestTO - ); + const resp: IRespBatchRequest = await this._batcherClient.queueForNextBatch(batchRequestTO); if (resp.error) { - logger.debug( - "LnurlWithdraw.processFallbacks, queueForNextBatch error!" - ); + logger.debug("LnurlWithdraw.processFallbacks, queueForNextBatch error!"); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.error - ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.error); } else { - logger.debug( - "LnurlWithdraw.processFallbacks, queueForNextBatch success!" - ); + logger.debug("LnurlWithdraw.processFallbacks, queueForNextBatch success!"); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - resp.result - ); - lnurlWithdrawEntity.batchRequestId = - resp.result?.batchRequestId || null; + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(resp.result); + lnurlWithdrawEntity.batchRequestId = resp.result?.batchRequestId || null; } - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); if (lnurlWithdrawEntity.batchRequestId) { this.checkWebhook(lnurlWithdrawEntity); } } } else { - logger.debug( - "LnurlWithdraw.processFallbacks, not batched fallback" - ); + logger.debug("LnurlWithdraw.processFallbacks, not batched fallback"); const spendRequestTO: IReqSpend = { address: lnurlWithdrawEntity.btcFallbackAddress || "", amount: Math.round(lnurlWithdrawEntity.msatoshi / 1000) / 1e8, }; - const spendResp: IRespSpend = await this._cyphernodeClient.spend( - spendRequestTO - ); + const spendResp: IRespSpend = await this._cyphernodeClient.spend(spendRequestTO); if (spendResp?.error) { // There was an error on Cyphernode end, return that. - logger.debug( - "LnurlWithdraw.processFallbacks: There was an error on Cyphernode spend." - ); + logger.debug("LnurlWithdraw.processFallbacks: There was an error on Cyphernode spend."); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - spendResp.error - ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(spendResp.error); } else if (spendResp?.result) { - logger.debug( - "LnurlWithdraw.processFallbacks: Cyphernode spent: ", - spendResp.result - ); - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - spendResp.result - ); + logger.debug("LnurlWithdraw.processFallbacks: Cyphernode spent: ", spendResp.result); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(spendResp.result); lnurlWithdrawEntity.withdrawnTs = new Date(); lnurlWithdrawEntity.paid = true; lnurlWithdrawEntity.fallbackDone = true; } - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); if (lnurlWithdrawEntity.fallbackDone) { this.checkWebhook(lnurlWithdrawEntity); @@ -1150,10 +890,7 @@ class LnurlWithdraw { async (): Promise => { logger.debug("acquired lock processFallbacks in forceFallback"); - logger.info( - "LnurlWithdraw.forceFallback, lnurlWithdrawId:", - lnurlWithdrawId - ); + logger.info("LnurlWithdraw.forceFallback, lnurlWithdrawId:", lnurlWithdrawId); const response: IRespLnurlWithdraw = {}; @@ -1161,15 +898,11 @@ class LnurlWithdraw { // Inputs are valid. logger.debug("LnurlWithdraw.forceFallback, Inputs are valid."); - let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById( - lnurlWithdrawId - ); + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawById(lnurlWithdrawId); // if (lnurlWithdrawEntity != null && lnurlWithdrawEntity.active) { if (lnurlWithdrawEntity == null) { - logger.debug( - "LnurlWithdraw.forceFallback, lnurlWithdraw not found" - ); + logger.debug("LnurlWithdraw.forceFallback, lnurlWithdraw not found"); response.error = { code: ErrorCodes.InvalidRequest, @@ -1177,21 +910,15 @@ class LnurlWithdraw { }; } else if (!lnurlWithdrawEntity.deleted) { if (!lnurlWithdrawEntity.paid) { - logger.debug( - "LnurlWithdraw.forceFallback, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!" - ); + logger.debug("LnurlWithdraw.forceFallback, unpaid lnurlWithdrawEntity found for this lnurlWithdrawId!"); if (lnurlWithdrawEntity.bolt11) { // Payment request has been made before... // Before falling back on-chain, let's make really sure the payment has not been done... - const paymentStatus = await this.lnFetchPaymentStatus( - lnurlWithdrawEntity.bolt11 - ); + const paymentStatus = await this.lnFetchPaymentStatus(lnurlWithdrawEntity.bolt11); if (paymentStatus.paymentStatus === undefined) { - logger.debug( - "LnurlWithdraw.forceFallback, can't get LnurlWithdraw previously paid status!" - ); + logger.debug("LnurlWithdraw.forceFallback, can't get LnurlWithdraw previously paid status!"); response.error = { code: ErrorCodes.InvalidRequest, @@ -1199,30 +926,21 @@ class LnurlWithdraw { }; } else { if (paymentStatus.paymentStatus !== "failed") { - logger.debug( - "LnurlWithdraw.forceFallback, LnurlWithdraw payment already " + - paymentStatus.paymentStatus - ); + logger.debug("LnurlWithdraw.forceFallback, LnurlWithdraw payment already " + paymentStatus.paymentStatus); response.error = { code: ErrorCodes.InvalidRequest, - message: - "LnurlWithdraw payment already " + - paymentStatus.paymentStatus, + message: "LnurlWithdraw payment already " + paymentStatus.paymentStatus, }; - lnurlWithdrawEntity.withdrawnDetails = JSON.stringify( - paymentStatus.result - ); + lnurlWithdrawEntity.withdrawnDetails = JSON.stringify(paymentStatus.result); // lnurlWithdrawEntity.withdrawnTs = new Date(); if (paymentStatus.paymentStatus === "complete") { // We set status to paid only if completed... not when pending! lnurlWithdrawEntity.paid = true; } - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); this.checkWebhook(lnurlWithdrawEntity); } @@ -1233,13 +951,9 @@ class LnurlWithdraw { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); lnurlWithdrawEntity.expiresAt = yesterday; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); - const lnurlDecoded = await Utils.decodeBech32( - lnurlWithdrawEntity?.lnurl || "" - ); + const lnurlDecoded = await Utils.decodeBech32(lnurlWithdrawEntity?.lnurl || ""); response.result = Object.assign(lnurlWithdrawEntity, { lnurlDecoded, @@ -1247,9 +961,7 @@ class LnurlWithdraw { } } else { // LnurlWithdraw already paid - logger.debug( - "LnurlWithdraw.forceFallback, LnurlWithdraw already paid." - ); + logger.debug("LnurlWithdraw.forceFallback, LnurlWithdraw already paid."); response.error = { code: ErrorCodes.InvalidRequest, @@ -1258,9 +970,7 @@ class LnurlWithdraw { } } else { // LnurlWithdraw already deactivated - logger.debug( - "LnurlWithdraw.forceFallback, LnurlWithdraw already deactivated." - ); + logger.debug("LnurlWithdraw.forceFallback, LnurlWithdraw already deactivated."); response.error = { code: ErrorCodes.InvalidRequest, @@ -1269,9 +979,7 @@ class LnurlWithdraw { } } else { // There is an error with inputs - logger.debug( - "LnurlWithdraw.forceFallback, there is an error with inputs." - ); + logger.debug("LnurlWithdraw.forceFallback, there is an error with inputs."); response.error = { code: ErrorCodes.InvalidRequest, @@ -1311,9 +1019,7 @@ class LnurlWithdraw { // } // } - let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawByBatchRequestId( - webhookBody.batchRequestId - ); + let lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawByBatchRequestId(webhookBody.batchRequestId); const result: IResponseMessage = { id: webhookBody.id, @@ -1333,9 +1039,7 @@ class LnurlWithdraw { lnurlWithdrawEntity.withdrawnTs = new Date(); lnurlWithdrawEntity.paid = true; lnurlWithdrawEntity.fallbackDone = true; - lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw( - lnurlWithdrawEntity - ); + lnurlWithdrawEntity = await this._lnurlDB.saveLnurlWithdraw(lnurlWithdrawEntity); this.checkWebhook(lnurlWithdrawEntity); diff --git a/src/lib/Scheduler.ts b/src/lib/Scheduler.ts index 701c6f9..2a0a128 100644 --- a/src/lib/Scheduler.ts +++ b/src/lib/Scheduler.ts @@ -21,10 +21,7 @@ class Scheduler { logger.info("Scheduler.checkCallbacksTimeout"); scheduler._cbStartedAt = new Date().getTime(); - logger.debug( - "Scheduler.checkCallbacksTimeout this._cbStartedAt =", - scheduler._cbStartedAt - ); + logger.debug("Scheduler.checkCallbacksTimeout this._cbStartedAt =", scheduler._cbStartedAt); // lnurlWithdraw.processCallbacks(undefined); const postdata = { @@ -33,10 +30,7 @@ class Scheduler { }; Utils.post( - scheduler._lnurlConfig.URL_API_SERVER + - ":" + - scheduler._lnurlConfig.URL_API_PORT + - scheduler._lnurlConfig.URL_API_CTX, + scheduler._lnurlConfig.URL_API_SERVER + ":" + scheduler._lnurlConfig.URL_API_PORT + scheduler._lnurlConfig.URL_API_CTX, postdata ).then((res) => { logger.debug("Scheduler.checkCallbacksTimeout, res=", res); @@ -47,10 +41,7 @@ class Scheduler { logger.info("Scheduler.checkFallbacksTimeout"); scheduler._fbStartedAt = new Date().getTime(); - logger.debug( - "Scheduler.checkFallbacksTimeout this._fbStartedAt =", - scheduler._fbStartedAt - ); + logger.debug("Scheduler.checkFallbacksTimeout this._fbStartedAt =", scheduler._fbStartedAt); // lnurlWithdraw.processFallbacks(); const postdata = { @@ -59,10 +50,7 @@ class Scheduler { }; Utils.post( - scheduler._lnurlConfig.URL_API_SERVER + - ":" + - scheduler._lnurlConfig.URL_API_PORT + - scheduler._lnurlConfig.URL_API_CTX, + scheduler._lnurlConfig.URL_API_SERVER + ":" + scheduler._lnurlConfig.URL_API_PORT + scheduler._lnurlConfig.URL_API_CTX, postdata ).then((res) => { logger.debug("Scheduler.checkFallbacksTimeout, res=", res); diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts index f8de7ab..03e1db8 100644 --- a/src/lib/Utils.ts +++ b/src/lib/Utils.ts @@ -10,13 +10,7 @@ class Utils { addedOptions?: unknown // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { - logger.info( - "Utils.request", - method, - url, - JSON.stringify(postdata), - addedOptions - ); + logger.info("Utils.request", method, url, JSON.stringify(postdata), addedOptions); let configs: AxiosRequestConfig = { baseURL: url, @@ -29,10 +23,7 @@ class Utils { try { const response = await axios.request(configs); - logger.debug( - "Utils.request :: response.data =", - JSON.stringify(response.data) - ); + logger.debug("Utils.request :: response.data =", JSON.stringify(response.data)); return { status: response.status, data: response.data }; } catch (err) { @@ -42,18 +33,9 @@ class Utils { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx - logger.info( - "Utils.request :: error.response.data =", - JSON.stringify(error.response.data) - ); - logger.info( - "Utils.request :: error.response.status =", - error.response.status - ); - logger.info( - "Utils.request :: error.response.headers =", - error.response.headers - ); + logger.info("Utils.request :: error.response.data =", JSON.stringify(error.response.data)); + logger.info("Utils.request :: error.response.status =", error.response.status); + logger.info("Utils.request :: error.response.headers =", error.response.headers); return { status: error.response.status, data: error.response.data }; } else if (error.request) { @@ -93,11 +75,7 @@ class Utils { static async encodeBech32(str: string): Promise { logger.info("Utils.encodeBech32:", str); - const lnurlBech32 = bech32.encode( - "LNURL", - bech32.toWords(Buffer.from(str, "utf8")), - 2000 - ); + const lnurlBech32 = bech32.encode("LNURL", bech32.toWords(Buffer.from(str, "utf8")), 2000); logger.debug("lnurlBech32:", lnurlBech32); return lnurlBech32.toUpperCase(); @@ -106,9 +84,7 @@ class Utils { static async decodeBech32(str: string): Promise { logger.info("Utils.decodeBech32:", str); - const lnurl = Buffer.from( - bech32.fromWords(bech32.decode(str, 2000).words) - ).toString(); + const lnurl = Buffer.from(bech32.fromWords(bech32.decode(str, 2000).words)).toString(); return lnurl; } diff --git a/src/types/IRespLnServiceWithdrawRequest.ts b/src/types/IRespLnServiceWithdrawRequest.ts index 0001f7c..09f88a7 100644 --- a/src/types/IRespLnServiceWithdrawRequest.ts +++ b/src/types/IRespLnServiceWithdrawRequest.ts @@ -1,7 +1,6 @@ import IRespLnServiceStatus from "./IRespLnServiceStatus"; -export default interface IRespLnServiceWithdrawRequest - extends IRespLnServiceStatus { +export default interface IRespLnServiceWithdrawRequest extends IRespLnServiceStatus { tag?: string; callback?: string; k1?: string; diff --git a/src/types/cyphernode/IAddToBatchResult.ts b/src/types/cyphernode/IAddToBatchResult.ts index f914f77..00e2cf2 100644 --- a/src/types/cyphernode/IAddToBatchResult.ts +++ b/src/types/cyphernode/IAddToBatchResult.ts @@ -2,10 +2,7 @@ import IBatcherIdent from "./IBatcherIdent"; import IOutput from "./IOutput"; import IBatchState from "./IBatchState"; -export default interface IAddToBatchResult - extends IBatcherIdent, - IOutput, - IBatchState { +export default interface IAddToBatchResult extends IBatcherIdent, IOutput, IBatchState { // - batcherId, the id of the batcher // - outputId, the id of the added output // - nbOutputs, the number of outputs currently in the batch diff --git a/src/validators/CreateLnurlPayValidator.ts b/src/validators/CreateLnurlPayValidator.ts index c3dd7c1..07894ed 100644 --- a/src/validators/CreateLnurlPayValidator.ts +++ b/src/validators/CreateLnurlPayValidator.ts @@ -2,12 +2,7 @@ import IReqCreateLnurlPay from "../types/IReqCreateLnurlPay"; class CreateLnurlPayValidator { static validateRequest(request: IReqCreateLnurlPay): boolean { - if ( - !!request.minMsatoshi && - !!request.maxMsatoshi && - request.minMsatoshi > 0 && - request.maxMsatoshi >= request.minMsatoshi - ) { + if (!!request.minMsatoshi && !!request.maxMsatoshi && request.minMsatoshi > 0 && request.maxMsatoshi >= request.minMsatoshi) { // Mandatory maxMsatoshi at least equal to minMsatoshi return true; } diff --git a/src/validators/LnServicePayValidator.ts b/src/validators/LnServicePayValidator.ts index c94b8b7..903923f 100644 --- a/src/validators/LnServicePayValidator.ts +++ b/src/validators/LnServicePayValidator.ts @@ -2,14 +2,8 @@ import { LnurlPayEntity } from ".prisma/client"; import IReqCreateLnurlPayRequest from "../types/IReqCreateLnurlPayRequest"; class LnServicePayValidator { - static validateRequest( - lnurlPay: LnurlPayEntity, - request: IReqCreateLnurlPayRequest - ): boolean { - if ( - request.amount >= lnurlPay.minMsatoshi && - request.amount <= lnurlPay.maxMsatoshi - ) { + static validateRequest(lnurlPay: LnurlPayEntity, request: IReqCreateLnurlPayRequest): boolean { + if (request.amount >= lnurlPay.minMsatoshi && request.amount <= lnurlPay.maxMsatoshi) { // Mandatory maxMsatoshi at least equal to minMsatoshi return true; } diff --git a/src/validators/UpdateLnurlPayValidator.ts b/src/validators/UpdateLnurlPayValidator.ts index a32bac3..dc29270 100644 --- a/src/validators/UpdateLnurlPayValidator.ts +++ b/src/validators/UpdateLnurlPayValidator.ts @@ -3,10 +3,7 @@ import IReqUpdateLnurlPay from "../types/IReqUpdateLnurlPay"; class UpdateLnurlPayValidator { static validateRequest(request: IReqUpdateLnurlPay): boolean { if (request.lnurlPayId) { - if ( - (!!request.minMsatoshi || !!request.maxMsatoshi) && - (request.maxMsatoshi || 0) >= (request.minMsatoshi || 0) - ) { + if ((!!request.minMsatoshi || !!request.maxMsatoshi) && (request.maxMsatoshi || 0) >= (request.minMsatoshi || 0)) { // Mandatory maxMsatoshi at least equal to minMsatoshi return true; } From b5ce2b7eadfc3e9c9a1831f3f501751b408471f9 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 19 Dec 2024 20:24:59 +0000 Subject: [PATCH 52/52] CORS headers and small bug fix --- cypherapps/docker-compose.yaml | 8 +- package-lock.json | 4512 -------------------------------- src/lib/LnurlWithdraw.ts | 12 +- 3 files changed, 14 insertions(+), 4518 deletions(-) delete mode 100644 package-lock.json diff --git a/cypherapps/docker-compose.yaml b/cypherapps/docker-compose.yaml index 3871de5..de2dbe8 100644 --- a/cypherapps/docker-compose.yaml +++ b/cypherapps/docker-compose.yaml @@ -36,7 +36,7 @@ services: - traefik.http.routers.lnurl.entrypoints=websecure - traefik.http.routers.lnurl.tls=true - traefik.http.routers.lnurl.service=lnurl - - traefik.http.routers.lnurl.middlewares=lnurl-redirectregex@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker + - traefik.http.routers.lnurl.middlewares=lnurl-cors@docker,lnurl-redirectregex@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker # - traefik.http.routers.lnurl.middlewares=lnurl-auth@docker,lnurl-allowedhosts@docker # PathPrefix won't be stripped because we don't want outsiders to access /api, only /lnurl/... @@ -62,6 +62,8 @@ services: - traefik.http.middlewares.lnurl-ratelimit.ratelimit.period=1s - traefik.http.middlewares.lnurl-ratelimit.ratelimit.average=1 - traefik.http.middlewares.lnurl-ratelimit.ratelimit.burst=2 + - "traefik.http.middlewares.lnurl-cors.headers.accesscontrolallowmethods=GET,OPTIONS,POST" + - "traefik.http.middlewares.lnurl-cors.headers.accesscontrolalloworiginlist=*" deploy: labels: - traefik.enable=true @@ -78,7 +80,7 @@ services: - traefik.http.routers.lnurl.entrypoints=websecure - traefik.http.routers.lnurl.tls=true - traefik.http.routers.lnurl.service=lnurl - - traefik.http.routers.lnurl.middlewares=lnurl-redirectregex@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker + - traefik.http.routers.lnurl.middlewares=lnurl-cors@docker,lnurl-redirectregex@docker,lnurl-ratelimit@docker,lnurl-stripprefix@docker # - traefik.http.routers.lnurl.middlewares=lnurl-auth@docker,lnurl-allowedhosts@docker - traefik.http.routers.lnurl-onion.rule=PathPrefix(`/lnurl`) @@ -103,6 +105,8 @@ services: - traefik.http.middlewares.lnurl-ratelimit.ratelimit.period=1s - traefik.http.middlewares.lnurl-ratelimit.ratelimit.average=1 - traefik.http.middlewares.lnurl-ratelimit.ratelimit.burst=2 + - "traefik.http.middlewares.lnurl-cors.headers.accesscontrolallowmethods=GET,OPTIONS,POST" + - "traefik.http.middlewares.lnurl-cors.headers.accesscontrolalloworiginlist=*" replicas: 1 placement: constraints: diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 8aad969..0000000 --- a/package-lock.json +++ /dev/null @@ -1,4512 +0,0 @@ -{ - "name": "lnurl", - "version": "0.1.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "lnurl", - "version": "0.1.0", - "license": "MIT", - "dependencies": { - "@prisma/client": "^3.2.1", - "@types/async-lock": "^1.1.2", - "async-lock": "^1.2.4", - "axios": "^0.21.1", - "bech32": "^2.0.0", - "date-fns": "^2.23.0", - "express": "^4.17.1", - "http-status-codes": "^1.4.0", - "prisma": "^3.2.1", - "reflect-metadata": "^0.1.13", - "tslog": "^3.2.0" - }, - "devDependencies": { - "@types/express": "^4.17.6", - "@types/node": "^13.13.52", - "@typescript-eslint/eslint-plugin": "^2.24.0", - "@typescript-eslint/parser": "^2.24.0", - "eslint": "^6.8.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-prettier": "^3.1.4", - "prettier": "2.0.5", - "rimraf": "^3.0.2", - "ts-node": "^8.10.2", - "typescript": "^4.1.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@prisma/client": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.15.2.tgz", - "integrity": "sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==", - "hasInstallScript": true, - "dependencies": { - "@prisma/engines-version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" - }, - "engines": { - "node": ">=12.6" - }, - "peerDependencies": { - "prisma": "*" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - } - } - }, - "node_modules/@prisma/engines": { - "version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz", - "integrity": "sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==", - "hasInstallScript": true - }, - "node_modules/@prisma/engines-version": { - "version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz", - "integrity": "sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==" - }, - "node_modules/@types/async-lock": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.3.tgz", - "integrity": "sha512-UpeDcjGKsYEQMeqEbfESm8OWJI305I7b9KE4ji3aBjoKWyN5CTdn8izcA1FM1DVDne30R5fNEnIy89vZw5LXJQ==" - }, - "node_modules/@types/body-parser": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", - "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, - "node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", - "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", - "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", - "dev": true - }, - "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true - }, - "node_modules/@types/node": { - "version": "13.13.52", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", - "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==", - "dev": true - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true - }, - "node_modules/@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", - "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/experimental-utils": "2.34.0", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "tsutils": "^3.17.1" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^2.0.0", - "eslint": "^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", - "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", - "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", - "dev": true, - "dependencies": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.34.0", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", - "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dependencies": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/async-lock": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.0.tgz", - "integrity": "sha512-8A7SkiisnEgME2zEedtDYPxUPzdv3x//E7n5IFktPAtMYSEAV7eNJF0rMwrVyUFj6d/8rgajLantbjcNRQYXIg==" - }, - "node_modules/axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", - "dependencies": { - "follow-redirects": "^1.10.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" - }, - "node_modules/body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dependencies": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "node_modules/bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/date-fns": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", - "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==", - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", - "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", - "dev": true, - "dependencies": { - "get-stdin": "^6.0.0" - }, - "bin": { - "eslint-config-prettier-check": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=3.14.1" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz", - "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "eslint": ">=5.0.0", - "prettier": ">=1.13.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint/node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/eslint/node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "engines": { - "node": ">=6.5.0" - } - }, - "node_modules/eslint/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "dependencies": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dependencies": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "dependencies": { - "flat-cache": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", - "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "node_modules/get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "dependencies": { - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-status-codes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-1.4.0.tgz", - "integrity": "sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ==" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lru-cache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", - "dependencies": { - "mime-db": "1.48.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/prisma": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.15.2.tgz", - "integrity": "sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==", - "hasInstallScript": true, - "dependencies": { - "@prisma/engines": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" - }, - "bin": { - "prisma": "build/index.js", - "prisma2": "build/index.js" - }, - "engines": { - "node": ">=12.6" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "node_modules/serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/table/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/table/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/table/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/table/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/table/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ts-node": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", - "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", - "dev": true, - "dependencies": { - "arg": "^4.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.17", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "typescript": ">=2.7" - } - }, - "node_modules/tslog": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.2.0.tgz", - "integrity": "sha512-xOCghepl5w+wcI4qXI7vJy6c53loF8OoC/EuKz1ktAPMtltEDz00yo1poKuyBYIQaq4ZDYKYFPD9PfqVrFXh0A==", - "dependencies": { - "source-map-support": "^0.5.19" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", - "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@prisma/client": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.15.2.tgz", - "integrity": "sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==", - "requires": { - "@prisma/engines-version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" - } - }, - "@prisma/engines": { - "version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz", - "integrity": "sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==" - }, - "@prisma/engines-version": { - "version": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz", - "integrity": "sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==" - }, - "@types/async-lock": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.3.tgz", - "integrity": "sha512-UpeDcjGKsYEQMeqEbfESm8OWJI305I7b9KE4ji3aBjoKWyN5CTdn8izcA1FM1DVDne30R5fNEnIy89vZw5LXJQ==" - }, - "@types/body-parser": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", - "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", - "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/json-schema": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", - "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", - "dev": true - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true - }, - "@types/node": { - "version": "13.13.52", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", - "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==", - "dev": true - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true - }, - "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@typescript-eslint/eslint-plugin": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", - "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "2.34.0", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "tsutils": "^3.17.1" - } - }, - "@typescript-eslint/experimental-utils": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", - "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/parser": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", - "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", - "dev": true, - "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.34.0", - "@typescript-eslint/typescript-estree": "2.34.0", - "eslint-visitor-keys": "^1.1.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", - "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async-lock": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.0.tgz", - "integrity": "sha512-8A7SkiisnEgME2zEedtDYPxUPzdv3x//E7n5IFktPAtMYSEAV7eNJF0rMwrVyUFj6d/8rgajLantbjcNRQYXIg==" - }, - "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", - "requires": { - "follow-redirects": "^1.10.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "date-fns": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", - "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "eslint-config-prettier": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", - "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", - "dev": true, - "requires": { - "get-stdin": "^6.0.0" - } - }, - "eslint-plugin-prettier": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz", - "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "follow-redirects": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", - "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "http-status-codes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-1.4.0.tgz", - "integrity": "sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ==" - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" - }, - "mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", - "requires": { - "mime-db": "1.48.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "prisma": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.15.2.tgz", - "integrity": "sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==", - "requires": { - "@prisma/engines": "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e" - } - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "ts-node": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", - "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", - "dev": true, - "requires": { - "arg": "^4.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.17", - "yn": "3.1.1" - } - }, - "tslog": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.2.0.tgz", - "integrity": "sha512-xOCghepl5w+wcI4qXI7vJy6c53loF8OoC/EuKz1ktAPMtltEDz00yo1poKuyBYIQaq4ZDYKYFPD9PfqVrFXh0A==", - "requires": { - "source-map-support": "^0.5.19" - } - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typescript": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", - "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - } - } -} diff --git a/src/lib/LnurlWithdraw.ts b/src/lib/LnurlWithdraw.ts index 35d956c..0cadf27 100644 --- a/src/lib/LnurlWithdraw.ts +++ b/src/lib/LnurlWithdraw.ts @@ -107,7 +107,7 @@ class LnurlWithdraw { "://" + this._lnurlConfig.LN_SERVICE_DOMAIN + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && this._lnurlConfig.LN_SERVICE_PORT === 443) || - (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && this._lnurlConfig.LN_SERVICE_PORT === 80) + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX + @@ -285,8 +285,12 @@ class LnurlWithdraw { logger.info("LnurlWithdraw.lnServiceWithdrawRequest:", secretToken); let result: IRespLnServiceWithdrawRequest; - const lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret(secretToken); - logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, lnurlWithdrawEntity:", lnurlWithdrawEntity); + let lnurlWithdrawEntity; + + if (secretToken && secretToken !== "") { + lnurlWithdrawEntity = await this._lnurlDB.getLnurlWithdrawBySecret(secretToken); + logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, lnurlWithdrawEntity:", lnurlWithdrawEntity); + } if (lnurlWithdrawEntity == null) { logger.debug("LnurlWithdraw.lnServiceWithdrawRequest, invalid k1 value:"); @@ -311,7 +315,7 @@ class LnurlWithdraw { "://" + this._lnurlConfig.LN_SERVICE_DOMAIN + ((this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "https" && this._lnurlConfig.LN_SERVICE_PORT === 443) || - (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && this._lnurlConfig.LN_SERVICE_PORT === 80) + (this._lnurlConfig.LN_SERVICE_SCHEME.toLowerCase() === "http" && this._lnurlConfig.LN_SERVICE_PORT === 80) ? "" : ":" + this._lnurlConfig.LN_SERVICE_PORT) + this._lnurlConfig.LN_SERVICE_CTX +