import { Parser } from "binary-parser" import { BigNumber, ethers } from "ethers" import { solidityKeccak256 } from "ethers/lib/utils" import * as elliptic from "elliptic" export interface Signature { guardianSetIndex: number signature: string } export interface VAA { version: number guardianSetIndex: number signatures: Signature[] timestamp: number nonce: number emitterChain: number emitterAddress: string sequence: bigint consistencyLevel: number payload: T } class P { private parser: Parser constructor(parser: Parser) { this.parser = parser } // Try to parse a buffer with a parser, and return null if it failed due to an // assertion error. parse(buffer: Buffer): T | null { try { let result = this.parser.parse(buffer) delete result['end'] return result } catch (e: any) { if (e.message?.includes("Assertion error")) { return null } else { throw e } } } or(other: P): P { let p = new P(other.parser); p.parse = (buffer: Buffer): T | U | null => { return this.parse(buffer) ?? other.parse(buffer) } return p } } export interface Other { type: "Other", hex: string, ascii?: string } // All the different types of payloads export type Payload = GuardianSetUpgrade | CoreContractUpgrade | PortalContractUpgrade<"TokenBridge"> | PortalContractUpgrade<"NFTBridge"> | PortalRegisterChain<"TokenBridge"> | PortalRegisterChain<"NFTBridge"> | TokenBridgeTransfer | TokenBridgeTransferWithPayload | TokenBridgeAttestMeta | NFTBridgeTransfer export type ContractUpgrade = CoreContractUpgrade | PortalContractUpgrade<"TokenBridge"> | PortalContractUpgrade<"NFTBridge"> export function parse(buffer: Buffer): VAA { const vaa = parseEnvelope(buffer) const parser = guardianSetUpgradeParser .or(coreContractUpgradeParser) .or(portalContractUpgradeParser("TokenBridge")) .or(portalContractUpgradeParser("NFTBridge")) .or(portalRegisterChainParser("TokenBridge")) .or(portalRegisterChainParser("NFTBridge")) .or(tokenBridgeTransferParser()) .or(tokenBridgeTransferWithPayloadParser()) .or(tokenBridgeAttestMetaParser()) .or(nftBridgeTransferParser()) let payload : Payload | Other | null = parser.parse(vaa.payload) if (payload === null) { payload = {type: "Other", hex: Buffer.from(vaa.payload).toString("hex"), ascii: Buffer.from(vaa.payload).toString('utf8')} } else { delete payload['tokenURILength'] } var myVAA = { ...vaa, payload } return myVAA } export function assertKnownPayload(vaa: VAA): asserts vaa is VAA { if (vaa.payload.type === "Other") { throw Error(`Couldn't parse VAA payload: ${vaa.payload.hex}`); } } // Parse the VAA envelope without looking into the payload. // If you want to parse the payload as well, use 'parse'. export function parseEnvelope(buffer: Buffer): VAA { var vaa = vaaParser.parse(buffer) delete vaa['end'] delete vaa['signatureCount'] vaa.payload = Buffer.from(vaa.payload) return vaa } // Parse a signature const signatureParser = new Parser() .endianess("big") .uint8("guardianSetIndex") .array("signature", { type: "uint8", lengthInBytes: 65, formatter: arr => Buffer.from(arr).toString("hex") }) function serialiseSignature(sig: Signature): string { const body = [ encode("uint8", sig.guardianSetIndex), sig.signature ] return body.join("") } // Parse a vaa envelope. The payload is returned as a byte array. const vaaParser = new Parser() .endianess("big") .uint8("version") .uint32("guardianSetIndex") .uint8("signatureCount") .array("signatures", { type: signatureParser, length: "signatureCount" }) .uint32("timestamp") .uint32("nonce") .uint16("emitterChain") .array("emitterAddress", { type: "uint8", lengthInBytes: 32, formatter: arr => "0x" + Buffer.from(arr).toString("hex") }) .uint64("sequence") .uint8("consistencyLevel") .array("payload", { type: "uint8", readUntil: "eof" }) .string("end", { greedy: true, assert: str => str === "" }) export function serialiseVAA(vaa: VAA) { const body = [ encode("uint8", vaa.version), encode("uint32", vaa.guardianSetIndex), encode("uint8", vaa.signatures.length), ...(vaa.signatures.map((sig) => serialiseSignature(sig))), vaaBody(vaa) ] return body.join("") } export function vaaDigest(vaa: VAA) { return solidityKeccak256(["bytes"], [solidityKeccak256(["bytes"], ["0x" + vaaBody(vaa)])]) } function vaaBody(vaa: VAA) { let payload_str: string if (vaa.payload.type === "Other") { payload_str = vaa.payload.hex } else { let payload = vaa.payload; switch (payload.module) { case "Core": switch (payload.type) { case "GuardianSetUpgrade": payload_str = serialiseGuardianSetUpgrade(payload) break case "ContractUpgrade": payload_str = serialiseCoreContractUpgrade(payload) break default: impossible(payload) break } break case "NFTBridge": switch (payload.type) { case "ContractUpgrade": payload_str = serialisePortalContractUpgrade(payload) break case "RegisterChain": payload_str = serialisePortalRegisterChain(payload) break case "Transfer": payload_str = serialiseNFTBridgeTransfer(payload) break default: impossible(payload) break } break case "TokenBridge": switch (payload.type) { case "ContractUpgrade": payload_str = serialisePortalContractUpgrade(payload) break case "RegisterChain": payload_str = serialisePortalRegisterChain(payload) break case "Transfer": payload_str = serialiseTokenBridgeTransfer(payload) break case "TransferWithPayload": payload_str = serialiseTokenBridgeTransferWithPayload(payload) break case "AttestMeta": payload_str = serialiseTokenBridgeAttestMeta(payload) break default: impossible(payload) break } break default: impossible(payload) break } } const body = [ encode("uint32", vaa.timestamp), encode("uint32", vaa.nonce), encode("uint16", vaa.emitterChain), encode("bytes32", hex(vaa.emitterAddress)), encode("uint64", vaa.sequence), encode("uint8", vaa.consistencyLevel), payload_str ] return body.join("") } export function sign(signers: string[], vaa: VAA): Signature[] { const hash = vaaDigest(vaa) const ec = new elliptic.ec("secp256k1") return signers.map((signer, i) => { const key = ec.keyFromPrivate(signer) const signature = key.sign(Buffer.from(hash.substr(2), "hex"), { canonical: true }) const packed = [ signature.r.toString("hex").padStart(64, "0"), signature.s.toString("hex").padStart(64, "0"), encode("uint8", signature.recoveryParam) ].join("") return { guardianSetIndex: i, signature: packed } }) } // Parse an address of given length, and render it as hex const addressParser = (length: number) => new Parser() .endianess("big") .array("address", { type: "uint8", lengthInBytes: length, formatter: (arr) => Buffer.from(arr).toString("hex") }) //////////////////////////////////////////////////////////////////////////////// // Guardian set upgrade export interface GuardianSetUpgrade { module: "Core" type: "GuardianSetUpgrade" chain: number newGuardianSetIndex: number newGuardianSetLength: number newGuardianSet: string[] } // Parse a guardian set upgrade payload const guardianSetUpgradeParser: P = new P(new Parser() .endianess("big") .string("module", { length: 32, encoding: "hex", assert: Buffer.from("Core").toString("hex").padStart(64, "0"), formatter: (_str) => "Core" }) .uint8("type", { assert: 2, formatter: (_action) => "GuardianSetUpgrade" }) .uint16("chain") .uint32("newGuardianSetIndex") .uint8("newGuardianSetLength") .array("newGuardianSet", { type: addressParser(20), length: "newGuardianSetLength", formatter: (arr: [{ address: string }]) => arr.map((addr) => addr.address) }) .string("end", { greedy: true, assert: str => str === "" })) function serialiseGuardianSetUpgrade(payload: GuardianSetUpgrade): string { const body = [ encode("bytes32", encodeString(payload.module)), encode("uint8", 2), encode("uint16", payload.chain), encode("uint32", payload.newGuardianSetIndex), encode("uint8", payload.newGuardianSet.length), ...payload.newGuardianSet ] return body.join("") } //////////////////////////////////////////////////////////////////////////////// // Contract upgrades export interface CoreContractUpgrade { module: "Core" type: "ContractUpgrade" chain: number address: string } // Parse a core contract upgrade payload const coreContractUpgradeParser: P = new P(new Parser() .endianess("big") .string("module", { length: 32, encoding: "hex", assert: Buffer.from("Core").toString("hex").padStart(64, "0"), formatter: (_str) => "Core" }) .uint8("type", { assert: 1, formatter: (_action) => "ContractUpgrade" }) .uint16("chain") .array("address", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .string("end", { greedy: true, assert: str => str === "" })) function serialiseCoreContractUpgrade(payload: CoreContractUpgrade): string { const body = [ encode("bytes32", encodeString(payload.module)), encode("uint8", 1), encode("uint16", payload.chain), encode("bytes32", payload.address) ] return body.join("") } export interface PortalContractUpgrade { module: Module type: "ContractUpgrade" chain: number address: string } // Parse a portal contract upgrade payload function portalContractUpgradeParser(module: Module): P> { return new P(new Parser() .endianess("big") .string("module", { length: 32, encoding: "hex", assert: Buffer.from(module).toString("hex").padStart(64, "0"), formatter: (_str: string) => module }) .uint8("type", { assert: 2, formatter: (_action: number) => "ContractUpgrade" }) .uint16("chain") .array("address", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .string("end", { greedy: true, assert: str => str === "" })) } function serialisePortalContractUpgrade(payload: PortalContractUpgrade): string { const body = [ encode("bytes32", encodeString(payload.module)), encode("uint8", 2), encode("uint16", payload.chain), encode("bytes32", payload.address) ] return body.join("") } //////////////////////////////////////////////////////////////////////////////// // Registrations export interface PortalRegisterChain { module: Module type: "RegisterChain" chain: number emitterChain: number emitterAddress: string } // Parse a portal chain registration payload function portalRegisterChainParser(module: Module): P> { return new P(new Parser() .endianess("big") .string("module", { length: 32, encoding: "hex", assert: Buffer.from(module).toString("hex").padStart(64, "0"), formatter: (_str) => module }) .uint8("type", { assert: 1, formatter: (_action) => "RegisterChain" }) .uint16("chain") .uint16("emitterChain") .array("emitterAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .string("end", { greedy: true, assert: str => str === "" }) ) } function serialisePortalRegisterChain(payload: PortalRegisterChain): string { const body = [ encode("bytes32", encodeString(payload.module)), encode("uint8", 1), encode("uint16", payload.chain), encode("uint16", payload.emitterChain), encode("bytes32", payload.emitterAddress) ] return body.join("") } //////////////////////////////////////////////////////////////////////////////// // Token bridge // payload 1 export interface TokenBridgeTransfer { module: "TokenBridge" type: "Transfer" amount: bigint tokenAddress: string tokenChain: number toAddress: string chain: number fee: bigint } function tokenBridgeTransferParser(): P { return new P(new Parser() .endianess("big") .string("module", { length: (_) => 0, formatter: (_) => "TokenBridge" }) .uint8("type", { assert: 1, formatter: (_action) => "Transfer" }) .array("amount", { type: "uint8", lengthInBytes: 32, formatter: (bytes) => BigNumber.from(bytes).toBigInt() }) .array("tokenAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .uint16("tokenChain") .array("toAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .uint16("chain") .array("fee", { type: "uint8", lengthInBytes: 32, formatter: (bytes) => BigNumber.from(bytes).toBigInt() }) .string("end", { greedy: true, assert: str => str === "" }) ) } function serialiseTokenBridgeTransfer(payload: TokenBridgeTransfer): string { const body = [ encode("uint8", 1), encode("uint256", payload.amount), encode("bytes32", hex(payload.tokenAddress)), encode("uint16", payload.tokenChain), encode("bytes32", hex(payload.toAddress)), encode("uint16", payload.chain), encode("uint256", payload.fee), ] return body.join("") } // payload 2 export interface TokenBridgeAttestMeta { module: "TokenBridge" type: "AttestMeta" chain: 0, tokenAddress: string tokenChain: number decimals: number symbol: string name: string } function tokenBridgeAttestMetaParser(): P { return new P(new Parser() .endianess("big") .string("module", { length: (_) => 0, formatter: (_) => "TokenBridge" }) .string("chain", { length: (_) => 0, formatter: (_) => 0 }) .uint8("type", { assert: 2, formatter: (_action) => "AttestMeta" }) .array("tokenAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .uint16("tokenChain") .uint8("decimals") .array("symbol", { type: "uint8", lengthInBytes: 32, formatter: (arr: Uint8Array) => Buffer.from(arr).toString("utf8", arr.findIndex((val) => val != 0)) }) .array("name", { type: "uint8", lengthInBytes: 32, formatter: (arr: Uint8Array) => Buffer.from(arr).toString("utf8", arr.findIndex((val) => val != 0)) }) .string("end", { greedy: true, assert: str => str === "" }) ) } function serialiseTokenBridgeAttestMeta(payload: TokenBridgeAttestMeta): string { const body = [ encode("uint8", 2), encode("bytes32", hex(payload.tokenAddress)), encode("uint16", payload.tokenChain), encode("uint8", payload.decimals), encode("bytes32", encodeStringRight(payload.symbol)), encode("bytes32", encodeStringRight(payload.name)), ] return body.join("") } // payload 3 export interface TokenBridgeTransferWithPayload { module: "TokenBridge" type: "TransferWithPayload" amount: bigint tokenAddress: string tokenChain: number toAddress: string chain: number fromAddress: string, payload: string } function tokenBridgeTransferWithPayloadParser(): P { return new P(new Parser() .endianess("big") .string("module", { length: (_) => 0, formatter: (_) => "TokenBridge" }) .uint8("type", { assert: 3, formatter: (_action) => "TransferWithPayload" }) .array("amount", { type: "uint8", lengthInBytes: 32, formatter: (bytes) => BigNumber.from(bytes).toBigInt() }) .array("tokenAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .uint16("tokenChain") .array("toAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .uint16("chain") .array("fromAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }). array("payload", { type: "uint8", greedy: true, readUntil: "eof", formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) ) } function serialiseTokenBridgeTransferWithPayload(payload: TokenBridgeTransferWithPayload): string { const body = [ encode("uint8", 3), encode("uint256", payload.amount), encode("bytes32", hex(payload.tokenAddress)), encode("uint16", payload.tokenChain), encode("bytes32", hex(payload.toAddress)), encode("uint16", payload.chain), encode("bytes32", hex(payload.fromAddress)), payload.payload.substring(2) ] return body.join("") } //////////////////////////////////////////////////////////////////////////////// // NFT bridge export interface NFTBridgeTransfer { module: "NFTBridge" type: "Transfer" tokenAddress: string tokenChain: number tokenSymbol: string tokenName: string tokenId: bigint tokenURI: string toAddress: string chain: number } function nftBridgeTransferParser(): P { return new P(new Parser() .endianess("big") .string("module", { length: (_) => 0, formatter: (_) => "NFTBridge" }) .uint8("type", { assert: 1, formatter: (_action) => "Transfer" }) .array("tokenAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .uint16("tokenChain") .array("tokenSymbol", { type: "uint8", lengthInBytes: 32, formatter: (arr: Uint8Array) => Buffer.from(arr).toString("utf8", arr.findIndex((val) => val != 0)) }) .array("tokenName", { type: "uint8", lengthInBytes: 32, formatter: (arr: Uint8Array) => Buffer.from(arr).toString("utf8", arr.findIndex((val) => val != 0)) }) .array("tokenId", { type: "uint8", lengthInBytes: 32, formatter: (bytes) => BigNumber.from(bytes).toBigInt() }) .uint8("tokenURILength") .array("tokenURI", { type: "uint8", lengthInBytes: function() { return this.tokenURILength }, formatter: (arr: Uint8Array) => Buffer.from(arr).toString("utf8") }) .array("toAddress", { type: "uint8", lengthInBytes: 32, formatter: (arr) => "0x" + Buffer.from(arr).toString("hex") }) .uint16("chain") .string("end", { greedy: true, assert: str => str === "" }) ) } function serialiseNFTBridgeTransfer(payload: NFTBridgeTransfer): string { const body = [ encode("uint8", 1), encode("bytes32", hex(payload.tokenAddress)), encode("uint16", payload.tokenChain), encode("bytes32", encodeStringRight(payload.tokenSymbol)), encode("bytes32", encodeStringRight(payload.tokenName)), encode("uint256", payload.tokenId), encode("uint8", payload.tokenURI.length), Buffer.from(payload.tokenURI, "utf8").toString("hex"), encode("bytes32", hex(payload.toAddress)), encode("uint16", payload.chain), ] return body.join("") } // This function should be called after pattern matching on all possible options // of an enum (union) type, so that typescript can derive that no other options // are possible. If (from JavaScript land) an unsupported argument is passed // in, this function just throws. If the enum type is extended with new cases, // the call to this function will then fail to compile, drawing attention to an // unhandled case somewhere. export function impossible(a: never): any { throw new Error(`Impossible: ${a}`) } //////////////////////////////////////////////////////////////////////////////// // Encoder utils export type Encoding = "uint8" | "uint16" | "uint32" | "uint64" | "uint128" | "uint256" | "bytes32" | "address" export function typeWidth(type: Encoding): number { switch (type) { case "uint8": return 1 case "uint16": return 2 case "uint32": return 4 case "uint64": return 8 case "uint128": return 16 case "uint256": return 32 case "bytes32": return 32 case "address": return 20 } } // Couldn't find a satisfactory binary serialisation solution, so we just use // the ethers library's encoding logic export function encode(type: Encoding, val: any): string { // ethers operates on hex strings (sigh) and left pads everything to 32 // bytes (64 characters). We take last 2*n characters where n is the width // of the type being serialised in bytes (since a byte is represented as 2 // digits in hex). return ethers.utils.defaultAbiCoder.encode([type], [val]).substr(-2 * typeWidth(type)) } // Encode a string as binary left-padded to 32 bytes, represented as a hex // string (64 chars long) export function encodeString(str: string): Buffer { return Buffer.from(Buffer.from(str).toString("hex").padStart(64, "0"), "hex") } // Encode a string as binary right-padded to 32 bytes, represented as a hex // string (64 chars long) export function encodeStringRight(str: string): Buffer { return Buffer.from(Buffer.from(str).toString("hex").padEnd(64, "0"), "hex") } // Turn hex string with potentially missing 0x prefix into Buffer function hex(x: string): Buffer { return Buffer.from(ethers.utils.hexlify(x, { allowMissingPrefix: true }).substring(2), "hex") }