Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wallet",
"version": "1.23.0",
"version": "1.23.1",
"description": "Web wallet for managing $EDGE",
"private": true,
"license": "GPL",
Expand Down
35 changes: 29 additions & 6 deletions src/components/TransactionsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ import TransactionsTableItem from '@/components/TransactionsTableItem.vue'
import { mapState } from 'vuex'

const txsRefreshInterval = 5 * 1000
const txCache = {}

export default {
name: 'TransactionsTable',
data: function () {
return {
loaded: false,
loading: false,
loading: true,
metadata: null,
transactions: [],
iTransactions: null
Expand All @@ -90,6 +91,12 @@ export default {
}
},
mounted() {
const cached = txCache[this.address]
if (cached) {
this.transactions = cached.transactions
this.loaded = true
this.loading = false
}
this.updateTransactions()
// initiate polling
this.iTransactions = setInterval(() => {
Expand All @@ -102,21 +109,27 @@ export default {
methods: {
async updateTransactions() {
this.loading = true
const addr = this.address
if (!addr) return
// the sort query sent to index needs to include "-created", but this is hidden from user in browser url
const sortQuery = this.$route.query.sort ? `${this.$route.query.sort},-timestamp` : '-timestamp'
const transactions = await index.tx.transactions(
import.meta.env.VITE_INDEX_API_URL,
this.address,
addr,
{
limit: this.limit,
page: this.page,
sort: sortQuery
}
)
this.transactions = transactions.results
if (this.receiveMetadata) this.receiveMetadata(transactions.metadata)
this.loaded = true
this.loading = false
txCache[addr] = { transactions: transactions.results }
// Only update display if address hasn't changed during fetch
if (this.address === addr) {
this.transactions = transactions.results
if (this.receiveMetadata) this.receiveMetadata(transactions.metadata)
this.loaded = true
this.loading = false
}
},
updateSorting(newSortQuery) {
const query = { ...this.$route.query, sort: newSortQuery }
Expand All @@ -125,6 +138,16 @@ export default {
}
},
watch: {
address(newAddr) {
const cached = txCache[newAddr]
if (cached) {
this.transactions = cached.transactions
} else {
this.transactions = []
this.loaded = false
}
this.updateTransactions()
},
page() {
this.updateTransactions()
},
Expand Down
12 changes: 10 additions & 2 deletions src/components/index/RestoreModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
<template v-slot:body>
<div class="pt-15">
<form>
<div v-if="isAdditionalWallet" class="flex items-start leading-8 text-gray mb-14">
<span class="flex-shrink-0 inline-block mt-8 mr-12 text-white icon w-27">
<ShieldExclamationIcon/>
</span>
<p>Enter a private key below to import a wallet. The wallet will be encrypted with your existing password.</p>
</div>
<div class="form-group" :class="{'form-group__error': v$.privateKey.$error || importError}">
<label for="key">ENTER private key</label>
<div class="relative input-wrap">
Expand Down Expand Up @@ -74,7 +80,8 @@ import useVuelidate from '@vuelidate/core'
import { mapState } from 'vuex'
import {
KeyIcon,
LockOpenIcon
LockOpenIcon,
ShieldExclamationIcon
} from '@heroicons/vue/outline'
import { helpers, sameAs } from '@vuelidate/validators'

Expand All @@ -85,7 +92,8 @@ export default {
components: {
KeyIcon,
LockOpenIcon,
Modal
Modal,
ShieldExclamationIcon
},
props: {
afterRestore: Function,
Expand Down
83 changes: 66 additions & 17 deletions src/components/index/UnlockModal.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
<template>
<Modal :close="close" :visible="visible">
<template v-slot:header>
<h2>Unlock wallet</h2>
<h2>{{ migrationFailed ? 'Migration Failed' : 'Unlock wallet' }}</h2>
</template>

<template v-slot:body>
<div class="pt-15">
<div v-if="migrationFailed" class="pt-15">
<div class="flex items-start leading-8 text-gray mb-14">
<p>Wallet migration to the new format failed. Please copy your private key below and reset your wallet to continue.</p>
</div>
<div class="form-group">
<label>wallet address</label>
<span class="break-all">{{ address }}</span>
</div>
<div class="form-group mb-25">
<label>PRIVATE KEY</label>
<span class="font-mono break-all text-sm2">{{ legacyPrivateKey }}</span>
</div>
</div>

<div v-else class="pt-15">
<form>
<div v-if="walletVersion < 2" class="form-group">
<label>wallet address</label>
Expand Down Expand Up @@ -34,7 +48,15 @@
</template>

<template v-slot:footer>
<div class="grid grid-cols-1 gap-24 px-24 pt-48 border-gray-700 border-solid md:grid-cols-2 border-t-default border-opacity-30 pb-54">
<div v-if="migrationFailed" class="px-24 pt-48 border-gray-700 border-solid border-t-default border-opacity-30 pb-54">
<button
class="w-full border-red-600 button button--outline-success hover:border-red-600 hover:bg-red-600"
@click="switchToForgetModal"
>
Reset wallet
</button>
</div>
<div v-else class="grid grid-cols-1 gap-24 px-24 pt-48 border-gray-700 border-solid md:grid-cols-2 border-t-default border-opacity-30 pb-54">
<button
class="w-full border-red-600 button button--outline-success hover:border-red-600 hover:bg-red-600"
@click="switchToForgetModal"
Expand All @@ -48,7 +70,6 @@
</template>

<script>
import * as xe from '@edge/xe-utils'
import * as storage from '../../utils/storage'
import * as validation from '../../utils/validation'
import { LockOpenIcon } from '@heroicons/vue/outline'
Expand All @@ -65,7 +86,9 @@ export default {
data() {
return {
password: '',
passwordError: ''
passwordError: '',
migrationFailed: false,
legacyPrivateKey: ''
}
},
validations() {
Expand Down Expand Up @@ -106,22 +129,48 @@ export default {
if (!await this.v$.$validate()) return
if (!await this.checkPassword()) return

// Migrate vault from older versions if needed
if (await storage.needsMigration()) {
await storage.migrateToV2(this.password)
// Attempt migration from older versions
try {
if (await storage.needsMigration()) {
await storage.migrateToV2(this.password)
}
}
catch (err) {
// Migration failed — old data preserved (write-verify-delete pattern)
// Show private key so user can export and reset
console.error('Migration failed:', err)
this.legacyPrivateKey = await storage.getLegacyPrivateKey(this.password)
if (this.legacyPrivateKey) {
this.migrationFailed = true
} else {
this.passwordError = 'Migration failed and wallet data could not be read.'
}
return
}

const publicKey = await storage.getPublicKey(this.password)
const highestVersion = storage.getHighestWalletVersion()
const address = xe.wallet.deriveAddress(publicKey)
this.$store.commit('setAddress', address)
this.$store.commit('setVersion', highestVersion)
this.$store.commit('unlock')
try {
// Use actual stored version (reflects migration success)
this.$store.commit('setVersion', await storage.getWalletVersion())
this.$store.commit('unlock')

await this.$store.dispatch('loadWallets', this.password)
this.$store.dispatch('refresh')
// loadWallets derives addresses from vault (v2) or state (legacy)
await this.$store.dispatch('loadWallets', this.password)

this.afterUnlock()
// Verify wallet loaded before navigating away
if (!this.$store.state.address) {
this.$store.commit('lock')
this.passwordError = 'Failed to load wallet data. Please try again.'
return
}

this.$store.dispatch('refresh')
this.afterUnlock()
}
catch (err) {
// Roll back unlock state so user stays on unlock screen
this.$store.commit('lock')
this.passwordError = err.message || 'An error occurred while unlocking.'
}
},
unlockOnEnter(event) {
if (event.charCode !== 13) return
Expand Down
4 changes: 2 additions & 2 deletions src/components/wallet/WalletIndicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default {
methods: {
truncateAddress(addr) {
if (!addr || addr.length < 11) return addr || ''
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
return `${addr.slice(0, 7)}...${addr.slice(-4)}`
},
toggleDropdown() {
this.showDropdown = !this.showDropdown
Expand Down Expand Up @@ -189,7 +189,7 @@ export default {
}

.wallet-indicator__address {
@apply font-mono text-sm;
@apply text-base3;
}

.wallet-indicator__chevron {
Expand Down
7 changes: 4 additions & 3 deletions src/components/wallet/WalletListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

<span class="wallet-item__balance">
<svg
v-if="loading"
v-if="loading && balance == null"
class="wallet-item__spinner"
viewBox="0 0 24 24"
fill="none"
Expand Down Expand Up @@ -126,7 +126,7 @@ export default {
truncatedAddress() {
const addr = this.wallet.address || ''
if (addr.length < 11) return addr
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
return `${addr.slice(0, 7)}...${addr.slice(-4)}`
},
formattedBalance() {
if (this.balance === undefined || this.balance === null) return '-.--'
Expand Down Expand Up @@ -202,7 +202,7 @@ export default {
}

.wallet-item__address {
@apply font-mono text-sm text-gray-400 leading-tight;
@apply text-sm2 text-gray-400 leading-tight;
}

.wallet-item--active .wallet-item__address {
Expand All @@ -216,6 +216,7 @@ export default {

.wallet-item__balance {
@apply ml-auto text-right text-sm flex-shrink-0 pl-16 text-gray-400;
min-width: 90px;
}

.wallet-item--active .wallet-item__balance {
Expand Down
27 changes: 21 additions & 6 deletions src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ const init = async () => {
}
})()

// If wallet-version exists (> 0), a wallet exists
// empty() clears all keys including wallet-version
const hasWallet = version > 0
// Detect wallet presence: v1/v2 have wallet-version set,
// v0 has no wallet-version but may have keys (p1) in IndexedDB
const hasWallet = version > 0 || await storage.needsMigration()

return createStore({
state: {
Expand All @@ -45,6 +45,7 @@ const init = async () => {
nextNonce: 0,

usdBalance: undefined,
usdPerXE: null,

// TODO investigate whether we can set these in app mixin instead
config: {
Expand Down Expand Up @@ -98,6 +99,9 @@ const init = async () => {
setUSDBalance(state, usdBalance) {
state.usdBalance = usdBalance
},
setUsdPerXE(state, rate) {
state.usdPerXE = rate
},
setVersion(state, version) {
state.version = version
},
Expand Down Expand Up @@ -168,6 +172,10 @@ const init = async () => {

commit('setBalance', info.balance)
commit('setNextNonce', info.nonce)
// Keep dropdown cache in sync
if (state.activeWalletId) {
commit('setWalletBalance', { walletId: state.activeWalletId, balance: info.balance })
}
dispatch('refreshTokenValue')
} catch (err) {
// Ignore abort errors - these are expected during wallet switching
Expand All @@ -185,6 +193,7 @@ const init = async () => {
},
async refreshTokenValue({ commit, state }) {
const tokenValue = await fetchTokenValue()
commit('setUsdPerXE', tokenValue.usdPerXE)
commit('setUSDBalance', tokenValue.usdPerXE * (state.balance / 1e6))
},
async switchWallet({ commit, dispatch, state }, walletId) {
Expand All @@ -209,10 +218,16 @@ const init = async () => {
commit('setActiveWalletId', walletId)
commit('setAddress', wallet.address)

// Reset balance/nonce (will be fetched fresh)
commit('setBalance', 0)
// Use cached dropdown balance if available, otherwise reset to 0
const cachedBalance = state.walletBalances[walletId]
commit('setBalance', cachedBalance != null ? cachedBalance : 0)
commit('setNextNonce', 0)
commit('setUSDBalance', undefined)
// Compute USD from cached rate and balance
if (cachedBalance != null && state.usdPerXE != null) {
commit('setUSDBalance', state.usdPerXE * (cachedBalance / 1e6))
} else {
commit('setUSDBalance', undefined)
}

// Persist active wallet selection (plain storage, no password needed)
await storage.setActiveWalletId(walletId)
Expand Down
Loading