From d0a7fffb4bb7aafd423c3229331b3fc14187bea5 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Thu, 17 Oct 2024 19:49:59 -0400 Subject: [PATCH 1/8] solana: Add `mark_outbox_item_as_released` ix --- .../mark_outbox_item_as_released.rs | 36 +++++++++++++++++++ .../src/instructions/mod.rs | 2 ++ .../example-native-token-transfers/src/lib.rs | 4 +++ 3 files changed, 42 insertions(+) create mode 100644 solana/programs/example-native-token-transfers/src/instructions/mark_outbox_item_as_released.rs diff --git a/solana/programs/example-native-token-transfers/src/instructions/mark_outbox_item_as_released.rs b/solana/programs/example-native-token-transfers/src/instructions/mark_outbox_item_as_released.rs new file mode 100644 index 000000000..4ea058b7d --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/mark_outbox_item_as_released.rs @@ -0,0 +1,36 @@ +use crate::{ + config::*, error::NTTError, queue::outbox::OutboxItem, + registered_transceiver::RegisteredTransceiver, +}; +use anchor_lang::prelude::*; + +pub const OUTBOX_ITEM_SIGNER_SEED: &'static [u8] = b"outbox_item_signer"; + +#[derive(Accounts)] +pub struct MarkOutboxItemAsReleased<'info> { + #[account( + seeds = [OUTBOX_ITEM_SIGNER_SEED], + seeds::program = transceiver.transceiver_address, + bump + )] + pub signer: Signer<'info>, + + pub config: NotPausedConfig<'info>, + + #[account( + mut, + constraint = !outbox_item.released.get(transceiver.id)? @ NTTError::MessageAlreadySent, + )] + pub outbox_item: Account<'info, OutboxItem>, + + #[account( + constraint = config.enabled_transceivers.get(transceiver.id)? @ NTTError::DisabledTransceiver + )] + pub transceiver: Account<'info, RegisteredTransceiver>, +} + +pub fn mark_outbox_item_as_released(ctx: Context) -> Result { + let accs = ctx.accounts; + let released = accs.outbox_item.try_release(accs.transceiver.id)?; + Ok(released) +} diff --git a/solana/programs/example-native-token-transfers/src/instructions/mod.rs b/solana/programs/example-native-token-transfers/src/instructions/mod.rs index ed009b00c..d9fec7be5 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/mod.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/mod.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod initialize; pub mod luts; +pub mod mark_outbox_item_as_released; pub mod redeem; pub mod release_inbound; pub mod transfer; @@ -8,6 +9,7 @@ pub mod transfer; pub use admin::*; pub use initialize::*; pub use luts::*; +pub use mark_outbox_item_as_released::*; pub use redeem::*; pub use release_inbound::*; pub use transfer::*; diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 3f10f3c98..ec66c294d 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -152,6 +152,10 @@ pub mod example_native_token_transfers { instructions::set_inbound_limit(ctx, args) } + pub fn mark_outbox_item_as_released(ctx: Context) -> Result { + instructions::mark_outbox_item_as_released(ctx) + } + // standalone transceiver stuff pub fn set_wormhole_peer( From 97cf604800f64d1d775b3ec01b95ba049b7c58d3 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Thu, 17 Oct 2024 19:50:41 -0400 Subject: [PATCH 2/8] solana: Update `release_outbound`to CPI into manager --- .../wormhole/instructions/release_outbound.rs | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/solana/programs/ntt-transceiver/src/wormhole/instructions/release_outbound.rs b/solana/programs/ntt-transceiver/src/wormhole/instructions/release_outbound.rs index 7f57c63fd..fd9a174f3 100644 --- a/solana/programs/ntt-transceiver/src/wormhole/instructions/release_outbound.rs +++ b/solana/programs/ntt-transceiver/src/wormhole/instructions/release_outbound.rs @@ -3,14 +3,21 @@ use anchor_lang::prelude::*; use example_native_token_transfers::{ config::{anchor_reexports::*, *}, error::NTTError, + instructions::OUTBOX_ITEM_SIGNER_SEED, + program::ExampleNativeTokenTransfers, queue::outbox::OutboxItem, - registered_transceiver::*, + registered_transceiver::RegisteredTransceiver, transfer::Payload, }; use ntt_messages::{ ntt::NativeTokenTransfer, ntt_manager::NttManagerMessage, transceiver::TransceiverMessage, transceivers::wormhole::WormholeTransceiver, }; +use solana_program::{ + hash, + instruction::Instruction, + program::{get_return_data, invoke_signed}, +}; #[derive(Accounts)] pub struct ReleaseOutbound<'info> { @@ -48,6 +55,54 @@ pub struct ReleaseOutbound<'info> { pub emitter: UncheckedAccount<'info>, pub wormhole: WormholeAccounts<'info>, + + // NOTE: we put `manager` and `outbox_item_signer` at the end so that the generated + // IDL does not clash with the baked-in transceiver IDL in the manager + pub manager: Program<'info, ExampleNativeTokenTransfers>, + + #[account( + seeds = [OUTBOX_ITEM_SIGNER_SEED], + bump + )] + /// CHECK: this PDA is used to sign the CPI into NTT manager program + pub outbox_item_signer: UncheckedAccount<'info>, +} + +impl<'info> ReleaseOutbound<'info> { + pub fn mark_outbox_item_as_released(&self, bump_seed: u8) -> Result { + // calculate signhash of mark_outbox_item_as_released function + let ix_data = { + let preimage = format!("{}:{}", "global", "mark_outbox_item_as_released"); + let mut sighash = [0u8; 8]; + sighash.copy_from_slice(&hash::hash(preimage.as_bytes()).to_bytes()[..8]); + sighash + }; + // deref config account info from NotPausedConfig + assert!(self.config.to_account_infos().len() == 1); + let config_info = &self.config.to_account_infos()[0]; + // construct CPI call + let account_metas = vec![ + AccountMeta::new_readonly(self.outbox_item_signer.key(), true), + AccountMeta::new_readonly(config_info.key(), false), + AccountMeta::new(self.outbox_item.key(), false), + AccountMeta::new_readonly(self.transceiver.key(), false), + ]; + let instruction = Instruction::new_with_borsh(self.manager.key(), &ix_data, account_metas); + let account_infos = vec![ + self.outbox_item_signer.to_account_info(), + config_info.clone(), + self.outbox_item.to_account_info(), + self.transceiver.to_account_info(), + ]; + invoke_signed( + &instruction, + &account_infos, + &[&[OUTBOX_ITEM_SIGNER_SEED, &[bump_seed]]], + )?; + // get return value + let (_key, data) = get_return_data().unwrap(); + Ok(data.len() == 1 && data[0] == 1) + } } #[derive(AnchorSerialize, AnchorDeserialize)] @@ -55,9 +110,12 @@ pub struct ReleaseOutboundArgs { pub revert_on_delay: bool, } -pub fn release_outbound(ctx: Context, args: ReleaseOutboundArgs) -> Result<()> { +pub fn release_outbound<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseOutbound>, + args: ReleaseOutboundArgs, +) -> Result<()> { let accs = ctx.accounts; - let released = accs.outbox_item.try_release(accs.transceiver.id)?; + let released = accs.mark_outbox_item_as_released(ctx.bumps.outbox_item_signer)?; if !released { if args.revert_on_delay { @@ -67,7 +125,9 @@ pub fn release_outbound(ctx: Context, args: ReleaseOutboundArgs } } + accs.outbox_item.reload()?; assert!(accs.outbox_item.released.get(accs.transceiver.id)?); + let message: TransceiverMessage> = TransceiverMessage::new( // TODO: should we just put the ntt id here statically? From ce0ab7512ac5df2a698e2b0b5d90f575aa25881a Mon Sep 17 00:00:00 2001 From: nvsriram Date: Thu, 10 Oct 2024 12:59:31 -0400 Subject: [PATCH 3/8] solana: Add transceiver_type ix to ntt-transceiver program --- solana/programs/ntt-transceiver/src/lib.rs | 8 +++++++- solana/ts/idl/2_0_0/json/ntt_transceiver.json | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/solana/programs/ntt-transceiver/src/lib.rs b/solana/programs/ntt-transceiver/src/lib.rs index b6a134c78..6f3e45dc1 100644 --- a/solana/programs/ntt-transceiver/src/lib.rs +++ b/solana/programs/ntt-transceiver/src/lib.rs @@ -7,11 +7,17 @@ use wormhole::instructions::*; declare_id!("Ee6jpX9oq2EsGuqGb6iZZxvtcpmMGZk8SAUbnQy4jcHR"); +pub const TRANSCEIVER_TYPE: &str = "wormhole"; + #[program] pub mod ntt_transceiver { use super::*; + pub fn transceiver_type(_ctx: Context) -> Result { + Ok(TRANSCEIVER_TYPE.to_string()) + } + pub fn set_wormhole_peer( ctx: Context, args: SetTransceiverPeerArgs, @@ -43,4 +49,4 @@ pub mod ntt_transceiver { } #[derive(Accounts)] -pub struct Initialize {} +pub struct TransceiverType {} diff --git a/solana/ts/idl/2_0_0/json/ntt_transceiver.json b/solana/ts/idl/2_0_0/json/ntt_transceiver.json index 3d1cadebb..c95628feb 100644 --- a/solana/ts/idl/2_0_0/json/ntt_transceiver.json +++ b/solana/ts/idl/2_0_0/json/ntt_transceiver.json @@ -2,6 +2,12 @@ "version": "2.0.0", "name": "ntt_transceiver", "instructions": [ + { + "name": "transceiverType", + "accounts": [], + "args": [], + "returns": "string" + }, { "name": "setWormholePeer", "accounts": [ From d894530643a6b4aebd79b6e6dcab9769ed9a2755 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Thu, 10 Oct 2024 16:12:56 -0400 Subject: [PATCH 4/8] solana: Update IDL --- solana/ts/idl/2_0_0/ts/ntt_transceiver.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/solana/ts/idl/2_0_0/ts/ntt_transceiver.ts b/solana/ts/idl/2_0_0/ts/ntt_transceiver.ts index 47ca8cfe8..386f041f4 100644 --- a/solana/ts/idl/2_0_0/ts/ntt_transceiver.ts +++ b/solana/ts/idl/2_0_0/ts/ntt_transceiver.ts @@ -2,6 +2,12 @@ export type NttTransceiver = { "version": "2.0.0", "name": "ntt_transceiver", "instructions": [ + { + "name": "transceiverType", + "accounts": [], + "args": [], + "returns": "string" + }, { "name": "setWormholePeer", "accounts": [ @@ -684,6 +690,12 @@ export const IDL: NttTransceiver = { "version": "2.0.0", "name": "ntt_transceiver", "instructions": [ + { + "name": "transceiverType", + "accounts": [], + "args": [], + "returns": "string" + }, { "name": "setWormholePeer", "accounts": [ From ee540ba609355495baf9b0e7348dc6d36092005c Mon Sep 17 00:00:00 2001 From: nvsriram Date: Thu, 10 Oct 2024 16:14:01 -0400 Subject: [PATCH 5/8] solana[WIP]: Update SDK to support generic multi-transceivers --- sdk/definitions/src/ntt.ts | 46 ++- solana/tests/anchor.test.ts | 16 +- solana/ts/lib/ntt.ts | 509 ++++++++++++------------- solana/ts/sdk/ntt.ts | 742 +++++++++++++++++++----------------- 4 files changed, 674 insertions(+), 639 deletions(-) diff --git a/sdk/definitions/src/ntt.ts b/sdk/definitions/src/ntt.ts index 96f021e31..e438fbb64 100644 --- a/sdk/definitions/src/ntt.ts +++ b/sdk/definitions/src/ntt.ts @@ -38,7 +38,7 @@ export namespace Ntt { token: string; manager: string; transceiver: { - wormhole?: string; + [type: string]: string; }; quoter?: string; }; @@ -163,21 +163,23 @@ export interface Ntt { isPaused(): Promise; - pause( - payer?: AccountAddress - ): AsyncGenerator>; + pause(payer?: AccountAddress): AsyncGenerator>; - unpause( - payer?: AccountAddress - ): AsyncGenerator>; + unpause(payer?: AccountAddress): AsyncGenerator>; getOwner(): Promise>; getPauser(): Promise | null>; - setOwner(newOwner: AccountAddress, payer?: AccountAddress): AsyncGenerator>; + setOwner( + newOwner: AccountAddress, + payer?: AccountAddress + ): AsyncGenerator>; - setPauser(newOwner: AccountAddress, payer?: AccountAddress): AsyncGenerator>; + setPauser( + newOwner: AccountAddress, + payer?: AccountAddress + ): AsyncGenerator>; getThreshold(): Promise; @@ -188,7 +190,8 @@ export interface Ntt { payer?: AccountAddress ): AsyncGenerator>; - setWormholeTransceiverPeer( + setTransceiverPeer( + transceiverType: string, peer: ChainAddress, payer?: AccountAddress ): AsyncGenerator>; @@ -228,7 +231,7 @@ export interface Ntt { * @param attestations The attestations to redeem, the length should be equal to the number of transceivers */ redeem( - attestations: Ntt.Attestation[], + attestations: Map, payer?: AccountAddress ): AsyncGenerator>; @@ -241,7 +244,10 @@ export interface Ntt { /** Get the peer information for the given chain if it exists */ getPeer(chain: C): Promise | null>; - getTransceiver(ix: number): Promise | null>; + /** Get the transceiver corresponding to type */ + getTransceiver( + type: string + ): Promise | null>; /** * getCurrentOutboundCapacity returns the current outbound capacity of the Ntt manager @@ -256,7 +262,10 @@ export interface Ntt { /** * setOutboundLimit sets the maximum outbound capacity of the Ntt manager */ - setOutboundLimit(limit: bigint, payer?: AccountAddress): AsyncGenerator>; + setOutboundLimit( + limit: bigint, + payer?: AccountAddress + ): AsyncGenerator>; /** * getCurrentInboundCapacity returns the current inbound capacity of the Ntt manager @@ -346,17 +355,22 @@ export interface NttTransceiver< C extends Chain, A extends Ntt.Attestation > { - getAddress(): ChainAddress; /** setPeer sets a peer address for a given chain * Note: Admin only */ - setPeer(peer: ChainAddress, payer?: AccountAddress): AsyncGenerator>; + setPeer( + peer: ChainAddress, + payer?: AccountAddress + ): AsyncGenerator>; getPeer(chain: C): Promise | null>; - setPauser(newPauser: AccountAddress, payer?: AccountAddress): AsyncGenerator>; + setPauser( + newPauser: AccountAddress, + payer?: AccountAddress + ): AsyncGenerator>; getPauser(): Promise | null>; diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index e3605fe84..dc9ffe4dc 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -235,18 +235,16 @@ describe("example-native-token-transfers", () => { await signSendWait(ctx, initTxs, signer); // register - const registerTxs = ntt.registerTransceiver({ + const registerTxs = ntt.registerWormholeTransceiver({ payer: new SolanaAddress(payer.publicKey), owner: new SolanaAddress(payer.publicKey), - transceiver: nttTransceiver, }); await signSendWait(ctx, registerTxs, signer); // Set Wormhole xcvr peer - const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer2( + const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer( remoteXcvr, - sender, - nttTransceiver + sender ); await signSendWait(ctx, setXcvrPeerTxs, signer); @@ -296,12 +294,12 @@ describe("example-native-token-transfers", () => { // TODO: keep or remove the `outboxItem` param? // added as a way to keep tests the same but it technically breaks the Ntt interface const outboxItem = anchor.web3.Keypair.generate(); - const xferTxs = ntt.transfer2( + const xferTxs = ntt.transfer( sender, amount, receiver, { queue: false, automatic: false, gasDropoff: 0n }, - nttTransceiver, + "wormhole", outboxItem ); await signSendWait(ctx, xferTxs, signer); @@ -315,6 +313,7 @@ describe("example-native-token-transfers", () => { wormholeMessage ); + // TODO4: where is wormhole transfer coming from? const transceiverMessage = deserializePayload( "Ntt:WormholeTransfer", unsignedVaa.payload @@ -369,7 +368,8 @@ describe("example-native-token-transfers", () => { const published = emitter.publishMessage(0, serialized, 200); const rawVaa = guardians.addSignatures(published, [0]); const vaa = deserialize("Ntt:WormholeTransfer", serialize(rawVaa)); - const redeemTxs = ntt.redeem2([vaa], sender, nttTransceiver); + const vaaMap = new Map([["wormhole", vaa]]); + const redeemTxs = ntt.redeem(vaaMap, sender); try { await signSendWait(ctx, redeemTxs, signer); } catch (e) { diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index bf55fa9db..246bacf0f 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -12,11 +12,9 @@ import { AddressLookupTableProgram, Commitment, Connection, - Keypair, PublicKey, PublicKeyInitData, SystemProgram, - Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction, @@ -39,7 +37,6 @@ import { import { Ntt } from "@wormhole-foundation/sdk-definitions-ntt"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; -import { SolanaTransaction } from "@wormhole-foundation/sdk-solana"; import { utils } from "@wormhole-foundation/sdk-solana-core"; import { IdlVersion, @@ -55,7 +52,6 @@ import { programDataAddress, programVersionLayout, } from "./utils.js"; -import { NttTransceiver as NttTransceiverIdl } from "../idl/2_0_0/ts/ntt_transceiver.js"; export namespace NTT { /** Arguments for transfer instruction */ @@ -87,7 +83,6 @@ export namespace NTT { /** pdas returns an object containing all functions to compute program addresses */ export const pdas = (programId: PublicKeyInitData) => { const configAccount = (): PublicKey => derivePda("config", programId); - const emitterAccount = (): PublicKey => derivePda("emitter", programId); const inboxRateLimitAccount = (chain: Chain): PublicKey => derivePda(["inbox_rate_limit", chainToBytes(chain)], programId); const inboxItemAccount = ( @@ -104,17 +99,8 @@ export namespace NTT { derivePda("token_authority", programId); const peerAccount = (chain: Chain): PublicKey => derivePda(["peer", chainToBytes(chain)], programId); - const transceiverPeerAccount = (chain: Chain): PublicKey => - derivePda(["transceiver_peer", chainToBytes(chain)], programId); const registeredTransceiver = (transceiver: PublicKey): PublicKey => derivePda(["registered_transceiver", transceiver.toBytes()], programId); - const transceiverMessageAccount = ( - chain: Chain, - id: Uint8Array - ): PublicKey => - derivePda(["transceiver_message", chainToBytes(chain), id], programId); - const wormholeMessageAccount = (outboxItem: PublicKey): PublicKey => - derivePda(["message", outboxItem.toBytes()], programId); const lutAccount = (): PublicKey => derivePda("lut", programId); const lutAuthority = (): PublicKey => derivePda("lut_authority", programId); const sessionAuthority = ( @@ -145,17 +131,37 @@ export namespace NTT { inboxItemAccount, sessionAuthority, tokenAuthority, - emitterAccount, - wormholeMessageAccount, peerAccount, - transceiverPeerAccount, - transceiverMessageAccount, registeredTransceiver, lutAccount, lutAuthority, }; }; + /** Type of object containing methods to compute program addresses */ + export type TransceiverPdas = ReturnType; + /** pdas returns an object containing all functions to compute program addresses */ + export const transceiverPdas = (programId: PublicKeyInitData) => { + const emitterAccount = (): PublicKey => derivePda("emitter", programId); + const transceiverPeerAccount = (chain: Chain): PublicKey => + derivePda(["transceiver_peer", chainToBytes(chain)], programId); + const transceiverMessageAccount = ( + chain: Chain, + id: Uint8Array + ): PublicKey => + derivePda(["transceiver_message", chainToBytes(chain), id], programId); + const wormholeMessageAccount = (outboxItem: PublicKey): PublicKey => + derivePda(["message", outboxItem.toBytes()], programId); + + // TODO: memoize? + return { + emitterAccount, + transceiverPeerAccount, + transceiverMessageAccount, + wormholeMessageAccount, + }; + }; + export async function getVersion( connection: Connection, programId: PublicKey, @@ -522,57 +528,57 @@ export namespace NTT { /** * Creates a release_outbound instruction. The `payer` needs to sign the transaction. */ - export async function createReleaseOutboundInstruction( - program: Program>, - args: { - wormholeId: PublicKey; - payer: PublicKey; - outboxItem: PublicKey; - revertOnDelay: boolean; - transceiver?: Program; - }, - pdas?: Pdas - ): Promise { - pdas = pdas ?? NTT.pdas(program.programId); - - const whAccs = utils.getWormholeDerivedAccounts( - program.programId, - args.wormholeId - ); - - if (!args.transceiver) { - throw Error("no transceiver provided"); - } - - return await args.transceiver.methods - .releaseWormholeOutbound({ - revertOnDelay: args.revertOnDelay, - }) - .accounts({ - payer: args.payer, - config: { config: pdas.configAccount() }, - outboxItem: args.outboxItem, - wormholeMessage: derivePda( - ["message", args.outboxItem.toBytes()], - args.transceiver.programId - ), - emitter: derivePda("emitter", args.transceiver.programId), - transceiver: pdas.registeredTransceiver(args.transceiver.programId), - wormhole: { - bridge: whAccs.wormholeBridge, - feeCollector: whAccs.wormholeFeeCollector, - sequence: derivePda( - [ - "Sequence", - derivePda("emitter", args.transceiver.programId).toBytes(), - ], - args.wormholeId - ), - program: args.wormholeId, - }, - }) - .instruction(); - } + // export async function createReleaseOutboundInstruction( + // program: Program>, + // args: { + // wormholeId: PublicKey; + // payer: PublicKey; + // outboxItem: PublicKey; + // revertOnDelay: boolean; + // transceiver?: Program; + // }, + // pdas?: Pdas + // ): Promise { + // pdas = pdas ?? NTT.pdas(program.programId); + + // const whAccs = utils.getWormholeDerivedAccounts( + // program.programId, + // args.wormholeId + // ); + + // if (!args.transceiver) { + // throw Error("no transceiver provided"); + // } + + // return await args.transceiver.methods + // .releaseWormholeOutbound({ + // revertOnDelay: args.revertOnDelay, + // }) + // .accounts({ + // payer: args.payer, + // config: { config: pdas.configAccount() }, + // outboxItem: args.outboxItem, + // wormholeMessage: derivePda( + // ["message", args.outboxItem.toBytes()], + // args.transceiver.programId + // ), + // emitter: derivePda("emitter", args.transceiver.programId), + // transceiver: pdas.registeredTransceiver(args.transceiver.programId), + // wormhole: { + // bridge: whAccs.wormholeBridge, + // feeCollector: whAccs.wormholeFeeCollector, + // sequence: derivePda( + // [ + // "Sequence", + // derivePda("emitter", args.transceiver.programId).toBytes(), + // ], + // args.wormholeId + // ), + // program: args.wormholeId, + // }, + // }) + // .instruction(); + // } // TODO: document that if recipient is provided, then the instruction can be // created before the inbox item is created (i.e. they can be put in the same tx) @@ -804,133 +810,133 @@ export namespace NTT { .instruction(); } - export async function setWormholeTransceiverPeer( - program: Program>, - args: { - wormholeId: PublicKey; - payer: PublicKey; - owner: PublicKey; - chain: Chain; - address: ArrayLike; - transceiver?: Program; - }, - pdas?: Pdas - ) { - pdas = pdas ?? NTT.pdas(program.programId); - if (!args.transceiver) { - throw new Error("no trx provided"); - } - const ix = await args.transceiver.methods - .setWormholePeer({ - chainId: { id: toChainId(args.chain) }, - address: Array.from(args.address), - }) - .accounts({ - payer: args.payer, - owner: args.owner, - config: pdas.configAccount(), - peer: derivePda( - ["transceiver_peer", chainToBytes(args.chain)], - args.transceiver.programId - ), - }) - .instruction(); - - const wormholeMessage = Keypair.generate(); - const whAccs = utils.getWormholeDerivedAccounts( - program.programId, - args.wormholeId - ); - - const broadcastIx = await args.transceiver.methods - .broadcastWormholePeer({ chainId: toChainId(args.chain) }) - .accounts({ - payer: args.payer, - config: pdas.configAccount(), - peer: derivePda( - ["transceiver_peer", chainToBytes(args.chain)], - args.transceiver.programId - ), - wormholeMessage: wormholeMessage.publicKey, - emitter: derivePda("emitter", args.transceiver.programId), - wormhole: { - bridge: whAccs.wormholeBridge, - feeCollector: whAccs.wormholeFeeCollector, - sequence: derivePda( - [ - "Sequence", - derivePda("emitter", args.transceiver.programId).toBytes(), - ], - args.wormholeId - ), - program: args.wormholeId, - }, - }) - .instruction(); - - const transaction = new Transaction().add(ix, broadcastIx); - transaction.feePayer = args.payer; - return { - transaction, - signers: [wormholeMessage], - } as SolanaTransaction; - } - - export async function registerTransceiver( - program: Program>, - config: NttBindings.Config, - args: { - wormholeId: PublicKey; - payer: PublicKey; - owner: PublicKey; - transceiver: PublicKey; - }, - pdas?: Pdas - ) { - pdas = pdas ?? NTT.pdas(program.programId); - const ix = await program.methods - .registerTransceiver() - .accounts({ - payer: args.payer, - owner: args.owner, - config: pdas.configAccount(), - transceiver: args.transceiver, - registeredTransceiver: pdas.registeredTransceiver(args.transceiver), - }) - .instruction(); - - const wormholeMessage = Keypair.generate(); - const whAccs = utils.getWormholeDerivedAccounts( - program.programId, - args.wormholeId - ); - const broadcastIx = await program.methods - .broadcastWormholeId() - .accountsStrict({ - payer: args.payer, - config: pdas.configAccount(), - mint: config.mint, - wormholeMessage: wormholeMessage.publicKey, - emitter: pdas.emitterAccount(), - wormhole: { - bridge: whAccs.wormholeBridge, - feeCollector: whAccs.wormholeFeeCollector, - sequence: whAccs.wormholeSequence, - program: args.wormholeId, - systemProgram: SystemProgram.programId, - clock: web3.SYSVAR_CLOCK_PUBKEY, - rent: web3.SYSVAR_RENT_PUBKEY, - }, - }) - .instruction(); - - const transaction = new Transaction().add(ix, broadcastIx); - transaction.feePayer = args.payer; - return { - transaction, - signers: [wormholeMessage], - }; - } + // export async function setWormholeTransceiverPeer( + // program: Program>, + // args: { + // wormholeId: PublicKey; + // payer: PublicKey; + // owner: PublicKey; + // chain: Chain; + // address: ArrayLike; + // transceiver?: Program; + // }, + // pdas?: Pdas + // ) { + // pdas = pdas ?? NTT.pdas(program.programId); + // if (!args.transceiver) { + // throw new Error("no trx provided"); + // } + // const ix = await args.transceiver.methods + // .setWormholePeer({ + // chainId: { id: toChainId(args.chain) }, + // address: Array.from(args.address), + // }) + // .accounts({ + // payer: args.payer, + // owner: args.owner, + // config: pdas.configAccount(), + // peer: derivePda( + // ["transceiver_peer", chainToBytes(args.chain)], + // args.transceiver.programId + // ), + // }) + // .instruction(); + + // const wormholeMessage = Keypair.generate(); + // const whAccs = utils.getWormholeDerivedAccounts( + // program.programId, // xcvr programId + // args.wormholeId + // ); + + // const broadcastIx = await args.transceiver.methods + // .broadcastWormholePeer({ chainId: toChainId(args.chain) }) + // .accounts({ + // payer: args.payer, + // config: pdas.configAccount(), + // peer: derivePda( + // ["transceiver_peer", chainToBytes(args.chain)], + // args.transceiver.programId + // ), + // wormholeMessage: wormholeMessage.publicKey, + // emitter: derivePda("emitter", args.transceiver.programId), + // wormhole: { + // bridge: whAccs.wormholeBridge, + // feeCollector: whAccs.wormholeFeeCollector, + // sequence: derivePda( + // [ + // "Sequence", + // derivePda("emitter", args.transceiver.programId).toBytes(), + // ], + // args.wormholeId + // ), + // program: args.wormholeId, + // }, + // }) + // .instruction(); + + // const transaction = new Transaction().add(ix, broadcastIx); + // transaction.feePayer = args.payer; + // return { + // transaction, + // signers: [wormholeMessage], + // } as SolanaTransaction; + // } + + // export async function registerTransceiver( + // program: Program>, + // config: NttBindings.Config, + // args: { + // wormholeId: PublicKey; + // payer: PublicKey; + // owner: PublicKey; + // transceiver: PublicKey; + // }, + // pdas?: Pdas + // ) { + // pdas = pdas ?? NTT.pdas(program.programId); + // const ix = await program.methods + // .registerTransceiver() + // .accounts({ + // payer: args.payer, + // owner: args.owner, + // config: pdas.configAccount(), + // transceiver: args.transceiver, + // registeredTransceiver: pdas.registeredTransceiver(args.transceiver), + // }) + // .instruction(); + + // const wormholeMessage = Keypair.generate(); + // const whAccs = utils.getWormholeDerivedAccounts( + // program.programId, + // args.wormholeId + // ); + // const broadcastIx = await program.methods + // .broadcastWormholeId() + // .accountsStrict({ + // payer: args.payer, + // config: pdas.configAccount(), + // mint: config.mint, + // wormholeMessage: wormholeMessage.publicKey, + // emitter: pdas.emitterAccount(), + // wormhole: { + // bridge: whAccs.wormholeBridge, + // feeCollector: whAccs.wormholeFeeCollector, + // sequence: whAccs.wormholeSequence, + // program: args.wormholeId, + // systemProgram: SystemProgram.programId, + // clock: web3.SYSVAR_CLOCK_PUBKEY, + // rent: web3.SYSVAR_RENT_PUBKEY, + // }, + // }) + // .instruction(); + + // const transaction = new Transaction().add(ix, broadcastIx); + // transaction.feePayer = args.payer; + // return { + // transaction, + // signers: [wormholeMessage], + // }; + // } export async function createSetOutboundLimitInstruction( program: Program>, @@ -976,87 +982,78 @@ export namespace NTT { .instruction(); } - export async function createReceiveWormholeMessageInstruction( - program: Program>, - args: { - wormholeId: PublicKey; - payer: PublicKey; - vaa: VAA<"Ntt:WormholeTransfer">; - transceiver?: Program; - }, - pdas?: Pdas - ): Promise { - pdas = pdas ?? NTT.pdas(program.programId); - - const wormholeNTT = args.vaa; - const nttMessage = wormholeNTT.payload.nttManagerPayload; - const chain = wormholeNTT.emitterChain; - - if (!args.transceiver) { - throw Error("no transceiver provided"); - } - - const transceiverPeer = derivePda( - ["transceiver_peer", chainToBytes(chain)], - args.transceiver.programId - ); - - return args.transceiver.methods - .receiveWormholeMessage() - .accounts({ - payer: args.payer, - config: { config: pdas.configAccount() }, - peer: transceiverPeer, - vaa: utils.derivePostedVaaKey( - args.wormholeId, - Buffer.from(wormholeNTT.hash) - ), - transceiverMessage: derivePda( - ["transceiver_message", chainToBytes(chain), nttMessage.id], - args.transceiver.programId - ), - }) - .instruction(); - } + // export async function createReceiveWormholeMessageInstruction( + // program: Program>, + // args: { + // wormholeId: PublicKey; + // payer: PublicKey; + // vaa: VAA<"Ntt:WormholeTransfer">; + // transceiver?: Program; + // }, + // pdas?: Pdas + // ): Promise { + // pdas = pdas ?? NTT.pdas(program.programId); + + // const wormholeNTT = args.vaa; + // const nttMessage = wormholeNTT.payload.nttManagerPayload; + // const chain = wormholeNTT.emitterChain; + + // if (!args.transceiver) { + // throw Error("no transceiver provided"); + // } + + // const transceiverPeer = derivePda( + // ["transceiver_peer", chainToBytes(chain)], + // args.transceiver.programId + // ); + + // return args.transceiver.methods + // .receiveWormholeMessage() + // .accounts({ + // payer: args.payer, + // config: { config: pdas.configAccount() }, + // peer: transceiverPeer, + // vaa: utils.derivePostedVaaKey( + // args.wormholeId, + // Buffer.from(wormholeNTT.hash) + // ), + // transceiverMessage: derivePda( + // ["transceiver_message", chainToBytes(chain), nttMessage.id], + // args.transceiver.programId + // ), + // }) + // .instruction(); + // } export async function createRedeemInstruction( program: Program>, config: NttBindings.Config, + transceiverProgramId: PublicKey, args: { payer: PublicKey; vaa: VAA<"Ntt:WormholeTransfer">; - transceiver?: Program; }, - pdas?: Pdas + pdas?: Pdas, + transceiverPdas?: TransceiverPdas ): Promise { pdas = pdas ?? NTT.pdas(program.programId); + transceiverPdas = + transceiverPdas ?? NTT.transceiverPdas(transceiverProgramId); const wormholeNTT = args.vaa; const nttMessage = wormholeNTT.payload.nttManagerPayload; const chain = wormholeNTT.emitterChain; - if (!args.transceiver) { - throw Error("no transceiver provided"); - } - - console.log( - ( - await program.account.registeredTransceiver.fetch( - pdas.registeredTransceiver(args.transceiver.programId) - ) - ).transceiverAddress.toBase58() - ); - return program.methods .redeem({}) .accounts({ payer: args.payer, config: pdas.configAccount(), peer: pdas.peerAccount(chain), - transceiverMessage: derivePda( - ["transceiver_message", chainToBytes(chain), nttMessage.id], - args.transceiver.programId + transceiverMessage: transceiverPdas.transceiverMessageAccount( + chain, + nttMessage.id ), - transceiver: pdas.registeredTransceiver(args.transceiver.programId), + transceiver: pdas.registeredTransceiver(transceiverProgramId), mint: config.mint, inboxItem: pdas.inboxItemAccount(chain, nttMessage), inboxRateLimit: pdas.inboxRateLimitAccount(chain), diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index d810177c2..654ca2189 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -13,7 +13,7 @@ import { VersionedTransaction, } from "@solana/web3.js"; -import { Chain, Network } from "@wormhole-foundation/sdk-base"; +import { Chain, Network, toChainId } from "@wormhole-foundation/sdk-base"; import { AccountAddress, ChainAddress, @@ -42,18 +42,27 @@ import { utils, } from "@wormhole-foundation/sdk-solana-core"; import BN from "bn.js"; +import { + type NttTransceiver as NttTransceiverIdlType, + IDL as NttTransceiverIdl, +} from "../idl/2_0_0/ts/ntt_transceiver.js"; import { NTT, NttQuoter, WEI_PER_GWEI } from "../lib/index.js"; -import { NttTransceiver as NttTransceiverIdl } from "../idl/2_0_0/ts/ntt_transceiver.js"; import { IdlVersion, NttBindings, getNttProgram } from "../lib/bindings.js"; -import { derivePda } from "../lib/utils.js"; export class SolanaNttWormholeTransceiver< N extends Network, C extends SolanaChains > implements NttTransceiver { - constructor(readonly manager: SolanaNtt, readonly address: PublicKey) {} + pdas: NTT.TransceiverPdas; + + constructor( + readonly manager: SolanaNtt, + readonly program: Program + ) { + this.pdas = NTT.transceiverPdas(program.programId); + } async getPauser(): Promise | null> { return null; @@ -71,21 +80,109 @@ export class SolanaNttWormholeTransceiver< throw new Error("Method not implemented."); } + async receiveIx( + attestation: WormholeNttTransceiver.VAA<"WormholeTransfer">, + payer: PublicKey + ) { + const nttMessage = attestation.payload.nttManagerPayload; + const chain = attestation.emitterChain; + return this.program.methods + .receiveWormholeMessage() + .accounts({ + payer, + config: { config: this.manager.pdas.configAccount() }, + peer: this.pdas.transceiverPeerAccount(chain), + vaa: utils.derivePostedVaaKey( + this.manager.core.address, + Buffer.from(attestation.hash) + ), + transceiverMessage: this.pdas.transceiverMessageAccount( + chain, + nttMessage.id + ), + }) + .instruction(); + } + + // async getTransceiverType(sender?: PublicKey): Promise { + // if (!sender) { + // const address = + // connection.rpcEndpoint === rpc.rpcAddress("Devnet", "Solana") + // ? "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J" // The CI pubkey, funded on ci network + // : connection.rpcEndpoint.startsWith("http://localhost") + // ? "98evdAiWr7ey9MAQzoQQMwFQkTsSR6KkWQuFqKrgwNwb" // the anchor pubkey, funded on local network + // : "Hk3SdYTJFpawrvRz4qRztuEt2SqoCG7BGj2yJfDJSFbJ"; // The default pubkey is funded on mainnet and devnet we need a funded account to simulate the transaction below + // sender = new PublicKey(address); + // } + + // const ix = await this.program.methods + // .transceiverType() + // .accountsStrict({}) + // .instruction(); + // // Since we don't need the very very very latest blockhash, using finalized + // // ensures the blockhash will be found when we immediately simulate the tx + // const { blockhash } = await this.program.provider.connection.getLatestBlockhash( + // "finalized" + // ); + // const msg = new TransactionMessage({ + // payerKey: sender, + // recentBlockhash: blockhash, + // instructions: [ix], + // }).compileToV0Message(); + + // const tx = new VersionedTransaction(msg); + + // const txSimulation = await program.provider.connection.simulateTransaction( + // tx, + // { sigVerify: false } + // ); + // } + getAddress(): ChainAddress { return { chain: this.manager.chain, - address: toUniversal(this.manager.chain, this.address.toBase58()), + address: toUniversal( + this.manager.chain, + this.program.programId.toBase58() + ), }; } async *setPeer(peer: ChainAddress, payer: AccountAddress) { - yield* this.manager.setWormholeTransceiverPeer(peer, payer); + const sender = new SolanaAddress(payer).unwrap(); + const ix = await this.program.methods + .setWormholePeer({ + chainId: { id: toChainId(peer.chain) }, + address: Array.from(peer.address.toUniversalAddress().toUint8Array()), + }) + .accounts({ + payer: sender, + owner: sender, + config: this.manager.pdas.configAccount(), + peer: this.pdas.transceiverPeerAccount(peer.chain), + }) + .instruction(); + + const wormholeMessage = Keypair.generate(); + const broadcastIx = await this.broadcastWormholePeer( + peer.chain, + sender, + wormholeMessage.publicKey + ); + + const tx = new Transaction(); + tx.feePayer = sender; + tx.add(ix, broadcastIx); + yield this.manager.createUnsignedTx( + { transaction: tx, signers: [wormholeMessage] }, + "Ntt.Ntt.SetWormholeTransceiverPeer" + ); } async getPeer(chain: C): Promise | null> { const peer = await this.manager.program.account.transceiverPeer.fetchNullable( - this.manager.pdas.transceiverPeerAccount(chain) + this.pdas.transceiverPeerAccount(chain) ); if (!peer) return null; @@ -95,6 +192,98 @@ export class SolanaNttWormholeTransceiver< address: toUniversal(chain, new Uint8Array(peer.address)), }; } + + async broadcastWormholeId( + payer: PublicKey, + config: NttBindings.Config, + wormholeMessage: PublicKey + ): Promise { + const whAccs = utils.getWormholeDerivedAccounts( + this.program.programId, + this.manager.core.address + ); + + return this.program.methods + .broadcastWormholeId() + .accountsStrict({ + payer, + config: this.manager.pdas.configAccount(), + mint: config.mint, + wormholeMessage: wormholeMessage, + emitter: whAccs.wormholeEmitter, + wormhole: { + bridge: whAccs.wormholeBridge, + feeCollector: whAccs.wormholeFeeCollector, + sequence: whAccs.wormholeSequence, + program: this.manager.core.address, + systemProgram: SystemProgram.programId, + clock: web3.SYSVAR_CLOCK_PUBKEY, + rent: web3.SYSVAR_RENT_PUBKEY, + }, + }) + .instruction(); + } + + async broadcastWormholePeer( + chain: Chain, + payer: PublicKey, + wormholeMessage: PublicKey + ): Promise { + const whAccs = utils.getWormholeDerivedAccounts( + this.program.programId, + this.manager.core.address + ); + + return this.program.methods + .broadcastWormholePeer({ chainId: toChainId(chain) }) + .accounts({ + payer: payer, + config: this.manager.pdas.configAccount(), + peer: this.pdas.transceiverPeerAccount(chain), + wormholeMessage: wormholeMessage, + emitter: whAccs.wormholeEmitter, + wormhole: { + bridge: whAccs.wormholeBridge, + feeCollector: whAccs.wormholeFeeCollector, + sequence: whAccs.wormholeSequence, + program: this.manager.core.address, + }, + }) + .instruction(); + } + + async releaseWormholeOutbound( + payer: PublicKey, + outboxItem: PublicKey, + revertOnDelay: boolean + ): Promise { + const whAccs = utils.getWormholeDerivedAccounts( + this.program.programId, + this.manager.core.address + ); + + return this.program.methods + .releaseWormholeOutbound({ + revertOnDelay: revertOnDelay, + }) + .accounts({ + payer, + config: { config: this.manager.pdas.configAccount() }, + outboxItem, + wormholeMessage: this.pdas.wormholeMessageAccount(outboxItem), + emitter: whAccs.wormholeEmitter, + transceiver: this.manager.pdas.registeredTransceiver( + this.program.programId + ), + wormhole: { + bridge: whAccs.wormholeBridge, + feeCollector: whAccs.wormholeFeeCollector, + sequence: whAccs.wormholeSequence, + program: this.manager.core.address, + }, + }) + .instruction(); + } } export class SolanaNtt @@ -109,11 +298,13 @@ export class SolanaNtt quoter?: NttQuoter; addressLookupTable?: AddressLookupTableAccount; + // key refers to `transceiver_type` + transceivers: Map>; + // NOTE: these are stored from the constructor, but are not used directly // (only in verifyAddresses) private managerAddress: string; private tokenAddress: string; - private whTransceiverAddress?: string; constructor( readonly network: N, @@ -130,9 +321,24 @@ export class SolanaNtt version as IdlVersion ); + this.transceivers = new Map(); + for (const [transceiverType, transceiverProgramId] of Object.entries( + contracts.ntt.transceiver + )) { + this.transceivers.set( + transceiverType, + new Program( + NttTransceiverIdl, + transceiverProgramId, + { + connection, + } + ) + ); + } + this.managerAddress = contracts.ntt.manager; this.tokenAddress = contracts.ntt.token; - this.whTransceiverAddress = contracts.ntt.transceiver.wormhole; if (this.contracts.ntt?.quoter) this.quoter = new NttQuoter( @@ -150,14 +356,26 @@ export class SolanaNtt this.pdas = NTT.pdas(this.program.programId); } - async getTransceiver(ix: number): Promise | null> { - if (ix !== 0) return null; - if (this.whTransceiverAddress === undefined) return null; + async getTransceiver( + type: T + ): Promise< + | (T extends "wormhole" + ? SolanaNttWormholeTransceiver + : NttTransceiver) + | null + > { + const transceiverProgram = this.transceivers.get(type) ?? null; + if (!transceiverProgram) return null; + if (type === "wormhole") + return new SolanaNttWormholeTransceiver(this, transceiverProgram); + return null; + } - return new SolanaNttWormholeTransceiver( - this, - new PublicKey(this.whTransceiverAddress) - ); + async getWormholeTransceiver(): Promise | null> { + return this.getTransceiver("wormhole"); } async getMode(): Promise { @@ -223,7 +441,7 @@ export class SolanaNtt } async *setPauser(_newPauser: AnySolanaAddress, _payer: AccountAddress) { - throw new Error("Pauser role not supported on Solna."); + throw new Error("Pauser role not supported on Solana."); } async isRelayingAvailable(destination: Chain): Promise { @@ -381,62 +599,26 @@ export class SolanaNtt yield this.createUnsignedTx({ transaction: tx }, "Ntt.InitializeLUT"); } - async *registerTransceiver(args: { + async *registerWormholeTransceiver(args: { payer: AccountAddress; owner: AccountAddress; - transceiver: Program; }) { - const config = await this.getConfig(); const payer = new SolanaAddress(args.payer).unwrap(); const owner = new SolanaAddress(args.owner).unwrap(); + + const config = await this.getConfig(); if (config.paused) throw new Error("Contract is paused"); - const ix = await this.program.methods - .registerTransceiver() - .accountsStrict({ - payer, - owner, - config: this.pdas.configAccount(), - transceiver: args.transceiver.programId, - registeredTransceiver: this.pdas.registeredTransceiver( - args.transceiver.programId - ), - systemProgram: SystemProgram.programId, - }) - .instruction(); + const ix = await this.registerTransceiver("wormhole", payer, owner); + const whTransceiver = (await this.getWormholeTransceiver())!; const wormholeMessage = Keypair.generate(); - const whAccs = utils.getWormholeDerivedAccounts( - this.program.programId, - this.core.address + const broadcastIx = await whTransceiver.broadcastWormholeId( + payer, + config, + wormholeMessage.publicKey ); - const broadcastIx = await args.transceiver.methods - .broadcastWormholeId() - .accountsStrict({ - payer, - config: this.pdas.configAccount(), - mint: config.mint, - wormholeMessage: wormholeMessage.publicKey, - emitter: derivePda("emitter", args.transceiver.programId), - wormhole: { - bridge: whAccs.wormholeBridge, - feeCollector: whAccs.wormholeFeeCollector, - sequence: derivePda( - [ - "Sequence", - derivePda("emitter", args.transceiver.programId).toBytes(), - ], - this.core.address - ), - program: this.core.address, - systemProgram: SystemProgram.programId, - clock: web3.SYSVAR_CLOCK_PUBKEY, - rent: web3.SYSVAR_RENT_PUBKEY, - }, - }) - .instruction(); - const tx = new Transaction(); tx.feePayer = payer; tx.add(ix, broadcastIx); @@ -446,40 +628,50 @@ export class SolanaNtt ); } + async registerTransceiver( + transceiverType: string, + payer: web3.PublicKey, + owner: web3.PublicKey + ): Promise { + const transceiver = await this.getTransceiver(transceiverType); + if (!transceiver) { + throw new Error(`${transceiverType} transceiver not found`); + } + const transceiverProgramId = transceiver + .getAddress() + .address.toNative("Solana").address; + + return this.program.methods + .registerTransceiver() + .accountsStrict({ + payer, + owner, + config: this.pdas.configAccount(), + transceiver: transceiverProgramId, + registeredTransceiver: + this.pdas.registeredTransceiver(transceiverProgramId), + systemProgram: SystemProgram.programId, + }) + .instruction(); + } + async *setWormholeTransceiverPeer( peer: ChainAddress, payer: AccountAddress ) { - const sender = new SolanaAddress(payer).unwrap(); - yield this.createUnsignedTx( - await NTT.setWormholeTransceiverPeer(this.program, { - wormholeId: new PublicKey(this.core.address), - payer: sender, - owner: sender, - chain: peer.chain, - address: peer.address.toUniversalAddress().toUint8Array(), - }), - "Ntt.SetWormholeTransceiverPeer" - ); + return this.setTransceiverPeer("wormhole", peer, payer); } - async *setWormholeTransceiverPeer2( + async *setTransceiverPeer( + transceiverType: string, peer: ChainAddress, - payer: AccountAddress, - transceiver: Program + payer: AccountAddress ) { - const sender = new SolanaAddress(payer).unwrap(); - yield this.createUnsignedTx( - await NTT.setWormholeTransceiverPeer(this.program, { - wormholeId: new PublicKey(this.core.address), - payer: sender, - owner: sender, - chain: peer.chain, - address: peer.address.toUniversalAddress().toUint8Array(), - transceiver, - }), - "Ntt.SetWormholeTransceiverPeer" - ); + const transceiver = await this.getTransceiver(transceiverType); + if (!transceiver) { + throw new Error(`${transceiverType} transceiver not found`); + } + return transceiver.setPeer(peer, payer); } async *setPeer( @@ -510,6 +702,7 @@ export class SolanaNtt amount: bigint, destination: ChainAddress, options: Ntt.TransferOptions, + transceiverType?: string, outboxItem?: Keypair ): AsyncGenerator, any, unknown> { const config = await this.getConfig(); @@ -540,6 +733,7 @@ export class SolanaNtt config.tokenProgram ); + const asyncIxs: Promise[] = []; const transferIx = config.mode.locking != null ? NTT.createTransferLockInstruction( @@ -554,126 +748,26 @@ export class SolanaNtt txArgs, this.pdas ); + asyncIxs.push(transferIx); - const releaseIx = NTT.createReleaseOutboundInstruction( - this.program, - { - payer: payerAddress, - outboxItem: outboxItem.publicKey, - revertOnDelay: !options.queue, - wormholeId: new PublicKey(this.core.address), - }, - this.pdas - ); - - const tx = new Transaction(); - tx.feePayer = payerAddress; - tx.add(approveIx, ...(await Promise.all([transferIx, releaseIx]))); - - if (options.automatic) { - if (!this.quoter) - throw new Error( - "No quoter available, cannot initiate an automatic transfer." + switch (transceiverType) { + case "wormhole": + const whTransceiver = await this.getWormholeTransceiver(); + if (!whTransceiver) { + throw new Error(`${transceiverType} transceiver not found`); + } + const releaseIx = whTransceiver.releaseWormholeOutbound( + payerAddress, + outboxItem.publicKey, + !options.queue ); - - const fee = await this.quoteDeliveryPrice(destination.chain, options); - - const relayIx = await this.quoter.createRequestRelayInstruction( - payerAddress, - outboxItem.publicKey, - destination.chain, - Number(fee) / LAMPORTS_PER_SOL, - // Note: quoter expects gas dropoff to be in terms of gwei - Number(options.gasDropoff ?? 0n) / WEI_PER_GWEI - ); - tx.add(relayIx); + asyncIxs.push(releaseIx); + break; } - const luts: AddressLookupTableAccount[] = []; - try { - luts.push(await this.getAddressLookupTable()); - } catch {} - - const messageV0 = new TransactionMessage({ - payerKey: payerAddress, - instructions: tx.instructions, - recentBlockhash: (await this.connection.getRecentBlockhash()).blockhash, - }).compileToV0Message(luts); - - const vtx = new VersionedTransaction(messageV0); - - yield this.createUnsignedTx( - { transaction: vtx, signers: [outboxItem] }, - "Ntt.Transfer" - ); - } - - async *transfer2( - sender: AccountAddress, - amount: bigint, - destination: ChainAddress, - options: Ntt.TransferOptions, - transceiver: Program, - outboxItem?: Keypair - ): AsyncGenerator, any, unknown> { - const config = await this.getConfig(); - if (config.paused) throw new Error("Contract is paused"); - - outboxItem = outboxItem ?? Keypair.generate(); - - const payerAddress = new SolanaAddress(sender).unwrap(); - const fromAuthority = payerAddress; - const from = await this.getTokenAccount(fromAuthority); - - const transferArgs = NTT.transferArgs(amount, destination, options.queue); - - const txArgs = { - transferArgs, - payer: payerAddress, - from, - fromAuthority, - outboxItem: outboxItem.publicKey, - }; - - const approveIx = splToken.createApproveInstruction( - from, - this.pdas.sessionAuthority(fromAuthority, transferArgs), - fromAuthority, - amount, - [], - config.tokenProgram - ); - - const transferIx = - config.mode.locking != null - ? NTT.createTransferLockInstruction( - this.program, - config, - txArgs, - this.pdas - ) - : NTT.createTransferBurnInstruction( - this.program, - config, - txArgs, - this.pdas - ); - - const releaseIx = NTT.createReleaseOutboundInstruction( - this.program, - { - payer: payerAddress, - outboxItem: outboxItem.publicKey, - revertOnDelay: !options.queue, - wormholeId: new PublicKey(this.core.address), - transceiver, - }, - this.pdas - ); - const tx = new Transaction(); tx.feePayer = payerAddress; - tx.add(approveIx, ...(await Promise.all([transferIx, releaseIx]))); + tx.add(approveIx, ...(await Promise.all(asyncIxs))); if (options.automatic) { if (!this.quoter) @@ -699,12 +793,10 @@ export class SolanaNtt luts.push(await this.getAddressLookupTable()); } catch {} - const { blockhash } = await this.connection.getLatestBlockhash(); - const messageV0 = new TransactionMessage({ payerKey: payerAddress, instructions: tx.instructions, - recentBlockhash: blockhash, + recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, }).compileToV0Message(luts); const vtx = new VersionedTransaction(messageV0); @@ -749,174 +841,104 @@ export class SolanaNtt } } - async *redeem(attestations: Ntt.Attestation[], payer: AccountAddress) { + async *redeem( + attestations: Map, + payer: AccountAddress + ) { const config = await this.getConfig(); if (config.paused) throw new Error("Contract is paused"); // TODO: not this, we should iterate over the set of enabled xcvrs? // if (attestations.length !== this.xcvrs.length) throw "No"; - const wormholeNTT = attestations[0]; - if (!wormholeNTT || wormholeNTT.payloadName !== "WormholeTransfer") { - throw new Error("Invalid attestation payload"); + if (attestations.size !== this.transceivers.size) { + throw new Error("Not enough attestations provided"); } - // Create the vaa if necessary - yield* this.createAta(payer); - - // Post the VAA that we intend to redeem - yield* this.core.postVaa(payer, wormholeNTT); - - const senderAddress = new SolanaAddress(payer).unwrap(); - - const receiveMessageIx = NTT.createReceiveWormholeMessageInstruction( - this.program, - { - wormholeId: new PublicKey(this.core.address), - payer: senderAddress, - vaa: wormholeNTT, - }, - this.pdas - ); - - const nttMessage = wormholeNTT.payload.nttManagerPayload; - const emitterChain = wormholeNTT.emitterChain; - const releaseArgs = { - payer: senderAddress, - config, - nttMessage, - recipient: new PublicKey( - nttMessage.payload.recipientAddress.toUint8Array() - ), - chain: emitterChain, - revertOnDelay: false, - }; - - // TODO: loop through transceivers etc. - const redeemIx = NTT.createRedeemInstruction(this.program, config, { - payer: senderAddress, - vaa: wormholeNTT, - }); + for (const [transceiverType, attestation] of attestations) { + switch (transceiverType) { + case "wormhole": { + const wormholeNTT = attestation; + if (wormholeNTT.payloadName !== "WormholeTransfer") { + throw new Error("Invalid attestation payload"); + } + const whTransceiver = await this.getWormholeTransceiver(); + if (!whTransceiver) { + throw new Error(`${transceiverType} transceiver not found`); + } + + // Create the vaa if necessary + yield* this.createAta(payer); + + // Post the VAA that we intend to redeem + yield* this.core.postVaa(payer, wormholeNTT); + + const senderAddress = new SolanaAddress(payer).unwrap(); + + const receiveMessageIx = whTransceiver.receiveIx( + wormholeNTT, + senderAddress + ); - const releaseIx = - config.mode.locking != null - ? NTT.createReleaseInboundUnlockInstruction( - this.program, - config, - releaseArgs - ) - : NTT.createReleaseInboundMintInstruction( + // TODO: loop through transceivers etc. + const redeemIx = NTT.createRedeemInstruction( this.program, config, - releaseArgs + whTransceiver.program.programId, + { + payer: senderAddress, + vaa: wormholeNTT, + } ); - const tx = new Transaction(); - tx.feePayer = senderAddress; - tx.add(...(await Promise.all([receiveMessageIx, redeemIx, releaseIx]))); - - const luts: AddressLookupTableAccount[] = []; - try { - luts.push(await this.getAddressLookupTable()); - } catch {} - - const messageV0 = new TransactionMessage({ - payerKey: senderAddress, - instructions: tx.instructions, - recentBlockhash: (await this.connection.getRecentBlockhash()).blockhash, - }).compileToV0Message(luts); - - const vtx = new VersionedTransaction(messageV0); - - yield this.createUnsignedTx({ transaction: vtx }, "Ntt.Redeem"); - } - - async *redeem2( - attestations: Ntt.Attestation[], - payer: AccountAddress, - transceiver: Program - ) { - const config = await this.getConfig(); - if (config.paused) throw new Error("Contract is paused"); - - // TODO: not this, we should iterate over the set of enabled xcvrs? - // if (attestations.length !== this.xcvrs.length) throw "No"; - const wormholeNTT = attestations[0]; - if (!wormholeNTT || wormholeNTT.payloadName !== "WormholeTransfer") { - throw new Error("Invalid attestation payload"); - } - - // Create the vaa if necessary - yield* this.createAta(payer); - - // Post the VAA that we intend to redeem - yield* this.core.postVaa(payer, wormholeNTT); - - const senderAddress = new SolanaAddress(payer).unwrap(); - - const receiveMessageIx = NTT.createReceiveWormholeMessageInstruction( - this.program, - { - wormholeId: new PublicKey(this.core.address), - payer: senderAddress, - vaa: wormholeNTT, - transceiver, - }, - this.pdas - ); - - const nttMessage = wormholeNTT.payload.nttManagerPayload; - const emitterChain = wormholeNTT.emitterChain; - const releaseArgs = { - payer: senderAddress, - config, - nttMessage, - recipient: new PublicKey( - nttMessage.payload.recipientAddress.toUint8Array() - ), - chain: emitterChain, - revertOnDelay: false, - }; - - // TODO: loop through transceivers etc. - const redeemIx = NTT.createRedeemInstruction(this.program, config, { - payer: senderAddress, - vaa: wormholeNTT, - transceiver, - }); - - const releaseIx = - config.mode.locking != null - ? NTT.createReleaseInboundUnlockInstruction( - this.program, - config, - releaseArgs - ) - : NTT.createReleaseInboundMintInstruction( - this.program, + const nttMessage = wormholeNTT.payload.nttManagerPayload; + const emitterChain = wormholeNTT.emitterChain; + const releaseArgs = { + payer: senderAddress, config, - releaseArgs + nttMessage, + recipient: new PublicKey( + nttMessage.payload.recipientAddress.toUint8Array() + ), + chain: emitterChain, + revertOnDelay: false, + }; + const releaseIx = + config.mode.locking != null + ? NTT.createReleaseInboundUnlockInstruction( + this.program, + config, + releaseArgs + ) + : NTT.createReleaseInboundMintInstruction( + this.program, + config, + releaseArgs + ); + + const tx = new Transaction(); + tx.feePayer = senderAddress; + tx.add( + ...(await Promise.all([receiveMessageIx, redeemIx, releaseIx])) ); - const tx = new Transaction(); - tx.feePayer = senderAddress; - tx.add(...(await Promise.all([receiveMessageIx, redeemIx, releaseIx]))); + const luts: AddressLookupTableAccount[] = []; + try { + luts.push(await this.getAddressLookupTable()); + } catch {} - const luts: AddressLookupTableAccount[] = []; - try { - luts.push(await this.getAddressLookupTable()); - } catch {} + const messageV0 = new TransactionMessage({ + payerKey: senderAddress, + instructions: tx.instructions, + recentBlockhash: (await this.connection.getLatestBlockhash()) + .blockhash, + }).compileToV0Message(luts); - const { blockhash } = await this.connection.getLatestBlockhash(); + const vtx = new VersionedTransaction(messageV0); - const messageV0 = new TransactionMessage({ - payerKey: senderAddress, - instructions: tx.instructions, - recentBlockhash: blockhash, - }).compileToV0Message(luts); - - const vtx = new VersionedTransaction(messageV0); - - yield this.createUnsignedTx({ transaction: vtx }, "Ntt.Redeem"); + yield this.createUnsignedTx({ transaction: vtx }, "Ntt.Redeem"); + } + } + } } async getCurrentOutboundCapacity(): Promise { @@ -1105,15 +1127,17 @@ export class SolanaNtt const local: Partial = { manager: this.managerAddress, token: this.tokenAddress, - transceiver: { - wormhole: this.whTransceiverAddress, - }, + // transceiver: { + // wormhole: (await this.getTransceiver("wormhole")) + // ?.getAddress() + // .address.toString(), + // }, }; const remote: Partial = { manager: this.program.programId.toBase58(), token: (await this.getConfig()).mint.toBase58(), - transceiver: { wormhole: this.pdas.emitterAccount().toBase58() }, + // transceiver: { wormhole: this.pdas.emitterAccount().toBase58() }, }; const deleteMatching = (a: any, b: any) => { From 1c339ad9ff733da6f5b18e016a902d6bb318a5c8 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 16 Oct 2024 19:47:27 -0400 Subject: [PATCH 6/8] solana: Revert to store list of xcvrs in SolanaNtt class --- evm/ts/src/ntt.ts | 119 +++++++++---- sdk/__tests__/utils.ts | 18 +- sdk/definitions/src/ntt.ts | 14 +- sdk/route/src/automatic.ts | 2 +- sdk/route/src/manual.ts | 2 +- solana/tests/anchor.test.ts | 10 +- solana/ts/lib/ntt.ts | 59 +------ solana/ts/sdk/ntt.ts | 339 +++++++++++++++++------------------- 8 files changed, 270 insertions(+), 293 deletions(-) diff --git a/evm/ts/src/ntt.ts b/evm/ts/src/ntt.ts index 47af29766..928c6f61b 100644 --- a/evm/ts/src/ntt.ts +++ b/evm/ts/src/ntt.ts @@ -15,7 +15,11 @@ import { toUniversal, universalAddress, } from "@wormhole-foundation/sdk-definitions"; -import type { AnyEvmAddress, EvmChains, EvmPlatformType } from "@wormhole-foundation/sdk-evm"; +import type { + AnyEvmAddress, + EvmChains, + EvmPlatformType, +} from "@wormhole-foundation/sdk-evm"; import { EvmAddress, EvmPlatform, @@ -39,7 +43,8 @@ import { } from "./bindings.js"; export class EvmNttWormholeTranceiver - implements NttTransceiver { + implements NttTransceiver +{ transceiver: NttTransceiverBindings.NttTransceiver; constructor( readonly manager: EvmNtt, @@ -53,14 +58,19 @@ export class EvmNttWormholeTranceiver } getAddress(): ChainAddress { - return { chain: this.manager.chain, address: toUniversal(this.manager.chain, this.address) }; + return { + chain: this.manager.chain, + address: toUniversal(this.manager.chain, this.address), + }; } encodeFlags(flags: { skipRelay: boolean }): Uint8Array { return new Uint8Array([flags.skipRelay ? 1 : 0]); } - async *setPeer

(peer: ChainAddress

): AsyncGenerator> { + async *setPeer

( + peer: ChainAddress

+ ): AsyncGenerator> { const tx = await this.transceiver.setWormholePeer.populateTransaction( toChainId(peer.chain), universalAddress(peer) @@ -74,8 +84,14 @@ export class EvmNttWormholeTranceiver } async *setPauser(pauser: AccountAddress) { - const canonicalPauser = canonicalAddress({chain: this.manager.chain, address: pauser}); - const tx = await this.transceiver.transferPauserCapability.populateTransaction(canonicalPauser); + const canonicalPauser = canonicalAddress({ + chain: this.manager.chain, + address: pauser, + }); + const tx = + await this.transceiver.transferPauserCapability.populateTransaction( + canonicalPauser + ); yield this.manager.createUnsignedTx(tx, "WormholeTransceiver.setPauser"); } @@ -102,7 +118,10 @@ export class EvmNttWormholeTranceiver toChainId(chain), isEvm ); - yield this.manager.createUnsignedTx(tx, "WormholeTransceiver.setIsEvmChain"); + yield this.manager.createUnsignedTx( + tx, + "WormholeTransceiver.setIsEvmChain" + ); } async *receive(attestation: WormholeNttTransceiver.VAA) { @@ -122,10 +141,11 @@ export class EvmNttWormholeTranceiver } async *setIsWormholeRelayingEnabled(destChain: Chain, enabled: boolean) { - const tx = await this.transceiver.setIsWormholeRelayingEnabled.populateTransaction( - toChainId(destChain), - enabled - ); + const tx = + await this.transceiver.setIsWormholeRelayingEnabled.populateTransaction( + toChainId(destChain), + enabled + ); yield this.manager.createUnsignedTx( tx, "WormholeTransceiver.setWormholeRelayingEnabled" @@ -140,7 +160,8 @@ export class EvmNttWormholeTranceiver } export class EvmNtt - implements Ntt { + implements Ntt +{ tokenAddress: string; readonly chainId: bigint; manager: NttManagerBindings.NttManager; @@ -171,17 +192,24 @@ export class EvmNtt this.provider ); - if (contracts.ntt.transceiver.wormhole != null) { - this.xcvrs = [ - // Enable more Transceivers here - new EvmNttWormholeTranceiver( - this, - contracts.ntt.transceiver.wormhole, - abiBindings! - ), + this.xcvrs = []; + if (contracts.ntt.transceiver["wormhole"]) { + const transceiverTypes = [ + "wormhole", // wormhole xcvr should be ix 0 + ...Object.keys(contracts.ntt.transceiver).filter((transceiverType) => { + transceiverType !== "wormhole"; + }), ]; - } else { - this.xcvrs = []; + transceiverTypes.map((transceiverType) => { + // Enable more Transceivers here + this.xcvrs.push( + new EvmNttWormholeTranceiver( + this, + contracts.ntt!.transceiver[transceiverType]!, + abiBindings! + ) + ); + }); } } @@ -200,12 +228,12 @@ export class EvmNtt } async *pause() { - const tx = await this.manager.pause.populateTransaction() + const tx = await this.manager.pause.populateTransaction(); yield this.createUnsignedTx(tx, "Ntt.pause"); } async *unpause() { - const tx = await this.manager.unpause.populateTransaction() + const tx = await this.manager.unpause.populateTransaction(); yield this.createUnsignedTx(tx, "Ntt.unpause"); } @@ -219,13 +247,17 @@ export class EvmNtt async *setOwner(owner: AnyEvmAddress) { const canonicalOwner = new EvmAddress(owner).toString(); - const tx = await this.manager.transferOwnership.populateTransaction(canonicalOwner); + const tx = await this.manager.transferOwnership.populateTransaction( + canonicalOwner + ); yield this.createUnsignedTx(tx, "Ntt.setOwner"); } async *setPauser(pauser: AnyEvmAddress) { const canonicalPauser = new EvmAddress(pauser).toString(); - const tx = await this.manager.transferPauserCapability.populateTransaction(canonicalPauser); + const tx = await this.manager.transferPauserCapability.populateTransaction( + canonicalPauser + ); yield this.createUnsignedTx(tx, "Ntt.setPauser"); } @@ -387,9 +419,14 @@ export class EvmNtt } async *setWormholeTransceiverPeer(peer: ChainAddress) { - // TODO: we only have one right now, so just set the peer on that one - // in the future, these should(?) be keyed by attestation type - yield* this.xcvrs[0]!.setPeer(peer); + yield* this.setTransceiverPeer(0, peer); + } + + async *setTransceiverPeer(ix: number, peer: ChainAddress) { + if (ix >= this.xcvrs.length) { + throw new Error("Transceiver not found"); + } + yield* this.xcvrs[ix]!.setPeer(peer); } async *transfer( @@ -464,7 +501,9 @@ export class EvmNtt } async getOutboundLimit(): Promise { - const encoded: EncodedTrimmedAmount = (await this.manager.getOutboundLimitParams()).limit; + const encoded: EncodedTrimmedAmount = ( + await this.manager.getOutboundLimitParams() + ).limit; const trimmedAmount: TrimmedAmount = decodeTrimmedAmount(encoded); const tokenDecimals = await this.getTokenDecimals(); @@ -481,7 +520,9 @@ export class EvmNtt } async getInboundLimit(fromChain: Chain): Promise { - const encoded: EncodedTrimmedAmount = (await this.manager.getInboundLimitParams(toChainId(fromChain))).limit; + const encoded: EncodedTrimmedAmount = ( + await this.manager.getInboundLimitParams(toChainId(fromChain)) + ).limit; const trimmedAmount: TrimmedAmount = decodeTrimmedAmount(encoded); const tokenDecimals = await this.getTokenDecimals(); @@ -536,7 +577,7 @@ export class EvmNtt manager: this.managerAddress, token: this.tokenAddress, transceiver: { - wormhole: this.xcvrs[0]?.address, + wormhole: this.xcvrs[0]!.address, }, // TODO: what about the quoter? }; @@ -545,7 +586,7 @@ export class EvmNtt manager: this.managerAddress, token: await this.manager.token(), transceiver: { - wormhole: (await this.manager.getTransceivers())[0]! // TODO: make this more generic + wormhole: (await this.manager.getTransceivers())[0]!, // TODO: make this more generic }, }; @@ -558,7 +599,7 @@ export class EvmNtt delete a[k]; } } - } + }; deleteMatching(remote, local); @@ -601,14 +642,18 @@ function untrim(trimmed: TrimmedAmount, toDecimals: number): bigint { return scale(amount, fromDecimals, toDecimals); } -function scale(amount: bigint, fromDecimals: number, toDecimals: number): bigint { +function scale( + amount: bigint, + fromDecimals: number, + toDecimals: number +): bigint { if (fromDecimals == toDecimals) { return amount; } if (fromDecimals > toDecimals) { - return amount / (10n ** BigInt(fromDecimals - toDecimals)); + return amount / 10n ** BigInt(fromDecimals - toDecimals); } else { - return amount * (10n ** BigInt(toDecimals - fromDecimals)); + return amount * 10n ** BigInt(toDecimals - fromDecimals); } } diff --git a/sdk/__tests__/utils.ts b/sdk/__tests__/utils.ts index 657a131ae..776a14049 100644 --- a/sdk/__tests__/utils.ts +++ b/sdk/__tests__/utils.ts @@ -127,7 +127,7 @@ export async function link(chainInfos: Ctx[]) { chain: hubChain, emitter: Wormhole.chainAddress( hubChain, - hub.contracts!.transceiver.wormhole! + hub.contracts!.transceiver["wormhole"]! ).address.toUniversalAddress(), sequence: 0n, }; @@ -565,10 +565,11 @@ async function deploySolana(ctx: Ctx): Promise { // mindful in the deploy script too. await new Promise((resolve) => setTimeout(resolve, 400)); - const registrTxs = manager.registerTransceiver({ - payer: Wormhole.chainAddress("Solana", keypair.publicKey.toBase58()).address, - owner: Wormhole.chainAddress("Solana", keypair.publicKey.toBase58()).address, - transceiver: manager.program.programId, + const registrTxs = manager.registerWormholeTransceiver({ + payer: Wormhole.chainAddress("Solana", keypair.publicKey.toBase58()) + .address, + owner: Wormhole.chainAddress("Solana", keypair.publicKey.toBase58()) + .address, }); await signSendWait(ctx.context, registrTxs, signer); console.log("Registered transceiver with self"); @@ -578,7 +579,7 @@ async function deploySolana(ctx: Ctx): Promise { ...ctx, contracts: { transceiver: { - wormhole: manager.pdas.emitterAccount().toString(), + wormhole: manager.program.programId.toString(), }, manager: manager.program.programId.toString(), token: mint.toString(), @@ -611,7 +612,8 @@ async function setupPeer(targetCtx: Ctx, peerCtx: Ctx) { ); await signSendWait(target, setPeerTxs, signer); - const setXcvrPeerTxs = nttManager.setWormholeTransceiverPeer( + const setXcvrPeerTxs = nttManager.setTransceiverPeer( + 0, // 0 = Wormhole peerTransceiver, sender.address ); @@ -625,7 +627,7 @@ async function setupPeer(targetCtx: Ctx, peerCtx: Ctx) { ) { const nativeSigner = (signer as NativeSigner).unwrap(); const xcvr = WormholeTransceiver__factory.connect( - targetCtx.contracts!.transceiver.wormhole!, + targetCtx.contracts!.transceiver["wormhole"]!, nativeSigner.signer ); const peerChainId = toChainId(peer.chain); diff --git a/sdk/definitions/src/ntt.ts b/sdk/definitions/src/ntt.ts index e438fbb64..0e8c01448 100644 --- a/sdk/definitions/src/ntt.ts +++ b/sdk/definitions/src/ntt.ts @@ -190,8 +190,9 @@ export interface Ntt { payer?: AccountAddress ): AsyncGenerator>; + // TODO: replace ix with transceiver type setTransceiverPeer( - transceiverType: string, + ix: number, peer: ChainAddress, payer?: AccountAddress ): AsyncGenerator>; @@ -229,9 +230,11 @@ export interface Ntt { /** * redeem redeems a set of Attestations to the corresponding transceivers on the destination chain * @param attestations The attestations to redeem, the length should be equal to the number of transceivers + * + * TODO: replace with Map */ redeem( - attestations: Map, + attestations: Ntt.Attestation[], payer?: AccountAddress ): AsyncGenerator>; @@ -244,9 +247,12 @@ export interface Ntt { /** Get the peer information for the given chain if it exists */ getPeer(chain: C): Promise | null>; - /** Get the transceiver corresponding to type */ + /** Get the transceiver corresponding to index (0 = Wormhole) + * + * TODO: replace ix with transceiver type + */ getTransceiver( - type: string + ix: number ): Promise | null>; /** diff --git a/sdk/route/src/automatic.ts b/sdk/route/src/automatic.ts index 4bb10d386..80357d3ad 100644 --- a/sdk/route/src/automatic.ts +++ b/sdk/route/src/automatic.ts @@ -312,7 +312,7 @@ export class NttAutomaticRoute token: dstInfo.token, manager: dstInfo.manager, transceiver: { - wormhole: dstInfo.transceiver.wormhole, + wormhole: dstInfo.transceiver["wormhole"]!, }, }, }, diff --git a/sdk/route/src/manual.ts b/sdk/route/src/manual.ts index cde5e4360..d06694bbd 100644 --- a/sdk/route/src/manual.ts +++ b/sdk/route/src/manual.ts @@ -297,7 +297,7 @@ export class NttManualRoute token: dstInfo.token, manager: dstInfo.manager, transceiver: { - wormhole: dstInfo.transceiver.wormhole, + wormhole: dstInfo.transceiver["wormhole"]!, }, }, }, diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index dc9ffe4dc..9c43189a2 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -31,7 +31,7 @@ import { Transaction, } from "@solana/web3.js"; import { DummyTransferHook } from "../ts/idl/1_0_0/ts/dummy_transfer_hook.js"; -import { NttTransceiver } from "../ts/idl/2_0_0/ts/ntt_transceiver.js"; +import { type NttTransceiver as NttTransceiverIdlType } from "../ts/idl/2_0_0/ts/ntt_transceiver.js"; import { SolanaNtt } from "../ts/sdk/index.js"; import { derivePda } from "../ts/lib/utils.js"; @@ -59,7 +59,7 @@ const w = new Wormhole("Devnet", [SolanaPlatform], { }); const nttTransceiver = anchor.workspace - .NttTransceiver as anchor.Program; + .NttTransceiver as anchor.Program; const remoteXcvr: ChainAddress = { chain: "Ethereum", @@ -299,7 +299,6 @@ describe("example-native-token-transfers", () => { amount, receiver, { queue: false, automatic: false, gasDropoff: 0n }, - "wormhole", outboxItem ); await signSendWait(ctx, xferTxs, signer); @@ -313,7 +312,6 @@ describe("example-native-token-transfers", () => { wormholeMessage ); - // TODO4: where is wormhole transfer coming from? const transceiverMessage = deserializePayload( "Ntt:WormholeTransfer", unsignedVaa.payload @@ -368,8 +366,7 @@ describe("example-native-token-transfers", () => { const published = emitter.publishMessage(0, serialized, 200); const rawVaa = guardians.addSignatures(published, [0]); const vaa = deserialize("Ntt:WormholeTransfer", serialize(rawVaa)); - const vaaMap = new Map([["wormhole", vaa]]); - const redeemTxs = ntt.redeem(vaaMap, sender); + const redeemTxs = ntt.redeem([vaa], sender); try { await signSendWait(ctx, redeemTxs, signer); } catch (e) { @@ -377,7 +374,6 @@ describe("example-native-token-transfers", () => { throw e; } - // expect(released).toEqual(true); expect((await counterValue()).toString()).toEqual("2"); }); }); diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 246bacf0f..52fdbc64b 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -277,6 +277,7 @@ export namespace NTT { export async function initializeOrUpdateLUT( program: Program>, config: NttBindings.Config, + whTransceiver: PublicKey, args: { payer: PublicKey; wormholeId: PublicKey; @@ -299,7 +300,7 @@ export namespace NTT { }); const whAccs = utils.getWormholeDerivedAccounts( - program.programId, + whTransceiver, args.wormholeId.toString() ); @@ -313,6 +314,7 @@ export namespace NTT { wormhole: { bridge: whAccs.wormholeBridge, feeCollector: whAccs.wormholeFeeCollector, + emitter: whAccs.wormholeEmitter, sequence: whAccs.wormholeSequence, program: args.wormholeId, systemProgram: SystemProgram.programId, @@ -525,61 +527,6 @@ export namespace NTT { return transferIx; } - /** - * Creates a release_outbound instruction. The `payer` needs to sign the transaction. - */ - // export async function createReleaseOutboundInstruction( - // program: Program>, - // args: { - // wormholeId: PublicKey; - // payer: PublicKey; - // outboxItem: PublicKey; - // revertOnDelay: boolean; - // transceiver?: Program; - // }, - // pdas?: Pdas - // ): Promise { - // pdas = pdas ?? NTT.pdas(program.programId); - - // const whAccs = utils.getWormholeDerivedAccounts( - // program.programId, - // args.wormholeId - // ); - - // if (!args.transceiver) { - // throw Error("no transceiver provided"); - // } - - // return await args.transceiver.methods - // .releaseWormholeOutbound({ - // revertOnDelay: args.revertOnDelay, - // }) - // .accounts({ - // payer: args.payer, - // config: { config: pdas.configAccount() }, - // outboxItem: args.outboxItem, - // wormholeMessage: derivePda( - // ["message", args.outboxItem.toBytes()], - // args.transceiver.programId - // ), - // emitter: derivePda("emitter", args.transceiver.programId), - // transceiver: pdas.registeredTransceiver(args.transceiver.programId), - // wormhole: { - // bridge: whAccs.wormholeBridge, - // feeCollector: whAccs.wormholeFeeCollector, - // sequence: derivePda( - // [ - // "Sequence", - // derivePda("emitter", args.transceiver.programId).toBytes(), - // ], - // args.wormholeId - // ), - // program: args.wormholeId, - // }, - // }) - // .instruction(); - // } - // TODO: document that if recipient is provided, then the instruction can be // created before the inbox item is created (i.e. they can be put in the same tx) export async function createReleaseInboundMintInstruction( diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 654ca2189..669f1a2bd 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -72,15 +72,25 @@ export class SolanaNttWormholeTransceiver< throw new Error("Method not implemented."); } - async *receive(_attestation: WormholeNttTransceiver.VAA) { - // TODO: this is implemented below (in the transceiver code). it could get - // tricky in general with multiple transceivers, as they might return an - // instruction, or multiple instructions, etc. - // in any case, we should implement this here. - throw new Error("Method not implemented."); + // NOTE: this method is not used for the Solana Wormhole transceiver. + // `createReceiveIx` is used directly as it can be batched with other ixs in a single tx + async *receive( + attestation: WormholeNttTransceiver.VAA, + payer: AccountAddress + ) { + if (attestation.payloadName !== "WormholeTransfer") { + throw new Error("Invalid attestation payload"); + } + const senderAddress = new SolanaAddress(payer).unwrap(); + + const ix = await this.createReceiveIx(attestation, senderAddress); + const tx = new Transaction(); + tx.feePayer = senderAddress; + tx.add(ix); + yield this.manager.createUnsignedTx({ transaction: tx }, "Ntt.Redeem"); } - async receiveIx( + async createReceiveIx( attestation: WormholeNttTransceiver.VAA<"WormholeTransfer">, payer: PublicKey ) { @@ -104,40 +114,6 @@ export class SolanaNttWormholeTransceiver< .instruction(); } - // async getTransceiverType(sender?: PublicKey): Promise { - // if (!sender) { - // const address = - // connection.rpcEndpoint === rpc.rpcAddress("Devnet", "Solana") - // ? "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J" // The CI pubkey, funded on ci network - // : connection.rpcEndpoint.startsWith("http://localhost") - // ? "98evdAiWr7ey9MAQzoQQMwFQkTsSR6KkWQuFqKrgwNwb" // the anchor pubkey, funded on local network - // : "Hk3SdYTJFpawrvRz4qRztuEt2SqoCG7BGj2yJfDJSFbJ"; // The default pubkey is funded on mainnet and devnet we need a funded account to simulate the transaction below - // sender = new PublicKey(address); - // } - - // const ix = await this.program.methods - // .transceiverType() - // .accountsStrict({}) - // .instruction(); - // // Since we don't need the very very very latest blockhash, using finalized - // // ensures the blockhash will be found when we immediately simulate the tx - // const { blockhash } = await this.program.provider.connection.getLatestBlockhash( - // "finalized" - // ); - // const msg = new TransactionMessage({ - // payerKey: sender, - // recentBlockhash: blockhash, - // instructions: [ix], - // }).compileToV0Message(); - - // const tx = new VersionedTransaction(msg); - - // const txSimulation = await program.provider.connection.simulateTransaction( - // tx, - // { sigVerify: false } - // ); - // } - getAddress(): ChainAddress { return { chain: this.manager.chain, @@ -164,7 +140,7 @@ export class SolanaNttWormholeTransceiver< .instruction(); const wormholeMessage = Keypair.generate(); - const broadcastIx = await this.broadcastWormholePeer( + const broadcastIx = await this.createBroadcastWormholePeerIx( peer.chain, sender, wormholeMessage.publicKey @@ -175,7 +151,7 @@ export class SolanaNttWormholeTransceiver< tx.add(ix, broadcastIx); yield this.manager.createUnsignedTx( { transaction: tx, signers: [wormholeMessage] }, - "Ntt.Ntt.SetWormholeTransceiverPeer" + "Ntt.SetWormholeTransceiverPeer" ); } @@ -193,7 +169,7 @@ export class SolanaNttWormholeTransceiver< }; } - async broadcastWormholeId( + async createBroadcastWormholeIdIx( payer: PublicKey, config: NttBindings.Config, wormholeMessage: PublicKey @@ -224,7 +200,7 @@ export class SolanaNttWormholeTransceiver< .instruction(); } - async broadcastWormholePeer( + async createBroadcastWormholePeerIx( chain: Chain, payer: PublicKey, wormholeMessage: PublicKey @@ -252,7 +228,7 @@ export class SolanaNttWormholeTransceiver< .instruction(); } - async releaseWormholeOutbound( + async createReleaseWormholeOutboundIx( payer: PublicKey, outboxItem: PublicKey, revertOnDelay: boolean @@ -298,8 +274,8 @@ export class SolanaNtt quoter?: NttQuoter; addressLookupTable?: AddressLookupTableAccount; - // key refers to `transceiver_type` - transceivers: Map>; + // 0 = Wormhole xcvr + transceivers: Program[]; // NOTE: these are stored from the constructor, but are not used directly // (only in verifyAddresses) @@ -321,20 +297,23 @@ export class SolanaNtt version as IdlVersion ); - this.transceivers = new Map(); - for (const [transceiverType, transceiverProgramId] of Object.entries( - contracts.ntt.transceiver - )) { - this.transceivers.set( - transceiverType, - new Program( - NttTransceiverIdl, - transceiverProgramId, - { - connection, - } - ) - ); + this.transceivers = []; + if (contracts.ntt.transceiver["wormhole"]) { + const transceiverTypes = [ + "wormhole", // wormhole xcvr should be ix 0 + ...Object.keys(contracts.ntt.transceiver).filter((transceiverType) => { + transceiverType !== "wormhole"; + }), + ]; + transceiverTypes.map((transceiverType) => { + this.transceivers.push( + new Program( + NttTransceiverIdl, + contracts.ntt!.transceiver[transceiverType]!, + { connection } + ) + ); + }); } this.managerAddress = contracts.ntt.manager; @@ -356,17 +335,17 @@ export class SolanaNtt this.pdas = NTT.pdas(this.program.programId); } - async getTransceiver( - type: T + async getTransceiver( + ix: T ): Promise< - | (T extends "wormhole" + | (T extends 0 ? SolanaNttWormholeTransceiver : NttTransceiver) | null > { - const transceiverProgram = this.transceivers.get(type) ?? null; + const transceiverProgram = this.transceivers[ix] ?? null; if (!transceiverProgram) return null; - if (type === "wormhole") + if (ix === 0) return new SolanaNttWormholeTransceiver(this, transceiverProgram); return null; } @@ -375,7 +354,7 @@ export class SolanaNtt N, C > | null> { - return this.getTransceiver("wormhole"); + return this.getTransceiver(0); } async getMode(): Promise { @@ -586,10 +565,23 @@ export class SolanaNtt async *initializeOrUpdateLUT(args: { payer: PublicKey }) { const config = await this.getConfig(); - const ix = await NTT.initializeOrUpdateLUT(this.program, config, { - payer: args.payer, - wormholeId: new PublicKey(this.core.address), - }); + const whTransceiver = await this.getWormholeTransceiver(); + if (!whTransceiver) { + throw new Error("wormhole transceiver not found"); + } + const whTransceiverProgramId = whTransceiver + .getAddress() + .address.toNative("Solana").address; + + const ix = await NTT.initializeOrUpdateLUT( + this.program, + config, + whTransceiverProgramId, + { + payer: args.payer, + wormholeId: new PublicKey(this.core.address), + } + ); // Already up to date if (!ix) return; @@ -609,11 +601,11 @@ export class SolanaNtt const config = await this.getConfig(); if (config.paused) throw new Error("Contract is paused"); - const ix = await this.registerTransceiver("wormhole", payer, owner); + const ix = await this.createRegisterTransceiverIx(0, payer, owner); const whTransceiver = (await this.getWormholeTransceiver())!; const wormholeMessage = Keypair.generate(); - const broadcastIx = await whTransceiver.broadcastWormholeId( + const broadcastIx = await whTransceiver.createBroadcastWormholeIdIx( payer, config, wormholeMessage.publicKey @@ -628,14 +620,15 @@ export class SolanaNtt ); } - async registerTransceiver( - transceiverType: string, + // TODO: maybe add to Ntt interface + async createRegisterTransceiverIx( + ix: number, payer: web3.PublicKey, owner: web3.PublicKey ): Promise { - const transceiver = await this.getTransceiver(transceiverType); + const transceiver = await this.getTransceiver(ix); if (!transceiver) { - throw new Error(`${transceiverType} transceiver not found`); + throw new Error(`Transceiver not found`); } const transceiverProgramId = transceiver .getAddress() @@ -659,19 +652,19 @@ export class SolanaNtt peer: ChainAddress, payer: AccountAddress ) { - return this.setTransceiverPeer("wormhole", peer, payer); + yield* this.setTransceiverPeer(0, peer, payer); } async *setTransceiverPeer( - transceiverType: string, + ix: number, peer: ChainAddress, payer: AccountAddress ) { - const transceiver = await this.getTransceiver(transceiverType); + const transceiver = await this.getTransceiver(ix); if (!transceiver) { - throw new Error(`${transceiverType} transceiver not found`); + throw new Error("Transceiver not found"); } - return transceiver.setPeer(peer, payer); + yield* transceiver.setPeer(peer, payer); } async *setPeer( @@ -702,7 +695,6 @@ export class SolanaNtt amount: bigint, destination: ChainAddress, options: Ntt.TransferOptions, - transceiverType?: string, outboxItem?: Keypair ): AsyncGenerator, any, unknown> { const config = await this.getConfig(); @@ -750,19 +742,19 @@ export class SolanaNtt ); asyncIxs.push(transferIx); - switch (transceiverType) { - case "wormhole": + for (let ix = 0; ix < this.transceivers.length; ++ix) { + if (ix === 0) { const whTransceiver = await this.getWormholeTransceiver(); if (!whTransceiver) { - throw new Error(`${transceiverType} transceiver not found`); + throw new Error("wormhole transceiver not found"); } - const releaseIx = whTransceiver.releaseWormholeOutbound( + const releaseIx = whTransceiver.createReleaseWormholeOutboundIx( payerAddress, outboxItem.publicKey, !options.queue ); asyncIxs.push(releaseIx); - break; + } } const tx = new Transaction(); @@ -782,7 +774,7 @@ export class SolanaNtt outboxItem.publicKey, destination.chain, Number(fee) / LAMPORTS_PER_SOL, - // Note: quoter expects gas dropoff to be in terms of gwei + // NOTE: quoter expects gas dropoff to be in terms of gwei Number(options.gasDropoff ?? 0n) / WEI_PER_GWEI ); tx.add(relayIx); @@ -841,102 +833,95 @@ export class SolanaNtt } } - async *redeem( - attestations: Map, - payer: AccountAddress - ) { + async *redeem(attestations: Ntt.Attestation[], payer: AccountAddress) { const config = await this.getConfig(); if (config.paused) throw new Error("Contract is paused"); - // TODO: not this, we should iterate over the set of enabled xcvrs? - // if (attestations.length !== this.xcvrs.length) throw "No"; - if (attestations.size !== this.transceivers.size) { + if (attestations.length !== this.transceivers.length) { throw new Error("Not enough attestations provided"); } - for (const [transceiverType, attestation] of attestations) { - switch (transceiverType) { - case "wormhole": { - const wormholeNTT = attestation; - if (wormholeNTT.payloadName !== "WormholeTransfer") { - throw new Error("Invalid attestation payload"); - } - const whTransceiver = await this.getWormholeTransceiver(); - if (!whTransceiver) { - throw new Error(`${transceiverType} transceiver not found`); - } - - // Create the vaa if necessary - yield* this.createAta(payer); + for (const { attestation, ix } of attestations.map((attestation, ix) => ({ + attestation, + ix, + }))) { + if (ix === 0) { + const wormholeNTT = attestation; + if (wormholeNTT.payloadName !== "WormholeTransfer") { + throw new Error("Invalid attestation payload"); + } + const whTransceiver = await this.getWormholeTransceiver(); + if (!whTransceiver) { + throw new Error("wormhole transceiver not found"); + } - // Post the VAA that we intend to redeem - yield* this.core.postVaa(payer, wormholeNTT); + // Create the vaa if necessary + yield* this.createAta(payer); - const senderAddress = new SolanaAddress(payer).unwrap(); + // Post the VAA that we intend to redeem + yield* this.core.postVaa(payer, wormholeNTT); - const receiveMessageIx = whTransceiver.receiveIx( - wormholeNTT, - senderAddress - ); + const senderAddress = new SolanaAddress(payer).unwrap(); - // TODO: loop through transceivers etc. - const redeemIx = NTT.createRedeemInstruction( - this.program, - config, - whTransceiver.program.programId, - { - payer: senderAddress, - vaa: wormholeNTT, - } - ); + const receiveMessageIx = whTransceiver.createReceiveIx( + wormholeNTT, + senderAddress + ); - const nttMessage = wormholeNTT.payload.nttManagerPayload; - const emitterChain = wormholeNTT.emitterChain; - const releaseArgs = { + const redeemIx = NTT.createRedeemInstruction( + this.program, + config, + whTransceiver.program.programId, + { payer: senderAddress, - config, - nttMessage, - recipient: new PublicKey( - nttMessage.payload.recipientAddress.toUint8Array() - ), - chain: emitterChain, - revertOnDelay: false, - }; - const releaseIx = - config.mode.locking != null - ? NTT.createReleaseInboundUnlockInstruction( - this.program, - config, - releaseArgs - ) - : NTT.createReleaseInboundMintInstruction( - this.program, - config, - releaseArgs - ); - - const tx = new Transaction(); - tx.feePayer = senderAddress; - tx.add( - ...(await Promise.all([receiveMessageIx, redeemIx, releaseIx])) - ); - - const luts: AddressLookupTableAccount[] = []; - try { - luts.push(await this.getAddressLookupTable()); - } catch {} - - const messageV0 = new TransactionMessage({ - payerKey: senderAddress, - instructions: tx.instructions, - recentBlockhash: (await this.connection.getLatestBlockhash()) - .blockhash, - }).compileToV0Message(luts); - - const vtx = new VersionedTransaction(messageV0); + vaa: wormholeNTT, + } + ); - yield this.createUnsignedTx({ transaction: vtx }, "Ntt.Redeem"); - } + const nttMessage = wormholeNTT.payload.nttManagerPayload; + const emitterChain = wormholeNTT.emitterChain; + const releaseArgs = { + payer: senderAddress, + config, + nttMessage, + recipient: new PublicKey( + nttMessage.payload.recipientAddress.toUint8Array() + ), + chain: emitterChain, + revertOnDelay: false, + }; + const releaseIx = + config.mode.locking != null + ? NTT.createReleaseInboundUnlockInstruction( + this.program, + config, + releaseArgs + ) + : NTT.createReleaseInboundMintInstruction( + this.program, + config, + releaseArgs + ); + + const tx = new Transaction(); + tx.feePayer = senderAddress; + tx.add(...(await Promise.all([receiveMessageIx, redeemIx, releaseIx]))); + + const luts: AddressLookupTableAccount[] = []; + try { + luts.push(await this.getAddressLookupTable()); + } catch {} + + const messageV0 = new TransactionMessage({ + payerKey: senderAddress, + instructions: tx.instructions, + recentBlockhash: (await this.connection.getLatestBlockhash()) + .blockhash, + }).compileToV0Message(luts); + + const vtx = new VersionedTransaction(messageV0); + + yield this.createUnsignedTx({ transaction: vtx }, "Ntt.Redeem"); } } } @@ -1124,20 +1109,16 @@ export class SolanaNtt } async verifyAddresses(): Promise | null> { + // NOTE: transceivers are not being compared as there is no reverse lookup + // given manager address to the registered transceivers const local: Partial = { manager: this.managerAddress, token: this.tokenAddress, - // transceiver: { - // wormhole: (await this.getTransceiver("wormhole")) - // ?.getAddress() - // .address.toString(), - // }, }; const remote: Partial = { manager: this.program.programId.toBase58(), token: (await this.getConfig()).mint.toBase58(), - // transceiver: { wormhole: this.pdas.emitterAccount().toBase58() }, }; const deleteMatching = (a: any, b: any) => { From c905e45dcc1fd130269a2da580e36d72496591a3 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Thu, 17 Oct 2024 19:43:00 -0400 Subject: [PATCH 7/8] solana: Add check to ensure outboxItem bitmap is set --- solana/tests/anchor.test.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index 9c43189a2..53ca94861 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -58,8 +58,10 @@ const w = new Wormhole("Devnet", [SolanaPlatform], { chains: { Solana: { contracts: { coreBridge: CORE_BRIDGE_ADDRESS } } }, }); -const nttTransceiver = anchor.workspace - .NttTransceiver as anchor.Program; +const nttTransceivers = { + wormhole: anchor.workspace + .NttTransceiver as anchor.Program, +}; const remoteXcvr: ChainAddress = { chain: "Ethereum", @@ -202,7 +204,9 @@ describe("example-native-token-transfers", () => { ntt: { token: tokenAddress, manager: NTT_ADDRESS, - transceiver: { wormhole: nttTransceiver.programId.toBase58() }, + transceiver: { + wormhole: nttTransceivers["wormhole"].programId.toBase58(), + }, }, }); } catch (e) { @@ -303,9 +307,17 @@ describe("example-native-token-transfers", () => { ); await signSendWait(ctx, xferTxs, signer); + // assert that released bitmap has transceiver bits set + const outboxItemInfo = await ntt.program.account.outboxItem.fetch( + outboxItem.publicKey + ); + expect(outboxItemInfo.released.map.bitLength()).toBe( + Object.keys(nttTransceivers).length + ); + const wormholeMessage = derivePda( ["message", outboxItem.publicKey.toBytes()], - nttTransceiver.programId + nttTransceivers["wormhole"].programId ); const unsignedVaa = await coreBridge.parsePostMessageAccount( From c762433aa0d51b45a9f901fce977980c6dc1292c Mon Sep 17 00:00:00 2001 From: nvsriram Date: Thu, 17 Oct 2024 19:45:32 -0400 Subject: [PATCH 8/8] solana: Update accounts needed for releaseWormholeOutbound --- solana/ts/lib/ntt.ts | 2 ++ solana/ts/sdk/ntt.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 52fdbc64b..4af993d30 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -143,6 +143,7 @@ export namespace NTT { /** pdas returns an object containing all functions to compute program addresses */ export const transceiverPdas = (programId: PublicKeyInitData) => { const emitterAccount = (): PublicKey => derivePda("emitter", programId); + const outboxItemSigner = () => derivePda(["outbox_item_signer"], programId); const transceiverPeerAccount = (chain: Chain): PublicKey => derivePda(["transceiver_peer", chainToBytes(chain)], programId); const transceiverMessageAccount = ( @@ -156,6 +157,7 @@ export namespace NTT { // TODO: memoize? return { emitterAccount, + outboxItemSigner, transceiverPeerAccount, transceiverMessageAccount, wormholeMessageAccount, diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 669f1a2bd..9543e7959 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -257,6 +257,8 @@ export class SolanaNttWormholeTransceiver< sequence: whAccs.wormholeSequence, program: this.manager.core.address, }, + manager: this.manager.program.programId, + outboxItemSigner: this.pdas.outboxItemSigner(), }) .instruction(); }