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`
+
+ `,
+ 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`
-
- `)
+
+ 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`