diff --git a/src/custom/jstz-signer.ts b/src/custom/jstz-signer.ts index 00e736a..bf160ae 100644 --- a/src/custom/jstz-signer.ts +++ b/src/custom/jstz-signer.ts @@ -1,33 +1,40 @@ import Jstz from '../index'; // Extension events -enum ExtensionRequestEvent { - SIGN = 'JSTZ_SIGNER_SIGN_REQUEST', - GET_ADDRESS = 'JSTZ_SIGNER_GET_ADDRESS_REQUEST', +enum SignerRequestEventTypes { + CHECK_STATUS = 'JSTZ_CHECK_EXTENSION_AVAILABILITY_REQUEST_TO_EXTENSION', + SIGN = 'JSTZ_SIGN_REQUEST_TO_EXTENSION', + GET_ADDRESS = 'JSTZ_GET_ADDRESS_REQUEST_TO_EXTENSION', } -enum ExtensionResponseEvent { - SIGN_RESPONSE = 'JSTZ_SIGNER_SIGN_RESPONSE', - GET_ADDRESS_RESPONSE = 'JSTZ_SIGNER_GET_ADDRESS_RESPONSE', +enum SignerResponseEventTypes { + CHECK_STATUS_RESPONSE = 'JSTZ_CHECK_EXTENSION_AVAILABILITY_RESPONSE_FROM_EXTENSION', + SIGN_RESPONSE = 'JSTZ_SIGN_RESPONSE_FROM_EXTENSION', + GET_ADDRESS_RESPONSE = 'JSTZ_GET_ADDRESS_RESPONSE_FROM_EXTENSION', } interface ExtensionError { + type: SignerResponseEventTypes; error: string; } // Signer requests -interface SignRequest { - type: ExtensionRequestEvent.SIGN; - content: Jstz.Operation.RunFunction; +interface SignRequestCall { + type: SignerRequestEventTypes.SIGN; + content: unknown; } -interface GetSignerAddressRequest { - type: ExtensionRequestEvent.GET_ADDRESS; +interface CheckStatusCall { + type: SignerRequestEventTypes.CHECK_STATUS; +} + +interface GetSignerAddressCall { + type: SignerRequestEventTypes.GET_ADDRESS; } // Extension responses interface ExtensionResponse { - type: ExtensionResponseEvent; + type: SignerResponseEventTypes; data: T; } @@ -42,16 +49,32 @@ interface GetAddressResponse { accountAddress: string; } +interface CheckStatusResponse { + success: boolean; +} + /** * An event dispatcher for Signer extensions */ export class JstzSigner { eventTarget: EventTarget; - constructor(eventTarget: EventTarget) { this.eventTarget = eventTarget; } + private getResponseType(reqType: SignerRequestEventTypes) { + switch (reqType) { + case SignerRequestEventTypes.SIGN: + return SignerResponseEventTypes.SIGN_RESPONSE; + case SignerRequestEventTypes.GET_ADDRESS: + return SignerResponseEventTypes.GET_ADDRESS_RESPONSE; + case SignerRequestEventTypes.CHECK_STATUS: + return SignerResponseEventTypes.CHECK_STATUS_RESPONSE; + default: + throw new Error('Unknown request type'); + } + } + /** * Dispatch a Signer request event on `this.eventTarget`. * Returns a Promise listening for the corresponding `ExtensionResponse` event. @@ -60,38 +83,44 @@ export class JstzSigner { * on `this.eventTarget` and emit an `ExtensionResponse` if succesfully handled or * `ExtensionError` if not */ - callSignerExtension( - payload: SignRequest | GetSignerAddressRequest, + public callSignerExtension( + payload: SignRequestCall | GetSignerAddressCall | CheckStatusCall, + options?: { + timeout?: number; + }, ): Promise> { - function getResponseType(reqType: ExtensionRequestEvent): ExtensionResponseEvent { - switch (reqType) { - case ExtensionRequestEvent.SIGN: - return ExtensionResponseEvent.SIGN_RESPONSE; - case ExtensionRequestEvent.GET_ADDRESS: - return ExtensionResponseEvent.GET_ADDRESS_RESPONSE; - default: - throw new Error('Unknown request type'); - } - } - - // @ts-ignore + // @ts-expect-error - CustomEvent is available in ts DOM lib const event = new CustomEvent(payload.type, { detail: payload, }); this.eventTarget.dispatchEvent(event); + const { timeout } = options || {}; + return new Promise>((resolve, reject) => { + let eventFired = false; + + if (timeout) { + setTimeout(() => { + if (!eventFired) { + reject(new Error('Extension is not responding')); + } + }, timeout); + } + this.eventTarget.addEventListener( - getResponseType(payload.type), - // @ts-ignore + this.getResponseType(payload.type), + // @ts-expect-error - CustomEvent is available in ts DOM lib ((event: CustomEvent>) => { + eventFired = true; + if ('error' in event.detail) { reject(new Error(event.detail.error)); } else { resolve(event.detail); } - // @ts-ignore + // @ts-expect-error - EventListener is available in ts DOM lib }) as EventListener, { once: true }, ); @@ -99,17 +128,15 @@ export class JstzSigner { } } -export declare namespace JstzSigner { - export { - ExtensionResponseEvent, - ExtensionRequestEvent, - type ExtensionError, - type SignRequest, - type GetSignerAddressRequest, - type ExtensionResponse, - type SignResponse, - type GetAddressResponse, - }; -} - -export default JstzSigner; +export { + SignerResponseEventTypes, + SignerRequestEventTypes, + type ExtensionError, + type SignRequestCall, + type GetSignerAddressCall, + type ExtensionResponse, + type SignResponse, + type GetAddressResponse, + type CheckStatusCall, + type CheckStatusResponse, +}; diff --git a/tests/custom/jstz-signer.test.ts b/tests/custom/jstz-signer.test.ts new file mode 100644 index 0000000..d909e42 --- /dev/null +++ b/tests/custom/jstz-signer.test.ts @@ -0,0 +1,67 @@ +import { + JstzSigner, + SignerRequestEventTypes, + SignerResponseEventTypes, + CheckStatusCall, + SignRequestCall, + GetSignerAddressCall, +} from '../../src/custom/jstz-signer'; + +describe('JstzSigner', () => { + let eventTarget: EventTarget; + let signer: JstzSigner; + + beforeEach(() => { + eventTarget = new EventTarget(); + signer = new JstzSigner(eventTarget); + }); + + test('dispatches the correct event', () => { + const dispatchSpy = jest.spyOn(eventTarget, 'dispatchEvent'); + const payload: CheckStatusCall = { type: SignerRequestEventTypes.CHECK_STATUS }; + + signer.callSignerExtension(payload); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: SignerRequestEventTypes.CHECK_STATUS, + detail: payload, + }), + ); + }); + + test('resolves with the correct response', async () => { + const payload: CheckStatusCall = { type: SignerRequestEventTypes.CHECK_STATUS }; + const response = { type: SignerResponseEventTypes.CHECK_STATUS_RESPONSE, data: { success: true } }; + + setTimeout(() => { + // @ts-expect-error - CustomEvent is available in ts DOM lib + const event = new CustomEvent(SignerResponseEventTypes.CHECK_STATUS_RESPONSE, { detail: response }); + eventTarget.dispatchEvent(event); + }, 10); + + await expect(signer.callSignerExtension(payload)).resolves.toEqual(response); + }); + + test('rejects with an error if the response contains an error', async () => { + const payload: SignRequestCall = { type: SignerRequestEventTypes.SIGN, content: null }; + const errorResponse = { type: SignerResponseEventTypes.SIGN_RESPONSE, error: 'Some error occurred' }; + + setTimeout(() => { + // @ts-expect-error - CustomEvent is available in ts DOM lib + const event = new CustomEvent(SignerResponseEventTypes.SIGN_RESPONSE, { detail: errorResponse }); + eventTarget.dispatchEvent(event); + }, 10); + + await expect(signer.callSignerExtension(payload)).rejects.toThrow('Some error occurred'); + }); + + test('rejects if no response is received within the timeout', async () => { + const payload: GetSignerAddressCall = { type: SignerRequestEventTypes.GET_ADDRESS }; + + await expect(signer.callSignerExtension(payload, { timeout: 50 })).rejects.toThrow( + 'Extension is not responding', + ); + }); +});