diff --git a/package-lock.json b/package-lock.json index 096adb4..6f0e9ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,11 @@ "dependencies": { "@dashincubator/base58check": "^1.4.1", "@dashincubator/ripemd160": "^3.0.0", - "@dashincubator/secp256k1": "^1.7.1-5", + "@dashincubator/secp256k1": "dashhive/secp256k1.js#sync-with-upstream", "@zxing/library": "^0.21.2", + "crowdnode": "dashhive/crowdnode.js#ref-dashtx", "crypticstorage": "^0.0.2", - "dashhd": "^3.3.3", + "dashhd": "dashhive/DashHD.js#ref-secp256k1-2.1.0-compat", "dashkeys": "^1.1.4", "dashphrase": "^1.4.0", "dashsight": "dashhive/DashSight.js#feat/bulk-txs", @@ -33,6 +34,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/@dashincubator/base58check/-/base58check-1.4.1.tgz", "integrity": "sha512-JSAO+viM3pVgM93XSBAUhLvK18BlRIsmYU4eGuyrj1lz3Xa1oplSel94oG0SI5d2qTxdy0NfGT3vEFbiKpfj5Q==", + "license": "SEE LICENSE IN LICENSE", "bin": { "base58check": "bin/base58check.js" } @@ -40,17 +42,25 @@ "node_modules/@dashincubator/ripemd160": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@dashincubator/ripemd160/-/ripemd160-3.0.0.tgz", - "integrity": "sha512-EbdXcceP2mW76NchCKp8UYNbZgWkLuV4Mbi30G82xRED32ljJzXsKaaVdzU0oVo2fVzPRXF1GhSF6Lq9beTVvA==" + "integrity": "sha512-EbdXcceP2mW76NchCKp8UYNbZgWkLuV4Mbi30G82xRED32ljJzXsKaaVdzU0oVo2fVzPRXF1GhSF6Lq9beTVvA==", + "license": "MIT" }, "node_modules/@dashincubator/secp256k1": { - "version": "1.7.1-5", - "resolved": "https://registry.npmjs.org/@dashincubator/secp256k1/-/secp256k1-1.7.1-5.tgz", - "integrity": "sha512-3iA+RDZrJsRFPpWhlYkp3EdoFAlKjdqkNFiRwajMrzcpA/G/IBX0AnC1pwRLkTrM+tUowcyGrkJfT03U4ETZeg==" + "version": "2.1.0", + "resolved": "git+ssh://git@github.com/dashhive/secp256k1.js.git#d587c1a24ca7d5c47f7cb519ca70a54920da3245", + "license": "MIT" + }, + "node_modules/@root/request": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.9.2.tgz", + "integrity": "sha512-wVaL9yVV9oDR9UNbPZa20qgY+4Ch6YN8JUkaE4el/uuS5dmhD8Lusm/ku8qJVNtmQA56XLzEDCRS6/vfpiHK2A==", + "license": "(MIT OR Apache-2.0)", + "optional": true }, "node_modules/@types/node": { - "version": "20.14.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", - "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "version": "20.14.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", + "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -76,17 +86,40 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "license": "(Unlicense OR Apache-2.0)", "optional": true }, + "node_modules/crowdnode": { + "version": "1.8.0", + "resolved": "git+ssh://git@github.com/dashhive/crowdnode.js.git#2cfe200186a3d3ed7d1fb29edac0657825ddf7d0", + "license": "SEE LICENSE IN LICENSE", + "bin": { + "crowdnode": "bin/crowdnode.js" + }, + "optionalDependencies": { + "@dashincubator/base58check": "^1.4.1", + "@dashincubator/ripemd160": "^3.0.0", + "@dashincubator/secp256k1": "dashhive/secp256k1.js#sync-with-upstream", + "@root/request": "^1.9.2", + "dashhd": "dashhive/DashHD.js#ref-secp256k1-2.1.0-compat", + "dashkeys": "^1.1.4", + "dashsight": "^1.6.1", + "dashtx": "^0.16.0", + "dotenv": "^16.4.5", + "qrcode-svg": "^1.1.0", + "tough-cookie": "^4.1.4", + "ws": "^8.18.0" + } + }, "node_modules/crypticstorage": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypticstorage/-/crypticstorage-0.0.2.tgz", - "integrity": "sha512-PyiKq03ekJU1AwzYVchlm+LaUvDy939S1smmCD0fa60HLKVM0m6zEj4J/MXcm1BLrwdaZk9eRdJcau77nIOPAw==" + "integrity": "sha512-PyiKq03ekJU1AwzYVchlm+LaUvDy939S1smmCD0fa60HLKVM0m6zEj4J/MXcm1BLrwdaZk9eRdJcau77nIOPAw==", + "license": "MIT" }, "node_modules/dashhd": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/dashhd/-/dashhd-3.3.3.tgz", - "integrity": "sha512-sbhLV8EtmebnlIdx/d1hcbnxdfka/0rcLx+UO5y44kZdu5tyJ5ftBFbhhIb38vd+T+Xfcwpeo0z+0ZDznRkfaw==", + "resolved": "git+ssh://git@github.com/dashhive/DashHD.js.git#92cf91acf5633ed2ebed3b4f39d0d0f1d830679e", "license": "SEE LICENSE IN LICENSE" }, "node_modules/dashkeys": { @@ -98,11 +131,13 @@ "node_modules/dashphrase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/dashphrase/-/dashphrase-1.4.0.tgz", - "integrity": "sha512-o+LdiPkiYmg07kXBE+2bbcJzBmeTQVPn1GS2XlQeo8lene+KknAprSyiYi5XtqV/QVgNjvzOV7qBst2MijSPAA==" + "integrity": "sha512-o+LdiPkiYmg07kXBE+2bbcJzBmeTQVPn1GS2XlQeo8lene+KknAprSyiYi5XtqV/QVgNjvzOV7qBst2MijSPAA==", + "license": "SEE LICENSE IN LICENSE" }, "node_modules/dashsight": { "version": "1.6.1", "resolved": "git+ssh://git@github.com/dashhive/DashSight.js.git#11c1a740057ba646f89e8da2d85cb35dc67085f3", + "license": "SEE LICENSE IN LICENSE", "dependencies": { "dashtx": "^0.9.0-3" }, @@ -121,6 +156,7 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/dashtx/-/dashtx-0.9.0.tgz", "integrity": "sha512-DDbH5vPChUpOrYMOoM+6g/Iy99KqG4nkJ6f8TphnGibzAY7mitjMgtFSc62YzbZdoPGYeSPm8N4jmz+Mbwm7Eg==", + "license": "SEE LICENSE IN LICENSE", "bin": { "dashtx-inspect": "bin/inspect.js" } @@ -156,36 +192,41 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", "optional": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/html5-qrcode": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", - "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==" + "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==", + "license": "Apache-2.0" }, "node_modules/idb": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", - "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" + "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==", + "license": "ISC" }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" }, "node_modules/lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } @@ -194,22 +235,72 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", "dependencies": { "lie": "3.1.1" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/qrcode-svg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz", "integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw==", + "license": "MIT", "bin": { "qrcode-svg": "bin/qrcode-svg.js" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT", + "optional": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT", + "optional": true + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-custom-error": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -218,7 +309,51 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "optional": true, + "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 + } + } } } } diff --git a/package.json b/package.json index e44fb16..e904035 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,11 @@ "dependencies": { "@dashincubator/base58check": "^1.4.1", "@dashincubator/ripemd160": "^3.0.0", - "@dashincubator/secp256k1": "^1.7.1-5", + "@dashincubator/secp256k1": "dashhive/secp256k1.js#sync-with-upstream", "@zxing/library": "^0.21.2", - "crowdnode": "^1.8.0", + "crowdnode": "dashhive/crowdnode.js#ref-dashtx", "crypticstorage": "^0.0.2", - "dashhd": "^3.3.3", + "dashhd": "dashhive/DashHD.js#ref-secp256k1-2.1.0-compat", "dashkeys": "^1.1.4", "dashphrase": "^1.4.0", "dashsight": "dashhive/DashSight.js#feat/bulk-txs", diff --git a/src/components/balance.js b/src/components/balance.js index 9077705..7075f5c 100644 --- a/src/components/balance.js +++ b/src/components/balance.js @@ -1,6 +1,9 @@ -import { lit as html } from '../helpers/lit.js' -import { envoy, formatDash, } from '../helpers/utils.js' -import { updateAllFunds, } from '../helpers/wallet.js' +import { + lit as html, +} from '../utils/generic.js' +import { envoy, } from '../utils/retort.js' +import { formatDash, } from '../utils/dash/local.js' +import { updateAllFunds, } from '../utils/dash/network.js' const initialState = { id: 'Balance', @@ -68,7 +71,7 @@ const initialState = { // ) if (state?.wallet && state.walletFunds) { - updateAllFunds(state.wallet, state.walletFunds) + updateAllFunds(state.wallet) .then(balance => { console.log( 'Update Balance', diff --git a/src/components/contacts-list.js b/src/components/contacts-list.js index 16a112d..87a1bfc 100644 --- a/src/components/contacts-list.js +++ b/src/components/contacts-list.js @@ -1,13 +1,14 @@ -import { lit as html } from '../helpers/lit.js' import { - envoy, - restate, + lit as html, + timeago, + getAvatar, +} from '../utils/generic.js' +import { envoy, restate, } from '../utils/retort.js' +import { sortContactsByAlias, filterPairedContacts, filterUnpairedContacts, - timeago, - getAvatar, -} from '../helpers/utils.js' +} from '../utils/dash/local.js' let _handlers = [] diff --git a/src/components/crowdnode-card.js b/src/components/crowdnode-card.js new file mode 100644 index 0000000..b8df62a --- /dev/null +++ b/src/components/crowdnode-card.js @@ -0,0 +1,226 @@ +import { + lit as html, + formDataEntries, +} from '../utils/generic.js' + +import { + createSignal, +} from '../utils/retort.js' + +import { + CrowdNode, +} from '../imports.js' + +export const CrowdNodeCard = (() => { + console.log('CrowdNodeCard', CrowdNode) + const initCfg = { + state: {}, + slugs: {}, + events: {}, + elements: {}, + markup: {}, + } + const initialState = { + id: 'Card', + name: 'Card', + withdrawTxt: 'Withdraw', + withdrawAlt: 'Withdraw from Crowdnode', + depositTxt: 'Deposit', + depositAlt: `Deposit to Crowdnode`, + signupTxt: 'Signup', + signupAlt: `Signup for Crowdnode`, + placement: 'center', + rendered: null, + responsive: true, + } + + const cn = function Crowdnode( + config = {} + ) { + config = { + ...initCfg, + ...config, + } + + this.appElement = document.body + + this.api = createSignal({}) + + this.state = { + ...initialState, + ...config.state, + } + + this.slugs = { + form: this.state.name?.toLowerCase().replaceAll(' ', '_'), + ...config.slugs, + } + + this.elements = { + ...config.elements, + } + + this.markup = {} + this.markup.content = () => html` +
+ + + CrowdNode + +
+ +
+ ${!this.api.value?.acceptedToS && html` + Start earning interest by staking your Dash at CrowdNode + `} + ${this.api.value?.acceptedToS && html` +

+ Balance: Ð ${this.api.value?.balance} +

+ `} + ${this.api.value?.acceptedToS && html` +

+ Earned: Ð ${this.api.value?.earned} +

+ `} +
+ + + ` + this.markup = { + ...this.markup, + ...config.markup, + } + + this.events = { + submit: event => { + event.preventDefault() + event.stopPropagation() + + this.elements.form?.removeEventListener('submit', this.events.submit) + + let fde = formDataEntries(event) + + console.log( + `${this.slugs.form} submit`, + {event, fde}, + ) + }, + ...config.events, + } + + const $d = document + + const form = $d.createElement('form') + + this.elements.form = form + + form.name = `${this.slugs.form}` + form.classList.add('flex', 'col', 'card') + form.innerHTML = this.markup.content() + + this.api.on((apiChange) => { + console.log('CN Card API Change', apiChange) + this.render?.({}) + }) + + /** + * Update the config of the CN Card + * @function + */ + this.updateConfig = (config = {}) => { + console.log('CN Card updateConfig TOP', config) + + for (let param in config) { + this[param] = { + ...this[param], + ...(config[param] || {}), + } + } + + console.log('CN Card updateConfig BOT', this) + } + + /** + * Trigger the rendering of the CN Card + * @function + */ + this.render = ({ + cfg = {}, + position = 'afterend', + el = this.appElement, + }) => { + console.log('crowdnode render', this) + + this.elements.form?.removeEventListener?.( + 'submit', + this.events.submit, + ) + + if (el !== this.appElement) { + this.appElement = el + } + + this.updateConfig(cfg) + + this.elements.form.name = this.slugs.form + this.elements.form.innerHTML = this.markup.content() + + this.elements.form.addEventListener( + 'submit', + this.events.submit, + ) + + console.log('CARD RENDER', this, cfg) + + if (!this.state.rendered) { + el.insertAdjacentElement(position, this.elements.form) + this.state.rendered = this.elements.form + } + + this.events?.render?.(this) + } + + return this + } + + return cn +})(); + +export default CrowdNodeCard diff --git a/src/components/dialog.js b/src/components/dialog.js index b133a09..a1fe8d5 100644 --- a/src/components/dialog.js +++ b/src/components/dialog.js @@ -1,8 +1,15 @@ -import { lit as html } from '../helpers/lit.js' import { + DIALOG_STATUS, +} from '../utils/constants.js' + +import { + lit as html, formDataEntries, +} from '../utils/generic.js' + +import { envoy, -} from '../helpers/utils.js' +} from '../utils/retort.js' let modal = envoy( { @@ -26,7 +33,8 @@ const initialState = { rendered: null, responsive: true, delay: 500, - async render () {}, + status: DIALOG_STATUS.NOT_LOADING, + async render () { return this }, addListener () {}, addListeners () {}, removeAllListeners (targets) {}, @@ -210,6 +218,9 @@ const initialState = { // event.target === state.elements.dialog, // state.elements.dialog.returnValue // ) + state.status = DIALOG_STATUS.NOT_LOADING + state.elements.progress?.remove() + state.elements.progress = null if (state.elements.dialog.returnValue !== 'cancel') { resolve(state.elements.dialog.returnValue) @@ -298,30 +309,30 @@ export async function setupDialog( .replaceAll(/[^a-zA-Z _]/g, '') .replaceAll(' ', '_') - const dialog = document.createElement('dialog') - const form = document.createElement('form') - const progress = document.createElement('progress') + const dialogElement = document.createElement('dialog') + const formElement = document.createElement('form') + const progressElement = document.createElement('progress') - state.elements.dialog = dialog - state.elements.form = form - state.elements.progress = progress + state.elements.dialog = dialogElement + state.elements.form = formElement + state.elements.progress = progressElement - progress.classList.add('pending') + progressElement.classList.add('pending') - dialog.innerHTML = `` - dialog.id = state.slugs.dialog + dialogElement.innerHTML = `` + dialogElement.id = state.slugs.dialog if (state.responsive) { - dialog.classList.add('responsive') + dialogElement.classList.add('responsive') } - dialog.classList.add(state.placement) + dialogElement.classList.add(...(state.placement.split(' '))) - form.name = `${state.slugs.form}` - form.method = 'dialog' - form.innerHTML = await state.content(state) + formElement.name = `${state.slugs.form}` + formElement.method = 'dialog' + formElement.innerHTML = await state.content(state) - dialog.insertAdjacentElement( + dialogElement.insertAdjacentElement( 'afterbegin', - form + formElement ) function addListener( @@ -340,74 +351,74 @@ export async function setupDialog( ) { if (resolve && reject) { addListener( - dialog, + dialogElement, 'close', state.events.handleClose(state, resolve, reject), ) addListener( - dialog, + dialogElement, 'click', state.events.handleClick(state), ) } addListener( - form, + formElement, 'blur', state.events.handleBlur(state), ) addListener( - form, + formElement, 'focusout', state.events.handleFocusOut(state), ) addListener( - form, + formElement, 'focusin', state.events.handleFocusIn(state), ) addListener( - form, + formElement, 'change', state.events.handleChange(state), ) - // let updrop = form.querySelector('.updrop') + // let updrop = formElement.querySelector('.updrop') // state.elements.updrop = updrop // if (updrop) { addListener( - form, + formElement, 'drop', state.events.handleDrop(state), ) addListener( - form, + formElement, 'dragover', state.events.handleDragOver(state), ) addListener( - form, + formElement, 'dragend', state.events.handleDragEnd(state), ) addListener( - form, + formElement, 'dragleave', state.events.handleDragLeave(state), ) // } addListener( - form, + formElement, 'input', state.events.handleInput(state), ) addListener( - form, + formElement, 'reset', state.events.handleReset(state), ) addListener( - form, + formElement, 'submit', state.events.handleSubmit(state), ) @@ -416,7 +427,7 @@ export async function setupDialog( state.addListeners = addListeners function removeAllListeners( - targets = [dialog,form], + targets = [dialogElement,formElement], ) { if (state.elements.updrop) { targets.push(state.elements.updrop) @@ -456,29 +467,56 @@ export async function setupDialog( } } - dialog.id = state.slugs.dialog - form.name = `${state.slugs.form}` - form.innerHTML = await state.content(state) + dialogElement.id = state.slugs.dialog + formElement.name = `${state.slugs.form}` + formElement.innerHTML = await state.content(state) // console.log('DIALOG RENDER', state, position, state.slugs.dialog, modal.rendered) + if ( + state.status === DIALOG_STATUS.LOADING && + state.elements.progress + ) { + state.elements.form.insertAdjacentElement( + 'beforebegin', + state.elements.progress, + ) + + // document.body.insertAdjacentHTML( + // 'afterbegin', + // ``, + // ) + } + + if ( + state.status === DIALOG_STATUS.SUCCESS || + state.status === DIALOG_STATUS.ERROR + ) { + // document.getElementById('pageLoader')?.remove() + state.elements.progress?.remove() + } + if (!modal.rendered[state.slugs.dialog]) { - el.insertAdjacentElement(position, dialog) - modal.rendered[state.slugs.dialog] = dialog + el.insertAdjacentElement(position, dialogElement) + modal.rendered[state.slugs.dialog] = dialogElement } state.events.handleRender(state) + + return state } state.render = render return { - element: dialog, + element: dialogElement, + state, + elements: state.elements, show: (callback) => new Promise((resolve, reject) => { removeAllListeners() addListeners(resolve, reject) - // console.log('dialog show', dialog) - dialog.show() + // console.log('dialog show', dialogElement) + dialogElement.show() state.events.handleShow?.(state) callback?.() }), @@ -486,11 +524,11 @@ export async function setupDialog( removeAllListeners() addListeners(resolve, reject) // console.log('dialog showModal', dialog) - dialog.showModal() + dialogElement.showModal() state.events.handleShow?.(state) callback?.() }), - close: returnVal => dialog.close(returnVal), + close: returnVal => dialogElement.close(returnVal), render, } } diff --git a/src/components/main-footer.js b/src/components/main-footer.js index f419d49..4620ea5 100644 --- a/src/components/main-footer.js +++ b/src/components/main-footer.js @@ -1,4 +1,6 @@ -import { lit as html } from '../helpers/lit.js' +import { + lit as html, +} from '../utils/generic.js' const initialState = { rendered: null, diff --git a/src/components/modal.js b/src/components/modal.js new file mode 100644 index 0000000..14f673f --- /dev/null +++ b/src/components/modal.js @@ -0,0 +1,397 @@ +import { + DIALOG_STATUS, +} from '../utils/constants.js' + +import { + lit as html, + formDataEntries, + toSlug, + // addListener, + addListeners, + removeAllListeners, +} from '../utils/generic.js' + +import { + createSignal, + effect, +} from '../utils/retort.js' + +/** + * Create a new HTML Dialog + * + * @param {Object} config + */ +export function DialogContructor ( + config = {} +) { + const initCfg = { + state: {}, + slugs: {}, + events: {}, + elements: {}, + templates: {}, + } + const initialState = { + id: 'Dialog', + name: 'Dialog', + submitTxt: 'Submit', + submitAlt: 'Submit Form', + cancelTxt: 'Cancel', + cancelAlt: `Cancel Form`, + closeTxt: 'X', + closeAlt: `Close`, + placement: 'center', + rendered: null, + responsive: true, + delay: 500, + status: DIALOG_STATUS.NOT_LOADING, + } + + config = { + ...initCfg, + ...config, + } + + this.eventHandlers = [] + + this.state = createSignal({ + ...initialState, + ...config.state, + }) + + this.slugs = { + dialog: toSlug(this.state.value.name, this.state.value.id), + form: toSlug(this.state.value.id, this.state.value.name), + ...config.slugs, + } + + this.appElement = document.body + + this.elements = { + ...config.elements, + } + + this.markup = {} + + effect(() => { + this.markup.header = html` +
+ ${this.state.value.name} + ${ + this.state.value.closeTxt && html`` + } +
+ ` + this.markup.footer = html` + + ` + }) + + this.markup.fields = html` + + + +

Some instructions

+ ` + + this.markup.content = () => html` + ${this.markup.header} + +
+ ${this.markup.fields} + +
+
+ + ${this.markup.footer} + ` + + this.markup = { + ...this.markup, + ...config.markup, + } + + this.events = { + input: event => { + }, + change: event => { + }, + blur: event => { + // event.preventDefault() + if ( + event?.target?.validity?.patternMismatch && + event?.target?.type !== 'checkbox' + ) { + event.preventDefault() + let label = event.target?.previousElementSibling?.textContent?.trim() + if (label) { + event.target.setCustomValidity(`Invalid ${label}`) + } + } else { + event.target.setCustomValidity('') + } + event.target.reportValidity() + }, + focusout: event => { + // event.preventDefault() + }, + focusin: event => { + // event.preventDefault() + }, + drop: event => { + event.preventDefault() + }, + dragover: event => { + event.preventDefault() + }, + dragend: event => { + event.preventDefault() + }, + dragleave: event => { + event.preventDefault() + }, + render: ( + state, + ) => { + }, + show: ( + state, + ) => { + // focus first input + this.elements.form.querySelector( + 'input' + )?.focus() + }, + close: ( + resolve = res=>{}, + reject = res=>{}, + ) => async event => { + event.preventDefault() + removeAllListeners.bind(this) + + this.state.value.status = DIALOG_STATUS.NOT_LOADING + this.elements.dialog?.querySelector('progress')?.remove() + + if (this.elements.dialog.returnValue !== 'cancel') { + resolve(this.elements.dialog.returnValue) + } else { + resolve('cancel') + } + + setTimeout(t => { + this.state.value.rendered = null + event?.target?.remove() + }, this.state.value.delay) + }, + submit: event => { + event.preventDefault() + + let fde = formDataEntries(event) + + this.elements.dialog.returnValue = String(fde.intent) + + this.elements.dialog.close(String(fde.intent)) + }, + reset: event => { + event.preventDefault() + this.elements.form?.removeEventListener( + 'close', + this.events.reset + ) + this.elements.dialog.close('cancel') + }, + click: event => { + if (event.target === this.elements.dialog) { + this.elements.dialog.close('cancel') + } + }, + } + + const dialogElement = document.createElement('dialog') + const formElement = document.createElement('form') + const progressElement = document.createElement('progress') + + this.elements.dialog = dialogElement + this.elements.form = formElement + this.elements.progress = progressElement + + this.element = this.elements.dialog + + progressElement.classList.add('pending') + + dialogElement.innerHTML = `` + dialogElement.id = this.slugs.dialog + if (this.state.value.responsive) { + dialogElement.classList.add('responsive') + } + dialogElement.classList.add(...(this.state.value.placement.split(' '))) + + formElement.name = `${this.slugs.form}` + formElement.method = 'dialog' + formElement.innerHTML = this.markup.content() + + dialogElement.insertAdjacentElement( + 'afterbegin', + formElement + ) + + /** + * Show the dialog + * @function + */ + this.show = (callback, el = this.appElement) => new Promise((resolve, reject) => { + removeAllListeners.call(this) + addListeners.call(this, resolve, reject) + console.log('modal.js dialog show', this.elements.dialog) + + this.render({ + el, + position: 'afterend' + }) + + this.elements.dialog.show() + this.events.show?.(this) + callback?.() + }) + + /** + * Show the Modal form of the dialog + * @function + */ + this.showModal = (callback, el = this.appElement) => new Promise((resolve, reject) => { + removeAllListeners.call(this) + addListeners.call(this, resolve, reject) + console.log('modal.js dialog showModal', this, this.elements.dialog) + + this.render({ + el, + // cfg: config, + position: 'afterend' + }) + + this.elements.dialog.showModal() + this.events.show?.(this) + callback?.() + }) + + /** + * Close the dialog + * @function + */ + this.close = returnVal => this.elements.dialog.close(returnVal) + + /** + * Update the config of the dialog + * @function + */ + this.updateConfig = (config = {}) => { + console.log('Dialog updateConfig TOP', config) + + for (let param in config) { + if ('value' in this[param]) { + this[param].value = { + ...this[param].value, + ...(config[param] || {}), + } + } else { + this[param] = { + ...this[param], + ...(config[param] || {}), + } + } + } + + console.log('Dialog updateConfig BOT', this) + } + + /** + * Trigger the rendering of the dialog + * @function + */ + this.render = ({ + cfg = {}, + position = 'afterend', + el = this.appElement, + }) => { + console.log('dialog render', this) + + if (el !== this.appElement) { + this.appElement = el + } + + this.updateConfig(cfg) + + console.log('DIALOG elements', this, this.elements) + + this.elements.dialog.id = this.slugs.dialog + this.elements.form.name = this.slugs.form + this.elements.form.innerHTML = this.markup.content() + + console.log('DIALOG RENDER STATE', this.state.value, cfg) + + console.log('DIALOG RENDER', position, this.slugs.dialog) + + if ( + this.state.value.status === DIALOG_STATUS.LOADING + ) { + this.elements.form.insertAdjacentElement( + 'beforebegin', + this.elements.progress, + ) + } + + if ( + this.state.value.status === DIALOG_STATUS.SUCCESS || + this.state.value.status === DIALOG_STATUS.ERROR + ) { + this.elements.dialog.querySelector('progress')?.remove() + } + + if (!this.state.value.rendered) { + console.log('!this.state.rendered el', el, this.appElement, this.elements.dialog) + // @ts-ignore + el?.insertAdjacentElement(position, this.elements.dialog) + this.state.value.rendered = this.elements.dialog + + this.elements.dialog.addEventListener( + 'close', + this.events.close + ) + } + + this.events.render(this.state) + + return this + } +} + +export default DialogContructor diff --git a/src/components/nav.js b/src/components/nav.js index 1b25755..dbe7b9b 100644 --- a/src/components/nav.js +++ b/src/components/nav.js @@ -1,4 +1,6 @@ -import { lit as html } from '../helpers/lit.js' +import { + lit as html, +} from '../utils/generic.js' const initialState = { data: { diff --git a/src/components/send-request-btns.js b/src/components/send-request-btns.js index 413a3ed..81c2560 100644 --- a/src/components/send-request-btns.js +++ b/src/components/send-request-btns.js @@ -1,4 +1,6 @@ -import { lit as html } from '../helpers/lit.js' +import { + lit as html, +} from '../utils/generic.js' const initialState = { rendered: null, diff --git a/src/components/svg-sprite.js b/src/components/svg-sprite.js index 3fa2055..74b3c74 100644 --- a/src/components/svg-sprite.js +++ b/src/components/svg-sprite.js @@ -1,4 +1,6 @@ -import { lit as svg } from '../helpers/lit.js' +import { + lit as svg, +} from '../utils/generic.js' const initialState = { rendered: null, @@ -7,6 +9,10 @@ const initialState = { + diff --git a/src/components/transactions-list.js b/src/components/transactions-list.js index 07aadae..f45ce9e 100644 --- a/src/components/transactions-list.js +++ b/src/components/transactions-list.js @@ -1,11 +1,15 @@ -import { lit as html } from '../helpers/lit.js' +import { + lit as html, + timeago, + getAvatar, +} from '../utils/generic.js' import { envoy, restate, +} from '../utils/retort.js' +import { sortTransactionsByTime, - timeago, - getAvatar, -} from '../helpers/utils.js' +} from '../utils/dash/local.js' let _handlers = [] diff --git a/src/helpers/lit.js b/src/helpers/lit.js deleted file mode 100644 index f976e5a..0000000 --- a/src/helpers/lit.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * - * @param {TemplateStringsArray} s - * @param {...any} v - * - * @returns {string} - */ -export const lit = (s, ...v) => String.raw({ raw: s }, ...v) diff --git a/src/helpers/utils.js b/src/helpers/utils.js deleted file mode 100644 index 493808f..0000000 --- a/src/helpers/utils.js +++ /dev/null @@ -1,1215 +0,0 @@ -import { - DashHd, - DashPhrase, -} from '../imports.js' - -import { - DUFFS, - DASH_URI_REGEX, - OIDC_CLAIMS, - SUPPORTED_CLAIMS, - TIMEAGO_LOCALE_EN, - MOMENT, MOMENTS, NEVER, - SECONDS, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR, -} from './constants.js' - -/** - * - * @param {String} [phraseOrXkey] - * @param {Number} [accountIndex] - * @param {Number} [addressIndex] - * @param {Number} [usageIndex] - * - * @returns {Promise} - */ -export async function deriveWalletData( - phraseOrXkey, - accountIndex = 0, - addressIndex = 0, - usageIndex = DashHd.RECEIVE, -) { - if (!phraseOrXkey) { - throw new Error('Seed phrase or xkey value empty or invalid') - } - - let recoveryPhrase - let seed, derivedWallet, wpub, id, account - let xkey, xprv, xpub, xkeyId - let addressKey, addressKeyId, address - let secretSalt = ''; // "TREZOR"; - let recoveryPhraseArr = phraseOrXkey.trim().split(' ') - - if (recoveryPhraseArr?.length >= 12) { - recoveryPhrase = phraseOrXkey; - } - - if ( - ['xprv', 'xpub'].includes( - phraseOrXkey?.substring(0,4) || '' - ) - ) { - xkey = await DashHd.fromXKey(phraseOrXkey); - } else { - seed = await DashPhrase.toSeed(recoveryPhrase, secretSalt); - derivedWallet = await DashHd.fromSeed(seed); - wpub = await DashHd.toXPub(derivedWallet); - id = await DashHd.toId(derivedWallet); - account = await derivedWallet.deriveAccount(accountIndex); - xkey = await account.deriveXKey(usageIndex); - xprv = await DashHd.toXPrv(xkey); - } - - xkeyId = await DashHd.toId(xkey); - xpub = await DashHd.toXPub(xkey); - addressKey = await xkey.deriveAddress(addressIndex); - addressKeyId = await DashHd.toId(addressKey); - address = await DashHd.toAddr(addressKey.publicKey); - - return { - id, - accountIndex, - usageIndex, - addressIndex, - addressKeyId, - addressKey, - address, - xkeyId, - xkey, - xprv, - xpub, - seed, - wpub, - account, - derivedWallet, - recoveryPhrase, - } -} - -/** - * - * @param {Number} [accountIndex] - * @param {Number} [addressIndex] - * @param {Number} [use] - * - * @returns {Promise} - */ -export async function generateWalletData( - accountIndex = 0, - addressIndex = 0, - use = DashHd.RECEIVE -) { - let targetBitEntropy = 128; - let recoveryPhrase = await DashPhrase.generate(targetBitEntropy); - - return await deriveWalletData( - recoveryPhrase, - accountIndex, - addressIndex, - use - ) -} - -/** - * - * @example - * let acct = deriveAccountData(wallet, 0, 0, 0) - * - * @param {HDWallet} wallet - * @param {Number} [accountIndex] - * @param {Number} [addressIndex] - * @param {Number} [use] - * - * @returns - */ -export async function deriveAccountData( - wallet, - accountIndex = 0, - addressIndex = 0, - use = DashHd.RECEIVE, -) { - let account = await wallet.deriveAccount(accountIndex); - let xkey = await account.deriveXKey(use); - let xkeyId = await DashHd.toId(xkey); - let xprv = await DashHd.toXPrv(xkey); - let xpub = await DashHd.toXPub(xkey); - let xpubKey = await DashHd.fromXKey(xpub); - let xpubId = await DashHd.toId(xpubKey); - let key = await xkey.deriveAddress(addressIndex); - let address = await DashHd.toAddr(key.publicKey); - - return { - account, - xkeyId, - xkey, - xprv, - xpub, - xpubKey, - xpubId, - key, - address - } -} - -/** - * - * @example - * let addr = deriveAddressData(wallet, 0, 0, 0) - * - * @param {HDWallet} wallet - * @param {Number} [accountIndex] - * @param {Number} [addressIndex] - * @param {Number} [use] - * - * @returns - */ -export async function deriveAddressData( - wallet, - accountIndex = 0, - addressIndex = 0, - use = DashHd.RECEIVE, -) { - let account = await wallet.deriveAccount(accountIndex); - let xkey = await account.deriveXKey(use); - let key = await xkey.deriveAddress(addressIndex); - let address = await DashHd.toAddr(key.publicKey); - - return address -} - -// export async function batchAddressGenerate( -// wallet, -// accountIndex = 0, -// addressIndex = 0, -// use = DashHd.RECEIVE, -// batchSize = 20 -// ) { -// let batchLimit = addressIndex + batchSize -// let addresses = [] - -// let account = await wallet.deriveAccount(accountIndex); -// let xkey = await account.deriveXKey(use); - -// for (;addressIndex < batchLimit; addressIndex++) { -// let key = await xkey.deriveAddress(addressIndex); -// let address = await DashHd.toAddr(key.publicKey); -// addresses.push({ -// address, -// addressIndex, -// accountIndex, -// }) -// } - -// return { -// addresses, -// finalAddressIndex: addressIndex, -// } -// } - -export function phraseToEl(phrase, el = 'span', cls = 'tag') { - let words = phrase?.split(' ') - return words?.map( - w => `<${el} class="${cls}">${w}` - )?.join(' ') -} - -/** - * @param {Number} duffs - ex: 00000000 - * @param {Number} [fix] - value for toFixed - ex: 8 - */ -export function toDash(duffs, fix = 8) { - return (duffs / DUFFS).toFixed(fix); -} - -/** - * @param {String} dash - ex: 0.00000000 - */ -export function toDashStr(dash, pad = 12) { - return `Đ ` + `${dash}`.padStart(pad, " "); -} - -/** - * Based on https://stackoverflow.com/a/48100007 - * - * @param {Number} dash - ex: 0.00000000 - * @param {Number} [fix] - value for toFixed - ex: 8 - */ -export function fixedDash(dash, fix = 8) { - return ( - Math.trunc(dash * Math.pow(10, fix)) / Math.pow(10, fix) - ) - .toFixed(fix); -} - -// https://stackoverflow.com/a/27946310 -export function roundUsing(func, number, prec = 8) { - var tempnumber = number * Math.pow(10, prec); - tempnumber = func(tempnumber); - return tempnumber / Math.pow(10, prec); -} - -/** - * @param {Number} duffs - ex: 00000000 - */ -export function toDASH(duffs) { - let dash = toDash(duffs / DUFFS); - return toDashStr(dash); -} - -/** - * @param {Number} dash - ex: 0.00000000 - * @param {Number} [fix] - value for toFixed - ex: 8 - */ -export function fixedDASH(dash, fix = 8) { - return toDashStr(fixedDash(dash, fix)); -} - -/** - * @param {String} dash - ex: 0.00000000 - */ -export function toDuff(dash) { - return Math.round(parseFloat(dash) * DUFFS); -} - -export function formatDash( - unformattedBalance, - options = {}, -) { - let opts = { - maxlen: 10, - fract: 8, - sigsplit: 3, - ...options, - } - let funds = 0 - let balance = `${funds}` - - if (unformattedBalance) { - funds += unformattedBalance - balance = fixedDash(funds, opts.fract) - // TODO FIX: does not support large balances - - // console.log('balance fixedDash', balance, balance.length) - - let [fundsInt,fundsFract] = balance.split('.') - opts.maxlen -= fundsInt.length - - let fundsFraction = fundsFract?.substring( - 0, Math.min(Math.max(0, opts.maxlen), opts.sigsplit) - ) - - let fundsRemainder = fundsFract?.substring( - fundsFraction.length, - Math.max(0, opts.maxlen) - ) - - balance = `${ - fundsInt - }.${ - fundsFraction - }${ - fundsRemainder - }` - } - - return balance -} - -export function formDataEntries(event) { - let fd = new FormData( - event.target, - event.submitter - ) - - return Object.fromEntries(fd.entries()) -} - -export function copyToClipboard(target) { - target.select(); - document.execCommand("copy"); -} - -export function setClipboard(event) { - event.preventDefault() - let el = event.target?.previousElementSibling - let val = el.textContent?.trim() - if (el.nodeName === 'INPUT') { - val = el.value?.trim() - } - const type = "text/plain"; - const blob = new Blob([val], { type }); - - if ( - "clipboard" in navigator && - typeof navigator.clipboard.write === "function" - ) { - const data = [new ClipboardItem({ [type]: blob })]; - - navigator.clipboard.write(data).then( - cv => { - console.log('setClipboard', cv) - }, - ce => { - console.error('[fail] setClipboard', ce) - } - ); - } else { - copyToClipboard(el) - } -} - -export function openBlobSVG(target) { - const svgStr = new XMLSerializer().serializeToString(target); - const svgBlob = new Blob([svgStr], { type: "image/svg+xml" }); - const url = URL.createObjectURL(svgBlob); - const win = open(url); - win.onload = (evt) => URL.revokeObjectURL(url); -} - -/** - * Creates a `Proxy` wrapped object with optional listeners - * that react to changes - * - * @example - * let fooHistory = [] - * - * let kung = envoy( - * { foo: 'bar' }, - * function firstListener(state, oldState) { - * if (state.foo !== oldState.foo) { - * localStorage.foo = state.foo - * }, - * }, - * async function secondListener(state, oldState) { - * if (state.foo !== oldState.foo) { - * fooHistory.push(oldState.foo) - * } - * } - * ) - * kung.foo = 'baz' - * console.log(localStorage.foo) // 'baz' - * kung.foo = 'boo' - * console.log(fooHistory) // ['bar','baz'] - * - * @param {Object} obj - * @param {...( - * state: any, oldState: any, prop: string | symbol - * ) => void | Promise?} [initListeners] - * - * @returns {obj} - */ -export function envoy(obj, ...initListeners) { - let _listeners = [...initListeners] - return new Proxy(obj, { - get(obj, prop, receiver) { - if (prop === '_listeners') { - return _listeners - } - return Reflect.get(obj, prop, receiver) - }, - set(obj, prop, value) { - if ( - prop === '_listeners' && - Array.isArray(value) - ) { - _listeners = value - } - - _listeners.forEach( - fn => fn( - {...obj, [prop]: value}, - obj, - prop - ) - ) - - obj[prop] = value - - return true - } - }) -} - -/** - * Creates a reactive signal - * - * Inspired By - * {@link https://gist.github.com/developit/a0430c500f5559b715c2dddf9c40948d Valoo} & - * {@link https://dev.to/ratiu5/implementing-signals-from-scratch-3e4c Signals from Scratch} - * - * @example - * let count = createSignal(0) - * console.log(count.value) // 0 - * count.value = 2 - * console.log(count.value) // 2 - * - * let off = count.on((value) => { - * document.querySelector("body").innerHTML = value; - * }); - * - * off(); // unsubscribe - * - * @param {Object} initialValue inital value -*/ -export function createSignal(initialValue) { - let _value = initialValue; - let _last = _value; - const subs = []; - - function pub() { - for (let s of subs) { - s && s(_value, _last); - } - } - - return { - get value() { return _value; }, - set value(v) { - _last = _value - _value = v; - pub(); - }, - on: s => { - const i = subs.push(s)-1; - return () => { subs[i] = 0; }; - } - } -} - -export async function restate( - state = {}, - renderState = {}, -) { - let renderKeys = Object.keys(renderState) - - for await (let prop of renderKeys) { - state[prop] = renderState[prop] - } - - return state -} - -export function filterPairedContacts(contact) { - let outLen = Object.keys(contact.outgoing || {}).length - return outLen > 0 // && !!contact.alias -} - -export function filterUnpairedContacts(contact) { - return !filterPairedContacts(contact) -} - -export function sortContactsByAlias(a, b) { - const aliasA = a.alias || a.info?.preferred_username?.toUpperCase() || 'zzz'; - const aliasB = b.alias || b.info?.preferred_username?.toUpperCase() || 'zzz'; - - if (aliasA < aliasB) { - return -1; - } - if (aliasA > aliasB) { - return 1; - } - return 0; -} - -export function sortContactsByName(a, b) { - const nameA = a.info?.name?.toUpperCase(); - const nameB = b.info?.name?.toUpperCase(); - - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - return 0; -} - -export function sortTransactionsByTime(a, b) { - const timeA = a.time; - const timeB = b.time; - - if (timeA > timeB) { - return -1; - } - if (timeA < timeB) { - return 1; - } - return 0; -} - -export function DashURLSearchParams(params) { - let searchParams - let qry = {} - - Object.defineProperty(this, "entries", { - enumerable: false, - configurable: false, - writable: false, - value: () => Object.entries(qry), - }); - Object.defineProperty(this, "toString", { - enumerable: false, - configurable: false, - writable: false, - value: () => this.entries().map(p => p.join('=')).join('&'), - }); - Object.defineProperty(this, "size", { - get() { return this.entries().length }, - enumerable: false, - configurable: false, - }); - - if (typeof params === 'string' && params !== '') { - searchParams = params.split('&') - searchParams.forEach(q => { - let [prop,val] = q.split('=') - qry[prop] = val - }) - } - - if(Array.isArray(params) && params.length > 0) { - params.forEach(q => { - let [prop,val] = q - qry[prop] = val - }) - } - - // console.log('DashURLSearchParams', { - // params, searchParams, qry, - // qryStr: this.toString(), - // }) -} - -export function parseDashURI(uri) { - let result = {} - let parsedUri = [ - ...uri.matchAll(DASH_URI_REGEX) - ]?.[0]?.groups || {} - // let searchParams = new URLSearchParams(parsedUri?.params || '') - let searchParams = new DashURLSearchParams(parsedUri?.params || '') - - console.log( - 'parseDashURI', - parsedUri, - searchParams - ) - - if (parsedUri?.address) { - result.address = parsedUri?.address - } - - if (searchParams?.size > 0) { - let claims = Object.fromEntries( - searchParams?.entries() - ) - - for (let c in claims) { - if (SUPPORTED_CLAIMS.includes(c)) { - result[c] = claims[c] - } - } - } - - return result -} - -export function parseAddressField(uri) { - /* @type {Record} */ - let result = {} - - if (uri.includes(':')) { - let [protocol] = uri.split(':') - if (protocol.includes('dash')) { - // @ts-ignore - result = parseDashURI(uri) - } - } else if ( - 'xprv' === uri?.substring(0,4) - ) { - result.xprv = uri - } else if ( - 'xpub' === uri?.substring(0,4) - ) { - result.xpub = uri - } else { - result.address = uri - } - - return result -} - -export function isEmpty(value) { - if (value === null) { - return true - } - // if (typeof value === 'boolean' && value === false) { - // return true - // } - if (typeof value === 'string' && value?.length === 0) { - return true - } - if (typeof value === 'object' && Object.keys(value)?.length === 0) { - return true - } - if (Array.isArray(value) && value.length === 0) { - return true - } - return false; -} - -export function generateContactPairingURI( - state, - protocol = 'dash', // 'web+dash' - joiner = ':' -) { - let addr = state.wallet?.address || '' - let claims = [ - ["xpub", state.wallet?.xpub || ''], - ["sub", state.wallet?.xkeyId || ''], - ] - - if (state.userInfo) { - let filteredInfo = Array.from( - Object.entries(state.userInfo) - ).filter(p => { - let [key, val] = p - if ( - ![ - // 'updated_at', - 'email_verified', - 'phone_number_verified', - ].includes(key) && - !isEmpty(val) - ) { - return true - } - }) - - claims = [ - ...claims, - ...filteredInfo, - ] - } - - let scope = claims.map(p => p[0]).join(',') - // let searchParams = new URLSearchParams([ - // ...claims, - // ['scope', scope] - // ]) - let searchParams = new DashURLSearchParams([ - ...claims, - ['scope', scope] - ]) - - console.log( - 'Generate Dash URI claims', - claims, scope, searchParams, - searchParams.size, - searchParams.entries(), - ) - - let res = `${protocol}${joiner}${addr}` - - if (searchParams.size > 0) { - res += `?${searchParams.toString()}` - } - - return res -} - -export function generatePaymentRequestURI( - state, - protocol = 'dash', - joiner = ':' -) { - let addr = state.wallet?.address || '' - let claims = [] - - if (state.userInfo) { - let filteredInfo = Array.from( - Object.entries(state.userInfo) - ).filter(p => { - let [key, val] = p - if ( - ![ - 'updated_at', - 'email_verified', - 'phone_number_verified', - ].includes(key) && - !isEmpty(val) - ) { - return true - } - }) - - claims = [ - ...filteredInfo, - ] - } - - if (state.amount > 0) { - claims.push( - ["amount", state.amount], - ) - } - - if (state.label) { - claims.push( - ["label", state.label], - ) - } - - if (state.message) { - claims.push( - ["message", state.message], - ) - } - - // let searchParams = new URLSearchParams([ - // ...claims, - // ]) - let searchParams = new DashURLSearchParams([ - ...claims, - ]) - - let res = `${protocol}${joiner}${addr}` - - if (searchParams.size > 0) { - res += `?${searchParams.toString()}` - } - - return res -} - -export async function getStoreData( - store, - callback, - iterableCallback = res => async (v, k, i) => res.push(v) -) { - let result = [] - - return await store.keys().then(async function(keys) { - for (let k of keys) { - let v = await store.getItem(k) - await iterableCallback(result)(v, k) - } - - callback?.(result) - - return result - }).catch(function(err) { - console.error('getStoreData', err) - return null - }); -} - -export async function loadStore( - store, - callback, - iterableCallback = res => v => res.push(v) -) { - let result = [] - - return await store.iterate(iterableCallback(result)) - .then(() => callback?.(result)) - .catch(err => { - console.error('loadStore', err) - return null - }); -} - -export async function loadStoreObject(store, callback) { - // let storeLen = await store.length() - let result = {} - - return await store.iterate((v, k, i) => { - result[k] = v - - // if (i === storeLen) { - // return result - // } - }) - .then(() => callback?.(result)) - .then(() => result) - .catch(err => { - console.error('loadStoreObject', err) - return null - }); -} - -/** - * promise debounce changes - * - * https://www.freecodecamp.org/news/javascript-debounce-example/ - * - * @example - * const change = debounce((a) => console.log('Saving data', a)); - * change('b');change('c');change('d'); - * 'Saving data d' - * - * @param {(...args) => void} callback - * @param {number} [delay] -* -* @returns {Promise} -*/ -export async function debouncePromise(callback, delay = 300) { - let timer - - return await new Promise(resolve => async (...args) => { - clearTimeout(timer) - - timer = setTimeout(() => { - resolve(callback.apply(this, args)) - }, delay) - }) - - // return async (...args) => { - // clearTimeout(timer) - - // timer = resolve => setTimeout(() => { - // resolve(callback.apply(this, args)) - // }, delay) - - // return await new Promise(timer) - // } -} - -/** - * debounce changes - * - * https://www.freecodecamp.org/news/javascript-debounce-example/ - * - * @example - * const change = debounce((a) => console.log('Saving data', a)); - * change('b');change('c');change('d'); - * 'Saving data d' - * - * @param {(...args) => void} callback - * @param {number} [delay] -* -* @returns {(...args) => void} -*/ -export function debounce(callback, delay = 300) { - let timer - - return (...args) => { - clearTimeout(timer) - - timer = setTimeout(() => { - return callback.apply(this, args) - }, delay) - - return timer - } -} - -/** - * debounce that immediately triggers and black holes any extra - * executions within the time delay - * - * https://www.freecodecamp.org/news/javascript-debounce-example/ - * - * @example - * const dry = nobounce((a) => console.log('Saving data', a)); - * dry('b');dry('c');dry('d'); - * 'Saving data b' - * - * @param {(...args) => void} callback - * @param {number} [delay] -* -* @returns {(...args) => void} -*/ -export function nobounce(callback, delay = 300) { - let timer - - return (...args) => { - if (!timer) { - callback.apply(this, args) - } - - clearTimeout(timer) - - timer = setTimeout(() => { - timer = undefined - }, delay) - } -} - -export function timeago(ms, locale = TIMEAGO_LOCALE_EN) { - var ago = Math.floor(ms / 1000); - var part = 0; - - if (ago < MOMENTS) { return locale.moment; } - if (ago < SECONDS) { return locale.moments; } - if (ago < MINUTE) { return locale.seconds.replace(/%\w?/, `${ago}`); } - - if (ago < (2 * MINUTE)) { return locale.minute; } - if (ago < HOUR) { - while (ago >= MINUTE) { ago -= MINUTE; part += 1; } - return locale.minutes.replace(/%\w?/, `${part}`); - } - - if (ago < (2 * HOUR)) { return locale.hour; } - if (ago < DAY) { - while (ago >= HOUR) { ago -= HOUR; part += 1; } - return locale.hours.replace(/%\w?/, `${part}`); - } - - if (ago < (2 * DAY)) { return locale.day; } - if (ago < WEEK) { - while (ago >= DAY) { ago -= DAY; part += 1; } - return locale.days.replace(/%\w?/, `${part}`); - } - - if (ago < (2 * WEEK)) { return locale.week; } - if (ago < MONTH) { - while (ago >= WEEK) { ago -= WEEK; part += 1; } - return locale.weeks.replace(/%\w?/, `${part}`); - } - - if (ago < (2 * MONTH)) { return locale.month; } - if (ago < YEAR) { // 45 years, approximately the epoch - while (ago >= MONTH) { ago -= MONTH; part += 1; } - return locale.months.replace(/%\w?/, `${part}`); - } - - if (ago < NEVER) { - return locale.years; - } - - return locale.never; -} - -export async function sha256(str) { - const buf = await crypto.subtle.digest( - "SHA-256", new TextEncoder().encode(str) - ); - return Array.prototype.map.call( - new Uint8Array(buf), - x => (('00' + x.toString(16)).slice(-2)) - ).join(''); -} - -// https://stackoverflow.com/a/66494926 -export function getBackgroundColor(stringInput) { - let stringUniqueHash = [...stringInput].reduce((acc, char) => { - return char.charCodeAt(0) + ((acc << 5) - acc); - }, 0); - return `hsl(${stringUniqueHash % 360}, 100%, 67%)`; -} - -export async function getAvatarUrl( - email, - size = 48, - rating = 'pg', - srv = 'gravatar', -) { - let emailSHA = await sha256(email || '') - - if (srv === 'gravatar') { - return `https://gravatar.com/avatar/${ - emailSHA - }?s=${size}&r=${rating}&d=retro` - } - if (srv === 'libravatar') { - return `https://seccdn.libravatar.org/avatar/${ - emailSHA - }?s=${size}&r=${rating}&d=retro` - } - - return '' -} - -export async function getAvatar(c) { - let initials = c?.info?.name?. - split(' ').map(n => n[0]).slice(0,3).join('') || '' - let nameOrAlias = c?.info?.name || c?.alias || c?.info?.preferred_username - - if (!initials) { - initials = (c?.alias || c?.info?.preferred_username)?.[0] || '' - } - - let avStr = `
${initials}
` -} - -export function fileIsSubType(file, type) { - const fileType = file?.type?.split('/')?.[1] - - if (!fileType) { - return false - } - - return fileType === type -} - -// fileInTypes({type:'application/json'}, ['image/png']) -export function fileInMIMETypes(file, types = []) { - const fileType = file?.type - - if (!fileType) { - return false - } - - return types.includes(fileType) -} - -export function fileTypeInTypes(file, types = []) { - const fileType = file?.type?.split('/')?.[0] - - if (!fileType) { - return false - } - - return types.includes(fileType) -} - -export function fileTypeInSubtype(file, subtypes = []) { - const fileSubType = file?.type?.split('/')?.[1] - - if (!fileSubType) { - return false - } - - return subtypes.includes(fileSubType) -} - -export function readFile(file, options) { - let opts = { - expectedFileType: 'json', - denyFileTypes: ['audio','video','image','font','model'], - denyFileSubTypes: ['msword','xml'], - callback: () => {}, - errorCallback: () => {}, - ...options, - } - let reader = new FileReader(); - let result - - reader.addEventListener('load', () => { - if ( - fileTypeInTypes( - file, - opts.denyFileTypes, - ) || fileTypeInSubtype( - file, - opts.denyFileSubTypes, - ) - ) { - return opts.errorCallback?.({ - err: `Wrong file type: ${file.type}. Expected: ${opts.expectedFileType}.`, - file, - }) - } - - try { - // @ts-ignore - result = JSON.parse(reader?.result || '{}'); - - // console.log('parse loaded json', result); - - opts.callback?.(result, file) - - // state[key] = result - } catch(err) { - opts.errorCallback?.({ - err, - file, - }) - - throw new Error(`failed to parse JSON data from ${file.name}`) - } - }); - - reader.readAsText(file); -} - -export async function getRandomWords(len = 32) { - return await DashPhrase.generate(len) -} - -export async function verifyPhrase(phrase) { - return await DashPhrase.verify(phrase).catch(_ => false) -} - -export function isUniqueAlias(aliases, preferredAlias) { - return !aliases[preferredAlias] -} - -export async function getUniqueAlias(aliases, preferredAlias) { - let uniqueAlias = preferredAlias - let notUnique = !isUniqueAlias(aliases, uniqueAlias) - - if (notUnique) { - let aliasArr = uniqueAlias.split('_') - let randomWords = (await getRandomWords()).split(' ') - - if (aliasArr.length > 1) { - let lastWord = aliasArr.pop() - let index = DashPhrase.base2048.indexOf(lastWord); - - if (index < 0) { - aliasArr.push(lastWord) - } else { - aliasArr.push(randomWords[0]) - } - } else { - aliasArr.push(randomWords[0]) - } - - uniqueAlias = aliasArr.join('_') - - return await getUniqueAlias(aliases, uniqueAlias) - } - - return uniqueAlias -} - -export function getPartialHDPath(wallet) { - return [ - wallet.accountIndex, - wallet.usageIndex, - wallet.addressIndex, - ].join('/') -} - -export function getAddressIndexFromUsage(wallet, account, usageIdx) { - let usageIndex = usageIdx ?? wallet?.usageIndex ?? 0 - let addressIndex = account.usage?.[usageIndex] ?? account.addressIndex ?? 0 - let usage = account.usage ?? [ - account.addressIndex ?? 0, - 0 - ] - - // console.log( - // 'getAddressIndexFromUsage', - // usageIndex, - // addressIndex, - // account, - // usage, - // ) - - return { - ...account, - usage, - usageIndex, - addressIndex, - } -} diff --git a/src/helpers/wallet.js b/src/helpers/wallet.js deleted file mode 100644 index 78de19f..0000000 --- a/src/helpers/wallet.js +++ /dev/null @@ -1,2032 +0,0 @@ -import { - DashWallet, - DashTx, - DashHd, - DashSight, - DashSocket, - Cryptic, -} from '../imports.js' -import { - DatabaseSetup, -} from './db.js' -import { - deriveWalletData, - getAddressIndexFromUsage, - loadStoreObject, -} from './utils.js' -import { - STOREAGE_SALT, OIDC_CLAIMS, - KS_CIPHER, KS_PRF, USAGE, -} from './constants.js' -import { - walletFunds, -} from '../state/index.js' - -// @ts-ignore -import blake from 'blakejs' -// @ts-ignore -import { keccak_256 } from '@noble/hashes/sha3' - -// @ts-ignore -export const dashsight = DashSight.create({ - baseUrl: 'https://insight.dash.org', - // baseUrl: 'https://dashsight.dashincubator.dev', -}); - -let defaultSocketEvents = { - onClose: async (e) => console.log('onClose', e), - onError: async (e) => console.log('onError', e), - onMessage: async (e, data) => console.log('onMessage', e, data), -} - -// Cryptic.setConfig({ -// // cipherAlgorithm: 'AES-GCM', -// // cipherLength: 256, -// // hashingAlgorithm: 'SHA-256', -// // derivationAlgorithm: 'PBKDF2', -// iterations: 1000, -// }) - -export async function initDashSocket( - events = {} -) { - // @ts-ignore - let dashsocket = DashSocket.create({ - dashsocketBaseUrl: 'https://insight.dash.org/socket.io', - cookieStore: null, - debug: true, - ...defaultSocketEvents, - ...events, - }) - - await dashsocket.init() - .catch((e) => console.log('dashsocket catch err', e)); - - // setTimeout(() => { - // dashsocket.close() - // }, 15*60*1000); - - return dashsocket -} - -export const store = await DatabaseSetup() - -export async function getStoredItems(targStore) { - let result = {} - let storeLen = await targStore.length() - - return await targStore.iterate(( - value, key, iterationNumber - ) => { - result[key] = value - - if (iterationNumber === storeLen) { - return result - } - }) -} - -export async function getFilteredStoreLength(targStore, query = {}) { - let resLength = 0 - let storeLen = await targStore.length() - let qs = Object.entries(query) - - // console.log('getFilteredStoreLength qs', { - // storeName: targStore?._config?.storeName, - // storeLen, - // qs, - // }) - - if (storeLen === 0) { - return 0 - } - - return await targStore.iterate(( - value, key, iterationNumber - ) => { - let res = true - - // console.log('getFilteredStoreLength qs before each', key, res) - - qs.forEach(([k,v]) => { - // console.log('getFilteredStoreLength qs each', k, v, value[k]) - if (k === 'key' && key !== v || value[k] !== v) { - res = undefined - } - }) - - // console.log('getFilteredStoreLength qs after each', key, res) - - if (res) { - resLength += 1 - } - - if (iterationNumber === storeLen) { - return resLength - } - }) -} - -export async function findInStore(targStore, query = {}) { - let result = {} - let storeLen = await targStore.length() - let qs = Object.entries(query) - // console.log('findInStore qs', qs) - - return await targStore.iterate(( - value, key, iterationNumber - ) => { - let res = value - - // console.log('findInStore qs before each', key, res) - - qs.forEach(([k,v]) => { - // console.log('findInStore qs each', k, v, value[k]) - if (k === 'key' && key !== v || value[k] !== v) { - res = undefined - } - }) - - // console.log('findInStore qs after each', key, res) - - if (res) { - result[key] = res - } - - if (iterationNumber === storeLen) { - return result - } - }) -} - -export async function findOneInStore(targStore, query = {}) { - let storeLen = await targStore.length() - let qs = Object.entries(query) - - return await targStore.iterate(( - value, key, iterationNumber - ) => { - let res = value - - qs.forEach(([k,v]) => { - if (k === 'key' && key !== v || value[k] !== v) { - res = undefined - } - }) - - if (res) { - return res - } - - if (iterationNumber === storeLen) { - return undefined - } - }) -} - -export async function getUnusedChangeAddress(account) { - let filterQuery = { - xkeyId: account.xkeyId, - usageIndex: DashHd.CHANGE, - } - - let foundAddrs = await findInStore(store.addresses, filterQuery) - - for (let [fkey,fval] of Object.entries(foundAddrs)) { - if (!fval.insight?.balance) { - return fkey - } - } - - // return foundAddr.address - return null -} - -export async function loadWalletsForAlias($alias) { - $alias.$wallets = {} - - if ($alias?.wallets) { - for (let w of $alias.wallets) { - let wallet = await store.wallets.getItem(w) - $alias.$wallets[w] = wallet - } - } - - return $alias -} - -export async function initWalletsInfo( - info = {}, -) { - let wallets = await getStoredItems(store.wallets) - - info = { - ...OIDC_CLAIMS, - ...info, - } - - let alias = info.preferred_username - - wallets = Object.values(wallets || {}) - wallets = wallets - .filter(w => w.alias === alias) - .map(w => w.id) - - return { - alias, - wallets, - info, - } -} - -export async function decryptWallet( - decryptPass, - decryptIV, - decryptSalt, - ciphertext, -) { - const cryptic = Cryptic.create( - decryptPass, - decryptSalt, - // Cryptic.bufferToHex( - // Cryptic.stringToBuffer(decryptSalt) - // ) - ) - - return await cryptic.decrypt(ciphertext, decryptIV); -} - -export function blake256(data) { - if ('string' === typeof data) { - data = Cryptic.hexToBuffer(data) - } - const context = blake.blake2bInit(32, null); - blake.blake2bUpdate(context, data); - return Cryptic.toHex(blake.blake2bFinal(context)); -} - -export function getKeystoreData(keystore) { - const { - ciphertext, - cipher, - mac, - } = keystore.crypto - const [ - cipherAlgorithm, - cipherLength, - ] = KS_CIPHER[cipher] - - const derivationAlgorithm = keystore.crypto.kdf.toUpperCase() - const hashingAlgorithm = KS_PRF[keystore.crypto.kdfparams.prf] - const derivedKeyLength = keystore?.crypto?.kdfparams?.dklen - const iterations = keystore.crypto.kdfparams.c - const iv = keystore.crypto.cipherparams.iv - const ivBuffer = Cryptic.hexToBuffer(iv) - const salt = keystore.crypto.kdfparams.salt - const saltBuffer = Cryptic.hexToBuffer(salt) - - const keyLength = derivedKeyLength / 2 - const numBits = (keyLength + iv.length) * 8 - - return { - cipher, - cipherAlgorithm, - cipherLength, - ciphertext, - mac, - derivationAlgorithm, - hashingAlgorithm, - derivedKeyLength, - iterations, - iv, - ivBuffer, - salt, - saltBuffer, - keyLength, - numBits, - } -} - -export async function setupCryptic( - encryptionPassword, - keystore, -) { - const ks = getKeystoreData(keystore) - const { - cipherLength, cipherAlgorithm, - derivationAlgorithm, hashingAlgorithm, iv, - iterations, salt, - } = ks - - Cryptic.setConfig({ - cipherAlgorithm, - cipherLength, - hashingAlgorithm, - derivationAlgorithm, - iterations, - }) - - const cryptic = Cryptic.create( - encryptionPassword, - salt, - ); - - return { - Cryptic, - cryptic, - ks, - } -} - -export async function encryptData( - encryptionPassword, - keystore, - data, -) { - const { cryptic, ks } = await setupCryptic( - encryptionPassword, - keystore, - ) - - return await cryptic.encrypt(data, ks.iv); -} - -export async function decryptData( - encryptionPassword, - keystore, - data, -) { - const { cryptic, ks } = await setupCryptic( - encryptionPassword, - keystore, - ) - - return await cryptic.decrypt(data, ks.iv) -} - -export function storedData( - encryptionPassword, - keystore, -) { - const SD = {} - - SD.decryptData = async function(data) { - if (data && 'string' === typeof data && data.length > 0) { - data = JSON.parse(await decryptData( - encryptionPassword, - keystore, - data - )) - } - - return data - } - - SD.decryptItem = async function(targetStore, item,) { - let data = await targetStore.getItem( - item, - ) - - data = await SD.decryptData(data) - - return data - } - - /** - * - * @param {*} targetStore - * @param {*} item - * @param {*} data - * @param {*} extend - * @returns {Promise<[String,Object]>} - */ - SD.encryptData = async function( - targetStore, item, data = {}, extend = true - ) { - let encryptedData = '' - let storedData = {} - let jsonData = {} - if (extend) { - // storedData = await targetStore.getItem( - // item, - // ) - storedData = await SD.decryptItem( - targetStore, - item - ) - } - - if (data) { - jsonData = { - ...storedData, - ...data, - } - encryptedData = await encryptData( - encryptionPassword, - keystore, - JSON.stringify(jsonData) - ) - } - - return [ - encryptedData, - jsonData, - ] - } - - SD.encryptItem = async function( - targetStore, item, data = {}, extend = true - ) { - let encryptedData = '' - let encryptedResult = '' - let result = {} - - if (data || extend) { - let d = await SD.encryptData(targetStore, item, data, extend) - encryptedResult = d[0] - result = d[1] - encryptedData = await targetStore.setItem( - item, - encryptedResult - ) - } - - return result || data || encryptedData - // return encryptedData - } - - return SD -} - -export async function decryptKeystore( - encryptionPassword, - keystore, -) { - const { Cryptic, cryptic, ks } = await setupCryptic( - encryptionPassword, - keystore, - ) - - const derivedBytes = await cryptic.deriveBits(ks.numBits, ks.salt) - - const bMAC = blake256([ - ...new Uint8Array(derivedBytes.slice(16, 32)), - ...Cryptic.toBytes(ks.ciphertext), - ]) - const kMAC = Cryptic.toHex(keccak_256(new Uint8Array([ - ...new Uint8Array(derivedBytes.slice(16, 32)), - ...Cryptic.toBytes(ks.ciphertext), - ]))); - - if (ks.mac && ![bMAC, kMAC].includes(ks.mac)) { - throw new Error('Invalid password') - } - - return await cryptic.decrypt(ks.ciphertext, ks.iv) -} - -export function genKeystore( - // aes-256-gcm - cipher = 'aes-128-ctr', - salt = Cryptic.randomBytes(32), - iv = Cryptic.randomBytes(16), - iterations = 262144, - id = crypto.randomUUID(), -) { - return { - crypto: { - cipher, - ciphertext: '', - cipherparams: { - iv: Cryptic.bufferToHex(iv), - }, - kdf: "pbkdf2", - kdfparams: { - c: iterations, - dklen: 32, - prf: "hmac-sha256", - salt: Cryptic.bufferToHex(salt), - }, - mac: '', - }, - id, - meta: 'dash-incubator-keystore', - version: 3, - } -} - -export async function encryptKeystore( - encryptionPassword, - recoveryPhrase, -) { - let keystore = genKeystore() - const { Cryptic, cryptic, ks } = await setupCryptic( - encryptionPassword, - keystore, - ) - - const derivedBytes = await cryptic.deriveBits(ks.numBits, ks.salt) - const encryptedPhrase = await cryptic.encrypt(recoveryPhrase, ks.iv); - - keystore.crypto.ciphertext = encryptedPhrase - - const bMAC = blake256([ - ...new Uint8Array(derivedBytes.slice(16, 32)), - ...Cryptic.toBytes(keystore.crypto.ciphertext), - ]) - const kMAC = Cryptic.toHex(keccak_256(new Uint8Array([ - ...new Uint8Array(derivedBytes.slice(16, 32)), - ...Cryptic.toBytes(keystore.crypto.ciphertext), - ]))); - - keystore.crypto.mac = bMAC - - // console.log( - // 'encrypted keystore', - // ks, - // { - // encryptedPhrase, - // // keyMaterial, - // // derivedKey, - // // derivedBytes, - // }, - // { - // bMAC, - // kMAC, - // }, - // ) - - return keystore -} - -export async function generateAddressIterator( - xkey, - xkeyId, - addressIndex, -) { - let key = await xkey.deriveAddress(addressIndex); - let address = await DashHd.toAddr(key.publicKey); - - return { - address, - addressIndex, - usageIndex: xkey.index, - xkeyId, - } -} - -export async function generateAndStoreAddressIterator( - xkey, - xkeyId, - walletId, - accountIndex, - addressIndex, - usageIndex = DashHd.RECEIVE, -) { - let { address } = await generateAddressIterator( - xkey, - xkeyId, - addressIndex, - ) - - // console.log( - // 'generateAddressIterator', - // {xkey, xkeyId, key, address, accountIndex, addressIndex}, - // ) - - store.addresses.getItem(address) - .then(a => { - let $addr = a || {} - // console.log( - // 'generateAddressIterator store.addresses.getItem', - // {address, $addr}, - // ) - - store.addresses.setItem( - address, - { - ...$addr, - updatedAt: Date.now(), - walletId, - xkeyId, - accountIndex, - addressIndex, - usageIndex, - }, - ) - }) - - return { - address, - addressIndex, - accountIndex, - usageIndex: xkey.index, - xkeyId, - } -} - -export async function batchXkeyAddressGenerate( - wallet, - addressIndex = 0, - batchSize = 20, -) { - let batchLimit = addressIndex + batchSize - let addresses = [] - - for (let addrIdx = addressIndex; addrIdx < batchLimit; addrIdx++) { - addresses.push( - await generateAddressIterator( - wallet.xkey, - wallet.xkeyId, - addrIdx, - ) - ) - } - - return { - addresses, - finalAddressIndex: batchLimit, - } -} - -export async function batchAddressGenerate( - wallet, - accountIndex = 0, - addressIndex = 0, - usageIndex = DashHd.RECEIVE, - batchSize = 20, -) { - // let hdpath = `m/44'/5'/${accountIndex}'/${usageIndex}/${addressIndex}`, - let batchLimit = addressIndex + batchSize - let addresses = [] - - let account = await wallet.derivedWallet.deriveAccount(accountIndex); - let xkey = await account.deriveXKey(usageIndex); - let xkeyId = await DashHd.toId(xkey); - - if (usageIndex !== DashHd.RECEIVE) { - let xkeyReceive = await account.deriveXKey(DashHd.RECEIVE); - xkeyId = await DashHd.toId(xkeyReceive); - } - - for (let addrIdx = addressIndex; addrIdx < batchLimit; addrIdx++) { - addresses.push( - await generateAndStoreAddressIterator( - xkey, - xkeyId, - wallet.id, - accountIndex, - addrIdx, - usageIndex, - ) - ) - } - - return { - addresses, - finalAddressIndex: batchLimit, - } -} - -export async function batchAddressUsageGenerate( - wallet, - accountIndex = 0, - addressIndex = 0, - batchSize = 20, -) { - // let hdpath = `m/44'/5'/${accountIndex}'/${usageIndex}/${addressIndex}`, - let batchLimit = addressIndex + batchSize - let addresses = [] - - let account = await wallet.derivedWallet.deriveAccount(accountIndex); - let xkeyReceive = await account.deriveXKey(DashHd.RECEIVE); - let xkeyChange = await account.deriveXKey(DashHd.CHANGE); - let xkeyId = await DashHd.toId(xkeyReceive); - - console.log( - 'batchAddressUsageGenerate', - {batchLimit, account, xkeyReceive, xkeyChange}, - ) - - for (let addrIdx = addressIndex; addrIdx < batchLimit; addrIdx++) { - addresses.push( - await generateAndStoreAddressIterator( - xkeyReceive, - xkeyId, - wallet.id, - accountIndex, - addrIdx, - DashHd.RECEIVE, - ) - ) - addresses.push( - await generateAndStoreAddressIterator( - xkeyChange, - xkeyId, - wallet.id, - accountIndex, - addrIdx, - DashHd.CHANGE, - ) - ) - } - - return { - addresses, - finalAddressIndex: batchLimit, - } -} - -export async function initWallet( - encryptionPassword, - wallet, - keystore, - accountIndex = 0, - addressIndex = 0, - infoOverride = {}, -) { - let { - alias, - wallets, - info, - } = await initWalletsInfo(infoOverride) - - let { id, recoveryPhrase } = wallet - - // console.log( - // 'initWallet wallets', - // wallets, - // info, - // ) - - if (!wallets.includes(id)) { - wallets.push(id) - } - - let addrs = await batchAddressUsageGenerate( - wallet, - accountIndex, - addressIndex, - ) - - console.log('init wallet batchAddressUsageGenerate', addrs) - - for (let a of addrs.addresses) { - store.addresses.setItem( - a.address, - { - updatedAt: Date.now(), - walletId: wallet.id, - accountIndex: a.accountIndex, - addressIndex: a.addressIndex, - usageIndex: a.usageIndex, - xkeyId: a.xkeyId, - } - ) - } - - let storeWallet = await store.wallets.setItem( - `${id}`, - { - id, - updatedAt: Date.now(), - accountIndex, - addressIndex: addrs?.finalAddressIndex || addressIndex, - keystore: keystore || await encryptKeystore( - encryptionPassword, - recoveryPhrase - ), - } - ) - - let storedAlias = await store.aliases.setItem( - `${alias}`, - await encryptData( - encryptionPassword, - storeWallet.keystore, - JSON.stringify({ - wallets, - info, - }) - ) - ) - - // console.log( - // 'initWallet stored values', - // storeWallet, - // storedAlias, - // ) - - let contacts = '{}' - - return { - wallets, - contacts, - } -} - -// export async function checkWalletFunds(addr, wallet = {}) { -// const HOUR = 1000 * 60 * 60; - -// let { -// address, -// accountIndex, -// addressIndex, -// usageIndex, -// } = addr -// let updatedAt = Date.now() -// let $addr = await store.addresses.getItem(address) || {} - -// $addr = { -// walletId: wallet.id, -// accountIndex, -// addressIndex, -// usageIndex, -// ...$addr, -// } -// // console.log('checkWalletFunds $addr', $addr) -// let walletFunds = $addr?.insight - -// if ( -// !walletFunds?.updatedAt || -// updatedAt - walletFunds?.updatedAt > HOUR -// ) { -// // console.info('check insight api for addr', addr) - -// let insightRes = await dashsight.getInstantBalance(address) - -// if (insightRes) { -// let { addrStr, ...res } = insightRes -// walletFunds = res - -// $addr.insight = { -// ...walletFunds, -// updatedAt, -// } - -// store.addresses.setItem( -// address, -// $addr, -// ) -// } -// } - -// // console.info('check addr funds', addr, walletFunds) - -// return $addr -// } - -export async function updateAddrFunds( - wallet, insightRes, -) { - let updatedAt = Date.now() - let { addrStr, ...res } = insightRes - let $addr = await store.addresses.getItem(addrStr) || {} - let { - walletId, - xkeyId, - } = $addr - - // console.log( - // 'checkWalletFunds $addr', - // $addr, - // walletId, - // wallet?.id, - // walletId === wallet?.id - // ) - - if (walletId && walletId === wallet?.id) { - let storedWallet = await store.wallets.getItem(walletId) || {} - let storedAccount = await store.accounts.getItem(xkeyId) || {} - - $addr.insight = { - ...res, - updatedAt, - } - - store.addresses.setItem( - addrStr, - $addr, - ) - - if (storedAccount.usage[$addr.usageIndex] < $addr.addressIndex) { - storedAccount.usage[$addr.usageIndex] = $addr.addressIndex - store.accounts.setItem( - xkeyId, - storedAccount - ) - } - if (storedWallet.accountIndex < $addr.accountIndex) { - store.wallets.setItem( - walletId, - { - ...storedWallet, - accountIndex: $addr.accountIndex, - } - ) - let storeAcctLen = (await store.accounts.length())-1 - - console.log('updateAddrFunds', { - acctIdx: $addr.accountIndex, - storeAcctLen, - sameOrBigger: $addr.accountIndex >= storeAcctLen, - }) - - if ($addr.accountIndex >= storeAcctLen) { - batchGenAccts(wallet.recoveryPhrase, $addr.accountIndex) - .then(() => { - // updateAllFunds(wallet) - batchGenAcctsAddrs(wallet) - .then(accts => { - console.log('batchGenAcctsAddrs', { accts }) - - updateAllFunds(wallet) - .then(funds => { - console.log('updateAllFunds then funds', funds) - }) - .catch(err => console.error('catch updateAllFunds', err, wallet)) - }) - }) - } - } - - return res - } - - return { balance: 0 } -} - -export async function updateAllFunds(wallet) { - let funds = 0 - let addrKeys = await store.addresses.keys() - - if (addrKeys.length === 0) { - walletFunds.balance = funds - return funds - } - - console.log( - 'updateAllFunds getInstantBalances for', - {addrKeys}, - addrKeys.length, - ) - - let balances = await dashsight.getInstantBalances(addrKeys) - // let txs = await dashsight.getAllTxs( - // addrKeys - // ) - - // console.log('getAllTxs', txs) - - if (balances.length >= 0) { - walletFunds.balance = funds - } - - // add insight balances to address - for (const insightRes of balances) { - let { addrStr } = insightRes - let addrIdx = addrKeys.indexOf(addrStr) - if (addrIdx > -1) { - addrKeys.splice(addrIdx, 1) - } - funds += (await updateAddrFunds(wallet, insightRes))?.balance || 0 - walletFunds.balance = funds - } - - // remove insight balances from address - for (const addr of addrKeys) { - let { insight, ...$addr } = await store.addresses.getItem(addr) || {} - - // walletFunds.balance = funds - (_insight?.balance || 0) - - store.addresses.setItem( - addr, - $addr, - ) - } - - console.log('updateAllFunds funds', {balances, funds}) - - return funds -} - -export async function getTotalFunds(wallet) { - let funds = 0 - let result = {} - let addrsLen = await store.addresses.length() - - return await store.addresses.iterate(( - value, key, iterationNumber - ) => { - if (value?.walletId === wallet?.id) { - result[key] = value - funds += value?.insight?.balance || 0 - } - - if (iterationNumber === addrsLen) { - return funds - } - }) -} - -export async function getAddrsWithFunds(wallet) { - let result = {} - let addrsLen = await store.addresses.length() - - return await store.addresses.iterate(( - value, key, iterationNumber - ) => { - if ( - value?.walletId === wallet?.id && - value?.insight?.balance > 0 - ) { - result[key] = { - ...value, - address: key, - } - } - - if (iterationNumber === addrsLen) { - return result - } - }) -} - -export async function batchGenAccts( - phrase, - accountIndex = 0, - batchSize = 5, -) { - let $accts = await getStoredItems(store.accounts) - // let $acctsArr = Object.values($accts) - let accts = {} - let batch = batchSize + accountIndex - - console.log( - 'BATCH GENERATED ACCOUNTS START', - { - $accts, - // $acctsArr, - accountIndex, - batch, - } - ) - - for (let i = accountIndex; i < batch; i++) { - let acctWallet = await deriveWalletData( - phrase, - i, - ) - - if (!$accts[acctWallet.xkeyId]) { - let newAccount = await store.accounts.setItem( - acctWallet.xkeyId, - { - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - accountIndex: i, - usage: [0,0], - walletId: acctWallet.id, - xkeyId: acctWallet.xkeyId, - addressKeyId: acctWallet.addressKeyId, - address: acctWallet.address, - } - ) - - accts[`acct__${i}`] = [ acctWallet, newAccount ] - } - - // accts[`acct__${i}`] = batchGenAcctAddrs( - // acctWallet, - // newAccount, - // ) - } - - // let allBatches = Promise.allSettled(Object.values(accts)) - - return accts -} - -export async function batchGenAcctAddrs( - wallet, - account, - usageIndex = -1, - batchSize = 20, -) { - // console.log('batchGenAcctAddrs account', account, usageIndex) - - let filterQuery = { - accountIndex: account.accountIndex, - } - - if (usageIndex >= 0) { - filterQuery.usageIndex = usageIndex - } - - let acctAddrsLen = await getFilteredStoreLength( - store.addresses, - filterQuery, - ) - - // console.log('getFilteredStoreLength res', acctAddrsLen) - - let addrUsageIdx = account.usage?.[usageIndex] || 0 - let addrIdx = addrUsageIdx - let batSize = batchSize - - if (acctAddrsLen === 0) { - addrIdx = 0 - batSize = addrUsageIdx + batchSize - } - - if (acctAddrsLen <= addrUsageIdx + (batchSize / 2)) { - if (usageIndex >= 0) { - return await batchAddressGenerate( - wallet, - account.accountIndex, - account.usage[usageIndex], - usageIndex, - batSize, - ) - } else { - return await batchAddressUsageGenerate( - wallet, - account.accountIndex, - addrIdx, - batSize, - ) - } - } - - return null -} - -export async function batchGenAcctsAddrs( - wallet, - usageIndex = -1, - batchSize = 20, -) { - let $accts = await getStoredItems(store.accounts) - let $acctsArr = Object.values($accts) - let accts = {} - - if ($acctsArr.length > 0) { - for (let $a of $acctsArr) { - accts[`bat__${$a.accountIndex}`] = await batchGenAcctAddrs( - wallet, - $a, - usageIndex, - batchSize, - ) - } - - // console.warn( - // 'BATCH GENERATED ACCOUNTS', - // accts, - // ) - } - - return accts -} - -export async function getAccountWallet(wallet, phrase) { - let acctFromStore = await store.accounts.getItem( - wallet.xkeyId, - ) || {} - let acctFromStoreWallet = getAddressIndexFromUsage( - wallet, - acctFromStore, - ) - - if (acctFromStoreWallet?.addressIndex > 0) { - return { - wallet: await deriveWalletData( - phrase, - acctFromStoreWallet.accountIndex, - acctFromStoreWallet.addressIndex, - acctFromStoreWallet?.usageIndex ?? USAGE.RECEIVE, - ), - account: acctFromStore, - } - } - - return { - wallet, - account: acctFromStore, - } -} - -export async function forceInsightUpdateForAddress(addr) { - let currentAddr = await store.addresses.getItem( - addr - ) - await store.addresses.setItem( - addr, - { - ...currentAddr, - insight: { - ...currentAddr.insight, - updatedAt: 0 - } - } - ) -} - -export function sortAddrs(a, b) { - // Ascending Lexicographical on TxId (prev-hash) in-memory (not wire) byte order - if (a.accountIndex > b.accountIndex) { - return 1; - } - if (a.accountIndex < b.accountIndex) { - return -1; - } - // addressIndex - // Ascending Vout (Numerical) - let indexDiff = a.addressIndex - b.addressIndex; - return indexDiff; -} - -export function getBalance(utxos) { - return utxos.reduce(function (total, utxo) { - return total + utxo.satoshis; - }, 0); -} - -export function selectOptimalUtxos(utxos, output) { - let balance = getBalance(utxos); - let fees = DashTx.appraise({ - //@ts-ignore - inputs: [{}], - //@ts-ignore - outputs: [{}], - }); - - let fullSats = output + fees.min; - - if (balance < fullSats) { - return []; - } - - // from largest to smallest - utxos.sort(function (a, b) { - return b.satoshis - a.satoshis; - }); - - // /** @type Array */ - let included = []; - let total = 0; - - // try to get just one - utxos.every(function (utxo) { - if (utxo.satoshis > fullSats) { - included[0] = utxo; - total = utxo.satoshis; - return true; - } - return false; - }); - if (total) { - return included; - } - - // try to use as few coins as possible - utxos.some(function (utxo, i) { - included.push(utxo); - total += utxo.satoshis; - if (total >= fullSats) { - return true; - } - - // it quickly becomes astronomically unlikely to hit the one - // exact possibility that least to paying the absolute minimum, - // but remains about 75% likely to hit any of the mid value - // possibilities - if (i < 2) { - // 1 input 25% chance of minimum (needs ~2 tries) - // 2 inputs 6.25% chance of minimum (needs ~8 tries) - fullSats = fullSats + DashTx.MIN_INPUT_SIZE; - return false; - } - // but by 3 inputs... 1.56% chance of minimum (needs ~32 tries) - // by 10 inputs... 0.00953674316% chance (needs ~524288 tries) - fullSats = fullSats + DashTx.MIN_INPUT_SIZE + 1; - }); - return included; -} - -export async function deriveTxWallet( - fromWallet, - fundAddrs, -) { - let cachedAddrs = {} - let privateKeys = {} - let coreUtxos - // let transactions - let tmpWallet - - if (Array.isArray(fundAddrs) && fundAddrs.length > 0) { - fundAddrs.sort(sortAddrs) - - for (let w of fundAddrs) { - tmpWallet = await deriveWalletData( - fromWallet.recoveryPhrase, - w.accountIndex, - w.addressIndex, - w.usageIndex, - ) - privateKeys[tmpWallet.address] = tmpWallet.addressKey.privateKey - cachedAddrs[w.address] = { - checked_at: w.updatedAt, - hdpath: `m/44'/${DashWallet.COIN_TYPE}'/${w.accountIndex}'/${w.usageIndex}`, - index: w.addressIndex, - wallet: w.walletId, // maybe `selectedAlias`? - txs: [], - utxos: [], - } - } - - coreUtxos = await dashsight.getMultiCoreUtxos( - Object.keys(privateKeys) - ) - // transactions = await dashsight.getAllTxs( - // Object.keys(privateKeys) - // ) - } else { - tmpWallet = await deriveWalletData( - fromWallet.recoveryPhrase, - fundAddrs.accountIndex, - fundAddrs.addressIndex, - fundAddrs.usageIndex, - ) - privateKeys[tmpWallet.address] = tmpWallet.addressKey.privateKey - cachedAddrs[fundAddrs.address] = { - checked_at: fundAddrs.updatedAt, - hdpath: `m/44'/${DashWallet.COIN_TYPE}'/${fundAddrs.accountIndex}'/${fundAddrs.usageIndex}`, - index: fundAddrs.addressIndex, - wallet: fundAddrs.walletId, // maybe `selectedAlias`? - txs: [], - utxos: [], - } - coreUtxos = await dashsight.getCoreUtxos( - tmpWallet.address - ) - // transactions = await dashsight.getAllTxs( - // [tmpWallet.address] - // ) - } - - // console.log('getAllTxs', transactions) - - return { - privateKeys, - cachedAddrs, - coreUtxos, - // transactions, - } -} - -export async function createOptimalTx( - fromWallet, - fundAddrs, - changeAddrs, - recipient, - amount, -) { - const MIN_FEE = 191; - const DUST = 2000; - const amountSats = DashTx.toSats(amount) - - console.log('amount to send', { - amount, - amountSats, - fundAddrs, - }) - - let changeAddr = changeAddrs[0] - - let { - privateKeys, - coreUtxos, - } = await deriveTxWallet(fromWallet, fundAddrs) - - let optimalUtxos = selectOptimalUtxos( - coreUtxos, - amountSats, - ) - - console.log('utxos', { - core: coreUtxos, - optimal: optimalUtxos, - }) - - let recipientAddr = recipient?.address || recipient - - let payments = [ - { - address: recipientAddr, - satoshis: amountSats, - }, - ] - - let spendableDuffs = optimalUtxos.reduce(function (total, utxo) { - return total + utxo.satoshis; - }, 0) - let spentDuffs = payments.reduce(function (total, output) { - return total + output.satoshis; - }, 0) - let unspentDuffs = spendableDuffs - spentDuffs - - let txInfo = { - inputs: optimalUtxos, - outputs: payments, - } - - let sizes = DashTx.appraise(txInfo) - let midFee = sizes.mid - - if (unspentDuffs < MIN_FEE) { - throw new Error( - `overspend: inputs total '${spendableDuffs}', but outputs total '${spentDuffs}', which leaves no way to pay the fee of '${sizes.mid}'`, - ) - } - - txInfo.inputs.sort(DashTx.sortInputs) - - let outputs = txInfo.outputs.slice(0) - let change - - change = unspentDuffs - (midFee + DashTx.OUTPUT_SIZE) - if (change < DUST) { - change = 0 - } - if (change) { - txInfo.outputs = outputs.slice(0); - txInfo.outputs.push({ - address: changeAddr, - satoshis: change, - }) - } - - txInfo.outputs.sort(DashTx.sortOutputs) - - let keys = optimalUtxos.map( - utxo => privateKeys[utxo.address] - ) - - return [ - txInfo, - keys, - changeAddr, - ] -} - -export async function createStandardTx( - fromWallet, - fundAddrs, - changeAddrs, - recipient, - amount, - fullTransfer = false, -) { - const amountSats = DashTx.toSats(amount) - - console.log('amount to send', { - amount, - amountSats, - }) - - let selection - let receiverOutput - let outputs = [] - let { - privateKeys, - coreUtxos, - cachedAddrs, - } = await deriveTxWallet(fromWallet, fundAddrs) - let changeAddr = changeAddrs[0] - - let recipientAddr = recipient?.address || recipient - - // @ts-ignore - let dashwallet = await DashWallet.create({ - safe: { - cache: { - addresses: cachedAddrs - } - }, - store: { - save: data => console.log('dashwallet.store.save', {data}) - }, - dashsight, - }) - - receiverOutput = DashWallet._parseSendInfo(dashwallet, amountSats); - - selection = dashwallet.useMatchingCoins({ - output: receiverOutput, - utxos: coreUtxos, - breakChange: false, - }) - - console.log('coreUtxos', { - coreUtxos, - selection, - amount, - amountSats, - fullTransfer, - }) - - let stampVal = dashwallet.__STAMP__ * selection.output.stampsPerCoin - let receiverDenoms = receiverOutput?.denoms.slice(0) - - for (let denom of selection.output.denoms) { - let address = ''; - let matchingDenomIndex = receiverDenoms.indexOf(denom) - if (matchingDenomIndex >= 0) { - void receiverDenoms.splice(matchingDenomIndex, 1) - address = recipientAddr - } else { - address = changeAddr - } - - let coreOutput = { - address, - // address: addrsInfo.addresses.pop(), - satoshis: denom + stampVal, - faceValue: denom, - stamps: selection.output.stampsPerCoin, - } - - outputs.push(coreOutput) - } - - let txInfo = { - inputs: selection.inputs, - outputs: outputs, - }; - - txInfo.outputs.sort(DashTx.sortOutputs) - txInfo.inputs.sort(DashTx.sortInputs) - - let keys = txInfo.inputs.map( - utxo => privateKeys[utxo.address] - ) - - return [ - txInfo, - keys, - changeAddr, - ] -} - -export async function createDraftTx( - fromWallet, - fundAddrs, - changeAddrs, - recipient, - amount, - fullTransfer = false, -) { - const amountSats = DashTx.toSats(amount) - - console.log('amount to send', { - amount, - amountSats, - }) - - let { - privateKeys, - coreUtxos, - cachedAddrs, - } = await deriveTxWallet(fromWallet, fundAddrs) - let changeAddr = changeAddrs[0] - - let recipientAddr = recipient?.address || recipient - - // @ts-ignore - let dashwallet = await DashWallet.create({ - safe: { - cache: { - addresses: cachedAddrs - } - }, - store: { - save: data => console.log('dashwallet.store.save', {data}) - }, - dashsight, - }) - - let utxos = null; - let inputs = null; - let output = { - address: recipientAddr, - satoshis: amountSats - }; - - if (coreUtxos) { - inputs = coreUtxos; - if (fullTransfer) { - output.satoshis = null; - } - } else { - utxos = coreUtxos; - } - - let txDraft = await dashwallet.legacy.draftTx({ - utxos, - inputs, - output, - feeSize: 'max', - }) - - if (txDraft.change) { - txDraft.change.address = changeAddr; - } - - let keys = txDraft.inputs.map( - utxo => privateKeys[utxo.address] - ) - - let txSummary = await dashwallet.legacy.finalizeAndSignTx(txDraft, keys); - - return { - ...txSummary, - changeAddr, - } -} - -export async function createTx( - fromWallet, - fundAddrs, - changeAddrs, - recipient, - amount, - fullTransfer = false, - mode = null, -) { - let tmpTx - let dashTx = DashTx.create({ - // @ts-ignore - version: 3, - }); - - if (fullTransfer) { - let tx = await createDraftTx( - fromWallet, - fundAddrs, - changeAddrs, - recipient, - amount, - fullTransfer, - ) - - console.log('fullTransfer tx', tx); - - return { - tx, - changeAddr: tx.changeAddr, - fee: tx.fee, - // fee: inFee - outFee, - } - } else if (mode === 'cash') { - // Denominated TX - tmpTx = await createStandardTx( - fromWallet, - fundAddrs, - changeAddrs, - recipient, - amount, - fullTransfer, - ) - } else { - // Non-Denominated TX - tmpTx = await createOptimalTx( - fromWallet, - fundAddrs, - changeAddrs, - recipient, - amount, - ) - } - - let [txInfo, keys, changeAddr] = tmpTx - - let inFee = txInfo.inputs.reduce((acc, cur) => acc + cur.satoshis, 0) - let outFee = txInfo.outputs.reduce((acc, cur) => acc + cur.satoshis, 0) - - console.log('txInfo', { - txInfo, - calcFee: { - in: inFee, - out: outFee, - fee: inFee - outFee, - }, - }); - - let tx = await dashTx.hashAndSignAll(txInfo, keys); - - console.log('tx', tx); - - return { - tx, - changeAddr, - fee: inFee - outFee, - } -} - -export async function sendTx( - tx, -) { - let txHex = tx.transaction; - - console.log('txHex', [txHex]); - - let result = await dashsight.instantSend(txHex); - - console.log('instantSend result', result); - - return result -} - - -export function processInOut({ - conAddr, tx, addr, dir, sentAmount = null, receivedAmount = null, - byAlias = {}, byAddress = {}, byTx = {}, -}) { - let alias = byTx?.[tx.txid]?.alias || conAddr.alias - byAlias[conAddr.alias] = { - ...(byAlias[conAddr.alias] || []), - [tx.txid]: { - addr, - dir, - sentAmount, - receivedAmount, - ...tx, - ...conAddr, - alias, - } - } - byAddress[addr] = [ - ...(byAddress[addr] || []), - { - receivedAmount, - sentAmount, - dir, - ...tx, - ...conAddr, - alias, - } - ] - byTx[tx.txid] = { - receivedAmount, - sentAmount, - dir, - ...tx, - ...conAddr, - alias, - } - - // console.log( - // 'processInOut', - // conAddr.alias, conAddr.xkeyId, tx, - // ) - - return { - byAlias, - byAddress, - byTx, - } -} - -export async function getAddrsTransactions({ - appState, addrs, contactAddrs = {}, - txs = [], -}) { - let storeAddrs = await loadStoreObject(store.addresses) - if (txs.length === 0) { - txs = await dashsight.getAllTxs(addrs) - } - let byAddress = {} - let byAlias = {} - let byTx = {} - - // console.log('getAddrsTransactions', { - // txs, addrs, contactAddrs, appT: appState.transactions - // }) - - for await (let tx of txs) { - let dir = 'received' - let conAddr - let sentAmount = 0 - let receivedAmount = 0 - - for await (let vin of tx.vin) { - let addr = vin.addr - conAddr = contactAddrs[addr] - - if(storeAddrs[addr]) { - dir = 'sent' - sentAmount += Number(vin.value) - } - - if (conAddr) { - processInOut({ - tx, addr, conAddr, dir, sentAmount, - byAlias, byAddress, byTx, - }) - } - } - - for await (let vout of tx.vout) { - if (vout?.scriptPubKey?.addresses) { - for await (let addr of vout.scriptPubKey.addresses) { - // let addr = vout.scriptPubKey.addresses[0] - conAddr = contactAddrs[addr] - - if(storeAddrs[addr]) { - receivedAmount += Number(vout.value) - } else { - // sentAmount -= Number(vout.value) - } - - if (conAddr) { - processInOut({ - tx, addr, conAddr, dir, receivedAmount, - byAlias, byAddress, byTx, - }) - } - } - } - } - - byTx[tx.txid] = { - ...byTx[tx.txid], - receivedAmount, - sentAmount, - } - - if (!appState.transactions[tx.txid]?.vin) { - store.transactions.setItem( - tx.txid, - { - ...(appState.transactions[tx.txid] || {}), - ...tx, - dir, - sentAmount, - receivedAmount, - }, - ) - } - } - - // console.log('getAddrsTransactions by Tx', byTx) - // console.log('getAddrsTransactions by Alias', byAlias) - // console.log('getAddrsTransactions by Address', byAddress) - - return { - byAddress, - byAlias, - byTx, - } -} - -export async function getContactsByXkeyId( - appState, -) { - let contactsXkeys = {} - - for await (let c of appState.contacts) { - let og = Object.values(c.outgoing || {})?.[0] - let ic = Object.values(c.incoming || {})?.[0] - - if (og) { - contactsXkeys[og.xkeyId] = { - ...c, - dir: 'outgoing', - } - } - if (ic) { - contactsXkeys[ic.xkeyId] = { - ...c, - dir: 'incoming', - } - } - } - - return contactsXkeys -} - -export async function getContactsFromAddrs( - appState, -) { - let accts = await loadStoreObject(store.accounts) - let addrs = await loadStoreObject(store.addresses) - let contactAddrs = await getContactsByXkeyId(appState) - let contacts = {} - - for await (let [ck,cv] of Object.entries(addrs)) { - let contact = contactAddrs[cv.xkeyId] - let acct = accts[cv.xkeyId] - if (contact) { - contacts[ck] = contact - } else if (acct) { - contacts[ck] = { - ...acct, - alias: null, - } - } - } - - return contacts -} - -export async function deriveContactAddrs( - appState, dir = 'outgoing', -) { - let addrs = {} - - for await (let c of appState.contacts) { - let og = Object.values(c[dir] || {})?.[0] - let xkey = og?.xpub || og?.xprv - - if (xkey) { - let contactWallet = await deriveWalletData( - xkey, - ) - let contactAddrs = await batchXkeyAddressGenerate( - contactWallet, - contactWallet.addressIndex, - ) - - contactAddrs.addresses.forEach(g => { - addrs[g.address] = { - alias: c.alias, - xkeyId: contactWallet.xkeyId, - dir, - } - }) - } - } - - // console.log('deriveContactAddrs', { - // asc: appState.contacts, - // addrs, - // }) - - return addrs -} - -export async function getTxs(appState, transactions = []) { - let contactAddrs = await getContactsFromAddrs(appState) - let contactOutAddrs = await deriveContactAddrs(appState) - - contactAddrs = { - ...contactAddrs, - ...contactOutAddrs, - } - - let addrs = Object.keys(contactAddrs) - - if (addrs.length === 0) { - return - } - - // let TxStore = await store.transactions.keys() - // let txs = await dashsight.getAllTxs(addrs) - - let txs = await getAddrsTransactions({ - appState, addrs, contactAddrs, - txs: transactions, - }) - - // console.log('getTxs', { - // txs, contactAddrs, contactOutAddrs, addrs - // }) - - return txs -} - -export function getTransactionsByContactAlias(appState) { - return async res => { - if (!res) { - return [] - } - - appState.contacts = res - - // let contactAddrs = await deriveContactAddrs(appState) || {} - // let addrs = Object.keys(contactAddrs) - - // console.log('contactAddrs', addrs) - - // if (addrs?.length) { - // getAddrsTransactions({ - // appState, addrs, contactAddrs - // }) - // } - - // console.log('contacts', res, contactAddrs) - - return res - } -} diff --git a/src/imports.js b/src/imports.js index 9057285..985bf1c 100644 --- a/src/imports.js +++ b/src/imports.js @@ -11,6 +11,7 @@ * * See https://github.com/jojobyte/browser-import-rabbit-hole */ +import './secp.js'; import '../node_modules/dashtx/dashtx.js'; import '../node_modules/dashkeys/dashkeys.js'; @@ -20,10 +21,13 @@ import '../node_modules/dashsight/dashsight.js'; import '../node_modules/dashsight/dashsocket.js'; import '../node_modules/@dashincubator/base58check/base58check.js'; import '../node_modules/@dashincubator/ripemd160/ripemd160.js'; -import '../node_modules/@dashincubator/secp256k1/secp256k1.js'; +// import '../node_modules/@dashincubator/secp256k1/secp256k1.js'; import '../node_modules/crypticstorage/cryptic.js'; import '../node_modules/dashwallet/dashwallet.js'; import '../node_modules/localforage/dist/localforage.js'; +import '../node_modules/crowdnode/dashcore-lit.js'; +import '../node_modules/crowdnode/dashapi.js'; +import '../node_modules/crowdnode/crowdnode.js'; import * as DashTxTypes from '../node_modules/dashtx/dashtx.js'; import * as DashKeysTypes from '../node_modules/dashkeys/dashkeys.js'; @@ -37,6 +41,9 @@ import * as Secp256k1Types from '../node_modules/@dashincubator/secp256k1/secp25 import * as CrypticTypes from '../node_modules/crypticstorage/cryptic.js'; import * as CrypticStorageTypes from '../node_modules/crypticstorage/storage.js'; import * as DashWalletTypes from '../node_modules/dashwallet/dashwallet.js'; +import * as DashCoreTypes from '../node_modules/crowdnode/dashcore-lit.js'; +import * as DashApiTypes from '../node_modules/crowdnode/dashapi.js'; +import * as CrowdNodeTypes from '../node_modules/crowdnode/crowdnode.js'; // import * as LocalForageTypes from '../node_modules/localforage/dist/localforage.js'; /** @type {DashTxTypes} */ @@ -56,7 +63,10 @@ export let Base58Check = window?.Base58Check || globalThis?.Base58Check; /** @type {RIPEMD160Types} */ export let RIPEMD160 = window?.RIPEMD160 || globalThis?.RIPEMD160; /** @type {Secp256k1Types} */ -export let Secp256k1 = window?.nobleSecp256k1 || globalThis?.nobleSecp256k1; +export let Secp256k1 = ( + window?.nobleSecp256k1 || globalThis?.nobleSecp256k1 || + window?.Secp256k1 || globalThis?.Secp256k1 +); /** @type {CrypticTypes} */ export let Cryptic = window?.Cryptic || globalThis?.Cryptic; @@ -65,11 +75,17 @@ export let CrypticStorage = window?.CrypticStorage || globalThis?.CrypticStorage; /** @type {DashWalletTypes} */ export let DashWallet = window?.Wallet || globalThis?.Wallet; +/** @type {CrowdNodeTypes} */ +export let CrowdNode = window?.CrowdNode || globalThis?.CrowdNode export let localforage = window?.localforage || globalThis?.localforage; export default { + Base58Check, + CrowdNode, + Cryptic, + CrypticStorage, DashWallet, DashTx, DashKeys, @@ -77,10 +93,7 @@ export default { DashPhrase, DashSight, DashSocket, - Base58Check, + localforage, RIPEMD160, Secp256k1, - Cryptic, - CrypticStorage, - localforage, }; diff --git a/src/index.css b/src/index.css index afa2fa0..11382a1 100644 --- a/src/index.css +++ b/src/index.css @@ -96,16 +96,35 @@ figure h4 { font-size: 2rem; } -.mh-25 { +.min-h-auto { + min-height: auto; +} +.min-h-0 { + min-height: 0; +} +.min-h-25 { + min-height: 25%; +} +.min-h-50 { + min-height: 50%; +} +.min-h-75 { + min-height: 75%; +} +.min-h-100 { + min-height: 100%; +} + +.max-h-25 { max-height: 25%; } -.mh-50 { +.max-h-50 { max-height: 50%; } -.mh-75 { +.max-h-75 { max-height: 75%; } -.mh-100 { +.max-h-100 { max-height: 100%; } @@ -167,9 +186,24 @@ figure h4 { .jc-start { justify-content: start; } +.jc-center { + justify-content: center; +} +.jc-right { + justify-content: right; +} .jc-end { justify-content: end; } +.jc-around { + justify-content: space-around; +} +.jc-between { + justify-content: space-between; +} +.jc-evenly { + justify-content: space-evenly; +} .as-start { align-self: start; } @@ -320,6 +354,28 @@ figure h4 { font-size: 1.5rem; } +.p-0 { + padding: 0; +} +.p-1 { + padding: .25rem; +} +.p-2 { + padding: .75rem; +} +.p-3 { + padding: 1rem; +} +.p-4 { + padding: 1.5rem; +} +.p-5 { + padding: 2rem; +} +.p-6 { + padding: 2.5rem; +} + .px-0 { padding-left: 0; padding-right: 0; diff --git a/src/index.html b/src/index.html index 9722a8f..71a1ac2 100644 --- a/src/index.html +++ b/src/index.html @@ -47,6 +47,7 @@ } } + diff --git a/src/libs/path-to-regexp.js b/src/libs/path-to-regexp.js deleted file mode 100644 index 6394b64..0000000 --- a/src/libs/path-to-regexp.js +++ /dev/null @@ -1,490 +0,0 @@ -// @ts-nocheck - -/** - * https://unpkg.com/path-to-regexp@6.2.1/dist/index.js - * https://github.com/pillarjs/path-to-regexp - * https://github.com/expressjs/express/blob/master/lib/router/layer.js - */ - -/** - * Tokenize input string. - */ -function lexer(str) { - var tokens = []; - var i = 0; - while (i < str.length) { - var char = str[i]; - if (char === '*' || char === '+' || char === '?') { - tokens.push({ type: 'MODIFIER', index: i, value: str[i++] }); - continue; - } - if (char === '\\') { - tokens.push({ type: 'ESCAPED_CHAR', index: i++, value: str[i++] }); - continue; - } - if (char === '{') { - tokens.push({ type: 'OPEN', index: i, value: str[i++] }); - continue; - } - if (char === '}') { - tokens.push({ type: 'CLOSE', index: i, value: str[i++] }); - continue; - } - if (char === ':') { - var name = ''; - var j = i + 1; - while (j < str.length) { - var code = str.charCodeAt(j); - if ( - // `0-9` - (code >= 48 && code <= 57) || - // `A-Z` - (code >= 65 && code <= 90) || - // `a-z` - (code >= 97 && code <= 122) || - // `_` - code === 95 - ) { - name += str[j++]; - continue; - } - break; - } - if (!name) throw new TypeError('Missing parameter name at '.concat(i)); - tokens.push({ type: 'NAME', index: i, value: name }); - i = j; - continue; - } - if (char === '(') { - var count = 1; - var pattern = ''; - var j = i + 1; - if (str[j] === '?') { - throw new TypeError('Pattern cannot start with "?" at '.concat(j)); - } - while (j < str.length) { - if (str[j] === '\\') { - pattern += str[j++] + str[j++]; - continue; - } - if (str[j] === ')') { - count--; - if (count === 0) { - j++; - break; - } - } else if (str[j] === '(') { - count++; - if (str[j + 1] !== '?') { - throw new TypeError( - 'Capturing groups are not allowed at '.concat(j), - ); - } - } - pattern += str[j++]; - } - if (count) throw new TypeError('Unbalanced pattern at '.concat(i)); - if (!pattern) throw new TypeError('Missing pattern at '.concat(i)); - tokens.push({ type: 'PATTERN', index: i, value: pattern }); - i = j; - continue; - } - tokens.push({ type: 'CHAR', index: i, value: str[i++] }); - } - tokens.push({ type: 'END', index: i, value: '' }); - return tokens; -} - -/** - * Parse a string for the raw tokens. - */ -export function parse(str, options) { - if (options === void 0) { - options = {}; - } - var tokens = lexer(str); - var _a = options.prefixes, - prefixes = _a === void 0 ? './' : _a; - var defaultPattern = '[^'.concat( - escapeString(options.delimiter || '/#?'), - ']+?', - ); - var result = []; - var key = 0; - var i = 0; - var path = ''; - var tryConsume = function (type) { - if (i < tokens.length && tokens[i].type === type) return tokens[i++].value; - }; - var mustConsume = function (type) { - var value = tryConsume(type); - if (value !== undefined) return value; - var _a = tokens[i], - nextType = _a.type, - index = _a.index; - throw new TypeError( - 'Unexpected ' - .concat(nextType, ' at ') - .concat(index, ', expected ') - .concat(type), - ); - }; - var consumeText = function () { - var result = ''; - var value; - while ((value = tryConsume('CHAR') || tryConsume('ESCAPED_CHAR'))) { - result += value; - } - return result; - }; - while (i < tokens.length) { - var char = tryConsume('CHAR'); - var name = tryConsume('NAME'); - var pattern = tryConsume('PATTERN'); - if (name || pattern) { - var prefix = char || ''; - if (prefixes.indexOf(prefix) === -1) { - path += prefix; - prefix = ''; - } - if (path) { - result.push(path); - path = ''; - } - result.push({ - name: name || key++, - prefix: prefix, - suffix: '', - pattern: pattern || defaultPattern, - modifier: tryConsume('MODIFIER') || '', - }); - continue; - } - var value = char || tryConsume('ESCAPED_CHAR'); - if (value) { - path += value; - continue; - } - if (path) { - result.push(path); - path = ''; - } - var open = tryConsume('OPEN'); - if (open) { - var prefix = consumeText(); - var name_1 = tryConsume('NAME') || ''; - var pattern_1 = tryConsume('PATTERN') || ''; - var suffix = consumeText(); - mustConsume('CLOSE'); - result.push({ - name: name_1 || (pattern_1 ? key++ : ''), - pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1, - prefix: prefix, - suffix: suffix, - modifier: tryConsume('MODIFIER') || '', - }); - continue; - } - mustConsume('END'); - } - return result; -} - -/** - * Compile a string to a template function for the path. - */ -export function compile(str, options) { - return tokensToFunction(parse(str, options), options); -} - -/** - * Expose a method for transforming tokens into the path function. - */ -export function tokensToFunction(tokens, options) { - if (options === void 0) { - options = {}; - } - var reFlags = flags(options); - var _a = options.encode, - encode = - _a === void 0 - ? function (x) { - return x; - } - : _a, - _b = options.validate, - validate = _b === void 0 ? true : _b; - // Compile all the tokens into regexps. - var matches = tokens.map(function (token) { - if (typeof token === 'object') { - return new RegExp('^(?:'.concat(token.pattern, ')$'), reFlags); - } - }); - return function (data) { - var path = ''; - for (var i = 0; i < tokens.length; i++) { - var token = tokens[i]; - if (typeof token === 'string') { - path += token; - continue; - } - var value = data ? data[token.name] : undefined; - var optional = token.modifier === '?' || token.modifier === '*'; - var repeat = token.modifier === '*' || token.modifier === '+'; - if (Array.isArray(value)) { - if (!repeat) { - throw new TypeError( - 'Expected "'.concat( - token.name, - '" to not repeat, but got an array', - ), - ); - } - if (value.length === 0) { - if (optional) continue; - throw new TypeError( - 'Expected "'.concat(token.name, '" to not be empty'), - ); - } - for (var j = 0; j < value.length; j++) { - var segment = encode(value[j], token); - if (validate && !matches[i].test(segment)) { - throw new TypeError( - 'Expected all "' - .concat(token.name, '" to match "') - .concat(token.pattern, '", but got "') - .concat(segment, '"'), - ); - } - path += token.prefix + segment + token.suffix; - } - continue; - } - if (typeof value === 'string' || typeof value === 'number') { - var segment = encode(String(value), token); - if (validate && !matches[i].test(segment)) { - throw new TypeError( - 'Expected "' - .concat(token.name, '" to match "') - .concat(token.pattern, '", but got "') - .concat(segment, '"'), - ); - } - path += token.prefix + segment + token.suffix; - continue; - } - if (optional) continue; - var typeOfMessage = repeat ? 'an array' : 'a string'; - throw new TypeError( - 'Expected "'.concat(token.name, '" to be ').concat(typeOfMessage), - ); - } - return path; - }; -} - -/** - * Create path match function from `path-to-regexp` spec. - */ -export function match(str, options) { - var keys = []; - var re = pathToRegexp(str, keys, options); - return regexpToFunction(re, keys, options); -} - -/** - * Create a path match function from `path-to-regexp` output. - */ -export function regexpToFunction(re, keys, options) { - if (options === void 0) { - options = {}; - } - var _a = options.decode, - decode = - _a === void 0 - ? function (x) { - return x; - } - : _a; - return function (pathname) { - var m = re.exec(pathname); - if (!m) return false; - var path = m[0], - index = m.index; - var params = Object.create(null); - var _loop_1 = function (i) { - if (m[i] === undefined) return 'continue'; - var key = keys[i - 1]; - if (key.modifier === '*' || key.modifier === '+') { - params[key.name] = m[i] - .split(key.prefix + key.suffix) - .map(function (value) { - return decode(value, key); - }); - } else { - params[key.name] = decode(m[i], key); - } - }; - for (var i = 1; i < m.length; i++) { - _loop_1(i); - } - return { path: path, index: index, params: params }; - }; -} - -/** - * Escape a regular expression string. - */ -function escapeString(str) { - return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1'); -} - -/** - * Get the flags for a regexp from the options. - */ -function flags(options) { - return options && options.sensitive ? '' : 'i'; -} - -/** - * Pull out keys from a regexp. - */ -function regexpToRegexp(path, keys) { - if (!keys) return path; - var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; - var index = 0; - var execResult = groupsRegex.exec(path.source); - while (execResult) { - keys.push({ - // Use parenthesized substring match if available, index otherwise - name: execResult[1] || index++, - prefix: '', - suffix: '', - modifier: '', - pattern: '', - }); - execResult = groupsRegex.exec(path.source); - } - return path; -} - -/** - * Transform an array into a regexp. - */ -function arrayToRegexp(paths, keys, options) { - var parts = paths.map(function (path) { - return pathToRegexp(path, keys, options).source; - }); - return new RegExp('(?:'.concat(parts.join('|'), ')'), flags(options)); -} - -/** - * Create a path regexp from string input. - */ -function stringToRegexp(path, keys, options) { - return tokensToRegexp(parse(path, options), keys, options); -} - -/** - * Expose a function for taking tokens and returning a RegExp. - */ -export function tokensToRegexp(tokens, keys, options) { - if (options === void 0) { - options = {}; - } - var _a = options.strict, - strict = _a === void 0 ? false : _a, - _b = options.start, - start = _b === void 0 ? true : _b, - _c = options.end, - end = _c === void 0 ? true : _c, - _d = options.encode, - encode = - _d === void 0 - ? function (x) { - return x; - } - : _d, - _e = options.delimiter, - delimiter = _e === void 0 ? '/#?' : _e, - _f = options.endsWith, - endsWith = _f === void 0 ? '' : _f; - var endsWithRe = '['.concat(escapeString(endsWith), ']|$'); - var delimiterRe = '['.concat(escapeString(delimiter), ']'); - var route = start ? '^' : ''; - // Iterate over the tokens and create our regexp string. - for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { - var token = tokens_1[_i]; - if (typeof token === 'string') { - route += escapeString(encode(token)); - } else { - var prefix = escapeString(encode(token.prefix)); - var suffix = escapeString(encode(token.suffix)); - if (token.pattern) { - if (keys) keys.push(token); - if (prefix || suffix) { - if (token.modifier === '+' || token.modifier === '*') { - var mod = token.modifier === '*' ? '?' : ''; - route += '(?:' - .concat(prefix, '((?:') - .concat(token.pattern, ')(?:') - .concat(suffix) - .concat(prefix, '(?:') - .concat(token.pattern, '))*)') - .concat(suffix, ')') - .concat(mod); - } else { - route += '(?:' - .concat(prefix, '(') - .concat(token.pattern, ')') - .concat(suffix, ')') - .concat(token.modifier); - } - } else { - if (token.modifier === '+' || token.modifier === '*') { - route += '((?:' - .concat(token.pattern, ')') - .concat(token.modifier, ')'); - } else { - route += '('.concat(token.pattern, ')').concat(token.modifier); - } - } - } else { - route += '(?:' - .concat(prefix) - .concat(suffix, ')') - .concat(token.modifier); - } - } - } - if (end) { - if (!strict) route += ''.concat(delimiterRe, '?'); - route += !options.endsWith ? '$' : '(?='.concat(endsWithRe, ')'); - } else { - var endToken = tokens[tokens.length - 1]; - var isEndDelimited = - typeof endToken === 'string' - ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 - : endToken === undefined; - if (!strict) { - route += '(?:'.concat(delimiterRe, '(?=').concat(endsWithRe, '))?'); - } - if (!isEndDelimited) { - route += '(?='.concat(delimiterRe, '|').concat(endsWithRe, ')'); - } - } - return new RegExp(route, flags(options)); -} - -/** - * Normalize the given path string, returning a regular expression. - * - * An empty array can be passed in for the keys, which will hold the - * placeholder key descriptions. For example, using `/user/:id`, `keys` will - * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. - */ -export function pathToRegexp(path, keys, options) { - if (path instanceof RegExp) return regexpToRegexp(path, keys); - if (Array.isArray(path)) return arrayToRegexp(path, keys, options); - return stringToRegexp(path, keys, options); -} diff --git a/src/main.js b/src/main.js index 3f06a08..92f9a41 100644 --- a/src/main.js +++ b/src/main.js @@ -1,52 +1,58 @@ -import { lit as html } from './helpers/lit.js' - import { - generateWalletData, - deriveWalletData, - getStoreData, - loadStoreObject, - formDataEntries, -} from './helpers/utils.js' + DUFFS, + DIALOG_STATUS, +} from './utils/constants.js' import { - DUFFS, -} from './helpers/constants.js' + lit as html, + formDataEntries, +} from './utils/generic.js' import { - findInStore, - initDashSocket, batchGenAccts, batchGenAcctAddrs, batchGenAcctsAddrs, batchXkeyAddressGenerate, - updateAllFunds, - decryptKeystore, - getStoredItems, - loadWalletsForAlias, - store, - createTx, - sendTx, + deriveWalletData, + generateWalletData, + getAccountWallet, getAddrsWithFunds, - storedData, getUnusedChangeAddress, - getAccountWallet, + loadWalletsForAlias, + getTransactionsByContactAlias, +} from './utils/dash/local.js' + +import { + createTx, dashsight, getAddrsTransactions, - getTransactionsByContactAlias, getTxs, -} from './helpers/wallet.js' + initDashSocket, + store, + sendTx, + updateAllFunds, +} from './utils/dash/network.js' + +import { + decryptKeystore, + storedData, +} from './utils/cryptic.js' import { - localForageBaseCfg, - importFromJson, exportWalletData, + findInStore, + getStoreData, + getStoredItems, + importFromJson, + loadStoreObject, + localForageBaseCfg, saveJsonToFile, -} from './helpers/db.js' +} from './utils/db.js' import { + appDialogs, appState, appTools, - appDialogs, userInfo, walletFunds, } from './state/index.js' @@ -79,6 +85,8 @@ import pairQrRig from './rigs/pair-qr.js' import txInfoRig from './rigs/tx-info.js' import showErrorDialog from './rigs/show-error.js' +import crowdnodeTransactionRig from './rigs/crowdnode-tx.js' + // app/data state let accounts let wallets @@ -131,6 +139,7 @@ let contactsList = await setupContactsList( } let contactAccountID = Object.values(contactData.incoming || {})?.[0]?.accountIndex + console.log('contact click data', contactData) let shareAccount = await deriveWalletData( @@ -431,6 +440,40 @@ async function showNotification({ console.log('notification', {type, title, msg, sticky}) } +async function showQrCode(state = {}) { + let initState = { + name: 'Share to receive funds', + submitTxt: `Edit Amount or Contact`, + submitAlt: `Change the currently selected contact`, + footer: state => html` +
+ +
+ `, + amount: 0, + wallet, + contacts: appState.contacts, + ...state, + } + + let showRequestQRRender = await appDialogs.requestQr.render( + initState, + 'afterend', + ) + + let showRequestQR = await appDialogs.requestQr.showModal() + + return showRequestQRRender +} + async function main() { appState.encryptionPassword = window.atob( sessionStorage.encryptionPassword || '' @@ -673,32 +716,9 @@ async function main() { // } // ) - await appDialogs.requestQr.render( - { - name: 'Share to receive funds', - submitTxt: `Edit Amount or Contact`, - submitAlt: `Change the currently selected contact`, - footer: state => html` -
- -
- `, - amount: 0, - wallet, - contacts: appState.contacts, - }, - 'afterend', - ) + showQrCode() - let showRequestQR = await appDialogs.requestQr.showModal() + // let showRequestQR = await appDialogs.requestQr.showModal() } } else { await appDialogs.sendOrReceive.render({ @@ -766,6 +786,7 @@ async function main() { res.push(await appTools.storedData?.decryptData?.(v) || v) }, ) + console.log('appState.contacts', appState.contacts) await contactsList.render({ userInfo, @@ -790,23 +811,272 @@ async function main() { walletFunds, }) }) + import('./components/crowdnode-card.js') + .then(async ({ CrowdNodeCard }) => { + let cfg = { + state: { + // name: '', + }, + events: { + submit: async event => { + event.preventDefault() + event.stopPropagation() - integrationsSection.insertAdjacentHTML('beforeend', html` -
-
-
Coming soon
-

Earn interest with

-
- -
- `) + // this.elements.form?.removeEventListener('submit', this.events.submit) + + let fde = formDataEntries(event) + + console.log( + `Crowdnode Card submit`, + {event, fde}, + ) + + if (fde.intent === 'signup') { + let confAct = await appDialogs.confirmAction.render({ + name: 'Signup for Crowdnode', + actionTxt: 'Signup', + actionAlt: 'Signup for Crowdnode', + action: '', + actionType: 'infoo', + placement: 'center auto-height', + // status: DIALOG_STATUS.LOADING, + acceptedToS: false, + submitIcon: () => ``, + alert: state => html` +
+ +

This process may take a while, please be patient.

+
+ `, + fields: () => html` +
+
+ To stake your Dash and begin earning interest, read and accept the CrowdNode Terms and Conditions. + Funds are required to complete the signup process. +
+
+ `, + callback: async (state, fde) => { + state.status = DIALOG_STATUS.LOADING + + if (fde?.acceptToS === 'on') { + state.acceptedToS = true + } + + let cbConfAct = await appDialogs.confirmAction.render(state) + + console.log( + `confirm action`, + {state, fde, cbConfAct}, + ) + + let cnFunding = await showQrCode({ + name: 'CrowdNode Funding', + amount: 1.1, + status: DIALOG_STATUS.LOADING, + generateNextAddress: state => html` + Send 1.1 Dash or more
+ to signup & fund your CrowdNode account. + `, + footer: state => html` + + `, + }) + + state.status = DIALOG_STATUS.SUCCESS + + cbConfAct = await appDialogs.confirmAction.render(state) + + console.log('CN Card Funding Callback', cnCard) + + cnCard.api.value = { + acceptedToS: true, + balance: 1.234, + earned: 0.987, + } + + cnCard.render({ + cfg, + el: integrationsSection, + position: 'beforeend' + }) + + console.log( + `confirm action SUCCESS`, + {state, fde, cnFunding}, + ) + + return { state, fde } + }, + }) + + console.log('CN Card confAct Submit Event', cnCard) + console.log('confAct', confAct, appDialogs.confirmAction) + + confAct?.elements?.form?.classList.add?.('min-h-auto') + + appDialogs.confirmAction.showModal() + } + + console.log( + `Crowdnode Card submit TX`, + fde.intent, + {event, fde}, + ) + + if (fde.intent === 'deposit') { + appDialogs.sendOrReceive?.elements?.form?.classList.add?.('min-h-auto') + await appDialogs.sendOrReceive.render({ + name: 'Deposit to CrowdNode', + cashSend: () => html``, + hideAddressee: true, + action: fde.intent, + wallet, + account: appState.account, + userInfo, + contacts: appState.contacts, + to: '@crowdnode', + }) + appDialogs.sendOrReceive.showModal() + } + if (fde.intent === 'withdraw') { + crowdnodeTransactionRig.markup.fields = html` +
+ + +
+ Enter the percentage you wish to unstake. + ` + + let cnWithdraw = crowdnodeTransactionRig.render({ + el: mainApp, + cfg: { + state: { + name: 'Withdraw from CrowdNode', + submitTxt: 'Withdraw', + submitAlt: 'Withdraw funds from CrowdNode', + cancelTxt: 'Cancel', + cancelAlt: `Cancel Withdraw`, + callback: async (state, res) => { + console.log('cnWithdraw callback', { state, res }) + state.value = { + ...state.value, + status: DIALOG_STATUS.SUCCESS + } + return { state, res } + }, + }, + events: { + input: event => { + if ( + event?.target?.type === 'range' && + event.target.value > -1 + ) { + event.target.form.percent.value = event?.target?.value || 0 + } + + if ( + event?.target?.type === 'number' && + event.target.value > -1 + ) { + event.target.form.percentRange.value = event?.target?.value || 0 + } + }, + }, + }, + }) + cnWithdraw?.elements?.form?.classList.add?.('min-h-auto') + crowdnodeTransactionRig.markup.footer = html` +
+ + +
+ ` + + console.log( + `Crowdnode Card TX`, + fde.intent, + {cnWithdraw}, + ) + + cnWithdraw.showModal() + } + } + }, + } + + let cnCard = new CrowdNodeCard(cfg) + console.log('CN Card Outer', cnCard) + + cnCard.render({ + cfg, + el: integrationsSection, + position: 'beforeend' + }) + }) + + + // integrationsSection.insertAdjacentHTML('beforeend', html` + //
+ //
+ //
Coming soon
+ //

Earn interest with

+ //
+ // + //
+ // `) let txs = await getTxs( appState, @@ -895,7 +1165,7 @@ async function main() { action: 'lock', actionType: 'warn', alert: state => html``, - callback: () => { + callback: async () => { sessionStorage.clear() window.location.reload() }, @@ -925,7 +1195,7 @@ async function main() { This is an irreversable action which removes all wallet data from your browser, make sure to backup your data first.

WE RETAIN NO BACKUPS OF YOUR WALLET DATA.

`, - callback: () => { + callback: async () => { localStorage.clear() sessionStorage.clear() // @ts-ignore diff --git a/src/rigs/add-contact.js b/src/rigs/add-contact.js index e583b1f..80907fb 100644 --- a/src/rigs/add-contact.js +++ b/src/rigs/add-contact.js @@ -1,26 +1,29 @@ -import { lit as html } from '../helpers/lit.js' -import { qrSvg } from '../helpers/qr.js' import { - deriveWalletData, + OIDC_CLAIMS, + ALIAS_REGEX, +} from '../utils/constants.js' + +import { + lit as html, formDataEntries, setClipboard, openBlobSVG, - // sortContactsByAlias, - // sortContactsByName, + debounce, +} from '../utils/generic.js' + +import { + getStoreData, +} from '../utils/db.js' + +import { qrSvg } from '../utils/qr.js' + +import { + deriveWalletData, parseAddressField, generateContactPairingURI, - getStoreData, - debounce, - // nobounce, - // getRandomWords, getUniqueAlias, isUniqueAlias, -} from '../helpers/utils.js' - -import { - OIDC_CLAIMS, - ALIAS_REGEX, -} from '../helpers/constants.js' +} from '../utils/dash/local.js' export let addContactRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/confirm-action.js b/src/rigs/confirm-action.js index 1e43ca1..8722e7b 100644 --- a/src/rigs/confirm-action.js +++ b/src/rigs/confirm-action.js @@ -1,7 +1,11 @@ -import { lit as html } from '../helpers/lit.js' import { + DIALOG_STATUS, +} from '../utils/constants.js' + +import { + lit as html, formDataEntries, -} from '../helpers/utils.js' +} from '../utils/generic.js' export let confirmActionRig = (async function (globals) { 'use strict'; @@ -26,6 +30,7 @@ export let confirmActionRig = (async function (globals) { actionType: 'warn', actionClasses: { info: 'bg-info dark bg-info-hover', + infoo: 'outline brd-info info dark light-hover bg-info-hover', warn: 'outline brd-warn warn dark-hover bg-warn-hover', dang: 'outline brd-dang dang light-hover bg-dang-hover', }, @@ -79,16 +84,7 @@ export let confirmActionRig = (async function (globals) { `, alert: state => html``, - // alert: state => html` - //
- // - // This is an irreversable action, make sure to backup first. - // - //
- // `, - content: state => html` - ${state.header(state)} - + fields: state => html`
Are you sure you want to ${state.action} ${ @@ -96,6 +92,11 @@ export let confirmActionRig = (async function (globals) { }?
+ `, + content: state => html` + ${state.header(state)} + + ${state.fields(state)} ${state.footer(state)} `, @@ -108,8 +109,17 @@ export let confirmActionRig = (async function (globals) { if (fde?.intent === 'act') { // state.elements.dialog.returnValue = String(fde.intent) - state.callback?.(state, fde) - confirmAction.close(fde.intent) + let res = await state.callback?.(state, fde) + + if ( + res.state.status === DIALOG_STATUS.SUCCESS || + res.state.status === DIALOG_STATUS.ERROR + ) { + // state.elements.dialog?.querySelector('progress')?.remove() + state.elements.progress?.remove?.() + + confirmAction.close(fde.intent) + } } }, }, diff --git a/src/rigs/confirm-delete.js b/src/rigs/confirm-delete.js index 6bee8d7..04487df 100644 --- a/src/rigs/confirm-delete.js +++ b/src/rigs/confirm-delete.js @@ -1,9 +1,10 @@ -import { lit as html } from '../helpers/lit.js' import { + lit as html, formDataEntries, +} from '../utils/generic.js' +import { getStoreData, - sortContactsByAlias, -} from '../helpers/utils.js' +} from '../utils/db.js' export let confirmDeleteRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/crowdnode-tx.js b/src/rigs/crowdnode-tx.js new file mode 100644 index 0000000..586a588 --- /dev/null +++ b/src/rigs/crowdnode-tx.js @@ -0,0 +1,85 @@ +import { + DIALOG_STATUS, +} from '../utils/constants.js' + +import { + lit as html, + formDataEntries, +} from '../utils/generic.js' + +import { DialogContructor } from '../components/modal.js' + +export const crowdnodeTransactionRig = (() => { + 'use strict'; + + let dialogConfig = { + state: { + name: 'CrowdNode Deposit / Withdraw', + actionTxt: 'Do It!', + actionAlt: 'Yeah, really do this!', + cancelTxt: 'Cancel', + cancelAlt: `Cancel`, + closeTxt: html``, + closeAlt: `Cancel & Close`, + action: '', + target: '', + targetFallback: 'this wallet', + actionType: 'warn', + actionClasses: { + info: 'bg-info dark bg-info-hover', + infoo: 'outline brd-info info dark light-hover bg-info-hover', + warn: 'outline brd-warn warn dark-hover bg-warn-hover', + dang: 'outline brd-dang dang light-hover bg-dang-hover', + }, + showCancelBtn: true, + showActBtn: true, + }, + markup: {}, + events: {}, + appElement: document.body, + } + + let crowdnodeTransaction = new DialogContructor(dialogConfig) + + // console.log('Modal.js Dialog', crowdnodeTransaction) + + dialogConfig.events.submit = async function (event) { + event.preventDefault() + event.stopPropagation() + + let fde = formDataEntries(event) + + if (fde?.intent === 'act') { + let res = await crowdnodeTransaction.state.value.callback?.( + crowdnodeTransaction.state, + fde, + ) + + // console.log('crowdnodeTransaction submit res', res) + + if ( + res.state.value.status === DIALOG_STATUS.SUCCESS || + res.state.value.status === DIALOG_STATUS.ERROR + ) { + crowdnodeTransaction.elements.progress?.remove?.() + + crowdnodeTransaction.elements.dialog.returnValue = String(fde.intent) + crowdnodeTransaction.elements.dialog?.close(String(fde.intent)) + } + } else { + crowdnodeTransaction.elements.progress?.remove?.() + + crowdnodeTransaction.elements.dialog.returnValue = 'cancel' + crowdnodeTransaction.elements.dialog?.close('cancel') + } + } + + dialogConfig.markup.alert = html`` + dialogConfig.markup.submitIcon = `` + + crowdnodeTransaction.updateConfig(dialogConfig) + + return crowdnodeTransaction +})(); + +export default crowdnodeTransactionRig \ No newline at end of file diff --git a/src/rigs/edit-contact.js b/src/rigs/edit-contact.js index f80b933..97898b8 100644 --- a/src/rigs/edit-contact.js +++ b/src/rigs/edit-contact.js @@ -1,19 +1,25 @@ -import { lit as html } from '../helpers/lit.js' import { - deriveWalletData, + OIDC_CLAIMS, + ALIAS_REGEX, +} from '../utils/constants.js' + +import { + lit as html, formDataEntries, - parseAddressField, - getStoreData, debounce, getAvatar, - getUniqueAlias, - isUniqueAlias, -} from '../helpers/utils.js' +} from '../utils/generic.js' import { - OIDC_CLAIMS, - ALIAS_REGEX, -} from '../helpers/constants.js' + getStoreData, +} from '../utils/db.js' + +import { + deriveWalletData, + parseAddressField, + getUniqueAlias, + isUniqueAlias, +} from '../utils/dash/local.js' export let editContactRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/edit-profile.js b/src/rigs/edit-profile.js index afe0b00..8af59cc 100644 --- a/src/rigs/edit-profile.js +++ b/src/rigs/edit-profile.js @@ -1,17 +1,15 @@ -import { lit as html } from '../helpers/lit.js' -import { qrSvg } from '../helpers/qr.js' import { + ALIAS_REGEX, +} from '../utils/constants.js' + +import { + lit as html, formDataEntries, setClipboard, openBlobSVG, - // sortContactsByAlias, - // sortContactsByName, - // parseAddressField, -} from '../helpers/utils.js' +} from '../utils/generic.js' -import { - ALIAS_REGEX, -} from '../helpers/constants.js' +import { qrSvg } from '../utils/qr.js' export let editProfileRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/onboard.js b/src/rigs/onboard.js index 899120f..4f7d457 100644 --- a/src/rigs/onboard.js +++ b/src/rigs/onboard.js @@ -1,7 +1,7 @@ -import { lit as html } from '../helpers/lit.js' import { + lit as html, formDataEntries, -} from '../helpers/utils.js' +} from '../utils/generic.js' export let onboardRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/pair-qr.js b/src/rigs/pair-qr.js index c69d1e0..3c4f6b4 100644 --- a/src/rigs/pair-qr.js +++ b/src/rigs/pair-qr.js @@ -1,11 +1,14 @@ -import { lit as html } from '../helpers/lit.js' -import { qrSvg } from '../helpers/qr.js' import { + lit as html, setClipboard, openBlobSVG, - // generatePaymentRequestURI, +} from '../utils/generic.js' + +import { qrSvg } from '../utils/qr.js' + +import { generateContactPairingURI, -} from '../helpers/utils.js' +} from '../utils/dash/local.js' export let pairQrRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/phrase-backup.js b/src/rigs/phrase-backup.js index 4a23603..bbaef93 100644 --- a/src/rigs/phrase-backup.js +++ b/src/rigs/phrase-backup.js @@ -1,9 +1,11 @@ -import { lit as html } from '../helpers/lit.js' import { - formDataEntries, - phraseToEl, + lit as html, setClipboard, -} from '../helpers/utils.js' +} from '../utils/generic.js' + +import { + phraseToEl, +} from '../utils/dash/local.js' export let phraseBackupRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/phrase-generate.js b/src/rigs/phrase-generate.js index 7d003ed..c2ff9fa 100644 --- a/src/rigs/phrase-generate.js +++ b/src/rigs/phrase-generate.js @@ -1,10 +1,11 @@ -import { lit as html } from '../helpers/lit.js' -import { - formDataEntries, -} from '../helpers/utils.js' import { ALIAS_REGEX, -} from '../helpers/constants.js' +} from '../utils/constants.js' + +import { + lit as html, + formDataEntries, +} from '../utils/generic.js' export let phraseGenerateRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/phrase-import.js b/src/rigs/phrase-import.js index dc569f9..5e81336 100644 --- a/src/rigs/phrase-import.js +++ b/src/rigs/phrase-import.js @@ -1,14 +1,18 @@ -import { lit as html } from '../helpers/lit.js' import { + ALIAS_REGEX, + PHRASE_REGEX, +} from '../utils/constants.js' + +import { + lit as html, formDataEntries, readFile, - verifyPhrase, fileIsSubType, -} from '../helpers/utils.js' +} from '../utils/generic.js' + import { - ALIAS_REGEX, - PHRASE_REGEX, -} from '../helpers/constants.js' + verifyPhrase, +} from '../utils/dash/local.js' export let phraseImportRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/request-qr.js b/src/rigs/request-qr.js index 17191f1..72b92a0 100644 --- a/src/rigs/request-qr.js +++ b/src/rigs/request-qr.js @@ -1,15 +1,17 @@ -import { lit as html } from '../helpers/lit.js' -import { qrSvg } from '../helpers/qr.js' import { + lit as html, formDataEntries, setClipboard, openBlobSVG, +} from '../utils/generic.js' + +import { generatePaymentRequestURI, - fixedDash, - roundUsing, getPartialHDPath, getAddressIndexFromUsage, -} from '../helpers/utils.js' +} from '../utils/dash/local.js' + +import { qrSvg } from '../utils/qr.js' export let requestQrRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/scan.js b/src/rigs/scan.js index 0c481e1..7304224 100644 --- a/src/rigs/scan.js +++ b/src/rigs/scan.js @@ -1,7 +1,6 @@ -import { lit as html } from '../helpers/lit.js' -// import { -// formDataEntries, -// } from '../helpers/utils.js' +import { + lit as html, +} from '../utils/generic.js' export let scanContactRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/send-confirm.js b/src/rigs/send-confirm.js index 5e0fe8c..a89a120 100644 --- a/src/rigs/send-confirm.js +++ b/src/rigs/send-confirm.js @@ -1,10 +1,15 @@ -import { lit as html } from '../helpers/lit.js' import { + lit as html, formDataEntries, +} from '../utils/generic.js' + +import { getStoreData, - sortContactsByAlias, +} from '../utils/db.js' + +import { formatDash, -} from '../helpers/utils.js' +} from '../utils/dash/local.js' export let sendConfirmRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/send-or-request.js b/src/rigs/send-or-request.js index a2d94ac..eb5a5e9 100644 --- a/src/rigs/send-or-request.js +++ b/src/rigs/send-or-request.js @@ -1,15 +1,18 @@ -import { lit as html } from '../helpers/lit.js' -import { AMOUNT_REGEX, USAGE } from '../helpers/constants.js' +import { AMOUNT_REGEX, USAGE } from '../utils/constants.js' + import { + lit as html, formDataEntries, +} from '../utils/generic.js' + +import { parseAddressField, fixedDash, - toDASH, toDash, roundUsing, getPartialHDPath, getAddressIndexFromUsage, -} from '../helpers/utils.js' +} from '../utils/dash/local.js' export let sendOrReceiveRig = (async function (globals) { 'use strict'; @@ -38,6 +41,7 @@ export let sendOrReceiveRig = (async function (globals) { closeTxt: html``, closeAlt: `Close`, action: 'send', + hideAddressee: false, submitIcon: state => { const icon = { send: html` @@ -180,26 +184,56 @@ export let sendOrReceiveRig = (async function (globals) { ` }, + cashSend: state => html` +
+ + + +
+ `, + addressee: state => { + if (state.hideAddressee) { + return html` + + ` + } + + return html` +
+ + + ${state.qrScanBtn(state)} +
+ ` + }, content: state => html` ${state.header(state)}
-
- - - ${state.qrScanBtn(state)} -
+ ${state.addressee(state)}
-
- - - -
+ ${state.cashSend(state)}
@@ -444,6 +467,10 @@ export let sendOrReceiveRig = (async function (globals) { inWallet = Object.values(contact?.incoming)?.[0] } + console.log('send or request', { + to, contact, outWallet, inWallet + }) + if (!inWallet) { // state.wallet.addressIndex = ( // state.wallet?.addressIndex ?? -1 @@ -518,7 +545,7 @@ export let sendOrReceiveRig = (async function (globals) { } let leftoverBalance = walletFunds.balance - amount - let fullTransfer = leftoverBalance <= 0.0010_0200 + let fullTransfer = leftoverBalance > 0 && leftoverBalance <= 0.0010_0200 // let fullTransfer = leftoverBalance <= 0.0001_0200 if ( @@ -542,6 +569,7 @@ export let sendOrReceiveRig = (async function (globals) { state.wallet.accountIndex, state.wallet.addressIndex, ) + let amountNeeded = fixedDash(roundUsing(Math.floor, Math.abs( walletFunds.balance - Number(fde.amount) ))) diff --git a/src/rigs/show-error.js b/src/rigs/show-error.js index 0e590d5..b47cd6e 100644 --- a/src/rigs/show-error.js +++ b/src/rigs/show-error.js @@ -1,4 +1,4 @@ -import { lit as html } from '../helpers/lit.js' +import { lit as html } from '../utils/generic.js' export async function showErrorDialog(options) { let opts = { @@ -60,7 +60,7 @@ export async function showErrorDialog(options) { content: state => html` ${state.header(state)} -
+
diff --git a/src/rigs/tx-info.js b/src/rigs/tx-info.js index f7f7c92..5f89105 100644 --- a/src/rigs/tx-info.js +++ b/src/rigs/tx-info.js @@ -1,7 +1,7 @@ -import { lit as html } from '../helpers/lit.js' +import { lit as html } from '../utils/generic.js' import { formatDash, -} from '../helpers/utils.js' +} from '../utils/dash/local.js' export let txInfoRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/wallet-backup.js b/src/rigs/wallet-backup.js index fd32a49..5ed9551 100644 --- a/src/rigs/wallet-backup.js +++ b/src/rigs/wallet-backup.js @@ -1,7 +1,7 @@ -import { lit as html } from '../helpers/lit.js' import { + lit as html, formDataEntries, -} from '../helpers/utils.js' +} from '../utils/generic.js' export let walletBackupRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/wallet-decrypt.js b/src/rigs/wallet-decrypt.js index f43cd27..deffee0 100644 --- a/src/rigs/wallet-decrypt.js +++ b/src/rigs/wallet-decrypt.js @@ -1,10 +1,10 @@ -import { lit as html } from '../helpers/lit.js' import { + lit as html, formDataEntries, -} from '../helpers/utils.js' +} from '../utils/generic.js' import { initWallet, -} from '../helpers/wallet.js' +} from '../utils/dash/local.js' export let walletDecryptRig = (async function (globals) { 'use strict'; diff --git a/src/rigs/wallet-encrypt.js b/src/rigs/wallet-encrypt.js index 0cc9c5c..04fbda4 100644 --- a/src/rigs/wallet-encrypt.js +++ b/src/rigs/wallet-encrypt.js @@ -1,10 +1,10 @@ -import { lit as html } from '../helpers/lit.js' import { + lit as html, formDataEntries, -} from '../helpers/utils.js' +} from '../utils/generic.js' import { initWallet, -} from '../helpers/wallet.js' +} from '../utils/dash/local.js' export let walletEncryptRig = (async function (globals) { 'use strict'; diff --git a/src/secp.js b/src/secp.js new file mode 100644 index 0000000..f49b0e8 --- /dev/null +++ b/src/secp.js @@ -0,0 +1,11 @@ +import '../node_modules/@dashincubator/secp256k1/secp256k1.js'; + +// @ts-ignore +const secp = window?.nobleSecp256k1 || globalThis?.nobleSecp256k1 || window?.Secp256k1 || globalThis?.Secp256k1 + +// @ts-ignore +window.Secp256k1 = secp + +export const Secp256k1 = secp + +export default secp diff --git a/src/state/index.js b/src/state/index.js index fb549e3..7781f54 100644 --- a/src/state/index.js +++ b/src/state/index.js @@ -1,12 +1,12 @@ import { OIDC_CLAIMS, -} from '../helpers/constants.js' +} from '../utils/constants.js' import { envoy, -} from '../helpers/utils.js' +} from '../utils/retort.js' import { store, -} from '../helpers/wallet.js' +} from '../utils/dash/network.js' export const appDialogs = envoy( { diff --git a/src/styles/components.css b/src/styles/components.css index 43c7933..6751960 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -116,6 +116,9 @@ display: flex; } +.integration-sect { + padding: 0 1rem; +} .integration-sect > section > header { flex-direction: column; align-items: start; @@ -140,6 +143,34 @@ text-decoration: none; } +.card { + border-radius: 1rem; + overflow: auto; + flex: 1 1 auto; + max-width: 49%; + overflow: hidden; +} +.card header, +.card footer { + padding: 1rem 0; +} + +.card header a, +.card header a:active, +.card header a:focus, +.card header a:hover { + color: var(--fc); + text-decoration: none; +} +.card section { + padding: .25rem 0; +} +.card fieldset { + min-width: auto; +} +.card footer button { +} + @@ -150,6 +181,21 @@ .cols > section { padding: 0; } + + .card { + max-width: 32.5%; + padding: 0 1rem; + } + .card header, + .card footer { + padding: 1rem 0; + } + .card section { + padding: .25rem 0; + } + .integration-sect { + padding: 0; + } } @media (min-width: 980px) { .cols { diff --git a/src/styles/dialog.css b/src/styles/dialog.css index 75c3b1b..5be19e4 100644 --- a/src/styles/dialog.css +++ b/src/styles/dialog.css @@ -181,6 +181,9 @@ dialog > form > fieldset div:has(input + div) { border-radius: 6.25rem; border: 1px solid var(--dark-600); } +dialog > form > fieldset input[type="range"] { + padding: 0; +} dialog > form > fieldset input + p, dialog > form > fieldset label { text-align: left; @@ -197,7 +200,7 @@ dialog > form > fieldset input + p { font-weight: 400; margin: .5rem 0; } -dialog > form > fieldset p { +dialog > form fieldset p { display: flex; padding: 0; margin: .8rem; @@ -217,7 +220,7 @@ dialog > form > fieldset div + p { margin: 0 0 0 1rem; text-align: left; } -dialog > form > fieldset p > span { +dialog > form fieldset p > span { padding: .25rem; border: 1px solid var(--dark-500); } diff --git a/src/styles/form.css b/src/styles/form.css index c1fc1b1..d1a4aca 100644 --- a/src/styles/form.css +++ b/src/styles/form.css @@ -494,7 +494,7 @@ label { } label > input[type=checkbox], label + input[type=checkbox] { - width: 2rem; + width: 1.25rem; } div.switch { @@ -739,6 +739,25 @@ form[name="network"] { cursor: default; } +main form header { + margin: 0 auto; + min-height: auto; + /* width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-direction: column-reverse; */ +} + +.card footer { + gap: .25rem; +} + +.card footer button { + padding: 0.63rem .25rem; + flex: 1 1 50%; +} + @media (min-width: 650px) { main form { @@ -776,4 +795,8 @@ form[name="network"] { form > article > figure figcaption + div.big { font-size: 3.75rem; } */ + + .card footer button { + padding: 0.63rem 1rem; + } } \ No newline at end of file diff --git a/src/styles/motion.css b/src/styles/motion.css index 26aa428..85cff62 100644 --- a/src/styles/motion.css +++ b/src/styles/motion.css @@ -1,16 +1,16 @@ @-webkit-keyframes await-progress { - 0% { - background-position: -322px; + 0% { + background-position: -150%; } 100% { - background-position: 322px; + background-position: 200%; } } @keyframes await-progress { - 0% { - background-position: -322px; + 0% { + background-position: -150%; } 100% { - background-position: 322px; + background-position: 200%; } } diff --git a/src/styles/progress.css b/src/styles/progress.css index 1ad2c4f..6d30564 100644 --- a/src/styles/progress.css +++ b/src/styles/progress.css @@ -1,19 +1,3 @@ -progress:indeterminate { - background: linear-gradient( - 90deg, - #0000 0%, - #0000 50%, - var(--info) 100% - ); -} -progress.recording:indeterminate { - background: linear-gradient( - 90deg, - var(--livea) 0%, - var(--live) 50%, - var(--livea) 100% - ); -} progress.pending, progress.pending[role] { position: absolute; @@ -26,14 +10,15 @@ progress.pending[role] { padding: 0; border: none; background-color: transparent; - background-size: auto; + /* background-size: auto; */ + background-size: 50% 100%; background-repeat: no-repeat; background-position: 0 0; appearance: none; -moz-appearance: none; -webkit-appearance: none; border: 0 solid transparent; - visibility: hidden; + /* visibility: hidden; */ } progress.recording, progress.recording[role] { @@ -52,11 +37,3 @@ progress.pending:indeterminate { -webkit-animation: await-progress 2.5s ease-in-out infinite; animation: await-progress 2.5s ease-in-out infinite; } - - -/* form[name=toggle_relay]:has(input[type=checkbox]:checked) { - border-bottom: .25rem solid var(--dang); -} */ -form[name=toggle_relay]:has(input[type=checkbox]:checked) progress { - visibility: visible; -} diff --git a/src/styles/theme.css b/src/styles/theme.css index c8e5830..55d1c2e 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -19,6 +19,10 @@ --light-500: #004470; --light-600: #003e66; + --card-100: hsl(200deg 80% 60% / 15%); + --card-100: #00447066; + + --grayscale-gray-400: #9DA6A6; --c: var(--dark-900); @@ -140,11 +144,11 @@ a:hover { } svg { - fill: currentColor; vertical-align: bottom; } -svg path { +svg g:not(.lock), +svg path:not(.lock) { fill: currentColor; } @@ -316,6 +320,11 @@ th { border-radius: 6.25rem; } +.card { + background-color: var(--card-100); + /* background-color: var(--dark-600); */ +} + .cols > section > div { background-color: var(--nav-bg); } diff --git a/src/helpers/constants.js b/src/utils/constants.js similarity index 75% rename from src/helpers/constants.js rename to src/utils/constants.js index 629a956..655999a 100644 --- a/src/helpers/constants.js +++ b/src/utils/constants.js @@ -1,5 +1,3 @@ -export const STOREAGE_SALT = 'b9f4088bd3a93783147e3d78aa10cc911a2449a0d79a226ae33a5957b368cc18' - export const KS_PRF = { 'hmac-sha256': 'SHA-256', } @@ -142,3 +140,59 @@ export const USAGE = { RECEIVE, CHANGE, } + +const NOT_LOADING = 0 +const LOADING = 1 +const SUCCESS = 2 +const ERROR = 3 + +export const DIALOG_STATUS = { + NOT_LOADING, + LOADING, + SUCCESS, + ERROR +} + +export const CROWDNODE = { + offset: 20000, + duffs: 100000000, + satoshis: 100000000, + depositMinimum: 100000, + stakeMinimum: 50000000, + + /** + * @type {Record} + */ + requests: { + acceptTerms: 65536, + offset: 20000, + signupForApi: 131072, + toggleInstantPayout: 4096, + withdrawMin: 1, + withdrawMax: 1000, + }, + + /** + * @type {Record} + */ + messages: { + PleaseAcceptTerms: 2, + WelcomeToCrowdNodeBlockChainAPI: 4, + DepositReceived: 8, + WithdrawalQueued: 16, + WithdrawalFailed: 32, + AutoWithdrawalEnabled: 64, + AutoWithdrawalDisabled: 128, + }, + + /** + * @type {Record} + */ + responses: {}, +} + +CROWDNODE.responses = Object.fromEntries( + Object.entries(CROWDNODE.messages).map( + ([k,v]) => [v,k] + ) +) diff --git a/src/utils/cryptic.js b/src/utils/cryptic.js new file mode 100644 index 0000000..0b4a603 --- /dev/null +++ b/src/utils/cryptic.js @@ -0,0 +1,328 @@ +// @ts-ignore +import blake from 'blakejs' +// @ts-ignore +import { keccak_256 } from '@noble/hashes/sha3' + +import { + Cryptic, +} from '../imports.js' +import { + KS_CIPHER, KS_PRF, +} from './constants.js' + +export async function decryptWallet( + decryptPass, + decryptIV, + decryptSalt, + ciphertext, +) { + const cryptic = Cryptic.create( + decryptPass, + decryptSalt, + ) + + return await cryptic.decrypt(ciphertext, decryptIV); +} + +export function blake256(data) { + if ('string' === typeof data) { + data = Cryptic.hexToBuffer(data) + } + const context = blake.blake2bInit(32, null); + blake.blake2bUpdate(context, data); + return Cryptic.toHex(blake.blake2bFinal(context)); +} + +export function getKeystoreData(keystore) { + const { + ciphertext, + cipher, + mac, + } = keystore.crypto + const [ + cipherAlgorithm, + cipherLength, + ] = KS_CIPHER[cipher] + + const derivationAlgorithm = keystore.crypto.kdf.toUpperCase() + const hashingAlgorithm = KS_PRF[keystore.crypto.kdfparams.prf] + const derivedKeyLength = keystore?.crypto?.kdfparams?.dklen + const iterations = keystore.crypto.kdfparams.c + const iv = keystore.crypto.cipherparams.iv + const ivBuffer = Cryptic.hexToBuffer(iv) + const salt = keystore.crypto.kdfparams.salt + const saltBuffer = Cryptic.hexToBuffer(salt) + + const keyLength = derivedKeyLength / 2 + const numBits = (keyLength + iv.length) * 8 + + return { + cipher, + cipherAlgorithm, + cipherLength, + ciphertext, + mac, + derivationAlgorithm, + hashingAlgorithm, + derivedKeyLength, + iterations, + iv, + ivBuffer, + salt, + saltBuffer, + keyLength, + numBits, + } +} + +export async function setupCryptic( + encryptionPassword, + keystore, +) { + const ks = getKeystoreData(keystore) + const { + cipherLength, cipherAlgorithm, + derivationAlgorithm, hashingAlgorithm, iv, + iterations, salt, + } = ks + + Cryptic.setConfig({ + cipherAlgorithm, + cipherLength, + hashingAlgorithm, + derivationAlgorithm, + iterations, + }) + + const cryptic = Cryptic.create( + encryptionPassword, + salt, + ); + + return { + Cryptic, + cryptic, + ks, + } +} + +export async function encryptData( + encryptionPassword, + keystore, + data, +) { + const { cryptic, ks } = await setupCryptic( + encryptionPassword, + keystore, + ) + + return await cryptic.encrypt(data, ks.iv); +} + +export async function decryptData( + encryptionPassword, + keystore, + data, +) { + const { cryptic, ks } = await setupCryptic( + encryptionPassword, + keystore, + ) + + return await cryptic.decrypt(data, ks.iv) +} + +export function storedData( + encryptionPassword, + keystore, +) { + const SD = {} + + SD.decryptData = async function(data) { + if (data && 'string' === typeof data && data.length > 0) { + data = JSON.parse(await decryptData( + encryptionPassword, + keystore, + data + )) + } + + return data + } + + SD.decryptItem = async function(targetStore, item,) { + let data = await targetStore.getItem( + item, + ) + + data = await SD.decryptData(data) + + return data + } + + /** + * + * @param {*} targetStore + * @param {*} item + * @param {*} data + * @param {*} extend + * @returns {Promise<[String,Object]>} + */ + SD.encryptData = async function( + targetStore, item, data = {}, extend = true + ) { + let encryptedData = '' + let storedData = {} + let jsonData = {} + if (extend) { + // storedData = await targetStore.getItem( + // item, + // ) + storedData = await SD.decryptItem( + targetStore, + item + ) + } + + if (data) { + jsonData = { + ...storedData, + ...data, + } + encryptedData = await encryptData( + encryptionPassword, + keystore, + JSON.stringify(jsonData) + ) + } + + return [ + encryptedData, + jsonData, + ] + } + + SD.encryptItem = async function( + targetStore, item, data = {}, extend = true + ) { + let encryptedData = '' + let encryptedResult = '' + let result = {} + + if (data || extend) { + let d = await SD.encryptData(targetStore, item, data, extend) + encryptedResult = d[0] + result = d[1] + encryptedData = await targetStore.setItem( + item, + encryptedResult + ) + } + + return result || data || encryptedData + // return encryptedData + } + + return SD +} + +export async function decryptKeystore( + encryptionPassword, + keystore, +) { + const { Cryptic, cryptic, ks } = await setupCryptic( + encryptionPassword, + keystore, + ) + + const derivedBytes = await cryptic.deriveBits(ks.numBits, ks.salt) + + const bMAC = blake256([ + ...new Uint8Array(derivedBytes.slice(16, 32)), + ...Cryptic.toBytes(ks.ciphertext), + ]) + const kMAC = Cryptic.toHex(keccak_256(new Uint8Array([ + ...new Uint8Array(derivedBytes.slice(16, 32)), + ...Cryptic.toBytes(ks.ciphertext), + ]))); + + if (ks.mac && ![bMAC, kMAC].includes(ks.mac)) { + throw new Error('Invalid password') + } + + return await cryptic.decrypt(ks.ciphertext, ks.iv) +} + +export function genKeystore( + // aes-256-gcm + cipher = 'aes-128-ctr', + salt = Cryptic.randomBytes(32), + iv = Cryptic.randomBytes(16), + iterations = 262144, + id = crypto.randomUUID(), +) { + return { + crypto: { + cipher, + ciphertext: '', + cipherparams: { + iv: Cryptic.bufferToHex(iv), + }, + kdf: "pbkdf2", + kdfparams: { + c: iterations, + dklen: 32, + prf: "hmac-sha256", + salt: Cryptic.bufferToHex(salt), + }, + mac: '', + }, + id, + meta: 'dash-incubator-keystore', + version: 3, + } +} + +export async function encryptKeystore( + encryptionPassword, + recoveryPhrase, +) { + let keystore = genKeystore() + const { Cryptic, cryptic, ks } = await setupCryptic( + encryptionPassword, + keystore, + ) + + const derivedBytes = await cryptic.deriveBits(ks.numBits, ks.salt) + const encryptedPhrase = await cryptic.encrypt(recoveryPhrase, ks.iv); + + keystore.crypto.ciphertext = encryptedPhrase + + const bMAC = blake256([ + ...new Uint8Array(derivedBytes.slice(16, 32)), + ...Cryptic.toBytes(keystore.crypto.ciphertext), + ]) + const kMAC = Cryptic.toHex(keccak_256(new Uint8Array([ + ...new Uint8Array(derivedBytes.slice(16, 32)), + ...Cryptic.toBytes(keystore.crypto.ciphertext), + ]))); + + keystore.crypto.mac = bMAC + + // console.log( + // 'encrypted keystore', + // ks, + // { + // encryptedPhrase, + // // keyMaterial, + // // derivedKey, + // // derivedBytes, + // }, + // { + // bMAC, + // kMAC, + // }, + // ) + + return keystore +} \ No newline at end of file diff --git a/src/utils/dash/local.js b/src/utils/dash/local.js new file mode 100644 index 0000000..93a74cb --- /dev/null +++ b/src/utils/dash/local.js @@ -0,0 +1,1432 @@ +import { + USAGE, + DUFFS, + DASH_URI_REGEX, + OIDC_CLAIMS, + SUPPORTED_CLAIMS, +} from '../constants.js' + +import { + DashHd, + DashTx, + DashPhrase, +} from '../../imports.js' + +import { + DatabaseSetup, + findInStore, + getFilteredStoreLength, + getStoredItems, + loadStoreObject, +} from '../db.js' + +import { + encryptData, + encryptKeystore, +} from '../cryptic.js' + +export const store = await DatabaseSetup() + +/** + * + * @param {String} [phraseOrXkey] + * @param {Number} [accountIndex] + * @param {Number} [addressIndex] + * @param {Number} [usageIndex] + * + * @returns {Promise} + */ +export async function deriveWalletData( + phraseOrXkey, + accountIndex = 0, + addressIndex = 0, + usageIndex = DashHd.RECEIVE, +) { + if (!phraseOrXkey) { + throw new Error('Seed phrase or xkey value empty or invalid') + } + + let recoveryPhrase + let seed, derivedWallet, wpub, id, account + let xkey, xprv, xpub, xkeyId + let addressKey, addressKeyId, address + let secretSalt = ''; // "TREZOR"; + let recoveryPhraseArr = phraseOrXkey.trim().split(' ') + + if (recoveryPhraseArr?.length >= 12) { + recoveryPhrase = phraseOrXkey; + } + + if ( + ['xprv', 'xpub'].includes( + phraseOrXkey?.substring(0,4) || '' + ) + ) { + xkey = await DashHd.fromXKey(phraseOrXkey); + } else { + seed = await DashPhrase.toSeed(recoveryPhrase, secretSalt); + derivedWallet = await DashHd.fromSeed(seed); + wpub = await DashHd.toXPub(derivedWallet); + id = await DashHd.toId(derivedWallet); + account = await derivedWallet.deriveAccount(accountIndex); + xkey = await account.deriveXKey(usageIndex); + xprv = await DashHd.toXPrv(xkey); + } + + xkeyId = await DashHd.toId(xkey); + xpub = await DashHd.toXPub(xkey); + addressKey = await xkey.deriveAddress(addressIndex); + addressKeyId = await DashHd.toId(addressKey); + address = await DashHd.toAddr(addressKey.publicKey); + + return { + id, + accountIndex, + usageIndex, + addressIndex, + addressKeyId, + addressKey, + address, + xkeyId, + xkey, + xprv, + xpub, + seed, + wpub, + account, + derivedWallet, + recoveryPhrase, + } +} + +/** + * + * @param {Number} [accountIndex] + * @param {Number} [addressIndex] + * @param {Number} [use] + * + * @returns {Promise} + */ +export async function generateWalletData( + accountIndex = 0, + addressIndex = 0, + use = DashHd.RECEIVE +) { + let targetBitEntropy = 128; + let recoveryPhrase = await DashPhrase.generate(targetBitEntropy); + + return await deriveWalletData( + recoveryPhrase, + accountIndex, + addressIndex, + use + ) +} + +/** + * + * @example + * let acct = deriveAccountData(wallet, 0, 0, 0) + * + * @param {HDWallet} wallet + * @param {Number} [accountIndex] + * @param {Number} [addressIndex] + * @param {Number} [use] + * + * @returns + */ +export async function deriveAccountData( + wallet, + accountIndex = 0, + addressIndex = 0, + use = DashHd.RECEIVE, +) { + let account = await wallet.deriveAccount(accountIndex); + let xkey = await account.deriveXKey(use); + let xkeyId = await DashHd.toId(xkey); + let xprv = await DashHd.toXPrv(xkey); + let xpub = await DashHd.toXPub(xkey); + let xpubKey = await DashHd.fromXKey(xpub); + let xpubId = await DashHd.toId(xpubKey); + let key = await xkey.deriveAddress(addressIndex); + let address = await DashHd.toAddr(key.publicKey); + + return { + account, + xkeyId, + xkey, + xprv, + xpub, + xpubKey, + xpubId, + key, + address + } +} + +/** + * + * @example + * let addr = deriveAddressData(wallet, 0, 0, 0) + * + * @param {HDWallet} wallet + * @param {Number} [accountIndex] + * @param {Number} [addressIndex] + * @param {Number} [use] + * + * @returns + */ +export async function deriveAddressData( + wallet, + accountIndex = 0, + addressIndex = 0, + use = DashHd.RECEIVE, +) { + let account = await wallet.deriveAccount(accountIndex); + let xkey = await account.deriveXKey(use); + let key = await xkey.deriveAddress(addressIndex); + let address = await DashHd.toAddr(key.publicKey); + + return address +} + +export function phraseToEl(phrase, el = 'span', cls = 'tag') { + let words = phrase?.split(' ') + return words?.map( + w => `<${el} class="${cls}">${w}` + )?.join(' ') +} + +/** + * @param {Number} duffs - ex: 00000000 + * @param {Number} [fix] - value for toFixed - ex: 8 + */ +export function toDash(duffs, fix = 8) { + return (duffs / DUFFS).toFixed(fix); +} + +/** + * @param {String} dash - ex: 0.00000000 + */ +export function toDashStr(dash, pad = 12) { + return `Đ ` + `${dash}`.padStart(pad, " "); +} + +/** + * Based on https://stackoverflow.com/a/48100007 + * + * @param {Number} dash - ex: 0.00000000 + * @param {Number} [fix] - value for toFixed - ex: 8 + */ +export function fixedDash(dash, fix = 8) { + return ( + Math.trunc(dash * Math.pow(10, fix)) / Math.pow(10, fix) + ) + .toFixed(fix); +} + +// https://stackoverflow.com/a/27946310 +export function roundUsing(func, number, prec = 8) { + var tempnumber = number * Math.pow(10, prec); + tempnumber = func(tempnumber); + return tempnumber / Math.pow(10, prec); +} + +/** + * @param {Number} duffs - ex: 00000000 + */ +export function toDASH(duffs) { + let dash = toDash(duffs / DUFFS); + return toDashStr(dash); +} + +/** + * @param {Number} dash - ex: 0.00000000 + * @param {Number} [fix] - value for toFixed - ex: 8 + */ +export function fixedDASH(dash, fix = 8) { + return toDashStr(fixedDash(dash, fix)); +} + +/** + * @param {String} dash - ex: 0.00000000 + */ +export function toDuff(dash) { + return Math.round(parseFloat(dash) * DUFFS); +} + +export function formatDash( + unformattedBalance, + options = {}, +) { + let opts = { + maxlen: 10, + fract: 8, + sigsplit: 3, + ...options, + } + let funds = 0 + let balance = `${funds}` + + if (unformattedBalance) { + funds += unformattedBalance + balance = fixedDash(funds, opts.fract) + // TODO FIX: does not support large balances + + // console.log('balance fixedDash', balance, balance.length) + + let [fundsInt,fundsFract] = balance.split('.') + opts.maxlen -= fundsInt.length + + let fundsFraction = fundsFract?.substring( + 0, Math.min(Math.max(0, opts.maxlen), opts.sigsplit) + ) + + let fundsRemainder = fundsFract?.substring( + fundsFraction.length, + Math.max(0, opts.maxlen) + ) + + balance = `${ + fundsInt + }.${ + fundsFraction + }${ + fundsRemainder + }` + } + + return balance +} + +export async function getUnusedChangeAddress(account) { + let filterQuery = { + xkeyId: account.xkeyId, + usageIndex: DashHd.CHANGE, + } + + let foundAddrs = await findInStore(store.addresses, filterQuery) + + for (let [fkey,fval] of Object.entries(foundAddrs)) { + if (!fval.insight?.balance) { + return fkey + } + } + + // return foundAddr.address + return null +} + +export async function loadWalletsForAlias($alias) { + $alias.$wallets = {} + + if ($alias?.wallets) { + for (let w of $alias.wallets) { + let wallet = await store.wallets.getItem(w) + $alias.$wallets[w] = wallet + } + } + + return $alias +} + +export async function initWalletsInfo( + info = {}, +) { + let wallets = await getStoredItems(store.wallets) + + info = { + ...OIDC_CLAIMS, + ...info, + } + + let alias = info.preferred_username + + wallets = Object.values(wallets || {}) + wallets = wallets + .filter(w => w.alias === alias) + .map(w => w.id) + + return { + alias, + wallets, + info, + } +} + +export async function initWallet( + encryptionPassword, + wallet, + keystore, + accountIndex = 0, + addressIndex = 0, + infoOverride = {}, +) { + let { + alias, + wallets, + info, + } = await initWalletsInfo(infoOverride) + + let { id, recoveryPhrase } = wallet + + // console.log( + // 'initWallet wallets', + // wallets, + // info, + // ) + + if (!wallets.includes(id)) { + wallets.push(id) + } + + let addrs = await batchAddressUsageGenerate( + wallet, + accountIndex, + addressIndex, + ) + + console.log('init wallet batchAddressUsageGenerate', addrs) + + for (let a of addrs.addresses) { + store.addresses.setItem( + a.address, + { + updatedAt: Date.now(), + walletId: wallet.id, + accountIndex: a.accountIndex, + addressIndex: a.addressIndex, + usageIndex: a.usageIndex, + xkeyId: a.xkeyId, + } + ) + } + + let storeWallet = await store.wallets.setItem( + `${id}`, + { + id, + updatedAt: Date.now(), + accountIndex, + addressIndex: addrs?.finalAddressIndex || addressIndex, + keystore: keystore || await encryptKeystore( + encryptionPassword, + recoveryPhrase + ), + } + ) + + let storedAlias = await store.aliases.setItem( + `${alias}`, + await encryptData( + encryptionPassword, + storeWallet.keystore, + JSON.stringify({ + wallets, + info, + }) + ) + ) + + // console.log( + // 'initWallet stored values', + // storeWallet, + // storedAlias, + // ) + + let contacts = '{}' + + return { + wallets, + contacts, + } +} + +export function filterPairedContacts(contact) { + let outLen = Object.keys(contact.outgoing || {}).length + return outLen > 0 // && !!contact.alias +} + +export function filterUnpairedContacts(contact) { + return !filterPairedContacts(contact) +} + +export function sortContactsByAlias(a, b) { + const aliasA = a.alias || a.info?.preferred_username?.toUpperCase() || 'zzz'; + const aliasB = b.alias || b.info?.preferred_username?.toUpperCase() || 'zzz'; + + if (aliasA < aliasB) { + return -1; + } + if (aliasA > aliasB) { + return 1; + } + return 0; +} + +export function sortContactsByName(a, b) { + const nameA = a.info?.name?.toUpperCase(); + const nameB = b.info?.name?.toUpperCase(); + + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + return 0; +} + +export function sortTransactionsByTime(a, b) { + const timeA = a.time; + const timeB = b.time; + + if (timeA > timeB) { + return -1; + } + if (timeA < timeB) { + return 1; + } + return 0; +} + +export function DashURLSearchParams(params) { + let searchParams + let qry = {} + + Object.defineProperties(this, { + entries: { + enumerable: false, + configurable: false, + writable: false, + value: () => Object.entries(qry), + }, + toString: { + enumerable: false, + configurable: false, + writable: false, + value: () => this.entries().map(p => p.join('=')).join('&'), + }, + size: { + enumerable: false, + configurable: false, + get() { return this.entries().length }, + }, + }); + + if (typeof params === 'string' && params !== '') { + searchParams = params.split('&') + searchParams.forEach(q => { + let [prop,val] = q.split('=') + qry[prop] = val + }) + } + + if(Array.isArray(params) && params.length > 0) { + params.forEach(q => { + let [prop,val] = q + qry[prop] = val + }) + } + + // console.log('DashURLSearchParams', { + // params, searchParams, qry, + // qryStr: this.toString(), + // }) +} + +export function parseDashURI(uri) { + let result = {} + let parsedUri = [ + ...uri.matchAll(DASH_URI_REGEX) + ]?.[0]?.groups || {} + // let searchParams = new URLSearchParams(parsedUri?.params || '') + let searchParams = new DashURLSearchParams(parsedUri?.params || '') + + console.log( + 'parseDashURI', + parsedUri, + searchParams + ) + + if (parsedUri?.address) { + result.address = parsedUri?.address + } + + if (searchParams?.size > 0) { + let claims = Object.fromEntries( + searchParams?.entries() + ) + + for (let c in claims) { + if (SUPPORTED_CLAIMS.includes(c)) { + result[c] = claims[c] + } + } + } + + return result +} + +export function parseAddressField(uri) { + /* @type {Record} */ + let result = {} + + if (uri.includes(':')) { + let [protocol] = uri.split(':') + if (protocol.includes('dash')) { + // @ts-ignore + result = parseDashURI(uri) + } + } else if ( + 'xprv' === uri?.substring(0,4) + ) { + result.xprv = uri + } else if ( + 'xpub' === uri?.substring(0,4) + ) { + result.xpub = uri + } else { + result.address = uri + } + + return result +} + +export function isEmpty(value) { + if (value === null) { + return true + } + // if (typeof value === 'boolean' && value === false) { + // return true + // } + if (typeof value === 'string' && value?.length === 0) { + return true + } + if (typeof value === 'object' && Object.keys(value)?.length === 0) { + return true + } + if (Array.isArray(value) && value.length === 0) { + return true + } + return false; +} + +export function generateContactPairingURI( + state, + protocol = 'dash', // 'web+dash' + joiner = ':' +) { + let addr = state.wallet?.address || '' + let claims = [ + ["xpub", state.wallet?.xpub || ''], + ["sub", state.wallet?.xkeyId || ''], + ] + + if (state.userInfo) { + let filteredInfo = Array.from( + Object.entries(state.userInfo) + ).filter(p => { + let [key, val] = p + if ( + ![ + // 'updated_at', + 'email_verified', + 'phone_number_verified', + ].includes(key) && + !isEmpty(val) + ) { + return true + } + }) + + claims = [ + ...claims, + ...filteredInfo, + ] + } + + let scope = claims.map(p => p[0]).join(',') + let searchParams = new DashURLSearchParams([ + ...claims, + ['scope', scope] + ]) + + console.log( + 'Generate Dash URI claims', + claims, scope, searchParams, + searchParams.size, + searchParams.entries(), + ) + + let res = `${protocol}${joiner}${addr}` + + if (searchParams.size > 0) { + res += `?${searchParams.toString()}` + } + + return res +} + +export function generatePaymentRequestURI( + state, + protocol = 'dash', + joiner = ':' +) { + let addr = state.wallet?.address || '' + let claims = [] + + if (state.userInfo) { + let filteredInfo = Array.from( + Object.entries(state.userInfo) + ).filter(p => { + let [key, val] = p + if ( + ![ + 'updated_at', + 'email_verified', + 'phone_number_verified', + ].includes(key) && + !isEmpty(val) + ) { + return true + } + }) + + claims = [ + ...filteredInfo, + ] + } + + if (state.amount > 0) { + claims.push( + ["amount", state.amount], + ) + } + + if (state.label) { + claims.push( + ["label", state.label], + ) + } + + if (state.message) { + claims.push( + ["message", state.message], + ) + } + + let searchParams = new DashURLSearchParams([ + ...claims, + ]) + + let res = `${protocol}${joiner}${addr}` + + if (searchParams.size > 0) { + res += `?${searchParams.toString()}` + } + + return res +} + +export async function getRandomWords(len = 32) { + return await DashPhrase.generate(len) +} + +export async function verifyPhrase(phrase) { + return await DashPhrase.verify(phrase).catch(_ => false) +} + +export function isUniqueAlias(aliases, preferredAlias) { + return !aliases[preferredAlias] +} + +export async function getUniqueAlias(aliases, preferredAlias) { + let uniqueAlias = preferredAlias + let notUnique = !isUniqueAlias(aliases, uniqueAlias) + + if (notUnique) { + let aliasArr = uniqueAlias.split('_') + let randomWords = (await getRandomWords()).split(' ') + + if (aliasArr.length > 1) { + let lastWord = aliasArr.pop() + let index = DashPhrase.base2048.indexOf(lastWord); + + if (index < 0) { + aliasArr.push(lastWord) + } else { + aliasArr.push(randomWords[0]) + } + } else { + aliasArr.push(randomWords[0]) + } + + uniqueAlias = aliasArr.join('_') + + return await getUniqueAlias(aliases, uniqueAlias) + } + + return uniqueAlias +} + +export function getPartialHDPath(wallet) { + return [ + wallet.accountIndex, + wallet.usageIndex, + wallet.addressIndex, + ].join('/') +} + +export function getAddressIndexFromUsage(wallet, account, usageIdx) { + let usageIndex = usageIdx ?? wallet?.usageIndex ?? 0 + let addressIndex = account.usage?.[usageIndex] ?? account.addressIndex ?? 0 + let usage = account.usage ?? [ + account.addressIndex ?? 0, + 0 + ] + + // console.log( + // 'getAddressIndexFromUsage', + // usageIndex, + // addressIndex, + // account, + // usage, + // ) + + return { + ...account, + usage, + usageIndex, + addressIndex, + } +} + +export async function generateAddressIterator( + xkey, + xkeyId, + addressIndex, +) { + let key = await xkey.deriveAddress(addressIndex); + let address = await DashHd.toAddr(key.publicKey); + + return { + address, + addressIndex, + usageIndex: xkey.index, + xkeyId, + } +} + +export async function generateAndStoreAddressIterator( + xkey, + xkeyId, + walletId, + accountIndex, + addressIndex, + usageIndex = DashHd.RECEIVE, +) { + let { address } = await generateAddressIterator( + xkey, + xkeyId, + addressIndex, + ) + + // console.log( + // 'generateAddressIterator', + // {xkey, xkeyId, key, address, accountIndex, addressIndex}, + // ) + + store.addresses.getItem(address) + .then(a => { + let $addr = a || {} + // console.log( + // 'generateAddressIterator store.addresses.getItem', + // {address, $addr}, + // ) + + store.addresses.setItem( + address, + { + ...$addr, + updatedAt: Date.now(), + walletId, + xkeyId, + accountIndex, + addressIndex, + usageIndex, + }, + ) + }) + + return { + address, + addressIndex, + accountIndex, + usageIndex: xkey.index, + xkeyId, + } +} + +export async function batchXkeyAddressGenerate( + wallet, + addressIndex = 0, + batchSize = 20, +) { + let batchLimit = addressIndex + batchSize + let addresses = [] + + for (let addrIdx = addressIndex; addrIdx < batchLimit; addrIdx++) { + addresses.push( + await generateAddressIterator( + wallet.xkey, + wallet.xkeyId, + addrIdx, + ) + ) + } + + return { + addresses, + finalAddressIndex: batchLimit, + } +} + +export async function batchAddressGenerate( + wallet, + accountIndex = 0, + addressIndex = 0, + usageIndex = DashHd.RECEIVE, + batchSize = 20, +) { + // let hdpath = `m/44'/5'/${accountIndex}'/${usageIndex}/${addressIndex}`, + let batchLimit = addressIndex + batchSize + let addresses = [] + + let account = await wallet.derivedWallet.deriveAccount(accountIndex); + let xkey = await account.deriveXKey(usageIndex); + let xkeyId = await DashHd.toId(xkey); + + if (usageIndex !== DashHd.RECEIVE) { + let xkeyReceive = await account.deriveXKey(DashHd.RECEIVE); + xkeyId = await DashHd.toId(xkeyReceive); + } + + for (let addrIdx = addressIndex; addrIdx < batchLimit; addrIdx++) { + addresses.push( + await generateAndStoreAddressIterator( + xkey, + xkeyId, + wallet.id, + accountIndex, + addrIdx, + usageIndex, + ) + ) + } + + return { + addresses, + finalAddressIndex: batchLimit, + } +} + +export async function batchAddressUsageGenerate( + wallet, + accountIndex = 0, + addressIndex = 0, + batchSize = 20, +) { + // let hdpath = `m/44'/5'/${accountIndex}'/${usageIndex}/${addressIndex}`, + let batchLimit = addressIndex + batchSize + let addresses = [] + + let account = await wallet.derivedWallet.deriveAccount(accountIndex); + let xkeyReceive = await account.deriveXKey(DashHd.RECEIVE); + let xkeyChange = await account.deriveXKey(DashHd.CHANGE); + let xkeyId = await DashHd.toId(xkeyReceive); + + console.log( + 'batchAddressUsageGenerate', + {batchLimit, account, xkeyReceive, xkeyChange}, + ) + + for (let addrIdx = addressIndex; addrIdx < batchLimit; addrIdx++) { + addresses.push( + await generateAndStoreAddressIterator( + xkeyReceive, + xkeyId, + wallet.id, + accountIndex, + addrIdx, + DashHd.RECEIVE, + ) + ) + addresses.push( + await generateAndStoreAddressIterator( + xkeyChange, + xkeyId, + wallet.id, + accountIndex, + addrIdx, + DashHd.CHANGE, + ) + ) + } + + return { + addresses, + finalAddressIndex: batchLimit, + } +} + +export async function getTotalFunds(wallet) { + let funds = 0 + let result = {} + let addrsLen = await store.addresses.length() + + return await store.addresses.iterate(( + value, key, iterationNumber + ) => { + if (value?.walletId === wallet?.id) { + result[key] = value + funds += value?.insight?.balance || 0 + } + + if (iterationNumber === addrsLen) { + return funds + } + }) +} + +export async function getAddrsWithFunds(wallet) { + let result = {} + let addrsLen = await store.addresses.length() + + return await store.addresses.iterate(( + value, key, iterationNumber + ) => { + if ( + value?.walletId === wallet?.id && + value?.insight?.balance > 0 + ) { + result[key] = { + ...value, + address: key, + } + } + + if (iterationNumber === addrsLen) { + return result + } + }) +} + +export async function batchGenAccts( + phrase, + accountIndex = 0, + batchSize = 5, +) { + let $accts = await getStoredItems(store.accounts) + // let $acctsArr = Object.values($accts) + let accts = {} + let batch = batchSize + accountIndex + + console.log( + 'BATCH GENERATED ACCOUNTS START', + { + $accts, + // $acctsArr, + accountIndex, + batch, + } + ) + + for (let i = accountIndex; i < batch; i++) { + let acctWallet = await deriveWalletData( + phrase, + i, + ) + + if (!$accts[acctWallet.xkeyId]) { + let newAccount = await store.accounts.setItem( + acctWallet.xkeyId, + { + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + accountIndex: i, + usage: [0,0], + walletId: acctWallet.id, + xkeyId: acctWallet.xkeyId, + addressKeyId: acctWallet.addressKeyId, + address: acctWallet.address, + } + ) + + accts[`acct__${i}`] = [ acctWallet, newAccount ] + } + + // accts[`acct__${i}`] = batchGenAcctAddrs( + // acctWallet, + // newAccount, + // ) + } + + // let allBatches = Promise.allSettled(Object.values(accts)) + + return accts +} + +export async function batchGenAcctAddrs( + wallet, + account, + usageIndex = -1, + batchSize = 20, +) { + // console.log('batchGenAcctAddrs account', account, usageIndex) + + let filterQuery = { + accountIndex: account.accountIndex, + } + + if (usageIndex >= 0) { + filterQuery.usageIndex = usageIndex + } + + let acctAddrsLen = await getFilteredStoreLength( + store.addresses, + filterQuery, + ) + + // console.log('getFilteredStoreLength res', acctAddrsLen) + + let addrUsageIdx = account.usage?.[usageIndex] || 0 + let addrIdx = addrUsageIdx + let batSize = batchSize + + if (acctAddrsLen === 0) { + addrIdx = 0 + batSize = addrUsageIdx + batchSize + } + + if (acctAddrsLen <= addrUsageIdx + (batchSize / 2)) { + if (usageIndex >= 0) { + return await batchAddressGenerate( + wallet, + account.accountIndex, + account.usage[usageIndex], + usageIndex, + batSize, + ) + } else { + return await batchAddressUsageGenerate( + wallet, + account.accountIndex, + addrIdx, + batSize, + ) + } + } + + return null +} + +export async function batchGenAcctsAddrs( + wallet, + usageIndex = -1, + batchSize = 20, +) { + let $accts = await getStoredItems(store.accounts) + let $acctsArr = Object.values($accts) + let accts = {} + + if ($acctsArr.length > 0) { + for (let $a of $acctsArr) { + accts[`bat__${$a.accountIndex}`] = await batchGenAcctAddrs( + wallet, + $a, + usageIndex, + batchSize, + ) + } + + // console.warn( + // 'BATCH GENERATED ACCOUNTS', + // accts, + // ) + } + + return accts +} + +export async function getAccountWallet(wallet, phrase) { + let acctFromStore = await store.accounts.getItem( + wallet.xkeyId, + ) || {} + let acctFromStoreWallet = getAddressIndexFromUsage( + wallet, + acctFromStore, + ) + + if (acctFromStoreWallet?.addressIndex > 0) { + return { + wallet: await deriveWalletData( + phrase, + acctFromStoreWallet.accountIndex, + acctFromStoreWallet.addressIndex, + acctFromStoreWallet?.usageIndex ?? USAGE.RECEIVE, + ), + account: acctFromStore, + } + } + + return { + wallet, + account: acctFromStore, + } +} + +export async function forceInsightUpdateForAddress(addr) { + let currentAddr = await store.addresses.getItem( + addr + ) + await store.addresses.setItem( + addr, + { + ...currentAddr, + insight: { + ...currentAddr.insight, + updatedAt: 0 + } + } + ) +} + +export function sortAddrs(a, b) { + // Ascending Lexicographical on TxId (prev-hash) in-memory (not wire) byte order + if (a.accountIndex > b.accountIndex) { + return 1; + } + if (a.accountIndex < b.accountIndex) { + return -1; + } + // addressIndex + // Ascending Vout (Numerical) + let indexDiff = a.addressIndex - b.addressIndex; + return indexDiff; +} + +export function getBalance(utxos) { + return utxos.reduce(function (total, utxo) { + return total + utxo.satoshis; + }, 0); +} + +export function selectOptimalUtxos(utxos, output) { + let balance = getBalance(utxos); + let fees = DashTx.appraise({ + //@ts-ignore + inputs: [{}], + //@ts-ignore + outputs: [{}], + }); + + let fullSats = output + fees.min; + + if (balance < fullSats) { + return []; + } + + // from largest to smallest + utxos.sort(function (a, b) { + return b.satoshis - a.satoshis; + }); + + // /** @type Array */ + let included = []; + let total = 0; + + // try to get just one + utxos.every(function (utxo) { + if (utxo.satoshis > fullSats) { + included[0] = utxo; + total = utxo.satoshis; + return true; + } + return false; + }); + if (total) { + return included; + } + + // try to use as few coins as possible + utxos.some(function (utxo, i) { + included.push(utxo); + total += utxo.satoshis; + if (total >= fullSats) { + return true; + } + + // it quickly becomes astronomically unlikely to hit the one + // exact possibility that least to paying the absolute minimum, + // but remains about 75% likely to hit any of the mid value + // possibilities + if (i < 2) { + // 1 input 25% chance of minimum (needs ~2 tries) + // 2 inputs 6.25% chance of minimum (needs ~8 tries) + fullSats = fullSats + DashTx.MIN_INPUT_SIZE; + return false; + } + // but by 3 inputs... 1.56% chance of minimum (needs ~32 tries) + // by 10 inputs... 0.00953674316% chance (needs ~524288 tries) + fullSats = fullSats + DashTx.MIN_INPUT_SIZE + 1; + }); + return included; +} + +export function sortIncomingAndOutgoingTxs({ + conAddr, tx, addr, dir, sentAmount = null, receivedAmount = null, + byAlias = {}, byAddress = {}, byTx = {}, +}) { + let alias = byTx?.[tx.txid]?.alias || conAddr.alias + byAlias[conAddr.alias] = { + ...(byAlias[conAddr.alias] || []), + [tx.txid]: { + addr, + dir, + sentAmount, + receivedAmount, + ...tx, + ...conAddr, + alias, + } + } + byAddress[addr] = [ + ...(byAddress[addr] || []), + { + receivedAmount, + sentAmount, + dir, + ...tx, + ...conAddr, + alias, + } + ] + byTx[tx.txid] = { + receivedAmount, + sentAmount, + dir, + ...tx, + ...conAddr, + alias, + } + + // console.log( + // 'sortIncomingAndOutgoingTxs', + // conAddr.alias, conAddr.xkeyId, tx, + // ) + + return { + byAlias, + byAddress, + byTx, + } +} + +export async function getContactsByXkeyId( + appState, +) { + let contactsXkeys = {} + + for await (let c of appState.contacts) { + let og = Object.values(c.outgoing || {})?.[0] + let ic = Object.values(c.incoming || {})?.[0] + + if (og) { + contactsXkeys[og.xkeyId] = { + ...c, + dir: 'outgoing', + } + } + if (ic) { + contactsXkeys[ic.xkeyId] = { + ...c, + dir: 'incoming', + } + } + } + + return contactsXkeys +} + +export async function getContactsFromAddrs( + appState, +) { + let accts = await loadStoreObject(store.accounts) + let addrs = await loadStoreObject(store.addresses) + let contactAddrs = await getContactsByXkeyId(appState) + let contacts = {} + + for await (let [ck,cv] of Object.entries(addrs)) { + let contact = contactAddrs[cv.xkeyId] + let acct = accts[cv.xkeyId] + if (contact) { + contacts[ck] = contact + } else if (acct) { + contacts[ck] = { + ...acct, + alias: null, + } + } + } + + return contacts +} + +export async function deriveContactAddrs( + appState, dir = 'outgoing', +) { + let addrs = {} + + for await (let c of appState.contacts) { + let og = Object.values(c[dir] || {})?.[0] + let xkey = og?.xpub || og?.xprv + + if (xkey) { + let contactWallet = await deriveWalletData( + xkey, + ) + let contactAddrs = await batchXkeyAddressGenerate( + contactWallet, + contactWallet.addressIndex, + ) + + contactAddrs.addresses.forEach(g => { + addrs[g.address] = { + alias: c.alias, + xkeyId: contactWallet.xkeyId, + dir, + } + }) + } + } + + // console.log('deriveContactAddrs', { + // asc: appState.contacts, + // addrs, + // }) + + return addrs +} + +export function getTransactionsByContactAlias(appState) { + return async res => { + if (!res) { + return [] + } + + appState.contacts = res + + return res + } +} diff --git a/src/utils/dash/network.js b/src/utils/dash/network.js new file mode 100644 index 0000000..e259793 --- /dev/null +++ b/src/utils/dash/network.js @@ -0,0 +1,727 @@ +import { + DashWallet, + DashTx, + DashSight, + DashSocket, +} from '../../imports.js' + +import { + walletFunds, +} from '../../state/index.js' + +import { + DatabaseSetup, + loadStoreObject, +} from '../db.js' + +import { + batchGenAccts, + batchGenAcctsAddrs, + deriveWalletData, + deriveContactAddrs, + getContactsFromAddrs, + selectOptimalUtxos, + sortAddrs, + sortIncomingAndOutgoingTxs, +} from './local.js' + +let defaultSocketEvents = { + onClose: async (e) => console.log('onClose', e), + onError: async (e) => console.log('onError', e), + onMessage: async (e, data) => console.log('onMessage', e, data), +} + +export const store = await DatabaseSetup() + +// @ts-ignore +export const dashsight = DashSight.create({ + baseUrl: 'https://insight.dash.org', + // baseUrl: 'https://dashsight.dashincubator.dev', +}); + +export async function initDashSocket( + events = {} +) { + // @ts-ignore + let dashsocket = DashSocket.create({ + dashsocketBaseUrl: 'https://insight.dash.org/socket.io', + cookieStore: null, + debug: true, + ...defaultSocketEvents, + ...events, + }) + + await dashsocket.init() + .catch((e) => console.log('dashsocket catch err', e)); + + return dashsocket +} + +export async function updateAddrFunds( + wallet, insightRes, +) { + let updatedAt = Date.now() + let { addrStr, ...res } = insightRes + let $addr = await store.addresses.getItem(addrStr) || {} + let { + walletId, + xkeyId, + } = $addr + + if (walletId && walletId === wallet?.id) { + let storedWallet = await store.wallets.getItem(walletId) || {} + let storedAccount = await store.accounts.getItem(xkeyId) || {} + + $addr.insight = { + ...res, + updatedAt, + } + + store.addresses.setItem( + addrStr, + $addr, + ) + + if (storedAccount.usage[$addr.usageIndex] < $addr.addressIndex) { + storedAccount.usage[$addr.usageIndex] = $addr.addressIndex + store.accounts.setItem( + xkeyId, + storedAccount + ) + } + if (storedWallet.accountIndex < $addr.accountIndex) { + store.wallets.setItem( + walletId, + { + ...storedWallet, + accountIndex: $addr.accountIndex, + } + ) + let storeAcctLen = (await store.accounts.length())-1 + + console.log('updateAddrFunds', { + acctIdx: $addr.accountIndex, + storeAcctLen, + sameOrBigger: $addr.accountIndex >= storeAcctLen, + }) + + if ($addr.accountIndex >= storeAcctLen) { + batchGenAccts(wallet.recoveryPhrase, $addr.accountIndex) + .then(() => { + batchGenAcctsAddrs(wallet) + .then(accts => { + console.log('batchGenAcctsAddrs', { accts }) + + updateAllFunds(wallet) + .then(funds => { + console.log('updateAllFunds then funds', funds) + }) + .catch(err => console.error('catch updateAllFunds', err, wallet)) + }) + }) + } + } + + return res + } + + return { balance: 0 } +} + +export async function updateAllFunds(wallet) { + let funds = 0 + let addrKeys = await store.addresses.keys() + + if (addrKeys.length === 0) { + walletFunds.balance = funds + return funds + } + + console.log( + 'updateAllFunds getInstantBalances for', + {addrKeys}, + addrKeys.length, + ) + + let balances = await dashsight.getInstantBalances(addrKeys) + + if (balances.length >= 0) { + walletFunds.balance = funds + } + + // add insight balances to address + for (const insightRes of balances) { + let { addrStr } = insightRes + let addrIdx = addrKeys.indexOf(addrStr) + if (addrIdx > -1) { + addrKeys.splice(addrIdx, 1) + } + funds += (await updateAddrFunds(wallet, insightRes))?.balance || 0 + walletFunds.balance = funds + } + + // remove insight balances from address + for (const addr of addrKeys) { + let { insight, ...$addr } = await store.addresses.getItem(addr) || {} + + // walletFunds.balance = funds - (_insight?.balance || 0) + + store.addresses.setItem( + addr, + $addr, + ) + } + + console.log('updateAllFunds funds', {balances, funds}) + + return funds +} + +export async function deriveTxWallet( + fromWallet, + fundAddrs, +) { + let cachedAddrs = {} + let privateKeys = {} + let coreUtxos + let tmpWallet + + if (Array.isArray(fundAddrs) && fundAddrs.length > 0) { + fundAddrs.sort(sortAddrs) + + for (let w of fundAddrs) { + tmpWallet = await deriveWalletData( + fromWallet.recoveryPhrase, + w.accountIndex, + w.addressIndex, + w.usageIndex, + ) + privateKeys[tmpWallet.address] = tmpWallet.addressKey.privateKey + cachedAddrs[w.address] = { + checked_at: w.updatedAt, + hdpath: `m/44'/${DashWallet.COIN_TYPE}'/${w.accountIndex}'/${w.usageIndex}`, + index: w.addressIndex, + wallet: w.walletId, // maybe `selectedAlias`? + txs: [], + utxos: [], + } + } + + coreUtxos = await dashsight.getMultiCoreUtxos( + Object.keys(privateKeys) + ) + } else { + tmpWallet = await deriveWalletData( + fromWallet.recoveryPhrase, + fundAddrs.accountIndex, + fundAddrs.addressIndex, + fundAddrs.usageIndex, + ) + privateKeys[tmpWallet.address] = tmpWallet.addressKey.privateKey + cachedAddrs[fundAddrs.address] = { + checked_at: fundAddrs.updatedAt, + hdpath: `m/44'/${DashWallet.COIN_TYPE}'/${fundAddrs.accountIndex}'/${fundAddrs.usageIndex}`, + index: fundAddrs.addressIndex, + wallet: fundAddrs.walletId, // maybe `selectedAlias`? + txs: [], + utxos: [], + } + coreUtxos = await dashsight.getCoreUtxos( + tmpWallet.address + ) + } + + return { + privateKeys, + cachedAddrs, + coreUtxos, + } +} + +export async function createStandardTx( + fromWallet, + fundAddrs, + changeAddrs, + recipient, + amount, + fullTransfer = false, +) { + const amountSats = DashTx.toSats(amount) + + console.log('amount to send', { + amount, + amountSats, + }) + + let selection + let receiverOutput + let outputs = [] + let { + privateKeys, + coreUtxos, + cachedAddrs, + } = await deriveTxWallet(fromWallet, fundAddrs) + let changeAddr = changeAddrs[0] + + let recipientAddr = recipient?.address || recipient + + // @ts-ignore + let dashwallet = await DashWallet.create({ + safe: { + cache: { + addresses: cachedAddrs + } + }, + store: { + save: data => console.log('dashwallet.store.save', {data}) + }, + dashsight, + }) + + receiverOutput = DashWallet._parseSendInfo(dashwallet, amountSats); + + selection = dashwallet.useMatchingCoins({ + output: receiverOutput, + utxos: coreUtxos, + breakChange: false, + }) + + console.log('coreUtxos', { + coreUtxos, + selection, + amount, + amountSats, + fullTransfer, + }) + + let stampVal = dashwallet.__STAMP__ * selection.output.stampsPerCoin + let receiverDenoms = receiverOutput?.denoms.slice(0) + + for (let denom of selection.output.denoms) { + let address = ''; + let matchingDenomIndex = receiverDenoms.indexOf(denom) + if (matchingDenomIndex >= 0) { + void receiverDenoms.splice(matchingDenomIndex, 1) + address = recipientAddr + } else { + address = changeAddr + } + + let coreOutput = { + address, + // address: addrsInfo.addresses.pop(), + satoshis: denom + stampVal, + faceValue: denom, + stamps: selection.output.stampsPerCoin, + } + + outputs.push(coreOutput) + } + + let txInfo = { + inputs: selection.inputs, + outputs: outputs, + }; + + txInfo.outputs.sort(DashTx.sortOutputs) + txInfo.inputs.sort(DashTx.sortInputs) + + let keys = txInfo.inputs.map( + utxo => privateKeys[utxo.address] + ) + + return [ + txInfo, + keys, + changeAddr, + ] +} + +export async function createOptimalTx( + fromWallet, + fundAddrs, + changeAddrs, + recipient, + amount, +) { + const MIN_FEE = 191; + const DUST = 2000; + const amountSats = DashTx.toSats(amount) + + console.log('amount to send', { + amount, + amountSats, + fundAddrs, + }) + + let changeAddr = changeAddrs[0] + + let { + privateKeys, + coreUtxos, + } = await deriveTxWallet(fromWallet, fundAddrs) + + let optimalUtxos = selectOptimalUtxos( + coreUtxos, + amountSats, + ) + + console.log('utxos', { + core: coreUtxos, + optimal: optimalUtxos, + }) + + let recipientAddr = recipient?.address || recipient + + let payments = [ + { + address: recipientAddr, + satoshis: amountSats, + }, + ] + + let spendableDuffs = optimalUtxos.reduce(function (total, utxo) { + return total + utxo.satoshis; + }, 0) + let spentDuffs = payments.reduce(function (total, output) { + return total + output.satoshis; + }, 0) + let unspentDuffs = spendableDuffs - spentDuffs + + let txInfo = { + inputs: optimalUtxos, + outputs: payments, + } + + let sizes = DashTx.appraise(txInfo) + let midFee = sizes.mid + + if (unspentDuffs < MIN_FEE) { + throw new Error( + `overspend: inputs total '${spendableDuffs}', but outputs total '${spentDuffs}', which leaves no way to pay the fee of '${sizes.mid}'`, + ) + } + + txInfo.inputs.sort(DashTx.sortInputs) + + let outputs = txInfo.outputs.slice(0) + let change + + change = unspentDuffs - (midFee + DashTx.OUTPUT_SIZE) + if (change < DUST) { + change = 0 + } + if (change) { + txInfo.outputs = outputs.slice(0); + txInfo.outputs.push({ + address: changeAddr, + satoshis: change, + }) + } + + txInfo.outputs.sort(DashTx.sortOutputs) + + let keys = optimalUtxos.map( + utxo => privateKeys[utxo.address] + ) + + return [ + txInfo, + keys, + changeAddr, + ] +} + +export async function createDraftTx( + fromWallet, + fundAddrs, + changeAddrs, + recipient, + amount, + fullTransfer = false, +) { + const amountSats = DashTx.toSats(amount) + + console.log('amount to send', { + amount, + amountSats, + }) + + let { + privateKeys, + coreUtxos, + cachedAddrs, + } = await deriveTxWallet(fromWallet, fundAddrs) + let changeAddr = changeAddrs[0] + + let recipientAddr = recipient?.address || recipient + + // @ts-ignore + let dashwallet = await DashWallet.create({ + safe: { + cache: { + addresses: cachedAddrs + } + }, + store: { + save: data => console.log('dashwallet.store.save', {data}) + }, + dashsight, + }) + + let utxos = null; + let inputs = null; + let output = { + address: recipientAddr, + satoshis: amountSats + }; + + if (coreUtxos) { + inputs = coreUtxos; + if (fullTransfer) { + output.satoshis = null; + } + } else { + utxos = coreUtxos; + } + + let txDraft = await dashwallet.legacy.draftTx({ + utxos, + inputs, + output, + feeSize: 'max', + }) + + if (txDraft.change) { + txDraft.change.address = changeAddr; + } + + let keys = txDraft.inputs.map( + utxo => privateKeys[utxo.address] + ) + + let txSummary = await dashwallet.legacy.finalizeAndSignTx(txDraft, keys); + + return { + ...txSummary, + changeAddr, + } +} + +export async function createTx( + fromWallet, + fundAddrs, + changeAddrs, + recipient, + amount, + fullTransfer = false, + mode = null, +) { + let tmpTx + let dashTx = DashTx.create({ + // @ts-ignore + version: 3, + }); + + if (fullTransfer) { + let tx = await createDraftTx( + fromWallet, + fundAddrs, + changeAddrs, + recipient, + amount, + fullTransfer, + ) + + console.log('fullTransfer tx', tx); + + return { + tx, + changeAddr: tx.changeAddr, + fee: tx.fee, + // fee: inFee - outFee, + } + } else if (mode === 'cash') { + // Denominated TX + tmpTx = await createStandardTx( + fromWallet, + fundAddrs, + changeAddrs, + recipient, + amount, + fullTransfer, + ) + } else { + // Non-Denominated TX + tmpTx = await createOptimalTx( + fromWallet, + fundAddrs, + changeAddrs, + recipient, + amount, + ) + } + + let [txInfo, keys, changeAddr] = tmpTx + + let inFee = txInfo.inputs.reduce((acc, cur) => acc + cur.satoshis, 0) + let outFee = txInfo.outputs.reduce((acc, cur) => acc + cur.satoshis, 0) + + console.log('txInfo', { + txInfo, + calcFee: { + in: inFee, + out: outFee, + fee: inFee - outFee, + }, + }); + + // @ts-ignore + let tx = await dashTx.hashAndSignAll(txInfo, keys); + + console.log('tx', tx); + + return { + tx, + changeAddr, + fee: inFee - outFee, + } +} + +export async function sendTx( + tx, +) { + let txHex = tx.transaction; + + console.log('txHex', [txHex]); + + let result = await dashsight.instantSend(txHex); + + console.log('instantSend result', result); + + return result +} + + + +export async function getAddrsTransactions({ + appState, addrs, contactAddrs = {}, + txs = [], +}) { + let storeAddrs = await loadStoreObject(store.addresses) + if (txs.length === 0) { + txs = await dashsight.getAllTxs(addrs) + } + let byAddress = {} + let byAlias = {} + let byTx = {} + + // console.log('getAddrsTransactions', { + // txs, addrs, contactAddrs, appT: appState.transactions + // }) + + for await (let tx of txs) { + let dir = 'received' + let conAddr + let sentAmount = 0 + let receivedAmount = 0 + + for await (let vin of tx.vin) { + let addr = vin.addr + conAddr = contactAddrs[addr] + + if(storeAddrs[addr]) { + dir = 'sent' + sentAmount += Number(vin.value) + } + + if (conAddr) { + sortIncomingAndOutgoingTxs({ + tx, addr, conAddr, dir, sentAmount, + byAlias, byAddress, byTx, + }) + } + } + + for await (let vout of tx.vout) { + if (vout?.scriptPubKey?.addresses) { + for await (let addr of vout.scriptPubKey.addresses) { + // let addr = vout.scriptPubKey.addresses[0] + conAddr = contactAddrs[addr] + + if(storeAddrs[addr]) { + receivedAmount += Number(vout.value) + } else { + // sentAmount -= Number(vout.value) + } + + if (conAddr) { + sortIncomingAndOutgoingTxs({ + tx, addr, conAddr, dir, receivedAmount, + byAlias, byAddress, byTx, + }) + } + } + } + } + + byTx[tx.txid] = { + ...byTx[tx.txid], + receivedAmount, + sentAmount, + } + + if (!appState.transactions[tx.txid]?.vin) { + store.transactions.setItem( + tx.txid, + { + ...(appState.transactions[tx.txid] || {}), + ...tx, + dir, + sentAmount, + receivedAmount, + }, + ) + } + } + + // console.log('getAddrsTransactions by Tx', byTx) + // console.log('getAddrsTransactions by Alias', byAlias) + // console.log('getAddrsTransactions by Address', byAddress) + + return { + byAddress, + byAlias, + byTx, + } +} + +export async function getTxs(appState, transactions = []) { + let contactAddrs = await getContactsFromAddrs(appState) + let contactOutAddrs = await deriveContactAddrs(appState) + + contactAddrs = { + ...contactAddrs, + ...contactOutAddrs, + } + + let addrs = Object.keys(contactAddrs) + + if (addrs.length === 0) { + return + } + + // let TxStore = await store.transactions.keys() + // let txs = await dashsight.getAllTxs(addrs) + + let txs = await getAddrsTransactions({ + appState, addrs, contactAddrs, + txs: transactions, + }) + + // console.log('getTxs', { + // txs, contactAddrs, contactOutAddrs, addrs + // }) + + return txs +} diff --git a/src/helpers/db.js b/src/utils/db.js similarity index 52% rename from src/helpers/db.js rename to src/utils/db.js index 3314635..03b7222 100644 --- a/src/helpers/db.js +++ b/src/utils/db.js @@ -20,47 +20,26 @@ export const localForageBaseCfg = { version: 1.0, } -export async function DatabaseSetup() { - var wallets = localforage.createInstance({ - ...localForageBaseCfg, - storeName: 'wallets', - }); - var aliases = localforage.createInstance({ - ...localForageBaseCfg, - storeName: 'aliases', - }); - var contacts = localforage.createInstance({ - ...localForageBaseCfg, - storeName: 'contacts', - }); - var accounts = localforage.createInstance({ - ...localForageBaseCfg, - storeName: 'accounts', - }); - // accounts.ready(r => { - // console.log('accounts', r, accounts, accounts._dbInfo.db) - // let tx = accounts._dbInfo.db.transaction("accounts", "readwrite"); - // let accts = tx.objectStore("accounts"); - // let acctWalletIndex = accts.createIndex('wallet_id', 'walletId'); - // console.log('acctWalletIndex', tx, accts, acctWalletIndex) - // }) - var addresses = localforage.createInstance({ - ...localForageBaseCfg, - storeName: 'addresses', - }); - var transactions = localforage.createInstance({ +const loadedStores = {} + +export function loadInstance(name) { + loadedStores[name] = loadedStores[name] || localforage.createInstance({ ...localForageBaseCfg, - storeName: 'transactions', + storeName: [name], }); - return { - wallets, - addresses, - contacts, - accounts, - aliases, - transactions, - } + return loadedStores[name] +} + +export async function DatabaseSetup() { + loadInstance('wallets'); + loadInstance('aliases'); + loadInstance('contacts'); + loadInstance('accounts'); + loadInstance('addresses'); + loadInstance('transactions'); + + return loadedStores } // https://gist.github.com/loilo/ed43739361ec718129a15ae5d531095b#file-idb-backup-and-restore-mjs @@ -197,3 +176,167 @@ export function exportWalletData(name, version) { .catch(console.error) } } + +export async function getStoreData( + store, + callback, + iterableCallback = res => async (v, k, i) => res.push(v) +) { + let result = [] + + return await store.keys().then(async function(keys) { + for (let k of keys) { + let v = await store.getItem(k) + await iterableCallback(result)(v, k) + } + + callback?.(result) + + return result + }).catch(function(err) { + console.error('getStoreData', err) + return null + }); +} + +export async function loadStore( + store, + callback, + iterableCallback = res => v => res.push(v) +) { + let result = [] + + return await store.iterate(iterableCallback(result)) + .then(() => callback?.(result)) + .catch(err => { + console.error('loadStore', err) + return null + }); +} + +export async function loadStoreObject(store, callback) { + let result = {} + + return await store.iterate((v, k, i) => { + result[k] = v + }) + .then(() => callback?.(result)) + .then(() => result) + .catch(err => { + console.error('loadStoreObject', err) + return null + }); +} + +export async function getFilteredStoreLength(targStore, query = {}) { + let resLength = 0 + let storeLen = await targStore.length() + let qs = Object.entries(query) + + // console.log('getFilteredStoreLength qs', { + // storeName: targStore?._config?.storeName, + // storeLen, + // qs, + // }) + + if (storeLen === 0) { + return 0 + } + + return await targStore.iterate(( + value, key, iterationNumber + ) => { + let res = true + + // console.log('getFilteredStoreLength qs before each', key, res) + + qs.forEach(([k,v]) => { + // console.log('getFilteredStoreLength qs each', k, v, value[k]) + if (k === 'key' && key !== v || value[k] !== v) { + res = undefined + } + }) + + // console.log('getFilteredStoreLength qs after each', key, res) + + if (res) { + resLength += 1 + } + + if (iterationNumber === storeLen) { + return resLength + } + }) +} + +export async function findInStore(targStore, query = {}) { + let result = {} + let storeLen = await targStore.length() + let qs = Object.entries(query) + // console.log('findInStore qs', qs) + + return await targStore.iterate(( + value, key, iterationNumber + ) => { + let res = value + + // console.log('findInStore qs before each', key, res) + + qs.forEach(([k,v]) => { + // console.log('findInStore qs each', k, v, value[k]) + if (k === 'key' && key !== v || value[k] !== v) { + res = undefined + } + }) + + // console.log('findInStore qs after each', key, res) + + if (res) { + result[key] = res + } + + if (iterationNumber === storeLen) { + return result + } + }) +} + +export async function findOneInStore(targStore, query = {}) { + let storeLen = await targStore.length() + let qs = Object.entries(query) + + return await targStore.iterate(( + value, key, iterationNumber + ) => { + let res = value + + qs.forEach(([k,v]) => { + if (k === 'key' && key !== v || value[k] !== v) { + res = undefined + } + }) + + if (res) { + return res + } + + if (iterationNumber === storeLen) { + return undefined + } + }) +} + +export async function getStoredItems(targStore) { + let result = {} + let storeLen = await targStore.length() + + return await targStore.iterate(( + value, key, iterationNumber + ) => { + result[key] = value + + if (iterationNumber === storeLen) { + return result + } + }) +} diff --git a/src/utils/generic.js b/src/utils/generic.js new file mode 100644 index 0000000..68531db --- /dev/null +++ b/src/utils/generic.js @@ -0,0 +1,539 @@ +import { + TIMEAGO_LOCALE_EN, MOMENTS, NEVER, + SECONDS, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR, +} from './constants.js' + +let eventHandlers = [] + +/** + * Code Highlighting for String Literals. + * + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#raw_strings MDN Reference} + * + * @example + * import { lit as html, lit as css } from './utils.js' + * let h = html`
${example}
` + * let c = css`div > span { color: #bad; }` + * + * // falsey values now default to empty string + * let falsey = html`
${doesNotExist && html``}
` + + * // falsey === '
' + * // instead of + * // falsey === '
undefined
' + * + * @param {TemplateStringsArray} s + * @param {...any} v + * + * @returns {string} + */ +export const lit = (s, ...v) => String.raw({ raw: s }, ...(v.map(x => x || ''))) + +export function isEmpty(value) { + if (value === null) { + return true + } + // if (typeof value === 'boolean' && value === false) { + // return true + // } + if (typeof value === 'string' && value?.length === 0) { + return true + } + if (typeof value === 'object' && Object.keys(value)?.length === 0) { + return true + } + if (Array.isArray(value) && value.length === 0) { + return true + } + return false; +} + +/** + * promise debounce changes + * + * https://www.freecodecamp.org/news/javascript-debounce-example/ + * + * @example + * const change = debounce((a) => console.log('Saving data', a)); + * change('b');change('c');change('d'); + * 'Saving data d' + * + * @param {(...args) => void} callback + * @param {number} [delay] +* +* @returns {Promise} +*/ +export async function debouncePromise(callback, delay = 300) { + let timer + + return await new Promise(resolve => async (...args) => { + clearTimeout(timer) + + timer = setTimeout(() => { + resolve(callback.apply(this, args)) + }, delay) + }) +} + +/** + * debounce changes + * + * https://www.freecodecamp.org/news/javascript-debounce-example/ + * + * @example + * const change = debounce((a) => console.log('Saving data', a)); + * change('b');change('c');change('d'); + * 'Saving data d' + * + * @param {(...args) => void} callback + * @param {number} [delay] +* +* @returns {(...args) => void} +*/ +export function debounce(callback, delay = 300) { + let timer + + return (...args) => { + clearTimeout(timer) + + timer = setTimeout(() => { + return callback.apply(this, args) + }, delay) + + return timer + } +} + +/** + * debounce that immediately triggers and black holes any extra + * executions within the time delay + * + * https://www.freecodecamp.org/news/javascript-debounce-example/ + * + * @example + * const dry = nobounce((a) => console.log('Saving data', a)); + * dry('b');dry('c');dry('d'); + * 'Saving data b' + * + * @param {(...args) => void} callback + * @param {number} [delay] +* +* @returns {(...args) => void} +*/ +export function nobounce(callback, delay = 300) { + let timer + + return (...args) => { + if (!timer) { + callback.apply(this, args) + } + + clearTimeout(timer) + + timer = setTimeout(() => { + timer = undefined + }, delay) + } +} + + +/** + * @example + * await forIt(500); + * nowDoThis() + * + * @param {number} [delay] +* +* @returns {Promise} +*/ +export function forIt(delay) { + return new Promise(resolve => setTimeout(resolve, delay)); +} + +export function timeago(ms, locale = TIMEAGO_LOCALE_EN) { + var ago = Math.floor(ms / 1000); + var part = 0; + + if (ago < MOMENTS) { return locale.moment; } + if (ago < SECONDS) { return locale.moments; } + if (ago < MINUTE) { return locale.seconds.replace(/%\w?/, `${ago}`); } + + if (ago < (2 * MINUTE)) { return locale.minute; } + if (ago < HOUR) { + while (ago >= MINUTE) { ago -= MINUTE; part += 1; } + return locale.minutes.replace(/%\w?/, `${part}`); + } + + if (ago < (2 * HOUR)) { return locale.hour; } + if (ago < DAY) { + while (ago >= HOUR) { ago -= HOUR; part += 1; } + return locale.hours.replace(/%\w?/, `${part}`); + } + + if (ago < (2 * DAY)) { return locale.day; } + if (ago < WEEK) { + while (ago >= DAY) { ago -= DAY; part += 1; } + return locale.days.replace(/%\w?/, `${part}`); + } + + if (ago < (2 * WEEK)) { return locale.week; } + if (ago < MONTH) { + while (ago >= WEEK) { ago -= WEEK; part += 1; } + return locale.weeks.replace(/%\w?/, `${part}`); + } + + if (ago < (2 * MONTH)) { return locale.month; } + if (ago < YEAR) { // 45 years, approximately the epoch + while (ago >= MONTH) { ago -= MONTH; part += 1; } + return locale.months.replace(/%\w?/, `${part}`); + } + + if (ago < NEVER) { + return locale.years; + } + + return locale.never; +} + +// https://stackoverflow.com/a/66494926 +export function getBackgroundColor(stringInput) { + let stringUniqueHash = [...stringInput].reduce((acc, char) => { + return char.charCodeAt(0) + ((acc << 5) - acc); + }, 0); + return `hsl(${stringUniqueHash % 360}, 100%, 67%)`; +} + + + +export async function sha256(str) { + const buf = await crypto.subtle.digest( + "SHA-256", new TextEncoder().encode(str) + ); + return Array.prototype.map.call( + new Uint8Array(buf), + x => (('00' + x.toString(16)).slice(-2)) + ).join(''); +} + + + +export async function getAvatarUrl( + email, + size = 48, + rating = 'pg', + srv = 'gravatar', +) { + let emailSHA = await sha256(email || '') + + if (srv === 'gravatar') { + return `https://gravatar.com/avatar/${ + emailSHA + }?s=${size}&r=${rating}&d=retro` + } + if (srv === 'libravatar') { + return `https://seccdn.libravatar.org/avatar/${ + emailSHA + }?s=${size}&r=${rating}&d=retro` + } + + return '' +} + +export async function getAvatar(c) { + let initials = c?.info?.name?. + split(' ').map(n => n[0]).slice(0,3).join('') || '' + let nameOrAlias = c?.info?.name || c?.alias || c?.info?.preferred_username + + if (!initials) { + initials = (c?.alias || c?.info?.preferred_username)?.[0] || '' + } + + let avStr = `
${initials}
` +} + +export function fileIsSubType(file, type) { + const fileType = file?.type?.split('/')?.[1] + + if (!fileType) { + return false + } + + return fileType === type +} + +// fileInTypes({type:'application/json'}, ['image/png']) +export function fileInMIMETypes(file, types = []) { + const fileType = file?.type + + if (!fileType) { + return false + } + + return types.includes(fileType) +} + +export function fileTypeInTypes(file, types = []) { + const fileType = file?.type?.split('/')?.[0] + + if (!fileType) { + return false + } + + return types.includes(fileType) +} + +export function fileTypeInSubtype(file, subtypes = []) { + const fileSubType = file?.type?.split('/')?.[1] + + if (!fileSubType) { + return false + } + + return subtypes.includes(fileSubType) +} + +export function readFile(file, options) { + let opts = { + expectedFileType: 'json', + denyFileTypes: ['audio','video','image','font','model'], + denyFileSubTypes: ['msword','xml'], + callback: () => {}, + errorCallback: () => {}, + ...options, + } + let reader = new FileReader(); + let result + + reader.addEventListener('load', () => { + if ( + fileTypeInTypes( + file, + opts.denyFileTypes, + ) || fileTypeInSubtype( + file, + opts.denyFileSubTypes, + ) + ) { + return opts.errorCallback?.({ + err: `Wrong file type: ${file.type}. Expected: ${opts.expectedFileType}.`, + file, + }) + } + + try { + // @ts-ignore + result = JSON.parse(reader?.result || '{}'); + + // console.log('parse loaded json', result); + + opts.callback?.(result, file) + + // state[key] = result + } catch(err) { + opts.errorCallback?.({ + err, + file, + }) + + throw new Error(`failed to parse JSON data from ${file.name}`) + } + }); + + reader.readAsText(file); +} + + + + + + + +export function toSlug(...slugs) { + return slugs.join('_').toLowerCase() + .replaceAll(/[^a-zA-Z _]/g, '') + .replaceAll(' ', '_') +} + + + + + +export function addListener( + node, + event, + handler, + capture = false, + handlers = this?.eventHandlers || eventHandlers, +) { + console.log('addListener', this, { node, event, handler, capture }) + handlers.push({ node, event, handler, capture }) + node.addEventListener(event, handler, capture) +} + +export function addListeners( + resolve, + reject, +) { + if (resolve && reject) { + addListener( + this.elements.dialog, + 'close', + this.events.close(resolve, reject), + ) + + addListener( + this.elements.dialog, + 'click', + this.events.click, + ) + } + + addListener( + this.elements.form, + 'blur', + this.events.blur, + ) + addListener( + this.elements.form, + 'focusout', + this.events.focusout, + ) + addListener( + this.elements.form, + 'focusin', + this.events.focusin, + ) + addListener( + this.elements.form, + 'change', + this.events.change, + ) + // if (updrop) { + addListener( + this.elements.form, + 'drop', + this.events.drop, + ) + addListener( + this.elements.form, + 'dragover', + this.events.dragover, + ) + addListener( + this.elements.form, + 'dragend', + this.events.dragend, + ) + addListener( + this.elements.form, + 'dragleave', + this.events.dragleave, + ) + // } + addListener( + this.elements.form, + 'input', + this.events.input, + ) + addListener( + this.elements.form, + 'reset', + this.events.reset, + ) + addListener( + this.elements.form, + 'submit', + this.events.submit, + ) +} + +export function removeAllListeners( + targets = [ + this?.elements.dialog, + this?.elements.form, + ], + handlers = this?.eventHandlers || eventHandlers, +) { + if (this.elements.updrop) { + targets.push(this.elements.updrop) + } + handlers = handlers + .filter(({ node, event, handler, capture }) => { + if (targets.includes(node)) { + node.removeEventListener(event, handler, capture) + return false + } + return true + }) +} + + +export function formDataEntries(event) { + let fd = new FormData( + event.target, + event.submitter + ) + + return Object.fromEntries(fd.entries()) +} + +export function copyToClipboard(target) { + target.select(); + document.execCommand("copy"); +} + +export function setClipboard(event) { + event.preventDefault() + let el = event.target?.previousElementSibling + let val = el.textContent?.trim() + if (el.nodeName === 'INPUT') { + val = el.value?.trim() + } + const type = "text/plain"; + const blob = new Blob([val], { type }); + + if ( + "clipboard" in navigator && + typeof navigator.clipboard.write === "function" + ) { + const data = [new ClipboardItem({ [type]: blob })]; + + navigator.clipboard.write(data).then( + cv => { + console.log('setClipboard', cv) + }, + ce => { + console.error('[fail] setClipboard', ce) + } + ); + } else { + copyToClipboard(el) + } +} + +export function openBlobSVG(target) { + const svgStr = new XMLSerializer().serializeToString(target); + const svgBlob = new Blob([svgStr], { type: "image/svg+xml" }); + const url = URL.createObjectURL(svgBlob); + const win = open(url); + win.onload = (evt) => URL.revokeObjectURL(url); +} diff --git a/src/helpers/qr.js b/src/utils/qr.js similarity index 70% rename from src/helpers/qr.js rename to src/utils/qr.js index af57cd3..771aaf9 100644 --- a/src/helpers/qr.js +++ b/src/utils/qr.js @@ -1,7 +1,5 @@ "use strict"; -import { toDash } from './utils.js' - /** * @typedef QrOpts * @property {String} [background] @@ -31,7 +29,7 @@ export function create(data, opts) { background: opts?.background || "#fff", ecl: opts?.ecl || "M", }); -}; +} /** * @param {String} data @@ -41,18 +39,4 @@ export function qrSvg (data, opts) { // console.log('qrSvg', data) let qrcode = create(data, opts); return qrcode.svg(); -}; - -/** - * @param {String} addr - Base58Check pubKeyHash address - * @param {Number} duffs - 1/100000000 of a DASH - */ -export function showQr(addr, duffs = 0) { - let dashAmount = toDash(duffs); - let dashUri = `dash://${addr}`; - if (duffs) { - dashUri += `?amount=${dashAmount}`; - } - - return qrSvg(dashUri, { indent: 4, size: "mini" }); } diff --git a/src/utils/retort.js b/src/utils/retort.js new file mode 100644 index 0000000..c4b5703 --- /dev/null +++ b/src/utils/retort.js @@ -0,0 +1,285 @@ +let currentSignal; +let globalDerivedValue; + +/** + * Creates a reactive signal + * + * Inspired By + * {@link https://gist.github.com/developit/a0430c500f5559b715c2dddf9c40948d Valoo} & + * {@link https://dev.to/ratiu5/implementing-signals-from-scratch-3e4c Signals from Scratch} + * + * @example + * let count = createSignal(0) + * console.log(count.value) // 0 + * count.value = 2 + * console.log(count.value) // 2 + * + * let off = count.on((value) => { + * document.querySelector("body").innerHTML = value; + * }); + * + * off(); + * + * @param {Object} initialValue inital value +*/ +export function createSignal(initialValue) { + let _value = initialValue; + let _last = _value; + let subs = []; + + function pub() { + for (let s of subs) { + s && s(_value, _last); + } + } + + function unsub(fn) { + for (let i in subs) { + if (subs[i] === fn) { + subs[i] = 0; + // break; + } + } + } + + function on(s) { + const i = subs.push(s)-1; + return () => { subs[i] = 0; }; + } + + function once(s) { + const i = subs.length + + subs.push((_value, _last) => { + s && s(_value, _last); + subs[i] = 0; + }); + } + + return { + get value() { + if (currentSignal) { + on(currentSignal) + } + return _value; + }, + set value(v) { + _last = _value + _value = v; + pub(); + }, + on, + once, + unsub, + } +} + +/** + * Use a reactive signal in hook fashion + * + * @example + * let [count, setCount, on] = useSignal(0) + * console.log(count()) // 0 + * setCount(2) + * console.log(count()) // 2 + * + * let off = on(value => { + * document.querySelector("body").innerHTML = value; + * }); + * + * off() + * + * @param {Object} initialValue inital value +*/ +export function useSignal(initialValue) { + let _value = initialValue; + let _last = _value; + let subs = []; + + function pub() { + for (let s of subs) { + s && s(_value, _last); + } + } + + function unsub(fn) { + for (let i in subs) { + if (subs[i] === fn) { + subs[i] = 0; + // break; + } + } + } + + function getValue(v) { + if ( + currentSignal //&& + // currentSignal !== globalDerivedValue + ) { + on(currentSignal) + } + return _value; + } + + function setValue(v) { + _last = _value + _value = v; + pub(); + } + + function on(s) { + const i = subs.push(s)-1; + return () => { subs[i] = 0; }; + } + + function once(s) { + const i = subs.length + + subs.push((_value, _last) => { + s && s(_value, _last); + subs[i] = 0; + }); + } + + return [ + // _value, + getValue, + setValue, + on, + once, + unsub, + ] +} + +/** + * {@link https://youtu.be/t18Kzj9S8-M?t=351 Understanding Signals} + * + * {@link https://youtu.be/1TSLEzNzGQM Learn Why JavaScript Frameworks Love Signals By Implementing Them} + * + * @example + * const [count, setCount] = useSignal(10) + * effect(() => console.log(count())) + * setCount(25) + * + * let letter = createSignal('a') + * effect(() => console.log(letter.value)) + * letter.value = 'b' + * + * @param {Function} fn + */ +export function effect(fn) { + currentSignal = fn; + + fn(); + + currentSignal = null; + + return fn +} + +/** + * {@link https://youtu.be/1TSLEzNzGQM Learn Why JavaScript Frameworks Love Signals By Implementing Them} + * + * @example + * let count = createSignal(10) + * let double = derived(() => count.value * 2) + * + * effect( + * () => console.log( + * count.value, + * double.value, + * ) + * ) + * + * count.value = 25 + * + * @param {Function} fn + */ +export function derived(fn) { + const derived = createSignal() + + globalDerivedValue = function derivedValue() { + derived.value = fn() + } + + effect(globalDerivedValue) + + return derived +} + +/** + * Creates a `Proxy` wrapped object with optional listeners + * that react to changes + * + * @example + * let fooHistory = [] + * + * let kung = envoy( + * { foo: 'bar' }, + * function firstListener(state, oldState) { + * if (state.foo !== oldState.foo) { + * localStorage.foo = state.foo + * }, + * }, + * async function secondListener(state, oldState) { + * if (state.foo !== oldState.foo) { + * fooHistory.push(oldState.foo) + * } + * } + * ) + * kung.foo = 'baz' + * console.log(localStorage.foo) // 'baz' + * kung.foo = 'boo' + * console.log(fooHistory) // ['bar','baz'] + * + * @param {Object} obj + * @param {...( + * state: any, oldState: any, prop: string | symbol + * ) => void | Promise?} [initListeners] + * + * @returns {obj} + */ +export function envoy(obj, ...initListeners) { + let _listeners = [...initListeners] + return new Proxy(obj, { + get(obj, prop, receiver) { + if (prop === '_listeners') { + return _listeners + } + return Reflect.get(obj, prop, receiver) + }, + set(obj, prop, value) { + if ( + prop === '_listeners' && + Array.isArray(value) + ) { + _listeners = value + } + + _listeners.forEach( + fn => fn( + {...obj, [prop]: value}, + obj, + prop + ) + ) + + obj[prop] = value + + return true + } + }) +} + +export async function restate( + state = {}, + renderState = {}, +) { + let renderKeys = Object.keys(renderState) + + for await (let prop of renderKeys) { + state[prop] = renderState[prop] + } + + return state +}