Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 71 additions & 44 deletions src/custom/jstz-signer.ts
Original file line number Diff line number Diff line change
@@ -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<T = unknown> {
type: ExtensionResponseEvent;
type: SignerResponseEventTypes;
data: T;
}

Expand All @@ -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.
Expand All @@ -60,56 +83,60 @@ export class JstzSigner {
* on `this.eventTarget` and emit an `ExtensionResponse<T>` if succesfully handled or
* `ExtensionError` if not
*/
callSignerExtension<T = SignResponse | GetAddressResponse>(
payload: SignRequest | GetSignerAddressRequest,
public callSignerExtension<T = SignResponse | GetAddressResponse | CheckStatusResponse>(
payload: SignRequestCall | GetSignerAddressCall | CheckStatusCall,
options?: {
timeout?: number;
},
): Promise<ExtensionResponse<T>> {
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<typeof payload>(payload.type, {
detail: payload,
});

this.eventTarget.dispatchEvent(event);

const { timeout } = options || {};

return new Promise<ExtensionResponse<T>>((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<ExtensionError | ExtensionResponse<T>>) => {
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 },
);
});
}
}

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,
};
67 changes: 67 additions & 0 deletions tests/custom/jstz-signer.test.ts
Original file line number Diff line number Diff line change
@@ -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',
);
});
});