UNPKG

@slide-computer/signer-agent

Version:

Initiate transactions with signers on the Internet Computer

306 lines 15.8 kB
"use strict"; 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