@dfinity/agent
Version:
JavaScript and TypeScript library to interact with the Internet Computer
283 lines • 13.2 kB
JavaScript
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