mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
feat: serialize trusted mint transactions
This commit is contained in:
@@ -0,0 +1,287 @@
|
|||||||
|
// Owner-bound trusted-mint transactions under deterministic Web Locks.
|
||||||
|
// The fake parks every request until the test releases it in FIFO order.
|
||||||
|
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
||||||
|
|
||||||
|
import {linkingPubKeyHex, saveLinkingKey} from './keys'
|
||||||
|
import {
|
||||||
|
addTrustedMint,
|
||||||
|
cacheTrustedMintNodeInfo,
|
||||||
|
clearTrustedMints,
|
||||||
|
confirmTrustedMintRekey,
|
||||||
|
lockTrustedMint,
|
||||||
|
mergeTrustedMints,
|
||||||
|
onTrustedMintsChange,
|
||||||
|
readTrustedMints,
|
||||||
|
removeTrustedMint,
|
||||||
|
type TrustedMint,
|
||||||
|
} from './trustedMints'
|
||||||
|
import {stubLocalStorage} from './test-utils'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'sattle_trusted_mints'
|
||||||
|
const LINKING_KEY_A = new Uint8Array(32).fill(7)
|
||||||
|
const OWNER_A = linkingPubKeyHex(LINKING_KEY_A)
|
||||||
|
const OWNER_B = linkingPubKeyHex(new Uint8Array(32).fill(9))
|
||||||
|
const KEY_A = '02' + 'aa'.repeat(32)
|
||||||
|
const KEY_B = '03' + 'bb'.repeat(32)
|
||||||
|
const KEY_C = '02' + 'cc'.repeat(32)
|
||||||
|
const SERVER = 'mint.example'
|
||||||
|
|
||||||
|
type LockRequest = {
|
||||||
|
readonly name: string
|
||||||
|
readonly callback: () => unknown
|
||||||
|
readonly resolve: (value: unknown) => void
|
||||||
|
readonly reject: (reason: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeferredLocks {
|
||||||
|
readonly requests: LockRequest[] = []
|
||||||
|
held = false
|
||||||
|
|
||||||
|
readonly request = (name: string, callback: () => unknown): Promise<unknown> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
this.requests.push({name, callback, resolve, reject})
|
||||||
|
})
|
||||||
|
|
||||||
|
async releaseNext(): Promise<void> {
|
||||||
|
const request = this.requests.shift()
|
||||||
|
if (!request) throw new Error('Expected a queued lock request.')
|
||||||
|
this.held = true
|
||||||
|
try {
|
||||||
|
request.resolve(await request.callback())
|
||||||
|
} catch (error) {
|
||||||
|
request.reject(error instanceof Error ? error : new Error(String(error)))
|
||||||
|
} finally {
|
||||||
|
this.held = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mint = (overrides: Partial<TrustedMint> = {}): TrustedMint => ({
|
||||||
|
server: SERVER,
|
||||||
|
mintPubkey: KEY_A,
|
||||||
|
addedAt: 123,
|
||||||
|
locked: false,
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = (ownerId: string, mints: TrustedMint[]): void => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({version: 1, ownerId, mints}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const installLocks = (): DeferredLocks => {
|
||||||
|
const locks = new DeferredLocks()
|
||||||
|
vi.stubGlobal('navigator', {locks})
|
||||||
|
return locks
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForRequests = async (locks: DeferredLocks, count: number): Promise<void> => {
|
||||||
|
await vi.waitFor(() => expect(locks.requests).toHaveLength(count))
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
stubLocalStorage()
|
||||||
|
await saveLinkingKey(LINKING_KEY_A)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('serialized owner-bound mutations', () => {
|
||||||
|
it('reads current storage instead of retaining a stale snapshot', () => {
|
||||||
|
store(OWNER_A, [mint({nodeAlias: 'first'})])
|
||||||
|
expect(readTrustedMints(OWNER_A)[0]?.nodeAlias).toBe('first')
|
||||||
|
|
||||||
|
store(OWNER_A, [mint({nodeAlias: 'external update'})])
|
||||||
|
|
||||||
|
expect(readTrustedMints(OWNER_A)[0]?.nodeAlias).toBe('external update')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves two queued additions by reading fresh state in FIFO order', async () => {
|
||||||
|
const locks = installLocks()
|
||||||
|
const writes = vi.spyOn(localStorage, 'setItem')
|
||||||
|
|
||||||
|
const first = addTrustedMint('one.example', KEY_A, {ownerId: OWNER_A})
|
||||||
|
const second = addTrustedMint('two.example', KEY_B, {ownerId: OWNER_A})
|
||||||
|
await waitForRequests(locks, 2)
|
||||||
|
|
||||||
|
await locks.releaseNext()
|
||||||
|
await locks.releaseNext()
|
||||||
|
|
||||||
|
await expect(first).resolves.toBe('added')
|
||||||
|
await expect(second).resolves.toBe('added')
|
||||||
|
expect(readTrustedMints(OWNER_A).map((entry) => entry.server)).toEqual([
|
||||||
|
'one.example',
|
||||||
|
'two.example',
|
||||||
|
])
|
||||||
|
expect(writes).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not let metadata overwrite a concurrently staged rekey', async () => {
|
||||||
|
store(OWNER_A, [mint()])
|
||||||
|
const locks = installLocks()
|
||||||
|
|
||||||
|
const rekey = lockTrustedMint(SERVER, KEY_B, OWNER_A)
|
||||||
|
const metadata = cacheTrustedMintNodeInfo(SERVER, {nodeAlias: 'Fresh alias'}, OWNER_A)
|
||||||
|
await waitForRequests(locks, 2)
|
||||||
|
|
||||||
|
await locks.releaseNext()
|
||||||
|
await locks.releaseNext()
|
||||||
|
await Promise.all([rekey, metadata])
|
||||||
|
|
||||||
|
expect(readTrustedMints(OWNER_A)[0]).toMatchObject({
|
||||||
|
mintPubkey: KEY_A,
|
||||||
|
pendingMintPubkey: KEY_B,
|
||||||
|
nodeAlias: 'Fresh alias',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps a new staged key after queued explicit confirmation', async () => {
|
||||||
|
store(OWNER_A, [mint({pendingMintPubkey: KEY_B})])
|
||||||
|
const locks = installLocks()
|
||||||
|
|
||||||
|
const confirm = confirmTrustedMintRekey(SERVER, OWNER_A)
|
||||||
|
const stage = lockTrustedMint(SERVER, KEY_C, OWNER_A)
|
||||||
|
await waitForRequests(locks, 2)
|
||||||
|
|
||||||
|
await locks.releaseNext()
|
||||||
|
await locks.releaseNext()
|
||||||
|
await Promise.all([confirm, stage])
|
||||||
|
|
||||||
|
expect(readTrustedMints(OWNER_A)[0]).toMatchObject({
|
||||||
|
mintPubkey: KEY_B,
|
||||||
|
pendingMintPubkey: KEY_C,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lets a live lock corroborate a queued backup merge', async () => {
|
||||||
|
const locks = installLocks()
|
||||||
|
const incoming = mint({locked: true, pendingMintPubkey: KEY_B})
|
||||||
|
|
||||||
|
const merge = mergeTrustedMints([incoming], OWNER_A)
|
||||||
|
const live = lockTrustedMint(SERVER, KEY_A, OWNER_A)
|
||||||
|
await waitForRequests(locks, 2)
|
||||||
|
|
||||||
|
await locks.releaseNext()
|
||||||
|
await locks.releaseNext()
|
||||||
|
await Promise.all([merge, live])
|
||||||
|
|
||||||
|
expect(readTrustedMints(OWNER_A)[0]).toMatchObject({
|
||||||
|
mintPubkey: KEY_A,
|
||||||
|
locked: true,
|
||||||
|
})
|
||||||
|
expect(readTrustedMints(OWNER_A)[0]?.unconfirmed).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects queued removal after a live operation locks the mint', async () => {
|
||||||
|
store(OWNER_A, [mint()])
|
||||||
|
const locks = installLocks()
|
||||||
|
|
||||||
|
const live = lockTrustedMint(SERVER, KEY_A, OWNER_A)
|
||||||
|
const removal = removeTrustedMint(SERVER, OWNER_A)
|
||||||
|
await waitForRequests(locks, 2)
|
||||||
|
|
||||||
|
await locks.releaseNext()
|
||||||
|
await locks.releaseNext()
|
||||||
|
|
||||||
|
await expect(live).resolves.toBe('unchanged')
|
||||||
|
await expect(removal).rejects.toThrow(/bearer/)
|
||||||
|
expect(readTrustedMints(OWNER_A)[0]?.locked).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies a queued clear after an earlier writer', async () => {
|
||||||
|
store(OWNER_A, [mint()])
|
||||||
|
const locks = installLocks()
|
||||||
|
|
||||||
|
const add = addTrustedMint('queued.example', KEY_B, {ownerId: OWNER_A})
|
||||||
|
const clear = clearTrustedMints(OWNER_A)
|
||||||
|
await waitForRequests(locks, 2)
|
||||||
|
|
||||||
|
await locks.releaseNext()
|
||||||
|
await locks.releaseNext()
|
||||||
|
await Promise.all([add, clear])
|
||||||
|
|
||||||
|
expect(readTrustedMints(OWNER_A)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects stale, malformed, and foreign-owner mutations', async () => {
|
||||||
|
store(OWNER_B, [mint()])
|
||||||
|
|
||||||
|
await expect(addTrustedMint('stale.example', KEY_B, {ownerId: OWNER_A})).rejects.toThrow(
|
||||||
|
/owner/i,
|
||||||
|
)
|
||||||
|
await expect(addTrustedMint('invalid.example', KEY_B, {ownerId: 'invalid'})).rejects.toThrow(
|
||||||
|
/owner/i,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(readTrustedMints(OWNER_B)).toEqual([mint()])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a malformed stored envelope without overwriting it', async () => {
|
||||||
|
const malformed = JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
ownerId: 'invalid',
|
||||||
|
mints: [],
|
||||||
|
})
|
||||||
|
localStorage.setItem(STORAGE_KEY, malformed)
|
||||||
|
|
||||||
|
await expect(addTrustedMint('new.example', KEY_B, {ownerId: OWNER_A})).rejects.toThrow(
|
||||||
|
/malformed/i,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(malformed)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a malformed mint member without rewriting stored bytes', async () => {
|
||||||
|
const malformed = JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
ownerId: OWNER_A,
|
||||||
|
mints: [mint(), mint({server: 'broken.example', mintPubkey: 'not-hex'})],
|
||||||
|
})
|
||||||
|
localStorage.setItem(STORAGE_KEY, malformed)
|
||||||
|
|
||||||
|
await expect(addTrustedMint('new.example', KEY_B, {ownerId: OWNER_A})).rejects.toThrow(
|
||||||
|
/malformed/i,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(malformed)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps storage and listeners unchanged when the single write fails', async () => {
|
||||||
|
store(OWNER_A, [mint()])
|
||||||
|
vi.stubGlobal('navigator', {})
|
||||||
|
const before = localStorage.getItem(STORAGE_KEY)
|
||||||
|
const notified = vi.fn()
|
||||||
|
const unsubscribe = onTrustedMintsChange(notified)
|
||||||
|
const setItem = localStorage.setItem.bind(localStorage)
|
||||||
|
localStorage.setItem = (): void => {
|
||||||
|
throw new Error('QuotaExceededError')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(addTrustedMint('new.example', KEY_B, {ownerId: OWNER_A})).rejects.toThrow(
|
||||||
|
'QuotaExceededError',
|
||||||
|
)
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(before)
|
||||||
|
expect(notified).not.toHaveBeenCalled()
|
||||||
|
} finally {
|
||||||
|
localStorage.setItem = setItem
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('notifies listeners only after the storage lock is released', async () => {
|
||||||
|
const locks = installLocks()
|
||||||
|
const lockStates: boolean[] = []
|
||||||
|
const unsubscribe = onTrustedMintsChange(() => lockStates.push(locks.held))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const addition = addTrustedMint(SERVER, KEY_A, {ownerId: OWNER_A})
|
||||||
|
await waitForRequests(locks, 1)
|
||||||
|
await locks.releaseNext()
|
||||||
|
await addition
|
||||||
|
|
||||||
|
expect(lockStates).toEqual([false])
|
||||||
|
} finally {
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
// Owner-bound trusted-mint persistence. Web Locks serialize writers while an
|
||||||
|
// IndexedDB commit mirror bridges stale cross-renderer localStorage views.
|
||||||
|
|
||||||
|
import {isWalletOwnerId} from './storage/walletOwner'
|
||||||
|
import {savedKeyOwnerAllows} from './storage/currentOwner'
|
||||||
|
import {storageLocksAvailable, withStorageLock} from './storageLock'
|
||||||
|
import type {TrustedMint} from './trustedMints'
|
||||||
|
import {trustedMintsCommitStore} from './trustedMintsCommitStore'
|
||||||
|
import {notifyStoredTrustedMintsChange, TRUSTED_MINTS_STORAGE_KEY} from './trustedMintsEvents'
|
||||||
|
import {
|
||||||
|
parseLegacyTrustedMintsRegistry,
|
||||||
|
parseStoredTrustedMintsRegistry,
|
||||||
|
serializeTrustedMintsRegistry,
|
||||||
|
type StoredTrustedMintsRegistry,
|
||||||
|
} from './trustedMintsRegistry'
|
||||||
|
import type {MintTransition} from './trustedMintTransitions'
|
||||||
|
|
||||||
|
const STORAGE_KEY = TRUSTED_MINTS_STORAGE_KEY
|
||||||
|
|
||||||
|
type MutationState = {
|
||||||
|
readonly canonicalRaw: string | null
|
||||||
|
readonly current: TrustedMint[]
|
||||||
|
readonly localRaw: string | null
|
||||||
|
readonly mirrorEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidTrustedMintsOwnerError extends Error {
|
||||||
|
override readonly name = 'InvalidTrustedMintsOwnerError'
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('Trusted-mint mutation requires a valid wallet owner.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TrustedMintsOwnerMismatchError extends Error {
|
||||||
|
override readonly name = 'TrustedMintsOwnerMismatchError'
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('Trusted-mint registry belongs to a different wallet owner.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MalformedTrustedMintsRegistryError extends Error {
|
||||||
|
override readonly name = 'MalformedTrustedMintsRegistryError'
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('Trusted-mint registry storage is malformed.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requireOwnedRegistry = (
|
||||||
|
stored: StoredTrustedMintsRegistry,
|
||||||
|
ownerId: string,
|
||||||
|
): StoredTrustedMintsRegistry => {
|
||||||
|
if (stored.kind === 'malformed') throw new MalformedTrustedMintsRegistryError()
|
||||||
|
if (stored.kind === 'valid' && stored.envelope.ownerId !== ownerId) {
|
||||||
|
throw new TrustedMintsOwnerMismatchError()
|
||||||
|
}
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
|
||||||
|
const mirrorIsEnabled = (): boolean =>
|
||||||
|
storageLocksAvailable() && trustedMintsCommitStore.available()
|
||||||
|
|
||||||
|
const readMutationState = async (ownerId: string): Promise<MutationState> => {
|
||||||
|
const localRaw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
const local = requireOwnedRegistry(parseStoredTrustedMintsRegistry(localRaw), ownerId)
|
||||||
|
const mirrorEnabled = mirrorIsEnabled()
|
||||||
|
if (!mirrorEnabled) {
|
||||||
|
return {
|
||||||
|
canonicalRaw: localRaw,
|
||||||
|
current: local.kind === 'valid' ? local.envelope.mints : [],
|
||||||
|
localRaw,
|
||||||
|
mirrorEnabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mirrorRaw = await trustedMintsCommitStore.read()
|
||||||
|
if (mirrorRaw === null) {
|
||||||
|
return {
|
||||||
|
canonicalRaw: localRaw,
|
||||||
|
current: local.kind === 'valid' ? local.envelope.mints : [],
|
||||||
|
localRaw,
|
||||||
|
mirrorEnabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mirror = requireOwnedRegistry(parseStoredTrustedMintsRegistry(mirrorRaw), ownerId)
|
||||||
|
if (mirror.kind !== 'valid') throw new MalformedTrustedMintsRegistryError()
|
||||||
|
return {
|
||||||
|
canonicalRaw: mirrorRaw,
|
||||||
|
current: mirror.envelope.mints,
|
||||||
|
localRaw,
|
||||||
|
mirrorEnabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreLocalRegistry = (raw: string | null): void => {
|
||||||
|
if (raw === null) localStorage.removeItem(STORAGE_KEY)
|
||||||
|
else localStorage.setItem(STORAGE_KEY, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitRegistry = async (
|
||||||
|
previousRaw: string | null,
|
||||||
|
nextRaw: string,
|
||||||
|
mirrorEnabled: boolean,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const localChanged = previousRaw !== nextRaw
|
||||||
|
if (localChanged) localStorage.setItem(STORAGE_KEY, nextRaw)
|
||||||
|
if (!mirrorEnabled) return localChanged
|
||||||
|
try {
|
||||||
|
await trustedMintsCommitStore.write(nextRaw)
|
||||||
|
} catch (error) {
|
||||||
|
if (localChanged) restoreLocalRegistry(previousRaw)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return localChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
export {onStoredTrustedMintsChange} from './trustedMintsEvents'
|
||||||
|
|
||||||
|
export const readOwnedTrustedMints = (ownerId: unknown): TrustedMint[] => {
|
||||||
|
if (!isWalletOwnerId(ownerId)) return []
|
||||||
|
const stored = parseStoredTrustedMintsRegistry(localStorage.getItem(STORAGE_KEY))
|
||||||
|
return stored.kind === 'valid' && stored.envelope.ownerId === ownerId ? stored.envelope.mints : []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutateStoredTrustedMints = async <T>(
|
||||||
|
ownerId: unknown,
|
||||||
|
transition: (mints: TrustedMint[]) => MintTransition<T>,
|
||||||
|
): Promise<T> => {
|
||||||
|
if (!isWalletOwnerId(ownerId)) throw new InvalidTrustedMintsOwnerError()
|
||||||
|
|
||||||
|
const committed = await withStorageLock(STORAGE_KEY, async () => {
|
||||||
|
if (!savedKeyOwnerAllows(ownerId)) throw new TrustedMintsOwnerMismatchError()
|
||||||
|
const state = await readMutationState(ownerId)
|
||||||
|
const next = transition(state.current)
|
||||||
|
const nextRaw = next.changed
|
||||||
|
? serializeTrustedMintsRegistry(ownerId, next.mints)
|
||||||
|
: state.canonicalRaw
|
||||||
|
const localChanged =
|
||||||
|
nextRaw === null ? false : await commitRegistry(state.localRaw, nextRaw, state.mirrorEnabled)
|
||||||
|
return {localChanged, result: next.result}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (committed.localChanged) notifyStoredTrustedMintsChange()
|
||||||
|
return committed.result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adoptLegacyStoredTrustedMints = async (ownerId: unknown): Promise<number> => {
|
||||||
|
if (!isWalletOwnerId(ownerId)) throw new InvalidTrustedMintsOwnerError()
|
||||||
|
const adopted = await withStorageLock(STORAGE_KEY, async () => {
|
||||||
|
if (!savedKeyOwnerAllows(ownerId)) throw new TrustedMintsOwnerMismatchError()
|
||||||
|
const localRaw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
const legacy = parseLegacyTrustedMintsRegistry(localRaw)
|
||||||
|
if (legacy === null) {
|
||||||
|
requireOwnedRegistry(parseStoredTrustedMintsRegistry(localRaw), ownerId)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
await commitRegistry(
|
||||||
|
localRaw,
|
||||||
|
serializeTrustedMintsRegistry(ownerId, legacy),
|
||||||
|
mirrorIsEnabled(),
|
||||||
|
)
|
||||||
|
return legacy
|
||||||
|
})
|
||||||
|
if (adopted !== null) notifyStoredTrustedMintsChange()
|
||||||
|
return adopted?.length ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeStoredTrustedMintsForOwner = async (ownerId: unknown): Promise<void> => {
|
||||||
|
if (!isWalletOwnerId(ownerId)) throw new InvalidTrustedMintsOwnerError()
|
||||||
|
const removed = await withStorageLock(STORAGE_KEY, async () => {
|
||||||
|
if (!savedKeyOwnerAllows(ownerId)) throw new TrustedMintsOwnerMismatchError()
|
||||||
|
const state = await readMutationState(ownerId)
|
||||||
|
if (state.canonicalRaw === null) return false
|
||||||
|
if (state.localRaw !== null) localStorage.removeItem(STORAGE_KEY)
|
||||||
|
try {
|
||||||
|
if (state.mirrorEnabled) await trustedMintsCommitStore.clear()
|
||||||
|
} catch (error) {
|
||||||
|
if (state.localRaw !== null) restoreLocalRegistry(state.localRaw)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if (removed) notifyStoredTrustedMintsChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetStoredTrustedMints = async (): Promise<void> => {
|
||||||
|
const removed = await withStorageLock(STORAGE_KEY, async () => {
|
||||||
|
const localRaw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (localRaw !== null) localStorage.removeItem(STORAGE_KEY)
|
||||||
|
try {
|
||||||
|
if (mirrorIsEnabled()) await trustedMintsCommitStore.clear()
|
||||||
|
} catch (error) {
|
||||||
|
if (localRaw !== null) restoreLocalRegistry(localRaw)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return localRaw !== null
|
||||||
|
})
|
||||||
|
if (removed) notifyStoredTrustedMintsChange()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user