@slide-computer/signer-agent
Version:
Initiate transactions with signers on the Internet Computer
306 lines • 15.8 kB
JavaScript
;
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _SignerAgent_instances, _a, _SignerAgent_isInternalConstructing, _SignerAgent_options, _SignerAgent_certificates, _SignerAgent_queue, _SignerAgent_executeTimeout, _SignerAgent_scheduled, _SignerAgent_autoBatch, _SignerAgent_validation, _SignerAgent_executeQueue, _SignerAgent_executeBatch;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SignerAgent = exports.SignerAgentError = void 0;
const agent_1 = require("@dfinity/agent");
const candid_1 = require("@dfinity/candid");
const principal_1 = require("@dfinity/principal");
const signer_1 = require("@slide-computer/signer");
const utils_1 = require("./utils");
const queue_1 = require("./queue");
const ROOT_KEY = new Uint8Array(agent_1.IC_ROOT_KEY.match(/[\da-f]{2}/gi).map((h) => parseInt(h, 16))).buffer;
const MAX_AGE_IN_MINUTES = 5;
const INVALID_RESPONSE_MESSAGE = "Received invalid response from signer";
class SignerAgentError extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, SignerAgentError.prototype);
}
}
exports.SignerAgentError = SignerAgentError;
class SignerAgent {
constructor(options) {
_SignerAgent_instances.add(this);
_SignerAgent_options.set(this, void 0);
_SignerAgent_certificates.set(this, new Map());
_SignerAgent_queue.set(this, new queue_1.Queue());
_SignerAgent_executeTimeout.set(this, void 0);
_SignerAgent_scheduled.set(this, [[]]);
_SignerAgent_autoBatch.set(this, true);
_SignerAgent_validation.set(this, void 0);
const throwError = !__classPrivateFieldGet(_a, _a, "f", _SignerAgent_isInternalConstructing);
__classPrivateFieldSet(_a, _a, false, "f", _SignerAgent_isInternalConstructing);
if (throwError) {
throw new SignerAgentError("SignerAgent is not constructable");
}
__classPrivateFieldSet(this, _SignerAgent_options, options, "f");
}
get rootKey() {
var _b;
return (_b = __classPrivateFieldGet(this, _SignerAgent_options, "f").agent.rootKey) !== null && _b !== void 0 ? _b : ROOT_KEY;
}
get signer() {
return __classPrivateFieldGet(this, _SignerAgent_options, "f").signer;
}
static async create(options) {
var _b, _c, _d;
__classPrivateFieldSet(_a, _a, true, "f", _SignerAgent_isInternalConstructing);
return new _a(Object.assign(Object.assign({}, options), { agent: (_b = options.agent) !== null && _b !== void 0 ? _b : (await agent_1.HttpAgent.create()), scheduleDelay: (_c = options.scheduleDelay) !== null && _c !== void 0 ? _c : 20, validation: (_d = options.validation) !== null && _d !== void 0 ? _d : null }));
}
static createSync(options) {
var _b, _c, _d;
__classPrivateFieldSet(_a, _a, true, "f", _SignerAgent_isInternalConstructing);
return new _a(Object.assign(Object.assign({}, options), { agent: (_b = options.agent) !== null && _b !== void 0 ? _b : agent_1.HttpAgent.createSync(), scheduleDelay: (_c = options.scheduleDelay) !== null && _c !== void 0 ? _c : 20, validation: (_d = options.validation) !== null && _d !== void 0 ? _d : null }));
}
async execute() {
const scheduled = [...__classPrivateFieldGet(this, _SignerAgent_scheduled, "f")];
const validation = __classPrivateFieldGet(this, _SignerAgent_validation, "f");
this.clear();
const pending = scheduled.flat().length;
if (pending === 0) {
__classPrivateFieldSet(this, _SignerAgent_validation, undefined, "f");
return;
}
const needsBatch = pending > 1;
if (!needsBatch) {
await __classPrivateFieldGet(this, _SignerAgent_instances, "m", _SignerAgent_executeQueue).call(this, scheduled);
return;
}
const supportedStandards = await __classPrivateFieldGet(this, _SignerAgent_queue, "f").schedule(() => this.signer.supportedStandards());
const supportsBatch = supportedStandards.some((supportedStandard) => supportedStandard.name === "ICRC-112");
if (supportsBatch) {
await __classPrivateFieldGet(this, _SignerAgent_instances, "m", _SignerAgent_executeBatch).call(this, scheduled, validation);
}
else {
await __classPrivateFieldGet(this, _SignerAgent_instances, "m", _SignerAgent_executeQueue).call(this, scheduled);
}
}
async call(canisterId, options) {
// Make sure canisterId is a principal
canisterId = principal_1.Principal.from(canisterId);
// Manually open the transport channel here first to make sure that
// the scheduler does not e.g. block a popup window from opening.
await __classPrivateFieldGet(this, _SignerAgent_options, "f").signer.openChannel();
// Make call through scheduler that automatically performs a single call or batch call.
const response = await new Promise((resolve, reject) => {
clearTimeout(__classPrivateFieldGet(this, _SignerAgent_executeTimeout, "f"));
__classPrivateFieldGet(this, _SignerAgent_scheduled, "f").slice(-1)[0].push({
options: {
canisterId,
method: options.methodName,
arg: options.arg,
},
resolve,
reject,
});
if (__classPrivateFieldGet(this, _SignerAgent_autoBatch, "f")) {
__classPrivateFieldSet(this, _SignerAgent_executeTimeout, setTimeout(() => this.execute(), __classPrivateFieldGet(this, _SignerAgent_options, "f").scheduleDelay), "f");
}
});
// Validate content map
const requestBody = (0, utils_1.decodeCallRequest)(response.contentMap);
const contentMapMatchesRequest = agent_1.SubmitRequestType.Call === requestBody.request_type &&
canisterId.compareTo(requestBody.canister_id) === "eq" &&
options.methodName === requestBody.method_name &&
(0, agent_1.compare)(options.arg, requestBody.arg) === 0 &&
__classPrivateFieldGet(this, _SignerAgent_options, "f").account.compareTo(principal_1.Principal.from(requestBody.sender)) ===
"eq";
if (!contentMapMatchesRequest) {
throw new SignerAgentError(INVALID_RESPONSE_MESSAGE);
}
// Validate certificate
const requestId = (0, agent_1.requestIdOf)(requestBody);
const certificate = await agent_1.Certificate.create({
certificate: response.certificate,
rootKey: this.rootKey,
canisterId,
maxAgeInMinutes: MAX_AGE_IN_MINUTES,
}).catch(() => {
throw new SignerAgentError(INVALID_RESPONSE_MESSAGE);
});
const certificateIsResponseToContentMap = certificate.lookup(["request_status", requestId, "status"]).status ===
agent_1.LookupStatus.Found;
if (!certificateIsResponseToContentMap) {
throw new SignerAgentError(INVALID_RESPONSE_MESSAGE);
}
// Check if response has already been received previously to avoid replay attacks
const requestKey = (0, signer_1.toBase64)(requestId);
if (__classPrivateFieldGet(this, _SignerAgent_certificates, "f").has(requestKey)) {
throw new SignerAgentError(INVALID_RESPONSE_MESSAGE);
}
// Store certificate in map
__classPrivateFieldGet(this, _SignerAgent_certificates, "f").set(requestKey, response.certificate);
// Cleanup when certificate expires
const now = Date.now();
const lookupTime = (0, agent_1.lookupResultToBuffer)(certificate.lookup(["time"]));
if (!lookupTime) {
throw new SignerAgentError(INVALID_RESPONSE_MESSAGE);
}
const certificateTime = Number((0, candid_1.lebDecode)(new candid_1.PipeArrayBuffer(lookupTime))) / 1000000;
const expiry = certificateTime - now + MAX_AGE_IN_MINUTES * 60 * 1000;
setTimeout(() => __classPrivateFieldGet(this, _SignerAgent_certificates, "f").delete(requestKey), expiry);
// Return request id with http response
return {
requestId,
response: {
ok: true,
status: 202,
statusText: "Call has been sent over ICRC-25 JSON-RPC",
body: null,
headers: [],
},
};
}
async fetchRootKey() {
return __classPrivateFieldGet(this, _SignerAgent_options, "f").agent.fetchRootKey();
}
async getPrincipal() {
return __classPrivateFieldGet(this, _SignerAgent_options, "f").account;
}
async query(canisterId, options) {
// Make sure canisterId is a principal
canisterId = principal_1.Principal.from(canisterId);
// Upgrade query request to a call sent through signer
const submitResponse = await this.call(canisterId, options);
const readStateResponse = await this.readState(canisterId, {
paths: [
[new TextEncoder().encode("request_status"), submitResponse.requestId],
],
});
const certificate = await agent_1.Certificate.create({
certificate: readStateResponse.certificate,
rootKey: this.rootKey,
canisterId,
});
const status = certificate.lookup([
"request_status",
submitResponse.requestId,
"status",
]);
const reply = certificate.lookup([
"request_status",
submitResponse.requestId,
"reply",
]);
if (status.status !== agent_1.LookupStatus.Found ||
new TextDecoder().decode(status.value) !== "replied" ||
reply.status !== agent_1.LookupStatus.Found) {
throw new SignerAgentError("Certificate is missing reply");
}
return {
requestId: submitResponse.requestId,
status: "replied",
reply: {
arg: reply.value,
},
httpDetails: {
ok: true,
status: 202,
statusText: "Certificate with reply has been received over ICRC-25 JSON-RPC",
headers: [],
},
};
}
async createReadStateRequest(_options) {
// Since request is typed as any it shouldn't need any data,
// but since agent-js 2.1.3 this would cause a runtime error.
return {
body: {
content: {},
},
};
}
async readState(_canisterId, options, _identity,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_request) {
if (options.paths.length !== 1 ||
options.paths[0].length !== 2 ||
new TextDecoder().decode(options.paths[0][0]) !== "request_status") {
throw new SignerAgentError("Given paths are not supported");
}
const requestId = options.paths[0][1];
const key = (0, signer_1.toBase64)(requestId);
const certificate = __classPrivateFieldGet(this, _SignerAgent_certificates, "f").get(key);
if (!certificate) {
throw new SignerAgentError("Certificate could not be found");
}
return { certificate };
}
async status() {
return __classPrivateFieldGet(this, _SignerAgent_options, "f").agent.status();
}
replaceAccount(account) {
__classPrivateFieldGet(this, _SignerAgent_options, "f").account = account;
}
replaceValidation(validation) {
__classPrivateFieldSet(this, _SignerAgent_validation, validation, "f");
}
/**
* Enable manual triggering of canister calls execution
*/
batch() {
__classPrivateFieldSet(this, _SignerAgent_autoBatch, false, "f");
if (__classPrivateFieldGet(this, _SignerAgent_scheduled, "f").slice(-1)[0].length > 0) {
__classPrivateFieldGet(this, _SignerAgent_scheduled, "f").push([]);
}
}
/**
* Clear scheduled canister calls and switch back to automatic canister calls execution
*/
clear() {
__classPrivateFieldSet(this, _SignerAgent_scheduled, [[]], "f");
__classPrivateFieldSet(this, _SignerAgent_autoBatch, true, "f");
}
}
exports.SignerAgent = SignerAgent;
_a = SignerAgent, _SignerAgent_options = new WeakMap(), _SignerAgent_certificates = new WeakMap(), _SignerAgent_queue = new WeakMap(), _SignerAgent_executeTimeout = new WeakMap(), _SignerAgent_scheduled = new WeakMap(), _SignerAgent_autoBatch = new WeakMap(), _SignerAgent_validation = new WeakMap(), _SignerAgent_instances = new WeakSet(), _SignerAgent_executeQueue = async function _SignerAgent_executeQueue(scheduled) {
await Promise.all(scheduled.flat().map(({ options, resolve, reject }) => __classPrivateFieldGet(this, _SignerAgent_queue, "f").schedule(async () => {
try {
const response = await this.signer.callCanister(Object.assign({ sender: __classPrivateFieldGet(this, _SignerAgent_options, "f").account }, options));
resolve(response);
}
catch (error) {
reject(error);
}
})));
}, _SignerAgent_executeBatch = async function _SignerAgent_executeBatch(scheduled, validation) {
await __classPrivateFieldGet(this, _SignerAgent_queue, "f").schedule(async () => {
try {
const responses = await this.signer.batchCallCanister({
sender: __classPrivateFieldGet(this, _SignerAgent_options, "f").account,
requests: scheduled.map((entries) => entries.map(({ options }) => options)),
validation: validation !== null && validation !== void 0 ? validation : undefined,
});
scheduled.forEach((entries, sequenceIndex) => entries.forEach(({ resolve, reject }, requestIndex) => {
const response = responses[sequenceIndex][requestIndex];
if ("result" in response) {
resolve(response.result);
return;
}
if ("error" in response) {
reject(new SignerAgentError(`${response.error.code}: ${response.error.message}\n${JSON.stringify(response.error.data)}`));
return;
}
reject(new SignerAgentError(INVALID_RESPONSE_MESSAGE));
}));
}
catch (error) {
// Forward error to each canister call handler
scheduled.flat().forEach(({ reject }) => reject(error));
}
});
};
// noinspection JSUnusedLocalSymbols
_SignerAgent_isInternalConstructing = { value: false };
//# sourceMappingURL=agent.js.map