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
1 change: 1 addition & 0 deletions packages/sdk-multichain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^3.2.4",
"esbuild-plugin-umd-wrapper": "^3.0.0",
"fake-indexeddb": "^6.0.1",
"jsdom": "^26.1.0",
"nock": "^14.0.4",
"prettier": "^3.3.3",
Expand Down
22 changes: 20 additions & 2 deletions packages/sdk-multichain/src/fixtures.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: Tests require it */
/** biome-ignore-all lint/suspicious/noAsyncPromiseExecutor: ok for tests */
/** biome-ignore-all lint/style/noNonNullAssertion: Tests require it */
/**
* Test fixtures and utilities for the Multichain SDK tests
* This file is excluded from test discovery via vitest.config.ts
*/

// Additional imports for standardized setup functions
import AsyncStorage from '@react-native-async-storage/async-storage';
import { JSDOM as Page } from 'jsdom';
Expand All @@ -19,6 +19,7 @@ import { createMetamaskSDK as createMetamaskSDKWeb } from './index.browser';
import { createMetamaskSDK as createMetamaskSDKRN } from './index.native';
import { createMetamaskSDK as createMetamaskSDKNode } from './index.node';
import * as nodeStorage from './store/adapters/node';
import * as webStorage from './store/adapters/web';
import type { DappClient } from '@metamask/mobile-wallet-protocol-dapp-client';

// Mock logger at the top level
Expand Down Expand Up @@ -341,7 +342,6 @@ export const setupWebMocks = (nativeStorageStub: NativeStorageStub, dappUrl = 'h
...dom.window,
addEventListener: t.vi.fn(),
removeEventListener: t.vi.fn(),
localStorage: nativeStorageStub,
ethereum: {
isMetaMask: true,
},
Expand All @@ -356,6 +356,24 @@ export const setupWebMocks = (nativeStorageStub: NativeStorageStub, dappUrl = 'h
t.vi.stubGlobal('document', dom.window.document);
t.vi.stubGlobal('HTMLElement', dom.window.HTMLElement);
t.vi.stubGlobal('requestAnimationFrame', t.vi.fn());
t.vi.spyOn(webStorage, 'StoreAdapterWeb').mockImplementation(() => {
const __storage = {
get: t.vi.fn((key: string) => {
return nativeStorageStub.getItem(key);
}),
set: t.vi.fn((key: string, value: string) => {
return nativeStorageStub.setItem(key, value);
}),
delete: t.vi.fn((key: string) => {
return nativeStorageStub.removeItem(key);
}),
platform: 'web' as const,
get storage() {
return __storage;
},
} as any;
return __storage;
});
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm a little confused. Why do we need fake-indexeddb/auto if we're just mocking the adapter that wraps it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats true, fake-indexeddb/auto is no longer needed. This mock came after

};

// Helper functions to create standardized test configurations
Expand Down
81 changes: 75 additions & 6 deletions packages/sdk-multichain/src/store/adapters/web.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,92 @@
import { StoreAdapter } from '../../domain';

type kvStores = 'sdk-kv-store' | 'key-value-pairs';

export class StoreAdapterWeb extends StoreAdapter {
static readonly stores: kvStores[] = ['sdk-kv-store', 'key-value-pairs'];
static readonly DB_NAME = 'mmsdk';

readonly platform = 'web';
readonly dbPromise: Promise<IDBDatabase>;

private get internal() {
if (typeof window === 'undefined' || !window.localStorage) {
throw new Error('localStorage is not available in this environment');
if (typeof window === 'undefined' || !window.indexedDB) {
throw new Error('indexedDB is not available in this environment');
}
return window.localStorage;
return window.indexedDB;
}

constructor(
dbNameSuffix: `-${string}` = '-kv-store',
private storeName: kvStores = StoreAdapterWeb.stores[0],
) {
super();

const dbName = `${StoreAdapterWeb.DB_NAME}${dbNameSuffix}`;
this.dbPromise = new Promise((resolve, reject) => {
try {
const request = this.internal.open(dbName, 1);
request.onerror = () => reject(new Error('Failed to open IndexedDB.'));
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = () => {
const db = request.result;
for (const name of StoreAdapterWeb.stores) {
if (!db.objectStoreNames.contains(name)) {
db.createObjectStore(name);
}
}
};
} catch (error) {
reject(error);
}
});
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.

async get(key: string): Promise<string | null> {
return this.internal.getItem(key);
const { storeName } = this;
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
try {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(new Error('Failed to get value from IndexedDB.'));
request.onsuccess = () => resolve((request.result as string) ?? null);
} catch (error) {
reject(error);
}
});
}

async set(key: string, value: string): Promise<void> {
return this.internal.setItem(key, value);
const { storeName } = this;
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
try {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.put(value, key);
request.onerror = () => reject(new Error('Failed to set value in IndexedDB.'));
request.onsuccess = () => resolve();
} catch (error) {
reject(error);
}
});
}

async delete(key: string): Promise<void> {
return this.internal.removeItem(key);
const { storeName } = this;
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
try {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.delete(key);
request.onerror = () => reject(new Error('Failed to delete value from IndexedDB.'));
request.onsuccess = () => resolve();
} catch (error) {
reject(error);
}
});
Comment thread
elribonazo marked this conversation as resolved.
Comment thread
elribonazo marked this conversation as resolved.
}
}
6 changes: 5 additions & 1 deletion packages/sdk-multichain/src/store/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: Tests require it */
/** biome-ignore-all lint/style/noNonNullAssertion: Tests require it */
import 'fake-indexeddb/auto';
import { IDBFactory } from 'fake-indexeddb';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as t from 'vitest';
import type { StoreAdapter } from '../domain';
Expand Down Expand Up @@ -227,13 +229,15 @@ t.describe(`Store with WebAdapter`, () => {
() => {
t.vi.stubGlobal('window', {
localStorage: nativeStorageStub,
indexedDB: new IDBFactory(),
});
},
);

t.it("Should throw an exception if we try using the store with a browser that doesn't support localStorage", async () => {
t.vi.stubGlobal('window', {
localStorage: null,
localStorage: undefined,
indexedDB: undefined,
});
const store = new Store(new StoreAdapterWeb());
await t.expect(() => store.getAnonId()).rejects.toThrow();
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11279,6 +11279,7 @@ __metadata:
eciesjs: ^0.4.15
esbuild-plugin-umd-wrapper: ^3.0.0
eventemitter3: ^5.0.1
fake-indexeddb: ^6.0.1
jsdom: ^26.1.0
nock: ^14.0.4
prettier: ^3.3.3
Expand Down Expand Up @@ -32148,6 +32149,13 @@ __metadata:
languageName: node
linkType: hard

"fake-indexeddb@npm:^6.0.1":
version: 6.0.1
resolution: "fake-indexeddb@npm:6.0.1"
checksum: c4b8a0576cf3165238494b67641539d4ff36194e038b36e6992449eb882923dfaadba78a62cfc7d5ae9a5c0ac2fa1e70af5cb6c2228dc764ac79b65f0e68e942
languageName: node
linkType: hard

"fast-deep-equal@npm:^2.0.1":
version: 2.0.1
resolution: "fast-deep-equal@npm:2.0.1"
Expand Down
Loading