mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 11:18:24 +00:00
New import strategy merging with existing wallet fi any
This commit is contained in:
@@ -21,7 +21,7 @@ jest.mock('../src/services/logService', () => ({
|
||||
}))
|
||||
|
||||
import {mnemonicToSeedSync} from '@scure/bip39'
|
||||
import {types, getSnapshot, applySnapshot} from 'mobx-state-tree'
|
||||
import {types, getSnapshot} from 'mobx-state-tree'
|
||||
import {encodeBackup, decodeBackup} from '../src/services/backup/backupCodec'
|
||||
import {MintsStoreModel, MintsStoreSnapshot} from '../src/models/MintsStore'
|
||||
import {ProofsStoreModel} from '../src/models/ProofsStore'
|
||||
@@ -107,10 +107,18 @@ const importBackup = (root: Instance, encoded: string, keysByMintUrl: Record<str
|
||||
for (const keys of keysByMintUrl[mint.mintUrl] ?? []) mint.keys.push(keys)
|
||||
}
|
||||
|
||||
// Mints first: a merged mint keeps the url this wallet reaches it at, and the
|
||||
// proofs about to be imported have to be repointed at it.
|
||||
const urlByBackupUrl = root.mintsStore.mergeFromBackup(backup.mintsStore as MintsStoreSnapshot)
|
||||
|
||||
for (const proof of backup.proofsStore.proofs) {
|
||||
const resolvedUrl = urlByBackupUrl.get(proof.mintUrl)
|
||||
if (resolvedUrl && resolvedUrl !== proof.mintUrl) proof.mintUrl = resolvedUrl
|
||||
}
|
||||
|
||||
root.proofsStore.importProofs(backup.proofsStore.proofs)
|
||||
root.proofsStore.importPendingByMintSecrets(backup.proofsStore.pendingByMintSecrets)
|
||||
root.mintsStore.restoreFromBackup(backup.mintsStore as MintsStoreSnapshot)
|
||||
applySnapshot(root.contactsStore, backup.contactsStore)
|
||||
root.contactsStore.mergeFromBackup(backup.contactsStore)
|
||||
|
||||
// The counters ride in the backup's raw JSON (they are volatile in the model),
|
||||
// seed the SQLite authority, and are read back from it.
|
||||
@@ -231,8 +239,8 @@ describe('importing over an existing wallet', () => {
|
||||
// Onboarding adds the Minibits mint before the user can ever reach the import
|
||||
// screen, so the wallet ALWAYS has a mint here — and the backup's copy of that
|
||||
// same mint carries a different local id.
|
||||
const onboardedWallet = () => {
|
||||
const root = TestRoot.create({mintsStore: {mints: [mintSnapshot({id: 'onboard1'})]}})
|
||||
const onboardedWallet = (mints: any[] = [mintSnapshot({id: 'onboard1'})]) => {
|
||||
const root = TestRoot.create({mintsStore: {mints}})
|
||||
root.mintsStore.persistAllMints()
|
||||
root.mintsStore.observeMints()
|
||||
return root
|
||||
@@ -247,12 +255,14 @@ describe('importing over an existing wallet', () => {
|
||||
|
||||
importBackup(target, backup)
|
||||
|
||||
expect(Database.getMints().map(m => m.id)).toEqual(['backup01'])
|
||||
// Merged into the local entry, which keeps its own id: transactions reference
|
||||
// mintId, and this device's history must keep resolving.
|
||||
expect(Database.getMints().map(m => m.id)).toEqual(['onboard1'])
|
||||
})
|
||||
|
||||
// The duplicate was not even the worst of it: mint_keysets is keyed by keysetId
|
||||
// and its upsert moves mintId, so the replaced mint's row was left with no
|
||||
// keysets and no keys — and rehydrated on the next launch as an unusable husk.
|
||||
// and its upsert moves mintId, so a second entry for one mint left the first with
|
||||
// no keysets and no keys — an unusable husk after the next launch.
|
||||
test('and the next launch sees exactly one, intact', () => {
|
||||
const source = TestRoot.create({mintsStore: {mints: [mintSnapshot({id: 'backup01'})]}})
|
||||
|
||||
@@ -267,45 +277,86 @@ describe('importing over an existing wallet', () => {
|
||||
expect(restarted.mintsStore.mints[0].keys.map((k: any) => k.id)).toEqual([KEYSET_1])
|
||||
})
|
||||
|
||||
// The backup is the wallet being restored: a mint it does not carry is gone,
|
||||
// exactly as it was when the whole state lived in one MMKV snapshot.
|
||||
test('a mint the backup does not carry is removed from the database too', () => {
|
||||
const source = TestRoot.create({mintsStore: {mints: [mintSnapshot({id: 'backup01'})]}})
|
||||
|
||||
const target = TestRoot.create({
|
||||
mintsStore: {
|
||||
mints: [
|
||||
mintSnapshot({id: 'onboard1'}),
|
||||
mintSnapshot({
|
||||
id: 'straymint',
|
||||
mintUrl: OTHER_MINT_URL,
|
||||
hostname: 'other.test',
|
||||
keysets: [{id: KEYSET_2, unit: 'sat', active: true, input_fee_ppk: 0}],
|
||||
keys: [{id: KEYSET_2, unit: 'sat', keys: {'1': '02cc'}}],
|
||||
proofsCounters: [{keyset: KEYSET_2, unit: 'sat'}],
|
||||
}),
|
||||
],
|
||||
},
|
||||
// A mint can be reached at more than one url, so the url cannot be what says
|
||||
// "same mint" — the keysets do. Matching on the url would file this mint twice.
|
||||
test('the same mint at a different url is merged, not duplicated', () => {
|
||||
const source = TestRoot.create({
|
||||
mintsStore: {mints: [mintSnapshot({id: 'backup01', mintUrl: OTHER_MINT_URL, hostname: 'other.test'})]},
|
||||
proofsStore: {proofs: {b1: {...proofSnapshot('b1'), mintUrl: OTHER_MINT_URL}}} as any,
|
||||
})
|
||||
target.mintsStore.persistAllMints()
|
||||
target.mintsStore.observeMints()
|
||||
expect(Database.getMints()).toHaveLength(2)
|
||||
|
||||
const target = onboardedWallet()
|
||||
importBackup(target, exportBackup(source))
|
||||
|
||||
expect(target.mintsStore.mints).toHaveLength(1)
|
||||
// The local url wins — it is the one this device is reaching the mint on.
|
||||
expect(target.mintsStore.mints[0].mintUrl).toBe(MINT_URL)
|
||||
// ...so the imported proof has to arrive under it, or it belongs to no mint.
|
||||
expect(target.proofsStore.getBySecret('b1')?.mintUrl).toBe(MINT_URL)
|
||||
expect(target.proofsStore.findOrphanedProofs()).toEqual([])
|
||||
})
|
||||
|
||||
// The reason for merging rather than replacing: this mint is the only place the
|
||||
// wallet's own ecash can be spent, and the backup has never heard of it.
|
||||
test('a mint the backup does not carry is left alone, with its ecash spendable', () => {
|
||||
const source = TestRoot.create({
|
||||
mintsStore: {mints: [mintSnapshot({id: 'backup01'})]},
|
||||
proofsStore: {proofs: {b1: proofSnapshot('b1')}} as any, // 2 sat at MINT_URL
|
||||
})
|
||||
|
||||
const target = onboardedWallet([
|
||||
mintSnapshot({id: 'onboard1'}),
|
||||
mintSnapshot({
|
||||
id: 'straymint',
|
||||
mintUrl: OTHER_MINT_URL,
|
||||
hostname: 'other.test',
|
||||
keysets: [{id: KEYSET_2, unit: 'sat', active: true, input_fee_ppk: 0}],
|
||||
keys: [{id: KEYSET_2, unit: 'sat', keys: {'1': '02cc'}}],
|
||||
proofsCounters: [{keyset: KEYSET_2, unit: 'sat'}],
|
||||
}),
|
||||
])
|
||||
target.proofsStore.importProofs([
|
||||
{...proofSnapshot('own1'), id: KEYSET_2, mintUrl: OTHER_MINT_URL, amount: 42},
|
||||
] as any)
|
||||
|
||||
importBackup(target, exportBackup(source))
|
||||
|
||||
expect(Database.getMints().map(m => m.mintUrl)).toEqual([MINT_URL])
|
||||
expect(Database.getMints().map(m => m.mintUrl).sort()).toEqual([MINT_URL, OTHER_MINT_URL].sort())
|
||||
expect(target.proofsStore.findOrphanedProofs()).toEqual([])
|
||||
expect(target.proofsStore.getUnitBalance('sat').unitBalance).toBe(44)
|
||||
})
|
||||
|
||||
test('contacts are merged too, keeping the ones added on this device', () => {
|
||||
const source = TestRoot.create({
|
||||
contactsStore: {contacts: [{pubkey: 'aa', npub: 'npub-aa', name: 'from-backup'}]} as any,
|
||||
})
|
||||
|
||||
const target = onboardedWallet()
|
||||
target.contactsStore.mergeFromBackup({
|
||||
contacts: [{pubkey: 'bb', npub: 'npub-bb', name: 'added-here'}],
|
||||
} as any)
|
||||
|
||||
importBackup(target, exportBackup(source))
|
||||
|
||||
expect(target.contactsStore.contacts.map((c: any) => c.name).sort()).toEqual([
|
||||
'added-here',
|
||||
'from-backup',
|
||||
])
|
||||
})
|
||||
|
||||
// Counters are keyed by keysetId and deliberately outlive their mint, so
|
||||
// re-adding one recovers its real derivation index instead of restarting at 0.
|
||||
test('but that mint keeps its derivation counters', () => {
|
||||
test('a keyset the wallet already advanced keeps the higher counter', () => {
|
||||
const source = TestRoot.create({mintsStore: {mints: [mintSnapshot({id: 'backup01'})]}})
|
||||
source.mintsStore.mints[0].proofsCounters[0].increaseProofsCounter(5)
|
||||
|
||||
const target = onboardedWallet()
|
||||
Database.seedCounters([{keysetId: KEYSET_2, unit: 'sat', counter: 99}])
|
||||
Database.seedCounters([{keysetId: KEYSET_1, unit: 'sat', counter: 99}])
|
||||
|
||||
importBackup(target, exportBackup(source))
|
||||
|
||||
expect(Database.getCounters().find(c => c.keysetId === KEYSET_2)?.counter).toBe(99)
|
||||
// Monotonic: a backup taken before this device advanced can never rewind it,
|
||||
// which is what stops a blinded secret being derived at an index twice.
|
||||
expect(Database.getCounters().find(c => c.keysetId === KEYSET_1)?.counter).toBe(99)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,7 +71,54 @@ import { MINIBITS_NIP05_DOMAIN } from '@env'
|
||||
|
||||
return contactInstance
|
||||
},
|
||||
saveNote (pubkey: string, note: string) {
|
||||
/**
|
||||
* Fold a backup's contacts into the wallet's own.
|
||||
*
|
||||
* Same rule as the mints: the LOCAL entry wins, the backup fills gaps.
|
||||
* An import used to applySnapshot over this store, which silently threw
|
||||
* away every contact the user had added on this device — and unlike
|
||||
* ecash, a contact list has no other copy to recover it from.
|
||||
*
|
||||
* Not addContact: that stamps `createdAt` with now and resets `type`,
|
||||
* which would rewrite history the backup is carrying faithfully. It does
|
||||
* contribute the two checks worth keeping — a pubkey already present, and
|
||||
* a nip05 already claimed by a different pubkey, since the wallet treats
|
||||
* a nip05 as an address and two contacts sharing one would be ambiguous.
|
||||
*/
|
||||
mergeFromBackup(snapshot: ContactsStoreSnapshot) {
|
||||
let added = 0
|
||||
|
||||
for (const contact of snapshot?.contacts ?? []) {
|
||||
if (!contact?.pubkey || self.alreadyExists(contact.pubkey)) continue
|
||||
|
||||
if (contact.nip05 && self.nip05AlreadyExists(contact.nip05)) {
|
||||
log.warn('[mergeFromBackup]', 'Skipped a backup contact whose nip05 is already taken', {
|
||||
nip05: contact.nip05,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
self.contacts.push(ContactModel.create(contact))
|
||||
added++
|
||||
}
|
||||
|
||||
// Wallet-level fields the backup also carries: adopted only where this
|
||||
// wallet has nothing, so restoring onto a fresh install gets them and
|
||||
// merging into a live wallet leaves its own alone.
|
||||
if (!self.publicPubkey && snapshot?.publicPubkey) {
|
||||
self.publicPubkey = snapshot.publicPubkey
|
||||
}
|
||||
|
||||
if (!self.lastPendingReceivedCheck && snapshot?.lastPendingReceivedCheck) {
|
||||
self.lastPendingReceivedCheck = snapshot.lastPendingReceivedCheck
|
||||
}
|
||||
|
||||
log.info('[mergeFromBackup]', 'Contacts merged from a backup', {
|
||||
added,
|
||||
total: self.contacts.length,
|
||||
})
|
||||
},
|
||||
saveNote (pubkey: string, note: string) {
|
||||
const contactInstance = self.findByPubkey(pubkey)
|
||||
if (contactInstance) {
|
||||
contactInstance.setNoteToSelf(note)
|
||||
|
||||
+103
-47
@@ -3,7 +3,6 @@ import {
|
||||
SnapshotOut,
|
||||
types,
|
||||
destroy,
|
||||
applySnapshot,
|
||||
isStateTreeNode,
|
||||
detach,
|
||||
flow,
|
||||
@@ -48,8 +47,8 @@ export type MintsByUnit = {
|
||||
// class this whole effort has been about — each mint gets one onSnapshot observer
|
||||
// that persists its row whenever anything in its subtree changes. It cannot be
|
||||
// forgotten. Note the one thing it does NOT cover: nodes that arrive already-formed
|
||||
// rather than by mutation never fire an observer, which is why restoring a backup
|
||||
// goes through restoreFromBackup instead of a bare applySnapshot.
|
||||
// rather than by mutation never fire an observer, which is why a backup is folded in
|
||||
// by mergeFromBackup, which writes them through explicitly.
|
||||
//
|
||||
// Derivation counters are unaffected: `counter` is volatile, so it never appears in
|
||||
// a snapshot and a bump never fires these.
|
||||
@@ -238,9 +237,9 @@ export const MintsStoreModel = types
|
||||
/**
|
||||
* Write every mint through, unconditionally.
|
||||
*
|
||||
* For when mints arrive already-formed rather than by mutation — i.e.
|
||||
* ImportBackup's applySnapshot. Observers only fire on CHANGE, so freshly
|
||||
* applied nodes would otherwise never reach SQLite.
|
||||
* For when mints arrive already-formed rather than by mutation — i.e. the
|
||||
* ones mergeFromBackup builds. Observers only fire on CHANGE, so a mint that
|
||||
* was complete before it entered the tree would otherwise never reach SQLite.
|
||||
*/
|
||||
persistAllMints() {
|
||||
for (const mint of self.mints) self.persistMint(getSnapshot(mint as any))
|
||||
@@ -249,61 +248,118 @@ export const MintsStoreModel = types
|
||||
.actions(self => ({
|
||||
|
||||
/**
|
||||
* Replace the wallet's mints with the ones from a backup — in BOTH engines.
|
||||
* Fold a backup's mints into the wallet's own. Nothing is replaced and
|
||||
* nothing is removed — the counterpart to `backupSnapshot`.
|
||||
*
|
||||
* The counterpart to `backupSnapshot`, and it exists for the same reason:
|
||||
* ImportBackup used to do this inline, as applySnapshot + persistAllMints,
|
||||
* and that covers only half of it. applySnapshot REPLACES the mints in the
|
||||
* model, but a mint the wallet already had keeps its SQLite row — under its
|
||||
* own id, which the backup's copy of the same mint does not share. The
|
||||
* import screen is reached from an onboarded wallet, which always has the
|
||||
* Minibits mint (WelcomeScreen adds it), so this was not an edge case: the
|
||||
* next launch hydrated BOTH rows and the user saw the same mint twice.
|
||||
* IDENTITY IS THE KEYSETS, not the url. One mint can be reached at several
|
||||
* urls, and matching on the url would file the same mint twice; that is not
|
||||
* merely untidy, it breaks an invariant the storage layer rests on.
|
||||
* mint_keysets is keyed by keysetId with a single mintId column, and
|
||||
* hydrateCountersFromDatabase resolves a counter's owner as "whichever mint
|
||||
* holds that keyset" — so two entries claiming one keyset means the second
|
||||
* upsert steals the row (mintId = excluded.mintId) and the first rehydrates
|
||||
* as a husk with no keysets and no keys. Exactly one mint entry per keyset
|
||||
* id, wallet-wide, is the rule this enforces.
|
||||
*
|
||||
* Worse than a duplicate: mint_keysets is keyed by keysetId and its upsert
|
||||
* reassigns mintId, so the imported mint takes the keysets with it and the
|
||||
* stale row rehydrates as a husk with no keysets and no keys.
|
||||
* The url is the fallback signal, for a mint that has rotated every keyset
|
||||
* since the backup was taken. Two different mints can never share a url, so
|
||||
* a url match is safe once the keysets have failed to match.
|
||||
*
|
||||
* So the removals are the point. Under MMKV this came for free — the
|
||||
* snapshot WAS the state, and applying one dropped whatever it omitted.
|
||||
* With mints mastered in SQLite, dropping them has to be said out loud.
|
||||
* On a match the LOCAL entry wins: its id (transactions reference it), its
|
||||
* url (the one this device is reachable on — changing it is a deliberate act
|
||||
* in mint settings), its colour and name. The backup contributes keysets,
|
||||
* keys, and mint info the wallet does not have.
|
||||
*
|
||||
* Observers are disposed first (they point at nodes applySnapshot is about
|
||||
* to destroy) and re-attached at the end, once the array has settled.
|
||||
* @returns backup url → the url that mint now lives at, so the caller can
|
||||
* repoint the proofs it is about to import. `proofs.mintUrl` is a
|
||||
* denormalized copy of the locator, and a proof whose url matches no mint
|
||||
* is spendable by nothing.
|
||||
*/
|
||||
restoreFromBackup(snapshot: MintsStoreSnapshot) {
|
||||
const previousMintIds = self.mints.map(m => m.id as string)
|
||||
mergeFromBackup(snapshot: MintsStoreSnapshot): Map<string, string> {
|
||||
const urlByBackupUrl = new Map<string, string>()
|
||||
let added = 0
|
||||
|
||||
for (const mintId of [...self.mintObservers.keys()]) self.unobserveMint(mintId)
|
||||
for (const backupMint of snapshot?.mints ?? []) {
|
||||
if (!backupMint?.mintUrl) continue
|
||||
|
||||
applySnapshot(self, snapshot as any)
|
||||
const backupKeysetIds = (backupMint.keysets ?? []).map((k: any) => k.id)
|
||||
|
||||
const restoredMintIds = new Set(self.mints.map(m => m.id as string))
|
||||
let mint =
|
||||
self.mints.find(m => m.keysetIds.some((id: string) => backupKeysetIds.includes(id))) ??
|
||||
self.findByUrl(backupMint.mintUrl)
|
||||
|
||||
// Rows for mints the backup does not carry. Their mint_counters rows
|
||||
// stay behind, as they do on any mint removal, so re-adding a mint
|
||||
// recovers its real derivation counter rather than restarting at 0.
|
||||
for (const mintId of previousMintIds) {
|
||||
if (restoredMintIds.has(mintId)) continue
|
||||
try {
|
||||
Database.removeMintById(mintId)
|
||||
} catch (e: any) {
|
||||
log.error('[restoreFromBackup]', 'Could not remove a replaced mint', {
|
||||
error: e?.message,
|
||||
mintId,
|
||||
})
|
||||
const isNew = !mint
|
||||
|
||||
if (!mint) {
|
||||
// Built outside the tree and pushed once complete, as addMint does:
|
||||
// keysets, keys and units all arrive through initKeyset/initKeys
|
||||
// below, so a mint restored from a backup is assembled exactly like
|
||||
// one added by hand — collision check and counter shells included.
|
||||
mint = MintModel.create({
|
||||
...(backupMint as any),
|
||||
keysets: [],
|
||||
keys: [],
|
||||
units: [],
|
||||
proofsCounters: [],
|
||||
createdAt: backupMint.createdAt ? new Date(backupMint.createdAt) : undefined,
|
||||
} as any)
|
||||
added++
|
||||
}
|
||||
|
||||
// Per keyset, not per mint: a keyset the wallet cannot take (an unknown
|
||||
// unit, or an id colliding with another mint's) must not cost the user
|
||||
// the rest of the mint.
|
||||
for (const keyset of backupMint.keysets ?? []) {
|
||||
try {
|
||||
mint.initKeyset(keyset as any, self.allKeysetIds)
|
||||
} catch (e: any) {
|
||||
log.warn('[mergeFromBackup]', 'Skipped a keyset from the backup', {
|
||||
mintUrl: backupMint.mintUrl,
|
||||
keysetId: (keyset as any)?.id,
|
||||
error: e?.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const keys of backupMint.keys ?? []) {
|
||||
try {
|
||||
mint.initKeys(keys as any)
|
||||
} catch (e: any) {
|
||||
log.warn('[mergeFromBackup]', 'Skipped a keyset\'s keys from the backup', {
|
||||
mintUrl: backupMint.mintUrl,
|
||||
keysetId: (keys as any)?.id,
|
||||
error: e?.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Only where the wallet has none: capabilities are read off mintInfo,
|
||||
// and a backup's copy is better than nothing until the next refresh.
|
||||
if (!mint.mintInfo && backupMint.mintInfo) {
|
||||
mint.setMintInfo!(backupMint.mintInfo as any)
|
||||
}
|
||||
|
||||
if (isNew) self.mints.push(mint)
|
||||
|
||||
urlByBackupUrl.set(backupMint.mintUrl, mint.mintUrl)
|
||||
}
|
||||
|
||||
// Nodes that arrive already-formed never fire an observer, so the write
|
||||
// through has to be explicit.
|
||||
self.persistAllMints()
|
||||
self.observeMints()
|
||||
for (const blockedUrl of snapshot?.blockedMintUrls ?? []) {
|
||||
if (!self.blockedMintUrls.includes(blockedUrl)) self.blockedMintUrls.push(blockedUrl)
|
||||
}
|
||||
|
||||
log.info('[restoreFromBackup]', 'Mints restored from a backup', {
|
||||
restored: self.mints.length,
|
||||
removed: previousMintIds.filter(id => !restoredMintIds.has(id)).length,
|
||||
// Mints that arrive already-formed never fire an observer, so the write
|
||||
// through is explicit; observers are (re)attached once the array settles.
|
||||
self.observeMints()
|
||||
self.persistAllMints()
|
||||
|
||||
log.info('[mergeFromBackup]', 'Mints merged from a backup', {
|
||||
added,
|
||||
merged: urlByBackupUrl.size - added,
|
||||
total: self.mints.length,
|
||||
})
|
||||
|
||||
return urlByBackupUrl
|
||||
},
|
||||
|
||||
hydrateCountersFromDatabase() {
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useStores } from '../models'
|
||||
import {MnemonicInput} from './Recovery/MnemonicInput'
|
||||
import { MINIBITS_MINT_URL } from '@env'
|
||||
import { delay } from '../utils/utils'
|
||||
import { applySnapshot} from 'mobx-state-tree'
|
||||
import { verticalScale } from '@gocodingnow/rn-size-matters'
|
||||
import { translate } from '../i18n'
|
||||
import { MintsStoreSnapshot } from '../models/MintsStore'
|
||||
@@ -212,17 +211,39 @@ export const ImportBackupScreen = observer(function ImportBackupScreen({ route }
|
||||
}
|
||||
}
|
||||
|
||||
// applySnapshot(proofsStore, walletSnapshot.proofsStore)
|
||||
// Mints FIRST, because the proofs have to follow them. The backup is folded
|
||||
// into whatever this wallet already holds rather than replacing it — a mint
|
||||
// is recognised by its keysets, so the same mint reached at a different url
|
||||
// is merged rather than duplicated, and a mint only this device has keeps
|
||||
// its ecash spendable. See MintsStore.mergeFromBackup.
|
||||
const urlByBackupUrl = mintsStore.mergeFromBackup(walletSnapshot.mintsStore)
|
||||
|
||||
// A merged mint keeps the url THIS wallet reaches it at, so a proof arriving
|
||||
// under the backup's url has to be repointed: `proofs.mintUrl` is a
|
||||
// denormalized copy of the locator, and a proof whose url matches no mint
|
||||
// belongs to no mint — it counts toward the balance and cannot be spent.
|
||||
// Cheap to do here, while these are still plain objects from the JSON; once
|
||||
// they are rows, moving them means a transaction (see mintsRepo.updateMintUrl).
|
||||
for (const proof of walletSnapshot.proofsStore.proofs) {
|
||||
const resolvedUrl = urlByBackupUrl.get(proof.mintUrl)
|
||||
|
||||
if (resolvedUrl && resolvedUrl !== proof.mintUrl) {
|
||||
log.trace('[importWallet] Repointing an imported proof', {
|
||||
from: proof.mintUrl,
|
||||
to: resolvedUrl,
|
||||
})
|
||||
proof.mintUrl = resolvedUrl
|
||||
}
|
||||
}
|
||||
|
||||
proofsStore.importProofs(walletSnapshot.proofsStore.proofs)
|
||||
proofsStore.importPendingByMintSecrets(walletSnapshot.proofsStore.pendingByMintSecrets)
|
||||
// Mints are mastered in SQLite, so restoring them is more than an
|
||||
// applySnapshot — the mints being replaced have rows of their own that have
|
||||
// to go. See MintsStore.restoreFromBackup.
|
||||
mintsStore.restoreFromBackup(walletSnapshot.mintsStore)
|
||||
applySnapshot(contactsStore, walletSnapshot.contactsStore)
|
||||
// Merged, not applied: an applySnapshot here used to discard every contact
|
||||
// added on this device, and a contact list has no second copy anywhere.
|
||||
contactsStore.mergeFromBackup(walletSnapshot.contactsStore)
|
||||
|
||||
// The backup carries real derivation counters in its raw MST snapshot.
|
||||
// `counter` is VOLATILE in the model (mastered in SQLite), so the restore
|
||||
// `counter` is VOLATILE in the model (mastered in SQLite), so the merge
|
||||
// above does NOT load it — read the values straight from
|
||||
// the backup snapshot, seed SQLite (monotonic, never lowers), then
|
||||
// hydrate the in-memory cache from the authority. Seeding 0 from the
|
||||
|
||||
Reference in New Issue
Block a user