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": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "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": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "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": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "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.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, "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.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "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.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "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": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@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": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "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": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "@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.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, "@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.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "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/Home/Home.js b/web/cashtab/src/components/Home/Home.js --- a/web/cashtab/src/components/Home/Home.js +++ b/web/cashtab/src/components/Home/Home.js @@ -144,6 +144,10 @@ const { wallet, fiatPrice, apiError, cashtabSettings, contactList } = ContextValue; const walletState = getWalletState(wallet); + console.log( + `walletState.balances.totalBalanceInSatoshis`, + JSON.stringify(walletState.balances), + ); const { balances, parsedTxHistory, tokens } = walletState; const [activeTab, setActiveTab] = React.useState('txHistory'); diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -1,4 +1,6 @@ import BigNumber from 'bignumber.js'; +import Long from 'long'; +import { ChronikClient } from 'chronik-client'; import { currency } from 'components/Common/Ticker'; import { isValidTokenStats } from 'utils/validation'; import SlpWallet from 'minimal-slp-wallet'; @@ -17,6 +19,7 @@ import cashaddr from 'ecashaddrjs'; import ecies from 'ecies-lite'; import wif from 'wif'; +const chronik = new ChronikClient('https://chronik.be.cash/xec'); export default function useBCH() { const SEND_BCH_ERRORS = { @@ -660,6 +663,66 @@ }); }; + const getUtxosSingleHashChronik = async hash160 => { + let utxos; + try { + utxos = await chronik.script('p2pkh', hash160).utxos(); + if (utxos.length === 0) { + // This seems to be what chronik returns if no utxos at address + return []; + } + return utxos[0].utxos; + } catch (err) { + console.log(`Error in chronik.utxos(${hash160})`); + console.log(err); + } + }; + + const getTxDetailsChronik = async txid => { + let txDetails; + try { + txDetails = await chronik.tx(txid); + return txDetails; + } catch (err) { + console.log(`Error in chronik.tx(${txid})`); + console.log(err); + } + }; + + const returnGetUtxosChronikPromise = hashObj => { + return new Promise((resolve, reject) => { + getUtxosSingleHashChronik(hashObj.hash160).then( + result => { + // Add the address to each utxo + for (let i = 0; i < result.length; i += 1) { + const thisUtxo = result[i]; + thisUtxo.address = hashObj.address; + } + resolve(result); + }, + err => { + reject(err); + }, + ); + }); + }; + + const returnGetTokenInfoChronikPromise = tokenId => { + return new Promise((resolve, reject) => { + getTxDetailsChronik(tokenId).then( + result => { + const thisTokenInfo = result.slpTxData.genesisInfo; + thisTokenInfo.tokenId = tokenId; + // You only want the genesis info for tokenId + resolve(thisTokenInfo); + }, + err => { + reject(err); + }, + ); + }); + }; + const fetchTxDataForNullUtxos = async (BCH, nullUtxos) => { // Check nullUtxos. If they aren't eToken txs, count them console.log( @@ -885,7 +948,10 @@ let txFee = 0; for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i]; - originalAmount = originalAmount.plus(new BigNumber(utxo.value)); + + originalAmount = originalAmount.plus( + new BigNumber(new Long(utxo.value).toString()), + ); const vout = utxo.vout; const txid = utxo.txid; // add input with txid and index of vout @@ -1408,6 +1474,7 @@ airdropFlag, airdropTokenId, ) => { + console.log(`utxos in sendXEC()`, utxos); try { let value = new BigNumber(0); @@ -1571,6 +1638,7 @@ // End of building the OP_RETURN output. let originalAmount = new BigNumber(0); + let testOriginalAmount = new BigNumber(0); let txFee = 0; // A normal tx will have 2 outputs, destination and change // A one to many tx will have n outputs + 1 change output, where n is the number of recipients @@ -1579,9 +1647,27 @@ : 2; for (let i = 0; i < utxos.length; i++) { const utxo = utxos[i]; - originalAmount = originalAmount.plus(utxo.value); - const vout = utxo.vout; - const txid = utxo.txid; + console.log(`utxo in loop`, utxo); + console.log( + `new Long(utxo.value).toString(10)`, + new Long(utxo.value).toString(10), + ); + console.log(`utxo.value.toString()`, utxo.value.toString()); + console.log( + `utxo.value.toString`, + new Long(utxo.value).toString(10), + ); + originalAmount = originalAmount.plus( + new BigNumber(new Long(utxo.value).toString()), + ); + testOriginalAmount = testOriginalAmount.plus(utxo.value); + console.log(`originalAmount`, originalAmount.toString()); + console.log( + `testOriginalAmount`, + testOriginalAmount.toString(), + ); + const vout = utxo.outpoint.outIdx; + const txid = utxo.outpoint.txid; // add input with txid and index of vout transactionBuilder.addInput(txid, vout); @@ -1657,7 +1743,7 @@ BCH.ECPair.fromWIF(utxo.wif), undefined, transactionBuilder.hashTypes.SIGHASH_ALL, - utxo.value, + parseInt(new Long(utxo.value).toString()), ); } @@ -1708,6 +1794,250 @@ return ConstructedSlpWallet.bchjs; }; + const addressesToHashes = (BCH, addressArr) => { + // Convert addresses to hash160, the supported input of chronik's utxo method + console.log(`addressArr`, addressArr); + const hashObjArray = []; + for (let i = 0; i < addressArr.length; i += 1) { + const thisAddress = addressArr[i]; + const thisHashAddr = BCH.Address.toHash160(thisAddress); + // You still need to keep track of the address though, as the individual utxos need to have this info + const thisAddrHashObj = { + address: thisAddress, + hash160: thisHashAddr, + }; + hashObjArray.push(thisAddrHashObj); + } + return hashObjArray; + }; + + const getUtxosChronik = async (BCH, addresses) => { + // Convert addresses to hash160 + // get utxos (promise.all?) + const hashObjArr = addressesToHashes(BCH, addresses); + console.log(`hashObjArr`, hashObjArr); + // get utxos + const chronikUtxoPromises = []; + for (let i = 0; i < hashObjArr.length; i += 1) { + const thisPromise = returnGetUtxosChronikPromise(hashObjArr[i]); + chronikUtxoPromises.push(thisPromise); + } + const allUtxos = await Promise.all(chronikUtxoPromises); + // Since each individual utxo has address information, no need to keep them in distinct arrays + // Combine into one array of all utxos + const flatUtxos = allUtxos.flat(); + return flatUtxos; + }; + const getSlpBalancesAndUtxosFromChronik = chronikUtxos => { + /* Sample input + [ + { + "outpoint": { + "txid": "976753770d4fd3baa0a36e0792ba6b0f906efc771b25690b5300f5437ba0f0db", + "outIdx": 2 + }, + "blockHeight": 660149, + "isCoinbase": false, + "value": { + "low": 546, + "high": 0, + "unsigned": false + }, + "network": "XEC", + "address": "bitcoincash:qpv9fx6mjdpgltygudnpw3tvmxdyzx7savhphtzswu" + }, + + { + "outpoint": { + "txid": "4064e02fe523cb107fecaf3f5abaabb89f7e2bb6662751ba4f86f8d18ebeb1fa", + "outIdx": 1 + }, + "blockHeight": 670076, + "isCoinbase": false, + "value": { + "low": 546, + "high": 0, + "unsigned": false + }, + "slpMeta": { + "tokenType": "FUNGIBLE", + "txType": "SEND", + "tokenId": "bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef" + }, + "slpToken": { + "amount": { + "low": 1, + "high": 0, + "unsigned": true + }, + "isMintBaton": false + }, + "network": "XEC", + "address": "bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9" + }, +] + +Desired Output +{ + + "slpBalancesAndUtxos": { + "tokens": [ + { + "info": { + "height": 661700, + "tx_hash": "854d49d29819cdb5c4d9248146ffc82771cd3a7727f25a22993456f68050503e", + "tx_pos": 1, + "value": 546, + "txid": "854d49d29819cdb5c4d9248146ffc82771cd3a7727f25a22993456f68050503e", + "vout": 1, + "utxoType": "token", + "transactionType": "send", + "tokenId": "d4ffc597cb08b8c929e464f84069b9009649c7514860f673da48b1b3eba5b56e", + "tokenTicker": "JoeyTest2", + "tokenName": "Jt2", + "tokenDocumentUrl": "thecryptoguy.com", + "tokenDocumentHash": "", + "decimals": 0, + "tokenType": 1, + "isValid": true, + "tokenQty": "1", + "address": "bitcoincash:qpv9fx6mjdpgltygudnpw3tvmxdyzx7savhphtzswu" + }, + "tokenId": "d4ffc597cb08b8c929e464f84069b9009649c7514860f673da48b1b3eba5b56e", + "balance": "1", + "hasBaton": false + }, + + ] + }, + + +} + +1 - You need to pass the addresses along with the arrays +2 - Helper function to bump everything into one array and add the address to each utxo +3 - Construct the `nonSlpUtxos` and `slpUtxos` arrays +4 - iterate over `slpUtxos` to build the `tokens` array + */ + + const nonSlpUtxos = []; + const slpUtxos = []; + for (let i = 0; i < chronikUtxos.length; i += 1) { + // Construct nonSlpUtxos and slpUtxos arrays + const thisUtxo = chronikUtxos[i]; + const isEtoken = typeof thisUtxo.slpToken !== 'undefined'; + if (isEtoken) { + slpUtxos.push(thisUtxo); + } else { + // Exclude utxos of 546 sats as a precaution against accidentally burning eToken utxos + // Note: no known case of this being an issue on chronik, but preserve it for now + if (thisUtxo.value.low === 546) { + continue; + } else { + nonSlpUtxos.push(thisUtxo); + } + } + } + // Learn how this Long lib works + for (let i = 0; i < slpUtxos.length; i += 1) { + const thisSlpUtxo = slpUtxos[i]; + const tokenQty = new Long(thisSlpUtxo.slpToken.amount).toString(10); + slpUtxos[i]['tokenQty'] = tokenQty; + } + + let tokensById = {}; + + slpUtxos.forEach(slpUtxo => { + let token = tokensById[slpUtxo.slpMeta.tokenId]; + + if (token) { + if (slpUtxo.tokenQty) { + token.balance = token.balance.plus( + new BigNumber(slpUtxo.tokenQty), + ); + } + } else { + token = {}; + token.tokenId = slpUtxo.slpMeta.tokenId; + if (slpUtxo.tokenQty) { + token.balance = new BigNumber(slpUtxo.tokenQty); + } else { + token.balance = new BigNumber(0); + } + + tokensById[slpUtxo.slpMeta.tokenId] = token; + } + }); + + const tokens = Object.values(tokensById); + + const chronikSlpBalancesAndUtxos = { slpUtxos, nonSlpUtxos, tokens }; + return chronikSlpBalancesAndUtxos; + }; + + const addTokenInfo = async tokens => { + // for each token, get the genesis info + // parse token qty by decimal + const getTokenInfoPromises = []; + for (let i = 0; i < tokens.length; i += 1) { + const thisTokenId = tokens[i].tokenId; + const thisTokenInfoPromise = + returnGetTokenInfoChronikPromise(thisTokenId); + getTokenInfoPromises.push(thisTokenInfoPromise); + } + let tokenInfoArray = await Promise.all(getTokenInfoPromises); + // TODO iterate through tokens and add the required tokeninfo + // note: mb it's better if you organize the token info so it's accessible by tokenId index for this process + + // NB: tokenInfoArray should be in the same order as tokens, by tokenId + // Do not assume this + if (tokens.length !== tokenInfoArray.length) { + console.log( + `ERROR: tokenInfoArray length is ${tokenInfoArray.length}, while tokens length is ${tokens.length}`, + ); + } + for (let i = 0; i < tokens.length; i += 1) { + const thisToken = tokens[i]; + const thisTokenId = thisToken.tokenId; + tokenInfoArrayLoop: for ( + let j = 0; + j < tokenInfoArray.length; + j += 1 + ) { + const tokenInfoForTokenId = tokenInfoArray[j].tokenId; + if (thisTokenId === tokenInfoForTokenId) { + const thisGenesisInfo = tokenInfoArray[j]; + // Add this info to the utxo + tokens[i].info = thisGenesisInfo; + // Adjust the token balance for tokenDecimals + const tokenDecimals = thisGenesisInfo.decimals; + // Adjust tokenQty per decimal places + /* + console.log( + `starting balance for ${thisTokenId} with ${tokenDecimals} decimals`, + tokens[i].balance.toString(), + ); + */ + tokens[i].balance = tokens[i].balance.shiftedBy( + -1 * tokenDecimals, + ); + /* + console.log( + `ending balance for ${thisTokenId}`, + tokens[i].balance.toString(), + ); + */ + + // you won't need it again, so remove it from tokenInfoArray + tokenInfoArray.slice(j, 1); + // do not iterate through the rest of tokenInfoArray once you have found what you are looking for + break tokenInfoArrayLoop; + } + } + } + return tokens; + }; + return { getBCH, calcFee, @@ -1729,5 +2059,8 @@ handleEncryptedOpReturn, getRecipientPublicKey, burnEtoken, + getUtxosChronik, + getSlpBalancesAndUtxosFromChronik, + addTokenInfo, }; } 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 @@ -3,6 +3,7 @@ import usePrevious from 'hooks/usePrevious'; import useBCH from 'hooks/useBCH'; import BigNumber from 'bignumber.js'; +import Long from 'long'; import { fromSmallestDenomination, loadStoredWallet, @@ -39,6 +40,9 @@ getTxHistory, getTxData, addTokenTxData, + getUtxosChronik, + getSlpBalancesAndUtxosFromChronik, + addTokenInfo, } = useBCH(); const [loading, setLoading] = useState(true); const [apiIndex, setApiIndex] = useState(0); @@ -111,6 +115,28 @@ }; }; + const normalizeBalanceChronik = chronikSlpBalancesAndUtxos => { + const totalBalanceInSatoshis = + chronikSlpBalancesAndUtxos.nonSlpUtxos.reduce( + (previousBalance, utxo) => + parseInt( + new BigNumber(previousBalance) + .plus( + new BigNumber( + new Long(utxo.value).toString(10), + ), + ) + .toString(), + ), + 0, + ); + console.log(`totalBalanceInSatoshisChronik`, totalBalanceInSatoshis); + return { + totalBalanceInSatoshis, + totalBalance: fromSmallestDenomination(totalBalanceInSatoshis), + }; + }; + const deriveAccount = async (BCH, { masterHDNode, path }) => { const node = BCH.HDNode.derivePath(masterHDNode, path); const publicKey = BCH.HDNode.toPublicKey(node).toString('hex'); @@ -128,12 +154,22 @@ }; const loadWalletFromStorageOnStartup = async setWallet => { + console.log(`loadWalletFromStorageOnStartup`); // get wallet object from localforage const wallet = await getWallet(); + console.log(`loadWalletFromStorageOnStartup wallet`, wallet); + console.log( + `loadWalletFromStorageOnStartup isValidStoredWallet(wallet)`, + isValidStoredWallet(wallet), + ); // If wallet object in storage is valid, use it to set state on startup if (isValidStoredWallet(wallet)) { // Convert all the token balance figures to big numbers const liveWalletState = loadStoredWallet(wallet.state); + console.log( + `loadWalletFromStorageOnStartup liveWalletState`, + liveWalletState, + ); wallet.state = liveWalletState; setWallet(wallet); @@ -143,6 +179,30 @@ setWallet(wallet); }; + const haveUtxosChangedChronik = (utxos, previousUtxos) => { + console.log(`New utxos`, utxos); + console.log(`Previous utxos`, previousUtxos); + delete previousUtxos[0].wif; + delete previousUtxos[previousUtxos.length - 1].wif; + // TODO this function should have unit tests and be in cashMethods + // TODO work out the logic of when you want to force the calc anyway, i.e. !wallet you might want to be true + // Utxo set has changed if first or last utxo is not the same + const firstUtxo = JSON.stringify(utxos[0]); + const lastUtxo = JSON.stringify(utxos[utxos.length - 1]); + const previousFirstUtxo = JSON.stringify(previousUtxos[0]); + const previousLastUtxo = JSON.stringify( + previousUtxos[previousUtxos.length - 1], + ); + + console.log(`firstUtxo`, firstUtxo); + console.log(`lastUtxo`, lastUtxo); + console.log(`previousFirstUtxo`, previousFirstUtxo); + console.log(`previousLastUtxo`, previousLastUtxo); + return ( + (firstUtxo !== previousFirstUtxo) | (lastUtxo !== previousLastUtxo) + ); + }; + const haveUtxosChanged = (wallet, utxos, previousUtxos) => { // Relevant points for this array comparing exercise // https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why @@ -195,9 +255,130 @@ return !isEqual(utxos, utxosToCompare); }; + const chronikUpdate = async ({ wallet }) => { + console.log(`chronikUpdate`); + //console.log(`tick()`); + console.time('chronikUpdate'); + console.log(`wallet`, wallet); + console.log(`isValidStoredWallet(wallet)`, isValidStoredWallet(wallet)); + try { + if (!wallet) { + return; + } + const cashAddresses = [ + wallet.Path245.cashAddress, + wallet.Path145.cashAddress, + wallet.Path1899.cashAddress, + ]; + + const publicKeys = [ + wallet.Path145.publicKey, + wallet.Path245.publicKey, + wallet.Path1899.publicKey, + ]; + + const utxosChronik = await getUtxosChronik(BCH, cashAddresses); + console.log(`utxosChronik`, utxosChronik); + + const chronikUtxosHaveChanged = haveUtxosChangedChronik( + utxosChronik, + wallet.state.utxos, + ); + console.log(`chronikUtxosHaveChanged`, chronikUtxosHaveChanged); + // If utxo set is unchanged, no need to update wallet state + if (!chronikUtxosHaveChanged) { + console.log(`utxo set has not changed, wallet state unchanged`); + // remove api error here; otherwise it will remain if recovering from a rate + // limit error with an unchanged utxo set + setApiError(false); + console.timeEnd('chronikUpdate'); + return; + } + + let chronikSlpBalancesAndUtxos = + getSlpBalancesAndUtxosFromChronik(utxosChronik); + + const chronikBalances = normalizeBalanceChronik( + chronikSlpBalancesAndUtxos, + ); + console.log(`chronikBalances`, chronikBalances); + + const tokensWithGenesisInfo = await addTokenInfo( + chronikSlpBalancesAndUtxos.tokens, + ); + + chronikSlpBalancesAndUtxos.tokens = tokensWithGenesisInfo; + chronikSlpBalancesAndUtxos = normalizeSlpBalancesAndUtxos( + chronikSlpBalancesAndUtxos, + wallet, + ); + console.log( + `chronikSlpBalancesAndUtxos`, + chronikSlpBalancesAndUtxos, + ); + + // tx history is still legacy here + const txHistory = await getTxHistory(BCH, cashAddresses); + + // public keys are used to determined if a tx is incoming outgoing + const parsedTxHistory = await getTxData( + BCH, + txHistory, + publicKeys, + wallet, + ); + + const parsedWithTokens = await addTokenTxData(BCH, parsedTxHistory); + + if (typeof chronikSlpBalancesAndUtxos === 'undefined') { + console.log(`slpBalancesAndUtxos is undefined`); + throw new Error('slpBalancesAndUtxos is undefined'); + } + + const newState = { + balances: {}, + tokens: [], + slpBalancesAndUtxos: [], + }; + + newState.slpBalancesAndUtxos = chronikSlpBalancesAndUtxos; + + newState.balances = chronikBalances; + + newState.tokens = tokensWithGenesisInfo; + + newState.parsedTxHistory = parsedWithTokens; + + newState.utxos = utxosChronik; + + newState.hydratedUtxoDetails = utxosChronik; + console.log(`new state`, newState); + + // Set wallet with new state field + wallet.state = newState; + setWallet(wallet); + + // Write this state to indexedDb using localForage + writeWalletState(wallet, newState); + // If everything executed correctly, remove apiError + setApiError(false); + } catch (error) { + console.log(`Error in update({wallet})`); + console.log(error); + // Set this in state so that transactions are disabled until the issue is resolved + setApiError(true); + console.timeEnd('chronikUpdate'); + // Try another endpoint + console.log(`Trying next API...`); + tryNextAPI(); + } + console.timeEnd('chronikUpdate'); + }; + const update = async ({ wallet }) => { //console.log(`tick()`); //console.time("update"); + console.log(`update`); try { if (!wallet) { return; @@ -216,6 +397,41 @@ const utxos = await getUtxos(BCH, cashAddresses); + //Chronik testing + const utxosChronik = await getUtxosChronik(BCH, cashAddresses); + console.log(`utxosChronik`, utxosChronik); + // TODO + // write a function to turn this into slpBalancesAndUtxos + let chronikSlpBalancesAndUtxos = + getSlpBalancesAndUtxosFromChronik(utxosChronik); + console.log( + `chronikSlpBalancesAndUtxos`, + chronikSlpBalancesAndUtxos, + ); + const chronikBalances = normalizeBalanceChronik( + chronikSlpBalancesAndUtxos, + ); + console.log(`chronikBalances`, chronikBalances); + + const tokensWithGenesisInfo = await addTokenInfo( + chronikSlpBalancesAndUtxos.tokens, + ); + + chronikSlpBalancesAndUtxos.tokens = tokensWithGenesisInfo; + chronikSlpBalancesAndUtxos = normalizeSlpBalancesAndUtxos( + chronikSlpBalancesAndUtxos, + wallet, + ); + console.log( + `chronikSlpBalancesAndUtxos with info + normalized`, + chronikSlpBalancesAndUtxos, + ); + + // next steps + // You still need to get tokenQty right from tokenDecimals + // make sure you can get xec balance from your utxos + // make sure utxos have all the info you need to send txs + // If an error is returned or utxos from only 1 address are returned if (!utxos || isEmpty(utxos) || utxos.error || utxos.length < 2) { // Throw error here to prevent more attempted api calls @@ -232,6 +448,10 @@ // If the utxo set has not changed, if (!utxosHaveChanged) { + console.log( + `slpBalancesAndUtxos`, + wallet.state.slpBalancesAndUtxos, + ); // remove api error here; otherwise it will remain if recovering from a rate // limit error with an unchanged utxo set setApiError(false); @@ -377,8 +597,27 @@ const getActiveWalletFromLocalForage = async () => { let wallet; + console.log(`getActiveWalletFromLocalForage`); try { wallet = await localforage.getItem('wallet'); + console.log( + `wallet pulled in getActiveWalletFromLocalForage`, + wallet, + ); + // Confirm that balance is a number + const { balances } = wallet.state; + const { totalBalance, totalBalanceInSatoshis } = balances; + console.log(`totalBalance`, totalBalance); + console.log(`totalBalanceInSatoshis`, totalBalanceInSatoshis); + console.log(`isNaN(totalBalance)`, isNaN(totalBalance)); + console.log( + `isNaN(totalBalanceInSatoshis)`, + isNaN(totalBalanceInSatoshis), + ); + if (isNaN(totalBalance) || isNaN(totalBalanceInSatoshis)) { + console.log(`setting wallet to null`); + wallet = null; + } } catch (err) { console.log(`Error in getActiveWalletFromLocalForage`, err); wallet = null; @@ -409,10 +648,15 @@ }; const getWallet = async () => { + console.log(`getWallet`); let wallet; let existingWallet; try { existingWallet = await getActiveWalletFromLocalForage(); + console.log( + `existingWallet at beginning of getWallet`, + existingWallet, + ); // existing wallet will be // 1 - the 'wallet' value from localForage, if it exists // 2 - false if it does not exist in localForage @@ -464,6 +708,7 @@ } else { wallet = existingWallet; } + console.log(`wallet returned from getWallet()`, wallet); return wallet; }; @@ -1162,7 +1407,7 @@ // Update wallet every 10s useAsyncTimeout(async () => { const wallet = await getWallet(); - update({ + chronikUpdate({ wallet, }).finally(() => { setLoading(false); @@ -1170,7 +1415,7 @@ setHasUpdated(true); } }); - }, 1000); + }, 10000); const fetchBchPrice = async ( fiatCode = cashtabSettings ? cashtabSettings.fiatCurrency : 'usd', 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 @@ -6,6 +6,7 @@ isValidContactList, } from 'utils/validation'; import BigNumber from 'bignumber.js'; +import Long from 'long'; import cashaddr from 'ecashaddrjs'; export function parseOpReturn(hexStr) { @@ -204,7 +205,16 @@ // See BigNumber.js api for how to create a BigNumber object from an object // https://mikemcl.github.io/bignumber.js/ const liveWalletState = walletStateFromStorage; - const { slpBalancesAndUtxos, tokens } = liveWalletState; + const { slpBalancesAndUtxos, tokens, utxos, balances } = liveWalletState; + + /* + Three possible cases here + 1. Empty wallet, slpBalancesAndUtxos will have an empty utxos array + 2. Chronik wallet with utxos. A chronik utxo will have the `outpoint` key + 3. Bch-api wallet with utxos. A bch-api wallet will not have an `outpoint` key + */ + // Determine if this wallet has utxos from bch-api or chronik + for (let i = 0; i < tokens.length; i += 1) { const thisTokenBalance = tokens[i].balance; thisTokenBalance._isBigNumber = true; @@ -213,7 +223,17 @@ // Also confirm balance is correct // Necessary step in case currency.decimals changed since last startup - const balancesRebased = normalizeBalance(slpBalancesAndUtxos); + // Only need to do this for a bch-api wallet + let balancesRebased = normalizeBalance(slpBalancesAndUtxos); + if (utxos && utxos[0] && 'outpoint' in utxos[0]) { + console.log(`Chronik wallet, using normal balances`); + balancesRebased = balances; + // Make sure long values are serialized as such coming out of storage + for (let i = 0; i < utxos.length; i += 1) { + const { low, high, unsigned } = utxos[i].value; + utxos[i].value = new Long(low, high, unsigned); + } + } liveWalletState.balances = balancesRebased; return liveWalletState; }; @@ -243,6 +263,7 @@ }; export const getWalletState = wallet => { + console.log(`getWalletState called with`, wallet); if (!wallet || !wallet.state) { return { balances: { totalBalance: 0, totalBalanceInSatoshis: 0 }, @@ -253,6 +274,7 @@ utxos: [], }; } + console.log(`wallet.state from getWalletState`, wallet.state); return wallet.state; };