UNPKG

@nomad-xyz/sdk

Version:
448 lines 16.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NomadMessage = exports.parseMessage = void 0; const ethers_1 = require("ethers"); const utils_1 = require("ethers/lib/utils"); const bytes_1 = require("@ethersproject/bytes"); const logger_1 = require("@ethersproject/logger"); const core = __importStar(require("@nomad-xyz/contracts-core")); const multi_provider_1 = require("@nomad-xyz/multi-provider"); const types_1 = require("./types"); /** * Parse a serialized Nomad message from raw bytes. * * @param message * @returns */ function parseMessage(message) { const buf = Buffer.from((0, bytes_1.arrayify)(message)); const from = buf.readUInt32BE(0); const sender = (0, bytes_1.hexlify)(buf.slice(4, 36)); const nonce = buf.readUInt32BE(36); const destination = buf.readUInt32BE(40); const recipient = (0, bytes_1.hexlify)(buf.slice(44, 76)); const body = (0, bytes_1.hexlify)(buf.slice(76)); return { from, sender, nonce, destination, recipient, body }; } exports.parseMessage = parseMessage; /** * A deserialized Nomad message. */ class NomadMessage { constructor(context, dispatch, _backend) { this.context = context; this._backend = _backend; this.message = parseMessage(dispatch.args.message); this.dispatch = dispatch; this.home = context.mustGetCore(this.message.from).home; this.replica = context.mustGetReplicaFor(this.message.from, this.message.destination); } get backend() { const backend = this._backend || this.context._backend; if (!backend) { throw new Error(`No backend in the context`); } return backend; } get messageHash() { return this.dispatch.args.messageHash; } /** * Instantiate one or more messages from a receipt. * * @param context the {@link NomadContext} object to use * @param receipt the receipt * @returns an array of {@link NomadMessage} objects */ static async baseFromReceipt(context, receipt) { const messages = []; const home = core.Home__factory.createInterface(); for (const log of receipt.logs) { try { const parsed = home.parseLog(log); if (parsed.name === 'Dispatch') { const { messageHash, leafIndex, destinationAndNonce, committedRoot, message, } = parsed.args; const dispatch = { args: { messageHash, leafIndex, destinationAndNonce, committedRoot, message, }, transactionHash: receipt.transactionHash, }; messages.push(new NomadMessage(context, dispatch)); } } catch (e) { console.log('Unexpected error', e); const err = e; // Catch known errors that we'd like to squash if (err.code == logger_1.Logger.errors.INVALID_ARGUMENT && err.reason == 'no matching event') continue; } } return messages; } /** * Instantiate EXACTLY one message from a receipt. * * @param context the {@link NomadContext} object to use * @param receipt the receipt * @returns an array of {@link NomadMessage} objects * @throws if there is not EXACTLY 1 dispatch in the receipt */ static async baseSingleFromReceipt(context, receipt) { const messages = await NomadMessage.baseFromReceipt(context, receipt); if (messages.length !== 1) { throw new Error('Expected single Dispatch in transaction'); } return messages[0]; } /** * Instantiate one or more messages from a tx hash. * * @param context the {@link NomadContext} object to use * @param nameOrDomain the domain on which the receipt was logged * @param transactionHash the transaction hash on the origin chain * @returns an array of {@link NomadMessage} objects * @throws if there is no receipt for the TX */ static async baseFromTransactionHash(context, nameOrDomain, transactionHash) { const provider = context.mustGetProvider(nameOrDomain); const receipt = await provider.getTransactionReceipt(transactionHash); if (!receipt) { throw new Error(`No receipt for ${transactionHash} on ${nameOrDomain}`); } return await NomadMessage.baseFromReceipt(context, receipt); } /** * Instantiate EXACTLY one message from a transaction has. * * @param context the {@link NomadContext} object to use * @param nameOrDomain the domain on which the receipt was logged * @param transactionHash the transaction hash on the origin chain * @returns an array of {@link NomadMessage} objects * @throws if there is no receipt for the TX, or if not EXACTLY 1 dispatch in * the receipt */ static async baseSingleFromTransactionHash(context, nameOrDomain, transactionHash) { const provider = context.mustGetProvider(nameOrDomain); const receipt = await provider.getTransactionReceipt(transactionHash); if (!receipt) { throw new Error(`No receipt for ${transactionHash} on ${nameOrDomain}`); } return await NomadMessage.baseSingleFromReceipt(context, receipt); } static async baseFirstFromBackend(context, transactionHash) { if (!context._backend) { throw new Error(`No backend is set for the context`); } const dispatches = await context._backend.getDispatches(transactionHash, 1); if (!dispatches || dispatches.length === 0) throw new Error(`No dispatch`); const m = new NomadMessage(context, dispatches[0]); return m; } static async baseFromMessageHash(context, messageHash) { if (!context._backend) { throw new Error(`No backend is set for the context`); } const dispatch = await context._backend.getDispatchByMessageHash(messageHash); if (!dispatch) throw new Error(`No dispatch`); const m = new NomadMessage(context, dispatch); return m; } /** * Get the `Relay` event associated with this message (if any) * * @returns An relay tx (if any) */ async getRelay() { return await this.backend.relayTx(this.messageHash); } /** * Get the `Update` event associated with this message (if any) * * @returns An update tx (if any) */ async getUpdate() { return await this.backend.updateTx(this.messageHash); } /** * Get the Replica `Process` event associated with this message (if any) * * @returns An process tx (if any) */ async getProcess() { return await this.backend.processTx(this.messageHash); } /** * Returns the timestamp after which it is possible to process this message. * * Note: return the timestamp after which it is possible to process messages * within an Update. The timestamp is most relevant during the time AFTER the * Update has been Relayed to the Replica and BEFORE the message in question * has been Processed. * * Considerations: * - the timestamp will be 0 if the Update has not been relayed to the Replica * - after the Update has been relayed to the Replica, the timestamp will be * non-zero forever (even after all messages in the Update have been * processed) * - if the timestamp is in the future, the challenge period has not elapsed * yet; messages in the Update cannot be processed yet * - if the timestamp is in the past, this does not necessarily mean that all * messages in the Update have been processed * * @returns The timestamp at which a message can confirm */ /** * Calculates an expected confirmation timestamp from relayed event * * @returns Timestamp (if any) */ async confirmAt(messageHash) { const relayedAt = await this.backend.relayedAt(messageHash); if (relayedAt) { // Additional check for adequate numbers if (relayedAt?.valueOf() <= 946684800000) { throw new Error(`RelayedAt could not be smaller than 946684800000 (2000-01-01)`); } const destinationDomainId = await this.backend.destinationDomainId(messageHash); // Destination domain must be present as long as relayed at is found already, since // destination domain data is present in Dispatch event, and relay data at Relay event, // which is later. if (!destinationDomainId) { throw new Error(`Destination domain is not present`); } const domain = this.context.getDomain(destinationDomainId); if (domain === undefined) { throw new Error(`Destination domain is not in the config`); } const optimisticSecondsUnparsed = domain.configuration.optimisticSeconds; const optimisticSeconds = typeof optimisticSecondsUnparsed === 'string' ? parseInt(optimisticSecondsUnparsed) : optimisticSecondsUnparsed; const confirmAt = new Date(relayedAt.valueOf() + optimisticSeconds * 1000); return confirmAt; } return undefined; } async process(overrides = {}) { return this.context.process(this, overrides); } /** * Retrieve the replica status of this message. * * @returns The {@link ReplicaMessageStatus} corresponding to the solidity * status of the message. */ async replicaStatus() { // backwards compatibility. Older replica versions returned a number, // newer versions return a hash let root = await this.replica.messages(this.leaf); root = root; // case one: root is 0 if (root === ethers_1.ethers.constants.HashZero || root === 0) return { status: types_1.ReplicaStatusNames.None }; // case two: root is 2 const legacyProcessed = `0x${'00'.repeat(31)}02`; if (root === legacyProcessed || root === 2) return { status: types_1.ReplicaStatusNames.Processed }; // case 3: root is proven. Could be either the root, or the legacy proven // status const legacyProven = `0x${'00'.repeat(31)}01`; if (typeof root === 'number') root = legacyProven; return { status: types_1.ReplicaStatusNames.Proven, root: root }; } /** * Checks whether the message has been delivered. * * @returns true if processed, else false. */ async delivered() { const { status } = await this.replicaStatus(); return status === types_1.ReplicaStatusNames.Processed; } /** * Returns a promise that resolves when the message has been delivered. * * WARNING: May never resolve. Oftern takes hours to resolve. * * @param opts Polling options. */ async wait(opts) { const interval = opts?.pollTime ?? 50; // sad spider face for (;;) { if (await this.delivered()) { return; } await multi_provider_1.utils.delay(interval); } } /** * Get the status of a message * * 0 = dispatched * 1 = included * 2 = relayed * 3 = updated * 4 = received * 5 = processed * * @returns An record of all events and correlating txs */ async status() { if (await this.getProcess()) return types_1.MessageStatus.Processed; const confirmAt = await this.confirmAt(this.messageHash); const now = new Date(); if (confirmAt && confirmAt < now) return types_1.MessageStatus.Relayed; if (await this.getUpdate()) return types_1.MessageStatus.Included; return types_1.MessageStatus.Dispatched; } /** * The domain from which the message was sent */ get from() { return this.message.from; } /** * The domain from which the message was sent. Alias for `from` */ get origin() { return this.from; } /** * The name of the domain from which the message was sent */ get originName() { return this.context.resolveDomainName(this.origin); } /** * Get the name of this file in the s3 bucket */ get s3Name() { const index = this.leafIndex.toNumber(); return `${this.originName}_${index}`; } /** * Get the URI for the proof in S3 */ get s3Uri() { const s3 = this.context.conf.s3; if (!s3) throw new Error('s3 data not configured'); const { bucket, region } = s3; const root = `https://${bucket}.s3.${region}.amazonaws.com`; return `${root}/${this.s3Name}`; } /** * The identifier for the sender of this message */ get sender() { return this.message.sender; } /** * The domain nonce for this message */ get nonce() { return this.message.nonce; } /** * The destination domain for this message */ get destination() { return this.message.destination; } /** * The identifer for the recipient for this message */ get recipient() { return this.message.recipient; } /** * The message body */ get body() { return this.message.body; } /** * The keccak256 hash of the message body */ get bodyHash() { return (0, utils_1.keccak256)(this.body); } /** * The hash of the transaction that dispatched this message */ get transactionHash() { return this.dispatch.transactionHash; } /** * The messageHash committed to the tree in the Home contract. */ get leaf() { return this.dispatch.args.messageHash; } /** * The index of the leaf in the contract. */ get leafIndex() { return this.dispatch.args.leafIndex; } /** * The destination and nonceof this message. */ get destinationAndNonce() { return this.dispatch.args.destinationAndNonce; } /** * The committed root when this message was dispatched. */ get committedRoot() { return this.dispatch.args.committedRoot; } /** * Get the proof associated with this message * * @returns a proof, or undefined if no proof available * @throws if s3 is not configured for this env */ async getProof() { return this.context.fetchProof(this.origin, this.leafIndex.toNumber()); } } exports.NomadMessage = NomadMessage; //# sourceMappingURL=NomadMessage.js.map