diff --git a/__tests__/wallets/supported/defly.spec.ts b/__tests__/wallets/supported/defly.spec.ts index 3fb04c5..67d0faa 100644 --- a/__tests__/wallets/supported/defly.spec.ts +++ b/__tests__/wallets/supported/defly.spec.ts @@ -51,7 +51,7 @@ describe('DeflyWallet', () => { afterEach(async () => { await wallet.disconnect() localStorage.clear() - jest.resetAllMocks() + jest.clearAllMocks() }) describe('connect', () => { diff --git a/__tests__/wallets/supported/exodus.spec.ts b/__tests__/wallets/supported/exodus.spec.ts new file mode 100644 index 0000000..3895b7f --- /dev/null +++ b/__tests__/wallets/supported/exodus.spec.ts @@ -0,0 +1,432 @@ +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals' +import * as msgpack from 'algo-msgpack-with-bigint' +import algosdk from 'algosdk' +import { State, Store, createStore, defaultState } from 'src/store' +import { WalletId } from 'src/wallets/supported/constants' +import { EnableResult, Exodus, ExodusWallet, SignTxnsResult } from 'src/wallets/supported/exodus' +import { WalletTransaction } from 'src/wallets/types' + +// Spy/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 = {}) + } +})() + +const mockEnableFn = jest.fn<() => Promise>().mockImplementation(() => { + return Promise.resolve({ + genesisID: 'mainnet-v1.0', + genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + accounts: [ + '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + 'GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A' + ] + }) +}) + +const mockSignTxns = jest + .fn<(transactions: WalletTransaction[]) => Promise>() + .mockResolvedValue(['mockBase64SignedTxn']) + +// Mock Exodus extension +const mockExodus: Exodus = { + isConnected: true, + address: 'mock-address', + enable: mockEnableFn, + signTxns: mockSignTxns +} + +Object.defineProperties(global, { + localStorage: { + value: localStorageMock + }, + window: { + value: { + algorand: mockExodus + } + } +}) + +describe('ExodusWallet', () => { + let wallet: ExodusWallet + let store: Store + + const mockSubscribe: (callback: (state: State) => void) => () => void = jest.fn( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (callback: (state: State) => void) => { + return () => console.log('unsubscribe') + } + ) + + beforeEach(() => { + store = createStore(defaultState) + wallet = new ExodusWallet({ + id: WalletId.EXODUS, + metadata: {}, + store, + subscribe: mockSubscribe, + onStateChange: jest.fn() + }) + }) + + afterEach(async () => { + await wallet.disconnect() + localStorage.clear() + jest.clearAllMocks() + }) + + describe('connect', () => { + it('should initialize client, return account objects, and update store', async () => { + const account1 = { + name: 'Exodus Wallet 1', + address: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q' + } + const account2 = { + name: 'Exodus Wallet 2', + address: 'GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A' + } + + const accounts = await wallet.connect() + + expect(wallet.isConnected).toBe(true) + expect(accounts).toEqual([account1, account2]) + expect(store.getState().wallets.get(WalletId.EXODUS)).toEqual({ + accounts: [account1, account2], + activeAccount: account1 + }) + }) + + it('should log an error and return an empty array when no accounts are found', async () => { + mockEnableFn.mockResolvedValueOnce({ + genesisID: 'mainnet-v1.0', + genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + accounts: [] + }) + + const accounts = await wallet.connect() + + expect(wallet.isConnected).toBe(false) + expect(console.error).toHaveBeenCalledWith( + '[ExodusWallet] Error connecting: No accounts found!' + ) + expect(accounts).toEqual([]) + expect(store.getState().wallets.get(WalletId.EXODUS)).toBeUndefined() + }) + }) + + describe('disconnect', () => { + it('should disconnect client and remove wallet from store', async () => { + // Connect first to initialize client + await wallet.connect() + expect(wallet.isConnected).toBe(true) + + await wallet.disconnect() + expect(wallet.isConnected).toBe(false) + + expect(store.getState().wallets.get(WalletId.EXODUS)).toBeUndefined() + }) + }) + + describe('resumeSession', () => { + describe('when there is Exodus wallet data in the store', () => { + beforeEach(() => { + const account1 = { + name: 'Exodus Wallet 1', + address: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q' + } + const account2 = { + name: 'Exodus Wallet 2', + address: 'GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A' + } + + store = createStore({ + ...defaultState, + wallets: new Map([ + [ + WalletId.EXODUS, + { + accounts: [account1, account2], + activeAccount: account1 + } + ] + ]) + }) + + wallet = new ExodusWallet({ + id: WalletId.EXODUS, + metadata: {}, + store, + subscribe: mockSubscribe, + onStateChange: jest.fn() + }) + }) + + describe('when the Exodus extension is connected', () => { + it('should be a no-op', async () => { + expect(store.getState().wallets.get(WalletId.EXODUS)).toBeDefined() + await wallet.resumeSession() + expect(store.getState().wallets.get(WalletId.EXODUS)).toBeDefined() + }) + }) + + describe('when the Exodus extension is not connected', () => { + beforeEach(() => { + // @ts-expect-error - algorand does not exist on window + window.algorand.isConnected = false + }) + + it('should remove the wallet from the store if the extension is not found', async () => { + expect(store.getState().wallets.get(WalletId.EXODUS)).toBeDefined() + await wallet.resumeSession() + expect(store.getState().wallets.get(WalletId.EXODUS)).toBeUndefined() + }) + + afterEach(() => { + // @ts-expect-error - algorand does not exist on window + window.algorand.isConnected = true + }) + }) + }) + + describe('when there is no Exodus wallet data in the store', () => { + it('should be a no-op', async () => { + expect(store.getState().wallets.get(WalletId.EXODUS)).toBeUndefined() + await wallet.resumeSession() + expect(store.getState().wallets.get(WalletId.EXODUS)).toBeUndefined() + }) + }) + }) + + describe('signTransactions', () => { + const txnParams = { + fee: 10, + firstRound: 51, + lastRound: 61, + genesisHash: 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + genesisID: 'mainnet-v1.0' + } + + // Transactions used in tests + const txn1 = new algosdk.Transaction({ + ...txnParams, + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + amount: 1000 + }) + const txn2 = new algosdk.Transaction({ + ...txnParams, + from: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + amount: 2000 + }) + + // Signed transaction objects to be base64 encoded + const signedTxnObj1 = { + txn: Buffer.from(txn1.toByte()).toString('base64'), + sig: 'mockBase64Signature' + } + const signedTxnObj2 = { + txn: Buffer.from(txn2.toByte()).toString('base64'), + sig: 'mockBase64Signature' + } + + // Signed transactions (base64 strings) returned by Exodus extension + const signedTxnStr1 = Buffer.from( + new Uint8Array(msgpack.encode(signedTxnObj1, { sortKeys: true })) + ).toString('base64') + const signedTxnStr2 = Buffer.from( + new Uint8Array(msgpack.encode(signedTxnObj2, { sortKeys: true })) + ).toString('base64') + + // Signed transactions (Uint8Array) returned by ExodusWallet.signTransactions + const signedTxnEncoded1 = new Uint8Array(Buffer.from(signedTxnStr1, 'base64')) + const signedTxnEncoded2 = new Uint8Array(Buffer.from(signedTxnStr2, 'base64')) + + beforeEach(async () => { + await wallet.connect() + }) + + it('should correctly process and sign a single algosdk.Transaction', async () => { + mockSignTxns.mockResolvedValueOnce([signedTxnStr1]) + + const result = await wallet.signTransactions([txn1]) + + expect(result).toEqual([signedTxnEncoded1]) + expect(mockSignTxns).toHaveBeenCalledWith([ + { + txn: Buffer.from(txn1.toByte()).toString('base64') + } + ]) + }) + + it('should correctly process and sign a single algosdk.Transaction group', async () => { + mockSignTxns.mockResolvedValueOnce([signedTxnStr1, signedTxnStr2]) + + const result = await wallet.signTransactions([txn1, txn2]) + + expect(result).toEqual([signedTxnEncoded1, signedTxnEncoded2]) + expect(mockSignTxns).toHaveBeenCalledWith([ + { + txn: Buffer.from(txn1.toByte()).toString('base64') + }, + { + txn: Buffer.from(txn2.toByte()).toString('base64') + } + ]) + }) + + it('should correctly process and sign multiple algosdk.Transaction groups', async () => { + mockSignTxns.mockResolvedValueOnce([signedTxnStr1, signedTxnStr2]) + + const result = await wallet.signTransactions([[txn1], [txn2]]) + + expect(result).toEqual([signedTxnEncoded1, signedTxnEncoded2]) + expect(mockSignTxns).toHaveBeenCalledWith([ + { + txn: Buffer.from(txn1.toByte()).toString('base64') + }, + { + txn: Buffer.from(txn2.toByte()).toString('base64') + } + ]) + }) + + it('should correctly process and sign a single encoded transaction', async () => { + mockSignTxns.mockResolvedValueOnce([signedTxnStr1]) + + const encodedTxn = txn1.toByte() + const result = await wallet.signTransactions([encodedTxn]) + + expect(result).toEqual([signedTxnEncoded1]) + expect(mockSignTxns).toHaveBeenCalledWith([ + { + txn: Buffer.from(txn1.toByte()).toString('base64') + } + ]) + }) + + it('should correctly process and sign a single encoded transaction group', async () => { + mockSignTxns.mockResolvedValueOnce([signedTxnStr1, signedTxnStr2]) + + const txnGroup = [txn1, txn2] + const encodedTxnGroup = txnGroup.map((txn) => txn.toByte()) + + const result = await wallet.signTransactions(encodedTxnGroup) + + expect(result).toEqual([signedTxnEncoded1, signedTxnEncoded2]) + expect(mockSignTxns).toHaveBeenCalledWith([ + { + txn: Buffer.from(txn1.toByte()).toString('base64') + }, + { + txn: Buffer.from(txn2.toByte()).toString('base64') + } + ]) + }) + + it('should correctly process and sign multiple encoded transaction groups', async () => { + mockSignTxns.mockResolvedValueOnce([signedTxnStr1, signedTxnStr2]) + + const result = await wallet.signTransactions([[txn1.toByte()], [txn2.toByte()]]) + + expect(result).toEqual([signedTxnEncoded1, signedTxnEncoded2]) + expect(mockSignTxns).toHaveBeenCalledWith([ + { + txn: Buffer.from(txn1.toByte()).toString('base64') + }, + { + txn: Buffer.from(txn2.toByte()).toString('base64') + } + ]) + }) + + it('should determine which transactions to sign based on indexesToSign', async () => { + mockSignTxns.mockResolvedValueOnce([null, signedTxnStr2]) + + const txnGroup = [txn1, txn2] + const indexesToSign = [1] + const returnGroup = false // Return only the signed transaction + + const expectedResult = [signedTxnEncoded2] + + const result = await wallet.signTransactions(txnGroup, indexesToSign, returnGroup) + + expect(result).toEqual(expectedResult) + expect(mockSignTxns).toHaveBeenCalledWith([ + { + txn: Buffer.from(txn1.toByte()).toString('base64'), + signers: [] // txn1 should not be signed + }, + { + txn: Buffer.from(txn2.toByte()).toString('base64') + } + ]) + }) + + it('should correctly merge signed transactions back into the original group', async () => { + mockSignTxns.mockResolvedValueOnce([null, signedTxnStr2]) + + const txnGroup = [txn1, txn2] + const returnGroup = true // Merge signed transaction back into original group + + // Only txn2 should be signed + const indexesToSign1 = [1] + const expectedResult1 = [algosdk.encodeUnsignedTransaction(txn1), signedTxnEncoded2] + + const result1 = await wallet.signTransactions(txnGroup, indexesToSign1, returnGroup) + expect(result1).toEqual(expectedResult1) + + mockSignTxns.mockResolvedValueOnce([signedTxnStr1, null]) + + // Only txn1 should be signed + const indexesToSign2 = [0] + const expectedResult2 = [signedTxnEncoded1, algosdk.encodeUnsignedTransaction(txn2)] + + const result2 = await wallet.signTransactions(txnGroup, indexesToSign2, returnGroup) + expect(result2).toEqual(expectedResult2) + }) + + it('should only send transactions with connected signers for signature', async () => { + const txnCannotSign = new algosdk.Transaction({ + ...txnParams, + from: 'EW64GC6F24M7NDSC5R3ES4YUVE3ZXXNMARJHDCCCLIHZU6TBEOC7XRSBG4', // EW64GC is not connected + to: '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q', + amount: 3000 + }) + + mockSignTxns.mockResolvedValueOnce([signedTxnStr1, null, signedTxnStr2]) + + const result = await wallet.signTransactions([txn1, txnCannotSign, txn2]) + + // expectedResult[1] should be original unsigned transaction + const expectedResult = [ + signedTxnEncoded1, + algosdk.encodeUnsignedTransaction(txnCannotSign), + signedTxnEncoded2 + ] + + expect(result).toEqual(expectedResult) + expect(mockSignTxns).toHaveBeenCalledWith([ + { + txn: Buffer.from(txn1.toByte()).toString('base64') + }, + { + txn: Buffer.from(txnCannotSign.toByte()).toString('base64'), + signers: [] // should not be signed + }, + { + txn: Buffer.from(txn2.toByte()).toString('base64') + } + ]) + }) + }) +}) diff --git a/__tests__/wallets/supported/pera.spec.ts b/__tests__/wallets/supported/pera.spec.ts index b775591..bac6f07 100644 --- a/__tests__/wallets/supported/pera.spec.ts +++ b/__tests__/wallets/supported/pera.spec.ts @@ -51,7 +51,7 @@ describe('PeraWallet', () => { afterEach(async () => { await wallet.disconnect() localStorage.clear() - jest.resetAllMocks() + jest.clearAllMocks() }) describe('connect', () => { diff --git a/bun.lockb b/bun.lockb index 459a75e..c74294a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 252bde0..3a913ad 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@walletconnect/modal": "^2.6.2", "@walletconnect/sign-client": "^2.10.2", "@walletconnect/types": "^2.10.2", + "algo-msgpack-with-bigint": "^2.1.1", "bun-types": "^1.0.15", "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", diff --git a/src/wallets/supported/exodus.ts b/src/wallets/supported/exodus.ts index 0fa9020..8414974 100644 --- a/src/wallets/supported/exodus.ts +++ b/src/wallets/supported/exodus.ts @@ -32,18 +32,18 @@ interface EnableAccountsResult { accounts: string[] } -type EnableResult = EnableNetworkResult & EnableAccountsResult +export type EnableResult = EnableNetworkResult & EnableAccountsResult -type SignTxnsResult = (string | null)[] +export type SignTxnsResult = (string | null)[] -interface Exodus { +export interface Exodus { isConnected: boolean address: string | null enable: (options?: ExodusOptions) => Promise signTxns: (transactions: WalletTransaction[]) => Promise } -type WindowExtended = { algorand: Exodus } & Window & typeof globalThis +export type WindowExtended = { algorand: Exodus } & Window & typeof globalThis const icon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI2LjUuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzMDAgMzAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAzMDAgMzAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6dXJsKCNTVkdJRF8xXyk7fQoJLnN0MXtmaWxsOnVybCgjU1ZHSURfMDAwMDAwNDM0MjYxNjcxNDAxMDY1ODIyNzAwMDAwMDIxMzA3Njg5MDYwNzMxMTM0ODRfKTt9Cgkuc3Qye2ZpbGw6dXJsKCNTVkdJRF8wMDAwMDEwMjUxOTMxNjAxNTI3NjU4MTY0MDAwMDAxNjI3NDExMjM4MzE3NTY0MTc1OV8pO2ZpbHRlcjp1cmwoI0Fkb2JlX09wYWNpdHlNYXNrRmlsdGVyKTt9Cgkuc3Qze2ZpbGw6dXJsKCNTVkdJRF8wMDAwMDEzODU2MzM4MjQ2MjA4NjAyMDM1MDAwMDAxNDg3ODQ5MDI3MDc4MjA3MTIwN18pO30KCS5zdDR7bWFzazp1cmwoI21hc2swXzE2NjFfMjk1XzAwMDAwMDg4MTMyMjUxNTk3NDQxNTczNDkwMDAwMDExNjkzNjEyMDE4NTA2NjgxNDgxXyk7fQoJLnN0NXtmaWxsOnVybCgjU1ZHSURfMDAwMDAxMDYxMjA2MzI0NjE3OTI4NzExNjAwMDAwMDc0MzM5MTMwMzgzMzc3NjY1NzZfKTt9Cjwvc3R5bGU+CjxnPgoJCgkJPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8xXyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIyNDYuNjAzIiB5MT0iOS4yMjEyIiB4Mj0iMTc0LjE1OCIgeTI9IjMwOC41NDI2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDAgMzAyKSI+CgkJPHN0b3AgIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6IzBCNDZGOSIvPgoJCTxzdG9wICBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNCQkZCRTAiLz4KCTwvbGluZWFyR3JhZGllbnQ+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMjc0LjcsOTMuOUwxNjYuNiwyM3YzOS42bDY5LjQsNDUuMWwtOC4yLDI1LjhoLTYxLjJ2MzIuOWg2MS4ybDguMiwyNS44bC02OS40LDQ1LjFWMjc3bDEwOC4yLTcwLjdMMjU3LDE1MC4xCgkJTDI3NC43LDkzLjl6Ii8+CgkKCQk8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzAwMDAwMDE4MjI4MjM3MTUxMjM5MTUxMzIwMDAwMDE3ODM4NjY0MjU5NzY2MjczOTI1XyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIxMjkuMzUxNiIgeTE9Ii0xOS4xNTczIiB4Mj0iNTYuOTA2NiIgeTI9IjI4MC4xNjQxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDAgMzAyKSI+CgkJPHN0b3AgIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6IzBCNDZGOSIvPgoJCTxzdG9wICBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNCQkZCRTAiLz4KCTwvbGluZWFyR3JhZGllbnQ+Cgk8cGF0aCBzdHlsZT0iZmlsbDp1cmwoI1NWR0lEXzAwMDAwMDE4MjI4MjM3MTUxMjM5MTUxMzIwMDAwMDE3ODM4NjY0MjU5NzY2MjczOTI1Xyk7IiBkPSJNNzIuNSwxNjYuNGg2MXYtMzIuOUg3Mi4ybC03LjktMjUuOAoJCWw2OS4yLTQ1LjFWMjNMMjUuMyw5My45TDQzLDE1MC4xbC0xNy43LDU2LjJMMTMzLjcsMjc3di0zOS42bC02OS40LTQ1LjFMNzIuNSwxNjYuNHoiLz4KCTxkZWZzPgoJCTxmaWx0ZXIgaWQ9IkFkb2JlX09wYWNpdHlNYXNrRmlsdGVyIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjI1LjQiIHk9IjIzIiB3aWR0aD0iMjQ3LjYiIGhlaWdodD0iMjU0Ij4KCQkJPGZlQ29sb3JNYXRyaXggIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIxIDAgMCAwIDAgIDAgMSAwIDAgMCAgMCAwIDEgMCAwICAwIDAgMCAxIDAiLz4KCQk8L2ZpbHRlcj4KCTwvZGVmcz4KCQoJCTxtYXNrIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjI1LjQiIHk9IjIzIiB3aWR0aD0iMjQ3LjYiIGhlaWdodD0iMjU0IiBpZD0ibWFzazBfMTY2MV8yOTVfMDAwMDAwODgxMzIyNTE1OTc0NDE1NzM0OTAwMDAwMTE2OTM2MTIwMTg1MDY2ODE0ODFfIj4KCQkKCQkJPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8wMDAwMDE2NTkyOTcyNDMwMzE2NDIwMzAwMDAwMDAwNzEwMTkwNDk4NDUxOTkxNTE2Ml8iIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iMjQ2LjYwMzgiIHkxPSI5LjIyMTQiIHgyPSIxNzQuMTU4OCIgeTI9IjMwOC41NDI4IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDAgMzAyKSI+CgkJCTxzdG9wICBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiMwQjQ2RjkiLz4KCQkJPHN0b3AgIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I0JCRkJFMCIvPgoJCTwvbGluZWFyR3JhZGllbnQ+CgkJPHBhdGggc3R5bGU9ImZpbGw6dXJsKCNTVkdJRF8wMDAwMDE2NTkyOTcyNDMwMzE2NDIwMzAwMDAwMDAwNzEwMTkwNDk4NDUxOTkxNTE2Ml8pO2ZpbHRlcjp1cmwoI0Fkb2JlX09wYWNpdHlNYXNrRmlsdGVyKTsiIGQ9IgoJCQlNMjc0LjcsOTMuOUwxNjYuNiwyM3YzOS42bDY5LjQsNDUuMWwtOC4yLDI1LjhoLTYxLjJ2MzIuOWg2MS4ybDguMiwyNS44bC02OS40LDQ1LjFWMjc3bDEwOC4yLTcwLjdMMjU3LDE1MC4xTDI3NC43LDkzLjl6Ii8+CgkJCgkJCTxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfMDAwMDAxMTk4MTE3MDc2MjE0NzI4MTQyNzAwMDAwMTA4Mjk2NTkzODM4NTEyMDI0OTFfIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjEyOS4zNTIxIiB5MT0iLTE5LjE1NzEiIHgyPSI1Ni45MDcxIiB5Mj0iMjgwLjE2NDIiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMCAzMDIpIj4KCQkJPHN0b3AgIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6IzBCNDZGOSIvPgoJCQk8c3RvcCAgb2Zmc2V0PSIxIiBzdHlsZT0ic3RvcC1jb2xvcjojQkJGQkUwIi8+CgkJPC9saW5lYXJHcmFkaWVudD4KCQk8cGF0aCBzdHlsZT0iZmlsbDp1cmwoI1NWR0lEXzAwMDAwMTE5ODExNzA3NjIxNDcyODE0MjcwMDAwMDEwODI5NjU5MzgzODUxMjAyNDkxXyk7IiBkPSJNNzIuNSwxNjYuNGg2MXYtMzIuOUg3Mi4ybC03LjktMjUuOAoJCQlsNjkuMi00NS4xVjIzTDI1LjMsOTMuOUw0MywxNTAuMWwtMTcuNyw1Ni4yTDEzMy43LDI3N3YtMzkuNmwtNjkuNC00NS4xTDcyLjUsMTY2LjR6Ii8+Cgk8L21hc2s+Cgk8ZyBjbGFzcz0ic3Q0Ij4KCQkKCQkJPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8wMDAwMDEwOTAxOTkxODU1Nzc3MzA1MzQyMDAwMDAxNzYwMjQwNTkwODA2NzEyMDMwMF8iIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNDYuNDY2MiIgeTE9IjIyOC43NTU0IiB4Mj0iMTcxLjg2MzgiIHkyPSIxMzUuMTAzOSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAwIDMwMikiPgoJCQk8c3RvcCAgb2Zmc2V0PSIwLjExOTgiIHN0eWxlPSJzdG9wLWNvbG9yOiM4OTUyRkY7c3RvcC1vcGFjaXR5OjAuODciLz4KCQkJPHN0b3AgIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I0RBQkRGRjtzdG9wLW9wYWNpdHk6MCIvPgoJCTwvbGluZWFyR3JhZGllbnQ+CgkJCgkJCTxyZWN0IHg9IjI1LjQiIHk9IjIzIiBzdHlsZT0iZmlsbDp1cmwoI1NWR0lEXzAwMDAwMTA5MDE5OTE4NTU3NzczMDUzNDIwMDAwMDE3NjAyNDA1OTA4MDY3MTIwMzAwXyk7IiB3aWR0aD0iMjQ3LjYiIGhlaWdodD0iMjU0Ii8+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==' @@ -73,7 +73,7 @@ export class ExodusWallet extends BaseWallet { private async initializeClient(): Promise { console.info('[ExodusWallet] Initializing client...') - if (typeof window == 'undefined' || (window as WindowExtended).algorand === undefined) { + if (typeof window === 'undefined' || (window as WindowExtended).algorand === undefined) { throw new Error('Exodus is not available.') } const client = (window as WindowExtended).algorand @@ -88,7 +88,7 @@ export class ExodusWallet extends BaseWallet { const { accounts } = await client.enable(this.options) if (accounts.length === 0) { - throw new Error('[ExodusWallet] No accounts found!') + throw new Error('No accounts found!') } const walletAccounts = accounts.map((address: string, idx: number) => ({ @@ -113,7 +113,7 @@ export class ExodusWallet extends BaseWallet { if (error.name === 'UserRejectedRequestError') { console.info('[ExodusWallet] Connection cancelled.') } else { - console.error('[ExodusWallet] Error connecting:', error) + console.error(`[ExodusWallet] Error connecting: ${error.message}`) } return [] }