diff --git a/web/cashtab/package-lock.json b/web/cashtab/package-lock.json --- a/web/cashtab/package-lock.json +++ b/web/cashtab/package-lock.json @@ -28,6 +28,7 @@ "buffer": "^6.0.3", "camelcase": "^6.2.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", + "chronik-client": "^0.4.0", "crypto-browserify": "^3.12.0", "css-loader": "^6.5.1", "css-minimizer-webpack-plugin": "^3.2.0", @@ -52,6 +53,7 @@ "localforage": "^1.9.0", "lodash.isempty": "^4.4.0", "lodash.isequal": "^4.5.0", + "long": "^5.2.0", "mini-css-extract-plugin": "^2.4.5", "minimal-slp-wallet": "^3.3.1", "postcss": "^8.4.4", @@ -2988,6 +2990,60 @@ "node": ">= 8" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@psf/bch-js": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-4.21.0.tgz", @@ -3871,6 +3927,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -6274,6 +6335,66 @@ "node": ">=6.0" } }, + "node_modules/chronik-client": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chronik-client/-/chronik-client-0.4.0.tgz", + "integrity": "sha512-VgxGHPYN3rlRcn7g1gEh562Tj/2dEDlXQnbhU3qQYgfFIRnBt80TP4Rq2raek7nUy+TIen0d4JK7zxoSK5Kauw==", + "dependencies": { + "@types/ws": "^8.2.1", + "axios": "^0.21.1", + "isomorphic-ws": "^4.0.1", + "long": "^5.2.0", + "protobufjs": "^6.11.2", + "ws": "^8.3.0" + } + }, + "node_modules/chronik-client/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/chronik-client/node_modules/follow-redirects": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", + "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/chronik-client/node_modules/ws": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", + "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/ci-info": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", @@ -10695,6 +10816,14 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -13028,6 +13157,11 @@ "triple-beam": "^1.3.0" } }, + "node_modules/long": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", + "integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -15463,6 +15597,36 @@ "node": ">=4.0.0" } }, + "node_modules/protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -22066,6 +22230,60 @@ } } }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "@psf/bch-js": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-4.21.0.tgz", @@ -22722,6 +22940,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -24572,6 +24795,40 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" }, + "chronik-client": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chronik-client/-/chronik-client-0.4.0.tgz", + "integrity": "sha512-VgxGHPYN3rlRcn7g1gEh562Tj/2dEDlXQnbhU3qQYgfFIRnBt80TP4Rq2raek7nUy+TIen0d4JK7zxoSK5Kauw==", + "requires": { + "@types/ws": "^8.2.1", + "axios": "^0.21.1", + "isomorphic-ws": "^4.0.1", + "long": "^5.2.0", + "protobufjs": "^6.11.2", + "ws": "^8.3.0" + }, + "dependencies": { + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + }, + "follow-redirects": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", + "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==" + }, + "ws": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", + "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "requires": {} + } + } + }, "ci-info": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", @@ -27864,6 +28121,12 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "requires": {} + }, "istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -29603,6 +29866,11 @@ "triple-beam": "^1.3.0" } }, + "long": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", + "integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -31270,6 +31538,33 @@ "retry": "^0.10.0" } }, + "protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "dependencies": { + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/web/cashtab/package.json b/web/cashtab/package.json --- a/web/cashtab/package.json +++ b/web/cashtab/package.json @@ -23,6 +23,7 @@ "buffer": "^6.0.3", "camelcase": "^6.2.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", + "chronik-client": "^0.4.0", "crypto-browserify": "^3.12.0", "css-loader": "^6.5.1", "css-minimizer-webpack-plugin": "^3.2.0", @@ -47,6 +48,7 @@ "localforage": "^1.9.0", "lodash.isempty": "^4.4.0", "lodash.isequal": "^4.5.0", + "long": "^5.2.0", "mini-css-extract-plugin": "^2.4.5", "minimal-slp-wallet": "^3.3.1", "postcss": "^8.4.4", diff --git a/web/cashtab/src/components/Common/Notifications.js b/web/cashtab/src/components/Common/Notifications.js --- a/web/cashtab/src/components/Common/Notifications.js +++ b/web/cashtab/src/components/Common/Notifications.js @@ -144,6 +144,33 @@ }); }; +const xecReceivedNotificationWebsocket = ( + xecAmount, + cashtabSettings, + fiatPrice, +) => { + const notificationStyle = getDeviceNotificationStyle(); + notification.success({ + message: 'eCash received', + description: ( + + + {xecAmount.toLocaleString()} {currency.ticker}{' '} + {cashtabSettings && + cashtabSettings.fiatCurrency && + `(${ + currency.fiatCurrencies[cashtabSettings.fiatCurrency] + .symbol + }${(xecAmount * fiatPrice).toFixed( + currency.cashDecimals, + )} ${cashtabSettings.fiatCurrency.toUpperCase()})`} + + ), + duration: currency.notificationDurationShort, + icon: , + style: notificationStyle, + }); +}; + const eTokenReceivedNotification = ( currency, receivedSlpTicker, @@ -202,6 +229,7 @@ tokenIconSubmitSuccess, sendTokenNotification, xecReceivedNotification, + xecReceivedNotificationWebsocket, eTokenReceivedNotification, errorNotification, messageSignedNotification, diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js --- a/web/cashtab/src/hooks/useWallet.js +++ b/web/cashtab/src/hooks/useWallet.js @@ -13,6 +13,7 @@ addNewHydratedUtxos, removeConsumedUtxos, areAllUtxosIncludedInIncrementallyHydratedUtxos, + parseChronikTx, } from 'utils/cashMethods'; import { isValidCashtabSettings, isValidContactList } from 'utils/validation'; import localforage from 'localforage'; @@ -21,10 +22,17 @@ import isEqual from 'lodash.isequal'; import { xecReceivedNotification, + xecReceivedNotificationWebsocket, eTokenReceivedNotification, } from 'components/Common/Notifications'; + +import { ChronikClient } from 'chronik-client'; +// For XEC, eCash chain: +const chronik = new ChronikClient('https://chronik.be.cash/xec'); + const useWallet = () => { const [wallet, setWallet] = useState(false); + const [chronikWebsocket, setChronikWebsocket] = useState(null); const [contactList, setContactList] = useState(false); const [cashtabSettings, setCashtabSettings] = useState(false); const [fiatPrice, setFiatPrice] = useState(null); @@ -1207,6 +1215,101 @@ } }; + // Parse chronik ws message for incoming tx notifications + const processChronikWsMsg = async msg => { + console.log(`processChronikWsMsg called with`, msg); + // get txid info + const txid = msg.txid; + console.log(`txid`, txid); + const txDetails = await chronik.tx(txid); + console.log(`txDetails`, txDetails); + // parse tx for notification + const parsedChronikTx = parseChronikTx(txDetails, [ + BCH.Address.toHash160(wallet.Path245.cashAddress), + BCH.Address.toHash160(wallet.Path145.cashAddress), + BCH.Address.toHash160(wallet.Path1899.cashAddress), + ]); + console.log(`parsedChronikTx`, parsedChronikTx); + if (parsedChronikTx.incoming) { + if (parsedChronikTx.isEtokenTx) { + //notification + console.log(`eToken notification would be fired here`); + } else { + xecReceivedNotificationWebsocket( + parsedChronikTx.xecAmount, + cashtabSettings, + fiatPrice, + ); + } + } + + // incoming? notification + recalc utxo set. outgoing? recalc utxo set + }; + + // Chronik websockets + const initializeWebsocket = async wallet => { + console.log(`initializeWebsocket called with`, wallet.name); + console.log( + `initializeWebsocket called with chronikWebsocket in state`, + chronikWebsocket, + ); + // Because wallet is set to `false` before it is loaded, do nothing if you find this case + if (!wallet) { + return setChronikWebsocket(null); + } + + // Initialize if not in state + let ws = chronikWebsocket; + if (ws === null) { + ws = chronik.ws({ + onMessage: async msg => { + processChronikWsMsg(msg); + }, + onReconnect: e => { + // Fired before a reconnect attempt is made: + console.log( + 'Reconnecting websocket, disconnection cause: ', + e, + ); + }, + }); + // Wait for WS to be connected: + await ws.waitForOpen(); + console.log(`websocket open`); + } + + // Real method will need to iterate through wallet object + const hash160Array = [ + BCH.Address.toHash160(wallet.Path245.cashAddress), // will be .hash160 + BCH.Address.toHash160(wallet.Path145.cashAddress), + BCH.Address.toHash160(wallet.Path1899.cashAddress), + ]; + console.log(`hash160Array`, hash160Array); + + // Unsubscribe to any active subscriptions + const previousWebsocketSubscriptions = ws._subs; + console.log( + `previousWebsocketSubscriptions`, + previousWebsocketSubscriptions, + ); + if (previousWebsocketSubscriptions.length > 0) { + for (let i = 0; i < previousWebsocketSubscriptions.length; i += 1) { + const unsubHash160 = + previousWebsocketSubscriptions[i].scriptPayload; + ws.unsubscribe('p2pkh', unsubHash160); + console.log(`ws.unsubscribe('p2pkh', ${unsubHash160})`); + } + } + + // Subscribe to addresses of current wallet + for (let i = 0; i < hash160Array.length; i += 1) { + ws.subscribe('p2pkh', hash160Array[i]); + console.log(`ws.subscribe('p2pkh', ${hash160Array[i]})`); + } + + return setChronikWebsocket(ws); + }; + useEffect(async () => { handleUpdateWallet(setWallet); await loadContactList(); @@ -1214,6 +1317,10 @@ initializeFiatPriceApi(initialSettings.fiatCurrency); }, []); + useEffect(async () => { + await initializeWebsocket(wallet); + }, [wallet.mnemonic]); + return { BCH, wallet, diff --git a/web/cashtab/src/utils/__mocks__/chronikWebsocketTxs.js b/web/cashtab/src/utils/__mocks__/chronikWebsocketTxs.js new file mode 100644 --- /dev/null +++ b/web/cashtab/src/utils/__mocks__/chronikWebsocketTxs.js @@ -0,0 +1,316 @@ +export const lambdaHash160s = [ + '58549b5b93428fac88e36617456cd99a411bd0eb', + '438a162355ef683062a7fde9d08dd720397aaee8', + '76458db0ed96fe9863fc1ccec9fa2cfab884b0f6', +]; + +export const lambdaIncomingXecTx = { + txid: 'ac83faac54059c89c41dea4c3d6704e4f74fb82e4ad2fb948e640f1d19b760de', + version: 2, + inputs: [ + { + prevOut: { + txid: '783428349b7b040b473ca9720ddbb2eda6fe28db16883ae47f3113b7a0977915', + outIdx: 1, + }, + inputScript: + '48304502210094c497d6a0ce9ca6d79819467a1bb3953084b2e003ac7edac3b4f0634800baab02205729e229bd96d3a35cece712e3e9ec2d3f610a43d7712928f806983f209fbd72412103318d0e1109f32debc66952d0e3ec21b1cf96575ea4c2a97a6535628f7f8b10e6', + outputScript: '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + value: { + low: 517521, + high: 0, + unsigned: false, + }, + sequenceNo: 4294967295, + }, + ], + outputs: [ + { + value: { low: 4200, high: 0, unsigned: false }, + outputScript: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + }, + { + value: { + low: 512866, + high: 0, + unsigned: false, + }, + outputScript: '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + }, + ], + lockTime: 0, + timeFirstSeen: { + low: 1652811898, + high: 0, + unsigned: false, + }, + network: 'XEC', +}; +export const lambdaIncomingEtokenTx = { + txid: '46cf8bf009dbc6da45045c23af878cd2fd6dd3d3f62bf524d675e75959d5fdbd', + version: 2, + inputs: [ + { + prevOut: { + txid: '51c18b220c2ff1d3ead60c3031316f15ed1c7fa43fbfe563c8227e107f218751', + outIdx: 1, + }, + inputScript: + '473044022004db23a179194d5e2d8446159859a3e55521239c807f14d4666c772d1493a7d402206d6ea22a4fb8ef20cd6159d200a7292a3ff0181c8d596e7a3e1b9027e6912103412103318d0e1109f32debc66952d0e3ec21b1cf96575ea4c2a97a6535628f7f8b10e6', + outputScript: '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + value: { + low: 3891539, + high: 0, + unsigned: false, + }, + sequenceNo: 4294967295, + }, + { + prevOut: { + txid: '66f0663e79f6a7fa3bf0834a16b48cb86fa42076c0df25ae89b402d5ee97c311', + outIdx: 2, + }, + inputScript: + '483045022100c45951e15402b907c419f8a80bd76d374521faf885327ba3e55021345c2eb41902204cdb84e0190a5f671dd049b6b656f6b9e8b57254ec0123308345d5a634802acd412103318d0e1109f32debc66952d0e3ec21b1cf96575ea4c2a97a6535628f7f8b10e6', + outputScript: '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + value: { + low: 546, + high: 0, + unsigned: false, + }, + sequenceNo: 4294967295, + slpToken: { + amount: { + low: 240, + high: 0, + unsigned: true, + }, + isMintBaton: false, + }, + }, + ], + outputs: [ + { + value: { + low: 0, + high: 0, + unsigned: false, + }, + outputScript: + '6a04534c500001010453454e44204bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c308000000000000000c0800000000000000e4', + }, + { + value: { + low: 546, + high: 0, + unsigned: false, + }, + outputScript: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + slpToken: { + amount: { + low: 12, + high: 0, + unsigned: true, + }, + isMintBaton: false, + }, + }, + { + value: { + low: 546, + high: 0, + unsigned: false, + }, + outputScript: '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + slpToken: { + amount: { + low: 228, + high: 0, + unsigned: true, + }, + isMintBaton: false, + }, + }, + { + value: { + low: 3889721, + high: 0, + unsigned: false, + }, + outputScript: '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + }, + ], + lockTime: 0, + slpTxData: { + slpMeta: { + tokenType: 'FUNGIBLE', + txType: 'SEND', + tokenId: + '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + }, + }, + timeFirstSeen: { + low: 1652822000, + high: 0, + unsigned: false, + }, + network: 'XEC', +}; + +export const lambdaOutgoingXecTx = { + txid: 'b82a67f929d256c9beb04a850ad735f3b322156cc9df2e37cadc130cc4fab660', + version: 2, + inputs: [ + { + prevOut: { + txid: 'bb161d20f884ce45374fa3f9f1452290a2e52e93c8b552f559fad8ccd1ca33cc', + outIdx: 5, + }, + inputScript: + '473044022054a6b2065a0b0bbe70048e782aa9be048cc8bee0a241d08d0b98fcd74505a90202201ed5224f34c9ff73dc0c581390247686af521476a977a58e55ed33c4afd177c2412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', + outputScript: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + value: { + low: 4400000, + high: 0, + unsigned: false, + }, + sequenceNo: 4294967295, + }, + ], + outputs: [ + { + value: { + low: 22200, + high: 0, + unsigned: false, + }, + outputScript: '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + }, + { + value: { + low: 4377345, + high: 0, + unsigned: false, + }, + outputScript: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + }, + ], + lockTime: 0, + timeFirstSeen: { + low: 1652823464, + high: 0, + unsigned: false, + }, + network: 'XEC', +}; + +export const lambdaOutgoingEtokenTx = { + txid: '3d60d2d130eee3e45e6a2d0e88e2ecae82d70c1ed1afc8f62ca9c8564d38108d', + version: 2, + inputs: [ + { + prevOut: { + txid: 'bf7a7d1a063751d8f9c67e88523b3e6ffe8bb133e54ebf3cf500b859adfe16e0', + outIdx: 1, + }, + inputScript: + '473044022047077b516d8554aba4deb36c66b789b5136bf16657bf1675ae866fd8a62834f5022035a7bd45422e0d0c343ac832a5efb0c05269ebe591ea400a33c23849cfa7c3a0412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', + outputScript: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + value: { + low: 450747149, + high: 0, + unsigned: false, + }, + sequenceNo: 4294967295, + }, + { + prevOut: { + txid: '66f0663e79f6a7fa3bf0834a16b48cb86fa42076c0df25ae89b402d5ee97c311', + outIdx: 1, + }, + inputScript: + '47304402203ba0eff663f253805a4ae75fecf5886d7dbaf6369c9e6f0bbf5c114184223fa202207992c5f1a8cb69b552b1af54a75bbab341bfcf90591e535282bd9409981d8464412102c237f49dd4c812f27b09d69d4c8a4da12744fda8ad63ce151fed2a3f41fd8795', + outputScript: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + value: { + low: 546, + high: 0, + unsigned: false, + }, + sequenceNo: 4294967295, + slpToken: { + amount: { + low: 69, + high: 0, + unsigned: true, + }, + isMintBaton: false, + }, + }, + ], + outputs: [ + { + value: { + low: 0, + high: 0, + unsigned: false, + }, + outputScript: + '6a04534c500001010453454e44204bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3080000000000000011080000000000000034', + }, + { + value: { + low: 546, + high: 0, + unsigned: false, + }, + outputScript: '76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac', + slpToken: { + amount: { + low: 17, + high: 0, + unsigned: true, + }, + isMintBaton: false, + }, + }, + { + value: { + low: 546, + high: 0, + unsigned: false, + }, + outputScript: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + slpToken: { + amount: { + low: 52, + high: 0, + unsigned: true, + }, + isMintBaton: false, + }, + }, + { + value: { + low: 450745331, + high: 0, + unsigned: false, + }, + outputScript: '76a91476458db0ed96fe9863fc1ccec9fa2cfab884b0f688ac', + }, + ], + lockTime: 0, + slpTxData: { + slpMeta: { + tokenType: 'FUNGIBLE', + txType: 'SEND', + tokenId: + '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + }, + }, + timeFirstSeen: { + low: 1652823534, + high: 0, + unsigned: false, + }, + network: 'XEC', +}; diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js --- a/web/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js @@ -24,6 +24,8 @@ getUtxoCount, areAllUtxosIncludedInIncrementallyHydratedUtxos, convertEcashtoEtokenAddr, + parseChronikTx, + addTypeToLongsInChronikTxDetails, } from 'utils/cashMethods'; import { currency } from 'components/Common/Ticker'; import { @@ -156,6 +158,14 @@ incrementallyHydratedUtxosAfterProcessingOneMissing, } from '../__mocks__/incrementalUtxoMocks'; +import { + lambdaHash160s, + lambdaIncomingXecTx, + lambdaIncomingEtokenTx, + lambdaOutgoingXecTx, + lambdaOutgoingEtokenTx, +} from '../__mocks__/chronikWebsocketTxs'; + describe('Correctly executes cash utility functions', () => { it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => { expect(fromSmallestDenomination(1, 2)).toBe(0.01); @@ -873,4 +883,66 @@ it(`flattenContactList returns an empty array for invalid input`, () => { expect(flattenContactList(false)).toStrictEqual([]); }); + it(`Successfully parses an incoming XEC tx`, () => { + expect( + parseChronikTx( + addTypeToLongsInChronikTxDetails(lambdaIncomingXecTx), + lambdaHash160s, + ), + ).toStrictEqual({ + incoming: true, + xecAmount: '42', + isEtokenTx: false, + }); + }); + it(`Successfully parses an incoming eToken tx`, () => { + expect( + parseChronikTx( + addTypeToLongsInChronikTxDetails(lambdaIncomingEtokenTx), + lambdaHash160s, + ), + ).toStrictEqual({ + incoming: true, + xecAmount: '5.46', + isEtokenTx: true, + slpMeta: { + tokenId: + '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + tokenType: 'FUNGIBLE', + txType: 'SEND', + }, + etokenAmount: '12', + }); + }); + it(`Successfully parses an outgoing XEC tx`, () => { + expect( + parseChronikTx( + addTypeToLongsInChronikTxDetails(lambdaOutgoingXecTx), + lambdaHash160s, + ), + ).toStrictEqual({ + incoming: false, + xecAmount: '222', + isEtokenTx: false, + }); + }); + it(`Successfully parses an outgoing eToken tx`, () => { + expect( + parseChronikTx( + addTypeToLongsInChronikTxDetails(lambdaOutgoingEtokenTx), + lambdaHash160s, + ), + ).toStrictEqual({ + incoming: false, + xecAmount: '5.46', + isEtokenTx: true, + slpMeta: { + tokenId: + '4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3', + tokenType: 'FUNGIBLE', + txType: 'SEND', + }, + etokenAmount: '17', + }); + }); }); diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -7,6 +7,7 @@ } from 'utils/validation'; import BigNumber from 'bignumber.js'; import cashaddr from 'ecashaddrjs'; +import Long from 'long'; export function parseOpReturn(hexStr) { if ( @@ -975,3 +976,124 @@ incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch = true; return incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch; }; + +export const parseChronikTx = (tx, walletHash160s) => { + // Determine if incoming or outgoing tx + // Determine if XEC tx + // Determine how much XEC sent or received + // Determine how much eToken sent or received + // edge cases: minting token txs, airdrops, OP_RETURN msgs + const { inputs, outputs } = tx; + // Assign defaults + let incoming = true; + let xecAmount = new BigNumber(0); + let etokenAmount = new BigNumber(0); + const isEtokenTx = 'slpTxData' in tx && typeof tx.slpTxData !== 'undefined'; + + // Iterate over inputs to see if this is a sent tx + for (let i = 0; i < inputs.length; i += 1) { + const thisInput = inputs[i]; + const thisInputSendingHash160 = thisInput.outputScript; + for (let j = 0; j < walletHash160s.length; j += 1) { + const thisWalletHash160 = walletHash160s[j]; + if (thisInputSendingHash160.includes(thisWalletHash160)) { + // Then this is an outgoing tx + incoming = false; + } + } + } + // Iterate over outputs to get the amount sent + for (let i = 0; i < outputs.length; i += 1) { + const thisOutput = outputs[i]; + const thisOutputReceivedAtHash160 = thisOutput.outputScript; + // Find amounts at your wallet's addresses + for (let j = 0; j < walletHash160s.length; j += 1) { + const thisWalletHash160 = walletHash160s[j]; + if (thisOutputReceivedAtHash160.includes(thisWalletHash160)) { + // If incoming tx, this is amount received by the user's wallet + // if !incoming, then this is a change amount + const thisOutputAmount = new BigNumber(thisOutput.value); + xecAmount = incoming + ? xecAmount.plus(thisOutputAmount) + : xecAmount.minus(thisOutputAmount); + + // Parse token qty if token tx + // Note: edge case this is a token tx that sends XEC to recipient but token somewhere else + if (isEtokenTx) { + try { + const thisEtokenAmount = new BigNumber( + thisOutput.slpToken.amount, + ); + + etokenAmount = incoming + ? etokenAmount.plus(thisEtokenAmount) + : etokenAmount.minus(thisEtokenAmount); + } catch (err) { + // edge case described above; in this case there is no value, so do nothing + etokenAmount.plus(new BigNumber(0)); + } + } + } + } + // Output amounts not at your wallet are sent amounts if !incoming + if (!incoming) { + const thisOutputAmount = new BigNumber(thisOutput.value); + xecAmount = xecAmount.plus(thisOutputAmount); + if (isEtokenTx) { + try { + const thisEtokenAmount = new BigNumber( + thisOutput.slpToken.amount, + ); + + etokenAmount = etokenAmount.plus(thisEtokenAmount); + } catch (err) { + // edge case where tx is token but this particular output is not + etokenAmount.plus(new BigNumber(0)); + } + } + } + } + // Convert from sats to XEC + xecAmount = xecAmount.shiftedBy(-2); + // Convert from long to string + xecAmount = xecAmount.toString(); + etokenAmount = etokenAmount.toString(); + if (isEtokenTx) { + const { slpMeta } = tx.slpTxData; + return { + incoming, + xecAmount, + isEtokenTx, + etokenAmount, + slpMeta, + }; + } + return { incoming, xecAmount, isEtokenTx }; +}; + +// Chronik api calls return typed Longs, but this typing is lost in copy pasting, storing in mocks, or retrieving from local storage +export const addTypeToLong = untypedLong => { + try { + const { low, high, unsigned } = untypedLong; + return new Long(low, high, unsigned); + } catch (err) { + // Not a valid untyped long number + return false; + } +}; + +export const addTypeToLongsInChronikTxDetails = chronikTxDetails => { + const { inputs, outputs } = chronikTxDetails; + for (let i = 0; i < inputs.length; i += 1) { + inputs[i].value = addTypeToLong(inputs[i].value); + } + for (let i = 0; i < outputs.length; i += 1) { + outputs[i].value = addTypeToLong(outputs[i].value); + if ('slpToken' in outputs[i]) { + outputs[i].slpToken.amount = addTypeToLong( + outputs[i].slpToken.amount, + ); + } + } + return chronikTxDetails; +};