diff --git a/packages/use-wallet/src/__tests__/manager.test.ts b/packages/use-wallet/src/__tests__/manager.test.ts index b1ea00b5..68d05cd5 100644 --- a/packages/use-wallet/src/__tests__/manager.test.ts +++ b/packages/use-wallet/src/__tests__/manager.test.ts @@ -1,7 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' -import { NetworkId } from 'src/network' +import { NetworkConfigBuilder } from 'src/network' import { LOCAL_STORAGE_KEY, State, defaultState } from 'src/store' import { WalletManager } from 'src/manager' import { StorageAdapter } from 'src/storage' @@ -105,7 +105,8 @@ const mockDeflyWallet = new DeflyWallet({ metadata: { name: 'Defly', icon: 'icon' }, getAlgodClient: () => ({}) as any, store: mockStore, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: {} }) const mockKibisisWallet = new KibisisWallet({ @@ -113,7 +114,8 @@ const mockKibisisWallet = new KibisisWallet({ metadata: { name: 'Kibisis', icon: 'icon' }, getAlgodClient: () => ({}) as any, store: mockStore, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: {} }) describe('WalletManager', () => { @@ -134,42 +136,63 @@ describe('WalletManager', () => { }) describe('constructor', () => { - it('initializes with default network and wallets', () => { + it('initializes with default networks', () => { const manager = new WalletManager({ wallets: [WalletId.DEFLY, WalletId.KIBISIS] }) expect(manager.wallets.length).toBe(2) - expect(manager.activeNetwork).toBe(NetworkId.TESTNET) - expect(manager.algodClient).toBeDefined() + expect(manager.activeNetwork).toBe('testnet') + expect(manager.networks).toHaveProperty('mainnet') + expect(manager.networks).toHaveProperty('testnet') + expect(manager.networks).toHaveProperty('betanet') + expect(manager.networks).toHaveProperty('fnet') + expect(manager.networks).toHaveProperty('localnet') }) - it('initializes with custom network and wallets', () => { + it('initializes with custom network configurations', () => { + const networks = new NetworkConfigBuilder() + .mainnet({ + token: 'custom-token', + baseServer: 'https://custom-server.com', + headers: { 'X-API-Key': 'key' } + }) + .build() + const manager = new WalletManager({ wallets: [WalletId.DEFLY, WalletId.KIBISIS], - network: NetworkId.MAINNET + networks, + defaultNetwork: 'mainnet' + }) + + expect(manager.activeNetwork).toBe('mainnet') + expect(manager.networks.mainnet.algod).toEqual({ + token: 'custom-token', + baseServer: 'https://custom-server.com', + headers: { 'X-API-Key': 'key' } }) - expect(manager.wallets.length).toBe(2) - expect(manager.activeNetwork).toBe(NetworkId.MAINNET) - expect(manager.algodClient).toBeDefined() }) - it('initializes with custom algod config', () => { + it('initializes with custom network', () => { + const networks = new NetworkConfigBuilder() + .addNetwork('custom', { + name: 'Custom Network', + algod: { + token: 'token', + baseServer: 'https://custom-network.com', + headers: {} + }, + isTestnet: true + }) + .build() + const manager = new WalletManager({ wallets: [WalletId.DEFLY, WalletId.KIBISIS], - network: NetworkId.LOCALNET, - algod: { - baseServer: 'http://localhost', - port: '1234', - token: '1234', - headers: { - 'X-API-Key': '1234' - } - } + networks, + defaultNetwork: 'custom' }) - expect(manager.wallets.length).toBe(2) - expect(manager.activeNetwork).toBe(NetworkId.LOCALNET) - expect(manager.algodClient).toBeDefined() + expect(manager.activeNetwork).toBe('custom') + expect(manager.networks.custom).toBeDefined() }) }) @@ -238,14 +261,19 @@ describe('WalletManager', () => { const manager = new WalletManager({ wallets: [WalletId.DEFLY, WalletId.KIBISIS] }) - manager._clients = new Map([ - [WalletId.DEFLY, mockDeflyWallet], - [WalletId.KIBISIS, mockKibisisWallet] - ]) - await manager.setActiveNetwork(NetworkId.MAINNET) + await manager.setActiveNetwork('mainnet') + expect(manager.activeNetwork).toBe('mainnet') + }) - expect(manager.activeNetwork).toBe(NetworkId.MAINNET) + it('throws error for invalid network', async () => { + const manager = new WalletManager({ + wallets: [WalletId.DEFLY, WalletId.KIBISIS] + }) + + await expect(manager.setActiveNetwork('invalid')).rejects.toThrow( + 'Network "invalid" not found in network configuration' + ) }) }) @@ -258,13 +286,13 @@ describe('WalletManager', () => { const unsubscribe = manager.subscribe(callback) // Trigger a state change - await manager.setActiveNetwork(NetworkId.MAINNET) + await manager.setActiveNetwork('mainnet') expect(callback).toHaveBeenCalled() unsubscribe() // Trigger another state change - manager.setActiveNetwork(NetworkId.BETANET) + manager.setActiveNetwork('betanet') expect(callback).toHaveBeenCalledTimes(1) // Should not be called again }) @@ -279,10 +307,6 @@ describe('WalletManager', () => { { name: 'Kibisis 1', address: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q' - }, - { - name: 'Kibisis 2', - address: 'N2C374IRX7HEX2YEQWJBTRSVRHRUV4ZSF76S54WV4COTHRUNYRCI47R3WU' } ], activeAccount: { @@ -292,7 +316,7 @@ describe('WalletManager', () => { } }, activeWallet: WalletId.KIBISIS, - activeNetwork: NetworkId.BETANET, + activeNetwork: 'betanet', algodClient: new algosdk.Algodv2('', 'https://betanet-api.4160.nodely.dev/') } }) @@ -303,7 +327,7 @@ describe('WalletManager', () => { }) // expect(manager.store.state).toEqual(mockInitialState) expect(manager.activeWallet?.id).toBe(WalletId.KIBISIS) - expect(manager.activeNetwork).toBe(NetworkId.BETANET) + expect(manager.activeNetwork).toBe('betanet') }) it('returns null if no persisted state', () => { @@ -316,7 +340,7 @@ describe('WalletManager', () => { // Store initializes with default state if null is returned expect(manager.store.state).toEqual(defaultState) expect(manager.activeWallet).toBeNull() - expect(manager.activeNetwork).toBe(NetworkId.TESTNET) + expect(manager.activeNetwork).toBe('testnet') }) it('returns null and logs warning and error if persisted state is invalid', () => { @@ -341,13 +365,13 @@ describe('WalletManager', () => { const stateToSave: Omit = { wallets: {}, activeWallet: null, - activeNetwork: NetworkId.MAINNET + activeNetwork: 'mainnet' } const manager = new WalletManager({ wallets: [WalletId.DEFLY, WalletId.KIBISIS] }) - await manager.setActiveNetwork(NetworkId.MAINNET) + await manager.setActiveNetwork('mainnet') expect(vi.mocked(StorageAdapter.setItem)).toHaveBeenCalledWith( LOCAL_STORAGE_KEY, @@ -365,10 +389,6 @@ describe('WalletManager', () => { { name: 'Kibisis 1', address: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q' - }, - { - name: 'Kibisis 2', - address: 'N2C374IRX7HEX2YEQWJBTRSVRHRUV4ZSF76S54WV4COTHRUNYRCI47R3WU' } ], activeAccount: { @@ -378,7 +398,7 @@ describe('WalletManager', () => { } }, activeWallet: WalletId.KIBISIS, - activeNetwork: NetworkId.BETANET, + activeNetwork: 'betanet', algodClient: new algosdk.Algodv2('', 'https://betanet-api.4160.nodely.dev/') } }) @@ -403,10 +423,9 @@ describe('WalletManager', () => { const manager = new WalletManager({ wallets: [WalletId.DEFLY, WalletId.KIBISIS] }) - expect(manager.activeWalletAccounts?.length).toBe(2) + expect(manager.activeWalletAccounts?.length).toBe(1) expect(manager.activeWalletAddresses).toEqual([ - '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', - 'N2C374IRX7HEX2YEQWJBTRSVRHRUV4ZSF76S54WV4COTHRUNYRCI47R3WU' + '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q' ]) }) @@ -524,44 +543,44 @@ describe('WalletManager', () => { mockInitialState = { wallets: {}, activeWallet: null, - activeNetwork: NetworkId.MAINNET, - algodClient: new algosdk.Algodv2('', 'https://mainnet-api.algonode.cloud') + activeNetwork: 'mainnet', + algodClient: new algosdk.Algodv2('', 'https://mainnet-api.4160.nodely.dev') } const manager = new WalletManager({ wallets: [], - network: NetworkId.TESTNET, + defaultNetwork: 'testnet', options: { resetNetwork: true } }) - expect(manager.activeNetwork).toBe(NetworkId.TESTNET) + expect(manager.activeNetwork).toBe('testnet') }) it('uses the persisted network when resetNetwork is false', () => { mockInitialState = { wallets: {}, activeWallet: null, - activeNetwork: NetworkId.MAINNET, - algodClient: new algosdk.Algodv2('', 'https://mainnet-api.algonode.cloud') + activeNetwork: 'mainnet', + algodClient: new algosdk.Algodv2('', 'https://mainnet-api.4160.nodely.dev') } const manager = new WalletManager({ wallets: [], - network: NetworkId.TESTNET, + defaultNetwork: 'testnet', options: { resetNetwork: false } }) - expect(manager.activeNetwork).toBe(NetworkId.MAINNET) + expect(manager.activeNetwork).toBe('mainnet') }) it('uses the default network when resetNetwork is false and no persisted state exists', () => { const manager = new WalletManager({ wallets: [], - network: NetworkId.TESTNET, + defaultNetwork: 'testnet', options: { resetNetwork: false } }) - expect(manager.activeNetwork).toBe(NetworkId.TESTNET) + expect(manager.activeNetwork).toBe('testnet') }) it('preserves wallet state when resetNetwork is true, only changing the network', () => { @@ -573,18 +592,18 @@ describe('WalletManager', () => { } }, activeWallet: WalletId.PERA, - activeNetwork: NetworkId.MAINNET, - algodClient: new algosdk.Algodv2('', 'https://mainnet-api.algonode.cloud') + activeNetwork: 'mainnet', + algodClient: new algosdk.Algodv2('', 'https://mainnet-api.4160.nodely.dev') } const manager = new WalletManager({ wallets: [WalletId.PERA], - network: NetworkId.TESTNET, + defaultNetwork: 'testnet', options: { resetNetwork: true } }) - // Check that the network is forced to TESTNET - expect(manager.activeNetwork).toBe(NetworkId.TESTNET) + // Check that the network is forced to testnet + expect(manager.activeNetwork).toBe('testnet') // Check that the wallet state is preserved expect(manager.store.state.wallets[WalletId.PERA]).toEqual({ diff --git a/packages/use-wallet/src/__tests__/network.test.ts b/packages/use-wallet/src/__tests__/network.test.ts index 5afd1780..66b0b97e 100644 --- a/packages/use-wallet/src/__tests__/network.test.ts +++ b/packages/use-wallet/src/__tests__/network.test.ts @@ -1,80 +1,135 @@ -import { isAlgodConfig, isNetworkConfigMap, isValidNetworkId, NetworkId } from 'src/network' +import { NetworkConfigBuilder, isNetworkConfig, createNetworkConfig } from 'src/network' -describe('Network Type Guards', () => { - describe('isValidNetworkId', () => { - it('returns true for a valid NetworkId', () => { - expect(isValidNetworkId(NetworkId.TESTNET)).toBe(true) +describe('Network Configuration', () => { + describe('createNetworkConfig', () => { + it('returns default network configurations', () => { + const networks = createNetworkConfig() + + expect(networks).toHaveProperty('mainnet') + expect(networks).toHaveProperty('testnet') + expect(networks).toHaveProperty('betanet') + expect(networks).toHaveProperty('fnet') + expect(networks).toHaveProperty('localnet') }) - it('returns false for an invalid NetworkId', () => { - expect(isValidNetworkId('foo')).toBe(false) + it('includes correct default values for mainnet', () => { + const networks = createNetworkConfig() + + expect(networks.mainnet).toEqual({ + name: 'MainNet', + algod: { + token: '', + baseServer: 'https://mainnet-api.4160.nodely.dev', + headers: {} + }, + isTestnet: false, + genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + genesisId: 'mainnet-v1.0', + caipChainId: 'algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k' + }) }) }) - describe('isAlgodConfig', () => { - it('returns true for a valid AlgodConfig', () => { - expect( - isAlgodConfig({ - token: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - baseServer: 'http://localhost', - port: 1234, - headers: { - 'X-Foo': 'bar' - } + describe('NetworkConfigBuilder', () => { + it('allows customizing default network algod config', () => { + const networks = new NetworkConfigBuilder() + .mainnet({ + token: 'custom-token', + baseServer: 'custom-server', + headers: { 'X-API-Key': 'key' } }) - ).toBe(true) + .build() - expect( - isAlgodConfig({ - token: '', - baseServer: '' - }) - ).toBe(true) + expect(networks.mainnet.algod).toEqual({ + token: 'custom-token', + baseServer: 'custom-server', + headers: { 'X-API-Key': 'key' } + }) + // Other properties should remain unchanged + expect(networks.mainnet.name).toBe('MainNet') + expect(networks.mainnet.isTestnet).toBe(false) }) - it('returns false for an invalid AlgodConfig', () => { - expect( - isAlgodConfig({ - baseServer: '' - }) - ).toBe(false) + it('allows adding custom networks', () => { + const customNetwork = { + name: 'Custom Network', + algod: { + token: 'token', + baseServer: 'server', + headers: {} + }, + isTestnet: true + } - expect( - isAlgodConfig({ - token: '' + const networks = new NetworkConfigBuilder().addNetwork('custom', customNetwork).build() + + expect(networks.custom).toEqual(customNetwork) + // Default networks should still be present + expect(networks.mainnet).toBeDefined() + }) + + it('prevents overwriting default networks using addNetwork', () => { + const builder = new NetworkConfigBuilder() + + expect(() => + builder.addNetwork('mainnet', { + name: 'Custom MainNet', + algod: { + token: '', + baseServer: '' + } }) - ).toBe(false) + ).toThrow('Cannot add network with reserved id "mainnet"') + }) - expect( - isAlgodConfig({ - token: '', - baseServer: '', - foo: '' + it('maintains all default networks when customizing one', () => { + const networks = new NetworkConfigBuilder() + .mainnet({ + token: 'custom-token' }) - ).toBe(false) + .build() + + expect(networks.testnet).toBeDefined() + expect(networks.betanet).toBeDefined() + expect(networks.fnet).toBeDefined() + expect(networks.localnet).toBeDefined() }) }) - describe('isNetworkConfigMap', () => { - it('returns true for a valid NetworkConfigMap', () => { - const validConfigMap = { - [NetworkId.MAINNET]: { - token: '', - baseServer: '' - }, - [NetworkId.TESTNET]: { - token: '', - baseServer: '' + describe('isNetworkConfig', () => { + it('validates correct network configs', () => { + const validConfig = { + name: 'Test Network', + algod: { + token: 'token', + baseServer: 'server' } } - expect(isNetworkConfigMap(validConfigMap)).toBe(true) + expect(isNetworkConfig(validConfig)).toBe(true) + }) + + it('validates network configs with optional properties', () => { + const validConfig = { + name: 'Test Network', + algod: { + token: 'token', + baseServer: 'server' + }, + isTestnet: true, + genesisHash: 'hash', + genesisId: 'id' + } + expect(isNetworkConfig(validConfig)).toBe(true) }) - it('returns false for an invalid NetworkConfigMap', () => { + it('rejects invalid network configs', () => { + expect(isNetworkConfig(null)).toBe(false) + expect(isNetworkConfig({})).toBe(false) + expect(isNetworkConfig({ name: 'Test' })).toBe(false) expect( - isNetworkConfigMap({ - token: '', - baseServer: '' + isNetworkConfig({ + name: 'Test', + algod: { baseServer: 'server' } }) ).toBe(false) }) diff --git a/packages/use-wallet/src/__tests__/store.test.ts b/packages/use-wallet/src/__tests__/store.test.ts index 61a6dfa7..d727a816 100644 --- a/packages/use-wallet/src/__tests__/store.test.ts +++ b/packages/use-wallet/src/__tests__/store.test.ts @@ -1,6 +1,5 @@ import { Store } from '@tanstack/store' import { Algodv2 } from 'algosdk' -import { NetworkId } from 'src/network' import { State, addWallet, @@ -432,10 +431,10 @@ describe('Mutations', () => { describe('setActiveNetwork', () => { it('should set the active network', () => { - // Default network is TESTNET - expect(store.state.activeNetwork).toBe(NetworkId.TESTNET) + // Default network is testnet + expect(store.state.activeNetwork).toBe('testnet') - const networkId = NetworkId.MAINNET + const networkId = 'mainnet' const algodClient = new Algodv2('', 'https://mainnet-api.4160.nodely.dev/') setActiveNetwork(store, { networkId, algodClient }) expect(store.state.activeNetwork).toBe(networkId) @@ -565,7 +564,7 @@ describe('Type Guards', () => { const defaultState: State = { wallets: {}, activeWallet: null, - activeNetwork: NetworkId.TESTNET, + activeNetwork: 'testnet', algodClient: new Algodv2('', 'https://testnet-api.4160.nodely.dev/') } expect(isValidState(defaultState)).toBe(true) @@ -602,7 +601,7 @@ describe('Type Guards', () => { } }, activeWallet: WalletId.DEFLY, - activeNetwork: NetworkId.TESTNET, + activeNetwork: 'testnet', algodClient: new Algodv2('', 'https://testnet-api.4160.nodely.dev/') } expect(isValidState(state)).toBe(true) @@ -615,14 +614,14 @@ describe('Type Guards', () => { expect( isValidState({ activeWallet: WalletId.DEFLY, - activeNetwork: NetworkId.TESTNET + activeNetwork: 'testnet' }) ).toBe(false) expect( isValidState({ wallets: {}, - activeNetwork: NetworkId.TESTNET + activeNetwork: 'testnet' }) ).toBe(false) diff --git a/packages/use-wallet/src/__tests__/wallets/custom.test.ts b/packages/use-wallet/src/__tests__/wallets/custom.test.ts index c8372972..b7cf569e 100644 --- a/packages/use-wallet/src/__tests__/wallets/custom.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/custom.test.ts @@ -1,6 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, defaultState } from 'src/store' import { CustomProvider, CustomWallet, WalletId } from 'src/wallets' @@ -51,7 +52,8 @@ function createWalletWithStore(store: Store): CustomWallet { }, getAlgodClient: {} as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: {} }) } @@ -196,7 +198,8 @@ describe('CustomWallet', () => { metadata: {}, getAlgodClient: {} as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: {} }) vi.mocked(mockProvider.connect).mockResolvedValueOnce([account1]) @@ -282,7 +285,8 @@ describe('CustomWallet', () => { metadata: {}, getAlgodClient: {} as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: {} }) await wallet.resumeSession() @@ -331,7 +335,8 @@ describe('CustomWallet', () => { metadata: {}, getAlgodClient: {} as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: {} }) await expect(wallet.signTransactions(txnGroup, indexesToSign)).rejects.toThrowError( @@ -379,7 +384,8 @@ describe('CustomWallet', () => { metadata: {}, getAlgodClient: {} as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) await expect(wallet.transactionSigner(txnGroup, indexesToSign)).rejects.toThrowError( diff --git a/packages/use-wallet/src/__tests__/wallets/defly.test.ts b/packages/use-wallet/src/__tests__/wallets/defly.test.ts index 5d9d0dd6..611afe0d 100644 --- a/packages/use-wallet/src/__tests__/wallets/defly.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/defly.test.ts @@ -1,6 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { DeflyWallet } from 'src/wallets/defly' @@ -56,7 +57,8 @@ function createWalletWithStore(store: Store): DeflyWallet { metadata: {}, getAlgodClient: () => ({}) as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) // @ts-expect-error - Mocking the private client property diff --git a/packages/use-wallet/src/__tests__/wallets/exodus.test.ts b/packages/use-wallet/src/__tests__/wallets/exodus.test.ts index b2c43259..e101efda 100644 --- a/packages/use-wallet/src/__tests__/wallets/exodus.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/exodus.test.ts @@ -1,6 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { base64ToByteArray, byteArrayToBase64 } from 'src/utils' @@ -52,7 +53,8 @@ function createWalletWithStore(store: Store): ExodusWallet { metadata: {}, getAlgodClient: () => ({}) as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) } diff --git a/packages/use-wallet/src/__tests__/wallets/kibisis.test.ts b/packages/use-wallet/src/__tests__/wallets/kibisis.test.ts index 4e78afb9..c6379604 100644 --- a/packages/use-wallet/src/__tests__/wallets/kibisis.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/kibisis.test.ts @@ -10,6 +10,7 @@ import { import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { defaultState, LOCAL_STORAGE_KEY, State } from 'src/store' import { WalletId } from 'src/wallets' @@ -60,7 +61,8 @@ function createWalletWithStore(store: Store): KibisisWallet { }) }) as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) } diff --git a/packages/use-wallet/src/__tests__/wallets/kmd.test.ts b/packages/use-wallet/src/__tests__/wallets/kmd.test.ts index 86e7ed7b..b4751b9f 100644 --- a/packages/use-wallet/src/__tests__/wallets/kmd.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/kmd.test.ts @@ -1,6 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { KmdWallet } from 'src/wallets/kmd' @@ -53,7 +54,8 @@ function createWalletWithStore(store: Store): KmdWallet { metadata: {}, getAlgodClient: {} as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) } diff --git a/packages/use-wallet/src/__tests__/wallets/liquid.test.ts b/packages/use-wallet/src/__tests__/wallets/liquid.test.ts index 53db9c88..48726068 100644 --- a/packages/use-wallet/src/__tests__/wallets/liquid.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/liquid.test.ts @@ -6,6 +6,7 @@ import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { LiquidWallet } from 'src/wallets/liquid' import { WalletId } from 'src/wallets/types' +import { DEFAULT_NETWORKS } from 'src/network' // Mock logger vi.mock('src/logger', () => ({ @@ -56,7 +57,8 @@ function createWalletWithStore(store: Store): LiquidWallet { metadata: {}, getAlgodClient: () => ({}) as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) } diff --git a/packages/use-wallet/src/__tests__/wallets/lute.test.ts b/packages/use-wallet/src/__tests__/wallets/lute.test.ts index cd239ceb..74f8ea0c 100644 --- a/packages/use-wallet/src/__tests__/wallets/lute.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/lute.test.ts @@ -44,6 +44,7 @@ vi.mock('lute-connect', () => { // Import the mocked module import LuteConnect from 'lute-connect' +import { DEFAULT_NETWORKS } from 'src/network' function createWalletWithStore(store: Store): LuteWallet { return new LuteWallet({ @@ -60,7 +61,8 @@ function createWalletWithStore(store: Store): LuteWallet { }) }) as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) } diff --git a/packages/use-wallet/src/__tests__/wallets/magic.test.ts b/packages/use-wallet/src/__tests__/wallets/magic.test.ts index 330ce305..63d70bf5 100644 --- a/packages/use-wallet/src/__tests__/wallets/magic.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/magic.test.ts @@ -1,6 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { base64ToByteArray, byteArrayToBase64 } from 'src/utils' @@ -58,7 +59,8 @@ function createWalletWithStore(store: Store): MagicAuth { metadata: {}, getAlgodClient: {} as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) } diff --git a/packages/use-wallet/src/__tests__/wallets/mnemonic.test.ts b/packages/use-wallet/src/__tests__/wallets/mnemonic.test.ts index 498716ce..2f33f1a8 100644 --- a/packages/use-wallet/src/__tests__/wallets/mnemonic.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/mnemonic.test.ts @@ -2,7 +2,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' -import { NetworkId } from 'src/network' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { LOCAL_STORAGE_MNEMONIC_KEY, MnemonicWallet } from 'src/wallets/mnemonic' @@ -41,7 +41,8 @@ function createWalletWithStore(store: Store, persistToStorage = false): M metadata: {}, getAlgodClient: {} as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) } @@ -61,7 +62,7 @@ describe('MnemonicWallet', () => { address: TEST_ADDRESS } - const setActiveNetwork = (networkId: NetworkId) => { + const setActiveNetwork = (networkId: string) => { store.setState((state) => { return { ...state, @@ -138,9 +139,9 @@ describe('MnemonicWallet', () => { }) it('should throw an error if active network is MainNet', async () => { - setActiveNetwork(NetworkId.MAINNET) + setActiveNetwork('mainnet') - await expect(wallet.connect()).rejects.toThrow('MainNet active network detected. Aborting.') + await expect(wallet.connect()).rejects.toThrow('Production network detected. Aborting.') expect(store.state.wallets[WalletId.MNEMONIC]).toBeUndefined() expect(wallet.isConnected).toBe(false) }) @@ -229,11 +230,9 @@ describe('MnemonicWallet', () => { }) it('should throw an error if active network is MainNet', async () => { - setActiveNetwork(NetworkId.MAINNET) + setActiveNetwork('mainnet') - await expect(wallet.resumeSession()).rejects.toThrow( - 'MainNet active network detected. Aborting.' - ) + await expect(wallet.resumeSession()).rejects.toThrow('Production network detected. Aborting.') expect(store.state.wallets[WalletId.MNEMONIC]).toBeUndefined() expect(wallet.isConnected).toBe(false) }) @@ -333,10 +332,10 @@ describe('MnemonicWallet', () => { }) it('should throw an error if active network is MainNet', async () => { - setActiveNetwork(NetworkId.MAINNET) + setActiveNetwork('mainnet') await expect(wallet.signTransactions([])).rejects.toThrow( - 'MainNet active network detected. Aborting.' + 'Production network detected. Aborting.' ) expect(store.state.wallets[WalletId.MNEMONIC]).toBeUndefined() expect(wallet.isConnected).toBe(false) @@ -356,10 +355,10 @@ describe('MnemonicWallet', () => { }) it('should throw an error if active network is MainNet', async () => { - setActiveNetwork(NetworkId.MAINNET) + setActiveNetwork('mainnet') await expect(wallet.transactionSigner([], [])).rejects.toThrow( - 'MainNet active network detected. Aborting.' + 'Production network detected. Aborting.' ) expect(store.state.wallets[WalletId.MNEMONIC]).toBeUndefined() expect(wallet.isConnected).toBe(false) diff --git a/packages/use-wallet/src/__tests__/wallets/pera.test.ts b/packages/use-wallet/src/__tests__/wallets/pera.test.ts index 2d1d1cd6..ed4f2473 100644 --- a/packages/use-wallet/src/__tests__/wallets/pera.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/pera.test.ts @@ -1,6 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { PeraWallet } from 'src/wallets/pera' @@ -56,7 +57,8 @@ function createWalletWithStore(store: Store): PeraWallet { metadata: {}, getAlgodClient: () => ({}) as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) // @ts-expect-error - Mocking the private client property diff --git a/packages/use-wallet/src/__tests__/wallets/pera2.test.ts b/packages/use-wallet/src/__tests__/wallets/pera2.test.ts index e92a9265..98a691f4 100644 --- a/packages/use-wallet/src/__tests__/wallets/pera2.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/pera2.test.ts @@ -1,6 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { logger } from 'src/logger' +import { DEFAULT_NETWORKS } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { PeraWallet } from 'src/wallets/pera2' @@ -54,7 +55,8 @@ function createWalletWithStore(store: Store): PeraWallet { metadata: {}, getAlgodClient: () => ({}) as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) // @ts-expect-error - Mocking the private client property diff --git a/packages/use-wallet/src/__tests__/wallets/walletconnect.test.ts b/packages/use-wallet/src/__tests__/wallets/walletconnect.test.ts index 2ce7225c..9e2607fb 100644 --- a/packages/use-wallet/src/__tests__/wallets/walletconnect.test.ts +++ b/packages/use-wallet/src/__tests__/wallets/walletconnect.test.ts @@ -2,7 +2,7 @@ import { Store } from '@tanstack/store' import { ModalCtrl } from '@walletconnect/modal-core' import algosdk from 'algosdk' import { logger } from 'src/logger' -import { NetworkId, caipChainId } from 'src/network' +import { DEFAULT_NETWORKS, NetworkConfig } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, State, WalletState, defaultState } from 'src/store' import { base64ToByteArray, byteArrayToBase64 } from 'src/utils' @@ -58,11 +58,14 @@ vi.spyOn(ModalCtrl, 'subscribe').mockImplementation((_callback: (state: any) => return () => {} }) -function createMockSession(accounts: string[] = []): SessionTypes.Struct { +function createMockSession( + accounts: string[] = [], + networks: Record +): SessionTypes.Struct { return { namespaces: { algorand: { - accounts: accounts.map((address) => `${caipChainId[NetworkId.TESTNET]}:${address}`), + accounts: accounts.map((address) => `${networks.testnet.caipChainId}:${address}`), methods: ['algo_signTxn'], events: [] } @@ -77,11 +80,7 @@ function createMockSession(accounts: string[] = []): SessionTypes.Struct { controller: '', requiredNamespaces: { algorand: { - chains: [ - caipChainId[NetworkId.MAINNET]!, - caipChainId[NetworkId.TESTNET]!, - caipChainId[NetworkId.BETANET]! - ], + chains: [networks.testnet.caipChainId!], methods: ['algo_signTxn'], events: [] } @@ -117,7 +116,8 @@ function createWalletWithStore(store: Store): WalletConnect { metadata: {}, getAlgodClient: () => ({}) as any, store, - subscribe: vi.fn() + subscribe: vi.fn(), + networks: DEFAULT_NETWORKS }) } @@ -176,7 +176,10 @@ describe('WalletConnect', () => { describe('connect', () => { it('should initialize client, return accounts, and update store', async () => { - const mockSession = createMockSession([account1.address, account2.address]) + const mockSession = createMockSession( + [account1.address, account2.address], + wallet.networkConfig + ) mockSignClient.connect.mockResolvedValueOnce({ uri: 'mock-uri', approval: vi.fn().mockResolvedValue(mockSession) @@ -193,7 +196,7 @@ describe('WalletConnect', () => { }) it('should throw an error if no URI is returned', async () => { - const mockSession = createMockSession([]) + const mockSession = createMockSession([], wallet.networkConfig) mockSignClient.connect.mockResolvedValueOnce({ approval: vi.fn().mockResolvedValue(mockSession) }) @@ -205,7 +208,7 @@ describe('WalletConnect', () => { }) it('should throw an error if an empty array is returned', async () => { - const mockSession = createMockSession([]) + const mockSession = createMockSession([], wallet.networkConfig) mockSignClient.connect.mockResolvedValueOnce({ uri: 'mock-uri', approval: vi.fn().mockResolvedValue(mockSession) @@ -218,8 +221,8 @@ describe('WalletConnect', () => { }) it('should use the active chain when connecting', async () => { - store.setState((state) => ({ ...state, activeNetwork: NetworkId.TESTNET })) - const mockSession = createMockSession([account1.address]) + store.setState((state) => ({ ...state, activeNetwork: 'testnet' })) + const mockSession = createMockSession([account1.address], wallet.networkConfig) mockSignClient.connect.mockResolvedValueOnce({ uri: 'mock-uri', approval: vi.fn().mockResolvedValue(mockSession) @@ -241,7 +244,10 @@ describe('WalletConnect', () => { describe('disconnect', () => { it('should disconnect client and remove wallet from store', async () => { - const mockSession = createMockSession([account1.address, account2.address]) + const mockSession = createMockSession( + [account1.address, account2.address], + wallet.networkConfig + ) mockSignClient.connect.mockResolvedValueOnce({ uri: 'mock-uri', approval: vi.fn().mockResolvedValue(mockSession) @@ -278,7 +284,7 @@ describe('WalletConnect', () => { wallet = createWalletWithStore(store) - const mockSession = createMockSession([account1.address]) + const mockSession = createMockSession([account1.address], wallet.networkConfig) mockSignClient.session.get.mockImplementationOnce(() => mockSession) const mockSessionKey = 'mockSessionKey' @@ -335,7 +341,7 @@ describe('WalletConnect', () => { } // Client only returns 'GD64YI' on reconnect - const mockSession = createMockSession(newAccounts) + const mockSession = createMockSession(newAccounts, wallet.networkConfig) mockSignClient.session.get.mockImplementationOnce(() => mockSession) await wallet.resumeSession() @@ -391,7 +397,10 @@ describe('WalletConnect', () => { beforeEach(async () => { // Mock two connected accounts - const mockSession = createMockSession([account1.address, account2.address]) + const mockSession = createMockSession( + [account1.address, account2.address], + wallet.networkConfig + ) mockSignClient.connect.mockResolvedValueOnce({ uri: 'mock-uri', approval: vi.fn().mockResolvedValue(mockSession) @@ -582,8 +591,8 @@ describe('WalletConnect', () => { }) it('should use the active chain when signing transactions', async () => { - store.setState((state) => ({ ...state, activeNetwork: NetworkId.MAINNET })) - const mockSession = createMockSession([account1.address]) + store.setState((state) => ({ ...state, activeNetwork: 'mainnet' })) + const mockSession = createMockSession([account1.address], wallet.networkConfig) mockSignClient.connect.mockResolvedValueOnce({ uri: 'mock-uri', approval: vi.fn().mockResolvedValue(mockSession) @@ -617,18 +626,18 @@ describe('WalletConnect', () => { describe('activeChainId', () => { it('should return the correct CAIP-2 chain ID for the active network', () => { - store.setState((state) => ({ ...state, activeNetwork: NetworkId.MAINNET })) - expect(wallet.activeChainId).toBe(caipChainId[NetworkId.MAINNET]) + store.setState((state) => ({ ...state, activeNetwork: 'mainnet' })) + expect(wallet.activeChainId).toBe(wallet.networkConfig.mainnet.caipChainId) - store.setState((state) => ({ ...state, activeNetwork: NetworkId.TESTNET })) - expect(wallet.activeChainId).toBe(caipChainId[NetworkId.TESTNET]) + store.setState((state) => ({ ...state, activeNetwork: 'testnet' })) + expect(wallet.activeChainId).toBe(wallet.networkConfig.testnet.caipChainId) - store.setState((state) => ({ ...state, activeNetwork: NetworkId.BETANET })) - expect(wallet.activeChainId).toBe(caipChainId[NetworkId.BETANET]) + store.setState((state) => ({ ...state, activeNetwork: 'betanet' })) + expect(wallet.activeChainId).toBe(wallet.networkConfig.betanet.caipChainId) }) it('should log a warning and return an empty string if no CAIP-2 chain ID is found', () => { - store.setState((state) => ({ ...state, activeNetwork: 'invalid-network' as NetworkId })) + store.setState((state) => ({ ...state, activeNetwork: 'invalid-network' })) expect(wallet.activeChainId).toBe('') expect(mockLogger.warn).toHaveBeenCalledWith( 'No CAIP-2 chain ID found for network: invalid-network' diff --git a/packages/use-wallet/src/manager.ts b/packages/use-wallet/src/manager.ts index fd927868..d95bfcca 100644 --- a/packages/use-wallet/src/manager.ts +++ b/packages/use-wallet/src/manager.ts @@ -1,13 +1,7 @@ import { Store } from '@tanstack/store' import algosdk from 'algosdk' import { Logger, LogLevel, logger } from 'src/logger' -import { - createDefaultNetworkConfig, - isNetworkConfigMap, - NetworkId, - type NetworkConfig, - type NetworkConfigMap -} from 'src/network' +import { createNetworkConfig, isNetworkConfig, type NetworkConfig } from 'src/network' import { StorageAdapter } from 'src/storage' import { defaultState, @@ -18,7 +12,7 @@ import { setActiveWallet, type State } from 'src/store' -import { createWalletMap, deepMerge } from 'src/utils' +import { createWalletMap } from 'src/utils' import type { BaseWallet } from 'src/wallets/base' import type { SupportedWallet, @@ -38,8 +32,8 @@ export interface WalletManagerOptions { export interface WalletManagerConfig { wallets?: SupportedWallet[] - network?: NetworkId - algod?: NetworkConfig + networks?: Record + defaultNetwork?: string options?: WalletManagerOptions } @@ -47,7 +41,7 @@ export type PersistedState = Omit export class WalletManager { public _clients: Map = new Map() - public networkConfig: NetworkConfigMap + public networkConfig: Record public store: Store public subscribe: (callback: (state: State) => void) => () => void public options: { resetNetwork: boolean } @@ -56,8 +50,8 @@ export class WalletManager { constructor({ wallets = [], - network = NetworkId.TESTNET, - algod = {}, + networks, + defaultNetwork = 'testnet', options = {} }: WalletManagerConfig = {}) { // Initialize scoped logger @@ -65,13 +59,13 @@ export class WalletManager { this.logger.debug('Initializing WalletManager with config:', { wallets, - network, - algod, + networks, + defaultNetwork, options }) // Initialize network config - this.networkConfig = this.initNetworkConfig(network, algod) + this.networkConfig = this.initNetworkConfig(networks) // Initialize options this.options = { resetNetwork: options.resetNetwork || false } @@ -81,8 +75,13 @@ export class WalletManager { // Set active network const activeNetwork = this.options.resetNetwork - ? network - : persistedState?.activeNetwork || network + ? defaultNetwork + : persistedState?.activeNetwork || defaultNetwork + + // Validate active network exists in config + if (!this.networkConfig[activeNetwork]) { + throw new Error(`Network "${activeNetwork}" not found in network configuration`) + } // Create Algod client for active network const algodClient = this.createAlgodClient(activeNetwork) @@ -212,7 +211,8 @@ export class WalletManager { options: walletOptions as any, getAlgodClient: this.getAlgodClient, store: this.store, - subscribe: this.subscribe + subscribe: this.subscribe, + networks: this.networkConfig }) this._clients.set(walletId, walletInstance) @@ -260,27 +260,35 @@ export class WalletManager { // ---------- Network ----------------------------------------------- // - private initNetworkConfig(network: NetworkId, config: NetworkConfig): NetworkConfigMap { - this.logger.info('Initializing network...') + private initNetworkConfig( + networks?: Record + ): Record { + this.logger.info('Initializing network configuration...') - let networkConfig = createDefaultNetworkConfig() + // Use provided networks or create default config with all official networks + const config = networks || createNetworkConfig() - if (isNetworkConfigMap(config)) { - // Config for multiple networks - networkConfig = deepMerge(networkConfig, config) - } else { - // Config for single (active) network - networkConfig[network] = deepMerge(networkConfig[network], config) + // Validate network configurations + for (const [id, network] of Object.entries(config)) { + if (!isNetworkConfig(network)) { + throw new Error(`Invalid network configuration for "${id}"`) + } } - this.logger.debug('Algodv2 config:', networkConfig) + this.logger.debug('Network configuration:', config) - return networkConfig + return config } - private createAlgodClient(networkId: NetworkId): algosdk.Algodv2 { + private createAlgodClient(networkId: string): algosdk.Algodv2 { this.logger.info(`Creating Algodv2 client for ${networkId}...`) - const { token = '', baseServer, port = '', headers = {} } = this.networkConfig[networkId] + + const network = this.networkConfig[networkId] + if (!network) { + throw new Error(`Network "${networkId}" not found in network configuration`) + } + + const { token = '', baseServer, port = '', headers = {} } = network.algod return new algosdk.Algodv2(token, baseServer, port, headers) } @@ -288,21 +296,29 @@ export class WalletManager { return this.algodClient } - public setActiveNetwork = async (networkId: NetworkId): Promise => { + public setActiveNetwork = async (networkId: string): Promise => { if (this.activeNetwork === networkId) { return } + if (!this.networkConfig[networkId]) { + throw new Error(`Network "${networkId}" not found in network configuration`) + } + const algodClient = this.createAlgodClient(networkId) setActiveNetwork(this.store, { networkId, algodClient }) - this.logger.info(`✅ Active network set to ${networkId}.`) + this.logger.info(`✅ Active network set to ${networkId}`) } - public get activeNetwork(): NetworkId { + public get activeNetwork(): string { return this.store.state.activeNetwork } + public get networks(): Record { + return { ...this.networkConfig } + } + // ---------- Active Wallet ----------------------------------------- // public get activeWallet(): BaseWallet | null { diff --git a/packages/use-wallet/src/network.ts b/packages/use-wallet/src/network.ts index 174bb7f1..887d0303 100644 --- a/packages/use-wallet/src/network.ts +++ b/packages/use-wallet/src/network.ts @@ -1,19 +1,5 @@ import algosdk from 'algosdk' -export enum NetworkId { - MAINNET = 'mainnet', - TESTNET = 'testnet', - BETANET = 'betanet', - FNET = 'fnet', - LOCALNET = 'localnet', - VOIMAIN = 'voimain', - ARAMIDMAIN = 'aramidmain' -} - -export function isValidNetworkId(networkId: any): networkId is NetworkId { - return Object.values(NetworkId).includes(networkId) -} - export interface AlgodConfig { token: string | algosdk.AlgodTokenHeader | algosdk.CustomTokenHeader | algosdk.BaseHTTPClient baseServer: string @@ -21,69 +7,166 @@ export interface AlgodConfig { headers?: Record } -export function isAlgodConfig(config: any): config is AlgodConfig { - if (typeof config !== 'object') return false +export interface NetworkConfig { + name: string + algod: AlgodConfig + genesisHash?: string + genesisId?: string + isTestnet?: boolean + caipChainId?: string +} - for (const key of Object.keys(config)) { - if (!['token', 'baseServer', 'port', 'headers'].includes(key)) return false +// Default configurations +export const DEFAULT_NETWORKS: Record = { + mainnet: { + name: 'MainNet', + algod: { + token: '', + baseServer: 'https://mainnet-api.4160.nodely.dev', + headers: {} + }, + isTestnet: false, + genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + genesisId: 'mainnet-v1.0', + caipChainId: 'algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k' + }, + testnet: { + name: 'TestNet', + algod: { + token: '', + baseServer: 'https://testnet-api.4160.nodely.dev', + headers: {} + }, + isTestnet: true, + genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=', + genesisId: 'testnet-v1.0', + caipChainId: 'algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDe' + }, + betanet: { + name: 'BetaNet', + algod: { + token: '', + baseServer: 'https://betanet-api.4160.nodely.dev', + headers: {} + }, + isTestnet: true, + genesisHash: 'mFgazF-2uRS1tMiL9dsj01hJGySEmPN2OvOTQHJ6iQg=', + genesisId: 'betanet-v1.0', + caipChainId: 'algorand:mFgazF-2uRS1tMiL9dsj01hJGySEmPN2' + }, + fnet: { + name: 'FNet', + algod: { + token: '', + baseServer: 'https://fnet-api.4160.nodely.dev', + headers: {} + }, + isTestnet: true, + genesisHash: 'kUt08LxeVAAGHnh4JoAoAMM9ql_hBwSoRrQQKWSVgxk=', + genesisId: 'fnet-v1.0', + caipChainId: 'algorand:kUt08LxeVAAGHnh4JoAoAMM9ql_hBwSo' + }, + localnet: { + name: 'LocalNet', + algod: { + token: 'a'.repeat(64), + baseServer: 'http://localhost', + port: 4001, + headers: {} + }, + isTestnet: true } - - return ( - typeof config.token === 'string' && - typeof config.baseServer === 'string' && - ['string', 'number', 'undefined'].includes(typeof config.port) && - ['object', 'undefined'].includes(typeof config.headers) - ) } -export type NetworkConfigMap = Record +export class NetworkConfigBuilder { + private networks: Map -export function isNetworkConfigMap(config: NetworkConfig): config is NetworkConfigMap { - const networkKeys = Object.values(NetworkId) as string[] - return Object.keys(config).some((key) => networkKeys.includes(key)) -} + constructor() { + // Initialize with default networks + this.networks = new Map(Object.entries(DEFAULT_NETWORKS)) + } -export type NetworkConfig = Partial | Partial>> + // Methods to customize default networks + mainnet(config: Partial) { + this.networks.set('mainnet', { + ...DEFAULT_NETWORKS.mainnet, + algod: { ...DEFAULT_NETWORKS.mainnet.algod, ...config } + }) + return this + } -export const nodeServerMap = { - [NetworkId.MAINNET]: 'https://mainnet-api.4160.nodely.dev', - [NetworkId.TESTNET]: 'https://testnet-api.4160.nodely.dev', - [NetworkId.BETANET]: 'https://betanet-api.4160.nodely.dev', - [NetworkId.FNET]: 'https://fnet-api.4160.nodely.dev', - [NetworkId.VOIMAIN]: 'https://mainnet-api.voi.nodely.dev', - [NetworkId.ARAMIDMAIN]: 'https://algod.aramidmain.a-wallet.net' -} + testnet(config: Partial) { + this.networks.set('testnet', { + ...DEFAULT_NETWORKS.testnet, + algod: { ...DEFAULT_NETWORKS.testnet.algod, ...config } + }) + return this + } -export function createDefaultNetworkConfig(): NetworkConfigMap { - const localnetConfig: AlgodConfig = { - token: 'a'.repeat(64), - baseServer: 'http://localhost', - port: 4001, - headers: {} + betanet(config: Partial) { + this.networks.set('betanet', { + ...DEFAULT_NETWORKS.betanet, + algod: { ...DEFAULT_NETWORKS.betanet.algod, ...config } + }) + return this } - return Object.values(NetworkId).reduce((configMap, value) => { - const network = value as NetworkId - const isLocalnet = network === NetworkId.LOCALNET - - configMap[network] = isLocalnet - ? localnetConfig - : { - token: '', - baseServer: nodeServerMap[network], - port: '', - headers: {} - } - - return configMap - }, {} as NetworkConfigMap) + fnet(config: Partial) { + this.networks.set('fnet', { + ...DEFAULT_NETWORKS.fnet, + algod: { ...DEFAULT_NETWORKS.fnet.algod, ...config } + }) + return this + } + + localnet(config: Partial) { + this.networks.set('localnet', { + ...DEFAULT_NETWORKS.localnet, + algod: { ...DEFAULT_NETWORKS.localnet.algod, ...config } + }) + return this + } + + // Method to add custom networks (still needs full NetworkConfig) + addNetwork(id: string, config: NetworkConfig) { + if (DEFAULT_NETWORKS[id]) { + throw new Error( + `Cannot add network with reserved id "${id}". Use the ${id}() method instead.` + ) + } + this.networks.set(id, config) + return this + } + + build() { + return Object.fromEntries(this.networks) + } +} + +// Create a default builder with common presets +export const createNetworkConfig = () => new NetworkConfigBuilder().build() + +// Type guard for runtime validation +export function isNetworkConfig(config: unknown): config is NetworkConfig { + if (typeof config !== 'object' || config === null) return false + + const { name, algod, isTestnet, genesisHash, genesisId } = config as NetworkConfig + + return ( + typeof name === 'string' && + typeof algod === 'object' && + typeof algod.token === 'string' && + typeof algod.baseServer === 'string' && + (isTestnet === undefined || typeof isTestnet === 'boolean') && + (genesisHash === undefined || typeof genesisHash === 'string') && + (genesisId === undefined || typeof genesisId === 'string') + ) } -export const caipChainId: Partial> = { - [NetworkId.MAINNET]: 'algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k', - [NetworkId.TESTNET]: 'algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDe', - [NetworkId.BETANET]: 'algorand:mFgazF-2uRS1tMiL9dsj01hJGySEmPN2', - [NetworkId.FNET]: 'algorand:kUt08LxeVAAGHnh4JoAoAMM9ql_hBwSo', - [NetworkId.VOIMAIN]: 'algorand:r20fSQI8gWe_kFZziNonSPCXLwcQmH_n', - [NetworkId.ARAMIDMAIN]: 'algorand:PgeQVJJgx_LYKJfIEz7dbfNPuXmDyJ-O' +export enum NetworkId { + MAINNET = 'mainnet', + TESTNET = 'testnet', + BETANET = 'betanet', + FNET = 'fnet', + LOCALNET = 'localnet' } diff --git a/packages/use-wallet/src/store.ts b/packages/use-wallet/src/store.ts index c2aa8f41..69f644fb 100644 --- a/packages/use-wallet/src/store.ts +++ b/packages/use-wallet/src/store.ts @@ -1,6 +1,5 @@ import algosdk from 'algosdk' import { logger } from 'src/logger' -import { NetworkId, isValidNetworkId } from 'src/network' import { WalletId, type WalletAccount } from 'src/wallets/types' import type { Store } from '@tanstack/store' @@ -14,14 +13,14 @@ export type WalletStateMap = Partial> export interface State { wallets: WalletStateMap activeWallet: WalletId | null - activeNetwork: NetworkId + activeNetwork: string algodClient: algosdk.Algodv2 } export const defaultState: State = { wallets: {}, activeWallet: null, - activeNetwork: NetworkId.TESTNET, + activeNetwork: 'testnet', algodClient: new algosdk.Algodv2('', 'https://testnet-api.4160.nodely.dev/') } @@ -146,7 +145,7 @@ export function setAccounts( export function setActiveNetwork( store: Store, - { networkId, algodClient }: { networkId: NetworkId; algodClient: algosdk.Algodv2 } + { networkId, algodClient }: { networkId: string; algodClient: algosdk.Algodv2 } ) { store.setState((state) => ({ ...state, @@ -187,7 +186,7 @@ export function isValidState(state: any): state is State { if (!isValidWalletId(walletId) || !isValidWalletState(wallet)) return false } if (state.activeWallet !== null && !isValidWalletId(state.activeWallet)) return false - if (!isValidNetworkId(state.activeNetwork)) return false + if (typeof state.activeNetwork !== 'string') return false return true } diff --git a/packages/use-wallet/src/utils.ts b/packages/use-wallet/src/utils.ts index 3bb494d3..41d466a7 100644 --- a/packages/use-wallet/src/utils.ts +++ b/packages/use-wallet/src/utils.ts @@ -137,6 +137,7 @@ export function formatJsonRpcRequest(method: string, params: T): JsonRp } } +// @todo: remove export function deepMerge(target: any, source: any): any { const isObject = (obj: any) => obj && typeof obj === 'object' diff --git a/packages/use-wallet/src/wallets/base.ts b/packages/use-wallet/src/wallets/base.ts index aabdddd3..a53d9c45 100644 --- a/packages/use-wallet/src/wallets/base.ts +++ b/packages/use-wallet/src/wallets/base.ts @@ -3,8 +3,8 @@ import { StorageAdapter } from 'src/storage' import { setActiveWallet, setActiveAccount, removeWallet, type State } from 'src/store' import type { Store } from '@tanstack/store' import type algosdk from 'algosdk' -import type { NetworkId } from 'src/network' import type { WalletAccount, WalletConstructor, WalletId, WalletMetadata } from 'src/wallets/types' +import { NetworkConfig } from 'src/network' interface WalletConstructorType { new (...args: any[]): BaseWallet @@ -14,6 +14,7 @@ interface WalletConstructorType { export abstract class BaseWallet { readonly id: WalletId readonly metadata: WalletMetadata + protected readonly networks: Record protected store: Store protected getAlgodClient: () => algosdk.Algodv2 @@ -27,12 +28,14 @@ export abstract class BaseWallet { metadata, store, subscribe, - getAlgodClient + getAlgodClient, + networks }: WalletConstructor) { this.id = id this.store = store this.subscribe = subscribe this.getAlgodClient = getAlgodClient + this.networks = networks const ctor = this.constructor as WalletConstructorType this.metadata = { ...ctor.defaultMetadata, ...metadata } @@ -109,7 +112,7 @@ export abstract class BaseWallet { return this.activeAccount?.address ?? null } - public get activeNetwork(): NetworkId { + public get activeNetwork(): string { const state = this.store.state return state.activeNetwork } @@ -125,6 +128,14 @@ export abstract class BaseWallet { return state.activeWallet === this.id } + protected get activeNetworkConfig(): NetworkConfig { + const config = this.networks[this.activeNetwork] + if (!config) { + throw new Error(`No configuration found for network: ${this.activeNetwork}`) + } + return config + } + // ---------- Protected Methods ------------------------------------- // protected onDisconnect = (): void => { diff --git a/packages/use-wallet/src/wallets/custom.ts b/packages/use-wallet/src/wallets/custom.ts index 4418edbc..ee1d4e1a 100644 --- a/packages/use-wallet/src/wallets/custom.ts +++ b/packages/use-wallet/src/wallets/custom.ts @@ -38,10 +38,11 @@ export class CustomWallet extends BaseWallet { store, subscribe, getAlgodClient, + networks, options, metadata = {} }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) if (!options?.provider) { this.logger.error('Missing required option: provider') throw new Error('Missing required option: provider') diff --git a/packages/use-wallet/src/wallets/defly.ts b/packages/use-wallet/src/wallets/defly.ts index 67043294..1e844405 100644 --- a/packages/use-wallet/src/wallets/defly.ts +++ b/packages/use-wallet/src/wallets/defly.ts @@ -33,9 +33,10 @@ export class DeflyWallet extends BaseWallet { subscribe, getAlgodClient, options = {}, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) this.options = options this.store = store } diff --git a/packages/use-wallet/src/wallets/exodus.ts b/packages/use-wallet/src/wallets/exodus.ts index 8f9918c1..31438934 100644 --- a/packages/use-wallet/src/wallets/exodus.ts +++ b/packages/use-wallet/src/wallets/exodus.ts @@ -90,9 +90,10 @@ export class ExodusWallet extends BaseWallet { subscribe, getAlgodClient, options = {}, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) this.options = options this.store = store } diff --git a/packages/use-wallet/src/wallets/kibisis.ts b/packages/use-wallet/src/wallets/kibisis.ts index 3b799597..148f47eb 100644 --- a/packages/use-wallet/src/wallets/kibisis.ts +++ b/packages/use-wallet/src/wallets/kibisis.ts @@ -37,9 +37,10 @@ export class KibisisWallet extends BaseWallet { store, subscribe, getAlgodClient, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) this.store = store } @@ -183,9 +184,15 @@ export class KibisisWallet extends BaseWallet { } private async _getGenesisHash(): Promise { + // First try to get from network config + const network = this.activeNetworkConfig + if (network.genesisHash) { + return network.genesisHash + } + + // Fallback to API request const algodClient = this.getAlgodClient() const version = await algodClient.versionsCheck().do() - return algosdk.bytesToBase64(version.genesisHashB64) } diff --git a/packages/use-wallet/src/wallets/kmd.ts b/packages/use-wallet/src/wallets/kmd.ts index 92710086..f889ac68 100644 --- a/packages/use-wallet/src/wallets/kmd.ts +++ b/packages/use-wallet/src/wallets/kmd.ts @@ -69,10 +69,11 @@ export class KmdWallet extends BaseWallet { store, subscribe, getAlgodClient, + networks, options, metadata = {} }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) const { token = 'a'.repeat(64), diff --git a/packages/use-wallet/src/wallets/liquid.ts b/packages/use-wallet/src/wallets/liquid.ts index f247f059..41021d6a 100644 --- a/packages/use-wallet/src/wallets/liquid.ts +++ b/packages/use-wallet/src/wallets/liquid.ts @@ -35,9 +35,10 @@ export class LiquidWallet extends BaseWallet { subscribe, getAlgodClient, options, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) this.store = store this.options = options ?? { diff --git a/packages/use-wallet/src/wallets/lute.ts b/packages/use-wallet/src/wallets/lute.ts index 5e20d1a9..24127072 100644 --- a/packages/use-wallet/src/wallets/lute.ts +++ b/packages/use-wallet/src/wallets/lute.ts @@ -38,9 +38,10 @@ export class LuteWallet extends BaseWallet { subscribe, getAlgodClient, options, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) if (!options?.siteName) { this.logger.error('Missing required option: siteName') throw new Error('Missing required option: siteName') @@ -66,14 +67,17 @@ export class LuteWallet extends BaseWallet { } private async getGenesisId(): Promise { + const network = this.activeNetworkConfig + if (network.genesisId) { + return network.genesisId + } + const algodClient = this.getAlgodClient() const genesisStr = await algodClient.genesis().do() const genesis = algosdk.parseJSON(genesisStr, { intDecoding: algosdk.IntDecoding.MIXED }) - const genesisId = `${genesis.network}-${genesis.id}` - - return genesisId + return `${genesis.network}-${genesis.id}` } public connect = async (): Promise => { diff --git a/packages/use-wallet/src/wallets/magic.ts b/packages/use-wallet/src/wallets/magic.ts index dd13af37..2a4e6047 100644 --- a/packages/use-wallet/src/wallets/magic.ts +++ b/packages/use-wallet/src/wallets/magic.ts @@ -54,9 +54,10 @@ export class MagicAuth extends BaseWallet { subscribe, getAlgodClient, options, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) if (!options?.apiKey) { this.logger.error('Missing required option: apiKey') throw new Error('Missing required option: apiKey') diff --git a/packages/use-wallet/src/wallets/mnemonic.ts b/packages/use-wallet/src/wallets/mnemonic.ts index 4dda608b..63a1fc0c 100644 --- a/packages/use-wallet/src/wallets/mnemonic.ts +++ b/packages/use-wallet/src/wallets/mnemonic.ts @@ -1,5 +1,4 @@ import algosdk from 'algosdk' -import { NetworkId } from 'src/network' import { StorageAdapter } from 'src/storage' import { LOCAL_STORAGE_KEY, WalletState, addWallet, type State } from 'src/store' import { flattenTxnGroup, isSignedTxn, isTransactionArray } from 'src/utils' @@ -32,9 +31,10 @@ export class MnemonicWallet extends BaseWallet { subscribe, getAlgodClient, options, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) const { persistToStorage = false } = options || {} this.options = { persistToStorage } @@ -67,12 +67,12 @@ export class MnemonicWallet extends BaseWallet { private checkMainnet(): void { try { - const network = this.activeNetwork - if (network === NetworkId.MAINNET) { + const network = this.activeNetworkConfig + if (!network.isTestnet) { this.logger.warn( 'The Mnemonic wallet provider is insecure and intended for testing only. Any private key mnemonics used should never hold real Algos (i.e., on MainNet).' ) - throw new Error('MainNet active network detected. Aborting.') + throw new Error('Production network detected. Aborting.') } } catch (error) { this.disconnect() diff --git a/packages/use-wallet/src/wallets/pera.ts b/packages/use-wallet/src/wallets/pera.ts index fdeb7637..f83f0fd2 100644 --- a/packages/use-wallet/src/wallets/pera.ts +++ b/packages/use-wallet/src/wallets/pera.ts @@ -38,9 +38,10 @@ export class PeraWallet extends BaseWallet { subscribe, getAlgodClient, options = {}, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) this.options = options this.store = store } diff --git a/packages/use-wallet/src/wallets/pera2.ts b/packages/use-wallet/src/wallets/pera2.ts index bad03fa0..a28335e0 100644 --- a/packages/use-wallet/src/wallets/pera2.ts +++ b/packages/use-wallet/src/wallets/pera2.ts @@ -43,9 +43,10 @@ export class PeraWallet extends BaseWallet { subscribe, getAlgodClient, options, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) if (!options?.projectId) { this.logger.error('Missing required option: projectId') throw new Error('Missing required option: projectId') diff --git a/packages/use-wallet/src/wallets/types.ts b/packages/use-wallet/src/wallets/types.ts index 02235547..f2b02a42 100644 --- a/packages/use-wallet/src/wallets/types.ts +++ b/packages/use-wallet/src/wallets/types.ts @@ -17,6 +17,7 @@ import { BiatecWallet } from './biatec' import type { Store } from '@tanstack/store' import type algosdk from 'algosdk' import type { State } from 'src/store' +import type { NetworkConfig } from 'src/network' export enum WalletId { BIATEC = 'biatec', @@ -100,6 +101,7 @@ export interface BaseWalletConstructor { getAlgodClient: () => algosdk.Algodv2 store: Store subscribe: (callback: (state: State) => void) => () => void + networks: Record } export type WalletConstructor = BaseWalletConstructor & { diff --git a/packages/use-wallet/src/wallets/walletconnect.ts b/packages/use-wallet/src/wallets/walletconnect.ts index 4424ecc9..e5f2f4ae 100644 --- a/packages/use-wallet/src/wallets/walletconnect.ts +++ b/packages/use-wallet/src/wallets/walletconnect.ts @@ -1,5 +1,4 @@ import algosdk from 'algosdk' -import { caipChainId } from 'src/network' import { WalletState, addWallet, setAccounts, type State } from 'src/store' import { base64ToByteArray, @@ -21,6 +20,7 @@ import type { WalletId, WalletTransaction } from 'src/wallets/types' +import { NetworkConfig } from 'src/network' interface SignClientOptions { projectId: string @@ -70,9 +70,10 @@ export class WalletConnect extends BaseWallet { subscribe, getAlgodClient, options, - metadata = {} + metadata = {}, + networks }: WalletConstructor) { - super({ id, metadata, getAlgodClient, store, subscribe }) + super({ id, metadata, getAlgodClient, store, subscribe, networks }) if (!options?.projectId) { this.logger.error('Missing required option: projectId') throw new Error('Missing required option: projectId') @@ -354,12 +355,17 @@ export class WalletConnect extends BaseWallet { } public get activeChainId(): string { - const chainId = caipChainId[this.activeNetwork] - if (!chainId) { + const network = this.networks[this.activeNetwork] + if (!network?.caipChainId) { this.logger.warn(`No CAIP-2 chain ID found for network: ${this.activeNetwork}`) return '' } - return chainId + return network.caipChainId + } + + // Make networks accessible for testing + public get networkConfig(): Record { + return this.networks } public connect = async (): Promise => {