Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
165 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { beforeAll, describe, expect, it, vi } from 'vitest'; | ||
import { addEnsName, getEnsName, getStorageKey } from './ens'; | ||
|
||
// vitalik.eth | ||
const mockAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; | ||
const mockEnsName = 'vitalik.eth'; | ||
|
||
const store: Record<string, string> = {}; | ||
|
||
const localStorageMock: Storage = { | ||
getItem: (key: string): string => store[key] ?? null, | ||
setItem: (key: string, value: string): void => { | ||
store[key] = value.toString(); | ||
}, | ||
removeItem: (key: string): void => { | ||
delete store[key]; | ||
}, | ||
clear: (): void => {}, | ||
// biome-ignore lint/correctness/noUnusedVariables: <explanation> | ||
key: (index: number): string | null => '', | ||
length: Object.keys(store).length, | ||
// Easier to get number realtime, due to single object | ||
// reference not allowing us to use localStorage.length realtime | ||
total: () => Object.keys(store).length, | ||
}; | ||
|
||
beforeAll((): void => { | ||
localStorage = localStorageMock; | ||
}); | ||
|
||
describe('ens', () => { | ||
it('should return null if ens name is not found', () => { | ||
const ensName = getEnsName(mockAddress); | ||
|
||
expect(localStorage.total()).toBe(0); | ||
expect(ensName).toBeNull(); | ||
}); | ||
|
||
it('should return ens name if added to storage', () => { | ||
addEnsName(mockAddress, mockEnsName); | ||
|
||
const ensName = getEnsName(mockAddress); | ||
|
||
expect(localStorage.total()).toBe(1); | ||
expect(ensName).toBe(mockEnsName); | ||
}); | ||
|
||
it('should return null if ens name has expired', () => { | ||
const date = new Date(); | ||
|
||
// 16 minutes passed (expired) | ||
date.setMinutes(date.getMinutes() + 16); | ||
|
||
vi.useFakeTimers(); | ||
vi.setSystemTime(date); | ||
|
||
const ensName = getEnsName(mockAddress); | ||
|
||
expect(localStorage.total()).toBe(0); | ||
expect(ensName).toBeNull(); | ||
}); | ||
|
||
it('should return null if ens name from localStorage is not a string', () => { | ||
addEnsName(mockAddress, mockEnsName); | ||
|
||
expect(localStorage.total()).toBe(1); | ||
|
||
// Pretend someone messed up the localStorage by putting null on ensName field | ||
store[getStorageKey(mockAddress)] = JSON.stringify({ | ||
ensName: null, | ||
expires: 0, | ||
}); | ||
|
||
const ensName = getEnsName(mockAddress); | ||
|
||
expect(localStorage.total()).toBe(0); | ||
expect(ensName).toBeNull(); | ||
}); | ||
|
||
it('should return null if ens name expiration from localStorage is NaN', () => { | ||
addEnsName(mockAddress, mockEnsName); | ||
|
||
expect(localStorage.total()).toBe(1); | ||
|
||
// Pretend someone messed up the localStorage by putting undefined on expires field | ||
store[getStorageKey(mockAddress)] = JSON.stringify({ | ||
ensName: mockEnsName, | ||
expires: undefined, | ||
}); | ||
|
||
const ensName = getEnsName(mockAddress); | ||
|
||
expect(localStorage.total()).toBe(0); | ||
expect(ensName).toBeNull(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { Address, isAddress } from 'viem'; | ||
|
||
interface EnsData { | ||
ensName: string; | ||
expires: number; | ||
} | ||
|
||
export function getStorageKey(address: Address) { | ||
return `rk-ens-${address}`; | ||
} | ||
|
||
function safeParseJsonData(string: string | null): EnsData | null { | ||
try { | ||
const value = string ? JSON.parse(string) : null; | ||
return typeof value === 'object' ? value : null; | ||
} catch { | ||
return null; | ||
} | ||
} | ||
|
||
export function addEnsName(address: Address, ensName: string) { | ||
if (!isAddress(address)) return; | ||
|
||
const now = new Date(); | ||
|
||
const expiry = new Date(now.getTime() + 15 * 60_000); // Set expiry to 15 minutes from now | ||
|
||
localStorage.setItem( | ||
getStorageKey(address), | ||
JSON.stringify({ | ||
ensName, | ||
expires: expiry.getTime(), | ||
}), | ||
); | ||
} | ||
|
||
export function getEnsName(address: Address): string | null { | ||
const data = safeParseJsonData(localStorage.getItem(getStorageKey(address))); | ||
|
||
if (!data) return null; | ||
|
||
const { ensName, expires } = data; | ||
|
||
if (typeof ensName !== 'string' || Number.isNaN(Number(expires))) { | ||
localStorage.removeItem(getStorageKey(address)); | ||
return null; | ||
} | ||
|
||
const now = new Date(); | ||
|
||
if (now.getTime() > Number(expires)) { | ||
localStorage.removeItem(getStorageKey(address)); | ||
return null; | ||
} | ||
|
||
return ensName; | ||
} |