wormhole/clients/js/vaa.ts

818 lines
25 KiB
TypeScript

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<T> {
version: number
guardianSetIndex: number
signatures: Signature[]
timestamp: number
nonce: number
emitterChain: number
emitterAddress: string
sequence: bigint
consistencyLevel: number
payload: T
}
class P<T> {
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<U>(other: P<U>): P<T | U> {
let p = new P<T | U>(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<Payload | Other> {
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<Payload | Other>): asserts vaa is VAA<Payload> {
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<Buffer> {
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<Payload>) {
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<Payload | Other>) {
return solidityKeccak256(["bytes"], [solidityKeccak256(["bytes"], ["0x" + vaaBody(vaa)])])
}
function vaaBody(vaa: VAA<Payload | Other>) {
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<Payload>): 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<GuardianSetUpgrade> = 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<CoreContractUpgrade> =
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 extends "NFTBridge" | "TokenBridge"> {
module: Module
type: "ContractUpgrade"
chain: number
address: string
}
// Parse a portal contract upgrade payload
function portalContractUpgradeParser<Module extends "NFTBridge" | "TokenBridge">(module: Module): P<PortalContractUpgrade<Module>> {
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<Module extends "NFTBridge" | "TokenBridge">(payload: PortalContractUpgrade<Module>): 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 extends "NFTBridge" | "TokenBridge"> {
module: Module
type: "RegisterChain"
chain: number
emitterChain: number
emitterAddress: string
}
// Parse a portal chain registration payload
function portalRegisterChainParser<Module extends "NFTBridge" | "TokenBridge">(module: Module): P<PortalRegisterChain<Module>> {
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<Module extends "NFTBridge" | "TokenBridge">(payload: PortalRegisterChain<Module>): 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<TokenBridgeTransfer> {
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<TokenBridgeAttestMeta> {
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<TokenBridgeTransferWithPayload> {
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<NFTBridgeTransfer> {
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")
}