From 7f8ae69e2ee5aa82906a491289d312f193b69605 Mon Sep 17 00:00:00 2001 From: Mondei1 Date: Sun, 27 Dec 2020 23:27:35 +0100 Subject: [PATCH] Live status of transaction --- Socket Events.md | 11 + config.ts | 7 + package-lock.json | 533 +++++++++++++++++++++--- package.json | 5 +- src/app.ts | 15 +- src/controllers/invoice.ts | 107 ++++- src/helper/invoiceScheduler.ts | 81 ++-- src/helper/socketio.ts | 60 +++ src/helper/types.ts | 23 +- src/models/invoice/invoice.interface.ts | 17 +- src/models/invoice/invoice.schema.ts | 57 ++- src/routes/invoice.ts | 2 +- 12 files changed, 791 insertions(+), 127 deletions(-) create mode 100644 Socket Events.md create mode 100644 src/helper/socketio.ts diff --git a/Socket Events.md b/Socket Events.md new file mode 100644 index 0000000..7eee41f --- /dev/null +++ b/Socket Events.md @@ -0,0 +1,11 @@ +# Socket events + +## Invoice status +### Requests +* `subscribe` - Subscribe to a invoices progress. Returns `true` if successful. + * `selector` - Your selector + +### Events +* `status` - Status changed (see PaymentStatus enum) +* `confirmationUpdate` - When there is a new confirmation on the transaction + * `count` - Count of confirmations \ No newline at end of file diff --git a/config.ts b/config.ts index 42bcb40..0654b82 100644 --- a/config.ts +++ b/config.ts @@ -14,6 +14,10 @@ export const config: IConfig = { http: { port: 2009, host: "0.0.0.0" + }, + transcations: { + // If a payment has been made and its value is this amount less, it would be still accepted. + acceptMargin: 0.00000001 } } /** @@ -33,5 +37,8 @@ export interface IConfig { http: { port: number, host: string + }, + transcations: { + acceptMargin: number } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c081a0a..eaa5c81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "uplata", + "name": "librepay", "version": "1.0.0", "lockfileVersion": 1, "requires": true, @@ -19,6 +19,19 @@ "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==" }, + "@sindresorhus/is": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz", + "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==" + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, "@types/argon2": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/@types/argon2/-/argon2-0.15.0.tgz", @@ -46,10 +59,16 @@ "@types/node": "*" } }, - "@types/component-emitter": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", - "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } }, "@types/connect": { "version": "3.4.34", @@ -59,15 +78,11 @@ "@types/node": "*" } }, - "@types/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" - }, "@types/cors": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz", - "integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg==" + "integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg==", + "dev": true }, "@types/dotenv": { "version": "8.2.0", @@ -109,6 +124,11 @@ "@types/range-parser": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" + }, "@types/jsonwebtoken": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", @@ -118,6 +138,14 @@ "@types/node": "*" } }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.165", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz", @@ -172,6 +200,14 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/serve-static": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", @@ -252,6 +288,11 @@ "negotiator": "0.6.2" } }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -311,11 +352,26 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "async": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -331,6 +387,14 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "bignumber.js": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", @@ -369,6 +433,11 @@ } } }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", @@ -420,11 +489,43 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -487,10 +588,20 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "concat-map": { "version": "0.0.1", @@ -552,11 +663,31 @@ "ms": "2.0.0" } }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } + } + }, "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==" }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==" + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -615,24 +746,31 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "engine.io": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.5.tgz", - "integrity": "sha512-Ri+whTNr2PKklxQkfbGjwEo+kCBUM4Qxk4wtLqLrhH+b1up2NFL9g9pjYWiCV/oazwB0rArnvF/ZmZN2ab5Hpg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", + "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", "requires": { "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", + "cookie": "0.3.1", "debug": "~4.1.0", - "engine.io-parser": "~4.0.0", + "engine.io-parser": "~2.2.0", "ws": "^7.1.2" }, "dependencies": { "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, "debug": { "version": "4.1.1", @@ -649,12 +787,67 @@ } } }, - "engine.io-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", - "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "engine.io-client": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", + "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", "requires": { - "base64-arraybuffer": "0.1.4" + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" } }, "es6-promise": { @@ -822,6 +1015,14 @@ } } }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -835,11 +1036,54 @@ "path-is-absolute": "^1.0.0" } }, + "got": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.1.tgz", + "integrity": "sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -852,6 +1096,15 @@ "toidentifier": "1.0.0" } }, + "http2-wrapper": { + "version": "1.0.0-beta.5.2", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz", + "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -868,6 +1121,11 @@ "minimatch": "^3.0.4" } }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -925,6 +1183,11 @@ "uuid": "^3.4.0" } }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -983,6 +1246,14 @@ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", "integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==" }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "requires": { + "json-buffer": "3.0.1" + } + }, "kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -1047,6 +1318,11 @@ } } }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1091,6 +1367,11 @@ "mime-db": "1.44.0" } }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1313,6 +1594,11 @@ "osenv": "^0.1.4" } }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, "npm-bundled": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", @@ -1357,6 +1643,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -1405,6 +1696,27 @@ "os-tmpdir": "^1.0.0" } }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1434,11 +1746,25 @@ "ipaddr.js": "1.9.1" } }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1490,11 +1816,24 @@ "semver": "^5.1.0" } }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==" + }, "resolve-from": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -1604,26 +1943,18 @@ "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, "socket.io": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.0.4.tgz", - "integrity": "sha512-Vj1jUoO75WGc9txWd311ZJJqS9Dr8QtNJJ7gk2r7dcM/yGe9sit7qOijQl3GAwhpBOz/W8CwkD7R6yob07nLbA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", + "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", "requires": { - "@types/cookie": "^0.4.0", - "@types/cors": "^2.8.8", - "@types/node": "^14.14.7", - "accepts": "~1.3.4", - "base64id": "~2.0.0", "debug": "~4.1.0", - "engine.io": "~4.0.0", - "socket.io-adapter": "~2.0.3", - "socket.io-parser": "~4.0.1" + "engine.io": "~3.4.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.3.0", + "socket.io-parser": "~3.4.0" }, "dependencies": { - "@types/node": { - "version": "14.14.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz", - "integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==" - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -1640,18 +1971,94 @@ } }, "socket.io-adapter": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", - "integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" + }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "socket.io-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", + "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "requires": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + } + } }, "socket.io-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.2.tgz", - "integrity": "sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", + "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", "requires": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.1.0" + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "isarray": "2.0.1" }, "dependencies": { "debug": { @@ -1662,6 +2069,11 @@ "ms": "^2.1.1" } }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1761,6 +2173,11 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -1919,11 +2336,21 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==" }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index f4d1c17..68d6d29 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "uplata", + "name": "librepay", "version": "1.0.0", "description": "Next-gen payment processor for cryptocurrencies.", "main": "index.js", @@ -26,11 +26,12 @@ "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "got": "^11.8.1", "jayson": "^3.4.4", "jsonwebtoken": "^8.5.1", "mongoose": "^5.11.8", "mysql": "^2.18.1", - "socket.io": "^3.0.4", + "socket.io": "^2.3.0", "ts-node": "^9.1.1", "typescript": "^4.1.3", "winston": "^3.3.3", diff --git a/src/app.ts b/src/app.ts index cec88fa..fefe65a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,8 @@ import * as express from 'express'; import * as rpc from 'jayson'; import * as mongoose from 'mongoose'; import * as winston from 'winston'; +import * as socketio from 'socket.io'; +import { Server } from 'http'; import { config } from '../config'; import { hashPassword, randomPepper, randomString } from './helper/crypto'; @@ -12,6 +14,7 @@ import { InvoiceScheduler } from './helper/invoiceScheduler'; import { User } from './models/user/user.model'; import { invoiceRouter } from './routes/invoice'; import { userRouter } from './routes/user'; +import { SocketManager } from './helper/socketio'; // Load .env dconfig({ debug: true, encoding: 'UTF-8' }); @@ -23,6 +26,7 @@ export const INVOICE_SECRET = process.env.INVOICE_SECRET || ""; export let rpcClient: rpc.HttpClient | undefined = undefined; export let invoiceScheduler: InvoiceScheduler | undefined = undefined; +export let socketManager: SocketManager | undefined = undefined; export let logger: winston.Logger; @@ -105,15 +109,22 @@ async function run() { invoiceScheduler = new InvoiceScheduler(); const app = express(); + const http = new Server(app); + + // Socket.io + const io = new socketio(http); + socketManager = new SocketManager(io); + app.use(express.json()); app.use(cors()); app.use(bodyParser.json({ limit: '2kb' })); - app.get('/', (req, res) => res.status(200).send('OK')); app.use('/invoice', invoiceRouter); app.use('/user', userRouter); - app.listen(config.http.port, config.http.host, () => { + app.get('/', (req, res) => res.status(200).send('OK')); + + http.listen(config.http.port, config.http.host, () => { logger.info(`HTTP server started on port ${config.http.host}:${config.http.port}`); }); diff --git a/src/controllers/invoice.ts b/src/controllers/invoice.ts index 75e0495..1aa44dd 100644 --- a/src/controllers/invoice.ts +++ b/src/controllers/invoice.ts @@ -1,9 +1,12 @@ -import { Request, Response } from "express"; -import { invoiceScheduler, INVOICE_SECRET } from "../app"; -import { CryptoUnits, FiatUnits } from "../helper/types"; -import { ICart, IInvoice } from "../models/invoice/invoice.interface"; -import { Invoice } from "../models/invoice/invoice.model"; -import { rpcClient } from '../app'; +import { Request, Response } from 'express'; +import got from 'got'; + +import { invoiceScheduler, INVOICE_SECRET, rpcClient } from '../app'; +import { randomString } from '../helper/crypto'; +import { CryptoUnits, FiatUnits, findCryptoBySymbol, PaymentStatus } from '../helper/types'; +import { ICart, IInvoice, IPaymentMethod } from '../models/invoice/invoice.interface'; +import { Invoice } from '../models/invoice/invoice.model'; +import { calculateCart } from '../models/invoice/invoice.schema'; // POST /invoice/?sercet=XYZ export async function createInvoice(req: Request, res: Response) { @@ -20,14 +23,14 @@ export async function createInvoice(req: Request, res: Response) { } } - const paymentMethods: CryptoUnits[] = req.body.methods; + const paymentMethodsRaw: string[] = req.body.methods; const successUrl: string = req.body.successUrl; const cancelUrl: string = req.body.cancelUrl; const cart: ICart[] = req.body.cart; - const currency: FiatUnits = req.body.currency; - const totalPrice: number = req.body.totalPrice; + let currency: FiatUnits = req.body.currency; + let totalPrice: number = req.body.totalPrice; - if (paymentMethods === undefined) { + if (paymentMethodsRaw === undefined) { res.status(400).send({ message: '"paymentMethods" are not provided!' }); return; } @@ -45,19 +48,52 @@ export async function createInvoice(req: Request, res: Response) { if (currency === undefined) { res.status(400).send({ message: '"currency" is not provided!' }); return; + } else { + if (Object.keys(FiatUnits).indexOf(currency.toUpperCase()) === -1) { + res.status(400).send({ message: '"currency" can only be "eur" and "usd"' }); + return; + } else { + currency = FiatUnits[currency.toUpperCase()]; + } } - /*if (cart === undefined && totalPrice === undefined) { + if (cart === undefined && totalPrice === undefined) { res.status(400).send({ message: 'Either "cart" or "totalPrice" has to be defined.' }); return; - }*/ + } rpcClient.request('getnewaddress', ['', 'bech32'], async (err, response) => { if (err) throw err; - //console.log(response.result); + // Get price + // Convert coin symbol to full text in order to query Coin Gecko. eg.: ['btc', 'xmr'] => ['bitcoin', 'monero'] + let cgFormat = []; + + paymentMethodsRaw.forEach(coin => { + const crypto = findCryptoBySymbol(coin); + + if (crypto !== undefined) { + cgFormat.push(crypto.toLowerCase()); + } + }); + + const request = await got.get(`https://api.coingecko.com/api/v3/simple/price?ids=${cgFormat.join(',')}&vs_currencies=${currency.toLowerCase()}`, { + responseType: 'json' + }); + + // Calulate total price, if cart is provided + if (cart !== undefined && totalPrice === undefined) { + totalPrice = calculateCart(cart); + } + + let paymentMethods: IPaymentMethod[] = []; + Object.keys(request.body).forEach(coin => { + paymentMethods.push({ method: CryptoUnits[coin.toUpperCase()], amount: totalPrice / Number(request.body[coin][currency.toLowerCase()]) }); + }); + Invoice.create({ - paymentMethods, + selector: randomString(128), + paymentMethods: paymentMethods, successUrl, cancelUrl, cart, @@ -72,26 +108,37 @@ export async function createInvoice(req: Request, res: Response) { } invoiceScheduler.addInvoice(invoice); - res.status(200).send({ id: invoice.id }); + res.status(200).send({ id: invoice.selector }); }); }); } // GET /invoice/ -// GET /invoice/:id +// GET /invoice/:selector export async function getInvoice(req: Request, res: Response) { - const invoiceId = req.params.id; + const selector = req.params.selector; // If an id is provided - if (invoiceId !== undefined) { - const invoice: any = await Invoice.findById(invoiceId); + if (selector !== undefined) { + const invoice: IInvoice = await Invoice.findOne({ selector: selector }); if (invoice === null) { res.status(404).send(); return; } - - res.status(200).send(invoice); + + if(invoice.status === PaymentStatus.UNCONFIRMED || invoice.status === PaymentStatus.DONE) { + rpcClient.request('gettransaction', [invoice.transcationHashes[0]], (err, message) => { + let invoiceClone: any = invoice; + console.log(message.result.confirmations); + + invoiceClone['confirmation'] = message.result.confirmations; + res.status(200).send(invoiceClone); + }); + } else { + res.status(200).send(invoice); + } + return; } @@ -117,4 +164,22 @@ export async function getInvoice(req: Request, res: Response) { .sort({ createdAt: sort }); res.status(200).send(invoices); +} + +// DELETE /invoice/:selector +export async function cancelPaymnet(req: Request, res: Response) { + const selector = req.params.selector; + + // If an id is provided + if (selector !== undefined) { + const invoice = await Invoice.findOne({ selector: selector }); + if (invoice === null) { + res.status(404).send(); + return; + } + + invoice.status = PaymentStatus.CANCELLED; + await invoice.save(); + return; + } } \ No newline at end of file diff --git a/src/helper/invoiceScheduler.ts b/src/helper/invoiceScheduler.ts index 3a1a1e1..fc71c89 100644 --- a/src/helper/invoiceScheduler.ts +++ b/src/helper/invoiceScheduler.ts @@ -1,18 +1,21 @@ import { IInvoice } from "../models/invoice/invoice.interface"; import { Subscriber } from 'zeromq'; -import { logger, rpcClient } from "../app"; +import { logger, rpcClient, socketManager } from "../app"; import { invoiceRouter } from "../routes/invoice"; import { Invoice } from "../models/invoice/invoice.model"; -import { PaymentStatus } from "./types"; +import { CryptoUnits, PaymentStatus } from "./types"; +import { config } from "../../config"; export class InvoiceScheduler { private pendingInvoices: IInvoice[]; private unconfirmedTranscations: IInvoice[]; + private knownConfirmations: Map; // Invoice id / confirmation cound private sock: Subscriber; constructor() { this.unconfirmedTranscations = []; this.pendingInvoices = []; + this.knownConfirmations = new Map(); // Get all pending transcations Invoice.find({ status: PaymentStatus.PENDING }).then(invoices => { @@ -44,7 +47,7 @@ export class InvoiceScheduler { logger.info('Now listing for incoming transaction to any invoices ...'); for await (const [topic, msg] of this.sock) { const rawtx = msg.toString('hex'); - logger.debug(`New tx: ${rawtx}`); + //logger.debug(`New tx: ${rawtx}`); rpcClient.request('decoderawtransaction', [rawtx], (err, decoded) => { if (err) { logger.error(`Error while decoding raw tx: ${err.message}`); @@ -54,19 +57,29 @@ export class InvoiceScheduler { decoded.result.vout.forEach(output => { // Loop over each output and check if the address of one matches the one of an invoice. this.pendingInvoices.forEach(invoice => { - // We found our transaction if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses + + // We found our transaction (https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html) if (output.scriptPubKey.addresses.indexOf(invoice.receiveAddress) !== -1) { + invoice.paid += output.value; logger.info(`Transcation for invoice ${invoice.id} received! (${decoded.result.hash})`); // Change state in database - invoice.status = PaymentStatus.UNCONFIRMED; - invoice.transcationHash = decoded.result.txid; - invoice.save(); - - // Push to array & remove from pending - this.unconfirmedTranscations.push(invoice); - this.pendingInvoices.splice(this.pendingInvoices.indexOf(invoice), 1); + const price = invoice.paymentMethods.find((item) => { return item.method === CryptoUnits.BITCOIN }).amount; + if (invoice.paid < price - config.transcations.acceptMargin) { + const left = price - output.value; + invoice.status = PaymentStatus.PARTIALLY; + invoice.save(); + logger.info(`Transcation for invoice ${invoice.id} received but there are still ${left} BTC missing (${decoded.result.hash})`); + } else { + invoice.status = PaymentStatus.UNCONFIRMED; + invoice.transcationHashes.push(decoded.result.txid); + invoice.save(); + + // Push to array & remove from pending + this.unconfirmedTranscations.push(invoice); + this.pendingInvoices.splice(this.pendingInvoices.indexOf(invoice), 1); + } } }) }); @@ -81,22 +94,40 @@ export class InvoiceScheduler { private watchConfirmations() { setInterval(() => { this.unconfirmedTranscations.forEach(invoice => { - rpcClient.request('gettransaction', [invoice.transcationHash], (err, message) => { - if (err) { - logger.error(`Error while fetching confirmation state of ${invoice.transcationHash}: ${err.message}`); - return; - } + if (invoice.transcationHashes.length === 0) return; + let trustworthy = true; // Will be true if all transactions are above threshold. - if (Number(message.result.confirmations) > 2) { - logger.info(`Transaction (${invoice.transcationHash}) has reached more then 2 confirmations and can now be trusted!`); - invoice.status = PaymentStatus.DONE; - invoice.save(); // This will trigger a post save hook that will notify the user. + for (let i = 0; i < invoice.transcationHashes.length; i++) { + const transcation = invoice.transcationHashes[i]; + + rpcClient.request('gettransaction', [transcation], (err, message) => { + if (err) { + logger.error(`Error while fetching confirmation state of ${transcation}: ${err.message}`); + trustworthy = false; - this.unconfirmedTranscations.splice(this.unconfirmedTranscations.indexOf(invoice), 1); - } else { - logger.debug(`Transcation (${invoice.transcationHash}) has not reached his threshold yet.`); - } - }); + return; + } + + if (this.knownConfirmations.get(invoice.id) != message.result.confirmations) { + this.knownConfirmations.set(invoice.id, message.result.confirmations); + socketManager.getSocketByInvoice(invoice).emit('confirmationUpdate', { count: Number(message.result.confirmations) }); + } + + if (Number(message.result.confirmations) > 0) { + logger.info(`Transaction (${transcation}) has reached more then 2 confirmations and can now be trusted!`); + + this.unconfirmedTranscations.splice(this.unconfirmedTranscations.indexOf(invoice), 1); + } else { + trustworthy = false; + logger.debug(`Transcation (${transcation}) has not reached his threshold yet.`); + } + }); + } + + if (trustworthy) { + invoice.status = PaymentStatus.DONE; + invoice.save(); // This will trigger a post save hook that will notify the user. + } }); }, 2_000); } diff --git a/src/helper/socketio.ts b/src/helper/socketio.ts new file mode 100644 index 0000000..75d14b8 --- /dev/null +++ b/src/helper/socketio.ts @@ -0,0 +1,60 @@ +import { Server, Socket } from "socket.io"; +import { logger } from "../app"; +import { IInvoice } from "../models/invoice/invoice.interface"; +import { Invoice } from "../models/invoice/invoice.model"; +import { PaymentStatus } from "./types"; + +export class SocketManager { + io: Server; + + private socketInvoice: Map; // Socket ID / _id + private idSocket: Map; // Socket ID / Socket + private invoiceSocket: Map; // _id / Socket + + constructor(io: Server) { + this.io = io; + this.socketInvoice = new Map(); + this.idSocket = new Map(); + this.invoiceSocket = new Map(); + this.listen(); + } + + listen() { + console.log("Listen"); + + this.io.on('connection', (socket: Socket) => { + this.idSocket.set(socket.id, socket); + + // The frontend sends his selector, then pick _id and put it in `socketInvoice` map. + // Return `true` if successful and `false` if not. + socket.on('subscribe', async data => { + if (data.selector !== undefined) { + const invoice = await Invoice.findOne({ selector: data.selector }); + if (invoice === null) { + socket.emit('subscribe', false); + return; + } + + logger.info(`Socket ${socket.id} has subscribed to invoice ${invoice.id} (${PaymentStatus[invoice.status]})`); + + this.socketInvoice.set(socket.id, invoice.id); + this.invoiceSocket.set(invoice.id, socket); + socket.emit('subscribe', true); + } + }); + }); + } + + getSocketById(id: string) { + return this.idSocket.get(id); + } + + async getInvoiceBySocket(socketId: string) { + const invoiceId = this.socketInvoice.get(socketId); + return await Invoice.findById(invoiceId); + } + + getSocketByInvoice(invoice: IInvoice) { + return this.invoiceSocket.get(invoice.id); + } +} \ No newline at end of file diff --git a/src/helper/types.ts b/src/helper/types.ts index 5508b5f..7d31938 100644 --- a/src/helper/types.ts +++ b/src/helper/types.ts @@ -7,9 +7,16 @@ export enum CryptoUnits { MONERO = 'XMR' } +export function findCryptoBySymbol(symbol: string): string | null { + for (let coin in CryptoUnits) { + if (CryptoUnits[coin] === symbol.toUpperCase()) return coin; + } + return null; +} + export enum FiatUnits { USD = 'USD', - EUR = 'EURO' + EUR = 'EUR' } export enum PaymentStatus { @@ -18,13 +25,23 @@ export enum PaymentStatus { */ PENDING = 0, + /** + * The payment has been paid, but not completly. + */ + PARTIALLY = 1, + /** * The payment has been made but it's not yet confirmed. */ - UNCONFIRMED = 1, + UNCONFIRMED = 2, /** * The payment is completed and the crypto is now available. */ - DONE = 2 + DONE = 3, + + /** + * The payment has been cancelled by the user. + */ + CANCELLED = 4 } \ No newline at end of file diff --git a/src/models/invoice/invoice.interface.ts b/src/models/invoice/invoice.interface.ts index 23fcdd6..f9bb620 100644 --- a/src/models/invoice/invoice.interface.ts +++ b/src/models/invoice/invoice.interface.ts @@ -8,19 +8,30 @@ export interface ICart { quantity: number; } +export interface IPaymentMethod { + method: CryptoUnits; + amount: number +} + export interface IInvoice extends Document { + selector: string; + // Available payment methods - // [btc, xmr, eth, doge] - paymentMethods: CryptoUnits[]; + // [{ method: 'btc', amount: 0.0000105 }] + paymentMethods: IPaymentMethod[]; // 1Kss3e9iPB9vTgWJJZ1SZNkkFKcFJXPz9t receiveAddress: string; paidWith?: CryptoUnits; + // Already paid amount, in case that not the entire amount was paid with once. + // 0.000013 + paid?: number; + // Is set when invoice got paid // 3b38c3a215d4e7981e1516b2dcbf76fca58911274d5d55b3d615274d6e10f2c1 - transcationHash?: string; + transcationHashes?: string[]; cart?: ICart[]; totalPrice?: number; diff --git a/src/models/invoice/invoice.schema.ts b/src/models/invoice/invoice.schema.ts index c2b1f22..d6128c9 100644 --- a/src/models/invoice/invoice.schema.ts +++ b/src/models/invoice/invoice.schema.ts @@ -1,6 +1,7 @@ -import { NativeError, Schema, SchemaTypes } from 'mongoose'; +import { Schema } from 'mongoose'; +import { socketManager } from '../../app'; import { CryptoUnits, FiatUnits, PaymentStatus } from '../../helper/types'; -import { IInvoice } from './invoice.interface'; +import { ICart, IInvoice } from './invoice.interface'; const urlRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ @@ -9,16 +10,23 @@ const schemaCart = new Schema({ name: { type: String, trim: true, required: true }, image: { type: String, match: urlRegex, required: true }, quantity: { type: Number, default: 1 } -}) +}, { _id: false }); + +const schemaPaymentMethods = new Schema({ + method: { type: String, enum: Object.values(CryptoUnits), required: true }, + amount: { type: Number, required: false } +}, { _id: false }); const schemaInvoice = new Schema({ - paymentMethods: [{ type: String, enum: Object.values(CryptoUnits), default: [CryptoUnits.BITCOIN], required: true }], + selector: { type: String, length: 128, required: true }, + paymentMethods: [{ type: schemaPaymentMethods, required: true }], receiveAddress: { type: String, required: true }, paidWith: { type: String, enum: CryptoUnits }, - transcationHash: { type: String, required: false }, + paid: { type: Number, default: 0 }, + transcationHashes: [{ type: String, required: false }], cart: [{ type: schemaCart, required: false }], totalPrice: { type: Number, required: false }, - currency: { type: String, enum: Object.values(FiatUnits), required: false }, + currency: { type: String, enum: Object.values(FiatUnits), required: true }, dueBy: { type: Number, required: true }, status: { type: Number, enum: Object.values(PaymentStatus), default: PaymentStatus.PENDING }, email: { type: String, required: false }, @@ -31,8 +39,15 @@ const schemaInvoice = new Schema({ versionKey: false }); +schemaInvoice.pre('validate', function(next) { + let self = this as IInvoice; + self.currency = FiatUnits[self.currency]; + + next(); +}); + // Validate values -schemaInvoice.post('validate', function (res, next) { +schemaInvoice.post('validate', function (doc, next) { let self = this as IInvoice; // If cart is undefined and price too, error. @@ -40,19 +55,27 @@ schemaInvoice.post('validate', function (res, next) { next(new Error('Either cart or price has to be defined!')); return; } + + next(); +}); - // If cart is provided, calculate price. - if (self.cart !== undefined && self.totalPrice === undefined) { - let totalPrice = 0; - - for (let i = 0; i < self.cart.length; i++) { - const item = self.cart[i]; - totalPrice += item.price * item.quantity; - } +schemaInvoice.post('save', function(doc, next) { + let self = this as IInvoice; - self.set({ totalPrice }); - } + if (socketManager.getSocketByInvoice(self) === undefined) return; + socketManager.getSocketByInvoice(self).emit('status', self.status); next(); }) +export function calculateCart(cart: ICart[]): number { + let totalPrice = 0; + + for (let i = 0; i < cart.length; i++) { + const item = cart[i]; + totalPrice += item.price * item.quantity; + } + + return totalPrice; +} + export { schemaInvoice } \ No newline at end of file diff --git a/src/routes/invoice.ts b/src/routes/invoice.ts index 768e09f..3daf144 100644 --- a/src/routes/invoice.ts +++ b/src/routes/invoice.ts @@ -4,7 +4,7 @@ import { MW_User } from "../controllers/user"; const invoiceRouter = Router() -invoiceRouter.get('/:id', getInvoice); +invoiceRouter.get('/:selector', getInvoice); invoiceRouter.get('/', MW_User, getInvoice); invoiceRouter.post('/', MW_User, createInvoice);