diff --git a/packages/sdk/src/mintlayer-connect-sdk.ts b/packages/sdk/src/mintlayer-connect-sdk.ts index cf94cf3..1d00e93 100644 --- a/packages/sdk/src/mintlayer-connect-sdk.ts +++ b/packages/sdk/src/mintlayer-connect-sdk.ts @@ -46,6 +46,7 @@ import initWasm, { SignatureHashType, encode_output_htlc, extract_htlc_secret, + verify_challenge, } from '@mintlayer/wasm-lib'; const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; @@ -877,6 +878,12 @@ export type SignChallengeResponse = { signature: string; }; +export type VerifyChallengeArgs = { + message: string; + address: string; + signature: string; +}; + class Client { private network: 'mainnet' | 'testnet'; private connectedAddresses: { @@ -3605,6 +3612,25 @@ class Client { }); } + /** + * Verifies a signed challenge message. + * Used to verify that a signature was produced by the private key corresponding to the given address. + * + * Note: The provided address must be a 'pubkeyhash' address. + * + * @param args - Object containing message, address, and signature + * @returns Promise that resolves to true if the signature is valid, throws an error otherwise + */ + async verifyChallenge(args: VerifyChallengeArgs): Promise { + this.ensureInitialized(); + + const messageBytes = stringToUint8Array(args.message); + const signatureBytes = hexToUint8Array(args.signature); + const network = this.getMLNetwork(); + + return verify_challenge(args.address, network, signatureBytes, messageBytes); + } + /** * Requests a secret hash from the wallet for HTLC operations. * @param args - Additional arguments (currently unused) diff --git a/packages/sdk/tests/verify-challenge.test.ts b/packages/sdk/tests/verify-challenge.test.ts new file mode 100644 index 0000000..8f623ef --- /dev/null +++ b/packages/sdk/tests/verify-challenge.test.ts @@ -0,0 +1,75 @@ +import { Client } from '../src/mintlayer-connect-sdk'; +import fetchMock from 'jest-fetch-mock'; + +beforeEach(() => { + fetchMock.resetMocks(); + + (window as any).mojito = { + isExtension: true, + connect: jest.fn().mockResolvedValue({ + addressesByChain: { + mintlayer: { + receiving: ['tmt1q9cz2dkuqqrdv2g3kl8zqvjvqtu3u6j3j8z9z9z9'], + change: ['tmt1q9cz2dkuqqrdv2g3kl8zqvjvqtu3u6j3j8z9z9z8'], + }, + }, + }), + restore: jest.fn().mockResolvedValue({ + testnet: { + receiving: ['tmt1q9cz2dkuqqrdv2g3kl8zqvjvqtu3u6j3j8z9z9z9'], + }, + }), + disconnect: jest.fn().mockResolvedValue(undefined), + request: jest.fn().mockResolvedValue('signed-transaction'), + }; + + fetchMock.doMock(); + + fetchMock.mockResponse(async req => { + const url = req.url; + + if (url.endsWith('/chain/tip')) { + return JSON.stringify({ height: 200000 }); + } + + console.warn('No mock for:', url); + return JSON.stringify({ error: 'No mock defined' }); + }); +}); + +test('verifyChallenge should verify a valid signature', async () => { + const client = await Client.create({ + network: 'testnet', + autoRestore: false, + }); + + await client.connect(); + + // This is a test case - in real usage, the signature would come from signChallenge + // For now, we're testing that the method exists and can be called + const testMessage = 'Hello, Mintlayer!'; + const testAddress = 'tmt1q9cz2dkuqqrdv2g3kl8zqvjvqtu3u6j3j8z9z9z9'; + const testSignature = '0123456789abcdef'; // This would be a real signature in practice + + // The actual verification will fail with invalid signature, but we're testing the method exists + try { + await client.verifyChallenge({ + message: testMessage, + address: testAddress, + signature: testSignature, + }); + } catch (error) { + // Expected to fail with invalid signature, but the method should exist and be callable + expect(error).toBeDefined(); + } +}); + +test('verifyChallenge should be a function on Client', async () => { + const client = await Client.create({ + network: 'testnet', + autoRestore: false, + }); + + expect(typeof client.verifyChallenge).toBe('function'); +}); +