From 86952e97c19b348f65879704462bf19f09f21d2c Mon Sep 17 00:00:00 2001 From: Doug Richar Date: Sat, 9 Dec 2023 18:25:19 -0500 Subject: [PATCH] test: add *.spec.ts test files for Jest --- .github/workflows/ci.yml | 10 +- __tests__/network/utils.spec.ts | 84 +++++++ __tests__/store/pubsub.spec.ts | 50 +++++ __tests__/store/store.spec.ts | 282 +++++++++++++++++++++++ __tests__/store/utils.spec.ts | 221 ++++++++++++++++++ __tests__/wallets/manager.spec.ts | 267 ++++++++++++++++++++++ __tests__/wallets/utils.spec.ts | 361 ++++++++++++++++++++++++++++++ bun.lockb | Bin 258641 -> 258665 bytes jest.config.cjs | 2 + package.json | 4 +- 10 files changed, 1276 insertions(+), 5 deletions(-) create mode 100644 __tests__/network/utils.spec.ts create mode 100644 __tests__/store/pubsub.spec.ts create mode 100644 __tests__/store/store.spec.ts create mode 100644 __tests__/store/utils.spec.ts create mode 100644 __tests__/wallets/manager.spec.ts create mode 100644 __tests__/wallets/utils.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 046bc14..7c6cb93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,8 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.0.15 - name: Install dependencies run: bun install @@ -34,11 +36,11 @@ jobs: - name: Typecheck run: bun run typecheck - - name: Run tests - run: bun test + - name: Run tests (Bun) + run: bun run test:bun - # - name: Run Jest tests - # run: bun run test + - name: Run tests (Jest) + run: bun run test:jest - name: Build run: bun run build diff --git a/__tests__/network/utils.spec.ts b/__tests__/network/utils.spec.ts new file mode 100644 index 0000000..f4cd3e3 --- /dev/null +++ b/__tests__/network/utils.spec.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from '@jest/globals' +import { NetworkId } from 'src/network/constants' +import { isAlgodConfig, isNetworkConfigMap, isValidNetworkId } from 'src/network/utils' + +describe('Type Guards', () => { + describe('isValidNetworkId', () => { + it('returns true for a valid NetworkId', () => { + expect(isValidNetworkId(NetworkId.TESTNET)).toBe(true) + }) + + it('returns false for an invalid NetworkId', () => { + expect(isValidNetworkId('foo')).toBe(false) + }) + }) + + describe('isAlgodConfig', () => { + it('returns true for a valid AlgodConfig', () => { + expect( + isAlgodConfig({ + token: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + baseServer: 'http://localhost', + port: 1234, + headers: { + 'X-Foo': 'bar' + } + }) + ).toBe(true) + + expect( + isAlgodConfig({ + token: '', + baseServer: '' + }) + ).toBe(true) + }) + + it('returns false for an invalid AlgodConfig', () => { + expect( + isAlgodConfig({ + baseServer: '' + }) + ).toBe(false) + + expect( + isAlgodConfig({ + token: '' + }) + ).toBe(false) + + expect( + isAlgodConfig({ + token: '', + baseServer: '', + foo: '' + }) + ).toBe(false) + }) + }) + + describe('isNetworkConfigMap', () => { + it('returns true for a valid NetworkConfigMap', () => { + const validConfigMap = { + [NetworkId.MAINNET]: { + token: '', + baseServer: '' + }, + [NetworkId.TESTNET]: { + token: '', + baseServer: '' + } + } + expect(isNetworkConfigMap(validConfigMap)).toBe(true) + }) + + it('returns false for an invalid NetworkConfigMap', () => { + expect( + isNetworkConfigMap({ + token: '', + baseServer: '' + }) + ).toBe(false) + }) + }) +}) diff --git a/__tests__/store/pubsub.spec.ts b/__tests__/store/pubsub.spec.ts new file mode 100644 index 0000000..9e98376 --- /dev/null +++ b/__tests__/store/pubsub.spec.ts @@ -0,0 +1,50 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals' +import { PubSub } from 'src/store/pubsub' + +describe('PubSub', () => { + let pubSub: PubSub + + beforeEach(() => { + pubSub = new PubSub() + }) + + it('should call all callbacks subscribed to an event when it is published', () => { + const callback1 = jest.fn() + const callback2 = jest.fn() + const event = 'testEvent' + const data = { key: 'value' } + + pubSub.subscribe(event, callback1) + pubSub.subscribe(event, callback2) + pubSub.publish(event, data) + + expect(callback1).toHaveBeenCalledWith(data) + expect(callback2).toHaveBeenCalledWith(data) + }) + + it('should not call callbacks subscribed to different events', () => { + const callback1 = jest.fn() + const event1 = 'testEvent1' + const event2 = 'testEvent2' + const data = { key: 'value' } + + pubSub.subscribe(event1, callback1) + pubSub.publish(event2, data) + + expect(callback1).not.toHaveBeenCalled() + }) + + it('should pass the correct data to callbacks when an event is published', () => { + const callback = jest.fn() + const event = 'testEvent' + const data1 = { key: 'value1' } + const data2 = { key: 'value2' } + + pubSub.subscribe(event, callback) + pubSub.publish(event, data1) + pubSub.publish(event, data2) + + expect(callback).toHaveBeenNthCalledWith(1, data1) + expect(callback).toHaveBeenNthCalledWith(2, data2) + }) +}) diff --git a/__tests__/store/store.spec.ts b/__tests__/store/store.spec.ts new file mode 100644 index 0000000..6be487c --- /dev/null +++ b/__tests__/store/store.spec.ts @@ -0,0 +1,282 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals' +import { NetworkId } from 'src/network/constants' +import { StoreActions, StoreMutations, createStore, defaultState } from 'src/store' +import { LOCAL_STORAGE_KEY } from 'src/store/constants' +import { replacer } from 'src/store/utils' +import { WalletId } from 'src/wallets/supported/constants' + +// Suppress console output +jest.spyOn(console, 'info').mockImplementation(() => {}) +jest.spyOn(console, 'groupCollapsed').mockImplementation(() => {}) + +// Mock console.error +const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}) + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {} + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: any) => (store[key] = value.toString()), + clear: () => (store = {}) + } +})() +Object.defineProperty(global, 'localStorage', { + value: localStorageMock +}) + +describe('Store', () => { + beforeEach(() => { + localStorage.clear() + mockConsoleError.mockClear() + }) + + describe('Initialization', () => { + it('initializes with provided state', () => { + const initialState = { ...defaultState } + const store = createStore(initialState) + expect(store.getState()).toEqual(initialState) + }) + + it('initializes from persisted state', () => { + const persistedState = { ...defaultState, activeNetwork: NetworkId.MAINNET } + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(persistedState, replacer)) + const store = createStore(defaultState) + expect(store.getState()).toEqual(persistedState) + }) + }) + + describe('Dispatching Actions', () => { + it('dispatches a addWallet action correctly', () => { + const account = { + name: 'Defly Wallet 1', + address: 'address' + } + const walletState = { + accounts: [account], + activeAccount: account + } + const store = createStore(defaultState) + store.dispatch(StoreActions.ADD_WALLET, { + walletId: WalletId.DEFLY, + wallet: walletState + }) + expect(store.getState().wallets.get(WalletId.DEFLY)).toEqual(walletState) + }) + + it('dispatches a removeWallet action correctly', () => { + const account = { + name: 'Defly Wallet 1', + address: 'address' + } + const store = createStore({ + ...defaultState, + wallets: new Map([ + [ + WalletId.DEFLY, + { + accounts: [account], + activeAccount: account + } + ] + ]) + }) + expect(store.getState().wallets.get(WalletId.DEFLY)).toBeDefined() + store.dispatch(StoreActions.REMOVE_WALLET, { walletId: WalletId.DEFLY }) + expect(store.getState().wallets.get(WalletId.DEFLY)).toBeUndefined() + }) + + // @todo: Should fail if walletId is not in wallets map + it('dispatches a setActiveWallet action correctly', () => { + const store = createStore(defaultState) + store.dispatch(StoreActions.SET_ACTIVE_WALLET, { walletId: WalletId.DEFLY }) + expect(store.getState().activeWallet).toBe(WalletId.DEFLY) + }) + + it('dispatches a setActiveAccount action correctly', () => { + const account1 = { + name: 'Defly Wallet 1', + address: 'address1' + } + const account2 = { + name: 'Defly Wallet 2', + address: 'address2' + } + const store = createStore({ + ...defaultState, + wallets: new Map([ + [ + WalletId.DEFLY, + { + accounts: [account1, account2], + activeAccount: account1 + } + ] + ]) + }) + store.dispatch(StoreActions.SET_ACTIVE_ACCOUNT, { + walletId: WalletId.DEFLY, + address: account2.address + }) + expect(store.getState().wallets.get(WalletId.DEFLY)?.activeAccount).toEqual(account2) + }) + + it('dispatches a setActiveNetwork action correctly', () => { + const store = createStore(defaultState) + store.dispatch(StoreActions.SET_ACTIVE_NETWORK, { networkId: NetworkId.MAINNET }) + expect(store.getState().activeNetwork).toBe(NetworkId.MAINNET) + }) + + it('dispatches a setAccounts action correctly', () => { + const account1 = { + name: 'Defly Wallet 1', + address: 'address1' + } + const account2 = { + name: 'Defly Wallet 2', + address: 'address2' + } + const store = createStore({ + ...defaultState, + wallets: new Map([ + [ + WalletId.DEFLY, + { + accounts: [account1], + activeAccount: account1 + } + ] + ]) + }) + store.dispatch(StoreActions.SET_ACCOUNTS, { + walletId: WalletId.DEFLY, + accounts: [account1, account2] + }) + expect(store.getState().wallets.get(WalletId.DEFLY)?.accounts).toEqual([account1, account2]) + }) + + it('handles unknown actions correctly', () => { + const store = createStore(defaultState) + // @ts-expect-error Unknown action + const result = store.dispatch('unknownAction', {}) + expect(result).toBeFalsy() + expect(console.error).toHaveBeenCalledWith(`[Store] Action "unknownAction" doesn't exist`) + }) + }) + + describe('Committing Mutations', () => { + it('commits a addWallet mutation correctly', () => { + const account = { + name: 'Defly Wallet 1', + address: 'address' + } + const walletState = { + accounts: [account], + activeAccount: account + } + const store = createStore(defaultState) + store.commit(StoreMutations.ADD_WALLET, { + walletId: WalletId.DEFLY, + wallet: walletState + }) + expect(store.getState().wallets.get(WalletId.DEFLY)).toEqual(walletState) + }) + + it('commits a removeWallet mutation correctly', () => { + const account = { + name: 'Defly Wallet 1', + address: 'address' + } + const store = createStore({ + ...defaultState, + wallets: new Map([ + [ + WalletId.DEFLY, + { + accounts: [account], + activeAccount: account + } + ] + ]) + }) + expect(store.getState().wallets.get(WalletId.DEFLY)).toBeDefined() + store.commit(StoreMutations.REMOVE_WALLET, { walletId: WalletId.DEFLY }) + expect(store.getState().wallets.get(WalletId.DEFLY)).toBeUndefined() + }) + + // @todo: Should fail if walletId is not in wallets map + it('commits a setActiveWallet mutation correctly', () => { + const store = createStore(defaultState) + store.commit(StoreMutations.SET_ACTIVE_WALLET, { walletId: WalletId.DEFLY }) + expect(store.getState().activeWallet).toBe(WalletId.DEFLY) + }) + + it('commits a setActiveAccount mutation correctly', () => { + const account1 = { + name: 'Defly Wallet 1', + address: 'address1' + } + const account2 = { + name: 'Defly Wallet 2', + address: 'address2' + } + const store = createStore({ + ...defaultState, + wallets: new Map([ + [ + WalletId.DEFLY, + { + accounts: [account1, account2], + activeAccount: account1 + } + ] + ]) + }) + store.commit(StoreMutations.SET_ACTIVE_ACCOUNT, { + walletId: WalletId.DEFLY, + address: account2.address + }) + expect(store.getState().wallets.get(WalletId.DEFLY)?.activeAccount).toEqual(account2) + }) + + it('commits a setActiveNetwork mutation correctly', () => { + const store = createStore(defaultState) + store.commit(StoreMutations.SET_ACTIVE_NETWORK, { networkId: NetworkId.MAINNET }) + expect(store.getState().activeNetwork).toBe(NetworkId.MAINNET) + }) + + it('handles unknown mutations correctly', () => { + const store = createStore(defaultState) + // @ts-expect-error Unknown mutation + const result = store.commit('unknownMutation', {}) + expect(result).toBeFalsy() + expect(console.error).toHaveBeenCalledWith(`[Store] Mutation "unknownMutation" doesn't exist`) + }) + }) + + describe('State Persistence', () => { + it('saves state to local storage', () => { + const store = createStore(defaultState) + store.commit(StoreMutations.SET_ACTIVE_NETWORK, { networkId: NetworkId.MAINNET }) + store.savePersistedState() + const storedState = localStorage.getItem(LOCAL_STORAGE_KEY) + expect(storedState).toEqual(JSON.stringify(store.getState(), replacer)) + }) + + it('loads state from local storage', () => { + const persistedState = { ...defaultState, activeNetwork: NetworkId.MAINNET } + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(persistedState, replacer)) + const store = createStore(defaultState) + expect(store.getState()).toEqual(persistedState) + }) + }) + + describe('Proxy Behavior', () => { + it('updates state through proxy', () => { + const store = createStore(defaultState) + const newState = { ...store.getState(), activeNetwork: NetworkId.MAINNET } + store.commit(StoreMutations.SET_ACTIVE_NETWORK, { networkId: NetworkId.MAINNET }) + expect(store.getState()).toEqual(newState) + }) + }) +}) diff --git a/__tests__/store/utils.spec.ts b/__tests__/store/utils.spec.ts new file mode 100644 index 0000000..62f2eed --- /dev/null +++ b/__tests__/store/utils.spec.ts @@ -0,0 +1,221 @@ +import { describe, expect, it } from '@jest/globals' +import { NetworkId } from 'src/network/constants' +import { + isValidState, + isValidWalletAccount, + isValidWalletId, + isValidWalletState, + replacer, + reviver +} from 'src/store/utils' +import { WalletId } from 'src/wallets/supported/constants' + +describe('Type Guards', () => { + describe('isValidWalletId', () => { + it('returns true for a valid WalletId', () => { + expect(isValidWalletId(WalletId.DEFLY)).toBe(true) + }) + + it('returns false for an invalid WalletId', () => { + expect(isValidWalletId('foo')).toBe(false) + }) + }) + + describe('isValidWalletAccount', () => { + it('returns true for a valid WalletAccount', () => { + expect( + isValidWalletAccount({ + name: 'Defly Wallet 1', + address: 'address' + }) + ).toBe(true) + }) + + it('returns false for an invalid WalletAccount', () => { + expect(isValidWalletAccount('foo')).toBe(false) + expect(isValidWalletAccount(null)).toBe(false) + + expect( + isValidWalletAccount({ + name: 'Defly Wallet 1', + address: 123 + }) + ).toBe(false) + + expect( + isValidWalletAccount({ + address: 'address' + }) + ).toBe(false) + }) + }) + + describe('isValidWalletState', () => { + it('returns true for a valid WalletState', () => { + expect( + isValidWalletState({ + accounts: [ + { + name: 'Defly Wallet 1', + address: 'address' + } + ], + activeAccount: { + name: 'Defly Wallet 1', + address: 'address' + } + }) + ).toBe(true) + + expect( + isValidWalletState({ + accounts: [], + activeAccount: null + }) + ).toBe(true) + }) + + it('returns false for an invalid WalletState', () => { + expect(isValidWalletState('foo')).toBe(false) + expect(isValidWalletState(null)).toBe(false) + }) + + it('returns false if accounts is invalid', () => { + expect( + isValidWalletState({ + accounts: null, + activeAccount: { + name: 'Defly Wallet 1', + address: 'address' + } + }) + ).toBe(false) + + expect( + isValidWalletState({ + activeAccount: { + name: 'Defly Wallet 1', + address: 'address' + } + }) + ).toBe(false) + }) + + it('returns false if activeAccount is invalid', () => { + expect( + isValidWalletState({ + accounts: [ + { + name: 'Defly Wallet 1', + address: 'address' + } + ], + activeAccount: 'address' + }) + ).toBe(false) + + expect( + isValidWalletState({ + accounts: [ + { + name: 'Defly Wallet 1', + address: 'address' + } + ] + }) + ).toBe(false) + }) + }) + + describe('isValidState', () => { + it('returns true for a valid state', () => { + const defaultState = { + wallets: new Map(), + activeWallet: null, + activeNetwork: NetworkId.TESTNET + } + expect(isValidState(defaultState)).toBe(true) + + const state = { + wallets: new Map([ + [ + WalletId.DEFLY, + { + accounts: [ + { + name: 'Defly Wallet 1', + address: 'address' + }, + { + name: 'Defly Wallet 2', + address: 'address' + } + ], + activeAccount: { + name: 'Defly Wallet 1', + address: 'address' + } + } + ], + [ + WalletId.PERA, + { + accounts: [ + { + name: 'Pera Wallet 1', + address: 'address' + } + ], + activeAccount: { + name: 'Pera Wallet 1', + address: 'address' + } + } + ] + ]), + activeWallet: WalletId.DEFLY, + activeNetwork: NetworkId.TESTNET + } + expect(isValidState(state)).toBe(true) + }) + + it('returns false for an invalid state', () => { + expect(isValidState('foo')).toBe(false) + expect(isValidState(null)).toBe(false) + + expect( + isValidState({ + activeWallet: WalletId.DEFLY, + activeNetwork: NetworkId.TESTNET + }) + ).toBe(false) + + expect( + isValidState({ + wallets: new Map(), + activeNetwork: NetworkId.TESTNET + }) + ).toBe(false) + + expect( + isValidState({ + wallets: new Map(), + activeWallet: WalletId.DEFLY + }) + ).toBe(false) + }) + }) +}) + +describe('Serialization and Deserialization', () => { + it('correctly serializes and deserializes a state with Map', () => { + const originalState = { + wallets: new Map([[WalletId.DEFLY, { accounts: [], activeAccount: null }]]) + } + const serializedState = JSON.stringify(originalState, replacer) + const deserializedState = JSON.parse(serializedState, reviver) + + expect(deserializedState).toEqual(originalState) + expect(deserializedState.wallets instanceof Map).toBe(true) + }) +}) diff --git a/__tests__/wallets/manager.spec.ts b/__tests__/wallets/manager.spec.ts new file mode 100644 index 0000000..375b272 --- /dev/null +++ b/__tests__/wallets/manager.spec.ts @@ -0,0 +1,267 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals' +import { NetworkId } from 'src/network/constants' +import { LOCAL_STORAGE_KEY } from 'src/store/constants' +import { replacer } from 'src/store/utils' +import { WalletManager } from 'src/wallets/manager' +import { WalletId } from 'src/wallets/supported/constants' +import { DeflyWallet } from 'src/wallets/supported/defly' +import { PeraWallet } from 'src/wallets/supported/pera' + +// Suppress console output +jest.spyOn(console, 'info').mockImplementation(() => {}) +jest.spyOn(console, 'warn').mockImplementation(() => {}) +jest.spyOn(console, 'error').mockImplementation(() => {}) +jest.spyOn(console, 'groupCollapsed').mockImplementation(() => {}) + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {} + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: any) => (store[key] = value.toString()), + clear: () => (store = {}) + } +})() +Object.defineProperty(global, 'localStorage', { + value: localStorageMock +}) + +const deflyResumeSession = jest + .spyOn(DeflyWallet.prototype, 'resumeSession') + .mockImplementation(() => Promise.resolve()) +const peraResumeSession = jest + .spyOn(PeraWallet.prototype, 'resumeSession') + .mockImplementation(() => Promise.resolve()) + +describe('WalletManager', () => { + beforeEach(() => { + localStorage.clear() + }) + + describe('constructor', () => { + it('initializes with default network and wallets', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + expect(manager.wallets.length).toBe(2) + expect(manager.activeNetwork).toBe(NetworkId.TESTNET) + expect(manager.algodClient).toBeDefined() + }) + + it('initializes with custom network and wallets', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA], + network: NetworkId.MAINNET + }) + expect(manager.wallets.length).toBe(2) + expect(manager.activeNetwork).toBe(NetworkId.MAINNET) + expect(manager.algodClient).toBeDefined() + }) + + it('initializes with custom algod config', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA], + network: NetworkId.LOCALNET, + algod: { + baseServer: 'http://localhost', + port: '1234', + token: '1234', + headers: { + 'X-API-Key': '1234' + } + } + }) + + expect(manager.wallets.length).toBe(2) + expect(manager.activeNetwork).toBe(NetworkId.LOCALNET) + expect(manager.algodClient).toBeDefined() + }) + }) + + describe('initializeWallets', () => { + it('initializes wallets from string array', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + expect(manager.wallets.length).toBe(2) + }) + + it('initializes wallets from WalletIdConfig array', () => { + const manager = new WalletManager({ + wallets: [ + { + id: WalletId.DEFLY, + options: { + shouldShowSignTxnToast: false + } + }, + { + id: WalletId.WALLETCONNECT, + options: { + projectId: '1234' + } + } + ] + }) + expect(manager.wallets.length).toBe(2) + }) + + it('initializes wallets from mixed array', () => { + const manager = new WalletManager({ + wallets: [ + WalletId.DEFLY, + { + id: WalletId.WALLETCONNECT, + options: { + projectId: '1234' + } + } + ] + }) + expect(manager.wallets.length).toBe(2) + }) + + it('initializes wallets with custom metadata', () => { + const manager = new WalletManager({ + wallets: [ + { + id: WalletId.DEFLY, + metadata: { + name: 'Custom Wallet', + icon: 'icon' + } + } + ] + }) + expect(manager.wallets.length).toBe(1) + expect(manager.wallets[0]?.metadata.name).toBe('Custom Wallet') + expect(manager.wallets[0]?.metadata.icon).toBe('icon') + }) + + // @todo: Test for handling of invalid wallet configurations + }) + + describe('setActiveNetwork', () => { + it('sets active network correctly', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + manager.setActiveNetwork(NetworkId.MAINNET) + expect(manager.activeNetwork).toBe(NetworkId.MAINNET) + }) + + // @todo: Test for handling of invalid network + }) + + describe('subscribe', () => { + it('adds and removes a subscriber', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + const callback = jest.fn() + const unsubscribe = manager.subscribe(callback) + + // Trigger a state change + manager.setActiveNetwork(NetworkId.MAINNET) + + expect(callback).toHaveBeenCalled() + + unsubscribe() + // Trigger another state change + manager.setActiveNetwork(NetworkId.BETANET) + + expect(callback).toHaveBeenCalledTimes(1) // Should not be called again + }) + }) + + describe('activeWallet', () => { + const initialState = { + wallets: new Map([ + [ + WalletId.PERA, + { + accounts: [ + { + name: 'Pera Wallet 1', + address: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q' + }, + { + name: 'Pera Wallet 2', + address: 'N2C374IRX7HEX2YEQWJBTRSVRHRUV4ZSF76S54WV4COTHRUNYRCI47R3WU' + } + ], + activeAccount: { + name: 'Pera Wallet 1', + address: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q' + } + } + ] + ]), + activeWallet: WalletId.PERA, + activeNetwork: NetworkId.TESTNET + } + + beforeEach(() => { + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(initialState, replacer)) + }) + + it('returns the active wallet', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + expect(manager.activeWallet?.id).toBe(WalletId.PERA) + }) + + it('returns null if no active wallet', () => { + localStorage.clear() + + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + expect(manager.activeWallet).toBeNull() + }) + + it('returns active wallet accounts', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + expect(manager.activeWalletAccounts?.length).toBe(2) + expect(manager.activeWalletAddresses).toEqual([ + '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + 'N2C374IRX7HEX2YEQWJBTRSVRHRUV4ZSF76S54WV4COTHRUNYRCI47R3WU' + ]) + }) + + it('removes wallets in state that are not in config', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY] + }) + expect(manager.wallets.length).toBe(1) + expect(manager.wallets[0]?.id).toBe(WalletId.DEFLY) + expect(manager.activeWallet).toBeNull() + }) + }) + + describe('Transaction Signing', () => { + it('throws error if no active wallet', () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + expect(() => manager.signTransactions).toThrow() + }) + + // @todo: Tests for successful signing + }) + + describe('resumeSessions', () => { + it('resumes sessions for all wallets', async () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.PERA] + }) + await manager.resumeSessions() + + expect(deflyResumeSession).toHaveBeenCalled() + expect(peraResumeSession).toHaveBeenCalled() + }) + }) +}) diff --git a/__tests__/wallets/utils.spec.ts b/__tests__/wallets/utils.spec.ts new file mode 100644 index 0000000..c7f4993 --- /dev/null +++ b/__tests__/wallets/utils.spec.ts @@ -0,0 +1,361 @@ +import algosdk from 'algosdk' +import { describe, expect, it } from '@jest/globals' +import { + compareAccounts, + deepMerge, + formatJsonRpcRequest, + isSignedTxnObject, + isTransaction, + mergeSignedTxnsWithGroup, + normalizeTxnGroup, + shouldSignTxnObject +} from 'src/wallets/utils' + +describe('compareAccounts', () => { + it('should return true if both account lists have the same wallet accounts', () => { + const accounts1 = [ + { name: 'Acct 1', address: 'addr1' }, + { name: 'Acct 2', address: 'addr2' } + ] + const accounts2 = [ + { name: 'Acct 2', address: 'addr2' }, + { name: 'Acct 1', address: 'addr1' } + ] + + expect(compareAccounts(accounts1, accounts2)).toBe(true) + }) + + it('should return false if account lists have different wallet accounts', () => { + const accounts1 = [ + { name: 'Acct 1', address: 'addr1' }, + { name: 'Acct 2', address: 'addr2' } + ] + const accounts2 = [ + { name: 'Acct 3', address: 'addr3' }, + { name: 'Acct 1', address: 'addr1' } + ] + + expect(compareAccounts(accounts1, accounts2)).toBe(false) + }) + + it('should return false if account lists have different sizes', () => { + const accounts1 = [ + { name: 'Acct 1', address: 'addr1' }, + { name: 'Acct 2', address: 'addr2' } + ] + const accounts2 = [{ name: 'Acct 1', address: 'addr1' }] + + expect(compareAccounts(accounts1, accounts2)).toBe(false) + }) +}) + +describe('isTransaction', () => { + const transaction = new algosdk.Transaction({ + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + fee: 10, + amount: 847, + firstRound: 51, + lastRound: 61, + genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=', + genesisID: 'testnet-v1.0' + }) + + const uInt8Array = transaction.toByte() + + it('should return true if the item is a single Transaction', () => { + expect(isTransaction(transaction)).toBe(true) + }) + + it('should return true if the item is an array of transactions', () => { + expect(isTransaction([transaction, transaction])).toBe(true) + }) + + it('should return false if the item is a single Uint8Array', () => { + expect(isTransaction(uInt8Array)).toBe(false) + }) + + it('should return false if the item is an array of Uint8Arrays', () => { + expect(isTransaction([uInt8Array, uInt8Array])).toBe(false) + }) +}) + +describe('isSignedTxnObject', () => { + const transaction = new algosdk.Transaction({ + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + fee: 10, + amount: 847, + firstRound: 51, + lastRound: 61, + genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=', + genesisID: 'testnet-v1.0' + }) + + const encodedTxn = { + amt: transaction.amount, + fee: transaction.fee, + fv: transaction.firstRound, + lv: transaction.lastRound, + snd: Buffer.from(transaction.from.publicKey), + type: 'pay', + gen: transaction.genesisID, + gh: transaction.genesisHash, + grp: Buffer.from(new Uint8Array(0)) + } + + const encodedSignedTxn = { txn: encodedTxn, sig: Buffer.from('sig') } + + it('should return true if the object is a signed transaction', () => { + expect(isSignedTxnObject(encodedSignedTxn)).toBe(true) + }) + + it('should return false if the object is not a signed transaction', () => { + expect(isSignedTxnObject(encodedTxn)).toBe(false) + }) +}) + +describe('normalizeTxnGroup', () => { + it('should throw an error if the transaction group is empty', () => { + expect(() => normalizeTxnGroup([])).toThrow('Empty transaction group!') + }) + + const transaction1 = new algosdk.Transaction({ + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + fee: 10, + amount: 1000, + firstRound: 51, + lastRound: 61, + genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=', + genesisID: 'testnet-v1.0' + }) + const transaction2 = new algosdk.Transaction({ + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + fee: 10, + amount: 2000, + firstRound: 51, + lastRound: 61, + genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=', + genesisID: 'testnet-v1.0' + }) + + describe('with algosdk.Transaction[]', () => { + it('should return an array of Uint8Arrays for a single array of transactions', () => { + const txnGroup = [transaction1, transaction2] + + const normalized = normalizeTxnGroup(txnGroup) + expect(normalized).toBeInstanceOf(Array) + expect(normalized.every((item) => item instanceof Uint8Array)).toBe(true) + }) + }) + + describe('with algosdk.Transaction[][]', () => { + it('should return a flat array of Uint8Arrays for a nested array of transactions', () => { + const txnGroup = [[transaction1], [transaction2]] + + const normalized = normalizeTxnGroup(txnGroup) + expect(normalized).toBeInstanceOf(Array) + expect(normalized.length).toBe(2) + expect(normalized.every((item) => item instanceof Uint8Array)).toBe(true) + }) + }) + + const uInt8Array1 = transaction1.toByte() + const uInt8Array2 = transaction2.toByte() + + describe('with Uint8Array[]', () => { + it('should return the same array of Uint8Arrays if input is a single array of Uint8Arrays', () => { + const txnGroup = [uInt8Array1, uInt8Array2] + + const normalized = normalizeTxnGroup(txnGroup) + expect(normalized).toEqual(txnGroup) + }) + }) + + describe('with Uint8Array[][]', () => { + it('should return a flat array of Uint8Arrays for a nested array of Uint8Arrays', () => { + const txnGroup = [[uInt8Array1], [uInt8Array2]] + + const normalized = normalizeTxnGroup(txnGroup) + expect(normalized).toBeInstanceOf(Array) + expect(normalized.length).toBe(2) + expect(normalized.every((item) => item instanceof Uint8Array)).toBe(true) + }) + }) +}) + +describe('shouldSignTxnObject', () => { + const transaction = new algosdk.Transaction({ + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + fee: 10, + amount: 847, + firstRound: 51, + lastRound: 61, + genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=', + genesisID: 'testnet-v1.0' + }) + + const encodedTxn = { + amt: transaction.amount, + fee: transaction.fee, + fv: transaction.firstRound, + lv: transaction.lastRound, + snd: Buffer.from(transaction.from.publicKey), + type: 'pay', + gen: transaction.genesisID, + gh: transaction.genesisHash, + grp: Buffer.from(new Uint8Array(0)) + } + + const addresses = ['7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q'] + + it('should return true if the transaction object is not signed and indexesToSign is undefined', () => { + const indexesToSign = undefined + const idx = 0 + + expect(shouldSignTxnObject(encodedTxn, addresses, indexesToSign, idx)).toBe(true) + }) + + it('should return true if the transaction object is not signed and indexesToSign includes the index', () => { + const indexesToSign = [0] + const idx = 0 + + expect(shouldSignTxnObject(encodedTxn, addresses, indexesToSign, idx)).toBe(true) + }) + + it('should return false if the transaction object is not signed and indexesToSign does not include the index', () => { + const indexesToSign = [1] + const idx = 0 + + expect(shouldSignTxnObject(encodedTxn, addresses, indexesToSign, idx)).toBe(false) + }) + + it('should return false if the transaction object is signed', () => { + const indexesToSign = undefined + const idx = 0 + const encodedSignedTxn = { txn: encodedTxn, sig: Buffer.from('sig') } + + expect(shouldSignTxnObject(encodedSignedTxn, addresses, indexesToSign, idx)).toBe(false) + }) + + it('should return false if addresses do not include the sender address', () => { + const indexesToSign = undefined + const idx = 0 + const addresses = ['addr1', 'addr2'] + + expect(shouldSignTxnObject(encodedTxn, addresses, indexesToSign, idx)).toBe(false) + }) +}) + +describe('mergeSignedTxnsWithGroup', () => { + const transaction1 = new algosdk.Transaction({ + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + fee: 10, + amount: 1000, + firstRound: 51, + lastRound: 61, + genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=', + genesisID: 'testnet-v1.0' + }) + const transaction2 = new algosdk.Transaction({ + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + fee: 10, + amount: 2000, + firstRound: 51, + lastRound: 61, + genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=', + genesisID: 'testnet-v1.0' + }) + + const uInt8Array1 = transaction1.toByte() + const uInt8Array2 = transaction2.toByte() + + it('should merge all signed transactions into the group when all are signed', () => { + const signedTxn1 = new Uint8Array(Buffer.from('signedTxn1Str', 'base64')) + const signedTxn2 = new Uint8Array(Buffer.from('signedTxn2Str', 'base64')) + const txnGroup = [uInt8Array1, uInt8Array2] + const signedIndexes = [0, 1] + const returnGroup = true + + const merged = mergeSignedTxnsWithGroup( + [signedTxn1, signedTxn2], + txnGroup, + signedIndexes, + returnGroup + ) + expect(merged).toEqual([signedTxn1, signedTxn2]) + }) + + it('should merge all signed transactions into the group when only some are signed', () => { + const signedTxn1 = new Uint8Array(Buffer.from('signedTxn1Str', 'base64')) + const txnGroup = [uInt8Array1, uInt8Array2] + const signedIndexes = [0] + const returnGroup = true + + const merged = mergeSignedTxnsWithGroup([signedTxn1], txnGroup, signedIndexes, returnGroup) + expect(merged).toEqual([signedTxn1, uInt8Array2]) + }) + + it('should merge all signed transactions into the group when none are signed', () => { + const txnGroup = [uInt8Array1, uInt8Array2] + const returnGroup = true + + const merged = mergeSignedTxnsWithGroup([], txnGroup, [], returnGroup) + expect(merged).toEqual(txnGroup) + }) + + it('should only return signed transactions if returnGroup is false', () => { + const signedTxn1 = new Uint8Array(Buffer.from('signedTxn1Str', 'base64')) + const signedTxn2 = new Uint8Array(Buffer.from('signedTxn2Str', 'base64')) + const txnGroup = [uInt8Array1, uInt8Array2] + const returnGroup = false + + const signedIndexes1 = [0, 1] + + const merged1 = mergeSignedTxnsWithGroup( + [signedTxn1, signedTxn2], + txnGroup, + signedIndexes1, + returnGroup + ) + expect(merged1).toEqual([signedTxn1, signedTxn2]) + + const signedIndexes2 = [0] + + const merged2 = mergeSignedTxnsWithGroup([signedTxn1], txnGroup, signedIndexes2, returnGroup) + expect(merged2).toEqual([signedTxn1]) + }) +}) + +describe('deepMerge', () => { + it('should deeply merge two objects', () => { + const target = { a: 1, b: { c: 2 } } + const source = { b: { d: 3 }, e: 4 } + const expected = { a: 1, b: { c: 2, d: 3 }, e: 4 } + + expect(deepMerge(target, source)).toEqual(expected) + }) + + it('should throw an error if either argument is not an object', () => { + expect(() => deepMerge(null, {})).toThrow('Target and source must be objects') + expect(() => deepMerge({}, null)).toThrow('Target and source must be objects') + }) +}) + +describe('formatJsonRpcRequest', () => { + it('should format a JSON-RPC request with the given method and params', () => { + const method = 'algo_signTxn' + const params = [{ txn: 'base64Txn' }] + const request = formatJsonRpcRequest(method, params) + + expect(request).toHaveProperty('id') + expect(request).toHaveProperty('jsonrpc', '2.0') + expect(request).toHaveProperty('method', method) + expect(request).toHaveProperty('params', params) + }) +}) diff --git a/bun.lockb b/bun.lockb index a78400499e34476c63de3441b0447370c0d8e2b0..459a75ec177451a5b191c25a6fbc9076a82abcb4 100755 GIT binary patch delta 27428 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 OjYs4zTHxB3_J06?e-fAg delta 27301 zcmeHwcX$>>_xA2S39$5#kdQn;2u-9Uq(A}z9*UFzp(sV^0RluqfF!7Zm{27MNZcRJ zs~}A}f>NWXR6#>Eg361cB8oH-d=aF{ci%g+N#KjV`n#_0`-{DDo_o%jGiPSb)SX?H z^FNf!`=Q*tF8c25u5lS@x~7?$mYI+|e018-k=irKV>MnQ4JjW4EQRUNm!_43{t<p-6rhewg4fpg*t?upBT1NW&$80l>5| z$w}!EnpOjm^yoGqqB2(jso$%-ENBOXmw=_Hkd!haF*PYY?R6`}2*|jJY+2vSK>TGE z#}Dgu1>U2Xizr)V;7(sn3j`hm4@YL^qP`Gi?f}o?ZUj&HYYJxqY4=A&(#|l&r=U)h z?*PJ&8JS0*!1{E<4~m%itWx|MJQMZ?PfUPy;yNHRY6iI*I2EO0K~jM(o` z9-cg!O|7kfgILssK$?sWmRT+UGRrwY_K~ENVaZ9uN2F&%kM?|Fhx&I=6jr%wi0#w5 znNbL^E;&F(1Ow^Rh7i(Vey_H!x&ks9){w$&l_z^%hSw|q7 zv<|R3&;+7{nHOqGJOpI?dc|i0Y3CUrOEoM_cIHZ8(19vsdLuxWk53qtOn-c9z2+5` zvA>?I-I4mT(0&ahKR7jENMf3%y#$_>$pg|mH9}?lUg*<1Nofsi2cLwTdYd7qmxm>! zrH@4}sfpv#*g6BlWbV}($_}3lM1yB!zNr!nO-i*b#e~ZY-bcZhLE>|XLlR)92}TWz z9}1*=bV_1+(x^mjRMM!VAq|G5RENAYH6jINo3JWq@ox4ajbB9rjt!i@;Kv zwcoo!#UIxS1cgoMv#>Lf3B2Mr`DPO>q@CgzH6;1{zxmWgKXaM;V~)fz0bx;zE?-t zoC_6tfb?+`klkb$^n8H5EuRtr8Dl!j&afHC*gHUWicay;N&vLUUjR=`O-vY)-XJN> z(Qn#C%5MW{HJM`!r8%Yahm1%_9tr+hSLyT-326<6B_|9&j(Bv!jLg!g1~abNT^4^N zkS#k4i73CP-?o<(SoDO{AClAn^VMjrGz!l2j-3NNq}^S> zl90dCle0|*hEr=$jj}^ZRicH|R?p5#{&)@ZvE)VN3Ym$gW>4~Z1 zk3-J*%aFshnOERhR^-_MvgW}^&n6rXq&#&X#tt)ZYBUG}4z^iVlaj;r2y1D{m-Vhz zc&T6X=2l4Q0P}U!fJ@R-R;$t;eV;YCbg;hAT3R|oKW>GT3DDPAP09=pTsBB%lQTmW zr@qQA+j_4|aL90Q%wsxiao~1Vg&LS8`^N<9W|=T^EMi%O04uy~wAmk=AGoR3*s{U8 zZe`w=`d^YAeU|HY?sxyp9O-xTuN=z+fg_j#1?o5>bZGz077_HoL(fWQ% zFCSq1rddJdXPAyp_gZVpFV-7daX!8EO4d@JVEvM{+b2WUtX37G^)c4?3On_kRukVa zeX*6~`-nLoy_f~euwL_x4m<&l6}hrYR^Hjf)w0g}h6O&jC3Ci78#jmhed(urEb~#1 z{=5~3-%VB)elJz2=>(dO6SFz12m9vQ9IwsPxw^kJ4+&*QxW z8`8#BnR>DM7gkn1k6GSJrqitKN29$vf@@*LN5<-}SaJ0|<_Yl4V8`3B16iuKreUed z)R>dL%!+H^F>ZNVvl_H8n-!PYx~%REqRkoLT0+sZvg^^EaiJcw4Yr>hAv0`)bFAD@ zk9Qtq&8)+rv3eORF3h9%x3a=K=2EmjMk1<^S3lagZdxT9wlE)sFEP%Q@cEK`M;qRsB$7$+T; zZDmDx%rlUo=Vf|XK6RtbX!Ngk`5Kd~xJZwF*vi6heJeN8V-CW|U;}trdC=YiPTFHB z3@auGD%!Stz0idIMg*WJ#K9peNzj5t|O?+A3pI4l0q zSnr7ldF<5N5u*QWzngcdVRPAOF_ziHW4?@0>&drw4P30F?H%UtSjk|>k(-CWHOc3^ zF^zZ1=LUgmp3l7jt{~k-aQW8F+So1^aAWeh?8jqDfQipHQXad-{9fYyD!4XQnWoK3 zAW&etQcz)Q1Hq||j#l{?9J_c4_C0f`+ZkHj!=f?mnJqkeD=V&r$9$zS1{3tq=Hp_# zz|^&Kb_*oVu}mxmFOV8wYnrpTAE**^!@+D?dpq6b)JOAqy0dd#g=H7@L`hu^Jg z9cbA?Z)cgUJm!jESw^%zM#Sl0>p)ZsvsN{98CyTQ1-b+{nKQ<{xd2?#{9Hb_vRZre z5mqjKk630Kj~P;3b|*PpM}m{xCa-ghm5camkkBc`?E%vq+jVRXSlOs(GYcGDB)iFb z;8+U79)efFsoJ6Qx3=Qidc42HX1$LU-!|4eytbx2X@{~9lCm=hJ#NbyJ)&v->`*pB zL+#LS2o119Pt^q7t6OMP$4r)9Kv61b*T zyeC#aVa0Xwm{l4$w!3?x&EDXoZTfRQIO*%|i(*QE;T+*@_rLm~GFfrXndUHXY+}S= zo>>k~_CSno?;pT5v*H`X>eZ~Qc#ocH<;J7mgh?GN4RxdSudKK(9a7S6foFuHRqgk(3$>l0G~46~R1c@G?Y0Doet>5O0(X#hrFx3aon z1%ZqOl*KI@>9hu?;CA3x2>3rSri6-;uHK=d>~+HY0~||$Ofeo~;5BwHbhhl65@6`J zVs_Lva4cPMu1(%m&{z@H;knI9Fkq*k2PcDLM~Ag%V;Dta$QES|vCJMG@ABx%k6C4U z#CrEYsGS{p9ia|(===QG+L+U%ECnH{w*?`Y_V@hQmMD8?JMCJ1KM6OQxqrLd@ndILg3)cGH5`Vu6t;--qy

oLu184f-Ew4l8_OK*F{iiJv}jbiiaiuMU=GLFAcr2C0}j0bTwV*TqC-69R4l5f zbY?|c`y+6iOfg7WMw{2bsbSG2+8hL57j*EIijyly5xYdbE_L%1& z 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/src/$1', diff --git a/package.json b/package.json index 3044efb..252bde0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "scripts": { "build": "tsup", "start": "bun run build -- --watch", - "test": "node --experimental-vm-modules node_modules/.bin/jest", + "test:bun": "bun test .test.", + "test:jest": "node --experimental-vm-modules --no-warnings=ExperimentalWarning node_modules/.bin/jest", "lint": "eslint '**/*.{js,ts}'", "prettier": "prettier --check '**/*.{js,ts}'", "typecheck": "tsc --noEmit" @@ -66,6 +67,7 @@ }, "devDependencies": { "@blockshake/defly-connect": "^1.1.6", + "@jest/globals": "^29.7.0", "@perawallet/connect": "^1.3.3", "@randlabs/myalgo-connect": "^1.4.2", "@types/jest": "^29.5.10",