Skip to content

Commit dd44e7e

Browse files
Merge pull request #284 from edge/develop
v1.23.1
2 parents 7ac49f7 + 5d0d0be commit dd44e7e

10 files changed

Lines changed: 256 additions & 53 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wallet",
3-
"version": "1.23.0",
3+
"version": "1.23.1",
44
"description": "Web wallet for managing $EDGE",
55
"private": true,
66
"license": "GPL",

src/components/TransactionsTable.vue

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@ import TransactionsTableItem from '@/components/TransactionsTableItem.vue'
6161
import { mapState } from 'vuex'
6262
6363
const txsRefreshInterval = 5 * 1000
64+
const txCache = {}
6465
6566
export default {
6667
name: 'TransactionsTable',
6768
data: function () {
6869
return {
6970
loaded: false,
70-
loading: false,
71+
loading: true,
7172
metadata: null,
7273
transactions: [],
7374
iTransactions: null
@@ -90,6 +91,12 @@ export default {
9091
}
9192
},
9293
mounted() {
94+
const cached = txCache[this.address]
95+
if (cached) {
96+
this.transactions = cached.transactions
97+
this.loaded = true
98+
this.loading = false
99+
}
93100
this.updateTransactions()
94101
// initiate polling
95102
this.iTransactions = setInterval(() => {
@@ -102,21 +109,27 @@ export default {
102109
methods: {
103110
async updateTransactions() {
104111
this.loading = true
112+
const addr = this.address
113+
if (!addr) return
105114
// the sort query sent to index needs to include "-created", but this is hidden from user in browser url
106115
const sortQuery = this.$route.query.sort ? `${this.$route.query.sort},-timestamp` : '-timestamp'
107116
const transactions = await index.tx.transactions(
108117
import.meta.env.VITE_INDEX_API_URL,
109-
this.address,
118+
addr,
110119
{
111120
limit: this.limit,
112121
page: this.page,
113122
sort: sortQuery
114123
}
115124
)
116-
this.transactions = transactions.results
117-
if (this.receiveMetadata) this.receiveMetadata(transactions.metadata)
118-
this.loaded = true
119-
this.loading = false
125+
txCache[addr] = { transactions: transactions.results }
126+
// Only update display if address hasn't changed during fetch
127+
if (this.address === addr) {
128+
this.transactions = transactions.results
129+
if (this.receiveMetadata) this.receiveMetadata(transactions.metadata)
130+
this.loaded = true
131+
this.loading = false
132+
}
120133
},
121134
updateSorting(newSortQuery) {
122135
const query = { ...this.$route.query, sort: newSortQuery }
@@ -125,6 +138,16 @@ export default {
125138
}
126139
},
127140
watch: {
141+
address(newAddr) {
142+
const cached = txCache[newAddr]
143+
if (cached) {
144+
this.transactions = cached.transactions
145+
} else {
146+
this.transactions = []
147+
this.loaded = false
148+
}
149+
this.updateTransactions()
150+
},
128151
page() {
129152
this.updateTransactions()
130153
},

src/components/index/RestoreModal.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
<template v-slot:body>
88
<div class="pt-15">
99
<form>
10+
<div v-if="isAdditionalWallet" class="flex items-start leading-8 text-gray mb-14">
11+
<span class="flex-shrink-0 inline-block mt-8 mr-12 text-white icon w-27">
12+
<ShieldExclamationIcon/>
13+
</span>
14+
<p>Enter a private key below to import a wallet. The wallet will be encrypted with your existing password.</p>
15+
</div>
1016
<div class="form-group" :class="{'form-group__error': v$.privateKey.$error || importError}">
1117
<label for="key">ENTER private key</label>
1218
<div class="relative input-wrap">
@@ -74,7 +80,8 @@ import useVuelidate from '@vuelidate/core'
7480
import { mapState } from 'vuex'
7581
import {
7682
KeyIcon,
77-
LockOpenIcon
83+
LockOpenIcon,
84+
ShieldExclamationIcon
7885
} from '@heroicons/vue/outline'
7986
import { helpers, sameAs } from '@vuelidate/validators'
8087
@@ -85,7 +92,8 @@ export default {
8592
components: {
8693
KeyIcon,
8794
LockOpenIcon,
88-
Modal
95+
Modal,
96+
ShieldExclamationIcon
8997
},
9098
props: {
9199
afterRestore: Function,

src/components/index/UnlockModal.vue

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
<template>
22
<Modal :close="close" :visible="visible">
33
<template v-slot:header>
4-
<h2>Unlock wallet</h2>
4+
<h2>{{ migrationFailed ? 'Migration Failed' : 'Unlock wallet' }}</h2>
55
</template>
66

77
<template v-slot:body>
8-
<div class="pt-15">
8+
<div v-if="migrationFailed" class="pt-15">
9+
<div class="flex items-start leading-8 text-gray mb-14">
10+
<p>Wallet migration to the new format failed. Please copy your private key below and reset your wallet to continue.</p>
11+
</div>
12+
<div class="form-group">
13+
<label>wallet address</label>
14+
<span class="break-all">{{ address }}</span>
15+
</div>
16+
<div class="form-group mb-25">
17+
<label>PRIVATE KEY</label>
18+
<span class="font-mono break-all text-sm2">{{ legacyPrivateKey }}</span>
19+
</div>
20+
</div>
21+
22+
<div v-else class="pt-15">
923
<form>
1024
<div v-if="walletVersion < 2" class="form-group">
1125
<label>wallet address</label>
@@ -34,7 +48,15 @@
3448
</template>
3549

3650
<template v-slot:footer>
37-
<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">
51+
<div v-if="migrationFailed" class="px-24 pt-48 border-gray-700 border-solid border-t-default border-opacity-30 pb-54">
52+
<button
53+
class="w-full border-red-600 button button--outline-success hover:border-red-600 hover:bg-red-600"
54+
@click="switchToForgetModal"
55+
>
56+
Reset wallet
57+
</button>
58+
</div>
59+
<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">
3860
<button
3961
class="w-full border-red-600 button button--outline-success hover:border-red-600 hover:bg-red-600"
4062
@click="switchToForgetModal"
@@ -48,7 +70,6 @@
4870
</template>
4971

5072
<script>
51-
import * as xe from '@edge/xe-utils'
5273
import * as storage from '../../utils/storage'
5374
import * as validation from '../../utils/validation'
5475
import { LockOpenIcon } from '@heroicons/vue/outline'
@@ -65,7 +86,9 @@ export default {
6586
data() {
6687
return {
6788
password: '',
68-
passwordError: ''
89+
passwordError: '',
90+
migrationFailed: false,
91+
legacyPrivateKey: ''
6992
}
7093
},
7194
validations() {
@@ -106,22 +129,48 @@ export default {
106129
if (!await this.v$.$validate()) return
107130
if (!await this.checkPassword()) return
108131
109-
// Migrate vault from older versions if needed
110-
if (await storage.needsMigration()) {
111-
await storage.migrateToV2(this.password)
132+
// Attempt migration from older versions
133+
try {
134+
if (await storage.needsMigration()) {
135+
await storage.migrateToV2(this.password)
136+
}
137+
}
138+
catch (err) {
139+
// Migration failed — old data preserved (write-verify-delete pattern)
140+
// Show private key so user can export and reset
141+
console.error('Migration failed:', err)
142+
this.legacyPrivateKey = await storage.getLegacyPrivateKey(this.password)
143+
if (this.legacyPrivateKey) {
144+
this.migrationFailed = true
145+
} else {
146+
this.passwordError = 'Migration failed and wallet data could not be read.'
147+
}
148+
return
112149
}
113150
114-
const publicKey = await storage.getPublicKey(this.password)
115-
const highestVersion = storage.getHighestWalletVersion()
116-
const address = xe.wallet.deriveAddress(publicKey)
117-
this.$store.commit('setAddress', address)
118-
this.$store.commit('setVersion', highestVersion)
119-
this.$store.commit('unlock')
151+
try {
152+
// Use actual stored version (reflects migration success)
153+
this.$store.commit('setVersion', await storage.getWalletVersion())
154+
this.$store.commit('unlock')
120155
121-
await this.$store.dispatch('loadWallets', this.password)
122-
this.$store.dispatch('refresh')
156+
// loadWallets derives addresses from vault (v2) or state (legacy)
157+
await this.$store.dispatch('loadWallets', this.password)
123158
124-
this.afterUnlock()
159+
// Verify wallet loaded before navigating away
160+
if (!this.$store.state.address) {
161+
this.$store.commit('lock')
162+
this.passwordError = 'Failed to load wallet data. Please try again.'
163+
return
164+
}
165+
166+
this.$store.dispatch('refresh')
167+
this.afterUnlock()
168+
}
169+
catch (err) {
170+
// Roll back unlock state so user stays on unlock screen
171+
this.$store.commit('lock')
172+
this.passwordError = err.message || 'An error occurred while unlocking.'
173+
}
125174
},
126175
unlockOnEnter(event) {
127176
if (event.charCode !== 13) return

src/components/wallet/WalletIndicator.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export default {
9898
methods: {
9999
truncateAddress(addr) {
100100
if (!addr || addr.length < 11) return addr || ''
101-
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
101+
return `${addr.slice(0, 7)}...${addr.slice(-4)}`
102102
},
103103
toggleDropdown() {
104104
this.showDropdown = !this.showDropdown
@@ -189,7 +189,7 @@ export default {
189189
}
190190
191191
.wallet-indicator__address {
192-
@apply font-mono text-sm;
192+
@apply text-base3;
193193
}
194194
195195
.wallet-indicator__chevron {

src/components/wallet/WalletListItem.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
<span class="wallet-item__balance">
2929
<svg
30-
v-if="loading"
30+
v-if="loading && balance == null"
3131
class="wallet-item__spinner"
3232
viewBox="0 0 24 24"
3333
fill="none"
@@ -126,7 +126,7 @@ export default {
126126
truncatedAddress() {
127127
const addr = this.wallet.address || ''
128128
if (addr.length < 11) return addr
129-
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
129+
return `${addr.slice(0, 7)}...${addr.slice(-4)}`
130130
},
131131
formattedBalance() {
132132
if (this.balance === undefined || this.balance === null) return '-.--'
@@ -202,7 +202,7 @@ export default {
202202
}
203203
204204
.wallet-item__address {
205-
@apply font-mono text-sm text-gray-400 leading-tight;
205+
@apply text-sm2 text-gray-400 leading-tight;
206206
}
207207
208208
.wallet-item--active .wallet-item__address {
@@ -216,6 +216,7 @@ export default {
216216
217217
.wallet-item__balance {
218218
@apply ml-auto text-right text-sm flex-shrink-0 pl-16 text-gray-400;
219+
min-width: 90px;
219220
}
220221
221222
.wallet-item--active .wallet-item__balance {

src/store.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ const init = async () => {
3030
}
3131
})()
3232

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

3737
return createStore({
3838
state: {
@@ -45,6 +45,7 @@ const init = async () => {
4545
nextNonce: 0,
4646

4747
usdBalance: undefined,
48+
usdPerXE: null,
4849

4950
// TODO investigate whether we can set these in app mixin instead
5051
config: {
@@ -98,6 +99,9 @@ const init = async () => {
9899
setUSDBalance(state, usdBalance) {
99100
state.usdBalance = usdBalance
100101
},
102+
setUsdPerXE(state, rate) {
103+
state.usdPerXE = rate
104+
},
101105
setVersion(state, version) {
102106
state.version = version
103107
},
@@ -168,6 +172,10 @@ const init = async () => {
168172

169173
commit('setBalance', info.balance)
170174
commit('setNextNonce', info.nonce)
175+
// Keep dropdown cache in sync
176+
if (state.activeWalletId) {
177+
commit('setWalletBalance', { walletId: state.activeWalletId, balance: info.balance })
178+
}
171179
dispatch('refreshTokenValue')
172180
} catch (err) {
173181
// Ignore abort errors - these are expected during wallet switching
@@ -185,6 +193,7 @@ const init = async () => {
185193
},
186194
async refreshTokenValue({ commit, state }) {
187195
const tokenValue = await fetchTokenValue()
196+
commit('setUsdPerXE', tokenValue.usdPerXE)
188197
commit('setUSDBalance', tokenValue.usdPerXE * (state.balance / 1e6))
189198
},
190199
async switchWallet({ commit, dispatch, state }, walletId) {
@@ -209,10 +218,16 @@ const init = async () => {
209218
commit('setActiveWalletId', walletId)
210219
commit('setAddress', wallet.address)
211220

212-
// Reset balance/nonce (will be fetched fresh)
213-
commit('setBalance', 0)
221+
// Use cached dropdown balance if available, otherwise reset to 0
222+
const cachedBalance = state.walletBalances[walletId]
223+
commit('setBalance', cachedBalance != null ? cachedBalance : 0)
214224
commit('setNextNonce', 0)
215-
commit('setUSDBalance', undefined)
225+
// Compute USD from cached rate and balance
226+
if (cachedBalance != null && state.usdPerXE != null) {
227+
commit('setUSDBalance', state.usdPerXE * (cachedBalance / 1e6))
228+
} else {
229+
commit('setUSDBalance', undefined)
230+
}
216231

217232
// Persist active wallet selection (plain storage, no password needed)
218233
await storage.setActiveWalletId(walletId)

0 commit comments

Comments
 (0)