diff --git a/projects/xmint/chains/solana/Cargo.lock b/projects/xmint/chains/solana/Cargo.lock index c377ef2..ea5617b 100644 --- a/projects/xmint/chains/solana/Cargo.lock +++ b/projects/xmint/chains/solana/Cargo.lock @@ -344,6 +344,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -1081,6 +1092,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.27" @@ -1251,6 +1268,7 @@ dependencies = [ "anchor-lang", "anchor-spl", "borsh", + "bstr", "byteorder", "mpl-token-metadata", "primitive-types", diff --git a/projects/xmint/chains/solana/programs/solana/Cargo.toml b/projects/xmint/chains/solana/programs/solana/Cargo.toml index abe2f98..dd3a320 100644 --- a/projects/xmint/chains/solana/programs/solana/Cargo.toml +++ b/projects/xmint/chains/solana/programs/solana/Cargo.toml @@ -23,3 +23,4 @@ sha3 = "0.10.1" byteorder = "1.4.3" borsh = "0.9.3" primitive-types = { version = "0.11.1", default-features = false } +bstr = "0.2.16" diff --git a/projects/xmint/chains/solana/programs/solana/src/account.rs b/projects/xmint/chains/solana/programs/solana/src/account.rs index 865969a..a994e79 100644 --- a/projects/xmint/chains/solana/programs/solana/src/account.rs +++ b/projects/xmint/chains/solana/programs/solana/src/account.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; #[account] pub struct Config { pub owner: Pubkey, - pub nonce: u64, + pub nonce: u32, } #[account] @@ -23,4 +23,12 @@ pub struct Redeemer {} #[account] pub struct MintInfo { pub mint: Pubkey +} + +#[account] +pub struct Receipt { + pub amt_to_mint: u64, + pub foreign_receipient: [u8; 32], + pub foreign_chain: u16, + pub claimed: bool } \ No newline at end of file diff --git a/projects/xmint/chains/solana/programs/solana/src/context.rs b/projects/xmint/chains/solana/programs/solana/src/context.rs index 90f53b0..3664c14 100644 --- a/projects/xmint/chains/solana/programs/solana/src/context.rs +++ b/projects/xmint/chains/solana/programs/solana/src/context.rs @@ -6,7 +6,7 @@ use crate::wormhole::*; use crate::constant::*; use std::str::FromStr; use anchor_spl::token::ID as spl_id; - +use crate::*; #[derive(Accounts)] pub struct Initialize<'info>{ @@ -82,6 +82,7 @@ pub struct RegisterChain<'info> { pub emitter_acc: Account<'info, EmitterAddrAccount>, } + #[derive(Accounts)] pub struct SubmitForeignPurchase<'info> { #[account(mut)] @@ -93,6 +94,16 @@ pub struct SubmitForeignPurchase<'info> { bump, )] pub config: Account<'info, Config>, + #[account( + init, + seeds = [ + vaa_hash(core_bridge_vaa.clone()).as_slice() + ], + bump, + payer = payer, + space = 64 + )] + pub receipt: Account<'info, Receipt>, // Fetch the VAA /// CHECK: Checked in lib.rs because it requires some fancy hashing @@ -187,32 +198,151 @@ pub struct SubmitForeignPurchase<'info> { )] pub mint_authority_wrapped: AccountInfo<'info>, pub rent_account: Sysvar<'info, Rent>, - /// CHECK: Make sure this is the right token bridge account + /// CHECK: Make sure this is the right core bridge account #[account( - constraint = token_bridge_program.key() == Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap() + constraint = core_bridge.key() == Pubkey::from_str(CORE_BRIDGE_ADDRESS).unwrap() )] - pub token_bridge_program: AccountInfo<'info>, + pub core_bridge: AccountInfo<'info>, /// CHECK: SPL Program should be actual SPL Program #[account( constraint = spl_program.key() == spl_id )] pub spl_program: AccountInfo<'info>, + /// CHECK: Make sure this is the right token bridge account + #[account( + constraint = token_bridge.key() == Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap() + )] + pub token_bridge: AccountInfo<'info> +} - - +#[derive(Accounts)] +pub struct ClaimForeignPurchase<'info> { + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, + #[account( + mut, + seeds = [b"config"], + bump, + )] + pub config: Account<'info, Config>, + #[account(mut)] + pub receipt: Account<'info, Receipt>, // Mint SOL#T SPL tokens to Contract PDA + #[account(mut)] pub xmint_token_mint: Account<'info, Mint>, #[account( seeds=[b"mint_authority"], bump, + mut )] pub xmint_authority: Account<'info, MintInfo>, + /// CHECK: TODO: Check if owned by SPL program #[account(mut)] pub xmint_ata_account: AccountInfo<'info>, - // P1 Portal Transfer to Receipient + // P1 Portal Transfer to Recepient + #[account( + seeds = [ + xmint_token_mint.key().to_bytes().as_slice(), + ], + bump, + seeds::program = Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap(), + mut + )] + /// CHECK: The seeds constraint should check validity + pub token_bridge_mint_custody: AccountInfo<'info>, + #[account( + seeds = [ + b"authority_signer", + ], + bump, + seeds::program = Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap(), + )] + /// CHECK: The seeds constraint should check validity + pub token_bridge_authority_signer: AccountInfo<'info>, + #[account( + seeds = [ + b"custody_signer", + ], + bump, + seeds::program = Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap(), + )] + /// CHECK: The seeds constraint should check validity + pub token_bridge_custody_signer: AccountInfo<'info>, + #[account( + seeds = [ + b"Bridge", + ], + bump, + seeds::program = Pubkey::from_str(CORE_BRIDGE_ADDRESS).unwrap(), + mut, + )] + /// CHECK: The seeds constraint should check validity + pub core_bridge_config: AccountInfo<'info>, + #[account(mut)] + pub xmint_transfer_msg_key: Signer<'info>, + #[account( + seeds=[ + b"emitter", + ], + bump, + seeds::program = Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap(), + )] + /// CHECK: The seeds constraint should check validity + pub token_bridge_emitter: AccountInfo<'info>, + #[account( + seeds=[ + b"Sequence", + token_bridge_emitter.key().as_ref() + ], + bump, + seeds::program = Pubkey::from_str(CORE_BRIDGE_ADDRESS).unwrap(), + mut + )] + /// CHECK: The seeds constraint should check validity + pub token_bridge_sequence_key: AccountInfo<'info>, + #[account( + seeds = [ + b"fee_collector".as_ref() + ], + bump, + seeds::program = Pubkey::from_str(CORE_BRIDGE_ADDRESS).unwrap(), + mut + )] + /// CHECK: If someone passes in the wrong account, Guardians won't read the message + pub core_bridge_fee_collector: AccountInfo<'info>, + pub clock: Sysvar<'info, Clock>, + /// CHECK: Core Bridge matches constant address + #[account( + constraint = core_bridge.key() == Pubkey::from_str(CORE_BRIDGE_ADDRESS).unwrap() + )] + pub core_bridge: AccountInfo<'info>, + + + + /// CHECK: SPL Program should be actual SPL Program + #[account( + constraint = spl_program.key() == spl_id + )] + pub spl_program: AccountInfo<'info>, + #[account( + mut, + seeds = [b"config"], + bump, + seeds::program = Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap() + )] + /// CHECK: Token Bridge Config + pub token_bridge_config: AccountInfo<'info>, + pub rent_account: Sysvar<'info, Rent>, + + /// CHECK: Make sure this is the right token bridge account + #[account( + constraint = token_bridge.key() == Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap() + )] + pub token_bridge: AccountInfo<'info> } \ No newline at end of file diff --git a/projects/xmint/chains/solana/programs/solana/src/error.rs b/projects/xmint/chains/solana/programs/solana/src/error.rs index 59a9529..52e1209 100644 --- a/projects/xmint/chains/solana/programs/solana/src/error.rs +++ b/projects/xmint/chains/solana/programs/solana/src/error.rs @@ -9,4 +9,7 @@ pub enum XmintError { #[msg("Posted VAA Emitter Chain ID or Address Mismatch")] VAAEmitterMismatch, + + #[msg("Receipt already claimed!")] + ReceiptClaimed, } \ No newline at end of file diff --git a/projects/xmint/chains/solana/programs/solana/src/lib.rs b/projects/xmint/chains/solana/programs/solana/src/lib.rs index 27a996d..9a6d5d8 100644 --- a/projects/xmint/chains/solana/programs/solana/src/lib.rs +++ b/projects/xmint/chains/solana/programs/solana/src/lib.rs @@ -4,7 +4,7 @@ use mpl_token_metadata::ID as metadata_program_id; use anchor_lang::solana_program::program::invoke_signed; use anchor_lang::solana_program::instruction::{Instruction, AccountMeta}; use anchor_lang::solana_program::{sysvar::rent, system_program}; -use anchor_spl::token::{ID as spl_id, mint_to}; +use anchor_spl::token::{ID as spl_id, mint_to, approve, MintTo, Approve}; use sha3::Digest; use byteorder::{ @@ -25,8 +25,10 @@ mod event; mod portal; mod wormhole; +use account::*; use context::*; use wormhole::*; +use portal::*; use error::*; use constant::*; @@ -34,9 +36,6 @@ declare_id!("BHz6MJGvo8PJaBFqaxyzgJYdY6o8h1rBgsRrUmnHCU9k"); #[program] pub mod solana { - use anchor_spl::token::MintTo; - - use crate::account::EmitterAddrAccount; use super::*; @@ -97,17 +96,11 @@ pub mod solana { Ok(()) } - - //Submit Foreign Purchase - pub fn submit_foreign_purchase(ctx:Context) -> Result <()> { + pub fn submit_foreign_purchase(ctx:Context) -> Result<()> { // Fetch the VAA //Hash a VAA Extract and derive a VAA Key let vaa = PostedMessageData::try_from_slice(&ctx.accounts.core_bridge_vaa.data.borrow())?.0; - let serialized_vaa = serialize_vaa(&vaa); - - let mut h = sha3::Keccak256::default(); - h.write(serialized_vaa.as_slice()).unwrap(); - let vaa_hash: [u8; 32] = h.finalize().into(); + let vaa_hash: [u8; 32] = vaa_hash(ctx.accounts.core_bridge_vaa.clone()); let (vaa_key, _) = Pubkey::find_program_address(&[ b"PostedVAA", @@ -129,7 +122,6 @@ pub mod solana { - // Complete Transfer of P3 on Portal let complete_wrapped_ix = Instruction { program_id: Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap(), @@ -147,7 +139,7 @@ pub mod solana { AccountMeta::new_readonly(ctx.accounts.mint_authority_wrapped.key(), false), AccountMeta::new_readonly(rent::id(), false), AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(ctx.accounts.token_bridge_program.key(), false), + AccountMeta::new_readonly(ctx.accounts.core_bridge.key(), false), AccountMeta::new_readonly(spl_id, false), ], data: ( @@ -170,7 +162,7 @@ pub mod solana { ctx.accounts.mint_authority_wrapped.to_account_info(), ctx.accounts.rent_account.to_account_info(), ctx.accounts.system_program.to_account_info(), - ctx.accounts.token_bridge_program.to_account_info(), + ctx.accounts.core_bridge.to_account_info(), ctx.accounts.spl_program.to_account_info() ]; @@ -185,13 +177,24 @@ pub mod solana { &[&complete_p3_seeds[..]] )?; - - - - // Mint SOL#T SPL tokens to Contract PDA + //// Figure out how many tokens to mint based on p3 payload + let payload: PayloadTransferWithPayload = PayloadTransferWithPayload::deserialize(&mut vaa.payload.as_slice())?; + ctx.accounts.receipt.amt_to_mint = payload.amount.as_u64() * 100; + ctx.accounts.receipt.foreign_chain = vaa.emitter_chain; + ctx.accounts.receipt.foreign_receipient = payload.payload.as_slice().try_into().unwrap(); + ctx.accounts.receipt.claimed = false; + + Ok(()) + } + + pub fn claim_foreign_purchase(ctx:Context) -> Result<()> { + if ctx.accounts.receipt.claimed { + return err!(XmintError::ReceiptClaimed); + } + let mint_seeds:&[&[u8]] = &[ b"mint_authority", - &[*ctx.bumps.get("mint_authority").unwrap()] + &[*ctx.bumps.get("xmint_authority").unwrap()] ]; mint_to( @@ -205,13 +208,98 @@ pub mod solana { program: ctx.accounts.spl_program.to_account_info(), signer_seeds: &[&mint_seeds[..]] }, - 0 + ctx.accounts.receipt.amt_to_mint + )?; + + // Delgate transfer authority to Token Bridge for the newly minted tokens + approve( + CpiContext::new_with_signer( + ctx.accounts.spl_program.to_account_info(), + Approve { + to: ctx.accounts.xmint_ata_account.to_account_info(), + delegate: ctx.accounts.token_bridge_authority_signer.to_account_info(), + authority: ctx.accounts.xmint_authority.to_account_info() + }, + &[&mint_seeds[..]] + ), + ctx.accounts.receipt.amt_to_mint )?; - // Transfer tokens from Contract PDA to P1 on Portal + // Instruction + let transfer_ix = Instruction { + program_id: Pubkey::from_str(TOKEN_BRIDGE_ADDRESS).unwrap(), + accounts: vec![ + AccountMeta::new(ctx.accounts.payer.key(), true), + AccountMeta::new_readonly(ctx.accounts.token_bridge_config.key(), false), + AccountMeta::new(ctx.accounts.xmint_ata_account.key(), false), + AccountMeta::new(ctx.accounts.xmint_token_mint.key(), false), + AccountMeta::new(ctx.accounts.token_bridge_mint_custody.key(), false), + AccountMeta::new_readonly(ctx.accounts.token_bridge_authority_signer.key(), false), + AccountMeta::new_readonly(ctx.accounts.token_bridge_custody_signer.key(), false), + AccountMeta::new(ctx.accounts.core_bridge_config.key(), false), + AccountMeta::new(ctx.accounts.xmint_transfer_msg_key.key(), true), + AccountMeta::new_readonly(ctx.accounts.token_bridge_emitter.key(), false), + AccountMeta::new(ctx.accounts.token_bridge_sequence_key.key(), false), + AccountMeta::new(ctx.accounts.core_bridge_fee_collector.key(), false), + AccountMeta::new_readonly(ctx.accounts.clock.key(), false), + // Dependencies + AccountMeta::new_readonly(ctx.accounts.rent_account.key(), false), + AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), + // Program + AccountMeta::new_readonly(ctx.accounts.core_bridge.key(), false), + AccountMeta::new_readonly(ctx.accounts.spl_program.key(), false), + ], + data: ( + crate::portal::Instruction::TransferNative, + TransferNativeData { + nonce: ctx.accounts.config.nonce, + amount: ctx.accounts.receipt.amt_to_mint, + fee: 0, + target_address: ctx.accounts.receipt.foreign_receipient, + target_chain: ctx.accounts.receipt.foreign_chain + } + ).try_to_vec()? + }; - Ok(()) + // Accounts + let transfer_accs = vec![ + ctx.accounts.payer.to_account_info(), + ctx.accounts.token_bridge_config.to_account_info(), + ctx.accounts.xmint_ata_account.to_account_info(), + ctx.accounts.xmint_token_mint.to_account_info(), + ctx.accounts.token_bridge_mint_custody.to_account_info(), + ctx.accounts.token_bridge_authority_signer.to_account_info(), + ctx.accounts.token_bridge_custody_signer.to_account_info(), + ctx.accounts.core_bridge_config.to_account_info(), + ctx.accounts.xmint_transfer_msg_key.to_account_info(), + ctx.accounts.token_bridge_emitter.to_account_info(), + ctx.accounts.token_bridge_sequence_key.to_account_info(), + ctx.accounts.core_bridge_fee_collector.to_account_info(), + ctx.accounts.clock.to_account_info(), + // Dependencies + ctx.accounts.rent_account.to_account_info(), + ctx.accounts.system_program.to_account_info(), + // Program + ctx.accounts.core_bridge.to_account_info(), + ctx.accounts.spl_program.to_account_info(), + ]; + + // Signer Seeds + let xmint_authority_seeds:&[&[u8]] = &[ + b"mint_authority", + &[*ctx.bumps.get("xmint_authority").unwrap()] + ]; + + invoke_signed( + &transfer_ix, + &transfer_accs, + &[&xmint_authority_seeds[..]] + )?; + + ctx.accounts.config.nonce += 1; + ctx.accounts.receipt.claimed = true; + Ok(()) } } @@ -230,36 +318,11 @@ pub fn serialize_vaa(vaa: &MessageData) -> Vec { v.into_inner() } -/* - Ok(Instruction { - program_id, - accounts: vec![ - AccountMeta::new(payer, true), - AccountMeta::new_readonly(config_key, false), - message_acc, - claim_acc, - AccountMeta::new_readonly(endpoint, false), - AccountMeta::new(to, false), - AccountMeta::new_readonly(to_owner, true), - if let Some(fee_r) = fee_recipient { - AccountMeta::new(fee_r, false) - } else { - AccountMeta::new(to, false) - }, - AccountMeta::new(mint_key, false), - AccountMeta::new_readonly(meta_key, false), - AccountMeta::new_readonly(mint_authority_key, false), - // Dependencies - AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), - AccountMeta::new_readonly(solana_program::system_program::id(), false), - // Program - AccountMeta::new_readonly(bridge_id, false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: ( - crate::instruction::Instruction::CompleteWrappedWithPayload, - data, - ) - .try_to_vec()?, - }) -*/ \ No newline at end of file +pub fn vaa_hash(vaa: AccountInfo) -> [u8; 32] { + let vaa = PostedMessageData::try_from_slice(&vaa.data.borrow()).unwrap().0; + let serialized_vaa = serialize_vaa(&vaa); + let mut h = sha3::Keccak256::default(); + h.write(serialized_vaa.as_slice()).unwrap(); + let vaa_hash: [u8; 32] = h.finalize().into(); + return vaa_hash; +} diff --git a/projects/xmint/chains/solana/programs/solana/src/portal.rs b/projects/xmint/chains/solana/programs/solana/src/portal.rs index fc56675..4441184 100644 --- a/projects/xmint/chains/solana/programs/solana/src/portal.rs +++ b/projects/xmint/chains/solana/programs/solana/src/portal.rs @@ -2,15 +2,20 @@ use anchor_lang::prelude::*; use primitive_types::U256; use anchor_lang::solana_program::{ program_error::ProgramError::InvalidAccountData, - pubkey::Pubkey, +}; + +use byteorder::{ + BigEndian, + ReadBytesExt, + WriteBytesExt, }; use std::{ + cmp, io::{ Cursor, Read, Write, }, - ops::Deref, }; pub trait SerializePayload: Sized { @@ -50,13 +55,13 @@ pub struct PayloadTransfer { /// Amount being transferred (big-endian uint256) pub amount: U256, /// Address of the token. Left-zero-padded if shorter than 32 bytes - pub token_address: Address, + pub token_address: [u8; 32], /// Chain ID of the token - pub token_chain: ChainID, + pub token_chain: u16, /// Address of the recipient. Left-zero-padded if shorter than 32 bytes - pub to: Address, + pub to: [u8; 32], /// Chain ID of the recipient - pub to_chain: ChainID, + pub to_chain: u16, /// Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount. pub fee: U256, } @@ -66,19 +71,19 @@ impl DeserializePayload for PayloadTransfer { let mut v = Cursor::new(buf); if v.read_u8()? != 1 { - return Err(SolitaireError::Custom(0)); + return err!(PortalError::CustomZeroError); }; let mut am_data: [u8; 32] = [0; 32]; v.read_exact(&mut am_data)?; let amount = U256::from_big_endian(&am_data); - let mut token_address = Address::default(); + let mut token_address = [0; 32]; v.read_exact(&mut token_address)?; let token_chain = v.read_u16::()?; - let mut to = Address::default(); + let mut to = [0; 32]; v.read_exact(&mut to)?; let to_chain = v.read_u16::()?; @@ -103,7 +108,7 @@ impl DeserializePayload for PayloadTransfer { } impl SerializePayload for PayloadTransfer { - fn serialize(&self, writer: &mut W) -> Result<(), SolitaireError> { + fn serialize(&self, writer: &mut W) -> Result<()> { // Payload ID writer.write_u8(1)?; @@ -143,6 +148,68 @@ pub struct PayloadTransferWithPayload { pub payload: Vec, } +impl DeserializePayload for PayloadTransferWithPayload { + fn deserialize(buf: &mut &[u8]) -> Result { + let mut v = Cursor::new(buf); + + if v.read_u8()? != 3 { + return err!(PortalError::CustomZeroError); + }; + + let mut am_data: [u8; 32] = [0; 32]; + v.read_exact(&mut am_data)?; + let amount = U256::from_big_endian(&am_data); + + let mut token_address = [0; 32]; + v.read_exact(&mut token_address)?; + + let token_chain = v.read_u16::()?; + + let mut to = [0; 32]; + v.read_exact(&mut to)?; + + let to_chain = v.read_u16::()?; + + let mut from_address = [0; 32]; + v.read_exact(&mut from_address)?; + + let mut payload = vec![]; + v.read_to_end(&mut payload)?; + + Ok(PayloadTransferWithPayload { + amount, + token_address, + token_chain, + to, + to_chain, + from_address, + payload, + }) + } +} + +impl SerializePayload for PayloadTransferWithPayload { + fn serialize(&self, writer: &mut W) -> Result<()> { + // Payload ID + writer.write_u8(3)?; + + let mut am_data: [u8; 32] = [0; 32]; + self.amount.to_big_endian(&mut am_data); + writer.write_all(&am_data)?; + + writer.write_all(&self.token_address)?; + writer.write_u16::(self.token_chain)?; + writer.write_all(&self.to)?; + writer.write_u16::(self.to_chain)?; + + writer.write_all(&self.from_address)?; + + writer.write_all(self.payload.as_slice())?; + + Ok(()) + } +} + #[derive(PartialEq, Debug)] pub struct PayloadAssetMeta { /// Address of the token. Left-zero-padded if shorter than 32 bytes @@ -157,8 +224,96 @@ pub struct PayloadAssetMeta { pub name: String, } +impl DeserializePayload for PayloadAssetMeta { + fn deserialize(buf: &mut &[u8]) -> Result { + use bstr::ByteSlice; + + let mut v = Cursor::new(buf); + + if v.read_u8()? != 2 { + return err!(PortalError::CustomZeroError); + }; + + let mut token_address = [0; 32]; + v.read_exact(&mut token_address)?; + + let token_chain = v.read_u16::()?; + let decimals = v.read_u8()?; + + let mut symbol_data = vec![0u8; 32]; + v.read_exact(&mut symbol_data)?; + symbol_data.retain(|&c| c != 0); + let mut symbol: Vec = symbol_data.chars().collect(); + symbol.retain(|&c| c != '\u{FFFD}'); + let symbol: String = symbol.iter().collect(); + + let mut name_data = vec![0u8; 32]; + v.read_exact(&mut name_data)?; + name_data.retain(|&c| c != 0); + let mut name: Vec = name_data.chars().collect(); + name.retain(|&c| c != '\u{FFFD}'); + let name: String = name.iter().collect(); + + if v.position() != v.into_inner().len() as u64 { + return Err(InvalidAccountData.into()); + } + + Ok(PayloadAssetMeta { + token_address, + token_chain, + decimals, + symbol, + name, + }) + } +} + +impl SerializePayload for PayloadAssetMeta { + fn serialize(&self, writer: &mut W) -> Result<()> { + // Payload ID + writer.write_u8(2)?; + + writer.write_all(&self.token_address)?; + writer.write_u16::(self.token_chain)?; + + writer.write_u8(self.decimals)?; + + let mut symbol: [u8; 32] = [0; 32]; + let count = cmp::min(symbol.len(), self.symbol.len()); + symbol[..count].copy_from_slice(self.symbol[..count].as_bytes()); + + writer.write_all(&symbol)?; + + let mut name: [u8; 32] = [0; 32]; + let count = cmp::min(name.len(), self.name.len()); + name[..count].copy_from_slice(self.name[..count].as_bytes()); + + writer.write_all(&name)?; + + Ok(()) + } +} #[error_code] pub enum PortalError { - #[msg("")] + #[msg("Solitare Custom(0)")] + CustomZeroError, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Default)] +pub struct TransferWrappedData { + pub nonce: u32, + pub amount: u64, + pub fee: u64, + pub target_address: [u8; 32], + pub target_chain: u16, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Default)] +pub struct TransferNativeData { + pub nonce: u32, + pub amount: u64, + pub fee: u64, + pub target_address: [u8; 32], + pub target_chain: u16, } \ No newline at end of file diff --git a/projects/xmint/chains/solana/programs/solana/src/wormhole.rs b/projects/xmint/chains/solana/programs/solana/src/wormhole.rs index b8a14c5..87bfdcc 100644 --- a/projects/xmint/chains/solana/programs/solana/src/wormhole.rs +++ b/projects/xmint/chains/solana/programs/solana/src/wormhole.rs @@ -5,8 +5,7 @@ use std::{ }; -pub type Address = [u8; 32]; -pub type ChainID = u16; + #[derive(AnchorDeserialize, AnchorSerialize)] pub struct PostMessageData { @@ -89,10 +88,10 @@ pub struct MessageData { pub sequence: u64, /// Emitter of the message - pub emitter_chain: ChainID, + pub emitter_chain: u16, /// Emitter of the message - pub emitter_address: Address, + pub emitter_address: [u8; 32], /// Message payload pub payload: Vec, diff --git a/projects/xmint/handlers/solana.ts b/projects/xmint/handlers/solana.ts index 7b89298..40cad20 100644 --- a/projects/xmint/handlers/solana.ts +++ b/projects/xmint/handlers/solana.ts @@ -85,15 +85,6 @@ export async function deploy(src: string){ mint.toBuffer() ], metaplexProgramID)[0] - /* - const tokenReceipientAddress = getOrCreateAssociatedTokenAccount( - connection, - srcKey, - //WETH MINT\\, - - ) - */ - const redeemerAcc = findProgramAddressSync([Buffer.from("redeemer")], xmint.programId)[0]; // Initalize will also CPI into metaplex metadata program to setup metadata for the token @@ -482,10 +473,38 @@ export async function submitForeignPurchase(src:string, target:string, vaa:strin srcKey, wethMint, redeemerAcc, - true // Allow off curve because the owner of the mintATA acc is a PDA + true // Allow off curve because the owner of the ATA acc is a PDA ); - const tx = await xmint.methods + // MINT SOL#T Tokens to Contract PDA Accounts + const xmintTokenMint = new anchor.web3.PublicKey(srcDeployInfo.tokenAddress); + const xmintAuthority = findProgramAddressSync([Buffer.from("mint_authority")], xmint.programId)[0]; + const xmintAtaAccount = await getOrCreateAssociatedTokenAccount( + connection, + srcKey, + xmintTokenMint, + xmintAuthority, + true // Allow off curve because the owner of the ATA acc is a PDA + ); + + // P1 Portal Transfer back to Recepient Accounts + const tokenBridgeMintCustody = findProgramAddressSync([xmintTokenMint.toBuffer()], tokenBridgePubKey)[0]; + const tokenBridgeAuthoritySigner = findProgramAddressSync([Buffer.from("authority_signer")], tokenBridgePubKey)[0]; + const tokenBridgeCustodySigner = findProgramAddressSync([Buffer.from("custody_signer")], tokenBridgePubKey)[0]; + const coreBridgeConfig = findProgramAddressSync([Buffer.from("Bridge")], coreBridgePubKey)[0]; + const transferMsgKey = new anchor.web3.Keypair(); + const tokenBridgeEmitter = findProgramAddressSync([Buffer.from("emitter")], tokenBridgePubKey)[0]; + const tokenBridgeSequenceKey = findProgramAddressSync([ + Buffer.from("Sequence"), + tokenBridgeEmitter.toBuffer() + ], coreBridgePubKey)[0]; + const coreBridgeFeeCollector = findProgramAddressSync([Buffer.from("fee_collector")], coreBridgePubKey)[0]; + + // need to split the following into two because Solana account limit + const receiptAcc = findProgramAddressSync([ Buffer.from(vaaHash, "hex") ], xmint.programId)[0]; + + // Submit Foreign Purchase + const submitTx = await xmint.methods .submitForeignPurchase() .accounts({ payer: srcKey.publicKey, @@ -494,17 +513,21 @@ export async function submitForeignPurchase(src:string, target:string, vaa:strin coreBridgeVaa: coreBridgeVaaKey, processedVaa: processedVaaKey, emitterAcc: emitterAddressAcc, + receipt: receiptAcc, + tokenBridgeConfig: tokenBridgeConfigAcc, tokenBridgeClaimKey: tokenBridgeClaimAcc, wethAtaAccount: wethAtaAcc.address, + redeemer:redeemerAcc, feeRecipient: wethAtaAcc.address, wethMint: wethMint, wethMintWrappedMeta: wethWrappedMeta, mintAuthorityWrapped: mintAuthorityWrapped, rentAccount: anchor.web3.SYSVAR_RENT_PUBKEY, - tokenBridgeProgram: tokenBridgePubKey, + coreBridge: coreBridgePubKey, splProgram: TOKEN_PROGRAM_ID, - redeemer:redeemerAcc, + + tokenBridge: tokenBridgePubKey }) .preInstructions([ anchor.web3.ComputeBudgetProgram.requestUnits({ @@ -514,7 +537,51 @@ export async function submitForeignPurchase(src:string, target:string, vaa:strin ]) .rpc(); - } + await new Promise((r) => setTimeout(r, 16000)); //wait for accounts to finialize + + // Claim Foreign Purchase + const claimTx = await xmint.methods + .claimForeignPurchase() + .accounts({ + payer: srcKey.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + config: configAcc, + receipt: receiptAcc, + + rentAccount: anchor.web3.SYSVAR_RENT_PUBKEY, + tokenBridgeConfig: tokenBridgeConfigAcc, + splProgram: TOKEN_PROGRAM_ID, + + xmintTokenMint: xmintTokenMint, + xmintAuthority: xmintAuthority, + xmintAtaAccount: xmintAtaAccount.address, + + tokenBridgeMintCustody: tokenBridgeMintCustody, + tokenBridgeAuthoritySigner: tokenBridgeAuthoritySigner, + tokenBridgeCustodySigner: tokenBridgeCustodySigner, + coreBridgeConfig: coreBridgeConfig, + xmintTransferMsgKey: transferMsgKey.publicKey, + tokenBridgeEmitter: tokenBridgeEmitter, + tokenBridgeSequenceKey: tokenBridgeSequenceKey, + coreBridgeFeeCollector: coreBridgeFeeCollector, + clock: anchor.web3.SYSVAR_CLOCK_PUBKEY, + coreBridge: coreBridgePubKey, + + tokenBridge: tokenBridgePubKey + }) + .preInstructions([ + anchor.web3.ComputeBudgetProgram.requestUnits({ + units: 1400000, + additionalFee: 0, + }) + ]) + .signers([transferMsgKey]) + .rpc(); + + const sfpVaa = await fetchVaa(src, claimTx, true); + console.log(sfpVaa); + return sfpVaa; +} export async function sellToken(src:string, target:string, amount:number){} diff --git a/projects/xmint/tests/test.bash b/projects/xmint/tests/test.bash index 6e60c38..45c5d03 100644 --- a/projects/xmint/tests/test.bash +++ b/projects/xmint/tests/test.bash @@ -19,3 +19,9 @@ ts-node orchestrator.ts balance sol0 evm0 # Buy SOL0-TOKEN with eth ts-node orchestrator.ts buy-token evm0 sol0 100 + +# Print Balances for EVM0 and SOL0 Keypairs +ts-node orchestrator.ts balance evm0 evm0 +ts-node orchestrator.ts balance evm0 sol0 +ts-node orchestrator.ts balance sol0 sol0 +ts-node orchestrator.ts balance sol0 evm0 \ No newline at end of file diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 11ead77..c41b811 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -72,6 +72,8 @@ --- +# Reference + - [Other Resources](./reference/overview.md) - [Glossary](./reference/glossary.md) - [Useful Links](./reference/usefulLinks.md) diff --git a/src/introduction/introduction.md b/src/introduction/introduction.md index 4e24a3d..66c3307 100644 --- a/src/introduction/introduction.md +++ b/src/introduction/introduction.md @@ -12,6 +12,4 @@ Ready to step _into the wormhole_? --- -Check out this project's [github repository](https://www.github.com/wormhole-foundation/xdapp-book) to find the accompanying code examples. - -For additional references, see the **Reference** section. +For additional resources, see the **Reference** section. diff --git a/src/reference/glossary.md b/src/reference/glossary.md index cfb3a2e..4e353b6 100644 --- a/src/reference/glossary.md +++ b/src/reference/glossary.md @@ -1,8 +1,8 @@ # Glossary -In some instances, Wormhole uses general terms fo decentralized, cross-chain elements as branded verbiage. In most casese, the definition of the general term does not differ from Wormhole's definition though Wormhole's definitions may be more narrow than general interpretations. +_Disclaimer: In some instances, Wormhole uses general terms for decentralized, cross-chain elements as branded verbiage. In most casese, the definition of the general term does not differ from Wormhole's definition though Wormhole's definitions may be more narrow than general interpretations._ -**Guardian** - One of the 19 validators in the Guardian Network that Contributes to the VAA multisig. +**Guardian** - One of the 19 validators in the Guardian Network that contributes to the VAA multisig. [**Guardian Network**](../wormhole/5_guardianNetwork.md) - Validators that exist in their own p2p network that serve as Wormhole's oracle by observing activity on-chain and generating signed messages attesting to that activity. @@ -12,9 +12,9 @@ In some instances, Wormhole uses general terms fo decentralized, cross-chain ele [**Wormchain**](../wormhole/8_wormchain.md) - A purpose-built cosmos blockchain which aids the Guardian Network and allows for formal interaction with the Guardians. -[**xAssets**](../dapps/3_xdataxassets.md) - Chain-and-path agnostic token that exists on a layer outside the blockchain ecosystem, which can be used to conduct transactions on any blockchain. There are currently two implemented modules: (1) [Token Bridge Module](../technical/evm/xassetLayer.md) and (2) [NFT Bridge Module](../technical/evm/nftLayer.md) +[**xAssets**](../dapps/3_xdataxassets.md) - Chain-and-path agnostic token that exists on a layer outside the blockchain ecosystem, which can be used to conduct transactions on any blockchain. There are currently two implemented modules: (1) [Token Bridge Module](../technical/evm/tokenLayer.md) and (2) [NFT Bridge Module](../technical/evm/nftLayer.md) -**xChain** - Term that referrs to the full range of cross-blockchain interoperability. +**xChain** - Term that refers to the full range of cross-blockchain interoperability. [**xDapp**](../dapps/4_whatIsanXdapp.md) - Decentralized application that enables users to create and/or use xData. diff --git a/src/reference/overview.md b/src/reference/overview.md index 83ea2e2..2c93451 100644 --- a/src/reference/overview.md +++ b/src/reference/overview.md @@ -3,8 +3,6 @@ Here is a collection of other resources and reference sources which you're likely to find helpful. - [Glossary & Terms](./glossary.md) -- [Tools & Helpful Links](./tools.md) -- [Github](./github.md) +- [Tools & Helpful Links](./usefulLinks.md) - [Contract Addresses & Environment Information](./contracts.md) - [RPC Info](./rpcnodes.md) -- [Block Finality Suggestions](./finality.md) diff --git a/src/reference/usefulLinks.md b/src/reference/usefulLinks.md index ef4f913..e8348c9 100644 --- a/src/reference/usefulLinks.md +++ b/src/reference/usefulLinks.md @@ -1,30 +1,32 @@ -# Useful Links +# Tools and Useful Links -Below are a variety of useful links to tools and information in the Wormhole ecosystem that can help you develop xDapps. +Below are a variety of tools and information in the Wormhole ecosystem that can help you develop xDapps. -### Design Documents +### [Design Documents](https://github.com/certusone/wormhole/tree/dev.v2/whitepapers) -Wormhole's component design specifications can be found [here](https://github.com/certusone/wormhole/tree/dev.v2/whitepapers). These outline the reasoning behind design decisions with added technical depth. +Wormhole's component design specifications outline the reasoning behind design decisions with added technical depth. ### Testnet -Wormhole has deployed Core Bridge, Token Bridge and NFT Bridge contracts on various testnets of the chains connected by Wormhole. You can see the deployed addresses [here](./contracts.md). There's only a single Guardian that oversees the testnets, so you might get a higher rate of missed VAAs than you would on mainnet. +Wormhole has deployed Core Bridge, Token Bridge and NFT Bridge contracts on various testnets of the chains connected by Wormhole. You can see the deployed addresses [here](./contracts.md). -### Testnet Bridge UI +_Note: There's only a single Guardian that oversees the testnets, so you might experience a higher rate of missed VAAs than you would on mainnet._ -If you'd like to try out bridging tokens on testnet, there's a UI you can use to attest and transfer tokens for testnet, hosted [here](https://wormhole-foundation.github.io/example-token-bridge-ui/#/transfer). +### [Testnet Bridge UI](https://wormhole-foundation.github.io/example-token-bridge-ui/#/transfer) + +An example UI provided to test out attesting and bridging tokens on testnet. ### Tilt -Tilt is a Kubernetes-based tool that runs a copy of every chain along side a Guardian node to create a simulated testing environment. To set it up and test against it, start [here](../development/tilt/overview.md). +Tilt is a Kubernetes-based tool that runs a copy of every chain along side a Guardian node to create a simulated testing environment. Details on how to set it up and test against it is [here](../development/tilt/overview.md). ### Wormhole Core Repository The Wormhole core repository can be found at [https://github.com/wormhole-foundation/wormhole](https://github.com/wormhole-foundation/wormhole). -### Wormhole Explorer +### [Wormhole Explorer](https://wormholenetwork.com/en/explorer) -[Wormhole Explorer](https://wormholenetwork.com/en/explorer) is a tool that will help you parse VAAs after they've been picked up the Guardian network. +Tool to observe all Wormhole activity and can help you parse VAAs after they've been picked up the Guardian network. ### Wormhole SDK diff --git a/src/technical/evm/relayer.md b/src/technical/evm/relayer.md index ae5536b..892c77a 100644 --- a/src/technical/evm/relayer.md +++ b/src/technical/evm/relayer.md @@ -1,16 +1,16 @@ # Relayer Module -Note: this module is only available in devnet, and is subject to change while still in development. +**_Disclaimer: This module is only available in devnet, and is subject to change while still in development._** In order to integrate with the relayer module (which enables generic relaying), there are two requirements placed on the integrator. -1. The integrator must implement the `wormholeReceiver` interface, which will be called by the relayer to deliver the requested messages. If the recipient contract does not implement this function on their contract, the delivery will automatically fail. +1. To receive messages, the integrator must implement the `wormholeReceiver` interface, which will be called by the relayer to deliver the requested messages. If the recipient contract does not implement this function on their contract, the delivery will automatically fail. -2. The integrator must request delivery to the target chain via the `requestDelivery(DeliveryInstructions instructions)` function on the relayer module. +2. To request message delivery, the integrator must call the `requestDelivery(DeliveryInstructions instructions)` function on the relayer module. ## Receiving Messages -Receiving messages through the relayer module is almost trivial. Simply implement this public function in your contract. +Receiving messages through the relayer module is almost trivial. Simply implement the public function `wormholeReciever` in your contract that the relayer module will invoke. ``` function wormholeReceiver( @@ -21,19 +21,19 @@ function wormholeReceiver( ) ``` -This is the function which will be invoked by the relayer module to deliver messages. +This is the function takes the following four inputs: -- `vaas` are the VAAs which were requested for delivery. -- `sourceChain` is the Wormhole chain ID of the chain the messages were sent from. -- `sourceAddress` is the address which requested delivery. (In Wormhole format!) -- `payload` an additional payload which is at the top level. +- `vaas`: VAAs which were requested for delivery +- `sourceChain`: Wormhole chain ID of the chain the messages were sent from +- `sourceAddress`: address which requested delivery (_In Wormhole format!_) +- `payload`: additional payload which is at the top level -There are only a few noteworthy items here: +There are a few noteworthy items here: -- your `wormholeReceiver` function should never throw an exception. Throwing an exception here will just cause a delivery failure and _will not revert the transaction(!!!)_. -- `wormholeReceiver` will only be called with as much gas as was specified by the compute budget specified by the contract which requested delivery. -- Batch VAAs are always used by the relayer module. `vaas` is an array of all the headless VAAs for which delivery was requested. You still always need to call `core_bridge.parseAndVerifyVM`! The VAAs aren't verified until you have VM objects. (More on this in [Best Practices](./bestPractices.md)) -- The generic relay VAA will be included in the `vaas` array you receive. Usually this VAA is ignored, but you can use it if it's useful to you. +- `wormholeReceiver` function should never throw an exception. Throwing an exception here will just cause a delivery failure and _will not revert the transaction(!!!)_. +- `wormholeReceiver` will only be called with as much gas as was specified by the compute budget specified when the message delivery was requested. +- Batch VAAs are always used by the relayer module. `vaas` is an array of all the headless VAAs for which delivery was requested. These VAAs are not verified until you have VM objects which is obtained by calling `core_bridge.parseAndVerifyVM`! (More on this in [Best Practices](./bestPractices.md)) +- The generic relay VAA will be included in the `vaas` array you receive. This VAA can be ignored, but you can use it if it's useful to you. ## Sending Messages @@ -54,33 +54,33 @@ struct DeliveryParameters { } ``` -- `targetChain` is the chain Id of the chain this should be delivered to. -- `targetAddress` contract address (in Wormhole format) to deliver to. -- `payload` an additional payload which will be included in the delivery. -- `deliveryList` optional. The relayer will also deliver these (already existing) VAAs. This is the mechanism for re-delivery -- `relayParameters` information required to relay to the target env. Contains compute budget. -- `chainPayload` information which can be used for computation efficiency when relaying to other ecosystems. -- `nonce` optional. If included, only messages with this nonce will be relayed. -- `consistencyLevel` how long to wait before emitting the relay request. -- `msg.value` you must send enough native currency with this call to cover the compute budget specified in the relayer parameters. +- `targetChain`: chain ID of the chain this should be delivered to +- `targetAddress`: contract address to deliver to (_in Wormhole format_) +- `payload`: additional payload which will be included in the delivery +- `deliveryList` (_optional_): mechanism for re-delivery of already existing VAAs +- `relayParameters`: information required to relay to the target env. Contains compute budget +- `chainPayload`: information used for computation efficiency when relaying to other ecosystems +- `nonce` (_optional_): If included, only messages with this nonce will be relayed +- `consistencyLevel`: how long to wait before emitting the relay request +- `msg.value`: payment in native currency to relayer that must cover the compute budget specified in the relayer parameters ## Compute Budget -Part of the relay parameters is 'computeBudget'. This specifies the maximum amount of computation which can be spent executing delivery on the destination contract. This is effectively a 'gasLimit' in the EVM ecosystem, but due to the relayer network supporting blockchains which don't utilize the concept of gas, we instead need the more generalizable concept of 'computation budget'. +Part of the relay parameters is a 'computeBudget' which specifies the maximum amount of computation that can be spent executing delivery on the destination contract. This is effectively a 'gasLimit' in the EVM ecosystem, but due to the relayer network supporting blockchains that don't utilize the concept of gas, we use a more generalizable concept of 'computation budget'. -When requesting delivery, the caller must specify and pay for the compute budget upfront. Compute budget which is not utilized will be refunded on the target chain. If the compute budget is exhausted during execution of the delivery, a delivery failure occurs. When a delivery failure occurs, the computation budget from the source chain is not refunded, as the relayer used it to process the failed transaction. +When requesting delivery, the caller must specify and pay for the compute budget upfront. Compute budget which is not utilized will be refunded on the target chain. If the compute budget is exhausted during the execution of the delivery, a delivery failure occurs. When a delivery failure occurs, the computation budget from the source chain is not refunded, as the relayer used it to process the failed transaction. -The computation 'rate' is specified by the relayer module and is different for each blockchain. The quote provided by the relayer module contains not only the fee for the requested compute budget, but also the fixed overheads of the computation which is done by the relayer contract. +The computation 'rate' is specified by the relayer module and is different for each blockchain. The quote provided by the relayer module contains the fee for the requested compute budget AND the fixed overheads of the computation which is done by the relayer contract. ## Delivery Failures -'Delivery Failure' is a technical term in the case of the the relayer module. It does not mean 'something went wrong', but rather that the relayer attempted to deliver the VAA, and was unsuccessful. There are only 3 causes of a delivery failure. +'Delivery Failure' is a technical term in the case of the relayer module. It does not mean 'something went wrong', but rather that the relayer attempted to deliver the VAA, and was unsuccessful. There are only 3 causes of a delivery failure. - The `wormholeReceiver` function is either missing or otherwise uncallable on the recipient contract. - The `wormholeReceiver` function encountered an exception while processing. -- The `wormholeReceiver` function exhausted the gasLimit that was specified by the delivery requester. +- The `wormholeReceiver` function exhausted the computeBudget that was specified by the delivery requester. -All three of these scenarios are controllable by the integrator. In order to avoid delivery failures, the integrators should have a top-level try-catch, such that the wormholeReceiver never reverts, and should always request a worst-case compute budget, because excess budget will be refunded. +All three of these scenarios are controllable by the integrator. In order to avoid delivery failures, the integrators should have a top-level try-catch such that the wormholeReceiver never reverts, and should always request a worst-case compute budget since excess budget will be refunded. ## Delivery Retries diff --git a/src/technical/relayer/genericRelayer.md b/src/technical/relayer/genericRelayer.md index 110051d..a57f480 100644 --- a/src/technical/relayer/genericRelayer.md +++ b/src/technical/relayer/genericRelayer.md @@ -2,12 +2,16 @@ The defining characteristic of generic relayers is that they do not have any off-chain components for the xDapp developer. All aspects of this integration are on chain. -The implementation details vary by blockchain, and you should reference the `relayer module` documentation for each ecosystem. The general strategy is the same however. +The implementation details vary by blockchain so you should reference the `relayer module` documentation for each ecosystem. However, the general workflow is the same. Developers are responsible for implementing a standardized interface which is part of the API agreement with the generic relayer network. This interface generally looks something like ``` -receiveVAA(byte[] batchVAA) +wormholeReceiver( + bytes[] batchVAA, + sourceChain + sourceAddress + payload) ``` This is the entrypoint on your contract which will be called by the relayer. diff --git a/src/technical/relayer/specializedRelayers.md b/src/technical/relayer/specializedRelayers.md index 118ae7f..4049dbe 100644 --- a/src/technical/relayer/specializedRelayers.md +++ b/src/technical/relayer/specializedRelayers.md @@ -2,7 +2,7 @@ Rather than home-rolling a relayer, it's recommended that integrators start from the existing [Spy Relayer](https://github.com/wormhole-foundation/wormhole/tree/dev.v2/relayer/spy_relayer) provided in the Wormhole Core Repository. -There's additionally an extensible relayer (called the [Plugin Relayer](https://github.com/wormhole-foundation/wormhole/tree/feat/plugin_relayer/relayer/plugin_relayer)) currently in development. +Additionally there's an extensible relayer (called the [Plugin Relayer](https://github.com/wormhole-foundation/wormhole/tree/feat/plugin_relayer/relayer/plugin_relayer)) currently in development.