Skip to content

Commit

Permalink
feat(wallets): add WalletConnect v1 session management (#275)
Browse files Browse the repository at this point in the history
* feat(wallets): add WalletConnect v1 session management

This commit introduces WalletConnect v1 session management functionality for Pera (v1) and Defly wallets to improve multi-wallet support.

- Added `manageWalletConnectSession` method to Pera and Defly wallet classes
- Implemented session backup and restore logic in `connect` and `setActive` methods
- Updated `StorageAdapter` mock in tests to handle WalletConnect data
- Added new test cases for WalletConnect session management

* refactor(wallets): improve WalletConnect session management

- Move `manageWalletConnectSession` to `BaseWallet`
- Update `connect`, `disconnect`, and `setActive` methods
- Add delay after disconnect to prevent race condition
- Adjust tests to reflect new behavior
  • Loading branch information
drichar committed Sep 27, 2024
1 parent 7e1d925 commit 484b2b5
Show file tree
Hide file tree
Showing 16 changed files with 529 additions and 25 deletions.
3 changes: 2 additions & 1 deletion packages/use-wallet/src/__tests__/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ vi.mock('src/logger', () => {
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down
3 changes: 2 additions & 1 deletion packages/use-wallet/src/__tests__/wallets/custom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ vi.mock('src/logger', () => ({
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down
214 changes: 210 additions & 4 deletions packages/use-wallet/src/__tests__/wallets/defly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ vi.mock('src/logger', () => ({
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down Expand Up @@ -87,17 +88,31 @@ describe('DeflyWallet', () => {
beforeEach(() => {
vi.clearAllMocks()

let mockWalletConnectData: string | null = null

vi.mocked(StorageAdapter.getItem).mockImplementation((key: string) => {
if (key === LOCAL_STORAGE_KEY && mockInitialState !== null) {
return JSON.stringify(mockInitialState)
}
if (key === 'walletconnect') {
return mockWalletConnectData
}
return null
})

vi.mocked(StorageAdapter.setItem).mockImplementation((key: string, value: string) => {
if (key === LOCAL_STORAGE_KEY) {
mockInitialState = JSON.parse(value)
}
if (key.startsWith('walletconnect-')) {
mockWalletConnectData = value
}
})

vi.mocked(StorageAdapter.removeItem).mockImplementation((key: string) => {
if (key === 'walletconnect') {
mockWalletConnectData = null
}
})

mockLogger = {
Expand Down Expand Up @@ -154,6 +169,29 @@ describe('DeflyWallet', () => {

expect(mockDeflyWallet.connector.on).toHaveBeenCalledWith('disconnect', expect.any(Function))
})

it('should backup WalletConnect session when connecting and another wallet is active', async () => {
const mockWalletConnectData = 'mockWalletConnectData'
vi.mocked(StorageAdapter.getItem).mockReturnValueOnce(mockWalletConnectData)
mockDeflyWallet.connect.mockResolvedValueOnce([account1.address])

// Set Pera as the active wallet
store.setState((state) => ({
...state,
activeWallet: WalletId.PERA
}))

const manageWalletConnectSessionSpy = vi.spyOn(wallet, 'manageWalletConnectSession' as any)

await wallet.connect()

expect(manageWalletConnectSessionSpy).toHaveBeenCalledWith('backup', WalletId.PERA)
expect(StorageAdapter.getItem).toHaveBeenCalledWith('walletconnect')
expect(StorageAdapter.setItem).toHaveBeenCalledWith(
`walletconnect-${WalletId.PERA}`,
mockWalletConnectData
)
})
})

describe('disconnect', () => {
Expand All @@ -168,6 +206,87 @@ describe('DeflyWallet', () => {
expect(wallet.isConnected).toBe(false)
})

it('should backup and restore active wallet session when disconnecting non-active wallet', async () => {
const mockWalletConnectData = 'mockWalletConnectData'
vi.mocked(StorageAdapter.getItem).mockReturnValueOnce(mockWalletConnectData)
mockDeflyWallet.connect.mockResolvedValueOnce([account1.address])
await wallet.connect()

// Set Pera as the active wallet
store.setState((state) => ({
...state,
activeWallet: WalletId.PERA
}))

const manageWalletConnectSessionSpy = vi.spyOn(wallet, 'manageWalletConnectSession' as any)

await wallet.disconnect()

expect(manageWalletConnectSessionSpy).toHaveBeenCalledWith('backup', WalletId.PERA)
expect(mockDeflyWallet.disconnect).toHaveBeenCalled()
expect(manageWalletConnectSessionSpy).toHaveBeenCalledWith('restore', WalletId.PERA)
expect(store.state.wallets[WalletId.DEFLY]).toBeUndefined()
})

it('should not backup or restore session when disconnecting active wallet', async () => {
mockDeflyWallet.connect.mockResolvedValueOnce([account1.address])
await wallet.connect()

// Set Defly as the active wallet
store.setState((state) => ({
...state,
activeWallet: WalletId.DEFLY
}))

const manageWalletConnectSessionSpy = vi.spyOn(wallet, 'manageWalletConnectSession' as any)

await wallet.disconnect()

expect(manageWalletConnectSessionSpy).not.toHaveBeenCalled()
expect(mockDeflyWallet.disconnect).toHaveBeenCalled()
expect(store.state.wallets[WalletId.DEFLY]).toBeUndefined()
})

it('should backup active wallet, restore inactive wallet, disconnect, and restore active wallet when disconnecting an inactive wallet', async () => {
mockDeflyWallet.connect.mockResolvedValueOnce([account1.address])
await wallet.connect()

// Set Pera as the active wallet
store.setState((state) => ({
...state,
activeWallet: WalletId.PERA
}))

const manageWalletConnectSessionSpy = vi.spyOn(wallet, 'manageWalletConnectSession' as any)

await wallet.disconnect()

expect(manageWalletConnectSessionSpy).toHaveBeenCalledWith('backup', WalletId.PERA)
expect(manageWalletConnectSessionSpy).toHaveBeenCalledWith('restore', WalletId.DEFLY)
expect(mockDeflyWallet.disconnect).toHaveBeenCalled()
expect(manageWalletConnectSessionSpy).toHaveBeenCalledWith('restore', WalletId.PERA)
expect(store.state.wallets[WalletId.DEFLY]).toBeUndefined()
})

it('should not remove backup when disconnecting the active wallet', async () => {
mockDeflyWallet.connect.mockResolvedValueOnce([account1.address])
await wallet.connect()

// Set Defly as the active wallet
store.setState((state) => ({
...state,
activeWallet: WalletId.DEFLY
}))

const manageWalletConnectSessionSpy = vi.spyOn(wallet, 'manageWalletConnectSession' as any)

await wallet.disconnect()

expect(manageWalletConnectSessionSpy).not.toHaveBeenCalled()
expect(mockDeflyWallet.disconnect).toHaveBeenCalled()
expect(store.state.wallets[WalletId.DEFLY]).toBeUndefined()
})

it('should throw an error if client.disconnect fails', async () => {
mockDeflyWallet.connect.mockResolvedValueOnce([account1.address])
mockDeflyWallet.disconnect.mockRejectedValueOnce(new Error('Disconnect error'))
Expand All @@ -176,9 +295,8 @@ describe('DeflyWallet', () => {

await expect(wallet.disconnect()).rejects.toThrow('Disconnect error')

// Should still update store/state
expect(store.state.wallets[WalletId.DEFLY]).toBeUndefined()
expect(wallet.isConnected).toBe(false)
expect(store.state.wallets[WalletId.DEFLY]).toBeDefined()
expect(wallet.isConnected).toBe(true)
})
})

Expand Down Expand Up @@ -333,6 +451,94 @@ describe('DeflyWallet', () => {
})
})

describe('setActive', () => {
it('should set the wallet as active', async () => {
const mockWalletConnectData = 'mockWalletConnectData'
vi.mocked(StorageAdapter.getItem).mockReturnValueOnce(mockWalletConnectData)
mockDeflyWallet.connect.mockResolvedValueOnce([account1.address])

await wallet.connect()
wallet.setActive()

expect(store.state.activeWallet).toBe(WalletId.DEFLY)
expect(StorageAdapter.setItem).toHaveBeenCalledWith('walletconnect', mockWalletConnectData)
expect(StorageAdapter.removeItem).toHaveBeenCalledWith(`walletconnect-${WalletId.DEFLY}`)
})

it('should backup current active wallet session and restore Pera session when setting active', async () => {
const mockWalletConnectData = 'mockWalletConnectData'
vi.mocked(StorageAdapter.getItem).mockReturnValueOnce(mockWalletConnectData)

store.setState((state) => ({
...state,
activeWallet: WalletId.PERA,
wallets: {
...state.wallets,
[WalletId.DEFLY]: { accounts: [account1], activeAccount: account1 },
[WalletId.PERA]: { accounts: [account2], activeAccount: account2 }
}
}))

const manageWalletConnectSessionSpy = vi.spyOn(wallet, 'manageWalletConnectSession' as any)

wallet.setActive()

expect(manageWalletConnectSessionSpy).toHaveBeenCalledWith('backup', WalletId.PERA)
expect(manageWalletConnectSessionSpy).toHaveBeenCalledWith('restore')
expect(store.state.activeWallet).toBe(WalletId.DEFLY)
})
})

describe('manageWalletConnectSession', () => {
it('should backup WalletConnect session', async () => {
const mockWalletConnectData = 'mockWalletConnectData'
vi.mocked(StorageAdapter.getItem).mockReturnValueOnce(mockWalletConnectData)

// @ts-expect-error - Accessing protected method for testing
wallet.manageWalletConnectSession('backup', WalletId.PERA)

expect(StorageAdapter.getItem).toHaveBeenCalledWith('walletconnect')
expect(StorageAdapter.setItem).toHaveBeenCalledWith(
`walletconnect-${WalletId.PERA}`,
mockWalletConnectData
)
})

it('should not backup WalletConnect session if no data exists', async () => {
vi.mocked(StorageAdapter.getItem).mockReturnValueOnce(null)

// @ts-expect-error - Accessing protected method for testing
wallet.manageWalletConnectSession('backup', WalletId.PERA)

expect(StorageAdapter.getItem).toHaveBeenCalledWith('walletconnect')
expect(StorageAdapter.setItem).not.toHaveBeenCalled()
expect(StorageAdapter.removeItem).not.toHaveBeenCalled()
})

it('should restore WalletConnect session', () => {
const mockWalletConnectData = 'mockWalletConnectData'
vi.mocked(StorageAdapter.getItem).mockReturnValueOnce(mockWalletConnectData)

// @ts-expect-error - Accessing protected method for testing
wallet.manageWalletConnectSession('restore')

expect(StorageAdapter.getItem).toHaveBeenCalledWith(`walletconnect-${WalletId.DEFLY}`)
expect(StorageAdapter.setItem).toHaveBeenCalledWith('walletconnect', mockWalletConnectData)
expect(StorageAdapter.removeItem).toHaveBeenCalledWith(`walletconnect-${WalletId.DEFLY}`)
})

it('should not restore WalletConnect session if no backup exists', () => {
vi.mocked(StorageAdapter.getItem).mockReturnValueOnce(null)

// @ts-expect-error - Accessing protected method for testing
wallet.manageWalletConnectSession('restore')

expect(StorageAdapter.getItem).toHaveBeenCalledWith(`walletconnect-${WalletId.DEFLY}`)
expect(StorageAdapter.setItem).not.toHaveBeenCalled()
expect(StorageAdapter.removeItem).not.toHaveBeenCalled()
})
})

describe('signing transactions', () => {
// Connected accounts
const connectedAcct1 = '7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q'
Expand Down
3 changes: 2 additions & 1 deletion packages/use-wallet/src/__tests__/wallets/exodus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ vi.mock('src/logger', () => ({
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down
3 changes: 2 additions & 1 deletion packages/use-wallet/src/__tests__/wallets/kibisis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ vi.mock('src/logger', () => ({
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down
3 changes: 2 additions & 1 deletion packages/use-wallet/src/__tests__/wallets/kmd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ vi.mock('src/logger', () => ({
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down
3 changes: 2 additions & 1 deletion packages/use-wallet/src/__tests__/wallets/lute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ vi.mock('src/logger', () => ({
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down
3 changes: 2 additions & 1 deletion packages/use-wallet/src/__tests__/wallets/magic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ vi.mock('src/logger', () => ({
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down
3 changes: 2 additions & 1 deletion packages/use-wallet/src/__tests__/wallets/mnemonic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ vi.mock('src/logger', () => ({
vi.mock('src/storage', () => ({
StorageAdapter: {
getItem: vi.fn(),
setItem: vi.fn()
setItem: vi.fn(),
removeItem: vi.fn()
}
}))

Expand Down
Loading

0 comments on commit 484b2b5

Please sign in to comment.