413 lines
11 KiB
TypeScript
413 lines
11 KiB
TypeScript
import { BigNumber, ethers } from "ethers";
|
|
import { arrayify } from "ethers/lib/utils";
|
|
|
|
export enum RelayerPayloadId {
|
|
Delivery = 1,
|
|
Redelivery = 2,
|
|
}
|
|
|
|
export enum ExecutionInfoVersion {
|
|
EVM_V1 = 0,
|
|
}
|
|
|
|
export enum DeliveryStatus {
|
|
WaitingForVAA = "Waiting for VAA",
|
|
PendingDelivery = "Pending Delivery",
|
|
DeliverySuccess = "Delivery Success",
|
|
ReceiverFailure = "Receiver Failure",
|
|
ForwardRequestSuccess = "Forward Request Success",
|
|
ForwardRequestFailure = "Forward Request Failure",
|
|
ThisShouldNeverHappen = "This should never happen. Contact Support.",
|
|
DeliveryDidntHappenWithinRange = "Delivery didn't happen within given block range",
|
|
}
|
|
|
|
export enum RefundStatus {
|
|
RefundSent = "Refund Sent",
|
|
RefundFail = "Refund Fail",
|
|
CrossChainRefundSent = "Cross Chain Refund Sent",
|
|
CrossChainRefundFailProviderNotSupported = "Cross Chain Refund Fail - Provider does not support the refund chain",
|
|
CrossChainRefundFailNotEnough = "Cross Chain Refund Fail - Refund too low for cross chain refund",
|
|
}
|
|
|
|
export function parseRefundStatus(index: number) {
|
|
return index === 0
|
|
? RefundStatus.RefundSent
|
|
: index === 1
|
|
? RefundStatus.RefundFail
|
|
: index === 2
|
|
? RefundStatus.CrossChainRefundSent
|
|
: index === 3
|
|
? RefundStatus.CrossChainRefundFailProviderNotSupported
|
|
: index === 4
|
|
? RefundStatus.CrossChainRefundFailNotEnough
|
|
: RefundStatus.CrossChainRefundFailProviderNotSupported;
|
|
}
|
|
|
|
export interface VaaKey {
|
|
chainId: number;
|
|
emitterAddress: Buffer;
|
|
sequence: BigNumber;
|
|
}
|
|
|
|
export interface DeliveryInstruction {
|
|
targetChainId: number;
|
|
targetAddress: Buffer;
|
|
payload: Buffer;
|
|
requestedReceiverValue: BigNumber;
|
|
extraReceiverValue: BigNumber;
|
|
encodedExecutionInfo: Buffer;
|
|
refundChainId: number;
|
|
refundAddress: Buffer;
|
|
refundDeliveryProvider: Buffer;
|
|
sourceDeliveryProvider: Buffer;
|
|
senderAddress: Buffer;
|
|
vaaKeys: VaaKey[];
|
|
}
|
|
|
|
export interface RedeliveryInstruction {
|
|
deliveryVaaKey: VaaKey;
|
|
targetChainId: number;
|
|
newRequestedReceiverValue: BigNumber;
|
|
newEncodedExecutionInfo: Buffer;
|
|
newSourceDeliveryProvider: Buffer;
|
|
newSenderAddress: Buffer;
|
|
}
|
|
|
|
type StringLeaves<Type> =
|
|
| string
|
|
| string[]
|
|
| { [P in keyof Type]: StringLeaves<Type[P]> };
|
|
|
|
export type DeliveryInstructionPrintable = {
|
|
[Property in keyof DeliveryInstruction]: StringLeaves<
|
|
DeliveryInstruction[Property]
|
|
>;
|
|
};
|
|
|
|
export type RedeliveryInstructionPrintable = {
|
|
[Property in keyof RedeliveryInstruction]: StringLeaves<
|
|
RedeliveryInstruction[Property]
|
|
>;
|
|
};
|
|
|
|
export interface EVMExecutionInfoV1 {
|
|
gasLimit: BigNumber;
|
|
targetChainRefundPerGasUnused: BigNumber;
|
|
}
|
|
|
|
export enum VaaKeyType {
|
|
EMITTER_SEQUENCE = 0,
|
|
VAAHASH = 1,
|
|
}
|
|
|
|
export function parseWormholeRelayerPayloadType(
|
|
stringPayload: string | Buffer | Uint8Array
|
|
): RelayerPayloadId {
|
|
const payload =
|
|
typeof stringPayload === "string" ? arrayify(stringPayload) : stringPayload;
|
|
if (
|
|
payload[0] != RelayerPayloadId.Delivery &&
|
|
payload[0] != RelayerPayloadId.Redelivery
|
|
) {
|
|
throw new Error("Unrecognized payload type " + payload[0]);
|
|
}
|
|
return payload[0];
|
|
}
|
|
|
|
export function createVaaKey(
|
|
chainId: number,
|
|
emitterAddress: Buffer,
|
|
sequence: number | BigNumber
|
|
): VaaKey {
|
|
return {
|
|
chainId,
|
|
emitterAddress,
|
|
sequence: ethers.BigNumber.from(sequence),
|
|
};
|
|
}
|
|
|
|
export function parseWormholeRelayerSend(bytes: Buffer): DeliveryInstruction {
|
|
let idx = 0;
|
|
const payloadId = bytes.readUInt8(idx);
|
|
if (payloadId !== RelayerPayloadId.Delivery) {
|
|
throw new Error(
|
|
`Expected Delivery payload type (${RelayerPayloadId.Delivery}), found: ${payloadId}`
|
|
);
|
|
}
|
|
idx += 1;
|
|
|
|
const targetChainId = bytes.readUInt16BE(idx);
|
|
idx += 2;
|
|
const targetAddress = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
|
|
let payload: Buffer;
|
|
[payload, idx] = parsePayload(bytes, idx);
|
|
|
|
const requestedReceiverValue = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
|
);
|
|
idx += 32;
|
|
|
|
const extraReceiverValue = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
|
);
|
|
idx += 32;
|
|
|
|
let encodedExecutionInfo;
|
|
[encodedExecutionInfo, idx] = parsePayload(bytes, idx);
|
|
|
|
const refundChainId = bytes.readUInt16BE(idx);
|
|
idx += 2;
|
|
const refundAddress = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
const refundDeliveryProvider = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
const sourceDeliveryProvider = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
const senderAddress = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
const numMessages = bytes.readUInt8(idx);
|
|
idx += 1;
|
|
|
|
let messages = [] as VaaKey[];
|
|
for (let i = 0; i < numMessages; ++i) {
|
|
const res = parseVaaKey(bytes, idx);
|
|
idx = res[1];
|
|
messages.push(res[0]);
|
|
}
|
|
|
|
return {
|
|
targetChainId,
|
|
targetAddress,
|
|
payload,
|
|
requestedReceiverValue,
|
|
extraReceiverValue,
|
|
encodedExecutionInfo,
|
|
refundChainId,
|
|
refundAddress,
|
|
refundDeliveryProvider,
|
|
sourceDeliveryProvider,
|
|
senderAddress,
|
|
vaaKeys: messages,
|
|
};
|
|
}
|
|
|
|
function parsePayload(bytes: Buffer, idx: number): [Buffer, number] {
|
|
const length = bytes.readUInt32BE(idx);
|
|
idx += 4;
|
|
const payload = bytes.slice(idx, idx + length);
|
|
idx += length;
|
|
return [payload, idx];
|
|
}
|
|
|
|
function parseVaaKey(bytes: Buffer, idx: number): [VaaKey, number] {
|
|
const version = bytes.readUInt8(idx);
|
|
idx += 1;
|
|
|
|
const chainId = bytes.readUInt16BE(idx);
|
|
idx += 2;
|
|
const emitterAddress = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
const sequence = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 8)
|
|
);
|
|
idx += 8;
|
|
return [
|
|
{
|
|
chainId,
|
|
emitterAddress,
|
|
sequence,
|
|
},
|
|
idx,
|
|
];
|
|
}
|
|
|
|
export function parseEVMExecutionInfoV1(
|
|
bytes: Buffer,
|
|
idx: number
|
|
): [EVMExecutionInfoV1, number] {
|
|
idx += 31;
|
|
const version = bytes.readUInt8(idx);
|
|
idx += 1;
|
|
if (version !== ExecutionInfoVersion.EVM_V1) {
|
|
throw new Error("Unexpected Execution Info version");
|
|
}
|
|
const gasLimit = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
|
);
|
|
idx += 32;
|
|
const targetChainRefundPerGasUnused = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
|
);
|
|
idx += 32;
|
|
return [{ gasLimit, targetChainRefundPerGasUnused }, idx];
|
|
}
|
|
|
|
export function parseWormholeRelayerResend(
|
|
bytes: Buffer
|
|
): RedeliveryInstruction {
|
|
let idx = 0;
|
|
const payloadId = bytes.readUInt8(idx);
|
|
if (payloadId !== RelayerPayloadId.Redelivery) {
|
|
throw new Error(
|
|
`Expected Delivery payload type (${RelayerPayloadId.Redelivery}), found: ${payloadId}`
|
|
);
|
|
}
|
|
idx += 1;
|
|
|
|
const parsedKey = parseVaaKey(bytes, idx);
|
|
const key = parsedKey[0];
|
|
idx = parsedKey[1];
|
|
|
|
const targetChainId: number = bytes.readUInt16BE(idx);
|
|
idx += 2;
|
|
|
|
const newRequestedReceiverValue = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
|
);
|
|
idx += 32;
|
|
|
|
let newEncodedExecutionInfo;
|
|
[newEncodedExecutionInfo, idx] = parsePayload(bytes, idx);
|
|
|
|
const newSourceDeliveryProvider = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
|
|
const newSenderAddress = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
return {
|
|
deliveryVaaKey: key,
|
|
targetChainId,
|
|
newRequestedReceiverValue,
|
|
newEncodedExecutionInfo,
|
|
newSourceDeliveryProvider,
|
|
newSenderAddress,
|
|
};
|
|
}
|
|
|
|
export function executionInfoToString(encodedExecutionInfo: Buffer): string {
|
|
const [parsed] = parseEVMExecutionInfoV1(encodedExecutionInfo, 0);
|
|
return `Gas limit: ${parsed.gasLimit}, Target chain refund per unit gas unused: ${parsed.targetChainRefundPerGasUnused}`;
|
|
}
|
|
|
|
export function deliveryInstructionsPrintable(
|
|
ix: DeliveryInstruction
|
|
): DeliveryInstructionPrintable {
|
|
return {
|
|
targetChainId: ix.targetChainId.toString(),
|
|
targetAddress: ix.targetAddress.toString("hex"),
|
|
payload: ix.payload.toString("base64"),
|
|
requestedReceiverValue: ix.requestedReceiverValue.toString(),
|
|
extraReceiverValue: ix.requestedReceiverValue.toString(),
|
|
encodedExecutionInfo: executionInfoToString(ix.encodedExecutionInfo),
|
|
refundChainId: ix.refundChainId.toString(),
|
|
refundAddress: ix.refundAddress.toString("hex"),
|
|
refundDeliveryProvider: ix.refundDeliveryProvider.toString("hex"),
|
|
sourceDeliveryProvider: ix.sourceDeliveryProvider.toString("hex"),
|
|
senderAddress: ix.senderAddress.toString("hex"),
|
|
vaaKeys: ix.vaaKeys.map(vaaKeyPrintable),
|
|
};
|
|
}
|
|
|
|
export function vaaKeyPrintable(ix: VaaKey): StringLeaves<VaaKey> {
|
|
return {
|
|
chainId: ix.chainId?.toString(),
|
|
emitterAddress: ix.emitterAddress?.toString("hex"),
|
|
sequence: ix.sequence?.toString(),
|
|
};
|
|
}
|
|
|
|
export function redeliveryInstructionPrintable(
|
|
ix: RedeliveryInstruction
|
|
): RedeliveryInstructionPrintable {
|
|
return {
|
|
deliveryVaaKey: vaaKeyPrintable(ix.deliveryVaaKey),
|
|
targetChainId: ix.targetChainId.toString(),
|
|
newRequestedReceiverValue: ix.newRequestedReceiverValue.toString(),
|
|
newEncodedExecutionInfo: executionInfoToString(ix.newEncodedExecutionInfo),
|
|
newSourceDeliveryProvider: ix.newSourceDeliveryProvider.toString("hex"),
|
|
newSenderAddress: ix.newSenderAddress.toString("hex"),
|
|
};
|
|
}
|
|
|
|
export type DeliveryOverrideArgs = {
|
|
newReceiverValue: BigNumber;
|
|
newExecutionInfo: Buffer;
|
|
redeliveryHash: Buffer;
|
|
};
|
|
|
|
export function packOverrides(overrides: DeliveryOverrideArgs): string {
|
|
const packed = [
|
|
ethers.utils.solidityPack(["uint8"], [1]).substring(2), //version
|
|
ethers.utils
|
|
.solidityPack(["uint256"], [overrides.newReceiverValue])
|
|
.substring(2),
|
|
ethers.utils
|
|
.solidityPack(["uint32"], [overrides.newExecutionInfo.length])
|
|
.substring(2),
|
|
overrides.newExecutionInfo.toString("hex"),
|
|
overrides.redeliveryHash.toString("hex"), //toString('hex') doesn't add the 0x prefix
|
|
].join("");
|
|
|
|
return "0x" + packed;
|
|
}
|
|
|
|
export function parseForwardFailureError(bytes: Buffer): string {
|
|
let idx = 4;
|
|
idx += 32;
|
|
if (bytes.length <= idx) {
|
|
return `Delivery Provider failed in performing forward`;
|
|
}
|
|
try {
|
|
const amountOfFunds = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
|
);
|
|
idx += 32;
|
|
const amountOfFundsNeeded = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
|
);
|
|
return `Not enough funds leftover for forward: Had ${ethers.utils.formatEther(
|
|
amountOfFunds
|
|
)} and needed ${ethers.utils.formatEther(amountOfFundsNeeded)}.`;
|
|
} catch (err) {
|
|
return `Delivery Provider unexpectedly failed in performing forward`;
|
|
}
|
|
}
|
|
|
|
export function parseOverrideInfoFromDeliveryEvent(
|
|
bytes: Buffer
|
|
): DeliveryOverrideArgs {
|
|
let idx = 0;
|
|
const version = bytes.readUInt8(idx);
|
|
idx += 1;
|
|
const newReceiverValue = ethers.BigNumber.from(
|
|
Uint8Array.prototype.subarray.call(bytes, idx, idx + 32)
|
|
);
|
|
idx += 32;
|
|
|
|
let newExecutionInfo: Buffer;
|
|
[newExecutionInfo, idx] = parsePayload(bytes, idx);
|
|
|
|
const redeliveryHash = bytes.slice(idx, idx + 32);
|
|
idx += 32;
|
|
|
|
return {
|
|
newReceiverValue,
|
|
newExecutionInfo,
|
|
redeliveryHash,
|
|
};
|
|
}
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
export function dbg<T>(x: T, msg?: string): T {
|
|
if (msg) {
|
|
console.log("[DEBUG] " + msg);
|
|
}
|
|
console.log(x);
|
|
return x;
|
|
}
|