UNPKG

ic-websocket-js

Version:
161 lines 6.83 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WsAgent = void 0; const principal_1 = require("@dfinity/principal"); const agent_1 = require("@dfinity/agent"); const transforms_1 = require("./transforms"); // Default delta for ingress expiry is 5 minutes. const DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS = 5 * 60 * 1000; class DefaultWsError extends Error { constructor(message) { super(message); this.message = message; Object.setPrototypeOf(this, DefaultWsError.prototype); } } class WsAgent { constructor(options) { this._pipeline = []; this._timeDiffMsecs = 0; this._isAgent = true; if (options.source) { if (!(options.source instanceof WsAgent)) { throw new Error("An Agent's source can only be another WsAgent"); } this._pipeline = [...options.source._pipeline]; this._identity = options.source._identity; this._ws = options.source._ws; this._httpAgent = options.source._httpAgent; } else { if (!options.identity) { throw new Error('An identity must be provided to the WsAgent'); } this._identity = Promise.resolve(options.identity); if (!options.ws) { throw new Error('A WebSocket instance must be provided to the WsAgent'); } else if (options.ws.readyState !== WebSocket.OPEN) { throw new DefaultWsError('The provided WebSocket is not open'); } this._ws = options.ws; if (!options.httpAgent) { throw new Error('An httpAgent must be provided to the WsAgent'); } this._httpAgent = options.httpAgent; } // Default is 3, only set from option if greater or equal to 0 this._retryTimes = options.retryTimes !== undefined && options.retryTimes >= 0 ? options.retryTimes : 3; // Add a nonce transform to ensure calls are unique if (!options.disableNonce) { this.addTransform((0, transforms_1.makeWsNonceTransform)(agent_1.makeNonce)); } } addTransform(fn, priority = fn.priority || 0) { // Keep the pipeline sorted at all time, by priority. const i = this._pipeline.findIndex(x => (x.priority || 0) < priority); this._pipeline.splice(i >= 0 ? i : this._pipeline.length, 0, Object.assign(fn, { priority })); } async getPrincipal() { if (!this._identity) { throw new agent_1.IdentityInvalidError("This identity has expired due this application's security policy. Please refresh your authentication."); } return (await this._identity).getPrincipal(); } async call(canisterId, options) { const id = await this._identity; if (!id) { throw new agent_1.IdentityInvalidError("This identity has expired due this application's security policy. Please refresh your authentication."); } const canister = principal_1.Principal.from(canisterId); const sender = id.getPrincipal(); let ingress_expiry = new agent_1.Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS); // If the value is off by more than 30 seconds, reconcile system time with the network if (Math.abs(this._timeDiffMsecs) > 1000 * 30) { ingress_expiry = new agent_1.Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS + this._timeDiffMsecs); } const submit = { request_type: agent_1.SubmitRequestType.Call, canister_id: canister, method_name: options.methodName, arg: options.arg, sender, ingress_expiry, }; const transformedRequest = (await this._transform({ endpoint: "call" /* Endpoint.Call */, message: submit, })); // we need to adapt out ws request to the identity's transform // in order to get the signed request const idTransformedRequest = await id.transformRequest({ // mock the request to be compatible request: { body: null, method: 'POST', headers: {}, }, endpoint: "call" /* Endpoint.Call */, body: transformedRequest.message, }); const message = { envelope: idTransformedRequest.body, }; this._requestAndRetry(message); } /** * Sends a fire-and-forget request body to the WebSocket Gateway. * If the request fails, the request */ _requestAndRetry(message, tries = 0) { const messageBytes = agent_1.Cbor.encode(message); try { return this._ws.send(messageBytes); } catch (error) { if (this._retryTimes > tries) { console.warn(`${error} Retrying request.`); return this._requestAndRetry(message, tries + 1); } } throw new DefaultWsError("Sending the envelope through the WebSocket failed."); } /** * Allows agent to sync its time with the network. Can be called during intialization or mid-lifecycle if the device's clock has drifted away from the network time. This is necessary to set the Expiry for a request * @param {Principal} canisterId - Pass a canister ID if you need to sync the time with a particular replica. Uses the management canister by default */ async syncTime(canisterId) { const callTime = Date.now(); try { if (!canisterId) { console.log('Syncing time with the IC. No canisterId provided, so falling back to ryjl3-tyaaa-aaaaa-aaaba-cai'); } const status = await agent_1.CanisterStatus.request({ // Fall back with canisterId of the ICP Ledger canisterId: canisterId !== null && canisterId !== void 0 ? canisterId : principal_1.Principal.from('ryjl3-tyaaa-aaaaa-aaaba-cai'), agent: this._httpAgent, paths: ['time'], }); const replicaTime = status.get('time'); if (replicaTime) { this._timeDiffMsecs = Number(replicaTime) - Number(callTime); } } catch (error) { console.error('Caught exception while attempting to sync time:', error); } } replaceIdentity(identity) { this._identity = Promise.resolve(identity); } _transform(request) { let p = Promise.resolve(request); for (const fn of this._pipeline) { p = p.then(r => fn(r).then(r2 => r2 || r)); } return p; } } exports.WsAgent = WsAgent; //# sourceMappingURL=index.js.map