mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
fix: mirror trusted mint commits across tabs
This commit is contained in:
@@ -0,0 +1,208 @@
|
|||||||
|
import type { Page, TestInfo } from '@playwright/test';
|
||||||
|
|
||||||
|
import { test, expect } from '../fixtures';
|
||||||
|
import { createFreshWallet } from '../helpers/wallet';
|
||||||
|
|
||||||
|
const MINT_KEY_A = `02${'aa'.repeat(32)}`;
|
||||||
|
const MINT_KEY_B = `03${'bb'.repeat(32)}`;
|
||||||
|
const MINT_KEY_C = `02${'cc'.repeat(32)}`;
|
||||||
|
const VIEWPORT_WIDTHS = [375, 768, 1280] as const;
|
||||||
|
const CONCURRENT_ADD_ROUNDS = 10;
|
||||||
|
|
||||||
|
const mintsList = (page: Page) => page.locator('.q-list', { hasText: 'Your mints' });
|
||||||
|
|
||||||
|
const fillMintForm = async (page: Page, server: string, mintPubkey: string): Promise<void> => {
|
||||||
|
await page.getByLabel('Server').fill(server);
|
||||||
|
await page.getByLabel('Signing key (66 hex characters)').fill(mintPubkey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMint = async (page: Page, server: string, mintPubkey: string): Promise<void> => {
|
||||||
|
await fillMintForm(page, server, mintPubkey);
|
||||||
|
await page.getByRole('button', { name: 'Trust this mint' }).click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const ownerId = async (page: Page): Promise<string | null> =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem('sattle_linking_key');
|
||||||
|
if (raw === null) return null;
|
||||||
|
const saved: unknown = JSON.parse(raw);
|
||||||
|
if (typeof saved !== 'object' || saved === null || !('ownerId' in saved)) return null;
|
||||||
|
return typeof saved.ownerId === 'string' ? saved.ownerId : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const storedMintServers = async (page: Page): Promise<string[]> =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const raw = localStorage.getItem('sattle_trusted_mints');
|
||||||
|
if (raw === null) return [];
|
||||||
|
const registry: unknown = JSON.parse(raw);
|
||||||
|
if (
|
||||||
|
typeof registry !== 'object' ||
|
||||||
|
registry === null ||
|
||||||
|
!('mints' in registry) ||
|
||||||
|
!Array.isArray(registry.mints)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return registry.mints
|
||||||
|
.map((mint) =>
|
||||||
|
typeof mint === 'object' &&
|
||||||
|
mint !== null &&
|
||||||
|
'server' in mint &&
|
||||||
|
typeof mint.server === 'string'
|
||||||
|
? mint.server
|
||||||
|
: '',
|
||||||
|
)
|
||||||
|
.sort();
|
||||||
|
});
|
||||||
|
|
||||||
|
const retainFailureEvidence = async (
|
||||||
|
testInfo: TestInfo,
|
||||||
|
pages: readonly Page[],
|
||||||
|
consoleMessages: readonly string[],
|
||||||
|
): Promise<void> => {
|
||||||
|
for (const [index, page] of pages.entries()) {
|
||||||
|
if (page.isClosed()) continue;
|
||||||
|
const path = testInfo.outputPath(`tab-${index + 1}-failure.png`);
|
||||||
|
await page.screenshot({ path, fullPage: true });
|
||||||
|
await testInfo.attach(`tab-${index + 1}-failure`, { path, contentType: 'image/png' });
|
||||||
|
}
|
||||||
|
await testInfo.attach('browser-console', {
|
||||||
|
body: consoleMessages.join('\n'),
|
||||||
|
contentType: 'text/plain',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('trusted mint tabs', () => {
|
||||||
|
test('remote updates converge and concurrent additions survive Web Locks', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
await createFreshWallet(page);
|
||||||
|
test.skip(
|
||||||
|
!(await page.evaluate(() => 'locks' in navigator)),
|
||||||
|
'Web Locks are unavailable, so this browser provides no concurrent-write guarantee.',
|
||||||
|
);
|
||||||
|
|
||||||
|
const remote = await page.context().newPage();
|
||||||
|
const consoleMessages: string[] = [];
|
||||||
|
for (const [name, current] of [
|
||||||
|
['first', page],
|
||||||
|
['second', remote],
|
||||||
|
] as const) {
|
||||||
|
current.on('console', (message) => {
|
||||||
|
consoleMessages.push(`[${name}] ${message.type()}: ${message.text()}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([page.goto('/#/settings/mints'), remote.goto('/#/settings/mints')]);
|
||||||
|
await expect(mintsList(page).getByText('No mints yet', { exact: false })).toBeVisible();
|
||||||
|
await expect(mintsList(remote).getByText('No mints yet', { exact: false })).toBeVisible();
|
||||||
|
|
||||||
|
// A real storage event from the second tab updates the first tab without reload.
|
||||||
|
await addMint(remote, 'remote.example', MINT_KEY_A);
|
||||||
|
await expect(mintsList(page).getByText('remote.example', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
// Repeated UI races from separate pages must all survive Web Locks.
|
||||||
|
const expectedServers = ['remote.example'];
|
||||||
|
for (let round = 1; round <= CONCURRENT_ADD_ROUNDS; round++) {
|
||||||
|
const firstServer = `first-${round}.example`;
|
||||||
|
const secondServer = `second-${round}.example`;
|
||||||
|
expectedServers.push(firstServer, secondServer);
|
||||||
|
await Promise.all([
|
||||||
|
fillMintForm(page, firstServer, MINT_KEY_B),
|
||||||
|
fillMintForm(remote, secondServer, MINT_KEY_C),
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button', { name: 'Trust this mint' }).click(),
|
||||||
|
remote.getByRole('button', { name: 'Trust this mint' }).click(),
|
||||||
|
]);
|
||||||
|
await expect.poll(() => storedMintServers(page)).toEqual([...expectedServers].sort());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const width of VIEWPORT_WIDTHS) {
|
||||||
|
await Promise.all([
|
||||||
|
page.setViewportSize({ width, height: 900 }),
|
||||||
|
remote.setViewportSize({ width, height: 900 }),
|
||||||
|
]);
|
||||||
|
await expect(mintsList(page).getByText('first-10.example', { exact: true })).toBeVisible();
|
||||||
|
await expect(mintsList(page).getByText('second-10.example', { exact: true })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
mintsList(remote).getByText('first-10.example', { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
mintsList(remote).getByText('second-10.example', { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
expect(await storedMintServers(page)).toEqual([...expectedServers].sort());
|
||||||
|
} catch (error) {
|
||||||
|
await retainFailureEvidence(testInfo, [page, remote], consoleMessages);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await remote.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('wallet ownership', () => {
|
||||||
|
test('a stale owner tab locks and cannot recreate trust or NWC state', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
await createFreshWallet(page);
|
||||||
|
const oldOwner = await ownerId(page);
|
||||||
|
expect(oldOwner).not.toBeNull();
|
||||||
|
|
||||||
|
const stale = await page.context().newPage();
|
||||||
|
const consoleMessages: string[] = [];
|
||||||
|
for (const [name, current] of [
|
||||||
|
['successor', page],
|
||||||
|
['stale', stale],
|
||||||
|
] as const) {
|
||||||
|
current.on('console', (message) => {
|
||||||
|
consoleMessages.push(`[${name}] ${message.type()}: ${message.text()}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stale.goto('/#/settings/mints');
|
||||||
|
await expect(mintsList(stale).getByText('No mints yet', { exact: false })).toBeVisible();
|
||||||
|
|
||||||
|
// The active page forgets owner A, then creates owner B through the rendered onboarding flow.
|
||||||
|
await page.evaluate(() => window.__sattleWalletTest.forget());
|
||||||
|
await expect(page.getByRole('button', { name: 'Get started' })).toBeVisible();
|
||||||
|
await stale.goto('/#/');
|
||||||
|
await expect(stale.getByText('Wallet locked')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Get started' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Create wallet' }).click();
|
||||||
|
await page.locator('.q-checkbox', { hasText: 'I wrote it down' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('button', { name: 'Receive' })).toBeVisible();
|
||||||
|
|
||||||
|
const successorOwner = await ownerId(page);
|
||||||
|
expect(successorOwner).not.toBe(oldOwner);
|
||||||
|
for (const width of VIEWPORT_WIDTHS) {
|
||||||
|
await stale.setViewportSize({ width, height: 900 });
|
||||||
|
await expect(stale.getByText('Wallet locked')).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stale settings route cannot call owner-bound mutations after invalidation.
|
||||||
|
await stale.goto('/#/settings/mints');
|
||||||
|
await addMint(stale, 'stale.example', MINT_KEY_A);
|
||||||
|
await expect(stale.locator('.q-banner', { hasText: 'Wallet is locked.' })).toBeVisible();
|
||||||
|
await stale.goto('/#/settings/nwc');
|
||||||
|
await expect(stale.locator('.q-page', { hasText: 'Unlock your wallet first' })).toBeVisible();
|
||||||
|
|
||||||
|
const residue = await page.evaluate(() => ({
|
||||||
|
trustedMints: localStorage.getItem('sattle_trusted_mints'),
|
||||||
|
nwcConnections: localStorage.getItem('sattle_nwc_connections'),
|
||||||
|
nwcEnabled: localStorage.getItem('sattle_nwc_enabled'),
|
||||||
|
}));
|
||||||
|
expect(residue).toEqual({ trustedMints: null, nwcConnections: null, nwcEnabled: null });
|
||||||
|
} catch (error) {
|
||||||
|
await retainFailureEvidence(testInfo, [page, stale], consoleMessages);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await stale.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// A Web Lock handoff is not a localStorage visibility barrier. The next
|
||||||
|
// holder must reconcile from a durable cross-context commit before writing.
|
||||||
|
|
||||||
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
||||||
|
|
||||||
|
import {linkingPubKeyHex, saveLinkingKey} from './keys'
|
||||||
|
import {stubLocalStorage} from './test-utils'
|
||||||
|
import {addTrustedMint, readTrustedMints, type TrustedMint} from './trustedMints'
|
||||||
|
import {trustedMintsCommitStore} from './trustedMintsCommitStore'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'sattle_trusted_mints'
|
||||||
|
const LINKING_KEY = new Uint8Array(32).fill(7)
|
||||||
|
const OWNER_ID = linkingPubKeyHex(LINKING_KEY)
|
||||||
|
const OTHER_OWNER_ID = linkingPubKeyHex(new Uint8Array(32).fill(9))
|
||||||
|
const KEY_A = '02' + 'aa'.repeat(32)
|
||||||
|
const KEY_B = '03' + 'bb'.repeat(32)
|
||||||
|
|
||||||
|
const envelope = (mints: TrustedMint[]): string =>
|
||||||
|
JSON.stringify({version: 1, ownerId: OWNER_ID, mints})
|
||||||
|
|
||||||
|
const mint = (server: string): TrustedMint => ({
|
||||||
|
server,
|
||||||
|
mintPubkey: KEY_A,
|
||||||
|
addedAt: 1,
|
||||||
|
locked: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
stubLocalStorage()
|
||||||
|
vi.stubGlobal('navigator', {
|
||||||
|
locks: {
|
||||||
|
request: (_name: string, callback: () => unknown): Promise<unknown> =>
|
||||||
|
Promise.resolve().then(callback),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await saveLinkingKey(LINKING_KEY)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('trusted-mint commit visibility', () => {
|
||||||
|
it('reconciles the previous holder when localStorage is stale after lock handoff', async () => {
|
||||||
|
// Given a durable commit mirror shared by two lock holders
|
||||||
|
let committedRaw: string | null = null
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'available').mockReturnValue(true)
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'read').mockImplementation(async () => committedRaw)
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'write').mockImplementation(async (raw) => {
|
||||||
|
committedRaw = raw
|
||||||
|
})
|
||||||
|
|
||||||
|
localStorage.setItem(STORAGE_KEY, envelope([mint('remote.example')]))
|
||||||
|
await addTrustedMint('first.example', KEY_A, {ownerId: OWNER_ID})
|
||||||
|
|
||||||
|
// When the next holder sees the pre-commit localStorage view
|
||||||
|
localStorage.setItem(STORAGE_KEY, envelope([mint('remote.example')]))
|
||||||
|
const writes = vi.spyOn(localStorage, 'setItem')
|
||||||
|
await addTrustedMint('second.example', KEY_B, {ownerId: OWNER_ID})
|
||||||
|
|
||||||
|
// Then it writes one reconciled envelope and both accepted additions survive
|
||||||
|
expect(readTrustedMints(OWNER_ID).map((entry) => entry.server)).toEqual([
|
||||||
|
'remote.example',
|
||||||
|
'first.example',
|
||||||
|
'second.example',
|
||||||
|
])
|
||||||
|
expect(writes).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not resolve success before the durable commit mirror completes', async () => {
|
||||||
|
// Given a commit store whose durable write is gated
|
||||||
|
let releaseCommit: (() => void) | undefined
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'available').mockReturnValue(true)
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'read').mockResolvedValue(null)
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'write').mockImplementation(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
releaseCommit = resolve
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
let settled = false
|
||||||
|
|
||||||
|
// When a mutation has written localStorage but not the commit mirror
|
||||||
|
const addition = addTrustedMint('first.example', KEY_A, {ownerId: OWNER_ID}).finally(() => {
|
||||||
|
settled = true
|
||||||
|
})
|
||||||
|
await vi.waitFor(() => expect(releaseCommit).toBeTypeOf('function'))
|
||||||
|
|
||||||
|
// Then success remains pending until the durable mirror completes
|
||||||
|
expect(settled).toBe(false)
|
||||||
|
releaseCommit?.()
|
||||||
|
await expect(addition).resolves.toBe('added')
|
||||||
|
expect(settled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects malformed local bytes instead of trusting the commit mirror', async () => {
|
||||||
|
// Given a valid mirror but malformed canonical local storage
|
||||||
|
const malformed = '{'
|
||||||
|
localStorage.setItem(STORAGE_KEY, malformed)
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'available').mockReturnValue(true)
|
||||||
|
const readMirror = vi
|
||||||
|
.spyOn(trustedMintsCommitStore, 'read')
|
||||||
|
.mockResolvedValue(envelope([mint('mirrored.example')]))
|
||||||
|
|
||||||
|
// When a mutation attempts reconciliation, then malformed local bytes stay authoritative
|
||||||
|
await expect(addTrustedMint('new.example', KEY_B, {ownerId: OWNER_ID})).rejects.toThrow(
|
||||||
|
/malformed/i,
|
||||||
|
)
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(malformed)
|
||||||
|
expect(readMirror).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a foreign-owner commit mirror without rewriting local storage', async () => {
|
||||||
|
// Given an owner-A local registry and an owner-B durable mirror
|
||||||
|
const localRaw = envelope([mint('local.example')])
|
||||||
|
const foreignRaw = JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
ownerId: OTHER_OWNER_ID,
|
||||||
|
mints: [mint('foreign.example')],
|
||||||
|
})
|
||||||
|
localStorage.setItem(STORAGE_KEY, localRaw)
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'available').mockReturnValue(true)
|
||||||
|
vi.spyOn(trustedMintsCommitStore, 'read').mockResolvedValue(foreignRaw)
|
||||||
|
|
||||||
|
// When owner A mutates, then exact owner validation rejects both sources unchanged
|
||||||
|
await expect(addTrustedMint('new.example', KEY_B, {ownerId: OWNER_ID})).rejects.toThrow(
|
||||||
|
/owner/i,
|
||||||
|
)
|
||||||
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(localRaw)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the documented stale-write limitation when Web Locks are unavailable', async () => {
|
||||||
|
// Given no cross-tab lock capability, even if IndexedDB exists
|
||||||
|
vi.stubGlobal('navigator', {})
|
||||||
|
const readMirror = vi.spyOn(trustedMintsCommitStore, 'read').mockResolvedValue(null)
|
||||||
|
const writeMirror = vi.spyOn(trustedMintsCommitStore, 'write').mockResolvedValue()
|
||||||
|
localStorage.setItem(STORAGE_KEY, envelope([mint('remote.example')]))
|
||||||
|
await addTrustedMint('first.example', KEY_A, {ownerId: OWNER_ID})
|
||||||
|
|
||||||
|
// When a later unlocked mutation reads a stale view, then no false convergence is claimed
|
||||||
|
localStorage.setItem(STORAGE_KEY, envelope([mint('remote.example')]))
|
||||||
|
await addTrustedMint('second.example', KEY_B, {ownerId: OWNER_ID})
|
||||||
|
|
||||||
|
expect(readTrustedMints(OWNER_ID).map((entry) => entry.server)).toEqual([
|
||||||
|
'remote.example',
|
||||||
|
'second.example',
|
||||||
|
])
|
||||||
|
expect(readMirror).not.toHaveBeenCalled()
|
||||||
|
expect(writeMirror).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Web Locks serialize registry writers but do not make one renderer's
|
||||||
|
// localStorage cache current in the next renderer. IndexedDB is the durable,
|
||||||
|
// cross-context commit mirror used to carry the last completed envelope.
|
||||||
|
|
||||||
|
const DATABASE_NAME = 'sattle-storage-coordination'
|
||||||
|
const DATABASE_VERSION = 1
|
||||||
|
const STORE_NAME = 'trusted-mints'
|
||||||
|
const REGISTRY_KEY = 'registry'
|
||||||
|
|
||||||
|
let databasePromise: Promise<IDBDatabase> | undefined
|
||||||
|
|
||||||
|
export class TrustedMintsCommitStoreError extends Error {
|
||||||
|
override readonly name = 'TrustedMintsCommitStoreError'
|
||||||
|
|
||||||
|
constructor(message: string, cause?: unknown) {
|
||||||
|
super(message, {cause})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDatabase = (): Promise<IDBDatabase> => {
|
||||||
|
if (databasePromise) return databasePromise
|
||||||
|
databasePromise = new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION)
|
||||||
|
request.onupgradeneeded = () => {
|
||||||
|
if (!request.result.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
request.result.createObjectStore(STORE_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const database = request.result
|
||||||
|
database.onversionchange = () => {
|
||||||
|
database.close()
|
||||||
|
databasePromise = undefined
|
||||||
|
}
|
||||||
|
resolve(database)
|
||||||
|
}
|
||||||
|
request.onerror = () => {
|
||||||
|
databasePromise = undefined
|
||||||
|
reject(
|
||||||
|
new TrustedMintsCommitStoreError(
|
||||||
|
'Unable to open trusted-mint commit storage.',
|
||||||
|
request.error,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
request.onblocked = () => {
|
||||||
|
databasePromise = undefined
|
||||||
|
reject(new TrustedMintsCommitStoreError('Trusted-mint commit storage upgrade is blocked.'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return databasePromise
|
||||||
|
}
|
||||||
|
|
||||||
|
const runRequest = async <T>(
|
||||||
|
mode: IDBTransactionMode,
|
||||||
|
createRequest: (store: IDBObjectStore) => IDBRequest<T>,
|
||||||
|
): Promise<T> => {
|
||||||
|
const database = await openDatabase()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = database.transaction(STORE_NAME, mode, {
|
||||||
|
durability: mode === 'readwrite' ? 'strict' : 'default',
|
||||||
|
})
|
||||||
|
const request = createRequest(transaction.objectStore(STORE_NAME))
|
||||||
|
transaction.oncomplete = () => resolve(request.result)
|
||||||
|
transaction.onerror = () =>
|
||||||
|
reject(
|
||||||
|
new TrustedMintsCommitStoreError(
|
||||||
|
'Trusted-mint commit storage transaction failed.',
|
||||||
|
transaction.error,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
transaction.onabort = () =>
|
||||||
|
reject(
|
||||||
|
new TrustedMintsCommitStoreError(
|
||||||
|
'Trusted-mint commit storage transaction was aborted.',
|
||||||
|
transaction.error,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const trustedMintsCommitStore = {
|
||||||
|
available: (): boolean => typeof indexedDB !== 'undefined',
|
||||||
|
read: async (): Promise<string | null> => {
|
||||||
|
const value = await runRequest('readonly', (store) => store.get(REGISTRY_KEY))
|
||||||
|
if (value === undefined) return null
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new TrustedMintsCommitStoreError('Trusted-mint commit storage is malformed.')
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
write: async (raw: string): Promise<void> => {
|
||||||
|
await runRequest('readwrite', (store) => store.put(raw, REGISTRY_KEY))
|
||||||
|
},
|
||||||
|
clear: async (): Promise<void> => {
|
||||||
|
await runRequest('readwrite', (store) => store.delete(REGISTRY_KEY))
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user