From 5a713b1880a2ba49f74186a3d5580a4d067fc8b1 Mon Sep 17 00:00:00 2001 From: Paul Young Date: Fri, 11 Oct 2019 17:37:28 -0700 Subject: [PATCH 1/5] Add HTTP agent --- js-user-library/package-lock.json | 6 + js-user-library/package.json | 3 +- js-user-library/src/callRequest.ts | 12 + js-user-library/src/httpAgent.test.ts | 139 ++++++++++ js-user-library/src/httpAgent.ts | 275 +++++++++++++++++++ js-user-library/src/index.ts | 1 + js-user-library/src/nonce.ts | 12 + js-user-library/src/queryRequest.ts | 12 + js-user-library/src/queryResponse.ts | 24 ++ js-user-library/src/readRequest.ts | 11 +- js-user-library/src/readRequestType.ts | 5 + js-user-library/src/rejectCode.ts | 7 + js-user-library/src/request.ts | 20 +- js-user-library/src/requestId.test.ts | 6 +- js-user-library/src/requestId.ts | 12 +- js-user-library/src/requestStatusRequest.ts | 9 + js-user-library/src/requestStatusResponse.ts | 36 +++ js-user-library/src/requestType.ts | 8 +- js-user-library/src/response.ts | 1 + js-user-library/src/senderPubKey.ts | 3 + js-user-library/src/senderSig.ts | 3 + js-user-library/src/submitRequest.ts | 16 +- js-user-library/src/submitRequestType.ts | 4 + js-user-library/src/submitResponse.ts | 7 + js-user-library/test-setup.js | 1 + 25 files changed, 599 insertions(+), 34 deletions(-) create mode 100644 js-user-library/src/callRequest.ts create mode 100644 js-user-library/src/httpAgent.test.ts create mode 100644 js-user-library/src/httpAgent.ts create mode 100644 js-user-library/src/index.ts create mode 100644 js-user-library/src/nonce.ts create mode 100644 js-user-library/src/queryRequest.ts create mode 100644 js-user-library/src/queryResponse.ts create mode 100644 js-user-library/src/readRequestType.ts create mode 100644 js-user-library/src/rejectCode.ts create mode 100644 js-user-library/src/requestStatusRequest.ts create mode 100644 js-user-library/src/requestStatusResponse.ts create mode 100644 js-user-library/src/response.ts create mode 100644 js-user-library/src/senderPubKey.ts create mode 100644 js-user-library/src/senderSig.ts create mode 100644 js-user-library/src/submitRequestType.ts create mode 100644 js-user-library/src/submitResponse.ts diff --git a/js-user-library/package-lock.json b/js-user-library/package-lock.json index e5c3f43b14..71a3296d16 100644 --- a/js-user-library/package-lock.json +++ b/js-user-library/package-lock.json @@ -6150,6 +6150,12 @@ "iconv-lite": "0.4.24" } }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", + "dev": true + }, "whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", diff --git a/js-user-library/package.json b/js-user-library/package.json index 45d6ba65c3..39c26af23a 100644 --- a/js-user-library/package.json +++ b/js-user-library/package.json @@ -32,6 +32,7 @@ "jest": "^24.9.0", "jest-expect-message": "^1.0.2", "tslint": "^5.20.0", - "typescript": "^3.6.3" + "typescript": "^3.6.3", + "whatwg-fetch": "^3.0.0" } } diff --git a/js-user-library/src/callRequest.ts b/js-user-library/src/callRequest.ts new file mode 100644 index 0000000000..0f7de81836 --- /dev/null +++ b/js-user-library/src/callRequest.ts @@ -0,0 +1,12 @@ +import { BinaryBlob } from "./blob"; +import { CanisterId } from "./canisterId"; +import { Request } from "./request"; +import { SubmitRequestType } from "./submitRequestType"; + +// The fields in a "call" submit request. +export interface CallRequest extends Request { + request_type: SubmitRequestType.Call; + canister_id: CanisterId; + method_name: string; + arg: BinaryBlob; +} diff --git a/js-user-library/src/httpAgent.test.ts b/js-user-library/src/httpAgent.test.ts new file mode 100644 index 0000000000..d138305990 --- /dev/null +++ b/js-user-library/src/httpAgent.test.ts @@ -0,0 +1,139 @@ +import { BinaryBlob } from "./blob"; +import * as canisterId from "./canisterId"; +import * as cbor from "./cbor"; +import { Hex } from "./hex"; +import { makeHttpAgent } from "./index"; +import { Nonce } from "./nonce"; +import { Request } from "./request"; +import { requestIdOf } from "./requestId"; +import { RequestType } from "./requestType"; +import { SenderPubKey } from "./senderPubKey"; +import { SenderSig } from "./senderSig"; + +test("call", async () => { + const mockFetch: jest.Mock = jest.fn((resource, init) => { + return Promise.resolve(new Response(null, { + status: 200, + })); + }); + + const canisterIdent = "0000000000000001" as Hex; + const nonce = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]) as Nonce; + const senderPubKey = new Uint8Array(32) as SenderPubKey; + const senderSig = new Uint8Array(64) as SenderSig; + + const httpAgent = makeHttpAgent({ + canisterId: canisterIdent, + fetchFn: mockFetch, + nonceFn: () => nonce, + senderPubKey, + senderSigFn: () => senderSig, + }); + + const methodName = "greet"; + const arg = Uint8Array.from([]) as BinaryBlob; + + const { requestId, response } = await httpAgent.call({ + methodName, + arg, + }); + + const expectedRequest: Request = { + request_type: "call" as RequestType, + nonce, + canister_id: canisterId.fromHex(canisterIdent), + method_name: methodName, + arg, + sender_pubkey: senderPubKey, + sender_sig: senderSig, + }; + + const expectedRequestId = await requestIdOf(expectedRequest); + + const { calls, results } = mockFetch.mock; + expect(calls.length).toBe(1); + expect(requestId).toEqual(expectedRequestId); + + expect(calls[0][0]).toBe("http://localhost:8000/api/v1/submit"); + expect(calls[0][1]).toEqual({ + method: "POST", + headers: { + "Content-Type": "application/cbor", + }, + body: cbor.encode(expectedRequest), + }); +}); + +test.todo("query"); + +test("requestStatus", async () => { + const mockResponse = { + status: "replied", + reply: { arg: Uint8Array.from([]) as BinaryBlob }, + }; + + const mockFetch: jest.Mock = jest.fn((resource, init) => { + const body = cbor.encode(mockResponse); + return Promise.resolve(new Response(body, { + status: 200, + })); + }); + + const canisterIdent = "0000000000000001" as Hex; + const nonce = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]) as Nonce; + const senderPubKey = new Uint8Array(32) as SenderPubKey; + const senderSig = new Uint8Array(64) as SenderSig; + + const httpAgent = makeHttpAgent({ + canisterId: canisterIdent, + fetchFn: mockFetch, + nonceFn: () => nonce, + senderPubKey, + senderSigFn: () => senderSig, + }); + + const requestId = await requestIdOf({ + request_type: "call" as RequestType, + nonce, + canister_id: canisterId.fromHex(canisterIdent), + method_name: "greet", + arg: Uint8Array.from([]), + }); + + const response = await httpAgent.requestStatus({ + requestId, + }); + + const expectedRequest: Request = { + request_type: "request-status" as RequestType, + nonce, + request_id: requestId, + sender_pubkey: senderPubKey, + sender_sig: senderSig, + }; + + const { calls, results } = mockFetch.mock; + expect(calls.length).toBe(1); + + const { + reply: { arg: responseArg }, + ...responseRest + } = response; + + const { + reply: { arg: mockResponseArg }, + ...mockResponseRest + } = mockResponse; + + expect(responseRest).toEqual(mockResponseRest); + expect(responseArg.equals(mockResponseArg)).toBe(true); + + expect(calls[0][0]).toBe("http://localhost:8000/api/v1/read"); + expect(calls[0][1]).toEqual({ + method: "POST", + headers: { + "Content-Type": "application/cbor", + }, + body: cbor.encode(expectedRequest), + }); +}); diff --git a/js-user-library/src/httpAgent.ts b/js-user-library/src/httpAgent.ts new file mode 100644 index 0000000000..3b7cd19e7a --- /dev/null +++ b/js-user-library/src/httpAgent.ts @@ -0,0 +1,275 @@ +import { BinaryBlob } from "./blob"; +import { CallRequest } from "./callRequest"; +import { CanisterId } from "./canisterId"; +import * as canisterId from "./canisterId"; +import * as cbor from "./cbor"; +import { Hex } from "./hex"; +import { makeNonce, Nonce } from "./nonce"; +import { QueryRequest } from "./queryRequest"; +import { QueryResponse } from "./queryResponse"; +import { ReadRequest } from "./readRequest"; +import { ReadRequestType } from "./readRequestType"; +import { RequestId, requestIdOf } from "./requestId"; +import { RequestStatusRequest } from "./requestStatusRequest"; +import { RequestStatusResponse } from "./requestStatusResponse"; +import { Response } from "./response"; +import { SenderPubKey } from "./senderPubKey"; +import { SenderSig } from "./senderSig"; +import { SubmitRequest } from "./submitRequest"; +import { SubmitRequestType } from "./submitRequestType"; +import { SubmitResponse } from "./submitResponse"; + +// A HTTP agent allows users to interact with a client of the internet computer +// using the available methods. It exposes an API that closely follows the +// public view of the internet computer, and is not intended to be exposed +// directly to the majority of users due to its low-level interface. +export const makeHttpAgent = (options: Options): HttpAgent => { + const config = makeConfig(options); + return { + call: call(config), + requestStatus: requestStatus(config), + query: query(config), + }; +}; + +export interface HttpAgent { + call(fields: { + methodName: string, + arg: BinaryBlob, + }): Promise; + + query(fields: { + methodName: string, + arg: BinaryBlob, + }): Promise; + + requestStatus(fields: { + requestId: RequestId, + }): Promise; +} + +// `Options` is the external representation of `Config` that allows us to +// provide optional fields with default values. +interface Options { + canisterId: Hex; + fetchFn?: WindowOrWorkerGlobalScope["fetch"]; + host?: string; + nonceFn?: () => Nonce; + senderPubKey: SenderPubKey; + senderSigFn?: (requestId: RequestId) => SenderSig; +} + +interface DefaultOptions { + fetchFn: WindowOrWorkerGlobalScope["fetch"]; + host: string; + nonceFn: () => Nonce; + senderSigFn: (requestId: RequestId) => SenderSig; +} + +const defaultOptions: DefaultOptions = { + fetchFn: typeof window === "undefined" ? fetch : window.fetch.bind(window), + host: "http://localhost:8000", + nonceFn: makeNonce, + senderSigFn: (requestId: RequestId): SenderSig => { + // TODO: calculate signature using `requestId` + return new Uint8Array(64) as SenderSig; + }, +}; + + +// `Config` is the internal representation if `Options`. +interface Config { + canisterId: CanisterId; + host: string; + nonceFn: () => Nonce; + senderPubKey: SenderPubKey; + runFetch(endpoint: Endpoint, body?: BodyInit | null): Promise; + senderSigFn(requestId: RequestId): SenderSig; +} + +const API_VERSION = "v1"; + +const makeConfig = (options: Options): Config => { + const withDefaults = { ...defaultOptions, ...options }; + return { + ...withDefaults, + canisterId: canisterId.fromHex(options.canisterId), + runFetch: (endpoint, body) => { + return withDefaults.fetchFn(`${withDefaults.host}/api/${API_VERSION}/${endpoint}`, { + method: "POST", + headers: { + "Content-Type": "application/cbor", + }, + body, + }); + }, + }; +}; + + +enum Endpoint { + Read = "read", + Submit = "submit", +} + + +// Execute a read request +const read = ( + config: Config, +) => async ( + request: ReadRequest, +): Promise => { + const body = cbor.encode(request); + return config.runFetch(Endpoint.Read, body); +}; + +// Execute a submit request +const submit = ( + config: Config, +) => async ( + request: SubmitRequest, +): Promise => { + const body = cbor.encode(request); + const response = await config.runFetch(Endpoint.Submit, body); + const requestId = await requestIdOf(request); + return { requestId, response }; +}; + + +// Execute a "call" request +const call = ( + config: Config, +) => async ({ + methodName, + arg, +}: { + methodName: string, + arg: BinaryBlob, +}): Promise => { + const request = await makeCallRequest(config, { + methodName, + arg, + }); + return submit(config)(request); +}; + +// Construct a "call" request. +const makeCallRequest = async ( + config: Config, + { + methodName, + arg, + }: { + methodName: string, + arg: BinaryBlob, + }, +): Promise => { + // TypeScript complains about `request_type` unless we manually add it to the + // return value, even though it's already present. + const requestType = SubmitRequestType.Call; + const fields = { + request_type: requestType, + nonce: config.nonceFn(), + canister_id: config.canisterId, + method_name: methodName, + arg, + }; + const requestId = await requestIdOf(fields); + return { + ...fields, + request_type: requestType, + sender_pubkey: config.senderPubKey, + sender_sig: config.senderSigFn(requestId), + }; +}; + + +// Construct a query request +const makeQueryRequest = async ( + config: Config, + { + methodName, + arg, + }: { + methodName: string, + arg: BinaryBlob, + }, +): Promise => { + // TypeScript complains about `request_type` unless we manually add it to the + // return value, even though it's already present. + const requestType = ReadRequestType.Query; + const fields = { + request_type: requestType, + nonce: config.nonceFn(), + canister_id: config.canisterId, + method_name: methodName, + arg, + }; + const requestId = await requestIdOf(fields); + return { + ...fields, + request_type: requestType, + sender_pubkey: config.senderPubKey, + sender_sig: config.senderSigFn(requestId), + }; +}; + +// Execute a query request +const query = ( + config: Config, +) => async ({ + methodName, + arg, +}: { + methodName: string, + arg: BinaryBlob, +}): Promise => { + const request = await makeQueryRequest(config, { + methodName, + arg, + }); + const response = await read(config)(request); + const body = Uint8Array.from(await response.arrayBuffer()); + return cbor.decode(body) as QueryResponse; +}; + + +// Execute a request status request +const requestStatus = ( + config: Config, +) => async ({ + requestId, +}: { + requestId: RequestId, +}): Promise => { + const request = await makeRequestStatusRequest(config, { requestId }); + const response = await read(config)(request); + const body = Uint8Array.from(await response.arrayBuffer()); + return cbor.decode(body) as RequestStatusResponse; +}; + +// Construct a request status request +const makeRequestStatusRequest = async ( + config: Config, + { + requestId, + }: { + requestId: RequestId, + }, +): Promise => { + // TypeScript complains about `request_type` unless we manually add it to the + // return value, even though it's already present. + const requestType = ReadRequestType.RequestStatus; + const fields = { + request_type: requestType, + nonce: config.nonceFn(), + request_id: requestId, + }; + const currentRequestId = await requestIdOf(fields); + return { + ...fields, + request_type: requestType, + sender_pubkey: config.senderPubKey, + sender_sig: config.senderSigFn(currentRequestId), + }; +}; diff --git a/js-user-library/src/index.ts b/js-user-library/src/index.ts new file mode 100644 index 0000000000..7a323775eb --- /dev/null +++ b/js-user-library/src/index.ts @@ -0,0 +1 @@ +export * from "./httpAgent"; diff --git a/js-user-library/src/nonce.ts b/js-user-library/src/nonce.ts new file mode 100644 index 0000000000..a281fc7417 --- /dev/null +++ b/js-user-library/src/nonce.ts @@ -0,0 +1,12 @@ +import { BinaryBlob } from "./blob"; + +export type Nonce = BinaryBlob & { __nonce__: void }; + +export const makeNonce = (): Nonce => { + return makeNonceFromDate(new Date()); +}; + +const makeNonceFromDate = (date: Date): Nonce => { + const ints = date.getTime().toString().split("").map((x) => parseInt(x, 10)); + return Uint8Array.from(ints) as Nonce; +}; diff --git a/js-user-library/src/queryRequest.ts b/js-user-library/src/queryRequest.ts new file mode 100644 index 0000000000..cb64528f9f --- /dev/null +++ b/js-user-library/src/queryRequest.ts @@ -0,0 +1,12 @@ +import { BinaryBlob } from "./blob"; +import { CanisterId } from "./canisterId"; +import { ReadRequestType } from "./readRequestType"; +import { Request } from "./request"; + +// The fields in a "query" read request. +export interface QueryRequest extends Request { + request_type: ReadRequestType.Query; + canister_id: CanisterId; + method_name: string; + arg: BinaryBlob; +} diff --git a/js-user-library/src/queryResponse.ts b/js-user-library/src/queryResponse.ts new file mode 100644 index 0000000000..d167688e7c --- /dev/null +++ b/js-user-library/src/queryResponse.ts @@ -0,0 +1,24 @@ +import { BinaryBlob } from "./blob"; +import { RejectCode } from "./rejectCode"; +import { Response } from "./response"; + +// An ADT that represents responses to a "query" read request. +export type QueryResponse + = QueryResponseReplied + | QueryResponseRejected; + +interface QueryResponseReplied extends Response { + status: QueryResponseStatus.Replied; + reply: { arg: BinaryBlob }; +} + +interface QueryResponseRejected extends Response { + status: QueryResponseStatus.Rejected; + reject_code: RejectCode; + reject_message: string; +} + +enum QueryResponseStatus { + Replied = "replied", + Rejected = "rejected", +} diff --git a/js-user-library/src/readRequest.ts b/js-user-library/src/readRequest.ts index 37e45a1331..28b57880cd 100644 --- a/js-user-library/src/readRequest.ts +++ b/js-user-library/src/readRequest.ts @@ -1,5 +1,6 @@ -// The types of values allowed in the `request_type` field for read requests. -export enum ReadRequestType { - Query = "query", - RequestStatus = "request-status", -} +import { QueryRequest } from "./queryRequest"; +import { RequestStatusRequest } from "./requestStatusRequest"; + +export type ReadRequest + = QueryRequest + | RequestStatusRequest; diff --git a/js-user-library/src/readRequestType.ts b/js-user-library/src/readRequestType.ts new file mode 100644 index 0000000000..37e45a1331 --- /dev/null +++ b/js-user-library/src/readRequestType.ts @@ -0,0 +1,5 @@ +// The types of values allowed in the `request_type` field for read requests. +export enum ReadRequestType { + Query = "query", + RequestStatus = "request-status", +} diff --git a/js-user-library/src/rejectCode.ts b/js-user-library/src/rejectCode.ts new file mode 100644 index 0000000000..9a6044d31f --- /dev/null +++ b/js-user-library/src/rejectCode.ts @@ -0,0 +1,7 @@ +export enum RejectCode { + SysFatal = 1, + SysTransient = 2, + DestinationInvalid = 3, + CanisterReject = 4, + CanisterError = 5, +} diff --git a/js-user-library/src/request.ts b/js-user-library/src/request.ts index 6c225871cd..ddd1399f80 100644 --- a/js-user-library/src/request.ts +++ b/js-user-library/src/request.ts @@ -1,14 +1,22 @@ -import { BinaryBlob } from "./blob"; +import { Nonce } from "./nonce"; import { RequestType } from "./requestType"; +import { SenderPubKey } from "./senderPubKey"; +import { SenderSig } from "./senderSig"; + +export interface AuthFields extends Record { + sender_pubkey: SenderPubKey; + sender_sig: SenderSig; +} -// Common request fields. // TODO: add missing common fields from the spec; `expiry` and `sender` -export interface Request extends Record { +export interface CommonFields extends Record { request_type: RequestType; // NOTE: `nonce`, but we provide it so that requests are unique and we avoid a // bug in the client when the same request is submitted more than once: // https://dfinity.atlassian.net/browse/DFN-895 - nonce?: BinaryBlob; - sender_pubkey: BinaryBlob; - sender_sig: BinaryBlob; + nonce?: Nonce; } + +export type Request + = AuthFields + & CommonFields; diff --git a/js-user-library/src/requestId.test.ts b/js-user-library/src/requestId.test.ts index 1656a4be79..d7d8469fa9 100644 --- a/js-user-library/src/requestId.test.ts +++ b/js-user-library/src/requestId.test.ts @@ -6,6 +6,8 @@ import * as blob from "./blob"; import { Request } from "./request"; import { hash, requestIdOf } from "./requestId"; import { RequestType } from "./requestType"; +import { SenderPubKey } from "./senderPubKey"; +import { SenderSig } from "./senderSig"; const testHashOfBlob = async (input: BinaryBlob, expected: string) => { const hashed = await hash(input); @@ -78,8 +80,8 @@ test("requestIdOf", async () => { // These fields are not included in the example provided in the spec but we // provide them here to verify that they do not affect the request ID: // "Remove the fields that are only used for authentication" - sender_pubkey: new Uint8Array(32) as BinaryBlob, - sender_sig: new Uint8Array(64) as BinaryBlob, + sender_pubkey: new Uint8Array(32) as SenderPubKey, + sender_sig: new Uint8Array(64) as SenderSig, }; const requestId = await requestIdOf(request); diff --git a/js-user-library/src/requestId.ts b/js-user-library/src/requestId.ts index 9aef5719fd..ff427cc82f 100644 --- a/js-user-library/src/requestId.ts +++ b/js-user-library/src/requestId.ts @@ -5,7 +5,7 @@ import { CborValue } from "./cbor"; import { Hex } from "./hex"; import * as int from "./int"; import { Int } from "./int"; -import { Request } from "./request"; +import * as Request from "./request"; export type RequestId = BinaryBlob & { __requestId__: void }; @@ -58,7 +58,15 @@ const concat = (bs: Array): BinaryBlob => { }, new Uint8Array()) as BinaryBlob; }; -export const requestIdOf = async (request: Request): Promise => { +export const requestIdOf = async ( + request: Request.CommonFields, +): Promise => { + // While the type signature of this function ensures the fields we care about + // are present, it does not prevent additional fields from being provided, + // including the fields used for authentication that we must omit when + // calculating the request ID. This is by design, since requests are expected + // to have more than just the common fields. As a result, we need to explictly + // ignore the authentication fields. const { sender_pubkey, sender_sig, ...fields } = request; const hashed: Array> = Object diff --git a/js-user-library/src/requestStatusRequest.ts b/js-user-library/src/requestStatusRequest.ts new file mode 100644 index 0000000000..3e4d887511 --- /dev/null +++ b/js-user-library/src/requestStatusRequest.ts @@ -0,0 +1,9 @@ +import { ReadRequestType } from "./readRequestType"; +import { Request } from "./request"; +import { RequestId } from "./requestId"; + +// The fields in a "request-status" read request. +export interface RequestStatusRequest extends Request { + request_type: ReadRequestType.RequestStatus; + request_id: RequestId; +} diff --git a/js-user-library/src/requestStatusResponse.ts b/js-user-library/src/requestStatusResponse.ts new file mode 100644 index 0000000000..f6af760967 --- /dev/null +++ b/js-user-library/src/requestStatusResponse.ts @@ -0,0 +1,36 @@ +import { BinaryBlob } from "./blob"; +import { RejectCode } from "./rejectCode"; +import { Response } from "./response"; + +// An ADT that represents responses to a "request-status" read request. +export type RequestStatusResponse + = RequestStatusResponsePending + | RequestStatusResponseReplied + | RequestStatusResponseRejected + | RequestStatusResponseUnknown; + +interface RequestStatusResponsePending extends Response { + status: RequestStatusResponseStatus.Pending; +} + +interface RequestStatusResponseReplied extends Response { + status: RequestStatusResponseStatus.Replied; + reply: { arg: BinaryBlob }; +} + +interface RequestStatusResponseRejected extends Response { + status: RequestStatusResponseStatus.Rejected; + reject_code: RejectCode; + reject_message: string; +} + +interface RequestStatusResponseUnknown extends Response { + status: RequestStatusResponseStatus.Unknown; +} + +export enum RequestStatusResponseStatus { + Pending = "pending", + Replied = "replied", + Rejected = "rejected", + Unknown = "unknown", +} diff --git a/js-user-library/src/requestType.ts b/js-user-library/src/requestType.ts index aa5a631b0a..fe2c58072c 100644 --- a/js-user-library/src/requestType.ts +++ b/js-user-library/src/requestType.ts @@ -1,4 +1,6 @@ -import { ReadRequestType } from "./readRequest"; -import { SubmitRequestType } from "./submitRequest"; +import { ReadRequestType } from "./readRequestType"; +import { SubmitRequestType } from "./submitRequestType"; -export type RequestType = ReadRequestType | SubmitRequestType; +export type RequestType + = ReadRequestType + | SubmitRequestType; diff --git a/js-user-library/src/response.ts b/js-user-library/src/response.ts new file mode 100644 index 0000000000..d961506b97 --- /dev/null +++ b/js-user-library/src/response.ts @@ -0,0 +1 @@ +export interface Response extends Record {} diff --git a/js-user-library/src/senderPubKey.ts b/js-user-library/src/senderPubKey.ts new file mode 100644 index 0000000000..011546fd87 --- /dev/null +++ b/js-user-library/src/senderPubKey.ts @@ -0,0 +1,3 @@ +import { BinaryBlob } from "./blob"; + +export type SenderPubKey = BinaryBlob & { __senderPubKey__: void }; diff --git a/js-user-library/src/senderSig.ts b/js-user-library/src/senderSig.ts new file mode 100644 index 0000000000..f168c274ae --- /dev/null +++ b/js-user-library/src/senderSig.ts @@ -0,0 +1,3 @@ +import { BinaryBlob } from "./blob"; + +export type SenderSig = BinaryBlob & { __senderSig__: void }; diff --git a/js-user-library/src/submitRequest.ts b/js-user-library/src/submitRequest.ts index f1953173f5..07e49a3f32 100644 --- a/js-user-library/src/submitRequest.ts +++ b/js-user-library/src/submitRequest.ts @@ -1,19 +1,5 @@ -import { BinaryBlob } from "./blob"; -import { CanisterId } from "./canisterId"; +import { CallRequest } from "./callRequest"; // An ADT that represents requests to the "submit" endpoint. export type SubmitRequest = CallRequest; - -// The types of values allowed in the `request_type` field for submit requests. -export enum SubmitRequestType { - Call = "call", -} - -// The fields in a "call" submit request. -interface CallRequest extends Request { - request_type: SubmitRequestType.Call; - canister_id: CanisterId; - method_name: string; - arg: BinaryBlob; -} diff --git a/js-user-library/src/submitRequestType.ts b/js-user-library/src/submitRequestType.ts new file mode 100644 index 0000000000..f3b50aaf26 --- /dev/null +++ b/js-user-library/src/submitRequestType.ts @@ -0,0 +1,4 @@ +// The types of values allowed in the `request_type` field for submit requests. +export enum SubmitRequestType { + Call = "call", +} diff --git a/js-user-library/src/submitResponse.ts b/js-user-library/src/submitResponse.ts new file mode 100644 index 0000000000..81ad745f4e --- /dev/null +++ b/js-user-library/src/submitResponse.ts @@ -0,0 +1,7 @@ +import { RequestId } from "./requestId"; +import { Response } from "./response"; + +export interface SubmitResponse extends Response { + requestId: RequestId; + response: Response; +} diff --git a/js-user-library/test-setup.js b/js-user-library/test-setup.js index b1465b0aae..f2c1865c9c 100644 --- a/js-user-library/test-setup.js +++ b/js-user-library/test-setup.js @@ -9,3 +9,4 @@ window.crypto = require("@trust/webcrypto"); window.TextEncoder = require("text-encoding").TextEncoder; +require("whatwg-fetch"); From d96bb4a36524f31420b17a8c5f0aa72b0cfb9d1d Mon Sep 17 00:00:00 2001 From: Paul Young Date: Mon, 14 Oct 2019 11:10:17 -0700 Subject: [PATCH 2/5] Fix a typo --- js-user-library/src/httpAgent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-user-library/src/httpAgent.ts b/js-user-library/src/httpAgent.ts index 3b7cd19e7a..1aef2d8487 100644 --- a/js-user-library/src/httpAgent.ts +++ b/js-user-library/src/httpAgent.ts @@ -77,7 +77,7 @@ const defaultOptions: DefaultOptions = { }; -// `Config` is the internal representation if `Options`. +// `Config` is the internal representation of `Options`. interface Config { canisterId: CanisterId; host: string; From 369d7764ba5fd66fe4e6778eaeae746c7c476b4d Mon Sep 17 00:00:00 2001 From: Paul Young Date: Mon, 14 Oct 2019 11:11:33 -0700 Subject: [PATCH 3/5] Use an explicit export list --- js-user-library/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js-user-library/src/index.ts b/js-user-library/src/index.ts index 7a323775eb..8320164c30 100644 --- a/js-user-library/src/index.ts +++ b/js-user-library/src/index.ts @@ -1 +1 @@ -export * from "./httpAgent"; +export { HttpAgent, makeHttpAgent } from "./httpAgent"; From 3b3524db0b11c562a7a01886dad5b9e21ade30f5 Mon Sep 17 00:00:00 2001 From: Paul Young Date: Mon, 14 Oct 2019 11:20:49 -0700 Subject: [PATCH 4/5] Differentiate async request fields as per the spec --- js-user-library/src/callRequest.ts | 4 ++-- js-user-library/src/request.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/js-user-library/src/callRequest.ts b/js-user-library/src/callRequest.ts index 0f7de81836..5f021862ed 100644 --- a/js-user-library/src/callRequest.ts +++ b/js-user-library/src/callRequest.ts @@ -1,10 +1,10 @@ import { BinaryBlob } from "./blob"; import { CanisterId } from "./canisterId"; -import { Request } from "./request"; +import { AsyncRequest } from "./request"; import { SubmitRequestType } from "./submitRequestType"; // The fields in a "call" submit request. -export interface CallRequest extends Request { +export interface CallRequest extends AsyncRequest { request_type: SubmitRequestType.Call; canister_id: CanisterId; method_name: string; diff --git a/js-user-library/src/request.ts b/js-user-library/src/request.ts index ddd1399f80..51621c6694 100644 --- a/js-user-library/src/request.ts +++ b/js-user-library/src/request.ts @@ -9,14 +9,21 @@ export interface AuthFields extends Record { } // TODO: add missing common fields from the spec; `expiry` and `sender` -export interface CommonFields extends Record { - request_type: RequestType; +export interface AsyncFields extends Record { // NOTE: `nonce`, but we provide it so that requests are unique and we avoid a // bug in the client when the same request is submitted more than once: // https://dfinity.atlassian.net/browse/DFN-895 nonce?: Nonce; } +export interface CommonFields extends Record { + request_type: RequestType; +} + export type Request = AuthFields & CommonFields; + +export type AsyncRequest + = Request + & AsyncFields; From 07a29286c312c5babe55252cb21a70a142a9d2c5 Mon Sep 17 00:00:00 2001 From: Paul Young Date: Mon, 14 Oct 2019 11:21:44 -0700 Subject: [PATCH 5/5] Fix comment about providing a nonce --- js-user-library/src/request.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js-user-library/src/request.ts b/js-user-library/src/request.ts index 51621c6694..9bcac1dd98 100644 --- a/js-user-library/src/request.ts +++ b/js-user-library/src/request.ts @@ -10,9 +10,9 @@ export interface AuthFields extends Record { // TODO: add missing common fields from the spec; `expiry` and `sender` export interface AsyncFields extends Record { - // NOTE: `nonce`, but we provide it so that requests are unique and we avoid a - // bug in the client when the same request is submitted more than once: - // https://dfinity.atlassian.net/browse/DFN-895 + // NOTE: `nonce` is optional, but we provide it so that requests are unique + // and we avoid a bug in the client when the same request is submitted more + // than once: https://dfinity.atlassian.net/browse/DFN-895 nonce?: Nonce; }