UNPKG

@dfinity/agent

Version:

JavaScript and TypeScript library to interact with the Internet Computer

283 lines 13.2 kB
import { isV2ResponseBody, isV3ResponseBody, QueryResponseStatus, } from "./agent/index.js"; import { CertifiedRejectErrorCode, ExternalError, InputError, MissingCanisterIdErrorCode, MissingRootKeyErrorCode, RejectError, UncertifiedRejectErrorCode, UncertifiedRejectUpdateErrorCode, UnexpectedErrorCode, UnknownError, } from "./errors.js"; import { IDL } from '@dfinity/candid'; import { pollForResponse, DEFAULT_POLLING_OPTIONS } from "./polling/index.js"; import { Principal } from '@dfinity/principal'; import { Certificate, lookupResultToBuffer } from "./certificate.js"; import { HttpAgent } from "./agent/http/index.js"; import { utf8ToBytes } from '@noble/hashes/utils'; const metadataSymbol = Symbol.for('ic-agent-metadata'); /** * An actor base class. An actor is an object containing only functions that will * return a promise. These functions are derived from the IDL definition. */ export class Actor { /** * Get the Agent class this Actor would call, or undefined if the Actor would use * the default agent (global.ic.agent). * @param actor The actor to get the agent of. */ static agentOf(actor) { return actor[metadataSymbol].config.agent; } /** * Get the interface of an actor, in the form of an instance of a Service. * @param actor The actor to get the interface of. */ static interfaceOf(actor) { return actor[metadataSymbol].service; } static canisterIdOf(actor) { return Principal.from(actor[metadataSymbol].config.canisterId); } static createActorClass(interfaceFactory, options) { const service = interfaceFactory({ IDL }); class CanisterActor extends Actor { constructor(config) { if (!config.canisterId) { throw InputError.fromCode(new MissingCanisterIdErrorCode(config.canisterId)); } const canisterId = typeof config.canisterId === 'string' ? Principal.fromText(config.canisterId) : config.canisterId; super({ config: { ...DEFAULT_ACTOR_CONFIG, ...config, canisterId, }, service, }); for (const [methodName, func] of service._fields) { if (options?.httpDetails) { func.annotations.push(ACTOR_METHOD_WITH_HTTP_DETAILS); } if (options?.certificate) { func.annotations.push(ACTOR_METHOD_WITH_CERTIFICATE); } this[methodName] = _createActorMethod(this, methodName, func, config.blsVerify); } } } return CanisterActor; } static createActor(interfaceFactory, configuration) { if (!configuration.canisterId) { throw InputError.fromCode(new MissingCanisterIdErrorCode(configuration.canisterId)); } return new (this.createActorClass(interfaceFactory))(configuration); } /** * Returns an actor with methods that return the http response details along with the result * @param interfaceFactory - the interface factory for the actor * @param configuration - the configuration for the actor * @deprecated - use createActor with actorClassOptions instead */ static createActorWithHttpDetails(interfaceFactory, configuration) { return new (this.createActorClass(interfaceFactory, { httpDetails: true }))(configuration); } /** * Returns an actor with methods that return the http response details along with the result * @param interfaceFactory - the interface factory for the actor * @param configuration - the configuration for the actor * @param actorClassOptions - options for the actor class extended details to return with the result */ static createActorWithExtendedDetails(interfaceFactory, configuration, actorClassOptions = { httpDetails: true, certificate: true, }) { return new (this.createActorClass(interfaceFactory, actorClassOptions))(configuration); } constructor(metadata) { this[metadataSymbol] = Object.freeze(metadata); } } // IDL functions can have multiple return values, so decoding always // produces an array. Ensure that functions with single or zero return // values behave as expected. function decodeReturnValue(types, msg) { const returnValues = IDL.decode(types, msg); switch (returnValues.length) { case 0: return undefined; case 1: return returnValues[0]; default: return returnValues; } } const DEFAULT_ACTOR_CONFIG = { pollingOptions: DEFAULT_POLLING_OPTIONS, }; export const ACTOR_METHOD_WITH_HTTP_DETAILS = 'http-details'; export const ACTOR_METHOD_WITH_CERTIFICATE = 'certificate'; function _createActorMethod(actor, methodName, func, blsVerify) { let caller; if (func.annotations.includes('query') || func.annotations.includes('composite_query')) { caller = async (options, ...args) => { // First, if there's a config transformation, call it. options = { ...options, ...actor[metadataSymbol].config.queryTransform?.(methodName, args, { ...actor[metadataSymbol].config, ...options, }), }; const agent = options.agent || actor[metadataSymbol].config.agent || new HttpAgent(); const cid = Principal.from(options.canisterId || actor[metadataSymbol].config.canisterId); const arg = IDL.encode(func.argTypes, args); const result = await agent.query(cid, { methodName, arg, effectiveCanisterId: options.effectiveCanisterId, }); const httpDetails = { ...result.httpDetails, requestDetails: result.requestDetails, }; switch (result.status) { case QueryResponseStatus.Rejected: { const uncertifiedRejectErrorCode = new UncertifiedRejectErrorCode(result.requestId, result.reject_code, result.reject_message, result.error_code, result.signatures); uncertifiedRejectErrorCode.callContext = { canisterId: cid, methodName, httpDetails, }; throw RejectError.fromCode(uncertifiedRejectErrorCode); } case QueryResponseStatus.Replied: return func.annotations.includes(ACTOR_METHOD_WITH_HTTP_DETAILS) ? { httpDetails, result: decodeReturnValue(func.retTypes, result.reply.arg), } : decodeReturnValue(func.retTypes, result.reply.arg); } }; } else { caller = async (options, ...args) => { // First, if there's a config transformation, call it. options = { ...options, ...actor[metadataSymbol].config.callTransform?.(methodName, args, { ...actor[metadataSymbol].config, ...options, }), }; const agent = options.agent || actor[metadataSymbol].config.agent || HttpAgent.createSync(); const { canisterId, effectiveCanisterId, pollingOptions } = { ...DEFAULT_ACTOR_CONFIG, ...actor[metadataSymbol].config, ...options, }; const cid = Principal.from(canisterId); const ecid = effectiveCanisterId !== undefined ? Principal.from(effectiveCanisterId) : cid; const arg = IDL.encode(func.argTypes, args); const { requestId, response, requestDetails } = await agent.call(cid, { methodName, arg, effectiveCanisterId: ecid, nonce: options.nonce, }); let reply; let certificate; if (isV3ResponseBody(response.body)) { if (agent.rootKey == null) { throw ExternalError.fromCode(new MissingRootKeyErrorCode()); } const cert = response.body.certificate; certificate = await Certificate.create({ certificate: cert, rootKey: agent.rootKey, canisterId: ecid, blsVerify, agent, }); const path = [utf8ToBytes('request_status'), requestId]; const status = new TextDecoder().decode(lookupResultToBuffer(certificate.lookup_path([...path, 'status']))); switch (status) { case 'replied': reply = lookupResultToBuffer(certificate.lookup_path([...path, 'reply'])); break; case 'rejected': { // Find rejection details in the certificate const rejectCode = new Uint8Array(lookupResultToBuffer(certificate.lookup_path([...path, 'reject_code'])))[0]; const rejectMessage = new TextDecoder().decode(lookupResultToBuffer(certificate.lookup_path([...path, 'reject_message']))); const error_code_buf = lookupResultToBuffer(certificate.lookup_path([...path, 'error_code'])); const error_code = error_code_buf ? new TextDecoder().decode(error_code_buf) : undefined; const certifiedRejectErrorCode = new CertifiedRejectErrorCode(requestId, rejectCode, rejectMessage, error_code); certifiedRejectErrorCode.callContext = { canisterId: cid, methodName, httpDetails: response, }; throw RejectError.fromCode(certifiedRejectErrorCode); } } } else if (isV2ResponseBody(response.body)) { const { reject_code, reject_message, error_code } = response.body; const errorCode = new UncertifiedRejectUpdateErrorCode(requestId, reject_code, reject_message, error_code); errorCode.callContext = { canisterId: cid, methodName, httpDetails: response, }; throw RejectError.fromCode(errorCode); } // Fall back to polling if we receive an Accepted response code if (response.status === 202) { const pollOptions = { ...pollingOptions, blsVerify, }; // Contains the certificate and the reply from the boundary node const response = await pollForResponse(agent, ecid, requestId, pollOptions); certificate = response.certificate; reply = response.reply; } const shouldIncludeHttpDetails = func.annotations.includes(ACTOR_METHOD_WITH_HTTP_DETAILS); const shouldIncludeCertificate = func.annotations.includes(ACTOR_METHOD_WITH_CERTIFICATE); const httpDetails = { ...response, requestDetails }; if (reply !== undefined) { if (shouldIncludeHttpDetails && shouldIncludeCertificate) { return { httpDetails, certificate, result: decodeReturnValue(func.retTypes, reply), }; } else if (shouldIncludeCertificate) { return { certificate, result: decodeReturnValue(func.retTypes, reply), }; } else if (shouldIncludeHttpDetails) { return { httpDetails, result: decodeReturnValue(func.retTypes, reply), }; } return decodeReturnValue(func.retTypes, reply); } else { const errorCode = new UnexpectedErrorCode(`Call was returned undefined. We cannot determine if the call was successful or not. Return types: [${func.retTypes.map(t => t.display()).join(',')}].`); errorCode.callContext = { canisterId: cid, methodName, httpDetails, }; throw UnknownError.fromCode(errorCode); } }; } const handler = (...args) => caller({}, ...args); handler.withOptions = (options) => (...args) => caller(options, ...args); return handler; } //# sourceMappingURL=actor.js.map