@hyperlane-xyz/utils
Version:
General utilities and types for the Hyperlane network
141 lines • 5.84 kB
JavaScript
import { ethers, utils } from 'ethers';
import { addressToBytes32, ensure0x } from './addresses.js';
import { fromHexString, toHexString } from './strings.js';
import { assert } from './validation.js';
/**
* JS Implementation of solidity/contracts/libs/Message.sol#formatMessage
* @returns Hex string of the packed message
*/
export const formatMessage = (version, nonce, originDomain, senderAddr, destinationDomain, recipientAddr, body) => {
senderAddr = addressToBytes32(senderAddr);
recipientAddr = addressToBytes32(recipientAddr);
return ethers.utils.solidityPack(['uint8', 'uint32', 'uint32', 'bytes32', 'uint32', 'bytes32', 'bytes'], [
version,
nonce,
originDomain,
senderAddr,
destinationDomain,
recipientAddr,
body,
]);
};
/**
* Get ID given message bytes
* @param message Hex string of the packed message (see formatMessage)
* @returns Hex string of message id
*/
export function messageId(message) {
return ethers.utils.solidityKeccak256(['bytes'], [message]);
}
/**
* Parse a serialized Hyperlane message from raw bytes.
*
* @param message
* @returns
*/
export function parseMessage(message) {
const VERSION_OFFSET = 0;
const NONCE_OFFSET = 1;
const ORIGIN_OFFSET = 5;
const SENDER_OFFSET = 9;
const DESTINATION_OFFSET = 41;
const RECIPIENT_OFFSET = 45;
const BODY_OFFSET = 77;
const buf = Buffer.from(utils.arrayify(message));
const version = buf.readUInt8(VERSION_OFFSET);
const nonce = buf.readUInt32BE(NONCE_OFFSET);
const origin = buf.readUInt32BE(ORIGIN_OFFSET);
const sender = utils.hexlify(buf.subarray(SENDER_OFFSET, DESTINATION_OFFSET));
const destination = buf.readUInt32BE(DESTINATION_OFFSET);
const recipient = utils.hexlify(buf.subarray(RECIPIENT_OFFSET, BODY_OFFSET));
const body = utils.hexlify(buf.subarray(BODY_OFFSET));
return { version, nonce, origin, sender, destination, recipient, body };
}
export function parseWarpRouteMessage(messageBody) {
const RECIPIENT_OFFSET = 0;
const AMOUNT_OFFSET = 32;
const buf = fromHexString(messageBody);
const recipient = toHexString(buf.subarray(RECIPIENT_OFFSET, RECIPIENT_OFFSET + 32));
const amount = BigInt(toHexString(buf.subarray(AMOUNT_OFFSET, AMOUNT_OFFSET + 32)));
return {
recipient,
amount,
};
}
// Match IGP's DEFAULT_GAS_USAGE so quote and execution use same gas
const DEFAULT_GAS_LIMIT = 50000n;
/**
* JS Implementation of solidity/contracts/hooks/libs/StandardHookMetadata.sol#formatMetadata
* @returns Hex string of the packed hook metadata
*/
export function formatStandardHookMetadata({ refundAddress = ethers.constants.AddressZero, msgValue = 0n, gasLimit = DEFAULT_GAS_LIMIT, }) {
return ethers.utils.solidityPack(['uint16', 'uint256', 'uint256', 'address'], [1, msgValue, gasLimit, refundAddress]);
}
// Offsets for StandardHookMetadata parsing
// Format: uint16 variant (2 bytes) + uint256 msgValue (32 bytes) + uint256 gasLimit (32 bytes) + address refundAddress (20 bytes)
const HEX_PREFIX_LEN = 2;
const VARIANT_HEX_LEN = 4;
const UINT256_HEX_LEN = 64;
const ADDRESS_HEX_LEN = 40;
const MSG_VALUE_START = HEX_PREFIX_LEN + VARIANT_HEX_LEN;
const GAS_LIMIT_START = MSG_VALUE_START + UINT256_HEX_LEN;
const REFUND_START = GAS_LIMIT_START + UINT256_HEX_LEN;
const REFUND_END = REFUND_START + ADDRESS_HEX_LEN;
/**
* Parse StandardHookMetadata bytes into its components.
* @returns Parsed metadata or null if invalid
*/
export function parseStandardHookMetadata(metadata) {
if (!metadata || metadata === '0x')
return null;
if (!/^0x[0-9a-fA-F]*$/.test(metadata))
return null;
if (!metadata.startsWith('0x0001'))
return null;
if (metadata.length < REFUND_END)
return null;
try {
const msgValue = BigInt('0x' + metadata.slice(MSG_VALUE_START, GAS_LIMIT_START));
const gasLimit = BigInt('0x' + metadata.slice(GAS_LIMIT_START, REFUND_START));
const refundAddress = ethers.utils.getAddress('0x' + metadata.slice(REFUND_START, REFUND_END));
return { msgValue, gasLimit, refundAddress };
}
catch {
return null;
}
}
export function extractRefundAddressFromMetadata(metadata) {
return parseStandardHookMetadata(metadata)?.refundAddress ?? null;
}
export function hasValidRefundAddress(metadata) {
const refundAddress = extractRefundAddressFromMetadata(metadata);
return (refundAddress !== null &&
refundAddress.toLowerCase() !== ethers.constants.AddressZero);
}
/**
* Compute the synthetic message ID the scraper stores for a same-chain CCR swap.
*
* Format: `0x00000000 || keccak256("SameChainCCR" || txHash32 || logIndex8)[0..28]`
*
* The 4-byte zero prefix makes synthetic IDs immediately distinguishable from
* real Hyperlane message IDs (which are uniform keccak256 outputs).
*
* @param txHash 32-byte transaction hash (0x-prefixed hex string)
* @param logIndex log index of the ReceivedTransferRemote event
*/
export function syntheticCcrSwapMessageId(txHash, logIndex) {
assert(ethers.utils.isHexString(txHash, 32), `txHash must be a 32-byte hex string, got: ${txHash}`);
assert((typeof logIndex === 'bigint' && logIndex >= 0n) ||
(typeof logIndex === 'number' &&
Number.isInteger(logIndex) &&
logIndex >= 0), `logIndex must be a non-negative integer, got: ${logIndex}`);
const logIndexHex = ethers.utils.hexZeroPad(ethers.BigNumber.from(logIndex).toHexString(), 8);
const hash = ethers.utils.keccak256(ethers.utils.concat([
ethers.utils.toUtf8Bytes('SameChainCCR'),
txHash,
logIndexHex,
]));
// 4 zero bytes || first 28 bytes of hash
return ensure0x('00'.repeat(4) + hash.slice(2, 58));
}
//# sourceMappingURL=messages.js.map