UNPKG

@dfinity/agent

Version:

JavaScript and TypeScript library to interact with the Internet Computer

143 lines 6.49 kB
import { Certificate, lookupResultToBuffer, } from "../certificate.js"; import { CertifiedRejectErrorCode, ExternalError, InputError, InvalidReadStateRequestErrorCode, MissingRootKeyErrorCode, RejectError, RequestStatusDoneNoReplyErrorCode, UnknownError, UNREACHABLE_ERROR, } from "../errors.js"; export * as strategy from "./strategy.js"; import { defaultStrategy } from "./strategy.js"; import { ReadRequestType } from "../agent/http/types.js"; import { RequestStatusResponseStatus } from "../agent/index.js"; import { utf8ToBytes } from '@noble/hashes/utils'; export { defaultStrategy } from "./strategy.js"; export const DEFAULT_POLLING_OPTIONS = { strategy: defaultStrategy(), preSignReadStateRequest: false, }; /** * Check if an object has a property * @param value the object that might have the property * @param property the key of property we're looking for */ function hasProperty(value, property) { return Object.prototype.hasOwnProperty.call(value, property); } function isObjectWithProperty(value, property) { return value !== null && typeof value === 'object' && hasProperty(value, property); } function hasFunction(value, property) { return hasProperty(value, property) && typeof value[property] === 'function'; } /** * Check if value is a signed read state request with expiry * @param value to check */ function isSignedReadStateRequestWithExpiry(value) { return (isObjectWithProperty(value, 'body') && isObjectWithProperty(value.body, 'content') && value.body.content.request_type === ReadRequestType.ReadState && isObjectWithProperty(value.body.content, 'ingress_expiry') && typeof value.body.content.ingress_expiry === 'object' && value.body.content.ingress_expiry !== null && hasFunction(value.body.content.ingress_expiry, 'toHash')); } /** * Polls the IC to check the status of the given request then * returns the response bytes once the request has been processed. * @param agent The agent to use to poll read_state. * @param canisterId The effective canister ID. * @param requestId The Request ID to poll status for. * @param options polling options to control behavior */ export async function pollForResponse(agent, canisterId, requestId, options = {}) { const path = [utf8ToBytes('request_status'), requestId]; let state; let currentRequest; const preSignReadStateRequest = options.preSignReadStateRequest ?? false; if (preSignReadStateRequest) { // If preSignReadStateRequest is true, we need to create a new request currentRequest = await constructRequest({ paths: [path], agent, pollingOptions: options, }); state = await agent.readState(canisterId, { paths: [path] }, undefined, currentRequest); } else { // If preSignReadStateRequest is false, we use the default strategy and sign the request each time state = await agent.readState(canisterId, { paths: [path] }); } if (agent.rootKey == null) { throw ExternalError.fromCode(new MissingRootKeyErrorCode()); } const cert = await Certificate.create({ certificate: state.certificate, rootKey: agent.rootKey, canisterId: canisterId, blsVerify: options.blsVerify, agent, }); const maybeBuf = lookupResultToBuffer(cert.lookup_path([...path, utf8ToBytes('status')])); let status; if (typeof maybeBuf === 'undefined') { // Missing requestId means we need to wait status = RequestStatusResponseStatus.Unknown; } else { status = new TextDecoder().decode(maybeBuf); } switch (status) { case RequestStatusResponseStatus.Replied: { return { reply: lookupResultToBuffer(cert.lookup_path([...path, 'reply'])), certificate: cert, }; } case RequestStatusResponseStatus.Received: case RequestStatusResponseStatus.Unknown: case RequestStatusResponseStatus.Processing: { // Execute the polling strategy, then retry. const strategy = options.strategy ?? defaultStrategy(); await strategy(canisterId, requestId, status); return pollForResponse(agent, canisterId, requestId, { ...options, request: currentRequest, }); } case RequestStatusResponseStatus.Rejected: { const rejectCode = new Uint8Array(lookupResultToBuffer(cert.lookup_path([...path, 'reject_code'])))[0]; const rejectMessage = new TextDecoder().decode(lookupResultToBuffer(cert.lookup_path([...path, 'reject_message']))); const errorCodeBuf = lookupResultToBuffer(cert.lookup_path([...path, 'error_code'])); const errorCode = errorCodeBuf ? new TextDecoder().decode(errorCodeBuf) : undefined; throw RejectError.fromCode(new CertifiedRejectErrorCode(requestId, rejectCode, rejectMessage, errorCode)); } case RequestStatusResponseStatus.Done: // This is _technically_ not an error, but we still didn't see the `Replied` status so // we don't know the result and cannot decode it. throw UnknownError.fromCode(new RequestStatusDoneNoReplyErrorCode(requestId)); } throw UNREACHABLE_ERROR; } // Determine if we should reuse the read state request or create a new one // based on the options provided. /** * Constructs a read state request for the given paths. * If the request is already signed and has an expiry, it will be returned as is. * Otherwise, a new request will be created. * @param options The options to use for creating the request. * @param options.paths The paths to read from. * @param options.agent The agent to use to create the request. * @param options.pollingOptions The options to use for creating the request. * @returns The read state request. */ export async function constructRequest(options) { const { paths, agent, pollingOptions } = options; if (pollingOptions.request && isSignedReadStateRequestWithExpiry(pollingOptions.request)) { return pollingOptions.request; } const request = await agent.createReadStateRequest?.({ paths, }, undefined); if (!isSignedReadStateRequestWithExpiry(request)) { throw InputError.fromCode(new InvalidReadStateRequestErrorCode(request)); } return request; } //# sourceMappingURL=index.js.map