From a544b5fd77d752440585f2c8ea33dda7af6b6e3a Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Tue, 25 Jun 2024 08:53:04 -0400 Subject: [PATCH] re-use existing CCTP serde, actually deserialize created VAA to correct type --- .../ts/src/cctp/messageTransmitter/index.ts | 41 +----- solana/ts/src/cctp/messages.ts | 127 +++++++----------- .../ts/src/cctp/tokenMessengerMinter/index.ts | 43 +----- solana/ts/src/matchingEngine/index.ts | 30 ++--- solana/ts/src/protocol/matchingEngine.ts | 4 +- solana/ts/src/protocol/tokenRouter.ts | 53 +++----- solana/ts/src/testing/mock.ts | 12 +- solana/ts/src/tokenRouter/index.ts | 28 ++-- solana/ts/src/upgradeManager/index.ts | 23 +--- solana/ts/tests/02__tokenRouter.ts | 78 +++++++++-- solana/ts/tests/04__interaction.ts | 1 + solana/ts/tests/12__testnetFork.ts | 38 +----- universal/ts/src/protocol.ts | 11 +- 13 files changed, 196 insertions(+), 293 deletions(-) diff --git a/solana/ts/src/cctp/messageTransmitter/index.ts b/solana/ts/src/cctp/messageTransmitter/index.ts index 78dd0c39..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] | string; +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 c8a00ce0..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] | string; - 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/matchingEngine/index.ts b/solana/ts/src/matchingEngine/index.ts index 66f2bce7..8c038857 100644 --- a/solana/ts/src/matchingEngine/index.ts +++ b/solana/ts/src/matchingEngine/index.ts @@ -68,18 +68,13 @@ import { ReserveFastFillSequenceCompositeOpts, } from "./types"; -export const PROGRAM_IDS = [ - "MatchingEngine11111111111111111111111111111", - "mPydpGUWxzERTNpyvTKdvS7v8kvw5sgwfiP8WQFrXVS", -] as const; -export type ProgramId = (typeof PROGRAM_IDS)[number] | string; +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 class MatchingEngineProgram { - private _programId: ProgramId; private _mint: PublicKey; private _custodian?: Custodian; @@ -87,12 +82,8 @@ export class MatchingEngineProgram { program: Program; - constructor( - connection: Connection, - programId: ProgramId, - private _addresses: MatchingEngine.Addresses, - ) { - this._programId = programId; + constructor(connection: Connection, private _addresses: MatchingEngine.Addresses) { + const programId = _addresses.matchingEngine; this._mint = new PublicKey(_addresses.cctp.usdcMint); this.pdas = programDerivedAddresses( new PublicKey(programId), @@ -100,7 +91,7 @@ export class MatchingEngineProgram { this.coreBridgeProgramId, ); this.program = new Program( - { ...(IDL as any), address: this._programId }, + { ...(IDL as any), address: programId }, { connection, }, @@ -2237,24 +2228,23 @@ export class MatchingEngineProgram { } upgradeManagerProgram(): UpgradeManagerProgram { - return new UpgradeManagerProgram( - this.program.provider.connection, - this._addresses.upgradeManager!, - { ...this._addresses, tokenRouter: this._addresses.tokenRouter! }, - ); + return new UpgradeManagerProgram(this.program.provider.connection, { + ...this._addresses, + tokenRouter: this._addresses.tokenRouter!, + }); } tokenMessengerMinterProgram(): TokenMessengerMinterProgram { return new TokenMessengerMinterProgram( this.program.provider.connection, - this._addresses.cctp.tokenMessenger, + this._addresses.cctp, ); } messageTransmitterProgram(): MessageTransmitterProgram { return new MessageTransmitterProgram( this.program.provider.connection, - this._addresses.cctp.messageTransmitter, + this._addresses.cctp, ); } diff --git a/solana/ts/src/protocol/matchingEngine.ts b/solana/ts/src/protocol/matchingEngine.ts index 015e7032..908c45fc 100644 --- a/solana/ts/src/protocol/matchingEngine.ts +++ b/solana/ts/src/protocol/matchingEngine.ts @@ -29,7 +29,7 @@ import { SolanaUnsignedTransaction, } from "@wormhole-foundation/sdk-solana"; import { vaaHash } from "../common"; -import { AuctionParameters, MatchingEngineProgram, ProgramId } from "../matchingEngine"; +import { AuctionParameters, MatchingEngineProgram } from "../matchingEngine"; import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core"; export class SolanaMatchingEngine @@ -44,7 +44,7 @@ export class SolanaMatchingEngine readonly _connection: Connection, readonly _contracts: Contracts & MatchingEngine.Addresses, ) { - super(_connection, _contracts.matchingEngine, _contracts); + super(_connection, _contracts); this.coreBridge = new SolanaWormholeCore(_network, _chain, _connection, { ...this._contracts, diff --git a/solana/ts/src/protocol/tokenRouter.ts b/solana/ts/src/protocol/tokenRouter.ts index c4713d40..6acdcff2 100644 --- a/solana/ts/src/protocol/tokenRouter.ts +++ b/solana/ts/src/protocol/tokenRouter.ts @@ -9,12 +9,8 @@ import { TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; -import { - FastMarketOrder, - Payload, - TokenRouter, -} from "@wormhole-foundation/example-liquidity-layer-definitions"; -import { ChainId, Network, Platform, encoding, toChainId } from "@wormhole-foundation/sdk-base"; +import { Payload, TokenRouter } from "@wormhole-foundation/example-liquidity-layer-definitions"; +import { ChainId, Network, Platform, toChainId } from "@wormhole-foundation/sdk-base"; import { ChainsConfig, CircleBridge, @@ -47,7 +43,7 @@ export class SolanaTokenRouter readonly _connection: Connection, readonly _contracts: Contracts & TokenRouter.Addresses, ) { - super(_connection, _contracts.tokenRouter, _contracts); + super(_connection, _contracts); this.coreBridge = new SolanaWormholeCore(_network, _chain, _connection, _contracts); this.matchingEngine = new SolanaMatchingEngine(_network, _chain, _connection, _contracts); @@ -89,30 +85,6 @@ export class SolanaTokenRouter yield this.createUnsignedTx({ transaction }, "TokenRouter.Initialize"); } - private makeFastMarketOrder( - sender: AnySolanaAddress, - order: Partial, - ): FastMarketOrder { - const senderAddress = new SolanaAddress(sender).toUniversalAddress(); - const o: FastMarketOrder = { - // TODO: from auction params? take as args? - maxFee: order.maxFee ?? 0n, - initAuctionFee: order.initAuctionFee ?? 0n, - deadline: order.deadline ?? 0, - // TODO: specify which params we need, not just partial - amountIn: order.amountIn!, - minAmountOut: order.minAmountOut!, - targetChain: order.targetChain!, - redeemer: order.redeemer!, - // TODO: which of these can we assume? any - sender: order.sender ?? senderAddress, - refundAddress: order.refundAddress ?? senderAddress, - redeemerMessage: order.redeemerMessage ?? new Uint8Array(), - }; - - return o; - } - private async _prepareMarketOrderIxs( sender: AnySolanaAddress, order: TokenRouter.OrderRequest, @@ -209,14 +181,18 @@ export class SolanaTokenRouter 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, }, - // TODO: add cctpDomain fn - { targetChain }, //,destinationDomain: 3 }, + { targetChain, destinationDomain }, ); ixs.push(ix); @@ -228,15 +204,16 @@ export class SolanaTokenRouter 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 { payload: fill } = vaa.payload; - if (!Payload.is(fill, "Fill")) { - throw new Error("Invalid VAA payload"); - } + 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( { @@ -256,7 +233,7 @@ export class SolanaTokenRouter units: 300_000, }); - const transaction = this.createTx(payer, [ix, computeIx]); + const transaction = this.createTx(payer, [ix, computeIx], lookupTables); yield this.createUnsignedTx({ transaction }, "TokenRouter.RedeemFill"); } diff --git a/solana/ts/src/testing/mock.ts b/solana/ts/src/testing/mock.ts index 89c33328..0b362151 100644 --- a/solana/ts/src/testing/mock.ts +++ b/solana/ts/src/testing/mock.ts @@ -1,8 +1,8 @@ import { Connection, Keypair } from "@solana/web3.js"; -import { FastTransfer } from "@wormhole-foundation/example-liquidity-layer-definitions"; +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 { toUniversal } from "@wormhole-foundation/sdk-definitions"; +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"; @@ -58,8 +58,12 @@ export async function createLiquidityLayerVaa( const published = foreignEmitter.publishMessage(0, msg, 0, timestamp); const vaa = MOCK_GUARDIANS.addSignatures(published, [0]); - // @ts-ignore -- TODO: this is lie, need to define discriminator - return vaa; + try { + return deserialize(FastTransfer.getPayloadDiscriminator(), serialize(vaa)); + } catch { + // @ts-expect-error -- needed to allow testing of invalid payloads + return vaa; + } } export async function postAndFetchVaa( diff --git a/solana/ts/src/tokenRouter/index.ts b/solana/ts/src/tokenRouter/index.ts index af9b0add..8433e279 100644 --- a/solana/ts/src/tokenRouter/index.ts +++ b/solana/ts/src/tokenRouter/index.ts @@ -37,24 +37,19 @@ import { RedeemFillCctpAccounts, TokenRouterCommonAccounts, } from "./types"; - -export const PROGRAM_IDS = [ - "TokenRouter11111111111111111111111111111111", - "tD8RmtdcV7bzBeuFgyrFc8wvayj988ChccEzRQzo6md", -] as const; - -export type ProgramId = (typeof PROGRAM_IDS)[number] | string; +export * from "./types"; export class TokenRouterProgram { - private _programId: ProgramId; private _addresses: TokenRouter.Addresses; program: Program; - constructor(connection: Connection, programId: ProgramId, addresses: TokenRouter.Addresses) { - this._programId = programId; + constructor(connection: Connection, addresses: TokenRouter.Addresses) { this._addresses = addresses; - this.program = new Program({ ...(IDL as any), address: this._programId }, { connection }); + this.program = new Program( + { ...(IDL as any), address: this._addresses.tokenRouter }, + { connection }, + ); } get ID(): PublicKey { @@ -840,31 +835,26 @@ export class TokenRouterProgram { } upgradeManagerProgram(): UpgradeManagerProgram { - return new UpgradeManagerProgram( - this.program.provider.connection, - this.upgradeManager.toBase58(), - this._addresses, - ); + return new UpgradeManagerProgram(this.program.provider.connection, this._addresses); } tokenMessengerMinterProgram(): TokenMessengerMinterProgram { return new TokenMessengerMinterProgram( this.program.provider.connection, - this.cctpTokenMessenger.toBase58(), + this._addresses.cctp, ); } messageTransmitterProgram(): MessageTransmitterProgram { return new MessageTransmitterProgram( this.program.provider.connection, - this.cctpMessageTransmitter.toBase58(), + this._addresses.cctp, ); } matchingEngineProgram(): matchingEngineSdk.MatchingEngineProgram { return new matchingEngineSdk.MatchingEngineProgram( this.program.provider.connection, - this.matchingEngineProgramId.toBase58(), this._addresses, ); } diff --git a/solana/ts/src/upgradeManager/index.ts b/solana/ts/src/upgradeManager/index.ts index 914ef3f0..cbd03306 100644 --- a/solana/ts/src/upgradeManager/index.ts +++ b/solana/ts/src/upgradeManager/index.ts @@ -17,28 +17,13 @@ import { BPF_LOADER_UPGRADEABLE_PROGRAM_ID, programDataAddress } from "../utils" import { UpgradeReceipt } from "./state"; import { TokenRouter } from "@wormhole-foundation/example-liquidity-layer-definitions"; -export const PROGRAM_IDS = [ - "UpgradeManager11111111111111111111111111111", - "ucdP9ktgrXgEUnn6roqD2SfdGMR2JSiWHUKv23oXwxt", -] as const; - -export type ProgramId = (typeof PROGRAM_IDS)[number] | string; - export class UpgradeManagerProgram { - private _programId: ProgramId; program: Program; - constructor( - connection: Connection, - programId: ProgramId, - private _addresses: TokenRouter.Addresses, - ) { - 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 }, ); } @@ -198,7 +183,6 @@ export class UpgradeManagerProgram { matchingEngineProgram(): matchingEngineSdk.MatchingEngineProgram { return new matchingEngineSdk.MatchingEngineProgram( this.program.provider.connection, - this._addresses.matchingEngine, this._addresses, ); } @@ -206,7 +190,6 @@ export class UpgradeManagerProgram { tokenRouterProgram(): tokenRouterSdk.TokenRouterProgram { return new tokenRouterSdk.TokenRouterProgram( this.program.provider.connection, - this._addresses.tokenRouter, this._addresses, ); } diff --git a/solana/ts/tests/02__tokenRouter.ts b/solana/ts/tests/02__tokenRouter.ts index 54e58915..04b1396b 100644 --- a/solana/ts/tests/02__tokenRouter.ts +++ b/solana/ts/tests/02__tokenRouter.ts @@ -11,12 +11,12 @@ import { } from "@solana/web3.js"; import { TokenRouter } from "@wormhole-foundation/example-liquidity-layer-definitions"; import { 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"; import { LiquidityLayerDeposit, LiquidityLayerMessage, uint64ToBN } from "../src/common"; -import { SolanaTokenRouter, SolanaTokenRouterContracts } from "../src/protocol"; +import { SolanaTokenRouter } from "../src/protocol"; import { CircleAttester, DEFAULT_ADDRESSES, @@ -56,9 +56,12 @@ describe("Token Router", function () { 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 contracts: SolanaTokenRouterContracts = DEFAULT_ADDRESSES["Devnet"]!; - const tokenRouter = new SolanaTokenRouter("Devnet", "Solana", connection, contracts); + const tokenRouter = new SolanaTokenRouter( + "Devnet", + "Solana", + connection, + DEFAULT_ADDRESSES["Devnet"]!, + ); let lookupTableAddress: PublicKey; @@ -1526,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, @@ -1689,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 a9dac179..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, diff --git a/solana/ts/tests/12__testnetFork.ts b/solana/ts/tests/12__testnetFork.ts index 2dca508c..a9f4b4f7 100644 --- a/solana/ts/tests/12__testnetFork.ts +++ b/solana/ts/tests/12__testnetFork.ts @@ -27,11 +27,7 @@ describe("Upgrade Manager", function () { 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.upgradeManager, - contracts, - ); + const upgradeManager = new UpgradeManagerProgram(connection, contracts); describe("Upgrade Matching Engine", function () { it("Cannot Execute without Owner", async function () { @@ -239,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" }, ); }); @@ -257,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" }, ); }); @@ -275,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 }, ); }); @@ -385,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/protocol.ts b/universal/ts/src/protocol.ts index 9300853e..502e7efe 100644 --- a/universal/ts/src/protocol.ts +++ b/universal/ts/src/protocol.ts @@ -8,8 +8,9 @@ import { ProtocolVAA, UnsignedTransaction, VAA, + payloadDiscriminator, } from "@wormhole-foundation/sdk-definitions"; -import { FastMarketOrder, MessageName } from "./messages"; +import { FastMarketOrder, MessageName, messageNames } from "./messages"; // Utility types to allow re-use of the same type while making some // fields optional or required @@ -17,7 +18,6 @@ type WithRequired = T & { [P in K]-?: T[P] }; type WithOptional = Pick, K> & Omit; export namespace FastTransfer { - // Add vaas and util methods const protocolName = "FastTransfer"; export type ProtocolName = typeof protocolName; @@ -27,13 +27,16 @@ export namespace FastTransfer { PayloadName >; + /** Addresses for FastTransfer protocol contracts */ export type Addresses = Contracts & { matchingEngine?: string; tokenRouter?: string; upgradeManager?: string; - // Add usdcMint to cctp, mostly for testing + // Add usdcMint to cctp contracts, mostly for testing cctp?: Contracts["cctp"] & { usdcMint: string }; }; + + export const getPayloadDiscriminator = () => payloadDiscriminator([protocolName, messageNames]); } export interface FastTransfer { @@ -49,6 +52,7 @@ export interface FastTransfer { } export namespace MatchingEngine { + /** Contract addresses required for MatchingEngine */ export type Addresses = WithRequired< FastTransfer.Addresses, "matchingEngine" | "coreBridge" | "cctp" @@ -139,6 +143,7 @@ export namespace TokenRouter { ); } + /** Contract addresses required for TokenRouter */ export type Addresses = WithRequired; /** The Address or Id of a prepared order */