@nomad-xyz/sdk
Version:
448 lines • 16.2 kB
JavaScript
"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