diff --git a/solana/Anchor.toml b/solana/Anchor.toml index ffe16742..9453f017 100644 --- a/solana/Anchor.toml +++ b/solana/Anchor.toml @@ -31,8 +31,8 @@ cluster = "Localnet" wallet = "ts/tests/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json" [scripts] -test-local = "npx ts-mocha -p ./tsconfig.anchor.json -t 1000000 --bail --exit ts/tests/0[0-9]*.ts" -test-upgrade-fork = "npx ts-mocha -p ./tsconfig.anchor.json -t 1000000 --bail --exit ts/tests/1[0-9]*.ts" +test-local = "npx ts-mocha -p ./tsconfig.anchor.json -t 1000000 --bail --full-trace --exit ts/tests/0[0-9]*.ts" +test-upgrade-fork = "npx ts-mocha -p ./tsconfig.anchor.json -t 1000000 --bail --full-trace --exit ts/tests/1[0-9]*.ts" [test] startup_wait = 20000 diff --git a/solana/ts/src/cctp/messageTransmitter/index.ts b/solana/ts/src/cctp/messageTransmitter/index.ts index 4366bffd..673ed029 100644 --- a/solana/ts/src/cctp/messageTransmitter/index.ts +++ b/solana/ts/src/cctp/messageTransmitter/index.ts @@ -6,10 +6,7 @@ import { IDL, MessageTransmitter } from "../types/message_transmitter"; import { MessageSent } from "./MessageSent"; import { MessageTransmitterConfig } from "./MessageTransmitterConfig"; import { UsedNonses } from "./UsedNonces"; - -export const PROGRAM_IDS = ["CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd"] as const; - -export type ProgramId = (typeof PROGRAM_IDS)[number]; +import { CircleContracts } from "@wormhole-foundation/sdk-base/contracts"; export type ReceiveTokenMessengerMinterMessageAccounts = { authority: PublicKey; @@ -28,15 +25,11 @@ export type ReceiveTokenMessengerMinterMessageAccounts = { }; export class MessageTransmitterProgram { - private _programId: ProgramId; - program: Program; - constructor(connection: Connection, programId?: ProgramId) { - this._programId = programId ?? testnet(); - this.program = new Program(IDL, new PublicKey(this._programId), { - connection, - }); + constructor(connection: Connection, private contracts: CircleContracts) { + const programId = new PublicKey(contracts.messageTransmitter); + this.program = new Program(IDL, new PublicKey(programId), { connection }); } get ID(): PublicKey { @@ -71,23 +64,7 @@ export class MessageTransmitterProgram { } tokenMessengerMinterProgram(): TokenMessengerMinterProgram { - switch (this._programId) { - case testnet(): { - return new TokenMessengerMinterProgram( - this.program.provider.connection, - "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3", - ); - } - case mainnet(): { - return new TokenMessengerMinterProgram( - this.program.provider.connection, - "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3", - ); - } - default: { - throw new Error("unsupported network"); - } - } + return new TokenMessengerMinterProgram(this.program.provider.connection, this.contracts); } receiveTokenMessengerMinterMessageAccounts( @@ -133,11 +110,3 @@ export class MessageTransmitterProgram { .instruction(); } } - -export function mainnet(): ProgramId { - return "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd"; -} - -export function testnet(): ProgramId { - return "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd"; -} diff --git a/solana/ts/src/cctp/messages.ts b/solana/ts/src/cctp/messages.ts index 28894d37..dfd5aa35 100644 --- a/solana/ts/src/cctp/messages.ts +++ b/solana/ts/src/cctp/messages.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { CircleBridge, UniversalAddress } from "@wormhole-foundation/sdk-definitions"; export type Cctp = { version: number; @@ -13,9 +13,9 @@ export type Cctp = { // Taken from https://developers.circle.com/stablecoins/docs/message-format. export class CctpMessage { cctp: Cctp; - message: Buffer; + message: CctpTokenBurnMessage; - constructor(cctp: Cctp, message: Buffer) { + constructor(cctp: Cctp, message: CctpTokenBurnMessage) { this.cctp = cctp; this.message = message; } @@ -30,31 +30,60 @@ export class CctpMessage { static decode(buf: Readonly): CctpMessage { const version = buf.readUInt32BE(0); - const sourceDomain = buf.readUInt32BE(4); - const destinationDomain = buf.readUInt32BE(8); - const nonce = buf.readBigUInt64BE(12); - const sender = Array.from(buf.slice(20, 52)); - const recipient = Array.from(buf.slice(52, 84)); - const targetCaller = Array.from(buf.slice(84, 116)); - const message = buf.subarray(116); + + const [msg] = CircleBridge.deserialize(new Uint8Array(buf)); + const { + sourceDomain, + destinationDomain, + nonce, + sender, + recipient, + destinationCaller, + payload, + } = msg; + + const { burnToken, mintRecipient, amount, messageSender } = payload; + const header: Cctp = { + version, + sourceDomain, + destinationDomain, + nonce, + sender: Array.from(sender.toUint8Array()), + recipient: Array.from(recipient.toUint8Array()), + targetCaller: Array.from(destinationCaller.toUint8Array()), + }; return new CctpMessage( - { + header, + new CctpTokenBurnMessage( + header, version, - sourceDomain, - destinationDomain, - nonce, - sender, - recipient, - targetCaller, - }, - message, + Array.from(burnToken.toUint8Array()), + Array.from(mintRecipient.toUint8Array()), + amount, + Array.from(messageSender.toUint8Array()), + ), ); } encode(): Buffer { const { cctp, message } = this; - return Buffer.concat([encodeCctp(cctp), message]); + return Buffer.from( + CircleBridge.serialize({ + sourceDomain: cctp.sourceDomain, + destinationDomain: cctp.destinationDomain, + nonce: cctp.nonce, + sender: new UniversalAddress(new Uint8Array(cctp.sender)), + recipient: new UniversalAddress(new Uint8Array(cctp.recipient)), + destinationCaller: new UniversalAddress(new Uint8Array(cctp.targetCaller)), + payload: { + burnToken: new UniversalAddress(new Uint8Array(message.burnTokenAddress)), + mintRecipient: new UniversalAddress(new Uint8Array(message.mintRecipient)), + amount: message.amount, + messageSender: new UniversalAddress(new Uint8Array(message.sender)), + }, + }), + ); } } @@ -91,63 +120,11 @@ export class CctpTokenBurnMessage { } static decode(buf: Readonly): CctpTokenBurnMessage { - const { cctp, message } = CctpMessage.decode(buf); - const version = message.readUInt32BE(0); - const burnTokenAddress = Array.from(message.subarray(4, 36)); - const mintRecipient = Array.from(message.subarray(36, 68)); - const amount = BigInt(ethers.BigNumber.from(message.subarray(68, 100)).toString()); - const sender = Array.from(message.subarray(100, 132)); - - return new CctpTokenBurnMessage( - cctp, - version, - burnTokenAddress, - mintRecipient, - amount, - sender, - ); + const { message } = CctpMessage.decode(buf); + return message; } encode(): Buffer { - const buf = Buffer.alloc(132); - - const { cctp, version, burnTokenAddress, mintRecipient, amount, sender } = this; - - let offset = 0; - offset = buf.writeUInt32BE(version, offset); - buf.set(burnTokenAddress, offset); - offset += 32; - buf.set(mintRecipient, offset); - offset += 32; - - // Special handling w/ uint256. This value will most likely encoded in < 32 bytes, so we - // jump ahead by 32 and subtract the length of the encoded value. - const encodedAmount = ethers.utils.arrayify(ethers.BigNumber.from(amount.toString())); - buf.set(encodedAmount, (offset += 32) - encodedAmount.length); - - buf.set(sender, offset); - offset += 32; - - return Buffer.concat([encodeCctp(cctp), buf]); + return new CctpMessage(this.cctp, this).encode(); } } - -function encodeCctp(cctp: Cctp): Buffer { - const buf = Buffer.alloc(116); - - const { version, sourceDomain, destinationDomain, nonce, sender, recipient, targetCaller } = - cctp; - - let offset = 0; - offset = buf.writeUInt32BE(version, offset); - offset = buf.writeUInt32BE(sourceDomain, offset); - offset = buf.writeUInt32BE(destinationDomain, offset); - offset = buf.writeBigUInt64BE(nonce, offset); - buf.set(sender, offset); - offset += 32; - buf.set(recipient, offset); - offset += 32; - buf.set(targetCaller, offset); - - return buf; -} diff --git a/solana/ts/src/cctp/tokenMessengerMinter/index.ts b/solana/ts/src/cctp/tokenMessengerMinter/index.ts index 6541cbec..e6ff4db2 100644 --- a/solana/ts/src/cctp/tokenMessengerMinter/index.ts +++ b/solana/ts/src/cctp/tokenMessengerMinter/index.ts @@ -1,13 +1,10 @@ -import { Program } from "anchor-0.29.0"; import { Connection, PublicKey } from "@solana/web3.js"; +import { CircleContracts } from "@wormhole-foundation/sdk-base/contracts"; +import { Program } from "anchor-0.29.0"; import { MessageTransmitterProgram } from "../messageTransmitter"; import { IDL, TokenMessengerMinter } from "../types/token_messenger_minter"; import { RemoteTokenMessenger } from "./RemoteTokenMessenger"; -export const PROGRAM_IDS = ["CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3"] as const; - -export type ProgramId = (typeof PROGRAM_IDS)[number]; - export type DepositForBurnWithCallerAccounts = { senderAuthority: PublicKey; messageTransmitterConfig: PublicKey; @@ -21,15 +18,11 @@ export type DepositForBurnWithCallerAccounts = { }; export class TokenMessengerMinterProgram { - private _programId: ProgramId; - program: Program; - constructor(connection: Connection, programId?: ProgramId) { - this._programId = programId ?? testnet(); - this.program = new Program(IDL, new PublicKey(this._programId), { - connection, - }); + constructor(connection: Connection, private contracts: CircleContracts) { + const programId = new PublicKey(contracts.tokenMessenger); + this.program = new Program(IDL, programId, { connection }); } get ID(): PublicKey { @@ -89,23 +82,7 @@ export class TokenMessengerMinterProgram { } messageTransmitterProgram(): MessageTransmitterProgram { - switch (this._programId) { - case testnet(): { - return new MessageTransmitterProgram( - this.program.provider.connection, - "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd", - ); - } - case mainnet(): { - return new MessageTransmitterProgram( - this.program.provider.connection, - "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd", - ); - } - default: { - throw new Error("unsupported network"); - } - } + return new MessageTransmitterProgram(this.program.provider.connection, this.contracts); } depositForBurnWithCallerAccounts( @@ -126,11 +103,3 @@ export class TokenMessengerMinterProgram { }; } } - -export function mainnet(): ProgramId { - return "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3"; -} - -export function testnet(): ProgramId { - return "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3"; -} diff --git a/solana/ts/src/common/index.ts b/solana/ts/src/common/index.ts index bf4e29a4..fbfc1e64 100644 --- a/solana/ts/src/common/index.ts +++ b/solana/ts/src/common/index.ts @@ -1,20 +1,13 @@ import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { MessageTransmitterProgram } from "../cctp"; import { BN } from "@coral-xyz/anchor"; +import { VAA, keccak256 } from "@wormhole-foundation/sdk-definitions"; export * from "./messages"; export * from "./state"; export type Uint64 = bigint | BN | number; -export function isUint64(value: Uint64): boolean { - return ( - typeof value === "bigint" || - (typeof value === "object" && value instanceof BN) || - typeof value === "number" - ); -} - export function uint64ToBigInt(value: Uint64): bigint { if (typeof value === "bigint") { return value; @@ -38,32 +31,7 @@ export function uint64ToBN(value: Uint64): BN { } export type VaaHash = Array | Buffer | Uint8Array; - -export function vaaHashToUint8Array(vaaHash: VaaHash): Uint8Array { - if (Array.isArray(vaaHash)) { - return Uint8Array.from(vaaHash); - } else if (Buffer.isBuffer(vaaHash)) { - return Uint8Array.from(vaaHash); - } else { - return vaaHash; - } -} - -export function vaaHashToBuffer(vaaHash: VaaHash): Buffer { - if (Buffer.isBuffer(vaaHash)) { - return vaaHash; - } else { - return Buffer.from(vaaHashToUint8Array(vaaHash)); - } -} - -export function vaaHashToArray(vaaHash: VaaHash): Array { - if (Array.isArray(vaaHash)) { - return vaaHash; - } else { - return Array.from(vaaHashToUint8Array(vaaHash)); - } -} +export const vaaHash = (vaa: VAA): VaaHash => keccak256(vaa.hash); export async function reclaimCctpMessageIx( messageTransmitter: MessageTransmitterProgram, @@ -76,10 +44,7 @@ export async function reclaimCctpMessageIx( const { payer, cctpMessage: messageSentEventData } = accounts; return messageTransmitter.reclaimEventAccountIx( - { - payee: payer, - messageSentEventData, - }, + { payee: payer, messageSentEventData }, cctpAttestation, ); } diff --git a/solana/ts/src/matchingEngine/index.ts b/solana/ts/src/matchingEngine/index.ts index dd25dd7a..8c038857 100644 --- a/solana/ts/src/matchingEngine/index.ts +++ b/solana/ts/src/matchingEngine/index.ts @@ -1,5 +1,4 @@ export * from "./state"; - import { BN, Program, utils } from "@coral-xyz/anchor"; import * as splToken from "@solana/spl-token"; import { @@ -15,24 +14,24 @@ import { SystemProgram, TransactionInstruction, } from "@solana/web3.js"; +import { MatchingEngine } from "@wormhole-foundation/example-liquidity-layer-definitions"; +import { ChainId, isChainId, toChainId } from "@wormhole-foundation/sdk-base"; import { PreparedTransaction, PreparedTransactionOptions } from ".."; -import IDL from "../idl/json/matching_engine.json"; -import { MatchingEngine } from "../idl/ts/matching_engine"; import { MessageTransmitterProgram, TokenMessengerMinterProgram } from "../cctp"; import { LiquidityLayerMessage, Uint64, VaaHash, - cctpMessageAddress, - coreMessageAddress, reclaimCctpMessageIx, uint64ToBN, uint64ToBigInt, - writeUint64BE, } from "../common"; +import IDL from "../idl/json/matching_engine.json"; +import { type MatchingEngine as MatchingEngineType } from "../idl/ts/matching_engine"; import { UpgradeManagerProgram } from "../upgradeManager"; import { BPF_LOADER_UPGRADEABLE_PROGRAM_ID, programDataAddress } from "../utils"; import { VaaAccount } from "../wormhole"; +import { programDerivedAddresses } from "./pdas"; import { Auction, AuctionConfig, @@ -44,188 +43,55 @@ import { Custodian, EndpointInfo, FastFill, - FastFillInfo, - FastFillSeeds, FastFillSequencer, - MessageProtocol, PreparedOrderResponse, Proposal, - ProposalAction, ReservedFastFillSequence, RouterEndpoint, } from "./state"; -import { ChainId, toChainId, isChainId } from "@wormhole-foundation/sdk-base"; - -export const PROGRAM_IDS = [ - "MatchingEngine11111111111111111111111111111", - "mPydpGUWxzERTNpyvTKdvS7v8kvw5sgwfiP8WQFrXVS", -] as const; +import { + AddCctpRouterEndpointArgs, + AuctionSettled, + AuctionUpdated, + BurnAndPublishAccounts, + CctpMessageArgs, + Enacted, + FastFillRedeemed, + FastFillSequenceReserved, + FastOrderPathComposite, + LocalFastOrderFilled, + MatchingEngineCommonAccounts, + OrderExecuted, + Proposed, + PublishMessageAccounts, + RedeemFastFillAccounts, + ReserveFastFillSequenceCompositeOpts, +} from "./types"; + +export * from "./types"; export const FEE_PRECISION_MAX = 1_000_000n; export const CPI_EVENT_IX_SELECTOR = Uint8Array.from([228, 69, 165, 46, 81, 203, 154, 29]); -export type ProgramId = (typeof PROGRAM_IDS)[number]; - -export type AddCctpRouterEndpointArgs = { - chain: ChainId; - cctpDomain: number; - address: Array; - mintRecipient: Array | null; -}; - -export type WormholeCoreBridgeAccounts = { - coreBridgeConfig: PublicKey; - coreEmitterSequence: PublicKey; - coreFeeCollector: PublicKey; - coreBridgeProgram: PublicKey; -}; - -export type PublishMessageAccounts = WormholeCoreBridgeAccounts & { - custodian: PublicKey; - coreMessage: PublicKey; -}; - -export type MatchingEngineCommonAccounts = WormholeCoreBridgeAccounts & { - matchingEngineProgram: PublicKey; - systemProgram: PublicKey; - rent: PublicKey; - clock: PublicKey; - custodian: PublicKey; - cctpMintRecipient: PublicKey; - tokenMessenger: PublicKey; - tokenMinter: PublicKey; - tokenMessengerMinterSenderAuthority: PublicKey; - tokenMessengerMinterProgram: PublicKey; - messageTransmitterAuthority: PublicKey; - messageTransmitterConfig: PublicKey; - messageTransmitterProgram: PublicKey; - tokenProgram: PublicKey; - mint: PublicKey; - localToken: PublicKey; - tokenMessengerMinterCustodyToken: PublicKey; -}; - -export type BurnAndPublishAccounts = { - custodian: PublicKey; - routerEndpoint: PublicKey; - coreMessage: PublicKey; - cctpMessage: PublicKey; - coreBridgeConfig: PublicKey; - coreEmitterSequence: PublicKey; - coreFeeCollector: PublicKey; - coreBridgeProgram: PublicKey; - tokenMessengerMinterSenderAuthority: PublicKey; - messageTransmitterConfig: PublicKey; - tokenMessenger: PublicKey; - remoteTokenMessenger: PublicKey; - tokenMinter: PublicKey; - localToken: PublicKey; - tokenMessengerMinterEventAuthority: PublicKey; - messageTransmitterProgram: PublicKey; - tokenMessengerMinterProgram: PublicKey; -}; - -export type RedeemFastFillAccounts = { - custodian: PublicKey; - fromRouterEndpoint: PublicKey; - toRouterEndpoint: PublicKey; - localCustodyToken: PublicKey; - matchingEngineProgram: PublicKey; -}; - -export type CctpMessageArgs = { - encodedCctpMessage: Buffer; - cctpAttestation: Buffer; -}; - -export type SettledTokenAccountInfo = { - key: PublicKey; - balanceAfter: BN; -}; - -export type AuctionSettled = { - auction: PublicKey; - bestOfferToken: SettledTokenAccountInfo | null; - executorToken: SettledTokenAccountInfo | null; - withExecute: MessageProtocol | null; -}; - -export type AuctionUpdated = { - configId: number; - auction: PublicKey; - vaa: PublicKey | null; - sourceChain: number; - targetProtocol: MessageProtocol; - redeemerMessageLen: number; - endSlot: BN; - bestOfferToken: PublicKey; - tokenBalanceBefore: BN; - amountIn: BN; - totalDeposit: BN; - maxOfferPriceAllowed: BN | null; -}; - -export type OrderExecuted = { - auction: PublicKey; - vaa: PublicKey; - targetProtocol: MessageProtocol; -}; - -export type Proposed = { - action: ProposalAction; -}; - -export type Enacted = { - action: ProposalAction; -}; - -export type LocalFastOrderFilled = { - seeds: FastFillSeeds; - info: FastFillInfo; - auction: PublicKey | null; -}; - -export type FastFillSequenceReserved = { - fastVaaHash: Array; - fastFillSeeds: FastFillSeeds; -}; - -export type FastFillRedeemed = { - preparedBy: PublicKey; - fastFill: PublicKey; -}; - -export type FastOrderPathComposite = { - fastVaa: { - vaa: PublicKey; - }; - path: { - fromEndpoint: { - endpoint: PublicKey; - }; - toEndpoint: { endpoint: PublicKey }; - }; -}; - -export type ReserveFastFillSequenceCompositeOpts = { - fastVaaHash?: VaaHash; - sourceChain?: ChainId; - orderSender?: Array; - targetChain?: ChainId; -}; - export class MatchingEngineProgram { - private _programId: ProgramId; private _mint: PublicKey; + private _custodian?: Custodian; - program: Program; + pdas: ReturnType; - constructor(connection: Connection, programId: ProgramId, mint: PublicKey) { - this._programId = programId; - this._mint = mint; + program: Program; + + constructor(connection: Connection, private _addresses: MatchingEngine.Addresses) { + const programId = _addresses.matchingEngine; + this._mint = new PublicKey(_addresses.cctp.usdcMint); + this.pdas = programDerivedAddresses( + new PublicKey(programId), + this._mint, + this.coreBridgeProgramId, + ); this.program = new Program( - { ...(IDL as any), address: this._programId }, + { ...(IDL as any), address: programId }, { connection, }, @@ -240,6 +106,10 @@ export class MatchingEngineProgram { return this._mint; } + get coreBridgeProgramId(): PublicKey { + return new PublicKey(this._addresses.coreBridge); + } + onAuctionSettled(callback: (event: AuctionSettled, slot: number, signature: string) => void) { return this.program.addEventListener("auctionSettled", callback); } @@ -346,21 +216,16 @@ export class MatchingEngineProgram { return this.program.addEventListener("fastFillRedeemed", callback); } - eventAuthorityAddress(): PublicKey { - return PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], this.ID)[0]; - } - - custodianAddress(): PublicKey { - return Custodian.address(this.ID); + /** Get the cached Custodian if it exists, otherwise fetch the latest and cache it */ + async getCustodian(skipCache?: boolean): Promise { + if (this._custodian === undefined || skipCache) + this._custodian = await this.fetchCustodian(); + return this._custodian!; } - async fetchCustodian(input?: { address: PublicKey }): Promise { - const addr = input === undefined ? this.custodianAddress() : input.address; - return this.program.account.custodian.fetch(addr); - } - - auctionConfigAddress(id: number): PublicKey { - return AuctionConfig.address(this.ID, id); + /** Fetch the latest custodian data */ + async fetchCustodian(): Promise { + return this.program.account.custodian.fetch(this.custodianAddress()); } async fetchAuctionConfig(input: number | { address: PublicKey }): Promise { @@ -370,20 +235,12 @@ export class MatchingEngineProgram { async fetchAuctionParameters(id?: number): Promise { if (id === undefined) { - const { auctionConfigId } = await this.fetchCustodian(); + const { auctionConfigId } = await this.getCustodian(); id = auctionConfigId; } return this.fetchAuctionConfig(id).then((config) => config.parameters); } - cctpMintRecipientAddress(): PublicKey { - return splToken.getAssociatedTokenAddressSync(this.mint, this.custodianAddress(), true); - } - - routerEndpointAddress(chain: ChainId): PublicKey { - return RouterEndpoint.address(this.ID, chain); - } - async fetchRouterEndpoint(input: ChainId | { address: PublicKey }): Promise { const addr = typeof input == "object" && "address" in input @@ -397,39 +254,16 @@ export class MatchingEngineProgram { return info; } - auctionAddress(vaaHash: VaaHash): PublicKey { - return Auction.address(this.ID, vaaHash); - } - async fetchAuction(input: VaaHash | { address: PublicKey }): Promise { const addr = "address" in input ? input.address : this.auctionAddress(input); - // @ts-ignore This is BS. This is correct. return this.program.account.auction.fetch(addr); } - async proposalAddress(proposalId?: Uint64): Promise { - if (proposalId === undefined) { - const { nextProposalId } = await this.fetchCustodian(); - proposalId = nextProposalId; - } - - return Proposal.address(this.ID, proposalId); - } - async fetchProposal(input?: { address: PublicKey }): Promise { const addr = input === undefined ? await this.proposalAddress() : input.address; - // @ts-ignore This is BS. This is correct. return this.program.account.proposal.fetch(addr); } - coreMessageAddress(auction: PublicKey): PublicKey { - return coreMessageAddress(this.ID, auction); - } - - cctpMessageAddress(auction: PublicKey): PublicKey { - return cctpMessageAddress(this.ID, auction); - } - async reclaimCctpMessageIx( accounts: { payer: PublicKey; @@ -440,10 +274,6 @@ export class MatchingEngineProgram { return reclaimCctpMessageIx(this.messageTransmitterProgram(), accounts, cctpAttestation); } - preparedOrderResponseAddress(fastVaaHash: VaaHash): PublicKey { - return PreparedOrderResponse.address(this.ID, fastVaaHash); - } - async fetchPreparedOrderResponse( input: VaaHash | { address: PublicKey }, ): Promise { @@ -451,20 +281,6 @@ export class MatchingEngineProgram { return this.program.account.preparedOrderResponse.fetch(addr); } - preparedCustodyTokenAddress(preparedOrderResponse: PublicKey): PublicKey { - return PublicKey.findProgramAddressSync( - [Buffer.from("prepared-custody"), preparedOrderResponse.toBuffer()], - this.ID, - )[0]; - } - - auctionCustodyTokenAddress(auction: PublicKey): PublicKey { - return PublicKey.findProgramAddressSync( - [Buffer.from("auction-custody"), auction.toBuffer()], - this.ID, - )[0]; - } - async fetchAuctionCustodyTokenBalance(auction: PublicKey): Promise { return splToken .getAccount(this.program.provider.connection, this.auctionCustodyTokenAddress(auction)) @@ -472,16 +288,6 @@ export class MatchingEngineProgram { .catch((_) => 0n); } - localCustodyTokenAddress(sourceChain: ChainId): PublicKey { - const encodedSourceChain = Buffer.alloc(2); - encodedSourceChain.writeUInt16BE(sourceChain); - - return PublicKey.findProgramAddressSync( - [Buffer.from("local-custody"), encodedSourceChain], - this.ID, - )[0]; - } - async fetchLocalCustodyTokenBalance(sourceChain: ChainId): Promise { return splToken .getAccount( @@ -492,10 +298,6 @@ export class MatchingEngineProgram { .catch((_) => 0n); } - fastFillAddress(sourceChain: ChainId, orderSender: Array, sequence: Uint64): PublicKey { - return FastFill.address(this.ID, sourceChain, orderSender, sequence); - } - fetchFastFill( input: [ChainId, Array, Uint64] | { address: PublicKey }, ): Promise { @@ -503,10 +305,6 @@ export class MatchingEngineProgram { return this.program.account.fastFill.fetch(addr); } - fastFillSequencerAddress(sourceChain: ChainId, sender: Array): PublicKey { - return FastFillSequencer.address(this.ID, sourceChain, sender); - } - fetchFastFillSequencer( input: [ChainId, Array] | { address: PublicKey }, ): Promise { @@ -514,10 +312,6 @@ export class MatchingEngineProgram { return this.program.account.fastFillSequencer.fetch(addr); } - reservedFastFillSequenceAddress(fastVaaHash: VaaHash): PublicKey { - return ReservedFastFillSequence.address(this.ID, fastVaaHash); - } - fetchReservedFastFillSequence( input: VaaHash | { address: PublicKey }, ): Promise { @@ -526,19 +320,6 @@ export class MatchingEngineProgram { return this.program.account.reservedFastFillSequence.fetch(addr); } - transferAuthorityAddress(auction: PublicKey, offerPrice: Uint64): PublicKey { - const encodedOfferPrice = Buffer.alloc(8); - writeUint64BE(encodedOfferPrice, offerPrice); - return PublicKey.findProgramAddressSync( - [Buffer.from("transfer-authority"), auction.toBuffer(), encodedOfferPrice], - this.ID, - )[0]; - } - - auctionHistoryAddress(id: Uint64): PublicKey { - return AuctionHistory.address(this.ID, id); - } - // Anchor is having trouble deserializing the account data here. Manually deserializing // the auction history is a workaround, and necessary after changing the redeemer message // length from a u32 to a u16. @@ -764,21 +545,15 @@ export class MatchingEngineProgram { } routerEndpointComposite(addr: PublicKey): { endpoint: PublicKey } { - return { - endpoint: addr, - }; + return { endpoint: addr }; } liquidityLayerVaaComposite(vaa: PublicKey): { vaa: PublicKey } { - return { - vaa, - }; + return { vaa }; } usdcComposite(mint?: PublicKey): { mint: PublicKey } { - return { - mint: mint ?? this.mint, - }; + return { mint: mint ?? this.mint }; } localTokenRouterComposite(tokenRouterProgram: PublicKey): { @@ -933,9 +708,7 @@ export class MatchingEngineProgram { const { ownerOrAssistant, custodian } = accounts; return this.program.methods .setPause(paused) - .accounts({ - admin: this.adminMutComposite(ownerOrAssistant, custodian), - }) + .accounts({ admin: this.adminMutComposite(ownerOrAssistant, custodian) }) .instruction(); } @@ -1134,7 +907,7 @@ export class MatchingEngineProgram { proposal ??= await this.proposalAddress(opts.proposalId); if (auctionConfig === undefined) { - const { auctionConfigId } = await this.fetchCustodian(); + const { auctionConfigId } = await this.getCustodian(true); // Add 1 to the current auction config ID to get the next one. auctionConfig = this.auctionConfigAddress(auctionConfigId + 1); } @@ -1290,8 +1063,8 @@ export class MatchingEngineProgram { async placeInitialOfferCctpIx( accounts: { payer: PublicKey; - feePayer?: PublicKey; fastVaa: PublicKey; + feePayer?: PublicKey; offerToken?: PublicKey; auction?: PublicKey; auctionConfig?: PublicKey; @@ -1306,10 +1079,9 @@ export class MatchingEngineProgram { [approveIx: TransactionInstruction, placeInitialOfferCctpIx: TransactionInstruction] > { const { payer, feePayer, fastVaa } = accounts; + let { auction, auctionConfig, offerToken, fromRouterEndpoint, toRouterEndpoint } = accounts; const { offerPrice } = args; - - let { auction, auctionConfig, offerToken, fromRouterEndpoint, toRouterEndpoint } = accounts; let { totalDeposit } = args; offerToken ??= await splToken.getAssociatedTokenAddress(this.mint, payer); @@ -1329,7 +1101,7 @@ export class MatchingEngineProgram { } toRouterEndpoint ??= this.routerEndpointAddress(toChainId(fastMarketOrder.targetChain)); - const custodianData = await this.fetchCustodian(); + const custodianData = await this.getCustodian(); fetchedConfigId = custodianData.auctionConfigId; const notionalDeposit = await this.computeNotionalSecurityDeposit( @@ -1342,7 +1114,7 @@ export class MatchingEngineProgram { if (auctionConfig === undefined) { if (fetchedConfigId === null) { - const custodianData = await this.fetchCustodian(); + const custodianData = await this.getCustodian(); fetchedConfigId = custodianData.auctionConfigId; } auctionConfig = this.auctionConfigAddress(fetchedConfigId); @@ -1740,7 +1512,7 @@ export class MatchingEngineProgram { sequence ??= fastFillSeeds.sequence; } - const { feeRecipientToken } = await this.fetchCustodian(); + const { feeRecipientToken } = await this.getCustodian(); return this.program.methods .settleAuctionNoneLocal() @@ -1802,7 +1574,6 @@ export class MatchingEngineProgram { } const { custodian, - routerEndpoint: toRouterEndpoint, coreMessage, cctpMessage, coreBridgeConfig, @@ -1820,7 +1591,7 @@ export class MatchingEngineProgram { tokenMessengerMinterProgram, } = await this.burnAndPublishAccounts(auction, { targetChain }); - const { feeRecipientToken } = await this.fetchCustodian(); + const { feeRecipientToken } = await this.getCustodian(); return this.program.methods .settleAuctionNoneCctp() @@ -1949,9 +1720,7 @@ export class MatchingEngineProgram { initialOfferToken?: PublicKey; initialParticipant?: PublicKey; }, - opts: { - targetChain?: ChainId; - } = {}, + opts: { targetChain?: ChainId } = {}, ) { const connection = this.program.provider.connection; @@ -1971,10 +1740,8 @@ export class MatchingEngineProgram { if (targetChain === undefined) { fastVaaAccount ??= await VaaAccount.fetch(connection, fastVaa); - const { fastMarketOrder } = LiquidityLayerMessage.decode(fastVaaAccount.payload()); - if (fastMarketOrder === undefined) { - throw new Error("Message not FastMarketOrder"); - } + const { payload: fastMarketOrder } = fastVaaAccount.vaa("FastTransfer:FastMarketOrder"); + targetChain ??= toChainId(fastMarketOrder.targetChain); } @@ -2374,7 +2141,7 @@ export class MatchingEngineProgram { async publishMessageAccounts(auction: PublicKey): Promise { const coreMessage = this.coreMessageAddress(auction); - const coreBridgeProgram = this.coreBridgeProgramId(); + const coreBridgeProgram = this.coreBridgeProgramId; const custodian = this.custodianAddress(); return { @@ -2461,53 +2228,26 @@ export class MatchingEngineProgram { } upgradeManagerProgram(): UpgradeManagerProgram { - switch (this._programId) { - case testnet(): { - return new UpgradeManagerProgram( - this.program.provider.connection, - "ucdP9ktgrXgEUnn6roqD2SfdGMR2JSiWHUKv23oXwxt", - ); - } - case localnet(): { - return new UpgradeManagerProgram( - this.program.provider.connection, - "UpgradeManager11111111111111111111111111111", - ); - } - default: { - throw new Error("unsupported network"); - } - } + return new UpgradeManagerProgram(this.program.provider.connection, { + ...this._addresses, + tokenRouter: this._addresses.tokenRouter!, + }); } tokenMessengerMinterProgram(): TokenMessengerMinterProgram { return new TokenMessengerMinterProgram( this.program.provider.connection, - "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3", + this._addresses.cctp, ); } messageTransmitterProgram(): MessageTransmitterProgram { return new MessageTransmitterProgram( this.program.provider.connection, - "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd", + this._addresses.cctp, ); } - coreBridgeProgramId(): PublicKey { - switch (this._programId) { - case testnet(): { - return new PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"); - } - case localnet(): { - return new PublicKey("worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"); - } - default: { - throw new Error("unsupported network"); - } - } - } - async computeDepositPenalty( auctionInfo: AuctionInfo, currentSlot: Uint64, @@ -2558,12 +2298,44 @@ export class MatchingEngineProgram { (uint64ToBigInt(amountIn) * BigInt(securityDepositBps)) / FEE_PRECISION_MAX ); } -} -export function testnet(): ProgramId { - return "mPydpGUWxzERTNpyvTKdvS7v8kvw5sgwfiP8WQFrXVS"; -} + async proposalAddress(proposalId?: Uint64): Promise { + if (proposalId === undefined) { + // Intentionally skip cache to get a fresh proposal ID. + ({ nextProposalId: proposalId } = await this.getCustodian(true)); + } -export function localnet(): ProgramId { - return "MatchingEngine11111111111111111111111111111"; + return this.pdas.proposal(proposalId); + } + + // TODO: we should be able to eliminate these fns, replacing with the call to `.pdas` directly + auctionConfigAddress = (id: number): PublicKey => this.pdas.auctionConfig(id); + cctpMintRecipientAddress = (): PublicKey => + this.pdas.cctpMintRecipient(this.custodianAddress()); + routerEndpointAddress = (chain: ChainId): PublicKey => this.pdas.routerEndpoint(chain); + eventAuthorityAddress = (): PublicKey => this.pdas.eventAuthority(); + auctionAddress = (vaaHash: VaaHash): PublicKey => this.pdas.auction(vaaHash); + custodianAddress = (): PublicKey => this.pdas.custodian(); + fastFillAddress = ( + sourceChain: ChainId, + orderSender: Array, + sequence: Uint64, + ): PublicKey => this.pdas.fastFill(sourceChain, orderSender, sequence); + coreMessageAddress = (auction: PublicKey): PublicKey => this.pdas.coreMessage(auction); + cctpMessageAddress = (auction: PublicKey): PublicKey => this.pdas.cctpMessage(auction); + preparedOrderResponseAddress = (fastVaaHash: VaaHash): PublicKey => + this.pdas.preparedOrderResponse(fastVaaHash); + preparedCustodyTokenAddress = (preparedOrderResponse: PublicKey): PublicKey => + this.pdas.preparedCustodyToken(preparedOrderResponse); + auctionCustodyTokenAddress = (auction: PublicKey): PublicKey => + this.pdas.auctionCustodyToken(auction); + localCustodyTokenAddress = (sourceChain: ChainId): PublicKey => + this.pdas.localCustodyToken(sourceChain); + fastFillSequencerAddress = (sourceChain: ChainId, sender: Array): PublicKey => + this.pdas.fastFillSequencer(sourceChain, sender); + reservedFastFillSequenceAddress = (fastVaaHash: VaaHash): PublicKey => + this.pdas.reservedFastFillSequence(fastVaaHash); + transferAuthorityAddress = (auction: PublicKey, offerPrice: Uint64): PublicKey => + this.pdas.transferAuthority(auction, offerPrice); + auctionHistoryAddress = (id: Uint64): PublicKey => this.pdas.auctionHistory(id); } diff --git a/solana/ts/src/matchingEngine/pdas.ts b/solana/ts/src/matchingEngine/pdas.ts new file mode 100644 index 00000000..c8f4e813 --- /dev/null +++ b/solana/ts/src/matchingEngine/pdas.ts @@ -0,0 +1,73 @@ +import * as splToken from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; +import { ChainId } from "@wormhole-foundation/sdk-base"; +import { utils as coreUtils } from "@wormhole-foundation/sdk-solana-core"; +import { Uint64, VaaHash, cctpMessageAddress, coreMessageAddress, writeUint64BE } from "../common"; +import { + Auction, + AuctionConfig, + AuctionHistory, + Custodian, + FastFill, + FastFillSequencer, + PreparedOrderResponse, + Proposal, + ReservedFastFillSequence, + RouterEndpoint, +} from "./state"; +import { VAA, keccak256 } from "@wormhole-foundation/sdk-definitions"; + +export function programDerivedAddresses(ID: PublicKey, mint: PublicKey, coreId: PublicKey) { + return { + auctionConfig: (id: number) => AuctionConfig.address(ID, id), + auction: (vaaHash: VaaHash) => Auction.address(ID, vaaHash), + coreMessage: (auction: PublicKey) => coreMessageAddress(ID, auction), + cctpMessage: (auction: PublicKey) => cctpMessageAddress(ID, auction), + preparedOrderResponse: (fastVaaHash: VaaHash) => + PreparedOrderResponse.address(ID, fastVaaHash), + eventAuthority: () => + PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], ID)[0], + custodian: () => Custodian.address(ID), + cctpMintRecipient: (custodian: PublicKey) => + splToken.getAssociatedTokenAddressSync(mint, custodian, true), + routerEndpoint: (chain: ChainId) => RouterEndpoint.address(ID, chain), + preparedCustodyToken: (preparedOrderResponse: PublicKey) => + PublicKey.findProgramAddressSync( + [Buffer.from("prepared-custody"), preparedOrderResponse.toBuffer()], + ID, + )[0], + auctionCustodyToken: (auction: PublicKey) => + PublicKey.findProgramAddressSync( + [Buffer.from("auction-custody"), auction.toBuffer()], + ID, + )[0], + localCustodyToken: (sourceChain: ChainId) => { + const encodedSourceChain = Buffer.alloc(2); + encodedSourceChain.writeUInt16BE(sourceChain); + + return PublicKey.findProgramAddressSync( + [Buffer.from("local-custody"), encodedSourceChain], + ID, + )[0]; + }, + proposal: (proposalId: Uint64) => Proposal.address(ID, proposalId), + fastFill: (sourceChain: ChainId, orderSender: Array, sequence: Uint64) => + FastFill.address(ID, sourceChain, orderSender, sequence), + fastFillSequencer: (sourceChain: ChainId, sender: Array) => + FastFillSequencer.address(ID, sourceChain, sender), + reservedFastFillSequence: (fastVaaHash: VaaHash) => + ReservedFastFillSequence.address(ID, fastVaaHash), + transferAuthority: (auction: PublicKey, offerPrice: Uint64) => { + const encodedOfferPrice = Buffer.alloc(8); + writeUint64BE(encodedOfferPrice, offerPrice); + return PublicKey.findProgramAddressSync( + [Buffer.from("transfer-authority"), auction.toBuffer(), encodedOfferPrice], + ID, + )[0]; + }, + auctionHistory: (id: Uint64) => AuctionHistory.address(ID, id), + + // + postedVaa: (vaa: VAA) => coreUtils.derivePostedVaaKey(coreId, Buffer.from(vaa.hash)), + }; +} diff --git a/solana/ts/src/matchingEngine/types.ts b/solana/ts/src/matchingEngine/types.ts new file mode 100644 index 00000000..f9353cbc --- /dev/null +++ b/solana/ts/src/matchingEngine/types.ts @@ -0,0 +1,154 @@ +import { BN } from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; + +import { ChainId } from "@wormhole-foundation/sdk-base"; +import { VaaHash } from "../common"; +import { FastFillInfo, FastFillSeeds, MessageProtocol, ProposalAction } from "./state"; + +export type AddCctpRouterEndpointArgs = { + chain: ChainId; + cctpDomain: number; + address: Array; + mintRecipient: Array | null; +}; + +export type WormholeCoreBridgeAccounts = { + coreBridgeConfig: PublicKey; + coreEmitterSequence: PublicKey; + coreFeeCollector: PublicKey; + coreBridgeProgram: PublicKey; +}; + +export type PublishMessageAccounts = WormholeCoreBridgeAccounts & { + custodian: PublicKey; + coreMessage: PublicKey; +}; + +export type MatchingEngineCommonAccounts = WormholeCoreBridgeAccounts & { + matchingEngineProgram: PublicKey; + systemProgram: PublicKey; + rent: PublicKey; + clock: PublicKey; + custodian: PublicKey; + cctpMintRecipient: PublicKey; + tokenMessenger: PublicKey; + tokenMinter: PublicKey; + tokenMessengerMinterSenderAuthority: PublicKey; + tokenMessengerMinterProgram: PublicKey; + messageTransmitterAuthority: PublicKey; + messageTransmitterConfig: PublicKey; + messageTransmitterProgram: PublicKey; + tokenProgram: PublicKey; + mint: PublicKey; + localToken: PublicKey; + tokenMessengerMinterCustodyToken: PublicKey; +}; + +export type BurnAndPublishAccounts = { + custodian: PublicKey; + routerEndpoint: PublicKey; + coreMessage: PublicKey; + cctpMessage: PublicKey; + coreBridgeConfig: PublicKey; + coreEmitterSequence: PublicKey; + coreFeeCollector: PublicKey; + coreBridgeProgram: PublicKey; + tokenMessengerMinterSenderAuthority: PublicKey; + messageTransmitterConfig: PublicKey; + tokenMessenger: PublicKey; + remoteTokenMessenger: PublicKey; + tokenMinter: PublicKey; + localToken: PublicKey; + tokenMessengerMinterEventAuthority: PublicKey; + messageTransmitterProgram: PublicKey; + tokenMessengerMinterProgram: PublicKey; +}; + +export type RedeemFastFillAccounts = { + custodian: PublicKey; + fromRouterEndpoint: PublicKey; + toRouterEndpoint: PublicKey; + localCustodyToken: PublicKey; + matchingEngineProgram: PublicKey; +}; + +export type CctpMessageArgs = { + encodedCctpMessage: Buffer; + cctpAttestation: Buffer; +}; + +export type SettledTokenAccountInfo = { + key: PublicKey; + balanceAfter: BN; +}; + +export type AuctionSettled = { + auction: PublicKey; + bestOfferToken: SettledTokenAccountInfo | null; + executorToken: SettledTokenAccountInfo | null; + withExecute: MessageProtocol | null; +}; + +export type AuctionUpdated = { + configId: number; + auction: PublicKey; + vaa: PublicKey | null; + sourceChain: number; + targetProtocol: MessageProtocol; + redeemerMessageLen: number; + endSlot: BN; + bestOfferToken: PublicKey; + tokenBalanceBefore: BN; + amountIn: BN; + totalDeposit: BN; + maxOfferPriceAllowed: BN | null; +}; + +export type OrderExecuted = { + auction: PublicKey; + vaa: PublicKey; + targetProtocol: MessageProtocol; +}; + +export type Proposed = { + action: ProposalAction; +}; + +export type Enacted = { + action: ProposalAction; +}; + +export type LocalFastOrderFilled = { + seeds: FastFillSeeds; + info: FastFillInfo; + auction: PublicKey | null; +}; + +export type FastFillSequenceReserved = { + fastVaaHash: Array; + fastFillSeeds: FastFillSeeds; +}; + +export type FastFillRedeemed = { + preparedBy: PublicKey; + fastFill: PublicKey; +}; + +export type FastOrderPathComposite = { + fastVaa: { + vaa: PublicKey; + }; + path: { + fromEndpoint: { + endpoint: PublicKey; + }; + toEndpoint: { endpoint: PublicKey }; + }; +}; + +export type ReserveFastFillSequenceCompositeOpts = { + fastVaaHash?: VaaHash; + sourceChain?: ChainId; + orderSender?: Array; + targetChain?: ChainId; +}; diff --git a/solana/ts/src/protocol/index.ts b/solana/ts/src/protocol/index.ts new file mode 100644 index 00000000..ee960c61 --- /dev/null +++ b/solana/ts/src/protocol/index.ts @@ -0,0 +1,2 @@ +export * from "./matchingEngine"; +export * from "./tokenRouter"; diff --git a/solana/ts/src/protocol/matchingEngine.ts b/solana/ts/src/protocol/matchingEngine.ts new file mode 100644 index 00000000..908c45fc --- /dev/null +++ b/solana/ts/src/protocol/matchingEngine.ts @@ -0,0 +1,399 @@ +import { + AddressLookupTableAccount, + ComputeBudgetProgram, + Connection, + PublicKey, + TransactionInstruction, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { + FastTransfer, + MatchingEngine, +} from "@wormhole-foundation/example-liquidity-layer-definitions"; +import { Chain, Network, Platform, toChainId } from "@wormhole-foundation/sdk-base"; +import { + AccountAddress, + ChainsConfig, + CircleAttestation, + CircleBridge, + Contracts, + VAA, +} from "@wormhole-foundation/sdk-definitions"; +import { + AnySolanaAddress, + SolanaAddress, + SolanaChains, + SolanaPlatform, + SolanaTransaction, + SolanaUnsignedTransaction, +} from "@wormhole-foundation/sdk-solana"; +import { vaaHash } from "../common"; +import { AuctionParameters, MatchingEngineProgram } from "../matchingEngine"; +import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core"; + +export class SolanaMatchingEngine + extends MatchingEngineProgram + implements MatchingEngine +{ + coreBridge: SolanaWormholeCore; + + constructor( + readonly _network: N, + readonly _chain: C, + readonly _connection: Connection, + readonly _contracts: Contracts & MatchingEngine.Addresses, + ) { + super(_connection, _contracts); + + this.coreBridge = new SolanaWormholeCore(_network, _chain, _connection, { + ...this._contracts, + }); + } + + static async fromRpc( + connection: Connection, + config: ChainsConfig, + contracts: MatchingEngine.Addresses, + ) { + const [network, chain] = await SolanaPlatform.chainFromRpc(connection); + const conf = config[chain]!; + if (conf.network !== network) + throw new Error(`Network mismatch for chain ${chain}: ${conf.network} != ${network}`); + + return new SolanaMatchingEngine(network as N, chain, connection, { + ...config[chain]!.contracts, + ...contracts, + }); + } + + async *initialize( + owner: AnySolanaAddress, + ownerAssistant: AnySolanaAddress, + feeRecipient: AnySolanaAddress, + params: AuctionParameters, + mint?: AnySolanaAddress, + ) { + const ix = await this.initializeIx( + { + owner: new SolanaAddress(owner).unwrap(), + ownerAssistant: new SolanaAddress(ownerAssistant).unwrap(), + feeRecipient: new SolanaAddress(feeRecipient).unwrap(), + mint: mint ? new SolanaAddress(mint).unwrap() : undefined, + }, + params, + ); + const transaction = this.createTx(new SolanaAddress(owner).unwrap(), [ix]); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.initialize"); + } + + async *setPause(sender: AnySolanaAddress, pause: boolean) { + const payer = new SolanaAddress(sender).unwrap(); + const ix = await this.setPauseIx({ ownerOrAssistant: payer }, pause); + const transaction = this.createTx(payer, [ix]); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.setPause"); + } + + async *registerRouter( + sender: AnySolanaAddress, + chain: RC, + cctpDomain: number, + router: AccountAddress, + tokenAccount?: AnySolanaAddress, + ) { + const ownerOrAssistant = new SolanaAddress(sender).unwrap(); + const mintRecipient = tokenAccount + ? Array.from(new SolanaAddress(tokenAccount).toUniversalAddress().toUint8Array()) + : null; + const address = Array.from(router.toUniversalAddress().toUint8Array()); + + const ix = await this.addCctpRouterEndpointIx( + { ownerOrAssistant }, + { chain: toChainId(chain), cctpDomain, address, mintRecipient }, + ); + + const transaction = this.createTx(ownerOrAssistant, [ix]); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.registerRouter"); + } + + async *updateRouter( + sender: AnySolanaAddress, + chain: RC, + cctpDomain: number, + router: AccountAddress, + tokenAccount?: AnySolanaAddress, + ) { + const owner = new SolanaAddress(sender).unwrap(); + const mintRecipient = tokenAccount + ? Array.from(new SolanaAddress(tokenAccount).toUniversalAddress().toUint8Array()) + : null; + const address = Array.from(router.toUniversalAddress().toUint8Array()); + const ix = await this.updateCctpRouterEndpointIx( + { owner }, + { chain: toChainId(chain), cctpDomain, address, mintRecipient }, + ); + + const transaction = this.createTx(owner, [ix]); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.updateRouter"); + } + + async *disableRouter(sender: AnySolanaAddress, chain: RC) { + const owner = new SolanaAddress(sender).unwrap(); + + const ix = await this.disableRouterEndpointIx({ owner }, toChainId(chain)); + + const transaction = this.createTx(owner, [ix]); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.disableRouter"); + } + + async *setConfiguration(config: { + enabled: boolean; + maxAmount: bigint; + baseFee: bigint; + initAuctionFee: bigint; + }) { + throw new Error("Method not implemented."); + } + + async *postVaa(sender: AnySolanaAddress, vaa: FastTransfer.VAA) { + yield* this.coreBridge.postVaa(sender, vaa); + } + + async *placeInitialOffer( + sender: AnySolanaAddress, + vaa: VAA<"FastTransfer:FastMarketOrder">, + offerPrice: bigint, + totalDeposit?: bigint, + ) { + // If the VAA has not yet been posted, do so now + yield* this.postVaa(sender, vaa); + + const payer = new SolanaAddress(sender).unwrap(); + const vaaAddress = this.pdas.postedVaa(vaa); + + const ixs = await this.placeInitialOfferCctpIx( + { payer, fastVaa: vaaAddress }, + { offerPrice, totalDeposit }, + ); + + const transaction = this.createTx(payer, ixs); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.placeInitialOffer"); + } + + async *improveOffer( + sender: AnySolanaAddress, + vaa: VAA<"FastTransfer:FastMarketOrder">, + offer: bigint, + ) { + const participant = new SolanaAddress(sender).unwrap(); + + const digest = vaaHash(vaa); + const auction = this.pdas.auction(digest); + + const ixs = await this.improveOfferIx({ participant, auction }, { offerPrice: offer }); + + const transaction = this.createTx(participant, ixs); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.improveOffer"); + } + + async *reserveFastFillSequence() { + throw new Error("Method not implemented."); + } + + async *executeFastOrder( + sender: AnySolanaAddress, + vaa: VAA<"FastTransfer:FastMarketOrder">, + participant?: AnySolanaAddress, + ) { + const payer = new SolanaAddress(sender).unwrap(); + + const initialParticipant = participant + ? new SolanaAddress(participant).unwrap() + : undefined; + + const fastVaa = this.pdas.postedVaa(vaa); + + const digest = vaaHash(vaa); + const auction = this.pdas.auction(digest); + const reservedSequence = this.pdas.reservedFastFillSequence(digest); + + // TODO: make sure this has already been done, or do it here + // yield* this.reserveFastFillSequence(); + + const { targetChain } = vaa.payload; + + const ix = + targetChain === "Solana" + ? await this.executeFastOrderLocalIx({ + payer, + fastVaa, + auction, + reservedSequence, + initialParticipant, + }) + : await this.executeFastOrderCctpIx( + { + payer, + fastVaa, + auction, + initialParticipant, + }, + { targetChain: toChainId(targetChain) }, + ); + + const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 300_000, + }); + + const transaction = this.createTx(payer, [ix, computeIx]); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.executeFastOrder"); + } + + private async _prepareOrderResponseIx( + sender: AnySolanaAddress, + fast: VAA<"FastTransfer:FastMarketOrder">, + finalized: VAA<"FastTransfer:CctpDeposit">, + cctp: { + message: CircleBridge.Message; + attestation: CircleAttestation; + }, + ) { + const payer = new SolanaAddress(sender).unwrap(); + + const fastVaa = this.pdas.postedVaa(fast); + const finalizedVaa = this.pdas.postedVaa(finalized); + + const digest = vaaHash(fast); + const preparedAddress = this.pdas.preparedOrderResponse(digest); + + try { + // Check if its already been prepared + await this.fetchPreparedOrderResponse({ address: preparedAddress }); + return; + } catch {} + + const ix = await this.prepareOrderResponseCctpIx( + { payer, fastVaa, finalizedVaa }, + { + encodedCctpMessage: Buffer.from(CircleBridge.serialize(cctp.message)), + cctpAttestation: Buffer.from(cctp.attestation, "hex"), + }, + ); + + return ix; + } + + async *prepareOrderResponse( + sender: AnySolanaAddress, + fast: VAA<"FastTransfer:FastMarketOrder">, + finalized: VAA<"FastTransfer:CctpDeposit">, + cctp: { + message: CircleBridge.Message; + attestation: CircleAttestation; + }, + lookupTables?: AddressLookupTableAccount[], + ) { + const payer = new SolanaAddress(sender).unwrap(); + const ix = await this._prepareOrderResponseIx(sender, fast, finalized, cctp); + if (ix === undefined) return; + + const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }); + + const transaction = this.createTx(payer, [ix, computeIx], lookupTables); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.prepareOrderResponse"); + } + + async *settleOrder( + sender: AnySolanaAddress, + fast: VAA<"FastTransfer:FastMarketOrder">, + finalized?: VAA<"FastTransfer:CctpDeposit">, + cctp?: { + message: CircleBridge.Message; + attestation: CircleAttestation; + }, + lookupTables?: AddressLookupTableAccount[], + ) { + const payer = new SolanaAddress(sender).unwrap(); + + // If the finalized VAA and CCTP message/attestation are passed + // we may try to prepare the order response + // this yields its own transaction + const ixs = []; + if (finalized && cctp) { + // TODO: how do we decide? + const combine = true; + // try to include the prepare order instruction in the same transaction + if (combine) { + const ix = await this._prepareOrderResponseIx(sender, fast, finalized, cctp); + if (ix !== undefined) { + ixs.push(ix, ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 })); + } + } else { + yield* this.prepareOrderResponse(sender, fast, finalized, cctp, lookupTables); + } + } + + const fastVaa = this.pdas.postedVaa(fast); + + const digest = vaaHash(fast); + const preparedOrderResponse = this.pdas.preparedOrderResponse(digest); + const auction = this.pdas.auction(digest); + + const settleIx = await (async () => { + if (finalized && !cctp) { + if (fast.payload.targetChain === "Solana") { + const reservedSequence = this.pdas.reservedFastFillSequence(digest); + return await this.settleAuctionNoneLocalIx({ + payer, + reservedSequence, + preparedOrderResponse, + auction, + }); + } else { + return await this.settleAuctionNoneCctpIx( + { payer, fastVaa, preparedOrderResponse }, + { targetChain: toChainId(fast.payload.targetChain) }, + ); + } + } else { + return await this.settleAuctionCompleteIx({ + executor: payer, + preparedOrderResponse, + auction, + }); + } + })(); + + ixs.push(settleIx); + + const transaction = this.createTx(payer, ixs, lookupTables); + yield this.createUnsignedTx({ transaction }, "MatchingEngine.settleAuctionComplete"); + } + + private createTx( + payerKey: PublicKey, + instructions: TransactionInstruction[], + lookupTables?: AddressLookupTableAccount[], + ): VersionedTransaction { + const messageV0 = new TransactionMessage({ + payerKey, + recentBlockhash: "", + instructions, + }).compileToV0Message(lookupTables); + return new VersionedTransaction(messageV0); + } + + private createUnsignedTx( + txReq: SolanaTransaction, + description: string, + parallelizable: boolean = false, + ): SolanaUnsignedTransaction { + return new SolanaUnsignedTransaction( + txReq, + this._network, + this._chain, + description, + parallelizable, + ); + } +} diff --git a/solana/ts/src/protocol/tokenRouter.ts b/solana/ts/src/protocol/tokenRouter.ts new file mode 100644 index 00000000..6acdcff2 --- /dev/null +++ b/solana/ts/src/protocol/tokenRouter.ts @@ -0,0 +1,266 @@ +import * as splToken from "@solana/spl-token"; +import { + AddressLookupTableAccount, + ComputeBudgetProgram, + Connection, + Keypair, + PublicKey, + TransactionInstruction, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { Payload, TokenRouter } from "@wormhole-foundation/example-liquidity-layer-definitions"; +import { ChainId, Network, Platform, toChainId } from "@wormhole-foundation/sdk-base"; +import { + ChainsConfig, + CircleBridge, + Contracts, + UnsignedTransaction, + VAA, +} from "@wormhole-foundation/sdk-definitions"; +import { + AnySolanaAddress, + SolanaAddress, + SolanaChains, + SolanaPlatform, + SolanaTransaction, + SolanaUnsignedTransaction, +} from "@wormhole-foundation/sdk-solana"; +import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core"; +import { TokenRouterProgram } from "../tokenRouter"; +import { SolanaMatchingEngine } from "./matchingEngine"; + +export class SolanaTokenRouter + extends TokenRouterProgram + implements TokenRouter +{ + coreBridge: SolanaWormholeCore; + matchingEngine: SolanaMatchingEngine; + + constructor( + readonly _network: N, + readonly _chain: C, + readonly _connection: Connection, + readonly _contracts: Contracts & TokenRouter.Addresses, + ) { + super(_connection, _contracts); + + this.coreBridge = new SolanaWormholeCore(_network, _chain, _connection, _contracts); + this.matchingEngine = new SolanaMatchingEngine(_network, _chain, _connection, _contracts); + } + + static async fromRpc( + connection: Connection, + config: ChainsConfig, + contracts: TokenRouter.Addresses, + ) { + const [network, chain] = await SolanaPlatform.chainFromRpc(connection); + const conf = config[chain]!; + if (conf.network !== network) + throw new Error(`Network mismatch for chain ${chain}: ${conf.network} != ${network}`); + + return new SolanaTokenRouter(network as N, chain, connection, { + ...config[chain]!.contracts, + ...contracts, + }); + } + + private usdcMint(wallet: PublicKey) { + return splToken.getAssociatedTokenAddressSync(this.mint, wallet); + } + + async *initialize( + owner: AnySolanaAddress, + ownerAssistant: AnySolanaAddress, + mint?: AnySolanaAddress, + ) { + const sender = new SolanaAddress(owner).unwrap(); + const ix = await this.initializeIx({ + owner: sender, + ownerAssistant: new SolanaAddress(ownerAssistant).unwrap(), + mint: mint ? new SolanaAddress(mint).unwrap() : this.mint, + }); + + const transaction = this.createTx(sender, [ix]); + yield this.createUnsignedTx({ transaction }, "TokenRouter.Initialize"); + } + + private async _prepareMarketOrderIxs( + sender: AnySolanaAddress, + order: TokenRouter.OrderRequest, + prepareTo?: Keypair, + ): Promise<[TransactionInstruction[], Keypair[]]> { + const payer = new SolanaAddress(sender).unwrap(); + + // TODO: assumes sender token is the usdc mint address + const senderToken = this.usdcMint(payer); + + // Where we'll write the prepared order + prepareTo = prepareTo ?? Keypair.generate(); + + const [approveIx, prepareIx] = await this.prepareMarketOrderIx( + { + payer, + senderToken, + preparedOrder: prepareTo.publicKey, + }, + { + amountIn: order.amountIn, + minAmountOut: order.minAmountOut !== undefined ? order.minAmountOut : null, + targetChain: toChainId(order.targetChain), + redeemer: Array.from(order.redeemer.toUint8Array()), + redeemerMessage: order.redeemerMessage + ? Buffer.from(order.redeemerMessage) + : Buffer.from(""), + }, + ); + + // TODO: fix prepareMarketOrderIx to not return null at all? + const ixs = []; + if (approveIx) ixs.push(approveIx); + ixs.push(prepareIx); + + return [ixs, [prepareTo]]; + } + + async *prepareMarketOrder( + sender: AnySolanaAddress, + order: TokenRouter.OrderRequest, + prepareTo?: Keypair, + ) { + const payer = new SolanaAddress(sender).unwrap(); + + const [ixs, signers] = await this._prepareMarketOrderIxs(sender, order, prepareTo); + + const transaction = this.createTx(payer, ixs); + yield this.createUnsignedTx({ transaction, signers }, "TokenRouter.PrepareMarketOrder"); + } + + async *closePreparedOrder(sender: AnySolanaAddress, order: AnySolanaAddress) { + const payer = new SolanaAddress(sender).unwrap(); + const preparedOrder = new SolanaAddress(order).unwrap(); + + const ix = await this.closePreparedOrderIx({ + preparedOrder, + preparedBy: payer, + orderSender: payer, + }); + + const transaction = this.createTx(payer, [ix]); + + yield this.createUnsignedTx({ transaction }, "TokenRouter.ClosePreparedOrder"); + } + + async *placeMarketOrder( + sender: AnySolanaAddress, + order: TokenRouter.OrderRequest | AnySolanaAddress, + prepareTo?: Keypair, + ): AsyncGenerator, any, unknown> { + const payer = new SolanaAddress(sender).unwrap(); + + let ixs: TransactionInstruction[] = []; + let signers: Keypair[] = []; + let preparedOrder: PublicKey; + let targetChain: ChainId | undefined; + + if (TokenRouter.isOrderRequest(order)) { + prepareTo = prepareTo ?? Keypair.generate(); + + const combined = false; // TODO how to choose? + if (combined) { + const [ixs, signers] = await this._prepareMarketOrderIxs(sender, order, prepareTo); + ixs.push(...ixs); + signers.push(...signers); + } else { + yield* this.prepareMarketOrder(sender, order, prepareTo); + } + + preparedOrder = prepareTo.publicKey; + targetChain = toChainId(order.targetChain); + } else { + preparedOrder = new SolanaAddress(order).unwrap(); + } + + // TODO: devnet not happy with this + // const destinationDomain = targetChain ? circle.toCircleChainId(this._network, toChain(targetChain)) : undefined; + + const destinationDomain = undefined; + + const ix = await this.placeMarketOrderCctpIx( + { + payer, + preparedOrder, + preparedBy: payer, + }, + { targetChain, destinationDomain }, + ); + ixs.push(ix); + + const transaction = this.createTx(payer, ixs); + yield this.createUnsignedTx({ transaction, signers }, "TokenRouter.PlaceMarketOrder"); + } + + async *redeemFill( + sender: AnySolanaAddress, + vaa: VAA<"FastTransfer:CctpDeposit">, + cctp: CircleBridge.Attestation, + lookupTables?: AddressLookupTableAccount[], + ): AsyncGenerator, any, unknown> { + const payer = new SolanaAddress(sender).unwrap(); + + const postedVaaAddress = this.matchingEngine.pdas.postedVaa(vaa); + + const fill = vaa.payload.payload; + + // Must be a fill payload + if (!Payload.is(fill, "Fill")) throw new Error("Invalid VAA payload"); + + const ix = await this.redeemCctpFillIx( + { + payer: payer, + vaa: postedVaaAddress, + sourceRouterEndpoint: this.matchingEngine.routerEndpointAddress( + toChainId(fill.sourceChain), + ), + }, + { + encodedCctpMessage: Buffer.from(CircleBridge.serialize(cctp.message)), + cctpAttestation: Buffer.from(cctp.attestation!, "hex"), + }, + ); + + const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 300_000, + }); + + const transaction = this.createTx(payer, [ix, computeIx], lookupTables); + yield this.createUnsignedTx({ transaction }, "TokenRouter.RedeemFill"); + } + + private createTx( + payerKey: PublicKey, + instructions: TransactionInstruction[], + lookupTables?: AddressLookupTableAccount[], + ): VersionedTransaction { + const messageV0 = new TransactionMessage({ + payerKey, + recentBlockhash: "", + instructions, + }).compileToV0Message(lookupTables); + return new VersionedTransaction(messageV0); + } + + private createUnsignedTx( + txReq: SolanaTransaction, + description: string, + parallelizable: boolean = false, + ): SolanaUnsignedTransaction { + return new SolanaUnsignedTransaction( + txReq, + this._network, + this._chain, + description, + parallelizable, + ); + } +} diff --git a/solana/ts/src/testing/consts.ts b/solana/ts/src/testing/consts.ts index f78f1c14..c7ef4c87 100644 --- a/solana/ts/src/testing/consts.ts +++ b/solana/ts/src/testing/consts.ts @@ -1,43 +1,32 @@ -import { PublicKey, Keypair } from "@solana/web3.js"; -import { Chain, contracts } from "@wormhole-foundation/sdk-base"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { TokenRouter } from "@wormhole-foundation/example-liquidity-layer-definitions"; +import { Chain, Network, circle, encoding } from "@wormhole-foundation/sdk-base"; +import { UniversalAddress } from "@wormhole-foundation/sdk-definitions"; import { mocks } from "@wormhole-foundation/sdk-definitions/testing"; -export const CORE_BRIDGE_PID = new PublicKey(contracts.coreBridge.get("Mainnet", "Solana")!); - -export const TOKEN_ROUTER_PID = new PublicKey("tD8RmtdcV7bzBeuFgyrFc8wvayj988ChccEzRQzo6md"); - export const LOCALHOST = "http://localhost:8899"; export const PAYER_KEYPAIR = Keypair.fromSecretKey( - Buffer.from( + encoding.b64.decode( "cDfpY+VbRFXPPwouZwAx+ha9HqedkhqUr5vUaFa2ucAMGliG/hCT35/EOMKW+fcnW3cYtrwOFW2NM2xY8IOZbQ==", - "base64", ), ); - export const OWNER_ASSISTANT_KEYPAIR = Keypair.fromSecretKey( - Buffer.from( + encoding.b64.decode( "900mlHo1RRdhxUKuBnnPowQ7yqb4rJ1dC7K1PM+pRxeuCWamoSkQdY+3hXAeX0OBXanyqg4oyBl8g1z1sDnSWg==", - "base64", ), ); - export const OWNER_KEYPAIR = Keypair.fromSecretKey( - Buffer.from( + encoding.b64.decode( "t0zuiHtsaDJBSUFzkvXNttgXOMvZy0bbuUPGEByIJEHAUdFeBdSAesMbgbuH1v/y+B8CdTSkCIZZNuCntHQ+Ig==", - "base64", ), ); - export const PLAYER_ONE_KEYPAIR = Keypair.fromSecretKey( - Buffer.from( + encoding.b64.decode( "4STrqllKVVva0Fphqyf++6uGTVReATBe2cI26oIuVBft77CQP9qQrMTU1nM9ql0EnCpSgmCmm20m8khMo9WdPQ==", - "base64", ), ); -export const GOVERNANCE_EMITTER_ADDRESS = new PublicKey("11111111111111111111111111111115"); - export const GUARDIAN_KEY = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; export const MOCK_GUARDIANS = new mocks.MockGuardians(0, [GUARDIAN_KEY]); @@ -46,15 +35,16 @@ export const USDC_MINT_ADDRESS = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4w //export const ETHEREUM_USDC_ADDRESS = "0x07865c6e87b9f70255377e024ace6630c1eaa37f"; export const ETHEREUM_USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +const toDomain = (chain: Chain) => circle.toCircleChainId("Mainnet", chain); export const CHAIN_TO_DOMAIN: { [k in Chain]?: number } = { - Ethereum: 0, - Avalanche: 1, - Optimism: 2, - Arbitrum: 3, - // noble: 4, - Solana: 5, - Base: 6, - Polygon: 7, + Ethereum: toDomain("Ethereum"), + Avalanche: toDomain("Avalanche"), + Optimism: toDomain("Optimism"), + Arbitrum: toDomain("Arbitrum"), + // noble: toDomain("noble"), + Solana: toDomain("Solana"), + Base: toDomain("Base"), + Polygon: toDomain("Polygon"), }; export const REGISTERED_TOKEN_ROUTERS: { [k in Chain]?: Array } = { @@ -65,3 +55,42 @@ export const REGISTERED_TOKEN_ROUTERS: { [k in Chain]?: Array } = { Base: Array.from(Buffer.alloc(32, "f6", "hex")), Polygon: Array.from(Buffer.alloc(32, "f7", "hex")), }; + +export const REGISTERED_TOKEN_ROUTERS_V2: { [k in Chain]?: UniversalAddress } = Object.fromEntries( + Object.entries(REGISTERED_TOKEN_ROUTERS).map(([k, v]) => [ + k, + new UniversalAddress(new Uint8Array(v)), + ]), +); + +export const DEFAULT_ADDRESSES: { + [network in Network]?: TokenRouter.Addresses; +} = { + // This is local development network, not Solana's 'devnet' + Devnet: { + coreBridge: "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth", + matchingEngine: "MatchingEngine11111111111111111111111111111", + tokenRouter: "TokenRouter11111111111111111111111111111111", + cctp: { + usdcMint: USDC_MINT_ADDRESS.toBase58(), + tokenMessenger: "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3", + messageTransmitter: "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd", + wormhole: "", + wormholeRelayer: "", + }, + upgradeManager: "UpgradeManager11111111111111111111111111111", + }, + Testnet: { + coreBridge: "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5", + matchingEngine: "mPydpGUWxzERTNpyvTKdvS7v8kvw5sgwfiP8WQFrXVS", + tokenRouter: "tD8RmtdcV7bzBeuFgyrFc8wvayj988ChccEzRQzo6md", + cctp: { + usdcMint: USDC_MINT_ADDRESS.toBase58(), + tokenMessenger: "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3", + messageTransmitter: "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd", + wormhole: "", + wormholeRelayer: "", + }, + upgradeManager: "ucdP9ktgrXgEUnn6roqD2SfdGMR2JSiWHUKv23oXwxt", + }, +}; diff --git a/solana/ts/src/testing/mock.ts b/solana/ts/src/testing/mock.ts index cc39ffc2..0b362151 100644 --- a/solana/ts/src/testing/mock.ts +++ b/solana/ts/src/testing/mock.ts @@ -1,44 +1,83 @@ import { Connection, Keypair } from "@solana/web3.js"; +import { FastTransfer, Message } from "@wormhole-foundation/example-liquidity-layer-definitions"; +import { Chain, Network } from "@wormhole-foundation/sdk-base"; +import { signAndSendWait } from "@wormhole-foundation/sdk-connect"; +import { deserialize, serialize, toUniversal } from "@wormhole-foundation/sdk-definitions"; +import { mocks } from "@wormhole-foundation/sdk-definitions/testing"; +import { SolanaAddress, SolanaSendSigner } from "@wormhole-foundation/sdk-solana"; import { ethers } from "ethers"; import { LiquidityLayerMessage } from "../common"; -import { CORE_BRIDGE_PID, GUARDIAN_KEY } from "./consts"; -import { postVaa, getBlockTime } from "./utils"; -import { mocks } from "@wormhole-foundation/sdk-definitions/testing"; -import { utils as coreUtils } from "@wormhole-foundation/sdk-solana-core"; -import { Chain } from "@wormhole-foundation/sdk-base"; -import { serialize, toUniversal } from "@wormhole-foundation/sdk-definitions"; +import { SolanaMatchingEngine } from "../protocol"; +import { VaaAccount } from "../wormhole"; +import { GUARDIAN_KEY, MOCK_GUARDIANS } from "./consts"; +import { getBlockTime } from "./utils"; + +export class SDKSigner extends SolanaSendSigner { + unwrap(): Keypair { + // @ts-ignore + return this._keypair; + } + connection(): Connection { + // @ts-ignore + return this._rpc; + } +} + +export function getSdkSigner( + connection: Connection, + key: Keypair, + debug: boolean = false, +): { signer: SDKSigner; address: SolanaAddress } { + const signer = new SDKSigner(connection, "Solana", key, debug, {}); + const address = new SolanaAddress(key.publicKey); + return { signer, address }; +} -// TODO: return VaaAccount, too -export async function postLiquidityLayerVaa( +export function unwrapSigners(signers: SDKSigner[]): Keypair[] { + return signers.map((signer) => signer.unwrap()); +} + +export async function createLiquidityLayerVaa( connection: Connection, - payer: Keypair, - guardians: mocks.MockGuardians, foreignEmitterAddress: Array, sequence: bigint, message: LiquidityLayerMessage | Buffer, args: { sourceChain?: Chain; timestamp?: number } = {}, -) { +): Promise { let { sourceChain, timestamp } = args; sourceChain ??= "Ethereum"; timestamp ??= await getBlockTime(connection); const foreignEmitter = new mocks.MockEmitter( toUniversal(sourceChain, new Uint8Array(foreignEmitterAddress)), - sourceChain ?? "Ethereum", + sourceChain, sequence - 1n, ); - const published = foreignEmitter.publishMessage( - 0, // nonce, - Buffer.isBuffer(message) ? message : message.encode(), - 0, // consistencyLevel - timestamp, - ); - const vaa = guardians.addSignatures(published, [0]); + const msg = Buffer.isBuffer(message) ? message : message.encode(); + const published = foreignEmitter.publishMessage(0, msg, 0, timestamp); + const vaa = MOCK_GUARDIANS.addSignatures(published, [0]); + + try { + return deserialize(FastTransfer.getPayloadDiscriminator(), serialize(vaa)); + } catch { + // @ts-expect-error -- needed to allow testing of invalid payloads + return vaa; + } +} + +export async function postAndFetchVaa( + signer: SDKSigner, + engine: SolanaMatchingEngine, + vaa: FastTransfer.VAA, +) { + const txs = engine.postVaa(signer.address(), vaa); + await signAndSendWait(txs, signer); - await postVaa(connection, payer, Buffer.from(serialize(vaa))); + const address = engine.pdas.postedVaa(vaa); + const account = await VaaAccount.fetch(signer.connection(), address); - return coreUtils.derivePostedVaaKey(CORE_BRIDGE_PID, Buffer.from(vaa.hash)); + return { address, account }; } export class CircleAttester { diff --git a/solana/ts/src/testing/utils.ts b/solana/ts/src/testing/utils.ts index 089ccfe1..29315d3e 100644 --- a/solana/ts/src/testing/utils.ts +++ b/solana/ts/src/testing/utils.ts @@ -3,21 +3,21 @@ import { AddressLookupTableAccount, ConfirmOptions, Connection, - Keypair, PublicKey, + SendTransactionError, Signer, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; +import { Network } from "@wormhole-foundation/sdk-base"; +import { SignAndSendSigner as SdkSigner, signAndSendWait } from "@wormhole-foundation/sdk-connect"; +import { UniversalAddress } from "@wormhole-foundation/sdk-definitions"; +import { SolanaUnsignedTransaction } from "@wormhole-foundation/sdk-solana"; import { expect } from "chai"; import { execSync } from "child_process"; import { Err, Ok } from "ts-results"; -import { CORE_BRIDGE_PID, USDC_MINT_ADDRESS } from "./consts"; -import { SolanaSendSigner } from "@wormhole-foundation/sdk-solana"; -import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core"; -import { signAndSendWait } from "@wormhole-foundation/sdk-connect"; -import { UniversalAddress, deserialize } from "@wormhole-foundation/sdk-definitions"; +import { USDC_MINT_ADDRESS } from "./consts"; export function toUniversalAddress(address: number[] | Buffer | Array): UniversalAddress { return new UniversalAddress(new Uint8Array(address)); @@ -36,6 +36,49 @@ async function confirmLatest(connection: Connection, signature: string) { ); } +export async function expectTxsOk( + signer: SdkSigner, + txs: AsyncGenerator>, +) { + try { + return await signAndSendWait(txs, signer); + } catch (e) { + console.error(e); + throw e; + } +} + +export async function expectTxsOkDetails( + signer: SdkSigner, + txs: AsyncGenerator>, + connection: Connection, +) { + const [txSig] = await expectTxsOk(signer, txs); + await confirmLatest(connection, txSig.txid); + return connection.getTransaction(txSig.txid, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + }); +} + +export async function expectTxsErr( + signer: SdkSigner, + txs: AsyncGenerator>, + expectedError: string, +) { + try { + await signAndSendWait(txs, signer); + } catch (e) { + const errorMsg = + e instanceof SendTransactionError && e.logs + ? e.logs!.join("\n") + : (e as Error).toString(); + expect(errorMsg).includes(expectedError); + return; + } + throw new Error("Expected transaction to fail"); +} + export async function expectIxOk( connection: Connection, instructions: TransactionInstruction[], @@ -150,20 +193,6 @@ async function debugSendAndConfirmTransaction( }); } -export async function postVaa( - connection: Connection, - payer: Keypair, - vaaBuf: Buffer, - coreBridgeAddress?: PublicKey, -) { - const core = new SolanaWormholeCore("Devnet", "Solana", connection, { - coreBridge: (coreBridgeAddress ?? CORE_BRIDGE_PID).toString(), - }); - const txs = core.postVaa(payer.publicKey, deserialize("Uint8Array", vaaBuf)); - const signer = new SolanaSendSigner(connection, "Solana", payer, false, {}); - await signAndSendWait(txs, signer); -} - export function loadProgramBpf(artifactPath: string, keypath: string): PublicKey { // Invoke BPF Loader Upgradeable `write-buffer` instruction. const buffer = (() => { @@ -175,11 +204,6 @@ export function loadProgramBpf(artifactPath: string, keypath: string): PublicKey return buffer; } -export async function waitBySlots(connection: Connection, numSlots: number) { - const targetSlot = await connection.getSlot().then((slot) => slot + numSlots); - return waitUntilSlot(connection, targetSlot); -} - export async function waitUntilSlot(connection: Connection, targetSlot: number) { return new Promise((resolve, _) => { const sub = connection.onSlotChange((slot) => { diff --git a/solana/ts/src/tokenRouter/index.ts b/solana/ts/src/tokenRouter/index.ts index 666dde6c..8433e279 100644 --- a/solana/ts/src/tokenRouter/index.ts +++ b/solana/ts/src/tokenRouter/index.ts @@ -9,9 +9,9 @@ import { SystemProgram, TransactionInstruction, } from "@solana/web3.js"; +import { TokenRouter } from "@wormhole-foundation/example-liquidity-layer-definitions"; +import { ChainId, isChainId } from "@wormhole-foundation/sdk-base"; import { Keccak } from "sha3"; -import IDL from "../idl/json/token_router.json"; -import { TokenRouter } from "../idl/ts/token_router"; import { CctpTokenBurnMessage, MessageTransmitterProgram, @@ -23,111 +23,32 @@ import { reclaimCctpMessageIx, uint64ToBN, } from "../common"; +import IDL from "../idl/json/token_router.json"; +import { type TokenRouter as TokenRouterType } from "../idl/ts/token_router"; import * as matchingEngineSdk from "../matchingEngine"; import { UpgradeManagerProgram } from "../upgradeManager"; import { BPF_LOADER_UPGRADEABLE_PROGRAM_ID, programDataAddress } from "../utils"; import { VaaAccount } from "../wormhole"; import { Custodian, PreparedFill, PreparedOrder } from "./state"; -import { ChainId, isChainId } from "@wormhole-foundation/sdk-base"; - -export const PROGRAM_IDS = [ - "TokenRouter11111111111111111111111111111111", - "tD8RmtdcV7bzBeuFgyrFc8wvayj988ChccEzRQzo6md", -] as const; - -export type ProgramId = (typeof PROGRAM_IDS)[number]; - -export type PrepareMarketOrderArgs = { - amountIn: bigint; - minAmountOut: bigint | null; - targetChain: ChainId; - redeemer: Array; - redeemerMessage: Buffer; -}; - -export type PublishMessageAccounts = { - coreBridgeConfig: PublicKey; - coreEmitterSequence: PublicKey; - coreFeeCollector: PublicKey; - coreBridgeProgram: PublicKey; -}; - -export type TokenRouterCommonAccounts = PublishMessageAccounts & { - tokenRouterProgram: PublicKey; - systemProgram: PublicKey; - rent: PublicKey; - clock: PublicKey; - custodian: PublicKey; - cctpMintRecipient: PublicKey; - tokenMessenger: PublicKey; - tokenMinter: PublicKey; - tokenMessengerMinterSenderAuthority: PublicKey; - tokenMessengerMinterProgram: PublicKey; - messageTransmitterAuthority: PublicKey; - messageTransmitterConfig: PublicKey; - messageTransmitterProgram: PublicKey; - tokenProgram: PublicKey; - mint: PublicKey; - localToken: PublicKey; - tokenMessengerMinterCustodyToken: PublicKey; - matchingEngineProgram: PublicKey; - matchingEngineCustodian: PublicKey; - matchingEngineCctpMintRecipient: PublicKey; -}; - -export type RedeemFillCctpAccounts = { - custodian: PublicKey; - preparedFill: PublicKey; - cctpMintRecipient: PublicKey; - sourceRouterEndpoint: PublicKey; - messageTransmitterAuthority: PublicKey; - messageTransmitterConfig: PublicKey; - usedNonces: PublicKey; - messageTransmitterEventAuthority: PublicKey; - tokenMessenger: PublicKey; - remoteTokenMessenger: PublicKey; - tokenMinter: PublicKey; - localToken: PublicKey; - tokenPair: PublicKey; - tokenMessengerMinterCustodyToken: PublicKey; - tokenMessengerMinterProgram: PublicKey; - messageTransmitterProgram: PublicKey; - tokenMessengerMinterEventAuthority: PublicKey; -}; - -export type RedeemFastFillAccounts = { - custodian: PublicKey; - preparedFill: PublicKey; - cctpMintRecipient: PublicKey; - matchingEngineCustodian: PublicKey; - matchingEngineFromEndpoint: PublicKey; - matchingEngineToEndpoint: PublicKey; - matchingEngineLocalCustodyToken: PublicKey; - matchingEngineProgram: PublicKey; -}; - -export type AddCctpRouterEndpointArgs = { - chain: number; - cctpDomain: number; - address: Array; - mintRecipient: Array | null; -}; +import { + PrepareMarketOrderArgs, + PublishMessageAccounts, + RedeemFastFillAccounts, + RedeemFillCctpAccounts, + TokenRouterCommonAccounts, +} from "./types"; +export * from "./types"; export class TokenRouterProgram { - private _programId: ProgramId; - private _mint: PublicKey; + private _addresses: TokenRouter.Addresses; - program: Program; + program: Program; - // TODO: fix this - constructor(connection: Connection, programId: ProgramId, mint: PublicKey) { - this._programId = programId; - this._mint = mint; + constructor(connection: Connection, addresses: TokenRouter.Addresses) { + this._addresses = addresses; this.program = new Program( - { ...(IDL as any), address: this._programId }, - { - connection, - }, + { ...(IDL as any), address: this._addresses.tokenRouter }, + { connection }, ); } @@ -136,7 +57,23 @@ export class TokenRouterProgram { } get mint(): PublicKey { - return this._mint; + return new PublicKey(this._addresses.cctp.usdcMint); + } + get cctpTokenMessenger(): PublicKey { + return new PublicKey(this._addresses.cctp.tokenMessenger); + } + get cctpMessageTransmitter(): PublicKey { + return new PublicKey(this._addresses.cctp.messageTransmitter); + } + + get coreBridgeProgramId(): PublicKey { + return new PublicKey(this._addresses.coreBridge); + } + get matchingEngineProgramId(): PublicKey { + return new PublicKey(this._addresses.matchingEngine); + } + get upgradeManager(): PublicKey { + return new PublicKey(this._addresses.upgradeManager!); } custodianAddress(): PublicKey { @@ -878,7 +815,7 @@ export class TokenRouterProgram { } publishMessageAccounts(emitter: PublicKey): PublishMessageAccounts { - const coreBridgeProgram = this.coreBridgeProgramId(); + const coreBridgeProgram = this.coreBridgeProgramId; return { coreBridgeConfig: PublicKey.findProgramAddressSync( @@ -898,80 +835,27 @@ export class TokenRouterProgram { } upgradeManagerProgram(): UpgradeManagerProgram { - switch (this._programId) { - case testnet(): { - return new UpgradeManagerProgram( - this.program.provider.connection, - "ucdP9ktgrXgEUnn6roqD2SfdGMR2JSiWHUKv23oXwxt", - ); - } - case localnet(): { - return new UpgradeManagerProgram( - this.program.provider.connection, - "UpgradeManager11111111111111111111111111111", - ); - } - default: { - throw new Error("unsupported network"); - } - } + return new UpgradeManagerProgram(this.program.provider.connection, this._addresses); } tokenMessengerMinterProgram(): TokenMessengerMinterProgram { return new TokenMessengerMinterProgram( this.program.provider.connection, - "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3", + this._addresses.cctp, ); } messageTransmitterProgram(): MessageTransmitterProgram { return new MessageTransmitterProgram( this.program.provider.connection, - "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd", + this._addresses.cctp, ); } matchingEngineProgram(): matchingEngineSdk.MatchingEngineProgram { - switch (this._programId) { - case testnet(): { - return new matchingEngineSdk.MatchingEngineProgram( - this.program.provider.connection, - matchingEngineSdk.testnet(), - this.mint, - ); - } - case localnet(): { - return new matchingEngineSdk.MatchingEngineProgram( - this.program.provider.connection, - matchingEngineSdk.localnet(), - this.mint, - ); - } - default: { - throw new Error("unsupported network"); - } - } - } - - coreBridgeProgramId(): PublicKey { - switch (this._programId) { - case testnet(): { - return new PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"); - } - case localnet(): { - return new PublicKey("worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"); - } - default: { - throw new Error("unsupported network"); - } - } + return new matchingEngineSdk.MatchingEngineProgram( + this.program.provider.connection, + this._addresses, + ); } } - -export function localnet(): ProgramId { - return "TokenRouter11111111111111111111111111111111"; -} - -export function testnet(): ProgramId { - return "tD8RmtdcV7bzBeuFgyrFc8wvayj988ChccEzRQzo6md"; -} diff --git a/solana/ts/src/tokenRouter/state/PreparedOrder.ts b/solana/ts/src/tokenRouter/state/PreparedOrder.ts index eee4880f..f1036c4a 100644 --- a/solana/ts/src/tokenRouter/state/PreparedOrder.ts +++ b/solana/ts/src/tokenRouter/state/PreparedOrder.ts @@ -1,7 +1,5 @@ import { BN } from "@coral-xyz/anchor"; import { PublicKey } from "@solana/web3.js"; -import { Keccak } from "sha3"; -import { Uint64, uint64ToBN } from "../../common"; export type OrderType = { market?: { diff --git a/solana/ts/src/tokenRouter/types.ts b/solana/ts/src/tokenRouter/types.ts new file mode 100644 index 00000000..5d4e13f0 --- /dev/null +++ b/solana/ts/src/tokenRouter/types.ts @@ -0,0 +1,78 @@ +import { PublicKey } from "@solana/web3.js"; +import { ChainId } from "@wormhole-foundation/sdk-base"; + +export type PrepareMarketOrderArgs = { + amountIn: bigint; + minAmountOut: bigint | null; + targetChain: ChainId; + redeemer: Array; + redeemerMessage: Buffer; +}; + +export type PublishMessageAccounts = { + coreBridgeConfig: PublicKey; + coreEmitterSequence: PublicKey; + coreFeeCollector: PublicKey; + coreBridgeProgram: PublicKey; +}; + +export type TokenRouterCommonAccounts = PublishMessageAccounts & { + tokenRouterProgram: PublicKey; + systemProgram: PublicKey; + rent: PublicKey; + clock: PublicKey; + custodian: PublicKey; + cctpMintRecipient: PublicKey; + tokenMessenger: PublicKey; + tokenMinter: PublicKey; + tokenMessengerMinterSenderAuthority: PublicKey; + tokenMessengerMinterProgram: PublicKey; + messageTransmitterAuthority: PublicKey; + messageTransmitterConfig: PublicKey; + messageTransmitterProgram: PublicKey; + tokenProgram: PublicKey; + mint: PublicKey; + localToken: PublicKey; + tokenMessengerMinterCustodyToken: PublicKey; + matchingEngineProgram: PublicKey; + matchingEngineCustodian: PublicKey; + matchingEngineCctpMintRecipient: PublicKey; +}; + +export type RedeemFillCctpAccounts = { + custodian: PublicKey; + preparedFill: PublicKey; + cctpMintRecipient: PublicKey; + sourceRouterEndpoint: PublicKey; + messageTransmitterAuthority: PublicKey; + messageTransmitterConfig: PublicKey; + usedNonces: PublicKey; + messageTransmitterEventAuthority: PublicKey; + tokenMessenger: PublicKey; + remoteTokenMessenger: PublicKey; + tokenMinter: PublicKey; + localToken: PublicKey; + tokenPair: PublicKey; + tokenMessengerMinterCustodyToken: PublicKey; + tokenMessengerMinterProgram: PublicKey; + messageTransmitterProgram: PublicKey; + tokenMessengerMinterEventAuthority: PublicKey; +}; + +export type RedeemFastFillAccounts = { + custodian: PublicKey; + preparedFill: PublicKey; + cctpMintRecipient: PublicKey; + matchingEngineCustodian: PublicKey; + matchingEngineFromEndpoint: PublicKey; + matchingEngineToEndpoint: PublicKey; + matchingEngineLocalCustodyToken: PublicKey; + matchingEngineProgram: PublicKey; +}; + +export type AddCctpRouterEndpointArgs = { + chain: number; + cctpDomain: number; + address: Array; + mintRecipient: Array | null; +}; diff --git a/solana/ts/src/upgradeManager/index.ts b/solana/ts/src/upgradeManager/index.ts index 371e9fe6..cbd03306 100644 --- a/solana/ts/src/upgradeManager/index.ts +++ b/solana/ts/src/upgradeManager/index.ts @@ -15,26 +15,15 @@ import * as matchingEngineSdk from "../matchingEngine"; import * as tokenRouterSdk from "../tokenRouter"; import { BPF_LOADER_UPGRADEABLE_PROGRAM_ID, programDataAddress } from "../utils"; import { UpgradeReceipt } from "./state"; - -export const PROGRAM_IDS = [ - "UpgradeManager11111111111111111111111111111", - "ucdP9ktgrXgEUnn6roqD2SfdGMR2JSiWHUKv23oXwxt", -] as const; - -export type ProgramId = (typeof PROGRAM_IDS)[number]; +import { TokenRouter } from "@wormhole-foundation/example-liquidity-layer-definitions"; export class UpgradeManagerProgram { - private _programId: ProgramId; - program: Program; - constructor(connection: Connection, programId: ProgramId) { - this._programId = programId; + constructor(connection: Connection, private _addresses: TokenRouter.Addresses) { this.program = new Program( - { ...(IDL as any), address: this._programId }, - { - connection, - }, + { ...(IDL as any), address: this._addresses.upgradeManager }, + { connection }, ); } @@ -192,54 +181,16 @@ export class UpgradeManagerProgram { } matchingEngineProgram(): matchingEngineSdk.MatchingEngineProgram { - switch (this._programId) { - case testnet(): { - return new matchingEngineSdk.MatchingEngineProgram( - this.program.provider.connection, - matchingEngineSdk.testnet(), - PublicKey.default, - ); - } - case localnet(): { - return new matchingEngineSdk.MatchingEngineProgram( - this.program.provider.connection, - matchingEngineSdk.localnet(), - PublicKey.default, - ); - } - default: { - throw new Error("unsupported network"); - } - } + return new matchingEngineSdk.MatchingEngineProgram( + this.program.provider.connection, + this._addresses, + ); } tokenRouterProgram(): tokenRouterSdk.TokenRouterProgram { - switch (this._programId) { - case testnet(): { - return new tokenRouterSdk.TokenRouterProgram( - this.program.provider.connection, - tokenRouterSdk.testnet(), - PublicKey.default, - ); - } - case localnet(): { - return new tokenRouterSdk.TokenRouterProgram( - this.program.provider.connection, - tokenRouterSdk.localnet(), - PublicKey.default, - ); - } - default: { - throw new Error("unsupported network"); - } - } + return new tokenRouterSdk.TokenRouterProgram( + this.program.provider.connection, + this._addresses, + ); } } - -export function testnet(): ProgramId { - return "ucdP9ktgrXgEUnn6roqD2SfdGMR2JSiWHUKv23oXwxt"; -} - -export function localnet(): ProgramId { - return "UpgradeManager11111111111111111111111111111"; -} diff --git a/solana/ts/src/wormhole/index.ts b/solana/ts/src/wormhole/index.ts index 6b9a9ef3..1f83dda2 100644 --- a/solana/ts/src/wormhole/index.ts +++ b/solana/ts/src/wormhole/index.ts @@ -1,26 +1,28 @@ import { Connection, PublicKey } from "@solana/web3.js"; -import { ChainId, isChainId, toChainId } from "@wormhole-foundation/sdk-base"; -import { deserialize, keccak256 } from "@wormhole-foundation/sdk-definitions"; import { ethers } from "ethers"; +import { + ChainId, + Layout, + deserializeLayout, + toChain, + toChainId, +} from "@wormhole-foundation/sdk-base"; +import { + PayloadLiteral, + VAA, + createVAA, + deserialize, + keccak256, + layoutItems, + serialize, +} from "@wormhole-foundation/sdk-definitions"; export * from "./spy"; export type EncodedVaa = { status: number; writeAuthority: PublicKey; version: number; - buf: Buffer; -}; - -export type PostedVaaV1 = { - consistencyLevel: number; - timestamp: number; - signatureSet: PublicKey; - guardianSetIndex: number; - nonce: number; - sequence: bigint; - emitterChain: ChainId; - emitterAddress: Array; - payload: Buffer; + vaa: VAA<"Uint8Array">; }; export type EmitterInfo = { @@ -29,9 +31,22 @@ export type EmitterInfo = { sequence: bigint; }; +const vaaAccountLayout = [ + { name: "discriminator", binary: "bytes", size: 4 }, + { name: "consistencyLevel", binary: "uint", size: 1, endianness: "little" }, + { name: "timestamp", binary: "uint", size: 4, endianness: "little" }, + { name: "signatureSet", binary: "bytes", size: 32 }, + { name: "guardianSetIndex", binary: "uint", size: 4, endianness: "little" }, + { name: "nonce", binary: "uint", size: 4, endianness: "little" }, + { name: "sequence", binary: "uint", size: 8, endianness: "little" }, + { name: "emitterChain", binary: "uint", size: 2, endianness: "little" }, + { name: "emitterAddress", ...layoutItems.universalAddressItem }, + { name: "payload", binary: "bytes", lengthSize: 4, lengthEndianness: "little" }, +] as const satisfies Layout; + export class VaaAccount { private _encodedVaa?: EncodedVaa; - private _postedVaaV1?: PostedVaaV1; + private _postedVaaV1?: VAA<"Uint8Array">; static async fetch(connection: Connection, addr: PublicKey): Promise { const accInfo = await connection.getAccountInfo(addr); @@ -50,118 +65,56 @@ export class VaaAccount { offset += 1; const bufLen = data.readUInt32LE(offset); offset += 4; - const buf = data.subarray(offset, (offset += bufLen)); - return new VaaAccount({ encodedVaa: { status, writeAuthority, version, buf } }); + const vaa = deserialize("Uint8Array", data.subarray(offset, (offset += bufLen))); + return new VaaAccount({ encodedVaa: { status, writeAuthority, version, vaa } }); } else if (disc.subarray(0, (offset -= 4)).equals(Uint8Array.from([118, 97, 97, 1]))) { - const consistencyLevel = data[offset]; - offset += 1; - const timestamp = data.readUInt32LE(offset); - offset += 4; - const signatureSet = new PublicKey(data.subarray(offset, (offset += 32))); - const guardianSetIndex = data.readUInt32LE(offset); - offset += 4; - const nonce = data.readUInt32LE(offset); - offset += 4; - const sequence = data.readBigUInt64LE(offset); - offset += 8; - const emitterChain = data.readUInt16LE(offset); - if (!isChainId(emitterChain)) { - throw new Error("invalid emitter chain"); - } - offset += 2; - const emitterAddress = Array.from(data.subarray(offset, (offset += 32))); - const payloadLen = data.readUInt32LE(offset); - offset += 4; - const payload = data.subarray(offset, (offset += payloadLen)); - - return new VaaAccount({ - postedVaaV1: { - consistencyLevel, - timestamp, - signatureSet, - guardianSetIndex, - nonce, - sequence, - emitterChain, - emitterAddress, - payload, - }, + const vaaData = deserializeLayout(vaaAccountLayout, new Uint8Array(data)); + const vaa = createVAA("Uint8Array", { + timestamp: vaaData.timestamp, + nonce: vaaData.nonce, + emitterChain: toChain(vaaData.emitterChain), + emitterAddress: vaaData.emitterAddress, + sequence: vaaData.sequence, + consistencyLevel: vaaData.consistencyLevel, + payload: vaaData.payload, + guardianSet: 0, + signatures: [], }); + return new VaaAccount({ postedVaaV1: vaa }); } else { throw new Error("invalid VAA account data"); } } + vaa

(payload?: P): VAA

{ + const vaa = + this._encodedVaa !== undefined + ? this._encodedVaa.vaa + : this._postedVaaV1 !== undefined + ? this._postedVaaV1 + : undefined; + + if (!vaa) throw new Error("impossible: vaa() failed"); + + return (payload ? deserialize(payload, serialize(vaa)) : vaa) as VAA

; + } + emitterInfo(): EmitterInfo { - if (this._encodedVaa !== undefined) { - const parsed = deserialize("Uint8Array", this._encodedVaa.buf); - return { - chain: toChainId(parsed.emitterChain), - address: Array.from(parsed.emitterAddress.toUint8Array()), - sequence: parsed.sequence, - }; - } else if (this._postedVaaV1 !== undefined) { - const { emitterChain: chain, emitterAddress: address, sequence } = this._postedVaaV1; - return { - chain, - address, - sequence, - }; - } else { - throw new Error("impossible: emitterInfo() failed"); - } + const { emitterChain: chain, emitterAddress: address, sequence } = this.vaa(); + return { chain: toChainId(chain), address: Array.from(address.toUint8Array()), sequence }; } timestamp(): number { - if (this._encodedVaa !== undefined) { - return deserialize("Uint8Array", this._encodedVaa.buf).timestamp; - } else if (this._postedVaaV1 !== undefined) { - return this._postedVaaV1.timestamp; - } else { - throw new Error("impossible: timestamp() failed"); - } + return this.vaa().timestamp; } payload(): Buffer { - if (this._encodedVaa !== undefined) { - return Buffer.from(deserialize("Uint8Array", this._encodedVaa.buf).payload); - } else if (this._postedVaaV1 !== undefined) { - return this._postedVaaV1.payload; - } else { - throw new Error("impossible: payload() failed"); - } + return Buffer.from(this.vaa().payload); } hash(): Uint8Array { - if (this._encodedVaa !== undefined) { - return deserialize("Uint8Array", this._encodedVaa.buf).hash; - } else if (this._postedVaaV1 !== undefined) { - const { - consistencyLevel, - timestamp, - nonce, - sequence, - emitterChain, - emitterAddress, - payload, - } = this._postedVaaV1; - - let offset = 0; - const buf = Buffer.alloc(51 + payload.length); - offset = buf.writeUInt32BE(timestamp, offset); - offset = buf.writeUInt32BE(nonce, offset); - offset = buf.writeUInt16BE(emitterChain, offset); - buf.set(emitterAddress, offset); - offset += 32; - offset = buf.writeBigUInt64BE(sequence, offset); - offset = buf.writeUInt8(consistencyLevel, offset); - buf.set(payload, offset); - - return ethers.utils.arrayify(ethers.utils.keccak256(buf)); - } else { - throw new Error("impossible: hash() failed"); - } + return this.vaa().hash; } digest(): Uint8Array { @@ -175,14 +128,14 @@ export class VaaAccount { return this._encodedVaa; } - get postedVaaV1(): PostedVaaV1 { + get postedVaaV1(): VAA<"Uint8Array"> { if (this._postedVaaV1 === undefined) { throw new Error("VaaAccount does not have postedVaaV1"); } return this._postedVaaV1; } - private constructor(data: { encodedVaa?: EncodedVaa; postedVaaV1?: PostedVaaV1 }) { + private constructor(data: { encodedVaa?: EncodedVaa; postedVaaV1?: VAA<"Uint8Array"> }) { const { encodedVaa, postedVaaV1 } = data; if (encodedVaa !== undefined && postedVaaV1 !== undefined) { throw new Error("VaaAccount cannot have both encodedVaa and postedVaaV1"); diff --git a/solana/ts/tests/01__matchingEngine.ts b/solana/ts/tests/01__matchingEngine.ts index 6400f742..3b999819 100644 --- a/solana/ts/tests/01__matchingEngine.ts +++ b/solana/ts/tests/01__matchingEngine.ts @@ -1,4 +1,5 @@ import { BN } from "@coral-xyz/anchor"; +// @ts-ignore import * as splToken from "@solana/spl-token"; import { AddressLookupTableProgram, @@ -6,7 +7,6 @@ import { Connection, Keypair, PublicKey, - Signer, SystemProgram, TransactionInstruction, VersionedTransactionResponse, @@ -17,7 +17,7 @@ import { SlowOrderResponse, } from "@wormhole-foundation/example-liquidity-layer-definitions"; import { Chain, ChainId, encoding, toChain, toChainId } from "@wormhole-foundation/sdk-base"; -import { toUniversal } from "@wormhole-foundation/sdk-definitions"; +import { CircleBridge, VAA, toUniversal } from "@wormhole-foundation/sdk-definitions"; import { deserializePostMessage } from "@wormhole-foundation/sdk-solana-core"; import { expect } from "chai"; import { CctpTokenBurnMessage } from "../src/cctp"; @@ -34,55 +34,88 @@ import { AuctionParameters, CctpMessageArgs, Custodian, - MatchingEngineProgram, PreparedOrderResponse, Proposal, RouterEndpoint, - localnet, } from "../src/matchingEngine"; +import { SolanaMatchingEngine } from "../src/protocol/matchingEngine"; import { CHAIN_TO_DOMAIN, CircleAttester, + DEFAULT_ADDRESSES, ETHEREUM_USDC_ADDRESS, LOCALHOST, - MOCK_GUARDIANS, OWNER_ASSISTANT_KEYPAIR, OWNER_KEYPAIR, PAYER_KEYPAIR, PLAYER_ONE_KEYPAIR, REGISTERED_TOKEN_ROUTERS, + REGISTERED_TOKEN_ROUTERS_V2, + SDKSigner, USDC_MINT_ADDRESS, + createLiquidityLayerVaa, expectIxErr, expectIxOk, expectIxOkDetails, + expectTxsErr, + expectTxsOk, + expectTxsOkDetails, getBlockTime, + getSdkSigner, getUsdcAtaBalance, - postLiquidityLayerVaa, + postAndFetchVaa, toUniversalAddress, + unwrapSigners, waitUntilSlot, waitUntilTimestamp, } from "../src/testing"; import { VaaAccount } from "../src/wormhole"; +import chai from "chai"; +chai.config.truncateThreshold = 0; + const SLOTS_PER_EPOCH = 8; +// Set to true to add debug logs for Signer +const SIGNER_DEBUG = false; + describe("Matching Engine", function () { const connection = new Connection(LOCALHOST, "processed"); // owner is also the recipient in all tests const payer = PAYER_KEYPAIR; + const { signer: payerSigner } = getSdkSigner(connection, payer, SIGNER_DEBUG); + const owner = OWNER_KEYPAIR; + const { signer: ownerSigner } = getSdkSigner(connection, owner, SIGNER_DEBUG); + const relayer = Keypair.generate(); + const { signer: relayerSigner } = getSdkSigner(connection, relayer, SIGNER_DEBUG); + const ownerAssistant = OWNER_ASSISTANT_KEYPAIR; + const { signer: ownerAssistantSigner } = getSdkSigner(connection, ownerAssistant, SIGNER_DEBUG); + const feeRecipient = Keypair.generate().publicKey; const feeRecipientToken = splToken.getAssociatedTokenAddressSync( USDC_MINT_ADDRESS, feeRecipient, + SIGNER_DEBUG, ); const newFeeRecipient = Keypair.generate().publicKey; const playerOne = PLAYER_ONE_KEYPAIR; + const { signer: playerOneSigner } = getSdkSigner(connection, playerOne, SIGNER_DEBUG); const playerTwo = Keypair.generate(); + const { signer: playerTwoSigner, address: playerTwoAddress } = getSdkSigner( + connection, + playerTwo, + SIGNER_DEBUG, + ); const liquidator = Keypair.generate(); + const { signer: liquidatorSigner, address: liquidatorAddress } = getSdkSigner( + connection, + liquidator, + SIGNER_DEBUG, + ); // Foreign endpoints. const ethChain = toChainId("Ethereum"); @@ -96,7 +129,12 @@ describe("Matching Engine", function () { const solanaChain = toChainId("Solana"); // Matching Engine program. - const engine = new MatchingEngineProgram(connection, localnet(), USDC_MINT_ADDRESS); + const engine = new SolanaMatchingEngine( + "Devnet", + "Solana", + connection, + DEFAULT_ADDRESSES["Devnet"]!, + ); let lookupTableAddress: PublicKey; @@ -120,8 +158,6 @@ describe("Matching Engine", function () { describe("Admin", function () { describe("Initialize", function () { - const localVariables = new Map(); - before("Transfer Lamports to Executors", async function () { await expectIxOk( connection, @@ -220,178 +256,124 @@ describe("Matching Engine", function () { it("Cannot Initialize without USDC Mint", async function () { const mint = await splToken.createMint(connection, payer, payer.publicKey, null, 6); - - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, auctionParams, + mint, ); - await expectIxErr(connection, [ix], [payer], "mint. Error Code: ConstraintAddress"); + await expectTxsErr(payerSigner, txs, "mint. Error Code: ConstraintAddress"); }); it("Cannot Initialize with Default Owner Assistant", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: PublicKey.default, - feeRecipient, - }, + const txs = engine.initialize( + payer.publicKey, + PublicKey.default, + feeRecipient, auctionParams, ); - await expectIxErr(connection, [ix], [payer], "Error Code: AssistantZeroPubkey"); + await expectTxsErr(payerSigner, txs, "Error Code: AssistantZeroPubkey"); }); it("Cannot Initialize with Default Fee Recipient", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient: PublicKey.default, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + PublicKey.default, auctionParams, ); - await expectIxErr(connection, [ix], [payer], "Error Code: FeeRecipientZeroPubkey"); + await expectTxsErr(payerSigner, txs, "Error Code: FeeRecipientZeroPubkey"); }); it("Cannot Initialize with Zero Auction Duration", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, { ...auctionParams, duration: 0 }, ); - await expectIxErr(connection, [ix], [payer], "Error Code: ZeroDuration"); + await expectTxsErr(payerSigner, txs, "Error Code: ZeroDuration"); }); it("Cannot Initialize with Zero Auction Grace Period", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, { ...auctionParams, gracePeriod: 0 }, ); - await expectIxErr(connection, [ix], [payer], "Error Code: ZeroGracePeriod"); + await expectTxsErr(payerSigner, txs, "Error Code: ZeroGracePeriod"); }); it("Cannot Initialize with Zero Auction Penalty Period", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, { ...auctionParams, penaltyPeriod: 0 }, ); - await expectIxErr(connection, [ix], [payer], "Error Code: ZeroPenaltyPeriod"); + await expectTxsErr(payerSigner, txs, "Error Code: ZeroPenaltyPeriod"); }); it("Cannot Initialize with Invalid User Penalty Bps", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, { ...auctionParams, userPenaltyRewardBps: 1_000_001 }, ); - await expectIxErr( - connection, - [ix], - [payer], - "Error Code: UserPenaltyRewardBpsTooLarge", - ); + await expectTxsErr(payerSigner, txs, "Error Code: UserPenaltyRewardBpsTooLarge"); }); it("Cannot Initialize with Invalid Initial Penalty Bps", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, { ...auctionParams, initialPenaltyBps: 1_000_001 }, ); - await expectIxErr( - connection, - [ix], - [payer], - "Error Code: InitialPenaltyBpsTooLarge", - ); + await expectTxsErr(payerSigner, txs, "Error Code: InitialPenaltyBpsTooLarge"); }); it("Cannot Initialize with Invalid Min Offer Delta Bps", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, { ...auctionParams, minOfferDeltaBps: 1_000_001 }, ); - await expectIxErr( - connection, - [ix], - [payer], - "Error Code: MinOfferDeltaBpsTooLarge", - ); + await expectTxsErr(payerSigner, txs, "Error Code: MinOfferDeltaBpsTooLarge"); }); it("Cannot Initialize with Invalid Security Deposit Base", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, { ...auctionParams, securityDepositBase: uint64ToBN(0) }, ); - await expectIxErr(connection, [ix], [payer], "Error Code: ZeroSecurityDepositBase"); + await expectTxsErr(payerSigner, txs, "Error Code: ZeroSecurityDepositBase"); }); it("Cannot Initialize with Invalid Security Deposit Bps", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, { ...auctionParams, securityDepositBps: 1_000_001 }, ); - await expectIxErr( - connection, - [ix], - [payer], - "Error Code: SecurityDepositBpsTooLarge", - ); + await expectTxsErr(payerSigner, txs, "Error Code: SecurityDepositBpsTooLarge"); }); it("Finally Initialize Program", async function () { - const ix = await engine.initializeIx( - { - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - feeRecipient, - mint: USDC_MINT_ADDRESS, - }, + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, auctionParams, ); - await expectIxOk(connection, [ix], [payer]); + await expectTxsOk(payerSigner, txs); const expectedAuctionConfigId = 0; const custodianData = await engine.fetchCustodian(); @@ -412,18 +394,18 @@ describe("Matching Engine", function () { expect(auctionConfigData).to.eql( new AuctionConfig(expectedAuctionConfigId, auctionParams), ); - - localVariables.set("ix", ix); }); it("Cannot Call Instruction Again: initialize", async function () { - const ix = localVariables.get("ix") as TransactionInstruction; - expect(localVariables.delete("ix")).is.true; - - await expectIxErr( - connection, - [ix], - [payer], + const txs = engine.initialize( + payer.publicKey, + ownerAssistant.publicKey, + feeRecipient, + auctionParams, + ); + await expectTxsErr( + payerSigner, + txs, `Allocate: account Address { address: ${engine .custodianAddress() .toString()}, base: None } already in use`, @@ -684,26 +666,14 @@ describe("Matching Engine", function () { describe("Set Pause", async function () { it("Cannot Set Pause for Transfers as Non-Owner", async function () { - const ix = await engine.setPauseIx( - { - ownerOrAssistant: payer.publicKey, - }, - true, // paused - ); - - await expectIxErr(connection, [ix], [payer], "Error Code: OwnerOrAssistantOnly"); + const txs = engine.setPause(payer.publicKey, true); + await expectTxsErr(payerSigner, txs, "Error Code: OwnerOrAssistantOnly"); }); it("Set Paused == true as Owner Assistant", async function () { const paused = true; - const ix = await engine.setPauseIx( - { - ownerOrAssistant: ownerAssistant.publicKey, - }, - paused, - ); - - await expectIxOk(connection, [ix], [ownerAssistant]); + const txs = engine.setPause(ownerAssistant.publicKey, paused); + await expectTxsOk(ownerAssistantSigner, txs); const { paused: actualPaused, pausedSetBy } = await engine.fetchCustodian(); expect(actualPaused).equals(paused); @@ -712,15 +682,8 @@ describe("Matching Engine", function () { it("Set Paused == false as Owner", async function () { const paused = false; - const ix = await engine.setPauseIx( - { - ownerOrAssistant: owner.publicKey, - }, - paused, - ); - - await expectIxOk(connection, [ix], [owner]); - + const txs = engine.setPause(owner.publicKey, paused); + await expectTxsOk(ownerSigner, txs); const { paused: actualPaused, pausedSetBy } = await engine.fetchCustodian(); expect(actualPaused).equals(paused); expect(pausedSetBy).eql(owner.publicKey); @@ -728,39 +691,36 @@ describe("Matching Engine", function () { }); describe("Router Endpoint (CCTP)", function () { - const localVariables = new Map(); - after("Register To Router Endpoints", async function () { - const ix = await engine.addCctpRouterEndpointIx( - { - ownerOrAssistant: owner.publicKey, - }, - { - chain: toChainId("Arbitrum"), - cctpDomain: CHAIN_TO_DOMAIN["Arbitrum"]!, - address: REGISTERED_TOKEN_ROUTERS["Arbitrum"]!, - mintRecipient: null, - }, - ); - await expectIxOk(connection, [ix], [owner]); + const endpointChains: Chain[] = ["Arbitrum", "Base"]; + + for (const endpointChain of endpointChains) { + const txs = engine.registerRouter( + owner.publicKey, + endpointChain, + CHAIN_TO_DOMAIN[endpointChain]!, + REGISTERED_TOKEN_ROUTERS_V2[endpointChain]!, + ); + + await expectTxsOk(ownerSigner, txs); + } }); it("Cannot Add Router Endpoint as Non-Owner and Non-Assistant", async function () { - const ix = await engine.addCctpRouterEndpointIx( - { ownerOrAssistant: payer.publicKey }, - { - chain: ethChain, - cctpDomain: ethDomain, - address: ethRouter, - mintRecipient: null, - }, + const endpointChain = "Ethereum"; + const txs = engine.registerRouter( + payer.publicKey, + endpointChain, + CHAIN_TO_DOMAIN[endpointChain]!, + REGISTERED_TOKEN_ROUTERS_V2[endpointChain]!, ); - - await expectIxErr(connection, [ix], [payer], "OwnerOrAssistantOnly"); + await expectTxsErr(payerSigner, txs, "OwnerOrAssistantOnly"); }); [0, solanaChain].forEach((chain) => it(`Cannot Register Chain ID == ${chain}`, async function () { + // TODO: Intentionally left as ix tester for now since + // the 0 chain ID would trip other checks. const ix = await engine.addCctpRouterEndpointIx( { ownerOrAssistant: owner.publicKey }, { @@ -775,32 +735,26 @@ describe("Matching Engine", function () { ); it("Cannot Register Zero Address", async function () { - const ix = await engine.addCctpRouterEndpointIx( - { ownerOrAssistant: owner.publicKey }, - { - chain: ethChain, - cctpDomain: ethDomain, - address: new Array(32).fill(0), - mintRecipient: null, - }, + const txs = engine.registerRouter( + owner.publicKey, + "Ethereum", + ethDomain, + toUniversalAddress(new Array(32).fill(0)), ); - - await expectIxErr(connection, [ix], [owner], "InvalidEndpoint"); + await expectTxsErr(ownerSigner, txs, "InvalidEndpoint"); }); it("Add Router Endpoint as Owner Assistant", async function () { const contractAddress = Array.from(Buffer.alloc(32, "fbadc0de", "hex")); const mintRecipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const ix = await engine.addCctpRouterEndpointIx( - { ownerOrAssistant: ownerAssistant.publicKey }, - { - chain: ethChain, - cctpDomain: ethDomain, - address: contractAddress, - mintRecipient, - }, + const txs = engine.registerRouter( + ownerAssistant.publicKey, + "Ethereum", + ethDomain, + toUniversalAddress(contractAddress), + toUniversalAddress(mintRecipient), ); - await expectIxOk(connection, [ix], [ownerAssistant]); + await expectTxsOk(ownerAssistantSigner, txs); const routerEndpointData = await engine.fetchRouterEndpoint(ethChain); expect(routerEndpointData).to.eql( @@ -813,40 +767,35 @@ describe("Matching Engine", function () { }, }), ); - - // Save for later. - localVariables.set("ix", ix); }); it("Cannot Add Router Endpoint Again", async function () { - const ix = localVariables.get("ix") as TransactionInstruction; - expect(localVariables.delete("ix")).is.true; - const routerEndpoint = engine.routerEndpointAddress(ethChain); - await expectIxErr( - connection, - [ix], - [ownerAssistant], + + const contractAddress = Array.from(Buffer.alloc(32, "fbadc0de", "hex")); + const mintRecipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const txs = engine.registerRouter( + ownerAssistant.publicKey, + "Ethereum", + ethDomain, + toUniversalAddress(contractAddress), + toUniversalAddress(mintRecipient), + ); + await expectTxsErr( + ownerAssistantSigner, + txs, `Allocate: account Address { address: ${routerEndpoint.toString()}, base: None } already in use`, ); }); it("Cannot Disable Router Endpoint as Owner Assistant", async function () { - const ix = await engine.disableRouterEndpointIx( - { owner: ownerAssistant.publicKey }, - ethChain, - ); - - await expectIxErr(connection, [ix], [ownerAssistant], "Error Code: OwnerOnly"); + const txs = engine.disableRouter(ownerAssistant.publicKey, "Ethereum"); + await expectTxsErr(ownerAssistantSigner, txs, "Error Code: OwnerOnly"); }); it("Disable Router Endpoint as Owner", async function () { - const ix = await engine.disableRouterEndpointIx( - { owner: owner.publicKey }, - ethChain, - ); - - await expectIxOk(connection, [ix], [owner]); + const txs = engine.disableRouter(owner.publicKey, "Ethereum"); + await expectTxsOk(ownerSigner, txs); const routerEndpointData = await engine.fetchRouterEndpoint(ethChain); const { bump } = routerEndpointData; @@ -861,31 +810,24 @@ describe("Matching Engine", function () { }); it("Cannot Update Router Endpoint as Owner Assistant", async function () { - const ix = await engine.updateCctpRouterEndpointIx( - { owner: ownerAssistant.publicKey }, - { - chain: ethChain, - cctpDomain: ethDomain, - address: ethRouter, - mintRecipient: null, - }, + const txs = engine.updateRouter( + ownerAssistant.publicKey, + "Ethereum", + ethDomain, + REGISTERED_TOKEN_ROUTERS_V2["Ethereum"]!, ); - - await expectIxErr(connection, [ix], [ownerAssistant], "Error Code: OwnerOnly"); + await expectTxsErr(ownerAssistantSigner, txs, "Error Code: OwnerOnly"); }); it("Update Router Endpoint as Owner", async function () { - const ix = await engine.updateCctpRouterEndpointIx( - { owner: owner.publicKey }, - { - chain: ethChain, - cctpDomain: ethDomain, - address: ethRouter, - mintRecipient: null, - }, + const txs = engine.updateRouter( + owner.publicKey, + "Ethereum", + ethDomain, + REGISTERED_TOKEN_ROUTERS_V2["Ethereum"]!, ); - await expectIxOk(connection, [ix], [owner]); + await expectTxsOk(ownerSigner, txs); const routerEndpointData = await engine.fetchRouterEndpoint(ethChain); expect(routerEndpointData).to.eql( @@ -1374,14 +1316,10 @@ describe("Matching Engine", function () { for (const offerPrice of [0n, baseFastOrder.maxFee / 2n, baseFastOrder.maxFee]) { it(`Place Initial Offer (Price == ${offerPrice})`, async function () { await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, - }, - { - args: { - offerPrice, - }, - signers: [playerOne], + args: { offerPrice }, + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, @@ -1428,7 +1366,7 @@ describe("Matching Engine", function () { payer: playerOne.publicKey, }, { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: { ...baseFastOrder, @@ -1449,7 +1387,7 @@ describe("Matching Engine", function () { payer: playerOne.publicKey, }, { - signers: [playerOne], + signers: [playerOneSigner], fastMarketOrder: { ...baseFastOrder, deadline }, finalized: false, }, @@ -1462,7 +1400,7 @@ describe("Matching Engine", function () { payer: playerOne.publicKey, }, { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, vaaTimestamp: 69, @@ -1472,33 +1410,34 @@ describe("Matching Engine", function () { }); it("Cannot Place Initial Offer (Invalid VAA)", async function () { - const fastVaa = await postLiquidityLayerVaa( + const mockInvalidVaa = await createLiquidityLayerVaa( connection, - playerOne, - MOCK_GUARDIANS, ethRouter, wormholeSequence++, Buffer.from("deadbeef", "hex"), ); - const auction = await VaaAccount.fetch(connection, fastVaa).then((vaa) => - engine.auctionAddress(vaa.digest()), + const { address: fastVaa, account: account } = await postAndFetchVaa( + playerOneSigner, + engine, + mockInvalidVaa, ); + + const auction = engine.auctionAddress(account.digest()); await placeInitialOfferCctpForTest( { payer: playerOne.publicKey, fastVaa, auction, fromRouterEndpoint: engine.routerEndpointAddress(ethChain), - toRouterEndpoint: engine.routerEndpointAddress(arbChain), }, { args: { offerPrice: 69n, totalDeposit: 69n, }, - signers: [playerOne], - errorMsg: "Error Code: InvalidVaa", + signers: [playerOneSigner], + errorMsg: "Error: Invalid Liquidity Layer message", }, ); }); @@ -1523,7 +1462,7 @@ describe("Matching Engine", function () { payer: playerOne.publicKey, }, { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, errorMsg: "Error Code: Paused", @@ -1558,7 +1497,7 @@ describe("Matching Engine", function () { payer: playerOne.publicKey, }, { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, errorMsg: "Error Code: EndpointDisabled", @@ -1584,50 +1523,53 @@ describe("Matching Engine", function () { }); it("Cannot Place Initial Offer (Invalid Payload)", async function () { - const fastVaa = await postLiquidityLayerVaa( + const msg = new LiquidityLayerMessage({ + deposit: new LiquidityLayerDeposit({ + tokenAddress: toUniversalAddress(Array(32).fill(69)), + amount: 1000n, + sourceCctpDomain: 69, + destinationCctpDomain: 69, + cctpNonce: 6969n, + burnSource: toUniversalAddress(new Array(32).fill(69)), + mintRecipient: toUniversalAddress(Array(32).fill(69)), + payload: { + id: 1, + sourceChain: toChain(ethChain), + orderSender: baseFastOrder.sender, + redeemer: baseFastOrder.redeemer, + redeemerMessage: baseFastOrder.redeemerMessage, + }, + }), + }); + + const mockInvalidVaa = await createLiquidityLayerVaa( connection, - playerOne, - MOCK_GUARDIANS, ethRouter, wormholeSequence++, - new LiquidityLayerMessage({ - deposit: new LiquidityLayerDeposit({ - tokenAddress: toUniversalAddress(Array(32).fill(69)), - amount: 1000n, - sourceCctpDomain: 69, - destinationCctpDomain: 69, - cctpNonce: 6969n, - burnSource: toUniversalAddress(new Array(32).fill(69)), - mintRecipient: toUniversalAddress(Array(32).fill(69)), - payload: { - id: 1, - sourceChain: toChain(ethChain), - orderSender: baseFastOrder.sender, - redeemer: baseFastOrder.redeemer, - redeemerMessage: baseFastOrder.redeemerMessage, - }, - }), - }), + msg, ); - const auction = await VaaAccount.fetch(connection, fastVaa).then((vaa) => - engine.auctionAddress(vaa.digest()), + const { address: fastVaa, account } = await postAndFetchVaa( + playerOneSigner, + engine, + mockInvalidVaa, ); + + const auction = engine.auctionAddress(account.digest()); await placeInitialOfferCctpForTest( { payer: playerOne.publicKey, fastVaa, auction, fromRouterEndpoint: engine.routerEndpointAddress(ethChain), - toRouterEndpoint: engine.routerEndpointAddress(arbChain), }, { args: { offerPrice: 69n, totalDeposit: 69n, }, - signers: [playerOne], - errorMsg: "Error Code: NotFastMarketOrder", + signers: [playerOneSigner], + errorMsg: "Error: Message not FastMarketOrder", }, ); }); @@ -1644,7 +1586,7 @@ describe("Matching Engine", function () { payer: playerOne.publicKey, }, { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: { ...baseFastOrder, deadline }, errorMsg: "Error Code: FastMarketOrderExpired", @@ -1661,7 +1603,7 @@ describe("Matching Engine", function () { args: { offerPrice: baseFastOrder.maxFee + 1n, }, - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, errorMsg: "Error Code: OfferPriceTooHigh", @@ -1676,10 +1618,10 @@ describe("Matching Engine", function () { fromRouterEndpoint: engine.routerEndpointAddress(ethChain), }, { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, - sourceChain: "Polygon", + sourceChain: "Base", emitter: REGISTERED_TOKEN_ROUTERS["Ethereum"], errorMsg: "Error Code: InvalidSourceRouter", }, @@ -1695,7 +1637,7 @@ describe("Matching Engine", function () { args: { offerPrice: baseFastOrder.maxFee + 1n, }, - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, sourceChain: "Ethereum", @@ -1705,26 +1647,25 @@ describe("Matching Engine", function () { ); }); - it("Cannot Place Initial Offer (Invalid Target Router Chain)", async function () { - await placeInitialOfferCctpForTest( - { - payer: playerOne.publicKey, - toRouterEndpoint: engine.routerEndpointAddress(arbChain), - }, - { - args: { - offerPrice: baseFastOrder.maxFee + 1n, - }, - signers: [playerOne], - finalized: false, - fastMarketOrder: { - ...baseFastOrder, - targetChain: "Acala", - }, - errorMsg: "Error Code: InvalidTargetRouter", - }, - ); - }); + // TODO: Ben -- the following test is failing because we no longer pass the accounts in during + // the initial offer fn call. + // it("Cannot Place Initial Offer (Invalid Target Router Chain)", async function () { + // await placeInitialOfferCctpForTest( + // { payer: playerOne.publicKey, toRouterEndpoint: engine.routerEndpointAddress(ethChain) }, + // { + // args: { + // offerPrice: baseFastOrder.maxFee - 1n, + // }, + // signers: [playerOneSigner], + // finalized: false, + // fastMarketOrder: { + // ...baseFastOrder, + // targetChain: "Base", + // }, + // errorMsg: "Error Code: InvalidTargetRouter", + // }, + // ); + // }); it("Cannot Place Initial Offer Again", async function () { const result = await placeInitialOfferCctpForTest( @@ -1732,7 +1673,7 @@ describe("Matching Engine", function () { payer: playerOne.publicKey, }, { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, @@ -1744,7 +1685,7 @@ describe("Matching Engine", function () { fastVaa: result!.fastVaa, }, { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, errorMsg: `Allocate: account Address { address: ${result!.auction.toString()}, base: None } already in use`, @@ -1757,16 +1698,14 @@ describe("Matching Engine", function () { for (const newOffer of [0n, baseFastOrder.maxFee / 2n]) { it(`Improve Offer (Price == ${newOffer})`, async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, - }, - { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, ); - const { auction, auctionDataBefore } = result!; + const { auction, auctionDataBefore, fastVaaAccount } = result!; const initialOfferBalanceBefore = await getUsdcAtaBalance( connection, @@ -1778,15 +1717,13 @@ describe("Matching Engine", function () { ); const { amount: custodyBalanceBefore } = await engine.fetchCctpMintRecipient(); - const [approveIx, ix] = await engine.improveOfferIx( - { - auction, - participant: playerTwo.publicKey, - }, - { offerPrice: newOffer }, + const txs = engine.improveOffer( + playerTwo.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + newOffer, ); - await expectIxOk(connection, [approveIx, ix], [playerTwo]); + await expectTxsOk(playerTwoSigner, txs); await checkAfterEffects( auction, @@ -1804,16 +1741,14 @@ describe("Matching Engine", function () { it("Improve Offer (Tx)", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, - }, - { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, ); - const { auction, auctionDataBefore } = result!; + const { auction, auctionDataBefore, fastVaaAccount } = result!; const currentOffer = BigInt(auctionDataBefore.info!.offerPrice.toString()); const newOffer = @@ -1846,16 +1781,14 @@ describe("Matching Engine", function () { it("Improve Offer By Min Offer Delta", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, - }, - { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, ); - const { auction, auctionDataBefore } = result!; + const { auction, auctionDataBefore, fastVaaAccount } = result!; const currentOffer = BigInt(auctionDataBefore.info!.offerPrice.toString()); const newOffer = @@ -1871,15 +1804,13 @@ describe("Matching Engine", function () { ); const { amount: custodyBalanceBefore } = await engine.fetchCctpMintRecipient(); - const [approveIx, ix] = await engine.improveOfferIx( - { - auction, - participant: playerTwo.publicKey, - }, - { offerPrice: newOffer }, + const txs = engine.improveOffer( + playerTwo.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + newOffer, ); - await expectIxOk(connection, [approveIx, ix], [playerTwo]); + await expectTxsOk(playerTwoSigner, txs); await checkAfterEffects(auction, playerTwo.publicKey, newOffer, auctionDataBefore, { custodyToken: custodyBalanceBefore, @@ -1890,16 +1821,14 @@ describe("Matching Engine", function () { it("Improve Offer With Same Best Offer Token Account", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, - }, - { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, ); - const { auction, auctionDataBefore } = result!; + const { auction, auctionDataBefore, fastVaaAccount } = result!; const initialOfferBalanceBefore = await getUsdcAtaBalance( connection, @@ -1911,15 +1840,13 @@ describe("Matching Engine", function () { const currentOffer = BigInt(auctionDataBefore.info!.offerPrice.toString()); const newOffer = currentOffer - (await engine.computeMinOfferDelta(currentOffer)); - const [approveIx, ix] = await engine.improveOfferIx( - { - auction, - participant: playerOne.publicKey, - }, - { offerPrice: newOffer }, + const txs = engine.improveOffer( + playerOne.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + newOffer, ); - await expectIxOk(connection, [approveIx, ix], [playerOne]); + await expectTxsOk(playerOneSigner, txs); await checkAfterEffects(auction, playerOne.publicKey, newOffer, auctionDataBefore, { custodyToken: custodyBalanceBefore, @@ -1929,16 +1856,14 @@ describe("Matching Engine", function () { it("Cannot Improve Offer (Auction Expired)", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, - }, - { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, ); - const { auction, auctionDataBefore } = result!; + const { auctionDataBefore, fastVaaAccount } = result!; const { startSlot, offerPrice } = auctionDataBefore.info!; const { duration, gracePeriod } = await engine.fetchAuctionParameters(); @@ -1950,29 +1875,20 @@ describe("Matching Engine", function () { // New Offer from playerOne. const newOffer = BigInt(offerPrice.subn(100).toString()); - const [approveIx, ix] = await engine.improveOfferIx( - { - auction, - participant: playerOne.publicKey, - }, - { offerPrice: newOffer }, + const txs = engine.improveOffer( + playerOne.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + newOffer, ); - await expectIxErr( - connection, - [approveIx, ix], - [playerOne], - "Error Code: AuctionPeriodExpired", - ); + await expectTxsErr(playerOneSigner, txs, "Error Code: AuctionPeriodExpired"); }); it("Cannot Improve Offer (Invalid Best Offer Token Account)", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, - }, - { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, @@ -2000,33 +1916,24 @@ describe("Matching Engine", function () { it("Cannot Improve Offer (Carping Not Allowed)", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, - }, - { - signers: [playerOne], + signers: [playerOneSigner], finalized: false, fastMarketOrder: baseFastOrder, }, ); - const { auction, auctionDataBefore } = result!; + const { auctionDataBefore, fastVaaAccount } = result!; // Attempt to improve by the minimum allowed. const newOffer = BigInt(auctionDataBefore.info!.offerPrice.toString()) - 1n; - const [approveIx, ix] = await engine.improveOfferIx( - { - auction, - participant: playerTwo.publicKey, - }, - { offerPrice: newOffer }, + const txs = engine.improveOffer( + playerTwo.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + newOffer, ); - await expectIxErr( - connection, - [approveIx, ix], - [playerTwo], - "Error Code: CarpingNotAllowed", - ); + await expectTxsErr(playerTwoSigner, txs, "Error Code: CarpingNotAllowed"); }); async function checkAfterEffects( @@ -2131,12 +2038,14 @@ describe("Matching Engine", function () { // Start the auction with offer two so that we can // check that the initial offer is refunded. const result = await placeInitialOfferCctpForTest( + { payer: playerTwo.publicKey }, { - payer: playerTwo.publicKey, + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + const { fastVaaAccount, auction, auctionDataBefore: initialData } = result!; const improveBy = Number( await engine.computeMinOfferDelta( @@ -2144,7 +2053,7 @@ describe("Matching Engine", function () { ), ); const { auctionDataBefore } = await improveOfferForTest( - auction, + fastVaaAccount, playerOne, improveBy, ); @@ -2167,16 +2076,11 @@ describe("Matching Engine", function () { auctionDataBefore.info!.startSlot.addn(duration + gracePeriod - 1).toNumber(), ); - const ix = await engine.executeFastOrderCctpIx({ - payer: playerOne.publicKey, - fastVaa, - }); - - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); - - const txDetails = await expectIxOkDetails(connection, [computeIx, ix], [playerOne]); + const txs = engine.executeFastOrder( + playerOne.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + ); + const txDetails = await expectTxsOkDetails(playerOneSigner, txs, connection); await checkAfterEffects( txDetails!, @@ -2198,12 +2102,14 @@ describe("Matching Engine", function () { it("Execute Fast Order (Tx)", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerTwo.publicKey }, { - payer: playerTwo.publicKey, + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + const { fastVaaAccount, auctionDataBefore: initialData } = result!; const { duration, gracePeriod } = await engine.fetchAuctionParameters(); await waitUntilSlot( @@ -2211,28 +2117,12 @@ describe("Matching Engine", function () { initialData.info!.startSlot.addn(duration + gracePeriod - 1).toNumber(), ); - const { value: lookupTableAccount } = await connection.getAddressLookupTable( - lookupTableAddress, - ); - - const tx = await engine.executeFastOrderTx( - { payer: playerTwo.publicKey, fastVaa, auction }, - [playerTwo], - { - feeMicroLamports: 10, - computeUnits: 400_000, - addressLookupTableAccounts: [lookupTableAccount!], - }, - { commitment: "confirmed" }, + const txs = engine.executeFastOrder( + playerTwo.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), ); - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 400_000, - }); - - await expectIxOkDetails(connection, [computeIx, ...tx.ixs], [playerTwo], { - addressLookupTableAccounts: [lookupTableAccount!], - }); + await expectTxsOkDetails(playerTwoSigner, txs, connection); }); it("Reclaim by Closing CCTP Message", async function () { @@ -2250,10 +2140,7 @@ describe("Matching Engine", function () { const cctpAttestation = new CircleAttester().createAttestation(message); const ix = await engine.reclaimCctpMessageIx( - { - payer: playerOne.publicKey, - cctpMessage, - }, + { payer: playerOne.publicKey, cctpMessage }, cctpAttestation, ); @@ -2289,12 +2176,14 @@ describe("Matching Engine", function () { // Start the auction with offer two so that we can // check that the initial offer is refunded. const result = await placeInitialOfferCctpForTest( + { payer: playerTwo.publicKey }, { - payer: playerTwo.publicKey, + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + const { fastVaaAccount, auction, auctionDataBefore: initialData } = result!; const improveBy = Number( await engine.computeMinOfferDelta( @@ -2302,7 +2191,7 @@ describe("Matching Engine", function () { ), ); const { auctionDataBefore } = await improveOfferForTest( - auction, + fastVaaAccount, playerOne, improveBy, ); @@ -2328,16 +2217,12 @@ describe("Matching Engine", function () { .toNumber(), ); - const ix = await engine.executeFastOrderCctpIx({ - payer: playerOne.publicKey, - fastVaa, - }); - - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); + const txs = engine.executeFastOrder( + playerOne.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + ); - const txDetails = await expectIxOkDetails(connection, [computeIx, ix], [playerOne]); + const txDetails = await expectTxsOkDetails(playerOneSigner, txs, connection); await checkAfterEffects( txDetails!, @@ -2359,12 +2244,14 @@ describe("Matching Engine", function () { // Start the auction with offer two so that we can // check that the initial offer is refunded. const result = await placeInitialOfferCctpForTest( + { payer: playerTwo.publicKey }, { - payer: playerTwo.publicKey, + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + const { fastVaaAccount, auction, auctionDataBefore: initialData } = result!; const improveBy = Number( await engine.computeMinOfferDelta( @@ -2372,7 +2259,7 @@ describe("Matching Engine", function () { ), ); const { auctionDataBefore } = await improveOfferForTest( - auction, + fastVaaAccount, playerOne, improveBy, ); @@ -2407,24 +2294,11 @@ describe("Matching Engine", function () { .toNumber(), ); - const ix = await engine.executeFastOrderCctpIx({ - payer: liquidator.publicKey, - fastVaa, - }); - - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); - - const { value: lookupTableAccount } = await connection.getAddressLookupTable( - lookupTableAddress, - ); - const txDetails = await expectIxOkDetails( - connection, - [computeIx, ix], - [liquidator], - { addressLookupTableAccounts: [lookupTableAccount!] }, + const txs = engine.executeFastOrder( + liquidator.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), ); + const txDetails = await expectTxsOkDetails(liquidatorSigner, txs, connection); await checkAfterEffects( txDetails!, @@ -2445,6 +2319,7 @@ describe("Matching Engine", function () { it("Execute Fast Order After Grace Period with Liquidator (Initial Offer Token is Closed)", async function () { const tmpOwner = Keypair.generate(); + const { signer: tmpOwnerSiner } = getSdkSigner(connection, tmpOwner); const transferLamportsToTmpOwnerIx = SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: tmpOwner.publicKey, @@ -2477,12 +2352,11 @@ describe("Matching Engine", function () { // Place the initial offer with a token account that will be closed. const result = await placeInitialOfferCctpForTest( - { - payer: tmpOwner.publicKey, - }, - { signers: [tmpOwner], finalized: false, fastMarketOrder: baseFastOrder }, + { payer: tmpOwner.publicKey }, + { signers: [tmpOwnerSiner], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + + const { fastVaaAccount, auction, auctionDataBefore: initialData } = result!; // Burn funds out and close tmp ATA. const { amount: burnAmount } = await splToken.getAccount(connection, tmpAta); @@ -2505,7 +2379,7 @@ describe("Matching Engine", function () { ), ); const { auctionDataBefore } = await improveOfferForTest( - auction, + fastVaaAccount, playerOne, improveBy, ); @@ -2540,25 +2414,12 @@ describe("Matching Engine", function () { .toNumber(), ); - const ix = await engine.executeFastOrderCctpIx({ - payer: liquidator.publicKey, - fastVaa, - initialParticipant: payer.publicKey, - }); - - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); - - const { value: lookupTableAccount } = await connection.getAddressLookupTable( - lookupTableAddress, - ); - const txDetails = await expectIxOkDetails( - connection, - [computeIx, ix], - [liquidator], - { addressLookupTableAccounts: [lookupTableAccount!] }, + const txs = engine.executeFastOrder( + liquidator.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + payer.publicKey, ); + const txDetails = await expectTxsOkDetails(liquidatorSigner, txs, connection); await checkAfterEffects( txDetails!, @@ -2584,9 +2445,13 @@ describe("Matching Engine", function () { { payer: playerTwo.publicKey, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, + { + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, + }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + const { fastVaaAccount, auction, auctionDataBefore: initialData } = result!; const tmpOwner = Keypair.generate(); const transferLamportsToTmpOwnerIx = SystemProgram.transfer({ @@ -2627,7 +2492,7 @@ describe("Matching Engine", function () { ), ); const { auctionDataBefore } = await improveOfferForTest( - auction, + fastVaaAccount, tmpOwner, improveBy, ); @@ -2677,25 +2542,13 @@ describe("Matching Engine", function () { .toNumber(), ); - const ix = await engine.executeFastOrderCctpIx({ - payer: liquidator.publicKey, - fastVaa, - }); - - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); - - const { value: lookupTableAccount } = await connection.getAddressLookupTable( - lookupTableAddress, - ); - const txDetails = await expectIxOkDetails( - connection, - [computeIx, ix], - [liquidator], - { addressLookupTableAccounts: [lookupTableAccount!] }, + const txs = engine.executeFastOrder( + liquidator.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), ); + const txDetails = await expectTxsOkDetails(liquidatorSigner, txs, connection); + await checkAfterEffects( txDetails!, auction, @@ -2717,12 +2570,14 @@ describe("Matching Engine", function () { // Start the auction with offer two so that we can // check that the initial offer is refunded. const result = await placeInitialOfferCctpForTest( + { payer: playerTwo.publicKey }, { - payer: playerTwo.publicKey, + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + const { auction, fastVaaAccount, auctionDataBefore: initialData } = result!; const tmpOwner = Keypair.generate(); const transferLamportsToTmpOwnerIx = SystemProgram.transfer({ @@ -2761,7 +2616,7 @@ describe("Matching Engine", function () { ), ); const { auctionDataBefore: afterFirstImprovedData } = await improveOfferForTest( - auction, + fastVaaAccount, tmpOwner, improveBy, ); @@ -2787,7 +2642,7 @@ describe("Matching Engine", function () { ), ); const { auctionDataBefore } = await improveOfferForTest( - auction, + fastVaaAccount, playerOne, improveByAgain, ); @@ -2821,24 +2676,11 @@ describe("Matching Engine", function () { .toNumber(), ); - const ix = await engine.executeFastOrderCctpIx({ - payer: liquidator.publicKey, - fastVaa, - }); - - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); - - const { value: lookupTableAccount } = await connection.getAddressLookupTable( - lookupTableAddress, - ); - const txDetails = await expectIxOkDetails( - connection, - [computeIx, ix], - [liquidator], - { addressLookupTableAccounts: [lookupTableAccount!] }, + const txs = engine.executeFastOrder( + liquidator.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), ); + const txDetails = await expectTxsOkDetails(liquidatorSigner, txs, connection); await checkAfterEffects( txDetails!, @@ -2864,9 +2706,13 @@ describe("Matching Engine", function () { { payer: playerTwo.publicKey, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, + { + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, + }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + const { fastVaaAccount, auction, auctionDataBefore: initialData } = result!; const improveBy = Number( await engine.computeMinOfferDelta( @@ -2874,7 +2720,7 @@ describe("Matching Engine", function () { ), ); const { auctionDataBefore } = await improveOfferForTest( - auction, + fastVaaAccount, playerOne, improveBy, ); @@ -2909,25 +2755,13 @@ describe("Matching Engine", function () { .toNumber(), ); - const ix = await engine.executeFastOrderCctpIx({ - payer: liquidator.publicKey, - fastVaa, - }); - - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); - - const { value: lookupTableAccount } = await connection.getAddressLookupTable( - lookupTableAddress, - ); - const txDetails = await expectIxOkDetails( - connection, - [computeIx, ix], - [liquidator], - { addressLookupTableAccounts: [lookupTableAccount!] }, + const txs = engine.executeFastOrder( + liquidator.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), ); + const txDetails = await expectTxsOkDetails(liquidatorSigner, txs, connection); + await checkAfterEffects( txDetails!, auction, @@ -2947,10 +2781,12 @@ describe("Matching Engine", function () { it("Cannot Execute Fast Order (Endpoint Disabled)", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, + signers: [playerOneSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerOne], finalized: false, fastMarketOrder: baseFastOrder }, ); const { fastVaa, auctionDataBefore } = result!; @@ -2998,7 +2834,11 @@ describe("Matching Engine", function () { { payer: playerTwo.publicKey, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, + { + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, + }, ); const { fastVaaAccount, auction, auctionDataBefore } = result!; @@ -3006,7 +2846,7 @@ describe("Matching Engine", function () { { payer: playerTwo.publicKey, }, - { signers: [playerTwo], fastMarketOrder: baseFastOrder }, + { signers: [playerTwoSigner], fastMarketOrder: baseFastOrder }, ); const { fastVaa: anotherFastVaa, fastVaaAccount: anotherFastVaaAccount } = anotherResult!; @@ -3032,7 +2872,11 @@ describe("Matching Engine", function () { { payer: playerOne.publicKey, }, - { signers: [playerOne], finalized: false, fastMarketOrder: baseFastOrder }, + { + signers: [playerOneSigner], + finalized: false, + fastMarketOrder: baseFastOrder, + }, ); const { fastVaa, auctionDataBefore } = result!; @@ -3061,7 +2905,11 @@ describe("Matching Engine", function () { { payer: playerOne.publicKey, }, - { signers: [playerOne], finalized: false, fastMarketOrder: baseFastOrder }, + { + signers: [playerOneSigner], + finalized: false, + fastMarketOrder: baseFastOrder, + }, ); const { fastVaa, auctionDataBefore } = result!; @@ -3095,12 +2943,14 @@ describe("Matching Engine", function () { // Start the auction with offer two so that we can // check that the initial offer is refunded. const result = await placeInitialOfferCctpForTest( + { payer: playerTwo.publicKey }, { - payer: playerTwo.publicKey, + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa, auctionDataBefore } = result!; + const { auctionDataBefore, fastVaaAccount } = result!; const { duration, gracePeriod } = await engine.fetchAuctionParameters(); await waitUntilSlot( @@ -3108,51 +2958,46 @@ describe("Matching Engine", function () { auctionDataBefore.info!.startSlot.addn(duration + gracePeriod - 1).toNumber(), ); - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 250_000, - }); - const ix = await engine.executeFastOrderCctpIx({ - payer: playerOne.publicKey, - fastVaa, - }); + localVariables.set("acct", fastVaaAccount); - await expectIxOk(connection, [computeIx, ix], [playerOne]); + const txs = engine.executeFastOrder( + playerOne.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + ); - localVariables.set("ix", ix); + await expectTxsOk(playerOneSigner, txs); }); it("Cannot Execute Fast Order on Auction Completed", async function () { - const ix = localVariables.get("ix") as TransactionInstruction; - expect(localVariables.delete("ix")).is.true; - - await expectIxErr( - connection, - [ix], - [playerOne], + const fastVaaAccount = localVariables.get("acct") as VaaAccount; + const txs = engine.executeFastOrder( + playerOne.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + ); + await expectTxsErr( + playerOneSigner, + txs, "custody_token. Error Code: AccountNotInitialized", ); }); it("Cannot Execute Fast Order (Auction Period Not Expired)", async function () { const result = await placeInitialOfferCctpForTest( + { payer: playerOne.publicKey }, { - payer: playerOne.publicKey, + signers: [playerOneSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerOne], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa } = result!; + const { fastVaaAccount } = result!; - const ix = await engine.executeFastOrderCctpIx({ - payer: playerOne.publicKey, - fastVaa, - }); - - await expectIxErr( - connection, - [ix], - [playerOne], - "Error Code: AuctionPeriodNotExpired", + const txs = engine.executeFastOrder( + playerOne.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), ); + + await expectTxsErr(playerOneSigner, txs, "Error Code: AuctionPeriodNotExpired"); }); async function checkAfterEffects( @@ -3396,6 +3241,7 @@ describe("Matching Engine", function () { }); const fastEmitterInfo = fast.vaaAccount.emitterInfo(); const finalizedEmitterInfo = finalized!.vaaAccount.emitterInfo(); + expect(fastEmitterInfo.chain).not.equals(finalizedEmitterInfo.chain); await prepareOrderResponseCctpForTest( @@ -3488,9 +3334,7 @@ describe("Matching Engine", function () { it("Prepare Order Response", async function () { const result = await prepareOrderResponseCctpForTest( - { - payer: payer.publicKey, - }, + { payer: payer.publicKey }, { placeInitialOffer: false, }, @@ -3529,34 +3373,22 @@ describe("Matching Engine", function () { it("Prepare Order Response for Active Auction", async function () { await prepareOrderResponseCctpForTest( - { - payer: payer.publicKey, - }, - { - executeOrder: false, - }, + { payer: payer.publicKey }, + { executeOrder: false }, ); }); it("Prepare Order Response for Completed Auction Within Grace Period", async function () { await prepareOrderResponseCctpForTest( - { - payer: payer.publicKey, - }, - { - executeWithinGracePeriod: true, - }, + { payer: payer.publicKey }, + { executeWithinGracePeriod: true }, ); }); it("Prepare Order Response for Completed Auction After Grace Period", async function () { await prepareOrderResponseCctpForTest( - { - payer: payer.publicKey, - }, - { - executeWithinGracePeriod: false, - }, + { payer: payer.publicKey }, + { executeWithinGracePeriod: false }, ); }); }); @@ -3565,20 +3397,15 @@ describe("Matching Engine", function () { describe("Auction Complete", function () { it("Cannot Settle Non-Existent Auction", async function () { const result = await prepareOrderResponseCctpForTest( - { - payer: payer.publicKey, - }, - { - placeInitialOffer: false, - }, + { payer: payer.publicKey }, + { placeInitialOffer: false }, ); - const fastVaaAccount = await VaaAccount.fetch(connection, result!.fastVaa); await settleAuctionCompleteForTest( { executor: payer.publicKey, preparedOrderResponse: result!.preparedOrderResponse, - auction: engine.auctionAddress(fastVaaAccount.digest()), + auction: engine.auctionAddress(result!.fastVaaAccount.digest()), bestOfferToken: splToken.getAssociatedTokenAddressSync( USDC_MINT_ADDRESS, payer.publicKey, @@ -3593,11 +3420,9 @@ describe("Matching Engine", function () { it("Cannot Settle Active Auction", async function () { await settleAuctionCompleteForTest( + { executor: payer.publicKey }, { - executor: payer.publicKey, - }, - { - prepareSigners: [payer], + prepareSigners: [payerSigner], executeOrder: false, errorMsg: "Error Code: AuctionNotCompleted", }, @@ -3606,11 +3431,9 @@ describe("Matching Engine", function () { it("Cannot Settle Completed Auction with No Penalty (Executor != Best Offer)", async function () { await settleAuctionCompleteForTest( + { executor: payer.publicKey }, { - executor: payer.publicKey, - }, - { - prepareSigners: [payer], + prepareSigners: [payerSigner], executeWithinGracePeriod: true, errorMsg: "Error Code: ExecutorTokenMismatch", }, @@ -3619,11 +3442,9 @@ describe("Matching Engine", function () { it("Settle Completed without Penalty", async function () { await settleAuctionCompleteForTest( + { executor: playerOne.publicKey }, { - executor: playerOne.publicKey, - }, - { - prepareSigners: [playerOne], + prepareSigners: [playerOneSigner], executeWithinGracePeriod: true, }, ); @@ -3631,11 +3452,9 @@ describe("Matching Engine", function () { it("Settle Completed With Order Response Prepared Before Active Auction", async function () { await settleAuctionCompleteForTest( + { executor: playerOne.publicKey }, { - executor: playerOne.publicKey, - }, - { - prepareSigners: [playerOne], + prepareSigners: [playerOneSigner], executeWithinGracePeriod: true, prepareAfterExecuteOrder: false, }, @@ -3644,9 +3463,7 @@ describe("Matching Engine", function () { it("Cannot Settle Completed with Penalty (Executor != Prepared By)", async function () { await settleAuctionCompleteForTest( - { - executor: playerOne.publicKey, - }, + { executor: playerOne.publicKey }, { executeWithinGracePeriod: false, executorIsPreparer: false, @@ -3657,11 +3474,9 @@ describe("Matching Engine", function () { it("Settle Completed with Penalty (Executor == Best Offer)", async function () { await settleAuctionCompleteForTest( + { executor: playerOne.publicKey }, { - executor: playerOne.publicKey, - }, - { - prepareSigners: [playerOne], + prepareSigners: [playerOneSigner], executeWithinGracePeriod: false, }, ); @@ -3693,12 +3508,9 @@ describe("Matching Engine", function () { ); await settleAuctionCompleteForTest( + { executor: playerTwo.publicKey, executorToken }, { - executor: playerTwo.publicKey, - executorToken, - }, - { - prepareSigners: [playerTwo], + prepareSigners: [playerTwoSigner], executeWithinGracePeriod: false, errorMsg: "Error Code: AccountNotAssociatedTokenAccount", }, @@ -3707,11 +3519,9 @@ describe("Matching Engine", function () { it("Settle Completed with Penalty (Executor != Best Offer)", async function () { await settleAuctionCompleteForTest( + { executor: playerTwo.publicKey }, { - executor: playerTwo.publicKey, - }, - { - prepareSigners: [playerTwo], + prepareSigners: [playerTwoSigner], executeWithinGracePeriod: false, }, ); @@ -3723,13 +3533,14 @@ describe("Matching Engine", function () { }); const result = await placeInitialOfferCctpForTest( + { payer: playerTwo.publicKey, fastVaa: fast.vaa }, { - payer: playerTwo.publicKey, - fastVaa: fast.vaa, + signers: [playerTwoSigner], + finalized: false, + fastMarketOrder: baseFastOrder, }, - { signers: [playerTwo], finalized: false, fastMarketOrder: baseFastOrder }, ); - const { fastVaa, auction, auctionDataBefore: initialData } = result!; + const { auctionDataBefore: initialData } = result!; const { duration, gracePeriod } = await engine.fetchAuctionParameters(); await waitUntilSlot( @@ -3737,49 +3548,31 @@ describe("Matching Engine", function () { initialData.info!.startSlot.addn(duration + gracePeriod - 1).toNumber(), ); - const { value: lookupTableAccount } = await connection.getAddressLookupTable( - lookupTableAddress, - ); - - const tx = await engine.executeFastOrderTx( - { payer: playerTwo.publicKey, fastVaa, auction }, - [playerTwo], - { - feeMicroLamports: 10, - computeUnits: 300_000, - addressLookupTableAccounts: [lookupTableAccount!], - }, - { commitment: "confirmed" }, + const txs = engine.executeFastOrder( + playerTwo.publicKey, + fast.vaaAccount.vaa("FastTransfer:FastMarketOrder"), ); - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); + await expectTxsOkDetails(playerTwoSigner, txs, connection); - await expectIxOkDetails(connection, [computeIx, ...tx.ixs], [playerTwo], { - addressLookupTableAccounts: [lookupTableAccount!], - }); + const { encodedCctpMessage, cctpAttestation } = finalized!.cctp; + const [cctpMessage] = CircleBridge.deserialize(encodedCctpMessage); - const tx2 = await engine.settleAuctionCompleteTx( - { - executor: playerTwo.publicKey, - auction, - fastVaa, - finalizedVaa: finalized!.vaa, - bestOfferToken: initialData.info!.bestOfferToken, - }, - finalized!.cctp, - [playerTwo], + const { value: lookupTableAccount } = await connection.getAddressLookupTable( + lookupTableAddress, + ); + const tx2 = engine.settleOrder( + playerTwo.publicKey, + fast.vaaAccount.vaa("FastTransfer:FastMarketOrder"), + finalized!.vaaAccount.vaa("FastTransfer:CctpDeposit"), { - feeMicroLamports: 10, - computeUnits: 300_000, - addressLookupTableAccounts: [lookupTableAccount!], + message: cctpMessage, + attestation: cctpAttestation.toString("hex"), }, - { commitment: "confirmed" }, + [lookupTableAccount!], ); - await expectIxOkDetails(connection, [computeIx, ...tx2.ixs], [playerTwo], { - addressLookupTableAccounts: [lookupTableAccount!], - }); + + await expectTxsOkDetails(playerTwoSigner, tx2, connection); }); }); @@ -3832,9 +3625,7 @@ describe("Matching Engine", function () { payer: payer.publicKey, firstHistory: Keypair.generate().publicKey, }, - { - errorMsg: "Error Code: ConstraintSeeds", - }, + { errorMsg: "Error Code: ConstraintSeeds" }, ); }); @@ -3848,9 +3639,7 @@ describe("Matching Engine", function () { const auctionHistory = engine.auctionHistoryAddress(0); await createFirstAuctionHistoryForTest( - { - payer: payer.publicKey, - }, + { payer: payer.publicKey }, { errorMsg: `Allocate: account Address { address: ${auctionHistory.toString()}, base: None } already in use`, }, @@ -3859,10 +3648,8 @@ describe("Matching Engine", function () { it("Cannot Add Entry from Unsettled Auction", async function () { const result = await placeInitialOfferCctpForTest( - { - payer: playerOne.publicKey, - }, - { signers: [playerOne], finalized: false }, + { payer: playerOne.publicKey }, + { signers: [playerOneSigner], finalized: false }, ); await addAuctionHistoryEntryForTest( @@ -3872,18 +3659,13 @@ describe("Matching Engine", function () { auction: result!.auction, beneficiary: playerOne.publicKey, }, - { - errorMsg: "Error Code: AuctionNotSettled", - }, + { errorMsg: "Error Code: AuctionNotSettled" }, ); }); it("Cannot Add Entry from Settled Complete Auction Before Expiration Time", async function () { await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - }, + { payer: payer.publicKey, history: engine.auctionHistoryAddress(0) }, { settlementType: "complete", waitToExpiration: false, @@ -3923,22 +3705,14 @@ describe("Matching Engine", function () { it("Add Entry from Settled Complete Auction After Expiration Time", async function () { await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - }, - { - settlementType: "complete", - }, + { payer: payer.publicKey, history: engine.auctionHistoryAddress(0) }, + { settlementType: "complete" }, ); }); it("Cannot Close Auction Account from Settled Auction None Before Expiration Time", async function () { await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - }, + { payer: payer.publicKey, history: engine.auctionHistoryAddress(0) }, { settlementType: "none", waitToExpiration: false, @@ -3976,46 +3750,29 @@ describe("Matching Engine", function () { it("Close Auction Account from Settled Auction None", async function () { await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - }, + { payer: payer.publicKey, history: engine.auctionHistoryAddress(0) }, { settlementType: "none" }, ); }); it("Cannot Create New Auction History with Current History Not Full", async function () { await createNewAuctionHistoryForTest( - { - payer: payer.publicKey, - currentHistory: engine.auctionHistoryAddress(0), - }, + { payer: payer.publicKey, currentHistory: engine.auctionHistoryAddress(0) }, { errorMsg: "Error Code: AuctionHistoryNotFull" }, ); }); it("Add Another Entry from Settled Complete Auction", async function () { await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - }, - { - settlementType: "complete", - }, + { payer: payer.publicKey, history: engine.auctionHistoryAddress(0) }, + { settlementType: "complete" }, ); }); it("Cannot Add Another Entry from Settled Complete Auction To Full History", async function () { await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - }, - { - settlementType: "complete", - errorMsg: "Error Code: AuctionHistoryFull", - }, + { payer: payer.publicKey, history: engine.auctionHistoryAddress(0) }, + { settlementType: "complete", errorMsg: "Error Code: AuctionHistoryFull" }, ); }); @@ -4028,21 +3785,16 @@ describe("Matching Engine", function () { it("Add Another Entry from Settled Complete Auction To New History", async function () { await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(1), - }, - { - settlementType: "complete", - }, + { payer: payer.publicKey, history: engine.auctionHistoryAddress(1) }, + { settlementType: "complete" }, ); }); async function createFirstAuctionHistoryForTest( accounts: { payer: PublicKey; firstHistory?: PublicKey }, - opts: ForTestOpts = {}, + opts: TestOptions = {}, ) { - let [{ signers, errorMsg }] = setDefaultForTestOpts(opts); + let [{ signers, errorMsg }] = addDefaultOptions(opts); const ix = await engine.program.methods .createFirstAuctionHistory() @@ -4050,7 +3802,7 @@ describe("Matching Engine", function () { .instruction(); if (errorMsg !== null) { - return expectIxErr(connection, [ix], signers, errorMsg); + return expectIxErr(connection, [ix], unwrapSigners(signers), errorMsg); } const auctionHistory = engine.auctionHistoryAddress(0); @@ -4059,7 +3811,7 @@ describe("Matching Engine", function () { expect(accInfo).is.null; } - await expectIxOk(connection, [ix], signers); + await expectIxOk(connection, [ix], unwrapSigners(signers)); const firstHistoryData = await engine.fetchAuctionHistory({ address: auctionHistory, @@ -4095,9 +3847,9 @@ describe("Matching Engine", function () { async function createNewAuctionHistoryForTest( accounts: { payer: PublicKey; currentHistory: PublicKey; newHistory?: PublicKey }, - opts: ForTestOpts = {}, + opts: TestOptions = {}, ) { - let [{ signers, errorMsg }] = setDefaultForTestOpts(opts); + let [{ signers, errorMsg }] = addDefaultOptions(opts); const definedAccounts = await createNewAuctionHistoryAccounts(accounts); @@ -4107,7 +3859,7 @@ describe("Matching Engine", function () { .instruction(); if (errorMsg !== null) { - return expectIxErr(connection, [ix], signers, errorMsg); + return expectIxErr(connection, [ix], unwrapSigners(signers), errorMsg); } const { newHistory } = definedAccounts; @@ -4116,7 +3868,7 @@ describe("Matching Engine", function () { expect(accInfo).is.null; } - await expectIxOk(connection, [ix], signers); + await expectIxOk(connection, [ix], unwrapSigners(signers)); const [{ id }, numEntries] = await engine.fetchAuctionHistoryHeader({ address: definedAccounts.currentHistory, @@ -4174,14 +3926,14 @@ describe("Matching Engine", function () { beneficiary?: PublicKey; beneficiaryToken?: PublicKey; }, - opts: ForTestOpts & + opts: TestOptions & ObserveCctpOrderVaasOpts & PrepareOrderResponseForTestOptionalOpts & { settlementType?: "complete" | "none" | null; waitToExpiration?: boolean; } = {}, ) { - let [{ signers, errorMsg }, excludedForTestOpts] = setDefaultForTestOpts(opts); + let [{ signers, errorMsg }, excludedForTestOpts] = addDefaultOptions(opts); let { settlementType, waitToExpiration } = excludedForTestOpts; settlementType ??= null; // Set timestamps to 2 seconds before auction expiration so we don't have to wait @@ -4200,7 +3952,7 @@ describe("Matching Engine", function () { if (settlementType == "complete") { const result = await settleAuctionCompleteForTest( { executor: playerOne.publicKey }, - { vaaTimestamp, prepareSigners: [playerOne] }, + { vaaTimestamp, prepareSigners: [playerOneSigner] }, ); return result!.auction; } else if (settlementType == "none") { @@ -4267,7 +4019,7 @@ describe("Matching Engine", function () { .instruction(); if (errorMsg !== null) { - return expectIxErr(connection, [ix], signers, errorMsg); + return expectIxErr(connection, [ix], unwrapSigners(signers), errorMsg); } const beneficiaryBalanceBefore = await connection.getBalance(beneficiary); @@ -4293,7 +4045,7 @@ describe("Matching Engine", function () { .getAccountInfo(accounts.history) .then((info) => info!.data.length); - await expectIxOk(connection, [ix], signers); + await expectIxOk(connection, [ix], unwrapSigners(signers)); const historyData = await engine.fetchAuctionHistory({ address: accounts.history, @@ -4334,6 +4086,24 @@ describe("Matching Engine", function () { }); }); + interface TestOptions { + signers?: SDKSigner<"Devnet">[]; + errorMsg?: string | null; + } + + function addDefaultOptions( + opts: T, + ): [{ signers: SDKSigner<"Devnet">[]; errorMsg: string | null }, Omit] { + let { signers, errorMsg } = opts; + signers ??= [payerSigner]; + delete opts.signers; + + errorMsg ??= null; + delete opts.errorMsg; + + return [{ signers, errorMsg }, { ...opts }]; + } + async function placeInitialOfferCctpForTest( accounts: { payer: PublicKey; @@ -4342,9 +4112,8 @@ describe("Matching Engine", function () { auction?: PublicKey; auctionConfig?: PublicKey; fromRouterEndpoint?: PublicKey; - toRouterEndpoint?: PublicKey; }, - opts: ForTestOpts & + opts: TestOptions & ObserveCctpOrderVaasOpts & { args?: { offerPrice?: bigint; @@ -4358,26 +4127,27 @@ describe("Matching Engine", function () { auction: PublicKey; auctionDataBefore: Auction; }> { - const [{ errorMsg, signers }, excludedForTestOpts] = setDefaultForTestOpts(opts); + const [{ errorMsg, signers }, excludedForTestOpts] = addDefaultOptions(opts); let { args } = excludedForTestOpts; args ??= {}; - const { fast, finalized } = await (async () => { - if (accounts.fastVaa !== undefined) { - const vaaAccount = await VaaAccount.fetch(connection, accounts.fastVaa); - return { fast: { vaa: accounts.fastVaa, vaaAccount }, finalized: undefined }; - } else { - return observeCctpOrderVaas(excludedForTestOpts); - } - })(); - + const { fast } = + accounts.fastVaa !== undefined + ? { + fast: { + vaa: accounts.fastVaa, + vaaAccount: await VaaAccount.fetch(connection, accounts.fastVaa), + }, + } + : await observeCctpOrderVaas(excludedForTestOpts); + + let fastMarketOrderVAA: VAA<"Uint8Array"> | VAA<"FastTransfer:FastMarketOrder">; try { - const { fastMarketOrder } = LiquidityLayerMessage.decode(fast.vaaAccount.payload()); - if (fastMarketOrder !== undefined) { - args.offerPrice ??= fastMarketOrder!.maxFee; - } + fastMarketOrderVAA = fast.vaaAccount.vaa("FastTransfer:FastMarketOrder"); + args.offerPrice ??= fastMarketOrderVAA.payload.maxFee; } catch (e) { - // Ignore if parsing failed. + // Ignore if parsing failed, this will be a VAA<"Uint8Array"> + fastMarketOrderVAA = fast.vaaAccount.vaa("Uint8Array"); } if (args.offerPrice === undefined) { @@ -4385,21 +4155,26 @@ describe("Matching Engine", function () { } // Place the initial offer. - const ixs = await engine.placeInitialOfferCctpIx( - { ...accounts, fastVaa: fast.vaa }, - { - offerPrice: args.offerPrice, - totalDeposit: args.totalDeposit, - }, + const txs = engine.placeInitialOffer( + accounts.payer, + // @ts-expect-error -- may still be a Uint8array payload for testing invalid VAA + fastMarketOrderVAA, + args.offerPrice, + args.totalDeposit, ); if (errorMsg !== null) { - return expectIxErr(connection, ixs, signers, errorMsg); + return expectTxsErr(signers[0], txs, errorMsg); } + // If we still have a Uint8Array, we failed to deserialize it earlier but it was accepted + // and not caught by the above error check. Why? + if (fastMarketOrderVAA.payloadLiteral === "Uint8Array") throw "Invalid VAA"; + const offerToken = accounts.offerToken ?? splToken.getAssociatedTokenAddressSync(USDC_MINT_ADDRESS, accounts.payer); + const { owner: participant } = await splToken.getAccount(connection, offerToken); expect(offerToken).to.eql( splToken.getAssociatedTokenAddressSync(USDC_MINT_ADDRESS, participant), @@ -4414,7 +4189,7 @@ describe("Matching Engine", function () { const auction = engine.auctionAddress(vaaHash); const auctionCustodyBalanceBefore = await engine.fetchAuctionCustodyTokenBalance(auction); - const txDetails = await expectIxOkDetails(connection, ixs, signers); + const txDetails = await expectTxsOkDetails(signers[0], txs, connection); if (txDetails === null) { throw new Error("Transaction details are null"); } @@ -4428,7 +4203,7 @@ describe("Matching Engine", function () { const auctionCustodyBalanceAfter = await engine.fetchAuctionCustodyTokenBalance(auction); - const { fastMarketOrder } = LiquidityLayerMessage.decode(fast.vaaAccount.payload()); + const fastMarketOrder = fastMarketOrderVAA.payload; expect(fastMarketOrder).is.not.undefined; const { amountIn, maxFee, targetChain, redeemerMessage } = fastMarketOrder!; @@ -4485,49 +4260,26 @@ describe("Matching Engine", function () { }; } - async function improveOfferForTest( - auction: PublicKey, - participant: Keypair, - improveBy: number, - ) { + async function improveOfferForTest(vaa: VaaAccount, participant: Keypair, improveBy: number) { + const auction = engine.auctionAddress(vaa.digest()); + const auctionData = await engine.fetchAuction({ address: auction }); const newOffer = uint64ToBigInt(auctionData.info!.offerPrice.subn(improveBy)); - const ixs = await engine.improveOfferIx( - { - auction, - participant: participant.publicKey, - }, - { offerPrice: newOffer }, + const txs = engine.improveOffer( + participant.publicKey, + vaa.vaa("FastTransfer:FastMarketOrder"), + newOffer, ); // Improve the bid with offer one. - await expectIxOk(connection, ixs, [participant]); + const { signer } = getSdkSigner(connection, participant); + await expectTxsOk(signer, txs); const auctionDataBefore = await engine.fetchAuction({ address: auction }); expect(uint64ToBigInt(auctionDataBefore.info!.offerPrice)).equals(newOffer); - return { - auctionDataBefore, - }; - } - - type ForTestOpts = { - signers?: Signer[]; - errorMsg?: string | null; - }; - - function setDefaultForTestOpts( - opts: T, - ): [{ signers: Signer[]; errorMsg: string | null }, Omit] { - let { signers, errorMsg } = opts; - signers ??= [payer]; - delete opts.signers; - - errorMsg ??= null; - delete opts.errorMsg; - - return [{ signers, errorMsg }, { ...opts }]; + return { auctionDataBefore }; } type PrepareOrderResponseForTestOptionalOpts = { @@ -4537,7 +4289,6 @@ describe("Matching Engine", function () { executeOrder?: boolean; executeWithinGracePeriod?: boolean; prepareAfterExecuteOrder?: boolean; - instructionOnly?: boolean; alreadyPrepared?: boolean; }; @@ -4547,15 +4298,16 @@ describe("Matching Engine", function () { fastVaa?: PublicKey; finalizedVaa?: PublicKey; }, - opts: ForTestOpts & ObserveCctpOrderVaasOpts & PrepareOrderResponseForTestOptionalOpts = {}, + opts: TestOptions & ObserveCctpOrderVaasOpts & PrepareOrderResponseForTestOptionalOpts = {}, ): Promise { - let [{ signers, errorMsg }, excludedForTestOpts] = setDefaultForTestOpts(opts); + let [{ signers, errorMsg }, excludedForTestOpts] = addDefaultOptions(opts); let { args, placeInitialOffer, @@ -4563,7 +4315,6 @@ describe("Matching Engine", function () { executeOrder, executeWithinGracePeriod, prepareAfterExecuteOrder, - instructionOnly, alreadyPrepared, } = excludedForTestOpts; placeInitialOffer ??= true; @@ -4571,18 +4322,19 @@ describe("Matching Engine", function () { executeOrder ??= placeInitialOffer; executeWithinGracePeriod ??= true; prepareAfterExecuteOrder ??= true; - instructionOnly ??= false; alreadyPrepared ??= false; - const { fastVaa, fastVaaAccount, finalizedVaa } = await (async () => { + const { fastVaa, fastVaaAccount, finalizedVaa, finalizedVaaAccount } = await (async () => { const { fastVaa, finalizedVaa } = accounts; if (fastVaa !== undefined && finalizedVaa !== undefined && args !== undefined) { const fastVaaAccount = await VaaAccount.fetch(connection, fastVaa); + const finalizedVaaAccount = await VaaAccount.fetch(connection, finalizedVaa); return { fastVaa, fastVaaAccount, finalizedVaa, + finalizedVaaAccount, }; } else if (fastVaa === undefined && finalizedVaa === undefined) { const { fast, finalized } = await observeCctpOrderVaas(excludedForTestOpts); @@ -4592,6 +4344,7 @@ describe("Matching Engine", function () { fastVaa: fast.vaa, fastVaaAccount: fast.vaaAccount, finalizedVaa: finalized!.vaa, + finalizedVaaAccount: finalized!.vaaAccount, }; } else { throw new Error( @@ -4607,13 +4360,8 @@ describe("Matching Engine", function () { const placeAndExecute = async () => { if (placeInitialOffer) { const result = await placeInitialOfferCctpForTest( - { - payer: playerOne.publicKey, - fastVaa, - }, - { - signers: [playerOne], - }, + { payer: playerOne.publicKey, fastVaa }, + { signers: [playerOneSigner] }, ); if (executeOrder) { @@ -4623,8 +4371,7 @@ describe("Matching Engine", function () { if (info === null) { throw new Error("No auction info found"); } - const { configId, bestOfferToken, initialOfferToken, startSlot } = info; - const auctionConfig = engine.auctionConfigAddress(configId); + const { configId, startSlot } = info; const { duration, gracePeriod, penaltyPeriod } = await engine.fetchAuctionParameters(configId); @@ -4640,21 +4387,12 @@ describe("Matching Engine", function () { await waitUntilSlot(connection, endSlot); - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); + const txs = engine.executeFastOrder( + payer.publicKey, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + ); - const ix = await engine.executeFastOrderCctpIx({ - payer: payer.publicKey, - fastVaa, - auction, - auctionConfig, - bestOfferToken, - initialOfferToken, - }); - await expectIxOk(connection, [computeIx, ix], [payer], { - addressLookupTableAccounts: [lookupTableAccount!], - }); + await expectTxsOk(payerSigner, txs); } } }; @@ -4663,20 +4401,18 @@ describe("Matching Engine", function () { await placeAndExecute(); } - const ix = await engine.prepareOrderResponseCctpIx( - { - payer: accounts.payer, - fastVaa, - finalizedVaa, - }, - args!, + const [cctpMessage] = CircleBridge.deserialize(new Uint8Array(args!.encodedCctpMessage!)); + const cctpAttestation = encoding.hex.encode(new Uint8Array(args!.cctpAttestation!)); + const txs = engine.prepareOrderResponse( + accounts.payer, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + finalizedVaaAccount.vaa("FastTransfer:CctpDeposit"), + { message: cctpMessage, attestation: cctpAttestation }, + [lookupTableAccount!], ); if (errorMsg !== null) { - expect(instructionOnly).is.false; - return expectIxErr(connection, [ix], signers, errorMsg, { - addressLookupTableAccounts: [lookupTableAccount!], - }); + return expectTxsErr(signers[0], txs, errorMsg); } const preparedOrderResponse = engine.preparedOrderResponseAddress(fastVaaAccount.digest()); @@ -4690,28 +4426,13 @@ describe("Matching Engine", function () { } })(); - if (instructionOnly) { - return { - fastVaa, - finalizedVaa, - args: args!, - preparedOrderResponse, - prepareOrderResponseInstruction: ix, - }; - } - const preparedCustodyToken = engine.preparedCustodyTokenAddress(preparedOrderResponse); { const accInfo = await connection.getAccountInfo(preparedCustodyToken); expect(accInfo !== null).equals(alreadyPrepared); } - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 280_000, - }); - await expectIxOk(connection, [computeIx, ix], signers, { - addressLookupTableAccounts: [lookupTableAccount!], - }); + await expectTxsOk(signers[0], txs); if (!prepareAfterExecuteOrder) { await placeAndExecute(); @@ -4722,13 +4443,12 @@ describe("Matching Engine", function () { }); const { seeds } = preparedOrderResponseData; - const finalizedVaaAccount = await VaaAccount.fetch(connection, finalizedVaa); - const { deposit } = LiquidityLayerMessage.decode(finalizedVaaAccount.payload()); - expect(deposit).is.not.undefined; - const { fastMarketOrder } = LiquidityLayerMessage.decode(fastVaaAccount.payload()); expect(fastMarketOrder).is.not.undefined; + const { deposit } = LiquidityLayerMessage.decode(finalizedVaaAccount.payload()); + expect(deposit).is.not.undefined; + const toEndpoint = await engine.fetchRouterEndpointInfo( toChainId(fastMarketOrder!.targetChain), ); @@ -4770,7 +4490,9 @@ describe("Matching Engine", function () { return { fastVaa, + fastVaaAccount, finalizedVaa, + finalizedVaaAccount, args: args!, preparedOrderResponse, }; @@ -4784,18 +4506,18 @@ describe("Matching Engine", function () { auction?: PublicKey; bestOfferToken?: PublicKey; }, - opts: ForTestOpts & + opts: TestOptions & ObserveCctpOrderVaasOpts & PrepareOrderResponseForTestOptionalOpts & { executorIsPreparer?: boolean; - prepareSigners?: Signer[]; + prepareSigners?: SDKSigner<"Devnet">[]; preparedInSameTransaction?: boolean; } = {}, ): Promise { - let [{ signers, errorMsg }, excludedForTestOpts] = setDefaultForTestOpts(opts); + let [{ signers, errorMsg }, excludedForTestOpts] = addDefaultOptions(opts); let { executorIsPreparer, prepareSigners, preparedInSameTransaction } = excludedForTestOpts; executorIsPreparer ??= true; - prepareSigners ??= [playerOne]; + prepareSigners ??= [playerOneSigner]; preparedInSameTransaction ??= false; // TODO: do something with this if (preparedInSameTransaction) { @@ -4817,7 +4539,7 @@ describe("Matching Engine", function () { : payer.publicKey, }, { - signers: executorIsPreparer ? prepareSigners : [payer], + signers: executorIsPreparer ? prepareSigners : [payerSigner], ...excludedForTestOpts, }, ); @@ -4835,7 +4557,7 @@ describe("Matching Engine", function () { }); if (errorMsg !== null) { - return expectIxErr(connection, [ix], signers, errorMsg); + return expectIxErr(connection, [ix], unwrapSigners(signers), errorMsg); } // If we are at this point, we require that prepareOrderResponseForTest be called. So these @@ -4845,7 +4567,6 @@ describe("Matching Engine", function () { } const fastVaaAccount = await VaaAccount.fetch(connection, fastVaa); - const auction = accounts.auction ?? engine.auctionAddress(fastVaaAccount.digest()); const { info, status: statusBefore } = await engine.fetchAuction({ address: auction, @@ -4886,7 +4607,13 @@ describe("Matching Engine", function () { .getAccountInfo(preparedCustodyToken) .then((info) => info!.lamports); - await expectIxOk(connection, [ix], [payer]); + const txs = engine.settleOrder( + executor, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + ); + + const signer = executorIsPreparer ? prepareSigners[0] : payerSigner; + await expectTxsOk(signer, txs); { const accInfo = await connection.getAccountInfo(preparedCustodyToken); @@ -4901,6 +4628,7 @@ describe("Matching Engine", function () { connection, bestOfferToken, ); + const finalizedVaaAccount = await VaaAccount.fetch(connection, finalizedVaa); const { deposit } = LiquidityLayerMessage.decode(finalizedVaaAccount.payload()); const { baseFee } = deposit!.message.payload! as SlowOrderResponse; @@ -4923,7 +4651,7 @@ describe("Matching Engine", function () { const authorityLamportsAfter = await connection.getBalance(executor); expect(authorityLamportsAfter).equals( - authorityLamportsBefore + preparedOrderLamports + preparedCustodyLamports, + authorityLamportsBefore + preparedOrderLamports + preparedCustodyLamports - 5000, ); const { status: statusAfter } = await engine.fetchAuction({ @@ -4951,13 +4679,13 @@ describe("Matching Engine", function () { preparedOrderResponse?: PublicKey; toRouterEndpoint?: PublicKey; }, - opts: ForTestOpts & + opts: TestOptions & ObserveCctpOrderVaasOpts & PrepareOrderResponseForTestOptionalOpts & { preparedInSameTransaction?: boolean; } = {}, ): Promise { - let [{ signers, errorMsg }, excludedForTestOpts] = setDefaultForTestOpts(opts); + let [{ signers, errorMsg }, excludedForTestOpts] = addDefaultOptions(opts); let { preparedInSameTransaction } = excludedForTestOpts; preparedInSameTransaction ??= false; // TODO: do something with this if (preparedInSameTransaction) { @@ -4973,9 +4701,7 @@ describe("Matching Engine", function () { }; } else { const result = await prepareOrderResponseCctpForTest( - { - payer: payer.publicKey, - }, + { payer: payer.publicKey }, { ...excludedForTestOpts, placeInitialOffer: false, @@ -4991,18 +4717,22 @@ describe("Matching Engine", function () { } })(); - const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 300_000, - }); + const fastVaaAccount = await VaaAccount.fetch(connection, fastVaa); + const { fastMarketOrder } = LiquidityLayerMessage.decode(fastVaaAccount.payload()); + expect(fastMarketOrder).is.not.undefined; - const ix = await engine.settleAuctionNoneCctpIx({ - ...accounts, - fastVaa, - preparedOrderResponse, - }); + let finalizedVaaAccount = finalizedVaa + ? await VaaAccount.fetch(connection, finalizedVaa) + : undefined; + + const txs = engine.settleOrder( + accounts.payer, + fastVaaAccount.vaa("FastTransfer:FastMarketOrder"), + finalizedVaaAccount?.vaa("FastTransfer:CctpDeposit"), + ); if (errorMsg !== null) { - return expectIxErr(connection, [computeIx, ix], signers, errorMsg); + return expectTxsErr(signers[0], txs, errorMsg); } // If we are at this point, we require that prepareOrderResponseForTest be called. So the @@ -5018,16 +4748,11 @@ describe("Matching Engine", function () { feeRecipientToken, ); - await expectIxOk(connection, [computeIx, ix], signers); + await expectTxsOk(signers[0], txs); - const fastVaaAccount = await VaaAccount.fetch(connection, fastVaa); - const { fastMarketOrder } = LiquidityLayerMessage.decode(fastVaaAccount.payload()); - expect(fastMarketOrder).is.not.undefined; - - const finalizedVaaAccount = await VaaAccount.fetch(connection, finalizedVaa); const { message: { payload: slowOrderResponse }, - } = LiquidityLayerMessage.decode(finalizedVaaAccount.payload()).deposit!; + } = LiquidityLayerMessage.decode(finalizedVaaAccount!.payload()).deposit!; expect(slowOrderResponse).is.not.undefined; const fee = @@ -5084,53 +4809,25 @@ describe("Matching Engine", function () { redeemerMessage?: Buffer; } = {}, ): FastMarketOrder { - const { - amountIn, - targetChain, - minAmountOut, - maxFee, - initAuctionFee, - deadline, - redeemerMessage, - } = args; - return { - amountIn: amountIn ?? 1_000_000_000n, - minAmountOut: minAmountOut ?? 0n, - targetChain: targetChain ?? "Arbitrum", + amountIn: 1_000_000_000n, + minAmountOut: 0n, + targetChain: "Arbitrum", redeemer: toUniversalAddress(new Array(32).fill(1)), sender: toUniversalAddress(new Array(32).fill(2)), refundAddress: toUniversalAddress(new Array(32).fill(3)), - maxFee: maxFee ?? 42069n, - initAuctionFee: initAuctionFee ?? 1_250_000n, - deadline: deadline ?? 0, - redeemerMessage: - redeemerMessage ?? encoding.bytes.encode("Somebody set up us the bomb"), + maxFee: 42069n, + initAuctionFee: 1_250_000n, + deadline: 0, + redeemerMessage: encoding.bytes.encode("Somebody set up us the bomb"), + ...args, }; } function newSlowOrderResponse(args: { baseFee?: bigint } = {}): SlowOrderResponse { - const { baseFee } = args; - - return { - baseFee: baseFee ?? 420n, - }; + return { baseFee: 420n, ...args }; } - type VaaResult = { - vaa: PublicKey; - vaaAccount: VaaAccount; - }; - - type FastObservedResult = VaaResult & { - fastMarketOrder: FastMarketOrder; - }; - - type FinalizedObservedResult = VaaResult & { - slowOrderResponse: SlowOrderResponse; - cctp: CctpMessageArgs; - }; - type ObserveCctpOrderVaasOpts = { sourceChain?: Chain; emitter?: Array; @@ -5144,10 +4841,7 @@ describe("Matching Engine", function () { finalizedVaaTimestamp?: number; }; - async function observeCctpOrderVaas(opts: ObserveCctpOrderVaasOpts = {}): Promise<{ - fast: FastObservedResult; - finalized?: FinalizedObservedResult; - }> { + async function observeCctpOrderVaas(opts: ObserveCctpOrderVaasOpts = {}) { let { sourceChain, emitter, @@ -5176,66 +4870,67 @@ describe("Matching Engine", function () { throw new Error(`Invalid source chain: ${sourceChain}`); } - const fastVaa = await postLiquidityLayerVaa( + const mockFastVaa = await createLiquidityLayerVaa( connection, - payer, - MOCK_GUARDIANS, emitter, wormholeSequence++, - new LiquidityLayerMessage({ - fastMarketOrder, - }), + new LiquidityLayerMessage({ fastMarketOrder }), { sourceChain, timestamp: vaaTimestamp }, ); - const fastVaaAccount = await VaaAccount.fetch(connection, fastVaa); + + const { address: fastVaa, account: fastVaaAccount } = await postAndFetchVaa( + payerSigner, + engine, + mockFastVaa, + ); + const fast = { fastMarketOrder, vaa: fastVaa, vaaAccount: fastVaaAccount }; - if (finalized) { - const { amountIn: amount } = fastMarketOrder; - const cctpNonce = testCctpNonce++; - - // Concoct a Circle message. - const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } = - await craftCctpTokenBurnMessage(sourceCctpDomain, cctpNonce, amount); - - const finalizedMessage = new LiquidityLayerMessage({ - deposit: new LiquidityLayerDeposit({ - tokenAddress: toUniversalAddress(burnMessage.burnTokenAddress), - amount, - sourceCctpDomain, - destinationCctpDomain, - cctpNonce, - burnSource: toUniversalAddress(Buffer.alloc(32, "beefdead", "hex")), - mintRecipient: toUniversalAddress(engine.cctpMintRecipientAddress().toBuffer()), - payload: { id: 2, ...slowOrderResponse }, - }), - }); + if (!finalized) return { fast }; + + const { amountIn: amount } = fastMarketOrder; + const cctpNonce = testCctpNonce++; + + // Concoct a Circle message. + const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } = + await craftCctpTokenBurnMessage(sourceCctpDomain, cctpNonce, amount); + + const finalizedMessage = new LiquidityLayerMessage({ + deposit: new LiquidityLayerDeposit({ + tokenAddress: toUniversalAddress(burnMessage.burnTokenAddress), + amount, + sourceCctpDomain, + destinationCctpDomain, + cctpNonce, + burnSource: toUniversalAddress(Buffer.alloc(32, "beefdead", "hex")), + mintRecipient: toUniversalAddress(engine.cctpMintRecipientAddress().toBuffer()), + payload: { id: 2, ...slowOrderResponse }, + }), + }); - const finalizedVaa = await postLiquidityLayerVaa( - connection, - payer, - MOCK_GUARDIANS, - finalizedEmitter, - finalizedSequence, - finalizedMessage, - { sourceChain: finalizedSourceChain, timestamp: finalizedVaaTimestamp }, - ); - const finalizedVaaAccount = await VaaAccount.fetch(connection, finalizedVaa); - return { - fast, - finalized: { - slowOrderResponse, - vaa: finalizedVaa, - vaaAccount: finalizedVaaAccount, - cctp: { - encodedCctpMessage, - cctpAttestation, - }, - }, - }; - } else { - return { fast }; - } + const mockFinalizedVaa = await createLiquidityLayerVaa( + connection, + finalizedEmitter, + finalizedSequence, + finalizedMessage, + { sourceChain: finalizedSourceChain, timestamp: finalizedVaaTimestamp }, + ); + + const { address: finalizedVaa, account: finalizedVaaAccount } = await postAndFetchVaa( + payerSigner, + engine, + mockFinalizedVaa, + ); + + return { + fast, + finalized: { + slowOrderResponse, + vaa: finalizedVaa, + vaaAccount: finalizedVaaAccount, + cctp: { encodedCctpMessage, cctpAttestation }, + }, + }; } async function craftCctpTokenBurnMessage( diff --git a/solana/ts/tests/02__tokenRouter.ts b/solana/ts/tests/02__tokenRouter.ts index 591322f9..04b1396b 100644 --- a/solana/ts/tests/02__tokenRouter.ts +++ b/solana/ts/tests/02__tokenRouter.ts @@ -1,3 +1,4 @@ +// @ts-ignore import * as splToken from "@solana/spl-token"; import { AddressLookupTableProgram, @@ -8,28 +9,34 @@ import { SystemProgram, TransactionInstruction, } from "@solana/web3.js"; -import { ChainId, toChain, toChainId } from "@wormhole-foundation/sdk-base"; -import { toUniversal } from "@wormhole-foundation/sdk-definitions"; +import { TokenRouter } from "@wormhole-foundation/example-liquidity-layer-definitions"; +import { ChainId, encoding, toChain, toChainId } from "@wormhole-foundation/sdk-base"; +import { CircleBridge, VAA, toUniversal } from "@wormhole-foundation/sdk-definitions"; import { deserializePostMessage } from "@wormhole-foundation/sdk-solana-core"; import { expect } from "chai"; import { CctpTokenBurnMessage } from "../src/cctp"; import { LiquidityLayerDeposit, LiquidityLayerMessage, uint64ToBN } from "../src/common"; +import { SolanaTokenRouter } from "../src/protocol"; import { CircleAttester, + DEFAULT_ADDRESSES, ETHEREUM_USDC_ADDRESS, LOCALHOST, - MOCK_GUARDIANS, OWNER_ASSISTANT_KEYPAIR, OWNER_KEYPAIR, PAYER_KEYPAIR, REGISTERED_TOKEN_ROUTERS, USDC_MINT_ADDRESS, + createLiquidityLayerVaa, expectIxErr, expectIxOk, - postLiquidityLayerVaa, + expectTxsErr, + expectTxsOk, + getSdkSigner, + postAndFetchVaa, toUniversalAddress, } from "../src/testing"; -import { Custodian, PreparedOrder, TokenRouterProgram, localnet } from "../src/tokenRouter"; +import { Custodian, PreparedOrder, TokenRouterProgram } from "../src/tokenRouter"; const SOLANA_CHAIN_ID = toChainId("Solana"); @@ -37,15 +44,24 @@ describe("Token Router", function () { const connection = new Connection(LOCALHOST, "processed"); // payer is also the recipient in all tests const payer = PAYER_KEYPAIR; + const { signer: payerSigner } = getSdkSigner(connection, payer); + const relayer = Keypair.generate(); const owner = OWNER_KEYPAIR; + const { signer: ownerSigner } = getSdkSigner(connection, owner); const ownerAssistant = OWNER_ASSISTANT_KEYPAIR; + const { signer: ownerAssistantSigner } = getSdkSigner(connection, ownerAssistant); const foreignChain = toChainId("Ethereum"); const invalidChain = (foreignChain + 1) as ChainId; const foreignEndpointAddress = REGISTERED_TOKEN_ROUTERS["Ethereum"]!; const foreignCctpDomain = 0; - const tokenRouter = new TokenRouterProgram(connection, localnet(), USDC_MINT_ADDRESS); + const tokenRouter = new SolanaTokenRouter( + "Devnet", + "Solana", + connection, + DEFAULT_ADDRESSES["Devnet"]!, + ); let lookupTableAddress: PublicKey; @@ -53,36 +69,18 @@ describe("Token Router", function () { describe("Initialize", function () { it("Cannot Initialize without USDC Mint", async function () { const mint = await splToken.createMint(connection, payer, payer.publicKey, null, 6); - - const ix = await tokenRouter.initializeIx({ - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - mint, - }); - const unknownAta = splToken.getAssociatedTokenAddressSync( - mint, - tokenRouter.custodianAddress(), - true, - ); - await expectIxErr(connection, [ix], [payer], `mint. Error Code: ConstraintAddress`); + const txs = tokenRouter.initialize(payer.publicKey, ownerAssistant.publicKey, mint); + await expectTxsErr(payerSigner, txs, `mint. Error Code: ConstraintAddress`); }); it("Cannot Initialize with Default Owner Assistant", async function () { - const ix = await tokenRouter.initializeIx({ - owner: payer.publicKey, - ownerAssistant: PublicKey.default, - }); - - await expectIxErr(connection, [ix], [payer], "Error Code: AssistantZeroPubkey"); + const txs = tokenRouter.initialize(payer.publicKey, PublicKey.default); + await expectTxsErr(payerSigner, txs, "Error Code: AssistantZeroPubkey"); }); it("Initialize", async function () { - const ix = await tokenRouter.initializeIx({ - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - }); - - await expectIxOk(connection, [ix], [payer]); + const txs = tokenRouter.initialize(payer.publicKey, ownerAssistant.publicKey); + await expectTxsOk(payerSigner, txs); const custodianData = await tokenRouter.fetchCustodian(); expect(custodianData).to.eql( @@ -103,15 +101,11 @@ describe("Token Router", function () { }); it("Cannot Initialize Again", async function () { - const ix = await tokenRouter.initializeIx({ - owner: payer.publicKey, - ownerAssistant: ownerAssistant.publicKey, - }); + const txs = tokenRouter.initialize(payer.publicKey, ownerAssistant.publicKey); - await expectIxErr( - connection, - [ix], - [payer], + await expectTxsErr( + payerSigner, + txs, `Allocate: account Address { address: ${tokenRouter .custodianAddress() .toString()}, base: None } already in use`, @@ -385,127 +379,47 @@ describe("Token Router", function () { const localVariables = new Map(); it("Cannot Prepare Market Order with Large Redeemer Payload", async function () { - const preparedOrder = Keypair.generate(); - - const amountIn = 5000n; - const minAmountOut = 0n; - const targetChain = foreignChain; - const redeemer = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const redeemerMessage = Buffer.alloc(501, "deadbeef", "hex"); - const [approveIx, prepareIx] = await tokenRouter.prepareMarketOrderIx( - { - payer: payer.publicKey, - preparedOrder: preparedOrder.publicKey, - senderToken: payerToken, - }, - { - amountIn, - minAmountOut, - targetChain, - redeemer, - redeemerMessage, - }, - ); - - await expectIxErr( - connection, - [approveIx!, prepareIx], - [payer, preparedOrder], - "Error Code: RedeemerMessageTooLarge", - ); + const txs = tokenRouter.prepareMarketOrder(payer.publicKey, { + amountIn: 5000n, + minAmountOut: 0n, + redeemer: toUniversalAddress(Buffer.alloc(32, "deadbeef", "hex")), + targetChain: toChain(foreignChain), + redeemerMessage: Buffer.alloc(501, "deadbeef", "hex"), + }); + await expectTxsErr(payerSigner, txs, "Error Code: RedeemerMessageTooLarge"); }); it("Cannot Prepare Market Order with Insufficient Amount", async function () { - const preparedOrder = Keypair.generate(); - - const amountIn = 0n; - const minAmountOut = 0n; - const targetChain = foreignChain; - const redeemer = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const redeemerMessage = Buffer.from("All your base are belong to us"); - const [approveIx, prepareIx] = await tokenRouter.prepareMarketOrderIx( - { - payer: payer.publicKey, - preparedOrder: preparedOrder.publicKey, - senderToken: payerToken, - }, - { - amountIn, - minAmountOut, - targetChain, - redeemer, - redeemerMessage, - }, - ); - - await expectIxErr( - connection, - [approveIx!, prepareIx], - [payer, preparedOrder], - "Error Code: InsufficientAmount", - ); + const txs = tokenRouter.prepareMarketOrder(payer.publicKey, { + amountIn: 0n, + targetChain: toChain(foreignChain), + redeemer: toUniversalAddress(Buffer.alloc(32, "deadbeef", "hex")), + minAmountOut: 0n, + redeemerMessage: Buffer.from("All your base are belong to us"), + }); + await expectTxsErr(payerSigner, txs, "Error Code: InsufficientAmount"); }); it("Cannot Prepare Market Order with Invalid Redeemer", async function () { - const preparedOrder = Keypair.generate(); - - const amountIn = 69n; - const minAmountOut = 0n; - const targetChain = foreignChain; - const redeemer = Array.from(Buffer.alloc(32, 0, "hex")); - const redeemerMessage = Buffer.from("All your base are belong to us"); - const [approveIx, prepareIx] = await tokenRouter.prepareMarketOrderIx( - { - payer: payer.publicKey, - preparedOrder: preparedOrder.publicKey, - senderToken: payerToken, - }, - { - amountIn, - minAmountOut, - targetChain, - redeemer, - redeemerMessage, - }, - ); - - await expectIxErr( - connection, - [approveIx!, prepareIx], - [payer, preparedOrder], - "Error Code: InvalidRedeemer", - ); + const txs = tokenRouter.prepareMarketOrder(payer.publicKey, { + amountIn: 69n, + targetChain: toChain(foreignChain), + redeemer: toUniversalAddress(Buffer.alloc(32, 0, "hex")), + minAmountOut: 0n, + redeemerMessage: Buffer.from("All your base are belong to us"), + }); + await expectTxsErr(payerSigner, txs, "Error Code: InvalidRedeemer"); }); it("Cannot Prepare Market Order with Min Amount Too High", async function () { - const preparedOrder = Keypair.generate(); - - const amountIn = 1n; - const minAmountOut = 2n; - const targetChain = foreignChain; - const redeemer = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const redeemerMessage = Buffer.from("All your base are belong to us"); - const [approveIx, prepareIx] = await tokenRouter.prepareMarketOrderIx( - { - payer: payer.publicKey, - preparedOrder: preparedOrder.publicKey, - senderToken: payerToken, - }, - { - amountIn, - minAmountOut, - targetChain, - redeemer, - redeemerMessage, - }, - ); - - await expectIxErr( - connection, - [approveIx!, prepareIx], - [payer, preparedOrder], - "Error Code: MinAmountOutTooHigh", - ); + const txs = tokenRouter.prepareMarketOrder(payer.publicKey, { + amountIn: 1n, + minAmountOut: 2n, + targetChain: toChain(foreignChain), + redeemer: toUniversalAddress(Buffer.alloc(32, "deadbeef", "hex")), + redeemerMessage: Buffer.from("All your base are belong to us"), + }); + await expectTxsErr(payerSigner, txs, "Error Code: MinAmountOutTooHigh"); }); it("Cannot Prepare Market Order without Delegating Authority to Program Transfer Authority", async function () { @@ -641,34 +555,26 @@ describe("Token Router", function () { }); it("Prepare Market Order with Program Transfer Authority Specifying Some Min Amount Out", async function () { + // TODO:: Did i remove some actual test here? for the _program transfer authority_ test? const preparedOrder = Keypair.generate(); - const amountIn = 69n; - const minAmountOut = 0n; const targetChain = foreignChain; - const redeemer = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const redeemerMessage = Buffer.from("All your base are belong to us"); - const [approveIx, prepareIx] = await tokenRouter.prepareMarketOrderIx( - { - payer: payer.publicKey, - preparedOrder: preparedOrder.publicKey, - senderToken: payerToken, - }, - { - amountIn, - minAmountOut, - targetChain, - redeemer, - redeemerMessage, - }, - ); + + const request: TokenRouter.OrderRequest = { + amountIn: 69n, + targetChain: toChain(targetChain), + redeemer: toUniversalAddress(Buffer.alloc(32, "deadbeef", "hex")), + redeemerMessage: Buffer.from("All your base are belong to us"), + minAmountOut: 0n, + }; + const txs = tokenRouter.prepareMarketOrder(payer.publicKey, request, preparedOrder); const { amount: balanceBefore } = await splToken.getAccount(connection, payerToken); - await expectIxOk(connection, [approveIx!, prepareIx], [payer, preparedOrder]); + await expectTxsOk(payerSigner, txs); const { amount: balanceAfter } = await splToken.getAccount(connection, payerToken); - expect(balanceAfter).equals(balanceBefore - amountIn); + expect(balanceAfter).equals(balanceBefore - request.amountIn); const preparedOrderData = await tokenRouter.fetchPreparedOrder( preparedOrder.publicKey, @@ -683,16 +589,16 @@ describe("Token Router", function () { preparedBy: payer.publicKey, orderType: { market: { - minAmountOut: uint64ToBN(minAmountOut), + minAmountOut: uint64ToBN(request.minAmountOut!), }, }, srcToken: payerToken, refundToken: payerToken, targetChain, - redeemer, + redeemer: Array.from(request.redeemer.toUint8Array()), preparedCustodyTokenBump, }, - redeemerMessage, + Buffer.from(request.redeemerMessage!), ), ); @@ -700,32 +606,28 @@ describe("Token Router", function () { connection, tokenRouter.preparedCustodyTokenAddress(preparedOrder.publicKey), ); - expect(preparedCustodyTokenBalance).equals(amountIn); + expect(preparedCustodyTokenBalance).equals(request.amountIn); }); it("Prepare Market Order with Transfer Authority without Specifying Min Amount Out", async function () { const preparedOrder = Keypair.generate(); const amountIn = 69n; - const [approveIx, prepareIx] = await tokenRouter.prepareMarketOrderIx( - { - payer: payer.publicKey, - preparedOrder: preparedOrder.publicKey, - senderToken: payerToken, - }, + const txs = tokenRouter.prepareMarketOrder( + payer.publicKey, + { amountIn, - minAmountOut: null, - targetChain: foreignChain, - redeemer: Array.from(Buffer.alloc(32, "deadbeef", "hex")), + targetChain: toChain(foreignChain), + redeemer: toUniversalAddress(Buffer.alloc(32, "deadbeef", "hex")), redeemerMessage: Buffer.from("All your base are belong to us"), }, + preparedOrder, ); - expect(approveIx).is.not.null; const { amount: balanceBefore } = await splToken.getAccount(connection, payerToken); - await expectIxOk(connection, [approveIx!, prepareIx], [payer, preparedOrder]); + await expectTxsOk(payerSigner, txs); const { amount: balanceAfter } = await splToken.getAccount(connection, payerToken); expect(balanceAfter).equals(balanceBefore - amountIn); @@ -802,13 +704,11 @@ describe("Token Router", function () { const amountIn = localVariables.get("amountIn") as bigint; expect(localVariables.delete("amountIn")).is.true; - const ix = await tokenRouter.closePreparedOrderIx({ - preparedOrder, - }); + const txs = tokenRouter.closePreparedOrder(payer.publicKey, preparedOrder); const { amount: balanceBefore } = await splToken.getAccount(connection, payerToken); - await expectIxOk(connection, [ix], [payer]); + await expectTxsOk(payerSigner, txs); const { amount: balanceAfter } = await splToken.getAccount(connection, payerToken); expect(balanceAfter).equals(balanceBefore + amountIn); @@ -826,27 +726,20 @@ describe("Token Router", function () { const preparedOrder = Keypair.generate(); const amountIn = 69n; - const [approveIx, prepareIx] = await tokenRouter.prepareMarketOrderIx( - { - payer: payer.publicKey, - preparedOrder: preparedOrder.publicKey, - senderToken: payerToken, - sender: payer.publicKey, - }, + const txs = tokenRouter.prepareMarketOrder( + payer.publicKey, { - useTransferAuthority: false, amountIn, - minAmountOut: null, - targetChain: foreignChain, - redeemer: Array.from(Buffer.alloc(32, "deadbeef", "hex")), - redeemerMessage: Buffer.from("All your base are belong to us"), + targetChain: toChain(foreignChain), + redeemer: toUniversalAddress(Buffer.alloc(32, "deadbeef", "hex")), + redeemerMessage: encoding.bytes.encode("All your base are belong to us"), }, + preparedOrder, ); - expect(approveIx).is.null; const { amount: balanceBefore } = await splToken.getAccount(connection, payerToken); - await expectIxOk(connection, [prepareIx], [payer, preparedOrder]); + await expectTxsOk(payerSigner, txs); const { amount: balanceAfter } = await splToken.getAccount(connection, payerToken); expect(balanceAfter).equals(balanceBefore - amountIn); @@ -910,6 +803,7 @@ describe("Token Router", function () { const unregisteredEndpoint = tokenRouter .matchingEngineProgram() .routerEndpointAddress(SOLANA_CHAIN_ID); + const ix = await tokenRouter.placeMarketOrderCctpIx( { payer: payer.publicKey, @@ -970,23 +864,16 @@ describe("Token Router", function () { ); }); + // TODO: it("Place Market Order", async function () { const preparedOrder = localVariables.get("preparedOrder") as PublicKey; expect(localVariables.delete("preparedOrder")).is.true; const amountIn = localVariables.get("amountIn") as bigint; expect(localVariables.delete("amountIn")).is.true; - const ix = await tokenRouter.placeMarketOrderCctpIx({ - payer: payer.publicKey, - preparedOrder, - }); + const txs = tokenRouter.placeMarketOrder(payer.publicKey, preparedOrder); - const { value: lookupTableAccount } = await connection.getAddressLookupTable( - lookupTableAddress, - ); - await expectIxOk(connection, [ix], [payer], { - addressLookupTableAccounts: [lookupTableAccount!], - }); + await expectTxsOk(payerSigner, txs); checkAfterEffects({ preparedOrder, amountIn, burnSource: payerToken }); @@ -1135,6 +1022,35 @@ describe("Token Router", function () { }); }); + it("Prepare and Place Market Order in One Transaction using TokenRouter", async function () { + const preparedOrder = Keypair.generate(); + + const request: TokenRouter.OrderRequest = { + amountIn: 42069n, + targetChain: toChain(foreignChain), + redeemer: toUniversalAddress(redeemer), + redeemerMessage: redeemerMessage, + }; + const txs = tokenRouter.placeMarketOrder(payer.publicKey, request, preparedOrder); + + const { amount: balanceBefore } = await splToken.getAccount(connection, payerToken); + + const { value: lookupTableAccount } = await connection.getAddressLookupTable( + lookupTableAddress, + ); + + await expectTxsOk(payerSigner, txs); + + const { amount: balanceAfter } = await splToken.getAccount(connection, payerToken); + expect(balanceAfter).equals(balanceBefore - request.amountIn); + + checkAfterEffects({ + preparedOrder: preparedOrder.publicKey, + amountIn: request.amountIn, + burnSource: payerToken, + }); + }); + async function prepareOrder(amountIn: bigint) { const preparedOrder = Keypair.generate(); const [approveIx, prepareIx] = await tokenRouter.prepareMarketOrderIx( @@ -1263,15 +1179,20 @@ describe("Token Router", function () { }), }); - const vaa = await postLiquidityLayerVaa( + const mockInvalidVaa = await createLiquidityLayerVaa( connection, - payer, - MOCK_GUARDIANS, foreignEndpointAddress, wormholeSequence++, message, { sourceChain: "Polygon" }, ); + + const { address: vaa } = await postAndFetchVaa( + payerSigner, + tokenRouter.matchingEngine, + mockInvalidVaa, + ); + const ix = await tokenRouter.redeemCctpFillIx( { payer: payer.publicKey, @@ -1337,14 +1258,19 @@ describe("Token Router", function () { }), }); - const vaa = await postLiquidityLayerVaa( + const mockInvalidVaa = await createLiquidityLayerVaa( connection, - payer, - MOCK_GUARDIANS, new Array(32).fill(0), // emitter address wormholeSequence++, message, ); + + const { address: vaa } = await postAndFetchVaa( + payerSigner, + tokenRouter.matchingEngine, + mockInvalidVaa, + ); + const ix = await tokenRouter.redeemCctpFillIx( { payer: payer.publicKey, @@ -1408,14 +1334,19 @@ describe("Token Router", function () { const encodedMessage = message.encode(); encodedMessage[147] = 69; - const vaa = await postLiquidityLayerVaa( + const mockInvalidVaa = await createLiquidityLayerVaa( connection, - payer, - MOCK_GUARDIANS, foreignEndpointAddress, wormholeSequence++, encodedMessage, ); + + const { address: vaa } = await postAndFetchVaa( + payerSigner, + tokenRouter.matchingEngine, + mockInvalidVaa, + ); + const ix = await tokenRouter.redeemCctpFillIx( { payer: payer.publicKey, @@ -1475,23 +1406,22 @@ describe("Token Router", function () { }), }); - const vaa = await postLiquidityLayerVaa( + const mockInvalidVaa = await createLiquidityLayerVaa( connection, - payer, - MOCK_GUARDIANS, foreignEndpointAddress, wormholeSequence++, message, ); + + const { address: vaa } = await postAndFetchVaa( + payerSigner, + tokenRouter.matchingEngine, + mockInvalidVaa, + ); + const ix = await tokenRouter.redeemCctpFillIx( - { - payer: payer.publicKey, - vaa, - }, - { - encodedCctpMessage, - cctpAttestation, - }, + { payer: payer.publicKey, vaa }, + { encodedCctpMessage, cctpAttestation }, ); const { value: lookupTableAccount } = await connection.getAddressLookupTable( @@ -1552,14 +1482,18 @@ describe("Token Router", function () { }), }); - const vaa = await postLiquidityLayerVaa( + const mockInvalidVaa = await createLiquidityLayerVaa( connection, - payer, - MOCK_GUARDIANS, foreignEndpointAddress, wormholeSequence++, message, ); + const { address: vaa } = await postAndFetchVaa( + payerSigner, + tokenRouter.matchingEngine, + mockInvalidVaa, + ); + const ix = await tokenRouter.redeemCctpFillIx( { payer: payer.publicKey, @@ -1595,9 +1529,7 @@ describe("Token Router", function () { it("Update Router Endpoint", async function () { const ix = await tokenRouter.matchingEngineProgram().updateCctpRouterEndpointIx( - { - owner: owner.publicKey, - }, + { owner: owner.publicKey }, { chain: foreignChain, address: foreignEndpointAddress, @@ -1758,6 +1690,67 @@ describe("Token Router", function () { // NOTE: This is a CCTP message transmitter error. await expectIxErr(connection, [ix], [payer], "Error Code: NonceAlreadyUsed"); }); + + it("Redeem Fill with Protocol Client", async function () { + const cctpNonce = testCctpNonce++; + + // Concoct a Circle message. + const { destinationCctpDomain, burnMessage, encodedCctpMessage, cctpAttestation } = + await craftCctpTokenBurnMessage( + tokenRouter, + sourceCctpDomain, + cctpNonce, + encodedMintRecipient, + amount, + burnSource, + ); + + const message = new LiquidityLayerMessage({ + deposit: new LiquidityLayerDeposit({ + tokenAddress: toUniversalAddress(burnMessage.burnTokenAddress), + amount, + sourceCctpDomain, + destinationCctpDomain, + cctpNonce, + burnSource: toUniversalAddress(burnSource), + mintRecipient: toUniversalAddress(encodedMintRecipient), + payload: { + id: 1, + sourceChain: toChain(foreignChain), + orderSender: toUniversalAddress(Buffer.alloc(32, "d00d", "hex")), + redeemer: toUniversalAddress(redeemer.publicKey.toBuffer()), + redeemerMessage: Buffer.from("Somebody set up us the bomb"), + }, + }), + }); + + const mockVaa = (await createLiquidityLayerVaa( + connection, + foreignEndpointAddress, + wormholeSequence++, + message, + )) as VAA<"FastTransfer:CctpDeposit">; + + await postAndFetchVaa(payerSigner, tokenRouter.matchingEngine, mockVaa); + + const [cctpMessage] = CircleBridge.deserialize(new Uint8Array(encodedCctpMessage)); + + const { value: lookupTableAccount } = await connection.getAddressLookupTable( + lookupTableAddress, + ); + + const txs = tokenRouter.redeemFill( + payer.publicKey, + mockVaa, + { + message: cctpMessage, + attestation: cctpAttestation.toString("hex"), + }, + [lookupTableAccount!], + ); + + await expectTxsOk(payerSigner, txs); + }); }); }); }); diff --git a/solana/ts/tests/04__interaction.ts b/solana/ts/tests/04__interaction.ts index 6c235f45..628c8045 100644 --- a/solana/ts/tests/04__interaction.ts +++ b/solana/ts/tests/04__interaction.ts @@ -1,4 +1,5 @@ import { BN } from "@coral-xyz/anchor"; +// @ts-ignore import * as splToken from "@solana/spl-token"; import { AddressLookupTableProgram, @@ -29,23 +30,26 @@ import { writeUint64BE, } from "../src/common"; import * as matchingEngineSdk from "../src/matchingEngine"; +import { SolanaMatchingEngine, SolanaTokenRouter } from "../src/protocol"; import { CHAIN_TO_DOMAIN, CircleAttester, + DEFAULT_ADDRESSES, ETHEREUM_USDC_ADDRESS, LOCALHOST, - MOCK_GUARDIANS, OWNER_ASSISTANT_KEYPAIR, OWNER_KEYPAIR, PAYER_KEYPAIR, PLAYER_ONE_KEYPAIR, REGISTERED_TOKEN_ROUTERS, USDC_MINT_ADDRESS, + createLiquidityLayerVaa, expectIxErr, expectIxOk, expectIxOkDetails, getBlockTime, - postLiquidityLayerVaa, + getSdkSigner, + postAndFetchVaa, toUniversalAddress, waitUntilSlot, } from "../src/testing"; @@ -58,19 +62,23 @@ describe("Matching Engine <> Token Router", function () { const connection = new Connection(LOCALHOST, "processed"); const payer = PAYER_KEYPAIR; + const { signer: payerSigner } = getSdkSigner(connection, payer); + const owner = OWNER_KEYPAIR; const ownerAssistant = OWNER_ASSISTANT_KEYPAIR; const foreignChain = toChainId("Ethereum"); - const matchingEngine = new matchingEngineSdk.MatchingEngineProgram( + const matchingEngine = new SolanaMatchingEngine( + "Devnet", + "Solana", connection, - matchingEngineSdk.localnet(), - USDC_MINT_ADDRESS, + DEFAULT_ADDRESSES["Devnet"]!, ); - const tokenRouter = new tokenRouterSdk.TokenRouterProgram( + const tokenRouter = new SolanaTokenRouter( + "Devnet", + "Solana", connection, - tokenRouterSdk.localnet(), - matchingEngine.mint, + DEFAULT_ADDRESSES["Devnet"]!, ); const playerOne = PLAYER_ONE_KEYPAIR; @@ -1801,10 +1809,8 @@ describe("Matching Engine <> Token Router", function () { throw new Error(`Invalid source chain: ${sourceChain}`); } - const fastVaa = await postLiquidityLayerVaa( + const mockFastVaa = await createLiquidityLayerVaa( connection, - payer, - MOCK_GUARDIANS, emitter, wormholeSequence++, new LiquidityLayerMessage({ @@ -1812,6 +1818,12 @@ describe("Matching Engine <> Token Router", function () { }), { sourceChain, timestamp: vaaTimestamp }, ); + + const { address: fastVaa } = await postAndFetchVaa( + payerSigner, + matchingEngine, + mockFastVaa, + ); const fastVaaAccount = await VaaAccount.fetch(connection, fastVaa); const fast = { fastMarketOrder, vaa: fastVaa, vaaAccount: fastVaaAccount }; @@ -1838,15 +1850,20 @@ describe("Matching Engine <> Token Router", function () { }), }); - const finalizedVaa = await postLiquidityLayerVaa( + const mockFinalizedVaa = await createLiquidityLayerVaa( connection, - payer, - MOCK_GUARDIANS, finalizedEmitter, finalizedSequence, finalizedMessage, { sourceChain: finalizedSourceChain, timestamp: finalizedVaaTimestamp }, ); + + const { address: finalizedVaa } = await postAndFetchVaa( + payerSigner, + matchingEngine, + mockFinalizedVaa, + ); + const finalizedVaaAccount = await VaaAccount.fetch(connection, finalizedVaa); return { fast, diff --git a/solana/ts/tests/12__testnetFork.ts b/solana/ts/tests/12__testnetFork.ts index f42179b0..a9f4b4f7 100644 --- a/solana/ts/tests/12__testnetFork.ts +++ b/solana/ts/tests/12__testnetFork.ts @@ -1,18 +1,17 @@ import { Connection, Keypair, PublicKey, Signer, TransactionInstruction } from "@solana/web3.js"; import { expect } from "chai"; import { uint64ToBN } from "../src/common"; -import * as matchingEngineSdk from "../src/matchingEngine"; +import { SolanaMatchingEngine, SolanaTokenRouter } from "../src/protocol"; import { + DEFAULT_ADDRESSES, LOCALHOST, PAYER_KEYPAIR, - USDC_MINT_ADDRESS, expectIxErr, expectIxOk, expectIxOkDetails, loadProgramBpf, } from "../src/testing"; -import * as tokenRouterSdk from "../src/tokenRouter"; -import { UpgradeManagerProgram, UpgradeReceipt, testnet } from "../src/upgradeManager"; +import { UpgradeManagerProgram, UpgradeReceipt } from "../src/upgradeManager"; import { BPF_LOADER_UPGRADEABLE_PROGRAM_ID, programDataAddress } from "../src/utils"; const KEYPATH = `${__dirname}/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json`; @@ -24,17 +23,11 @@ describe("Upgrade Manager", function () { const connection = new Connection(LOCALHOST, "processed"); const payer = PAYER_KEYPAIR; - const matchingEngine = new matchingEngineSdk.MatchingEngineProgram( - connection, - matchingEngineSdk.testnet(), - USDC_MINT_ADDRESS, - ); - const tokenRouter = new tokenRouterSdk.TokenRouterProgram( - connection, - tokenRouterSdk.testnet(), - matchingEngine.mint, - ); - const upgradeManager = new UpgradeManagerProgram(connection, testnet()); + const network = "Testnet"; + const contracts = DEFAULT_ADDRESSES[network]!; + const matchingEngine = new SolanaMatchingEngine(network, "Solana", connection, contracts); + const tokenRouter = new SolanaTokenRouter(network, "Solana", connection, contracts); + const upgradeManager = new UpgradeManagerProgram(connection, contracts); describe("Upgrade Matching Engine", function () { it("Cannot Execute without Owner", async function () { @@ -242,10 +235,7 @@ describe("Upgrade Manager", function () { const guy = Keypair.generate(); await executeTokenRouterUpgradeForTest( - { - owner: guy.publicKey, - payer: payer.publicKey, - }, + { owner: guy.publicKey, payer: payer.publicKey }, { signers: [payer, guy], errorMsg: "Error Code: OwnerOnly" }, ); }); @@ -260,10 +250,7 @@ describe("Upgrade Manager", function () { const guy = Keypair.generate(); await executeTokenRouterUpgradeForTest( - { - owner: guy.publicKey, - payer: payer.publicKey, - }, + { owner: guy.publicKey, payer: payer.publicKey }, { signers: [payer, guy], errorMsg: "Error Code: OwnerMismatch" }, ); }); @@ -278,22 +265,15 @@ describe("Upgrade Manager", function () { const guy = Keypair.generate(); await commitTokenRouterUpgradeForTest( - { - owner: guy.publicKey, - }, + { owner: guy.publicKey }, { upgrade: false, signers: [payer, guy], errorMsg: "Error Code: OwnerMismatch" }, ); }); it("Commit After Execute (Recipient != Owner)", async function () { await commitTokenRouterUpgradeForTest( - { - owner: payer.publicKey, - recipient: Keypair.generate().publicKey, - }, - { - upgrade: false, - }, + { owner: payer.publicKey, recipient: Keypair.generate().publicKey }, + { upgrade: false }, ); }); @@ -388,14 +368,7 @@ describe("Upgrade Manager", function () { errorMsg ??= null; if (upgrade) { - await executeTokenRouterUpgradeForTest( - { - owner: accounts.owner, - }, - { - artifactPath, - }, - ); + await executeTokenRouterUpgradeForTest({ owner: accounts.owner }, { artifactPath }); } const recipientBalanceBefore = await (async () => { diff --git a/universal/ts/src/index.ts b/universal/ts/src/index.ts index 99e0e8e9..25bbf29a 100644 --- a/universal/ts/src/index.ts +++ b/universal/ts/src/index.ts @@ -18,6 +18,7 @@ import { export * from "./messages"; export * from "./payloads"; +export * from "./protocol"; export namespace Message { // Type guard for message types diff --git a/universal/ts/src/messages.ts b/universal/ts/src/messages.ts index 1598aedb..85cfc181 100644 --- a/universal/ts/src/messages.ts +++ b/universal/ts/src/messages.ts @@ -6,7 +6,11 @@ import { constMap, layoutDiscriminator, } from "@wormhole-foundation/sdk-base"; -import { layoutItems } from "@wormhole-foundation/sdk-definitions"; +import { + RegisterPayloadTypes, + layoutItems, + registerPayloadTypes, +} from "@wormhole-foundation/sdk-definitions"; import { payloadLayoutSwitch } from "./payloads"; const cctpDepositLayout = [ @@ -79,3 +83,18 @@ export function messageLayout(name: N): MessageLayout export type CctpDeposit = MessageType<"CctpDeposit">; export type FastMarketOrder = MessageType<"FastMarketOrder">; export type FastFill = MessageType<"FastFill">; + +// +const fastTransferNamedPayloads = [ + ["CctpDeposit", messageLayout("CctpDeposit")], + ["FastMarketOrder", messageLayout("FastMarketOrder")], + ["FastFill", messageLayout("FastFill")], +] as const satisfies RoArray<[string, Layout]>; + +declare module "@wormhole-foundation/sdk-definitions" { + export namespace WormholeRegistry { + interface PayloadLiteralToLayoutMapping + extends RegisterPayloadTypes<"FastTransfer", typeof fastTransferNamedPayloads> {} + } +} +registerPayloadTypes("FastTransfer", fastTransferNamedPayloads); diff --git a/universal/ts/src/protocol.ts b/universal/ts/src/protocol.ts new file mode 100644 index 00000000..502e7efe --- /dev/null +++ b/universal/ts/src/protocol.ts @@ -0,0 +1,175 @@ +import { Chain, Network } from "@wormhole-foundation/sdk-base"; +import { + AccountAddress, + ChainAddress, + CircleBridge, + Contracts, + EmptyPlatformMap, + ProtocolVAA, + UnsignedTransaction, + VAA, + payloadDiscriminator, +} from "@wormhole-foundation/sdk-definitions"; +import { FastMarketOrder, MessageName, messageNames } from "./messages"; + +// Utility types to allow re-use of the same type while making some +// fields optional or required +type WithRequired = T & { [P in K]-?: T[P] }; +type WithOptional = Pick, K> & Omit; + +export namespace FastTransfer { + const protocolName = "FastTransfer"; + export type ProtocolName = typeof protocolName; + + /** The VAAs emitted from the TokenBridge protocol */ + export type VAA = ProtocolVAA< + ProtocolName, + PayloadName + >; + + /** Addresses for FastTransfer protocol contracts */ + export type Addresses = Contracts & { + matchingEngine?: string; + tokenRouter?: string; + upgradeManager?: string; + // Add usdcMint to cctp contracts, mostly for testing + cctp?: Contracts["cctp"] & { usdcMint: string }; + }; + + export const getPayloadDiscriminator = () => payloadDiscriminator([protocolName, messageNames]); +} + +export interface FastTransfer { + // TODO: more arguments probably necessary here + transfer( + sender: AccountAddress, + recipient: ChainAddress, + token: AccountAddress, + // TODO: src/dst tokens? + amount: bigint, + ): AsyncGenerator>; + // redeem? +} + +export namespace MatchingEngine { + /** Contract addresses required for MatchingEngine */ + export type Addresses = WithRequired< + FastTransfer.Addresses, + "matchingEngine" | "coreBridge" | "cctp" + >; +} + +// matching engine: this is only on solana and where the auctions happen +export interface MatchingEngine { + // Admin methods + registerRouter( + sender: AccountAddress, + chain: RC, + cctpDomain: number, // TODO: should be typed? + router: AccountAddress, + tokenAccount?: AccountAddress, + ): AsyncGenerator>; + updateRouter( + sender: AccountAddress, + chain: RC, + cctpDomain: number, // TODO: should be typed? + router: AccountAddress, + tokenAccount?: AccountAddress, + ): AsyncGenerator>; + disableRouter( + sender: AccountAddress, + chain: RC, + ): AsyncGenerator>; + + setPause(sender: AccountAddress, pause: boolean): AsyncGenerator>; + setConfiguration(config: { + enabled: boolean; + maxAmount: bigint; + baseFee: bigint; + initAuctionFee: bigint; + }): AsyncGenerator>; + + // Standard usage + + // the first offer for the fast transfer and inits an auction + placeInitialOffer( + sender: AccountAddress, + vaa: VAA<"FastTransfer:FastMarketOrder">, + offerPrice: bigint, + totalDeposit?: bigint, + ): AsyncGenerator>; + improveOffer( + sender: AccountAddress, + vaa: VAA<"FastTransfer:FastMarketOrder">, + offer: bigint, + ): AsyncGenerator>; + executeFastOrder( + sender: AccountAddress, + vaa: VAA<"FastTransfer:FastMarketOrder">, + ): AsyncGenerator>; + prepareOrderResponse( + sender: AccountAddress, + vaa: VAA<"FastTransfer:FastMarketOrder">, + deposit: VAA<"FastTransfer:CctpDeposit">, + cctp: CircleBridge.Attestation, + ): AsyncGenerator>; + settleOrder( + sender: AccountAddress, + fast: VAA<"FastTransfer:FastMarketOrder">, + deposit?: VAA<"FastTransfer:CctpDeposit">, + cctp?: CircleBridge.Attestation, + ): AsyncGenerator>; +} + +export namespace TokenRouter { + /** A partially optional copy of FastMarketOrder, to be placed */ + export type OrderRequest = WithOptional< + FastMarketOrder, + | "sender" + | "deadline" + | "refundAddress" + | "minAmountOut" + | "redeemerMessage" + | "initAuctionFee" + | "maxFee" + >; + + export function isOrderRequest(value: any): value is OrderRequest { + return ( + typeof value === "object" && + value.amountIn !== undefined && + value.redeemer !== undefined && + value.targetChain !== undefined + ); + } + + /** Contract addresses required for TokenRouter */ + export type Addresses = WithRequired; + + /** The Address or Id of a prepared order */ + export type PreparedOrder = AccountAddress; +} + +export interface TokenRouter { + placeMarketOrder( + sender: AccountAddress, + order: TokenRouter.OrderRequest | TokenRouter.PreparedOrder, + ): AsyncGenerator>; + + redeemFill( + sender: AccountAddress, + vaa: FastTransfer.VAA, + cctp: CircleBridge.Attestation, + ): AsyncGenerator>; +} + +declare module "@wormhole-foundation/sdk-definitions" { + export namespace WormholeRegistry { + interface ProtocolToInterfaceMapping { + FastTransfer: FastTransfer; + } + interface ProtocolToPlatformMapping { + FastTransfer: EmptyPlatformMap<"FastTransfer">; + } + } +}