diff --git a/packages/hathor-rpc-handler/integration-tests/__tests__/connection.test.ts b/packages/hathor-rpc-handler/integration-tests/__tests__/connection.test.ts new file mode 100644 index 00000000..790db53f --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/__tests__/connection.test.ts @@ -0,0 +1,67 @@ +import { + initializeWalletConnectClient, + getWalletConnectClient, + disconnectWalletConnectClient, +} from '../utils/walletConnectClient'; + +jest.setTimeout(120000); // Set longer timeout for the entire test suite + +describe('WalletConnect Connection', () => { + // Initialize client and wait for relay connection before tests + beforeAll(async () => { + try { + const client = await initializeWalletConnectClient(); + console.log('Test Suite: WalletConnect client object initialized.'); + + if (client.core.relayer.connected) { + console.log('Test Suite: Relayer already connected.'); + return; // Already connected + } + + console.log('Test Suite: Waiting for relayer connection...'); + await new Promise((resolve, reject) => { + // Timeout if connection takes too long + const timeout = setTimeout(() => { + console.error('Test Suite: Relayer connection attempt timed out.'); + reject(new Error('Relayer connection timeout')); + }, 60000); // Increased timeout to 60 seconds + + // Use .once() as we only need the first connection event for setup + client.core.relayer.once('relayer_connect', () => { + clearTimeout(timeout); + console.log("Test Suite: 'relayer_connect' event received."); + resolve(); + }); + + client.core.relayer.once('relayer_error', (error: Error) => { + clearTimeout(timeout); + console.error("Test Suite: 'relayer_error' event received.", error); + reject(error); + }); + }); + + } catch (error) { + console.error('Test Suite Error: Failed during WalletConnect client initialization or connection wait', error); + // Optionally throw to fail the suite immediately + throw error; + } + }); + + // Disconnect client after all tests in this suite have run + afterAll(async () => { + // No need for complex cleanup with done() since we're using --forceExit + await disconnectWalletConnectClient(); + console.log('Test Suite: WalletConnect client disconnected.'); + }); + + it('should initialize the WalletConnect client successfully', () => { + // The beforeAll block handles initialization, so we just check if getClient works + expect(() => getWalletConnectClient()).not.toThrow(); + const client = getWalletConnectClient(); + expect(client).toBeDefined(); + // Add more specific checks if needed, e.g., client properties + expect(client.core.relayer.connected).toBe(true); // Check if relay connection is active + }); + + // Add more tests related to connection status or initial setup here +}); diff --git a/packages/hathor-rpc-handler/integration-tests/__tests__/pairing.test.ts b/packages/hathor-rpc-handler/integration-tests/__tests__/pairing.test.ts new file mode 100644 index 00000000..ce917b0d --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/__tests__/pairing.test.ts @@ -0,0 +1,70 @@ +import { + initializeWalletConnectClient, + getWalletConnectClient, + disconnectWalletConnectClient +} from '../utils/walletConnectClient'; +import { createVirtualDappPairing } from '../utils/virtualDapp'; + +jest.setTimeout(120000); // Set longer timeout for the entire test suite + +describe('WalletConnect Pairing', () => { + // Initialize client before tests + beforeAll(async () => { + await initializeWalletConnectClient(); + console.log('Test Suite: WalletConnect client initialized for pairing tests'); + }); + + // Disconnect client after tests + afterAll(async () => { + await disconnectWalletConnectClient(); + console.log('Test Suite: WalletConnect client disconnected after pairing tests'); + }); + + it('should pair with a virtual dApp successfully', async () => { + const client = getWalletConnectClient(); + expect(client).toBeDefined(); + + // Create a virtual dApp and get the pairing URI + const result = await createVirtualDappPairing(); + expect(result).toBeDefined(); + + const { pairingUri, approvePairing, dappClient } = result; + expect(pairingUri).toBeDefined(); + + // Add non-null assertion to satisfy TypeScript + expect(pairingUri!.startsWith('wc:')).toBe(true); + + console.log('Test Suite: Pairing URI created:', pairingUri); + + // Ensure pairingUri is defined before using it + if (!pairingUri) { + throw new Error('Pairing URI is undefined'); + } + + // Simulate wallet connecting to dApp by pairing with the URI + const pairingPromise = client.pair({ uri: pairingUri }); + + // Approve the pairing from the dApp side + console.log('Test Suite: Approving pairing from dApp side'); + await approvePairing(); + + // Wait for pairing to complete + const pairingResult = await pairingPromise; + expect(pairingResult).toBeDefined(); + expect(pairingResult.topic).toBeDefined(); + + console.log('Test Suite: Pairing completed successfully with topic:', pairingResult.topic); + + // Verify that we have an active session + const sessions = client.session.getAll(); + expect(sessions.length).toBeGreaterThan(0); + + // Verify the pairing exists + const pairings = client.pairing.getAll(); + expect(pairings.some(p => p.topic === pairingResult.topic)).toBe(true); + + // Clean up the dApp client + console.log('Test Suite: Cleaning up dApp client'); + await dappClient.core.relayer.transportClose(); + }); +}); \ No newline at end of file diff --git a/packages/hathor-rpc-handler/integration-tests/docker-compose.yml b/packages/hathor-rpc-handler/integration-tests/docker-compose.yml new file mode 100644 index 00000000..abde49a3 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + redis: + image: redis:latest + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 1s + timeout: 3s + retries: 30 + + relay: + image: reown/relay + ports: + - "5555:5000" + environment: + - REDIS_URL=redis://redis:6379/0 + depends_on: + redis: + condition: service_healthy # Wait for Redis to be healthy diff --git a/packages/hathor-rpc-handler/integration-tests/jest.config.js b/packages/hathor-rpc-handler/integration-tests/jest.config.js new file mode 100644 index 00000000..ccd70af5 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/jest.config.js @@ -0,0 +1,27 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + roots: [ + '/__tests__', + '/utils' + ], + testMatch: [ + '**/__tests__/**/*.+(ts|tsx|js)', + '**/?(*.)+(spec|test).+(ts|tsx|js)' + ], + transform: { + '^.+.tsx?$': ['ts-jest', { + tsconfig: '/tsconfig.json' + }] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + // Specify global setup and teardown scripts + globalSetup: '/jest.globalSetup.ts', + globalTeardown: '/jest.globalTeardown.ts', + // Run tests sequentially because they share the Docker environment + maxWorkers: 1, + // Set a longer timeout for integration tests and hooks + testTimeout: 90000, // 90 seconds +}; diff --git a/packages/hathor-rpc-handler/integration-tests/jest.globalSetup.ts b/packages/hathor-rpc-handler/integration-tests/jest.globalSetup.ts new file mode 100644 index 00000000..81d8e269 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/jest.globalSetup.ts @@ -0,0 +1,26 @@ +import { execSync } from 'child_process'; +import * as path from 'path'; + +const integrationTestsDir = __dirname; // Correctly refers to the integration-tests directory +const composeFile = path.join(integrationTestsDir, 'docker-compose.yml'); + +export default async () => { + console.log('\nSetting up integration test environment...'); + try { + // Use --wait to ensure services are healthy/running before proceeding + execSync(`docker compose -f "${composeFile}" up -d --wait`, { stdio: 'inherit' }); + console.log('Docker containers started successfully.'); + // Add a small delay just in case services need a moment to fully initialize after becoming "healthy" + await new Promise(resolve => setTimeout(resolve, 3000)); // 3 seconds delay + console.log('Integration test environment setup complete.'); + } catch (error) { + console.error('Failed to start Docker containers:', error); + // Optionally, try to bring down containers if setup failed partially + try { + execSync(`docker compose -f "${composeFile}" down`, { stdio: 'inherit' }); + } catch (downError) { + console.error('Failed to run docker compose down after setup error:', downError); + } + process.exit(1); // Exit if setup fails + } +}; diff --git a/packages/hathor-rpc-handler/integration-tests/jest.globalTeardown.ts b/packages/hathor-rpc-handler/integration-tests/jest.globalTeardown.ts new file mode 100644 index 00000000..e5e247e8 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/jest.globalTeardown.ts @@ -0,0 +1,16 @@ +import { execSync } from 'child_process'; +import * as path from 'path'; + +const integrationTestsDir = __dirname; // Correctly refers to the integration-tests directory +const composeFile = path.join(integrationTestsDir, 'docker-compose.yml'); + +export default async () => { + console.log('\nTearing down integration test environment...'); + try { + execSync(`docker compose -f "${composeFile}" down -v --remove-orphans`, { stdio: 'inherit' }); + console.log('Docker containers stopped and removed successfully.'); + } catch (error) { + console.error('Failed to stop Docker containers:', error); + // Don't exit here, as tests might have finished, but teardown failed. + } +}; diff --git a/packages/hathor-rpc-handler/integration-tests/test-connection.ts b/packages/hathor-rpc-handler/integration-tests/test-connection.ts new file mode 100644 index 00000000..eb5d0618 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/test-connection.ts @@ -0,0 +1,47 @@ +import Client from '@walletconnect/sign-client'; +import { + initializeWalletConnectClient, + disconnectWalletConnectClient, +} from './utils/walletConnectClient'; + +let client: Client | null = null; + +async function runConnectionTest() { + console.log('Starting standalone connection test...'); + try { + // Initialize client + client = await initializeWalletConnectClient(); + console.log('Client initialized.'); + + // Check initial connection state + console.log(`Initial connection state: ${client.core.relayer.connected}`); + + // Set up event listener without waiting + client.core.relayer.once('relayer_connect', () => { + console.log('>>> relayer_connect event received! <<<'); + console.log(`Connection state after event: ${client!.core.relayer.connected}`); + }); + + client.core.relayer.once('relayer_error', (error: Error) => { + console.error('>>> relayer_error event received! <<<', error); + }); + + // Wait a bit to see if we get the connect event + await new Promise(resolve => setTimeout(resolve, 10000)); + + console.log(`Final connection state: ${client.core.relayer.connected}`); + console.log('Connection test completed.'); + + } catch (error) { + console.error('Connection test failed:', error); + process.exitCode = 1; // Set exit code to indicate failure + } finally { + // Attempt to disconnect + console.log('Attempting to disconnect...'); + await disconnectWalletConnectClient(); + console.log('Standalone test finished.'); + } +} + +// Run the test +runConnectionTest(); diff --git a/packages/hathor-rpc-handler/integration-tests/tsconfig.json b/packages/hathor-rpc-handler/integration-tests/tsconfig.json new file mode 100644 index 00000000..cccd9a63 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "outDir": "dist", + "rootDir": ".", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "lib": ["ES2016", "DOM"], + "types": ["node", "jest"] + }, + "include": [ + "src/**/*.ts", + "jest.config.js", + "jest.globalSetup.ts", + "jest.globalTeardown.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/hathor-rpc-handler/integration-tests/utils/config.ts b/packages/hathor-rpc-handler/integration-tests/utils/config.ts new file mode 100644 index 00000000..2f22a064 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/utils/config.ts @@ -0,0 +1 @@ +export const HATHOR_TEST_SEED = 'castle item critic hand disorder girl across prevent claw fault vacuum combine food debris arrow member autumn half spirit sick priority evil step genius'; diff --git a/packages/hathor-rpc-handler/integration-tests/utils/hathorWallet.ts b/packages/hathor-rpc-handler/integration-tests/utils/hathorWallet.ts new file mode 100644 index 00000000..8e9fd109 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/utils/hathorWallet.ts @@ -0,0 +1,56 @@ +import { HathorWallet } from '@hathor/wallet-lib'; +import { HATHOR_TEST_SEED } from './config'; + +let walletInstance: HathorWallet | null = null; + +/** + * Initializes and returns a singleton HathorWallet instance for testing. + * + * Note: This basic initialization might need adjustments depending on + * specific test requirements (e.g., network, server). + */ +export function initializeHathorWallet(): HathorWallet { + if (walletInstance) { + return walletInstance; + } + + // Basic configuration, adjust as necessary + const walletConfig = { + seed: HATHOR_TEST_SEED, + network: 'testnet', // Assuming testnet for integration tests + connection: undefined, // Use default connection or configure if needed + password: '123', // Default password for testing + pinCode: '123', // Default pin for testing + }; + + walletInstance = new HathorWallet(walletConfig); + + // Start the wallet (important step) + // Note: Starting might involve network requests, ensure environment allows this. + // Consider if starting should happen here or per-test suite. + // For now, let's assume starting here is fine. + // await walletInstance.start(); // Uncomment and handle async if needed + + console.log('Hathor Wallet initialized for testing.'); + return walletInstance; +} + +/** + * Gets the initialized HathorWallet instance. + * Throws an error if the wallet hasn't been initialized. + */ +export function getHathorWallet(): HathorWallet { + if (!walletInstance) { + throw new Error('Hathor Wallet is not initialized. Call initializeHathorWallet first.'); + } + return walletInstance; +} + +// Optional: Add a function to stop/clean up the wallet if necessary +export async function stopHathorWallet(): Promise { + if (walletInstance) { + // await walletInstance.stop(); // Uncomment if wallet needs explicit stopping + walletInstance = null; + console.log('Hathor Wallet stopped.'); + } +} diff --git a/packages/hathor-rpc-handler/integration-tests/utils/virtualDapp.ts b/packages/hathor-rpc-handler/integration-tests/utils/virtualDapp.ts new file mode 100644 index 00000000..d2ee5ba1 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/utils/virtualDapp.ts @@ -0,0 +1,78 @@ +import Client from '@walletconnect/sign-client'; +import { SignClientTypes } from '@walletconnect/types'; +import { + WC_LOGGER_LEVEL, + WC_PROJECT_ID, + WC_RELAY_URL, +} from './walletConnectConfig'; + +/** + * Creates a virtual dApp Client and generates a pairing URI + * Returns the URI and functions to approve or reject the pairing + */ +export async function createVirtualDappPairing() { + // Initialize a separate client to act as a dApp + console.log('Initializing virtual dApp client...'); + const dappMetadata: SignClientTypes.Metadata = { + name: 'Virtual Test dApp', + description: 'A virtual dApp for testing connections', + url: 'https://test.example.com', + icons: ['https://avatars.githubusercontent.com/u/37784886'] + }; + + const dappClient = await Client.init({ + logger: WC_LOGGER_LEVEL, + relayUrl: WC_RELAY_URL, + projectId: WC_PROJECT_ID, + metadata: dappMetadata + }); + + console.log('Virtual dApp client initialized'); + + // Create a new pairing and get the URI + const { uri, approval } = await dappClient.connect({ + requiredNamespaces: { + hathor: { + methods: ['wallet_sendTransaction', 'wallet_getAddresses'], + chains: ['hathor:testnet'], + events: ['chainChanged', 'accountsChanged'] + } + } + }); + + console.log('Created pairing proposal with URI'); + + // Return the URI and functions to approve/reject + return { + pairingUri: uri, + approvePairing: async () => { + console.log('Virtual dApp: Approving pairing...'); + + // Define the session namespaces for approval + const sessionNamespaces = { + hathor: { + accounts: ['hathor:testnet:0xWalletAddress1234567890'], + methods: ['wallet_sendTransaction', 'wallet_getAddresses'], + events: ['chainChanged', 'accountsChanged'] + } + }; + + // The approval function expects sessionNamespaces as a parameter + // @ts-expect-error - WalletConnect type definitions are incorrect + await approval(sessionNamespaces); + console.log('Virtual dApp: Pairing approved'); + }, + rejectPairing: async () => { + console.log('Virtual dApp: Rejecting pairing...'); + try { + // When rejecting, we use an empty function call + // The WalletConnect types are a bit off, but this works + await approval(); + console.log('Virtual dApp: Pairing rejected'); + } catch (error) { + console.error('Error rejecting pairing:', error); + } + }, + dappClient // Return the client for cleanup + }; +} diff --git a/packages/hathor-rpc-handler/integration-tests/utils/walletConnectClient.ts b/packages/hathor-rpc-handler/integration-tests/utils/walletConnectClient.ts new file mode 100644 index 00000000..6664d1a8 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/utils/walletConnectClient.ts @@ -0,0 +1,77 @@ +import Client from '@walletconnect/sign-client'; +import { + WC_APP_METADATA, + WC_LOGGER_LEVEL, + WC_PROJECT_ID, + WC_RELAY_URL, +} from './walletConnectConfig'; + +let clientInstance: Client | null = null; + +/** + * Initializes and returns a singleton WalletConnect Client instance. + */ +export async function initializeWalletConnectClient(): Promise { + if (clientInstance) { + console.log('WalletConnect Client already initialized.'); + return clientInstance; + } + + console.log(`Initializing WalletConnect Client with relay: ${WC_RELAY_URL}`); + console.log(`Using Project ID: ${WC_PROJECT_ID}`); + try { + console.log('Calling Client.init...', { + logger: WC_LOGGER_LEVEL, + relayUrl: WC_RELAY_URL, + projectId: WC_PROJECT_ID, + metadata: WC_APP_METADATA, + }); + clientInstance = await Client.init({ + logger: WC_LOGGER_LEVEL, + relayUrl: WC_RELAY_URL, + projectId: WC_PROJECT_ID, + metadata: WC_APP_METADATA, + }); + console.log('Client.init call completed.'); + + clientInstance.core.relayer.once('relayer_connect', () => { + console.log('RELAYER CONNECT'); + }); + console.log('WalletConnect Client initialized successfully.'); + return clientInstance; + } catch (error) { + console.error('Failed to initialize WalletConnect client:', error); + throw error; // Re-throw error to fail tests if connection fails + } +} + +/** + * Gets the initialized WalletConnect Client instance. + * Throws an error if the client hasn't been initialized. + */ +export function getWalletConnectClient(): Client { + if (!clientInstance) { + throw new Error('WalletConnect client is not initialized. Call initializeWalletConnectClient first.'); + } + return clientInstance; +} + +/** + * Disconnects the WalletConnect client if it's initialized. + */ +export async function disconnectWalletConnectClient(): Promise { + if (clientInstance) { + try { + console.log('Disconnecting WalletConnect Client...'); + // Just nullify the client and let garbage collection handle cleanup + clientInstance = null; + console.log('WalletConnect Client disconnected.'); + } catch (error) { + console.error('Error during WalletConnect client disconnection:', error); + // Ensure client is nullified + clientInstance = null; + } + } else { + console.log('WalletConnect Client was not initialized, no need to disconnect.'); + } +} diff --git a/packages/hathor-rpc-handler/integration-tests/utils/walletConnectConfig.ts b/packages/hathor-rpc-handler/integration-tests/utils/walletConnectConfig.ts new file mode 100644 index 00000000..8cfe4d03 --- /dev/null +++ b/packages/hathor-rpc-handler/integration-tests/utils/walletConnectConfig.ts @@ -0,0 +1,15 @@ +import { SignClientTypes } from '@walletconnect/types'; // Import Metadata type + +// Use the Project ID observed in the working relay log +export const WC_PROJECT_ID = '8264fff563181da658ce64ee80e80458'; + +export const WC_RELAY_URL = 'ws://localhost:5555'; // Keep as localhost for now + +export const WC_LOGGER_LEVEL = 'debug'; + +export const WC_APP_METADATA: SignClientTypes.Metadata = { + name: 'Hathor RPC Integration Test', + description: 'Integration tests for hathor-rpc-handler', + url: '#', // Placeholder URL + icons: [], // Placeholder Icons +}; diff --git a/packages/hathor-rpc-handler/package.json b/packages/hathor-rpc-handler/package.json index 661cddd9..9bb76975 100644 --- a/packages/hathor-rpc-handler/package.json +++ b/packages/hathor-rpc-handler/package.json @@ -14,6 +14,8 @@ "scripts": { "lint": "eslint . --ignore-pattern 'dist/*'", "test": "jest", + "test:integration": "jest --config ./integration-tests/jest.config.js --runInBand --forceExit", + "test:connect": "ts-node ./integration-tests/test-connection.ts", "build": "tsc --declaration", "watch": "tsc -w" }, @@ -23,9 +25,12 @@ "@types/eslint__js": "8.42.3", "@types/jest": "29.5.12", "@types/node": "20.14.2", + "@walletconnect/sign-client": "2.15.1", + "@walletconnect/types": "2.15.1", "eslint": "9.4.0", "jest": "29.7.0", "ts-jest": "29.1.4", + "ts-node": "^10.9.2", "typescript": "5.4.5", "typescript-eslint": "7.13.0" },