diff --git a/solana/modules/ntt-messages/src/transceivers/wormhole.rs b/solana/modules/ntt-messages/src/transceivers/wormhole.rs index 710ba3af6..565dec64c 100644 --- a/solana/modules/ntt-messages/src/transceivers/wormhole.rs +++ b/solana/modules/ntt-messages/src/transceivers/wormhole.rs @@ -1,4 +1,11 @@ -use crate::transceiver::Transceiver; +use std::io; + +use wormhole_io::{Readable, TypePrefixedPayload, Writeable}; + +#[cfg(feature = "anchor")] +use anchor_lang::prelude::*; + +use crate::{chain_id::ChainId, mode::Mode, transceiver::Transceiver}; #[derive(PartialEq, Eq, Clone, Debug)] pub struct WormholeTransceiver {} @@ -6,3 +13,137 @@ pub struct WormholeTransceiver {} impl Transceiver for WormholeTransceiver { const PREFIX: [u8; 4] = [0x99, 0x45, 0xFF, 0x10]; } + +impl WormholeTransceiver { + pub const INFO_PREFIX: [u8; 4] = [0x9c, 0x23, 0xbd, 0x3b]; + + pub const PEER_INFO_PREFIX: [u8; 4] = [0x18, 0xfc, 0x67, 0xc2]; +} + +// * Transceiver info + +#[derive(Debug, Clone)] +pub struct WormholeTransceiverInfo { + pub manager_address: [u8; 32], + pub manager_mode: Mode, + pub token_address: [u8; 32], + pub token_decimals: u8, +} + +impl Readable for WormholeTransceiverInfo { + const SIZE: Option = Some(32 + 1 + 32 + 1); + + fn read(reader: &mut R) -> std::io::Result + where + Self: Sized, + R: std::io::Read, + { + let prefix = <[u8; 4]>::read(reader)?; + if prefix != WormholeTransceiver::INFO_PREFIX { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid prefix", + )); + } + + let manager_address = <[u8; 32]>::read(reader)?; + let manager_mode = Mode::read(reader)?; + let token_address = <[u8; 32]>::read(reader)?; + let token_decimals = u8::read(reader)?; + + Ok(WormholeTransceiverInfo { + manager_address, + manager_mode, + token_address, + token_decimals, + }) + } +} + +impl Writeable for WormholeTransceiverInfo { + fn written_size(&self) -> usize { + WormholeTransceiverInfo::SIZE.unwrap() + } + + fn write(&self, writer: &mut W) -> std::io::Result<()> + where + W: std::io::Write, + { + WormholeTransceiver::INFO_PREFIX.write(writer)?; + self.manager_address.write(writer)?; + self.manager_mode.write(writer)?; + self.token_address.write(writer)?; + self.token_decimals.write(writer) + } +} + +impl TypePrefixedPayload for WormholeTransceiverInfo { + const TYPE: Option = None; +} + +// * Transceiver registration + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WormholeTransceiverRegistration { + pub chain_id: ChainId, + pub transceiver_address: [u8; 32], +} + +#[cfg(feature = "anchor")] +impl AnchorDeserialize for WormholeTransceiverRegistration { + fn deserialize_reader(reader: &mut R) -> io::Result { + Readable::read(reader) + } +} + +#[cfg(feature = "anchor")] +impl AnchorSerialize for WormholeTransceiverRegistration { + fn serialize(&self, writer: &mut W) -> io::Result<()> { + Writeable::write(self, writer) + } +} + +impl Readable for WormholeTransceiverRegistration { + const SIZE: Option = Some(2 + 32); + + fn read(reader: &mut R) -> std::io::Result + where + Self: Sized, + R: std::io::Read, + { + let prefix = <[u8; 4]>::read(reader)?; + if prefix != WormholeTransceiver::PEER_INFO_PREFIX { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid prefix", + )); + } + + let chain_id = ChainId::read(reader)?; + let transceiver_address = <[u8; 32]>::read(reader)?; + + Ok(WormholeTransceiverRegistration { + chain_id, + transceiver_address, + }) + } +} + +impl Writeable for WormholeTransceiverRegistration { + fn written_size(&self) -> usize { + WormholeTransceiverRegistration::SIZE.unwrap() + } + + fn write(&self, writer: &mut W) -> std::io::Result<()> + where + W: std::io::Write, + { + WormholeTransceiver::PEER_INFO_PREFIX.write(writer)?; + self.chain_id.write(writer)?; + self.transceiver_address.write(writer) + } +} + +impl TypePrefixedPayload for WormholeTransceiverRegistration { + const TYPE: Option = None; +} diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 225f574f2..c6d67e06c 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -119,4 +119,15 @@ pub mod example_native_token_transfers { ) -> Result<()> { transceivers::wormhole::instructions::release_outbound(ctx, args) } + + pub fn broadcast_wormhole_id(ctx: Context) -> Result<()> { + transceivers::wormhole::instructions::broadcast_id(ctx) + } + + pub fn broadcast_wormhole_peer( + ctx: Context, + args: BroadcastPeerArgs, + ) -> Result<()> { + transceivers::wormhole::instructions::broadcast_peer(ctx, args) + } } diff --git a/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/broadcast_id.rs b/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/broadcast_id.rs new file mode 100644 index 000000000..a64f1fea9 --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/broadcast_id.rs @@ -0,0 +1,54 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface; +use ntt_messages::transceivers::wormhole::WormholeTransceiverInfo; + +use crate::{config::*, transceivers::wormhole::accounts::*}; + +#[derive(Accounts)] +pub struct BroadcastId<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + pub config: Account<'info, Config>, + + #[account( + address = config.mint, + )] + pub mint: InterfaceAccount<'info, token_interface::Mint>, + + /// CHECK: initialized and written to by wormhole core bridge + #[account(mut)] + pub wormhole_message: Signer<'info>, + + #[account( + mut, + seeds = [b"emitter"], + bump + )] + pub emitter: UncheckedAccount<'info>, + + pub wormhole: WormholeAccounts<'info>, +} + +pub fn broadcast_id(ctx: Context) -> Result<()> { + let accs = ctx.accounts; + let message = WormholeTransceiverInfo { + manager_address: accs.config.to_account_info().owner.to_bytes(), + manager_mode: accs.config.mode, + token_address: accs.mint.to_account_info().key.to_bytes(), + token_decimals: accs.mint.decimals, + }; + + // TODO: should we send this as an unreliable message into a PDA? + post_message( + &accs.wormhole, + accs.payer.to_account_info(), + accs.wormhole_message.to_account_info(), + accs.emitter.to_account_info(), + ctx.bumps.emitter, + &message, + &[], + )?; + + Ok(()) +} diff --git a/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/broadcast_peer.rs b/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/broadcast_peer.rs new file mode 100644 index 000000000..a23dcb510 --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/broadcast_peer.rs @@ -0,0 +1,62 @@ +use anchor_lang::prelude::*; +use ntt_messages::{chain_id::ChainId, transceivers::wormhole::WormholeTransceiverRegistration}; + +use crate::{ + config::*, + transceivers::{accounts::peer::TransceiverPeer, wormhole::accounts::*}, +}; + +#[derive(Accounts)] +#[instruction(args: BroadcastPeerArgs)] +pub struct BroadcastPeer<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + pub config: Account<'info, Config>, + + #[account( + seeds = [TransceiverPeer::SEED_PREFIX, args.chain_id.to_be_bytes().as_ref()], + bump + )] + pub peer: Account<'info, TransceiverPeer>, + + /// CHECK: initialized and written to by wormhole core bridge + #[account(mut)] + pub wormhole_message: Signer<'info>, + + #[account( + mut, + seeds = [b"emitter"], + bump + )] + pub emitter: UncheckedAccount<'info>, + + pub wormhole: WormholeAccounts<'info>, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct BroadcastPeerArgs { + pub chain_id: u16, +} + +pub fn broadcast_peer(ctx: Context, args: BroadcastPeerArgs) -> Result<()> { + let accs = ctx.accounts; + + let message = WormholeTransceiverRegistration { + chain_id: ChainId { id: args.chain_id }, + transceiver_address: accs.peer.address + }; + + // TODO: should we send this as an unreliable message into a PDA? + post_message( + &accs.wormhole, + accs.payer.to_account_info(), + accs.wormhole_message.to_account_info(), + accs.emitter.to_account_info(), + ctx.bumps.emitter, + &message, + &[], + )?; + + Ok(()) +} diff --git a/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/mod.rs b/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/mod.rs index 97ef6addd..482396b70 100644 --- a/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/mod.rs +++ b/solana/programs/example-native-token-transfers/src/transceivers/wormhole/instructions/mod.rs @@ -1,9 +1,11 @@ pub mod admin; pub mod broadcast_id; +pub mod broadcast_peer; pub mod receive_message; pub mod release_outbound; pub use admin::*; pub use broadcast_id::*; +pub use broadcast_peer::*; pub use receive_message::*; pub use release_outbound::*; diff --git a/solana/programs/example-native-token-transfers/tests/broadcast.rs b/solana/programs/example-native-token-transfers/tests/broadcast.rs new file mode 100644 index 000000000..76a1c7872 --- /dev/null +++ b/solana/programs/example-native-token-transfers/tests/broadcast.rs @@ -0,0 +1,51 @@ +#![cfg(feature = "test-sbf")] +#![feature(type_changing_struct_update)] + +use common::setup::OTHER_CHAIN; +use ntt_messages::chain_id::ChainId; +use ntt_messages::mode::Mode; +use ntt_messages::transceivers::wormhole::WormholeTransceiverRegistration; +use solana_program_test::*; +use solana_sdk::{signature::Keypair, signer::Signer}; +use wormhole_anchor_sdk::wormhole::PostedVaa; + +use crate::common::query::GetAccountDataAnchor; +use crate::common::setup::{setup, OTHER_TRANSCEIVER}; +use crate::{ + common::submit::Submittable, + sdk::transceivers::wormhole::instructions::broadcast_peer::{broadcast_peer, BroadcastPeer}, +}; + +pub mod common; +pub mod sdk; + +#[tokio::test] +async fn test_broadcast_peer() { + let (mut ctx, test_data) = setup(Mode::Locking).await; + + let wh_message = Keypair::new(); + + broadcast_peer( + &test_data.ntt, + BroadcastPeer { + payer: ctx.payer.pubkey(), + wormhole_message: wh_message.pubkey(), + chain_id: OTHER_CHAIN, + }, + ) + .submit_with_signers(&[&wh_message], &mut ctx) + .await + .unwrap(); + + let msg: PostedVaa = ctx + .get_account_data_anchor_unchecked(wh_message.pubkey()) + .await; + + assert_eq!( + *msg.data(), + WormholeTransceiverRegistration { + chain_id: ChainId { id: OTHER_CHAIN }, + transceiver_address: OTHER_TRANSCEIVER + } + ); +} diff --git a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/accounts/mod.rs b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/accounts/mod.rs new file mode 100644 index 000000000..3ab4966d3 --- /dev/null +++ b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/accounts/mod.rs @@ -0,0 +1 @@ +pub mod wormhole; diff --git a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/accounts/wormhole.rs b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/accounts/wormhole.rs new file mode 100644 index 000000000..664b6baf2 --- /dev/null +++ b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/accounts/wormhole.rs @@ -0,0 +1,17 @@ +use anchor_lang::prelude::*; +use example_native_token_transfers::accounts::WormholeAccounts; +use solana_sdk::sysvar::SysvarId; + +use crate::sdk::accounts::NTT; + +pub fn wormhole_accounts(ntt: &NTT) -> WormholeAccounts { + WormholeAccounts { + bridge: ntt.wormhole.bridge(), + fee_collector: ntt.wormhole.fee_collector(), + sequence: ntt.wormhole_sequence(), + program: ntt.wormhole.program, + system_program: System::id(), + clock: Clock::id(), + rent: Rent::id(), + } +} diff --git a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/broadcast_id.rs b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/broadcast_id.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/broadcast_id.rs @@ -0,0 +1 @@ + diff --git a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/broadcast_peer.rs b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/broadcast_peer.rs new file mode 100644 index 000000000..ddcbd0907 --- /dev/null +++ b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/broadcast_peer.rs @@ -0,0 +1,34 @@ +use anchor_lang::{prelude::*, InstructionData}; +use example_native_token_transfers::transceivers::wormhole::BroadcastPeerArgs; +use solana_program::instruction::Instruction; + +use crate::sdk::{accounts::NTT, transceivers::wormhole::accounts::wormhole::wormhole_accounts}; + +pub struct BroadcastPeer { + pub payer: Pubkey, + pub wormhole_message: Pubkey, + pub chain_id: u16, +} + +pub fn broadcast_peer(ntt: &NTT, accs: BroadcastPeer) -> Instruction { + let data = example_native_token_transfers::instruction::BroadcastWormholePeer { + args: BroadcastPeerArgs { + chain_id: accs.chain_id, + }, + }; + + let accounts = example_native_token_transfers::accounts::BroadcastPeer { + payer: accs.payer, + config: ntt.config(), + peer: ntt.transceiver_peer(accs.chain_id), + wormhole_message: accs.wormhole_message, + emitter: ntt.emitter(), + wormhole: wormhole_accounts(ntt), + }; + + Instruction { + program_id: example_native_token_transfers::ID, + accounts: accounts.to_account_metas(None), + data: data.data(), + } +} diff --git a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/mod.rs b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/mod.rs index 9922536c3..10e9c2446 100644 --- a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/mod.rs +++ b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/mod.rs @@ -1,3 +1,5 @@ pub mod admin; +pub mod broadcast_id; +pub mod broadcast_peer; pub mod receive_message; pub mod release_outbound; diff --git a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/release_outbound.rs b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/release_outbound.rs index 529ed35d9..515a1c1b8 100644 --- a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/release_outbound.rs +++ b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/instructions/release_outbound.rs @@ -1,11 +1,10 @@ use anchor_lang::{prelude::*, InstructionData}; use example_native_token_transfers::{ - accounts::{NotPausedConfig, WormholeAccounts}, - transceivers::wormhole::ReleaseOutboundArgs, + accounts::NotPausedConfig, transceivers::wormhole::ReleaseOutboundArgs, }; -use solana_sdk::{instruction::Instruction, sysvar::SysvarId}; +use solana_sdk::instruction::Instruction; -use crate::sdk::accounts::NTT; +use crate::sdk::{accounts::NTT, transceivers::wormhole::accounts::wormhole::wormhole_accounts}; pub struct ReleaseOutbound { pub payer: Pubkey, @@ -27,15 +26,7 @@ pub fn release_outbound( wormhole_message: ntt.wormhole_message(&release_outbound.outbox_item), emitter: ntt.emitter(), transceiver: ntt.registered_transceiver(&ntt.program), - wormhole: WormholeAccounts { - bridge: ntt.wormhole.bridge(), - fee_collector: ntt.wormhole.fee_collector(), - sequence: ntt.wormhole_sequence(), - program: ntt.wormhole.program, - system_program: System::id(), - clock: Clock::id(), - rent: Rent::id(), - }, + wormhole: wormhole_accounts(ntt), }; Instruction { program_id: example_native_token_transfers::ID, diff --git a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/mod.rs b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/mod.rs index 4f177196d..5c4c40bc3 100644 --- a/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/mod.rs +++ b/solana/programs/example-native-token-transfers/tests/sdk/transceivers/wormhole/mod.rs @@ -1 +1,2 @@ +pub mod accounts; pub mod instructions;