diff --git a/package-lock.json b/package-lock.json index 368ae22..096adb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,21 +12,21 @@ "@dashincubator/base58check": "^1.4.1", "@dashincubator/ripemd160": "^3.0.0", "@dashincubator/secp256k1": "^1.7.1-5", - "@zxing/library": "^0.20.0", + "@zxing/library": "^0.21.2", "crypticstorage": "^0.0.2", - "dashhd": "^3.3.0", - "dashkeys": "^1.1.0", + "dashhd": "^3.3.3", + "dashkeys": "^1.1.4", "dashphrase": "^1.4.0", - "dashsight": "^1.6.1", - "dashtx": "^0.13.2", - "dashwallet": "^0.7.0-1", + "dashsight": "dashhive/DashSight.js#feat/bulk-txs", + "dashtx": "^0.16.0", + "dashwallet": "^0.7.1", "html5-qrcode": "^2.3.8", "idb": "^8.0.0", "localforage": "^1.10.0", "qrcode-svg": "^1.1.0" }, "devDependencies": { - "@types/node": "^20.11.5" + "@types/node": "^20.14.11" } }, "node_modules/@dashincubator/base58check": { @@ -48,18 +48,20 @@ "integrity": "sha512-3iA+RDZrJsRFPpWhlYkp3EdoFAlKjdqkNFiRwajMrzcpA/G/IBX0AnC1pwRLkTrM+tUowcyGrkJfT03U4ETZeg==" }, "node_modules/@types/node": { - "version": "20.11.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", - "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@zxing/library": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz", - "integrity": "sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.2.tgz", + "integrity": "sha512-VMCCSUJSld3tqG6aREJ6XBCxYuoQFcjrF1kowKPFqTA6QG1ixfm6bVfD7gP4jjfM0MX20wVB65DEXtjRsBmV6w==", + "license": "MIT", "dependencies": { "ts-custom-error": "^3.2.1" }, @@ -82,17 +84,16 @@ "integrity": "sha512-PyiKq03ekJU1AwzYVchlm+LaUvDy939S1smmCD0fa60HLKVM0m6zEj4J/MXcm1BLrwdaZk9eRdJcau77nIOPAw==" }, "node_modules/dashhd": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dashhd/-/dashhd-3.3.0.tgz", - "integrity": "sha512-1q80mdGC1RMopuoni/vNXtLIF37YSDE0B27sX6cGUpLb47sAU+uah6+WBAfNRNlXHShlBV5rHpUmAoG02HfPNQ==", - "dependencies": { - "dashkeys": "^1.0.0" - } + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dashhd/-/dashhd-3.3.3.tgz", + "integrity": "sha512-sbhLV8EtmebnlIdx/d1hcbnxdfka/0rcLx+UO5y44kZdu5tyJ5ftBFbhhIb38vd+T+Xfcwpeo0z+0ZDznRkfaw==", + "license": "SEE LICENSE IN LICENSE" }, "node_modules/dashkeys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dashkeys/-/dashkeys-1.1.0.tgz", - "integrity": "sha512-Xk4G2DCqmEG/VoESseaoFGMUPS/PndnwHOfjH/TsQWtv9ewroAfQf5QPoKPzKmpSsN1AC3+qYmBXxo8SwwfEMw==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/dashkeys/-/dashkeys-1.1.4.tgz", + "integrity": "sha512-y62hg+r8V56gqUfvnyNqCmQ0MvA/wGmRPWfuKFKDCZY02dvWkhCD0zBRVpBGPjfk+T2gWKbkcrHOCrb2RXZ9cw==", + "license": "SEE LICENSE IN LICENSE" }, "node_modules/dashphrase": { "version": "1.4.0", @@ -101,8 +102,7 @@ }, "node_modules/dashsight": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/dashsight/-/dashsight-1.6.1.tgz", - "integrity": "sha512-FCGeFO9NIic+Gzr9DwLTLY19xripHMbd3dHIrt+tXULHPaoBYs2P1GARqKuvYMWR0m+QXOcODs8Czlih6wXNYg==", + "resolved": "git+ssh://git@github.com/dashhive/DashSight.js.git#11c1a740057ba646f89e8da2d85cb35dc67085f3", "dependencies": { "dashtx": "^0.9.0-3" }, @@ -126,22 +126,33 @@ } }, "node_modules/dashtx": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/dashtx/-/dashtx-0.13.2.tgz", - "integrity": "sha512-zBs5lqwfB6gxlY2uJ19CA2swMV2EC4Eyi2UizC+/Dha2Z9qvTt/8yYdpmUYEo9RLl4IOG+ZBYSb9zxvAq07IhA==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/dashtx/-/dashtx-0.16.0.tgz", + "integrity": "sha512-YyLYzq8OPBLAU2DYmQEk0KXqAZkpQcNYXmZEBHJUjeRNEU4q9xDVK1Tqj3AUekQ2E71Ccyn1xp07zdU9XVD7WA==", + "license": "SEE LICENSE IN LICENSE", "bin": { "dashtx-inspect": "bin/inspect.js" } }, "node_modules/dashwallet": { - "version": "0.7.0-1", - "resolved": "https://registry.npmjs.org/dashwallet/-/dashwallet-0.7.0-1.tgz", - "integrity": "sha512-Qg4nwFmGHcverriEfG76cNzQJEak0SuTdOeX9FTrxhfzkI5jMTHOlBbxd4Ot1ME5otp0CMTG83ctgrA5wYegDw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/dashwallet/-/dashwallet-0.7.1.tgz", + "integrity": "sha512-ZaRCMkMSm0tISEncuqv/5XFOZhqPwCvHiI2aGruZyRRDMgtOlaJzewyhSj4No+s2mivlov+ZDevkfpL9CvfReA==", + "license": "SEE LICENSE IN LICENSE", "dependencies": { "dashhd": "^3.3.0", "dashphrase": "^1.4.0", "dashsight": "^1.6.1", - "dashtx": "^0.13.2" + "dashtx": "^0.14.1" + } + }, + "node_modules/dashwallet/node_modules/dashtx": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/dashtx/-/dashtx-0.14.5.tgz", + "integrity": "sha512-PTb509T6iKQXcEfP3blmmZr/gOdRL3NEQUAaXKOAG32MgespbHFtNVLO3cT8LfFDVVJ1HCoaIy5VsblgfV67xg==", + "license": "SEE LICENSE IN LICENSE", + "bin": { + "dashtx-inspect": "bin/inspect.js" } }, "node_modules/dotenv": { diff --git a/package.json b/package.json index 61eb716..e44fb16 100644 --- a/package.json +++ b/package.json @@ -37,20 +37,21 @@ "@dashincubator/base58check": "^1.4.1", "@dashincubator/ripemd160": "^3.0.0", "@dashincubator/secp256k1": "^1.7.1-5", - "@zxing/library": "^0.20.0", + "@zxing/library": "^0.21.2", + "crowdnode": "^1.8.0", "crypticstorage": "^0.0.2", - "dashhd": "^3.3.0", - "dashkeys": "^1.1.0", + "dashhd": "^3.3.3", + "dashkeys": "^1.1.4", "dashphrase": "^1.4.0", - "dashsight": "^1.6.1", - "dashtx": "^0.13.2", - "dashwallet": "^0.7.0-1", + "dashsight": "dashhive/DashSight.js#feat/bulk-txs", + "dashtx": "^0.16.0", + "dashwallet": "^0.7.1", "html5-qrcode": "^2.3.8", "idb": "^8.0.0", "localforage": "^1.10.0", "qrcode-svg": "^1.1.0" }, "devDependencies": { - "@types/node": "^20.11.5" + "@types/node": "^20.14.11" } } diff --git a/public/icons/dash-incubator-circle.png b/public/icons/dash-incubator-circle.png index c0f50e8..c2141b6 100644 Binary files a/public/icons/dash-incubator-circle.png and b/public/icons/dash-incubator-circle.png differ diff --git a/public/icons/dash-incubator-circle.svg b/public/icons/dash-incubator-circle.svg new file mode 100644 index 0000000..a0f5f51 --- /dev/null +++ b/public/icons/dash-incubator-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/dash-incubator-circle_x192.png b/public/icons/dash-incubator-circle_x192.png new file mode 100644 index 0000000..ff51ec7 Binary files /dev/null and b/public/icons/dash-incubator-circle_x192.png differ diff --git a/public/icons/dash-incubator-circle_x512.png b/public/icons/dash-incubator-circle_x512.png new file mode 100644 index 0000000..c9a2182 Binary files /dev/null and b/public/icons/dash-incubator-circle_x512.png differ diff --git a/public/icons/maskable_icon.png b/public/icons/maskable_icon.png new file mode 100644 index 0000000..3aa929b Binary files /dev/null and b/public/icons/maskable_icon.png differ diff --git a/public/icons/maskable_icon_x192.png b/public/icons/maskable_icon_x192.png new file mode 100644 index 0000000..60bdfcf Binary files /dev/null and b/public/icons/maskable_icon_x192.png differ diff --git a/src/components/contacts-list.js b/src/components/contacts-list.js index 307f4eb..16a112d 100644 --- a/src/components/contacts-list.js +++ b/src/components/contacts-list.js @@ -110,7 +110,7 @@ const initialState = { let name = c.info?.name if ( - !name && + !name?.trim() && !user && !out?.xkeyId && out?.address diff --git a/src/components/transactions-list.js b/src/components/transactions-list.js new file mode 100644 index 0000000..07aadae --- /dev/null +++ b/src/components/transactions-list.js @@ -0,0 +1,234 @@ +import { lit as html } from '../helpers/lit.js' +import { + envoy, + restate, + sortTransactionsByTime, + timeago, + getAvatar, +} from '../helpers/utils.js' + +let _handlers = [] + +const initialState = { + id: 'Transactions', + name: 'List', + placement: 'transactions', + rendered: null, + responsive: true, + showUnpaired: false, + delay: 500, + wallet: {}, + transactions: [], + restate, + render( + renderState = {}, + position = 'beforeend', + ) {}, + header: state => html` +
+
Transactions
+
+ `, + list: async state => { + if (state.transactions?.length === 0) { + return html`No Transactions found` + } + + let contact + + return ( + await Promise.all( + state.transactions + .sort(sortTransactionsByTime) + .map(async tx => { + if (state.contacts?.length > 0) { + contact = state.contacts.find( + c => c.alias === tx.alias + ) + } + return await state.item(tx, contact) + }), + ) + ).join('') + }, + content: async state => html` + ${state.header(state)} + +
+ ${await state.list(state)} +
+ `, + item: async (tx, cnt) => { + if ('string' === typeof tx) { + return html` +
+
+

Transaction

+
+
+ ` + } + + let time + let txDate = new Date(tx.time * 1000) + let user = cnt?.alias || cnt?.info?.preferred_username || tx?.alias || '' + let name = cnt?.info?.name + let addr = tx?.vout?.[0]?.scriptPubKey?.addresses?.[0] + + if (!['sent', 'outgoing'].includes(tx?.dir)) { + addr = tx?.vin?.[0]?.addr + } + if (tx.time) { + time = timeago(Date.now() - txDate.getTime()) + } + + if ( + !name && user + ) { + name = `@${user}` + } else if ( + !name && !user + ) { + name = html`${addr.substring(0,3)}...${addr.substring(addr.length - 3)}` + } + + let itemAmount = tx.receivedAmount || tx.valueOut || 0 + + let itemCtrls = html`` + let itemTitle = `Sent on` + let itemDir = `To ${name}` + + if (!['sent', 'outgoing'].includes(tx?.dir)) { + itemTitle = `Received on` + itemDir = `From ${name}` + itemCtrls = html`` + } + + return html` + + ${await getAvatar(cnt)} +
+

${itemDir}

+
${time}
+
+ ${itemCtrls} +
+ ` + }, + footer: async state => html``, + slugs: { + }, + elements: { + }, + events: { + handleClick: state => event => { + event.preventDefault() + event.stopPropagation() + console.log( + 'handle transactions click', + event, + state, + ) + }, + handleTransactionsChange: (newState, oldState) => { + if (newState.transactions !== oldState.transactions) { + newState.render?.({ + transactions: newState.transactions + }) + } + } + }, +} + +let state = envoy( + initialState, + initialState.events.handleTransactionsChange, +) + +export async function setupTransactionsList( + el, setupState = {} +) { + restate(state, setupState) + + state.slugs.section = `${state.name}_${state.id}` + .toLowerCase().replace(' ', '_') + + const section = document.createElement('section') + + section.id = state.slugs.section + section.classList.add(state.placement || '') + section.innerHTML = await state.content(state) + + state.elements.section = section + + const list = section.querySelector('& > div') + + state.elements.list = list + + function addListener( + node, + event, + handler, + capture = false + ) { + _handlers.push({ node, event, handler, capture }) + node.addEventListener(event, handler, capture) + } + + function removeAllListeners( + targets = [state.elements.section], + ) { + _handlers = _handlers + .filter(({ node, event, handler, capture }) => { + if (targets.includes(node)) { + node.removeEventListener(event, handler, capture) + return false + } + return true + }) + } + + function addListeners() { + addListener( + state.elements.section, + 'click', + state.events.handleClick(state), + ) + } + + state.removeAllListeners = removeAllListeners + state.addListeners = addListeners + + async function render( + renderState = {}, + position = 'beforeend', + ) { + await restate(state, renderState) + + state.elements.section.id = state.slugs.section + + state.elements.list.innerHTML = await state.list(state) + + state.removeAllListeners() + state.addListeners() + + if (!state.rendered) { + el.insertAdjacentElement(position, section) + state.rendered = section + } + } + + state.render = render + + return { + element: section, + render, + restate: async newState => await restate(state, newState), + } +} + +export default setupTransactionsList diff --git a/src/helpers/db.js b/src/helpers/db.js index b230644..3314635 100644 --- a/src/helpers/db.js +++ b/src/helpers/db.js @@ -48,6 +48,10 @@ export async function DatabaseSetup() { ...localForageBaseCfg, storeName: 'addresses', }); + var transactions = localforage.createInstance({ + ...localForageBaseCfg, + storeName: 'transactions', + }); return { wallets, @@ -55,6 +59,7 @@ export async function DatabaseSetup() { contacts, accounts, aliases, + transactions, } } diff --git a/src/helpers/utils.js b/src/helpers/utils.js index 79c70df..493808f 100644 --- a/src/helpers/utils.js +++ b/src/helpers/utils.js @@ -489,7 +489,8 @@ export async function restate( } export function filterPairedContacts(contact) { - return !!contact.alias || !!contact.info?.name?.trim() + let outLen = Object.keys(contact.outgoing || {}).length + return outLen > 0 // && !!contact.alias } export function filterUnpairedContacts(contact) { @@ -522,6 +523,19 @@ export function sortContactsByName(a, b) { 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 = {} diff --git a/src/helpers/wallet.js b/src/helpers/wallet.js index e1e7d0b..78de19f 100644 --- a/src/helpers/wallet.js +++ b/src/helpers/wallet.js @@ -12,11 +12,15 @@ import { 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' @@ -24,7 +28,7 @@ import blake from 'blakejs' import { keccak_256 } from '@noble/hashes/sha3' // @ts-ignore -let dashsight = DashSight.create({ +export const dashsight = DashSight.create({ baseUrl: 'https://insight.dash.org', // baseUrl: 'https://dashsight.dashincubator.dev', }); @@ -87,11 +91,11 @@ export async function getFilteredStoreLength(targStore, query = {}) { let storeLen = await targStore.length() let qs = Object.entries(query) - console.log('getFilteredStoreLength qs', { - storeName: targStore?._config?.storeName, - storeLen, - qs, - }) + // console.log('getFilteredStoreLength qs', { + // storeName: targStore?._config?.storeName, + // storeLen, + // qs, + // }) if (storeLen === 0) { return 0 @@ -556,6 +560,22 @@ export async function encryptKeystore( } 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, @@ -563,9 +583,11 @@ export async function generateAddressIterator( addressIndex, usageIndex = DashHd.RECEIVE, ) { - // let xkeyId = await DashHd.toId(xkey); - let key = await xkey.deriveAddress(addressIndex); - let address = await DashHd.toAddr(key.publicKey); + let { address } = await generateAddressIterator( + xkey, + xkeyId, + addressIndex, + ) // console.log( // 'generateAddressIterator', @@ -603,6 +625,30 @@ export async function generateAddressIterator( } } +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, @@ -625,7 +671,7 @@ export async function batchAddressGenerate( for (let addrIdx = addressIndex; addrIdx < batchLimit; addrIdx++) { addresses.push( - await generateAddressIterator( + await generateAndStoreAddressIterator( xkey, xkeyId, wallet.id, @@ -664,7 +710,7 @@ export async function batchAddressUsageGenerate( for (let addrIdx = addressIndex; addrIdx < batchLimit; addrIdx++) { addresses.push( - await generateAddressIterator( + await generateAndStoreAddressIterator( xkeyReceive, xkeyId, wallet.id, @@ -674,7 +720,7 @@ export async function batchAddressUsageGenerate( ) ) addresses.push( - await generateAddressIterator( + await generateAndStoreAddressIterator( xkeyChange, xkeyId, wallet.id, @@ -831,7 +877,7 @@ export async function initWallet( // } export async function updateAddrFunds( - wallet, walletFunds, insightRes, + wallet, insightRes, ) { let updatedAt = Date.now() let { addrStr, ...res } = insightRes @@ -889,12 +935,12 @@ export async function updateAddrFunds( if ($addr.accountIndex >= storeAcctLen) { batchGenAccts(wallet.recoveryPhrase, $addr.accountIndex) .then(() => { - // updateAllFunds(wallet, walletFunds) + // updateAllFunds(wallet) batchGenAcctsAddrs(wallet) .then(accts => { console.log('batchGenAcctsAddrs', { accts }) - updateAllFunds(wallet, walletFunds) + updateAllFunds(wallet) .then(funds => { console.log('updateAllFunds then funds', funds) }) @@ -910,7 +956,7 @@ export async function updateAddrFunds( return { balance: 0 } } -export async function updateAllFunds(wallet, walletFunds) { +export async function updateAllFunds(wallet) { let funds = 0 let addrKeys = await store.addresses.keys() @@ -926,6 +972,11 @@ export async function updateAllFunds(wallet, walletFunds) { ) let balances = await dashsight.getInstantBalances(addrKeys) + // let txs = await dashsight.getAllTxs( + // addrKeys + // ) + + // console.log('getAllTxs', txs) if (balances.length >= 0) { walletFunds.balance = funds @@ -938,7 +989,7 @@ export async function updateAllFunds(wallet, walletFunds) { if (addrIdx > -1) { addrKeys.splice(addrIdx, 1) } - funds += (await updateAddrFunds(wallet, walletFunds, insightRes))?.balance || 0 + funds += (await updateAddrFunds(wallet, insightRes))?.balance || 0 walletFunds.balance = funds } @@ -1062,7 +1113,7 @@ export async function batchGenAcctAddrs( usageIndex = -1, batchSize = 20, ) { - console.log('batchGenAcctAddrs account', account, usageIndex) + // console.log('batchGenAcctAddrs account', account, usageIndex) let filterQuery = { accountIndex: account.accountIndex, @@ -1077,7 +1128,7 @@ export async function batchGenAcctAddrs( filterQuery, ) - console.log('getFilteredStoreLength res', acctAddrsLen) + // console.log('getFilteredStoreLength res', acctAddrsLen) let addrUsageIdx = account.usage?.[usageIndex] || 0 let addrIdx = addrUsageIdx @@ -1270,6 +1321,7 @@ export async function deriveTxWallet( let cachedAddrs = {} let privateKeys = {} let coreUtxos + // let transactions let tmpWallet if (Array.isArray(fundAddrs) && fundAddrs.length > 0) { @@ -1296,6 +1348,9 @@ export async function deriveTxWallet( coreUtxos = await dashsight.getMultiCoreUtxos( Object.keys(privateKeys) ) + // transactions = await dashsight.getAllTxs( + // Object.keys(privateKeys) + // ) } else { tmpWallet = await deriveWalletData( fromWallet.recoveryPhrase, @@ -1315,12 +1370,18 @@ export async function deriveTxWallet( coreUtxos = await dashsight.getCoreUtxos( tmpWallet.address ) + // transactions = await dashsight.getAllTxs( + // [tmpWallet.address] + // ) } + // console.log('getAllTxs', transactions) + return { privateKeys, cachedAddrs, coreUtxos, + // transactions, } } @@ -1686,3 +1747,286 @@ export async function sendTx( 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/index.html b/src/index.html index 1ee863c..9722a8f 100644 --- a/src/index.html +++ b/src/index.html @@ -16,22 +16,23 @@ } + - - - - - - - + + + + + + + - - - + + + - - + + diff --git a/src/main.js b/src/main.js index eae2f7d..3f06a08 100644 --- a/src/main.js +++ b/src/main.js @@ -3,26 +3,22 @@ import { lit as html } from './helpers/lit.js' import { generateWalletData, deriveWalletData, - envoy, - sortContactsByAlias, getStoreData, + loadStoreObject, formDataEntries, - getAddressIndexFromUsage, } from './helpers/utils.js' import { DUFFS, - OIDC_CLAIMS, - // USAGE, } from './helpers/constants.js' import { findInStore, initDashSocket, - // batchAddressGenerate, batchGenAccts, batchGenAcctAddrs, batchGenAcctsAddrs, + batchXkeyAddressGenerate, updateAllFunds, decryptKeystore, getStoredItems, @@ -34,7 +30,12 @@ import { storedData, getUnusedChangeAddress, getAccountWallet, + dashsight, + getAddrsTransactions, + getTransactionsByContactAlias, + getTxs, } from './helpers/wallet.js' + import { localForageBaseCfg, importFromJson, @@ -42,10 +43,19 @@ import { saveJsonToFile, } from './helpers/db.js' +import { + appState, + appTools, + appDialogs, + userInfo, + walletFunds, +} from './state/index.js' + import setupNav from './components/nav.js' import setupMainFooter from './components/main-footer.js' import setupSendRequestBtns from './components/send-request-btns.js' import setupContactsList from './components/contacts-list.js' +import setupTransactionsList from './components/transactions-list.js' import setupSVGSprite from './components/svg-sprite.js' import setupDialog from './components/dialog.js' @@ -74,75 +84,6 @@ let accounts let wallets let wallet -let appState = envoy( - { - phrase: null, - encryptionPassword: null, - selectedWallet: '', - selectedAlias: '', - aliasInfo: {}, - contacts: [], - sentTransactions: {}, - account: {}, - }, -) -let appTools = envoy( - { - storedData: {}, - }, -) -let userInfo = envoy( - { - ...OIDC_CLAIMS, - }, - async (state, oldState, prop) => { - if (state[prop] !== oldState[prop]) { - let decryptedAlias = await appTools.storedData.decryptItem( - store.aliases, - appState.selectedAlias, - ) - appTools.storedData.encryptItem( - store.aliases, - appState.selectedAlias, - { - ...decryptedAlias, - updatedAt: (new Date()).toISOString(), - info: { - ...decryptedAlias.info, - [prop]: state[prop], - }, - }, - false, - ) - } - } -) - -// rigs -let appDialogs = envoy( - { - onboard: {}, - phraseBackup: {}, - phraseGenerate: {}, - phraseImport: {}, - walletEncrypt: {}, - walletDecrypt: {}, - addContact: {}, - editContact: {}, - editProfile: {}, - scanContact: {}, - sendOrReceive: {}, - sendConfirm: {}, - requestQr: {}, - }, -) - -let walletFunds = envoy( - { - balance: 0 - }, -) - // element let bodyNav let dashBalance @@ -368,6 +309,24 @@ let contactsList = await setupContactsList( }, } ) +let transactionsList = await setupTransactionsList(mainAppGrid, { + events: { + handleClick: state => async event => { + let txArticle = event.target?.closest('a, article') + + if (!txArticle) { + event.preventDefault() + event.stopPropagation() + } + + console.log( + 'setupTransactionsList click event', + event.target, + txArticle, + ) + }, + }, +}) async function getUserInfo() { let ks = wallets?.[appState.selectedWallet]?.keystore @@ -513,7 +472,7 @@ async function main() { }) appDialogs.walletEncrypt = await walletEncryptRig({ - setupDialog, appDialogs, appState, mainApp, + setupDialog, appDialogs, appState, appTools, mainApp, wallet, wallets, bodyNav, dashBalance, }) @@ -549,8 +508,9 @@ async function main() { }) appDialogs.addContact = await addContactRig({ - setupDialog, updateAllFunds, - appDialogs, appState, appTools, store, walletFunds, + setupDialog, updateAllFunds, batchXkeyAddressGenerate, + getAddrsTransactions, + appDialogs, appState, appTools, store, dashsight, mainApp, wallet, userInfo, contactsList, }) @@ -647,7 +607,7 @@ async function main() { ...walletFunds._listeners, (state, oldState) => { if (state.balance !== oldState.balance) { - dashBalance?.restate({ + appTools.balance?.restate({ wallet, walletFunds: { balance: state.balance @@ -762,7 +722,7 @@ async function main() { .then(accts => { console.log('batchGenAcctsAddrs', { accts }) - updateAllFunds(wallet, walletFunds) + updateAllFunds(wallet) .then(funds => { console.log('updateAllFunds then funds', funds) }) @@ -788,25 +748,23 @@ async function main() { await getUserInfo() - // console.log('appTools.storedData', appTools.storedData) + // contactsList.render({ + // contacts: appState.contacts, + // userInfo, + // }) - getStoreData( - store.contacts, - res => { - if (res) { - appState.contacts = res + appState.transactions = await loadStoreObject( + store.transactions, + ) - contactsList.render({ - contacts: res, - userInfo, - }) + // console.log('appState.transactions', appState.transactions) - console.log('contacts', res) - } - }, + appState.contacts = await getStoreData( + store.contacts, + getTransactionsByContactAlias(appState), res => async v => { res.push(await appTools.storedData?.decryptData?.(v) || v) - } + }, ) await contactsList.render({ @@ -815,6 +773,24 @@ async function main() { }) sendRequestBtn.render() + mainApp.insertAdjacentHTML('afterbegin', html` +
+ `) + + import('./components/balance.js') + .then(async ({ setupBalance }) => { + appTools.balance = await setupBalance( + mainApp.querySelector('& > header'), + { + wallet, + } + ) + appTools.balance.render({ + wallet, + walletFunds, + }) + }) + integrationsSection.insertAdjacentHTML('beforeend', html`
@@ -831,16 +807,27 @@ async function main() {
`) - mainAppGrid.insertAdjacentHTML('beforeend', html` -
-
-
Transactions
-
-
- Coming soon -
-
- `) + + let txs = await getTxs( + appState, + Object.values(appState.transactions || {}) + ) + + await transactionsList.render({ + userInfo, + contacts: appState.contacts, + transactions: Object.values(txs.byTx), + }) + + txs = await getTxs(appState) + + console.log('main getTxs', txs) + + transactionsList.render({ + userInfo, + contacts: appState.contacts, + transactions: Object.values(txs.byTx), + }) document.addEventListener('click', async event => { let { @@ -961,24 +948,6 @@ async function main() { } }) - mainApp.insertAdjacentHTML('afterbegin', html` -
- `) - - import('./components/balance.js') - .then(async ({ setupBalance }) => { - dashBalance = await setupBalance( - mainApp.querySelector('& > header'), - { - wallet, - } - ) - dashBalance.render({ - wallet, - walletFunds, - }) - }) - let storedAddrs = (await store.addresses.keys()) || [] initDashSocket({ @@ -1000,8 +969,8 @@ async function main() { appState?.sentTransactions?.[data.txid] ) - setTimeout(() => - updateAllFunds(wallet, walletFunds) + setTimeout(() => { + updateAllFunds(wallet) .then(funds => { console.log('updateAllFunds then funds', funds) }) @@ -1012,7 +981,18 @@ async function main() { title: 'Update funds', msg: err, }) - }), + }) + + getTxs(appState).then(txs => { + console.log('socket main getTxs', txs) + + transactionsList.render({ + userInfo, + contacts: appState.contacts, + transactions: Object.values(txs.byTx), + }) + }) + }, 1000 ) } @@ -1125,8 +1105,8 @@ async function main() { appDialogs.requestQr.close() } - setTimeout(() => - updateAllFunds(wallet, walletFunds) + setTimeout(() => { + updateAllFunds(wallet) .then(funds => { console.log('updateAllFunds then funds', funds) }) @@ -1137,11 +1117,24 @@ async function main() { title: 'Update funds', msg: err, }) - }), + }) + + getTxs(appState).then(txs => { + console.log('socket main getTxs', txs) + + transactionsList.render({ + userInfo, + contacts: appState.contacts, + transactions: Object.values(txs.byTx), + }) + }) + }, 1000 ) } + let txs = appState?.sentTransactions + let txsStartLen = Object.keys(txs).length Object.keys(txUpdates).forEach( txid => { @@ -1151,7 +1144,9 @@ async function main() { } ) - appState.sentTransactions = txs + if (txsStartLen > Object.keys(txs).length) { + appState.sentTransactions = txs + } }, }) } diff --git a/src/manifest.webmanifest b/src/manifest.webmanifest index f54874b..0b03053 100644 --- a/src/manifest.webmanifest +++ b/src/manifest.webmanifest @@ -20,9 +20,20 @@ "theme_color": "#111921", "icons": [ { - "src": "/public/icons/dash-incubator-circle.png", - "sizes": "176x176", + "src": "/public/icons/dash-incubator-circle_x512.png", + "sizes": "512x512", "type": "image/png" + }, + { + "src": "/public/icons/dash-incubator-circle_x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/public/icons/maskable_icon_x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" } ], "protocol_handlers": [ diff --git a/src/rigs/add-contact.js b/src/rigs/add-contact.js index baf9162..e583b1f 100644 --- a/src/rigs/add-contact.js +++ b/src/rigs/add-contact.js @@ -5,7 +5,7 @@ import { formDataEntries, setClipboard, openBlobSVG, - sortContactsByAlias, + // sortContactsByAlias, // sortContactsByName, parseAddressField, generateContactPairingURI, @@ -27,8 +27,8 @@ export let addContactRig = (async function (globals) { let { setupDialog, appDialogs, appState, appTools, store, - mainApp, wallet, userInfo, contactsList, - updateAllFunds, walletFunds, + mainApp, wallet, userInfo, contactsList, getAddrsTransactions, + updateAllFunds, batchXkeyAddressGenerate, dashsight, } = globals; let aliases = {} @@ -117,6 +117,7 @@ export let addContactRig = (async function (globals) { let outgoing = {} let existingContacts + let contactWallet if (!xkey && address) { existingContacts = appState.contacts.filter( @@ -132,14 +133,15 @@ export let addContactRig = (async function (globals) { } if (xkey) { + contactWallet = await deriveWalletData( + xkey, + ) let { xkeyId, addressKeyId, addressIndex, address: addr, - } = await deriveWalletData( - xkey, - ) + } = contactWallet existingContacts = appState.contacts.filter( c => c.outgoing?.[xkeyId] @@ -164,6 +166,8 @@ export let addContactRig = (async function (globals) { // ) } + let newContact + if (existingContacts?.length > 0) { console.warn( `You've already paired with this contact`, @@ -175,53 +179,103 @@ export let addContactRig = (async function (globals) { } } ) - } - let newContact = await appTools.storedData.encryptItem( - store.contacts, - state.wallet.xkeyId, - { - ...state.contact, - updatedAt: (new Date()).toISOString(), - info: { - ...OIDC_CLAIMS, - ...(state.contact.info || {}), - ...info, + // newContact = existingContacts[0] + + let pairings = existingContacts.map(c => `@${c.alias}`) + if (pairings.length > 1) { + let lastPairing = pairings.pop() + pairings = `${pairings.join(', ')} & ${lastPairing}` + } else { + pairings = pairings[0] + } + + // TODO: maybe prompt to show original pairing info + // in the scenario where your contact + // lost their contacts list + target.contactAddr.setCustomValidity( + `You've already paired with this contact (@${preferred_username}) as ${pairings}`, + ) + target.reportValidity() + return; + } else { + if (Object.keys(outgoing).length > 0 && contactWallet) { + let xkeyAddrs = await batchXkeyAddressGenerate( + contactWallet, + contactWallet.addressIndex, + ) + let contactAddrs = {} + let addresses = xkeyAddrs.addresses.map(g => { + contactAddrs[g.address] = { + alias: preferredAlias, + xkeyId: contactWallet.xkeyId, + } + return g.address + }) + + let txs = await getAddrsTransactions({ + appState, + addrs: addresses, + contactAddrs, + }) + + // outgoing[contactWallet.xkeyId] = { + // ...(outgoing[contactWallet.xkeyId] || {}), + // addressIndex: xkeyAddrs.finalAddressIndex, + // } + + // console.log('xkeyAddrs', {addresses, txs}) + } + + newContact = await appTools.storedData.encryptItem( + store.contacts, + state.wallet.xkeyId, + { + ...state.contact, + updatedAt: (new Date()).toISOString(), + info: { + ...OIDC_CLAIMS, + ...(state.contact.info || {}), + ...info, + }, + outgoing, + alias: preferredAlias, + uri: value, }, - outgoing, - alias: preferredAlias, - uri: value, - }, - false, - ) + false, + ) - getStoreData( - store.contacts, - res => { - if (res) { - appState.contacts = res + getStoreData( + store.contacts, + res => { + if (res) { + appState.contacts = res - return contactsList.restate({ - contacts: res, - userInfo, - }) + return contactsList.restate({ + contacts: res, + userInfo, + }) + } + }, + res => async v => { + res.push(await appTools.storedData.decryptData(v)) } - }, - res => async v => { - res.push(await appTools.storedData.decryptData(v)) - } - ) + ) - state.contact = newContact + state.contact = newContact - if (xkeyOrAddr) { - target.contactAddr.value = xkeyOrAddr - } - if (name) { - target.contactName.value = name - } - if (preferred_username) { - target.contactAlias.value = preferredAlias + if (value) { + target.contactURI.value = value + } + if (xkeyOrAddr) { + target.contactAddr.value = xkeyOrAddr + } + if (name) { + target.contactName.value = name + } + if (preferred_username) { + target.contactAlias.value = preferredAlias + } } return @@ -303,6 +357,12 @@ export let addContactRig = (async function (globals) { +

Paste a Dash Address, Xprv/Xpub, or Link

@@ -559,7 +619,7 @@ export let addContactRig = (async function (globals) { sub: parsedAddr?.sub || '', name: event.target.contactName.value, }, - uri: event.target.contactAddr.value, + uri: event.target.contactURI.value, alias: currentAlias || event.target.contactAlias.value, }, false, @@ -574,7 +634,7 @@ export let addContactRig = (async function (globals) { if (res) { appState.contacts = res - updateAllFunds(state.wallet, walletFunds) + updateAllFunds(state.wallet) .then(funds => { // console.log('updateAllFunds then funds', funds) }) diff --git a/src/rigs/edit-contact.js b/src/rigs/edit-contact.js index 3cacd2d..f80b933 100644 --- a/src/rigs/edit-contact.js +++ b/src/rigs/edit-contact.js @@ -147,13 +147,21 @@ export let editContactRig = (async function (globals) { `, + getAlias: state => { + let alias = state.contact?.alias || state.contact?.info?.preferred_username + if (!alias) { + return '' + } + + return html`@${alias}` + }, content: async state => html`
${state.header(state)}
${await getAvatar(state.contact)} -

@${state.contact?.alias || state.contact?.info?.preferred_username}

+

${state.getAlias(state)}