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/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 = { @@ -641,6 +644,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( @@ -1650,6 +1713,246 @@ 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].genesisInfo = 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, @@ -1671,5 +1974,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 @@ -39,6 +39,9 @@ getTxHistory, getTxData, addTokenTxData, + getUtxosChronik, + getSlpBalancesAndUtxosFromChronik, + addTokenInfo, } = useBCH(); const [loading, setLoading] = useState(true); const [apiIndex, setApiIndex] = useState(0); @@ -216,6 +219,37 @@ 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 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 @@ -1170,7 +1204,7 @@ setHasUpdated(true); } }); - }, 1000); + }, 10000); const fetchBchPrice = async ( fiatCode = cashtabSettings ? cashtabSettings.fiatCurrency : 'usd',