From fb244e6df63df7d4e4c4f0b8461c15af6af97ff3 Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Mon, 29 Apr 2024 17:00:55 +0100 Subject: [PATCH 1/8] sdk/solana: only create idl dir if it doesn't exist --- sdk/solana/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/solana/package.json b/sdk/solana/package.json index baa8f2a40..7cb9ce239 100644 --- a/sdk/solana/package.json +++ b/sdk/solana/package.json @@ -41,7 +41,7 @@ "test:ci": "jest --config ./jest.config.ts", "copy:idl": "cp ../../solana/target/idl/*.json ./src/anchor-idl/$IDL_VERSION/", "copy:types": "cp ../../solana/target/types/*.ts ./src/anchor-idl/$IDL_VERSION/", - "generate": "export IDL_VERSION=`tsx scripts/readVersion.ts` && mkdir ./src/anchor-idl/$IDL_VERSION && npm run copy:idl && npm run copy:types", + "generate": "export IDL_VERSION=`tsx scripts/readVersion.ts` && mkdir -p ./src/anchor-idl/$IDL_VERSION && npm run copy:idl && npm run copy:types", "build:contracts": "cd ../../solana && make build" }, "devDependencies": { From d9f3d02f1daeb96fb0652ae8cba4b1244e3042e5 Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Mon, 8 Apr 2024 20:51:33 +0200 Subject: [PATCH 2/8] solana: add dummy transfer hook --- solana/Anchor.toml | 6 +- solana/Cargo.lock | 11 ++ solana/idl/json/dummy_transfer_hook.json | 5 +- .../programs/dummy-transfer-hook/Cargo.toml | 27 +++ .../programs/dummy-transfer-hook/Xargo.toml | 2 + .../programs/dummy-transfer-hook/src/lib.rs | 171 ++++++++++++++++++ 6 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 solana/programs/dummy-transfer-hook/Cargo.toml create mode 100644 solana/programs/dummy-transfer-hook/Xargo.toml create mode 100644 solana/programs/dummy-transfer-hook/src/lib.rs diff --git a/solana/Anchor.toml b/solana/Anchor.toml index 1c1d40873..fa62d51a1 100644 --- a/solana/Anchor.toml +++ b/solana/Anchor.toml @@ -7,9 +7,10 @@ seeds = false skip-lint = false [programs.localnet] +dummy_transfer_hook = "BgabMDLaxsyB7eGMBt9L22MSk9KMrL4zY2iNe14kyFP5" example_native_token_transfers = "nttiK1SepaQt6sZ4WGW5whvc9tEnGXGxuKeptcQPCcS" -wormhole_governance = "wgvEiKVzX9yyEoh41jZAdC6JqGUTS4CFXbFGBV5TKdZ" ntt_quoter = "9jFBLvMZZERVmeY4tbq5MejbXRE18paGEuoB6xVJZgGe" +wormhole_governance = "wgvEiKVzX9yyEoh41jZAdC6JqGUTS4CFXbFGBV5TKdZ" [registry] url = "https://api.apr.dev" @@ -31,7 +32,10 @@ address = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" program = "programs/example-native-token-transfers/tests/fixtures/mainnet_core_bridge.so" [test.validator] +bind_address = "0.0.0.0" url = "https://api.mainnet-beta.solana.com" +ledger = ".anchor/test-ledger" +rpc_port = 8899 ticks_per_slot = 16 [[test.validator.account]] diff --git a/solana/Cargo.lock b/solana/Cargo.lock index 23a4206de..993cceb4a 100644 --- a/solana/Cargo.lock +++ b/solana/Cargo.lock @@ -1392,6 +1392,17 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "dummy-transfer-hook" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "solana-program", + "spl-tlv-account-resolution 0.6.3", + "spl-transfer-hook-interface 0.6.3", +] + [[package]] name = "dyn-clone" version = "1.0.16" diff --git a/solana/idl/json/dummy_transfer_hook.json b/solana/idl/json/dummy_transfer_hook.json index 586fb2b72..27f37cb16 100644 --- a/solana/idl/json/dummy_transfer_hook.json +++ b/solana/idl/json/dummy_transfer_hook.json @@ -106,8 +106,5 @@ ] } } - ], - "metadata": { - "address": "BgabMDLaxsyB7eGMBt9L22MSk9KMrL4zY2iNe14kyFP5" - } + ] } \ No newline at end of file diff --git a/solana/programs/dummy-transfer-hook/Cargo.toml b/solana/programs/dummy-transfer-hook/Cargo.toml new file mode 100644 index 000000000..6f78242db --- /dev/null +++ b/solana/programs/dummy-transfer-hook/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "dummy-transfer-hook" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "dummy_transfer_hook" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +mainnet = [] +solana-devnet = [] +tilt-devnet = [] +tilt-devnet2 = [ "tilt-devnet" ] + +[dependencies] +anchor-lang.workspace = true +anchor-spl.workspace = true +solana-program.workspace = true +spl-tlv-account-resolution = "0.6.3" +spl-transfer-hook-interface = "0.6.3" diff --git a/solana/programs/dummy-transfer-hook/Xargo.toml b/solana/programs/dummy-transfer-hook/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/solana/programs/dummy-transfer-hook/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/solana/programs/dummy-transfer-hook/src/lib.rs b/solana/programs/dummy-transfer-hook/src/lib.rs new file mode 100644 index 000000000..a49720bdf --- /dev/null +++ b/solana/programs/dummy-transfer-hook/src/lib.rs @@ -0,0 +1,171 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use spl_tlv_account_resolution::state::ExtraAccountMetaList; + +declare_id!("BgabMDLaxsyB7eGMBt9L22MSk9KMrL4zY2iNe14kyFP5"); + +/// Index of the sender token account in the accounts passed to the transfer hook +pub const SENDER_TOKEN_ACCOUNT_INDEX: u8 = 0; +/// Index of the mint account in the accounts passed to the transfer hook +pub const MINT_ACCOUNT_INDEX: u8 = 1; +/// Index of the destination token account in the accounts passed to the transfer hook +pub const DESTINATION_TOKEN_ACCOUNT_INDEX: u8 = 2; +/// Index of the authority account in the accounts passed to the transfer hook +pub const AUTHORITY_ACCOUNT_INDEX: u8 = 3; + +/// Number of extra accounts in the ExtraAccountMetaList account +pub const EXTRA_ACCOUNTS_LEN: u8 = 2; + +#[program] +pub mod dummy_transfer_hook { + use spl_tlv_account_resolution::{ + account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, + }; + use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction}; + + use super::*; + + pub fn initialize_extra_account_meta_list( + ctx: Context, + ) -> Result<()> { + let account_metas = vec![ + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: "dummy_account".as_bytes().to_vec(), + }, + // owner field of the sender token account + Seed::AccountData { + account_index: SENDER_TOKEN_ACCOUNT_INDEX, + data_index: 32, + length: 32, + }, + ], + false, // is_signer + false, // is_writable + )?, + ExtraAccountMeta::new_with_seeds( + &[Seed::Literal { + bytes: "counter".as_bytes().to_vec(), + }], + false, // is_signer + true, // is_writable + )?, + ]; + + assert_eq!(EXTRA_ACCOUNTS_LEN as usize, account_metas.len()); + + // initialize ExtraAccountMetaList account with extra accounts + ExtraAccountMetaList::init::( + &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?, + &account_metas, + )?; + + Ok(()) + } + + pub fn transfer_hook(ctx: Context, _amount: u64) -> Result<()> { + ctx.accounts.counter.count += 1; + Ok(()) + } + + // NOTE: the CPI call makes that the token2022 program makes (naturally) does not + // follow the anchor calling convention, so we need to implement a fallback + // instruction to handle the custom instruction + pub fn fallback<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], + ) -> Result<()> { + let instruction = TransferHookInstruction::unpack(data)?; + + // match instruction discriminator to transfer hook interface execute instruction + // token2022 program CPIs this instruction on token transfer + match instruction { + TransferHookInstruction::Execute { amount } => { + let amount_bytes = amount.to_le_bytes(); + + // invoke custom transfer hook instruction on our program + __private::__global::transfer_hook(program_id, accounts, &amount_bytes) + } + _ => Err(ProgramError::InvalidInstructionData.into()), + } + } +} + +#[account] +#[derive(InitSpace)] +pub struct Counter { + pub count: u64, +} + +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList<'info> { + #[account(mut)] + payer: Signer<'info>, + + /// CHECK: ExtraAccountMetaList Account, must use these seeds + #[account( + init, + payer = payer, + space = ExtraAccountMetaList::size_of(EXTRA_ACCOUNTS_LEN as usize)?, + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: AccountInfo<'info>, + pub mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + + #[account( + init, + payer = payer, + space = 8 + Counter::INIT_SPACE, + seeds = [b"counter"], + bump + )] + pub counter: Account<'info, Counter>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +/// NOTE: this is just a dummy transfer hook to test that the accounts are +/// passed in correctly. Do NOT use this as a starting point in a real +/// application, as it's not secure. +pub struct TransferHook<'info> { + #[account( + token::mint = mint, + )] + pub source_token: InterfaceAccount<'info, TokenAccount>, + pub mint: InterfaceAccount<'info, Mint>, + #[account( + token::mint = mint, + )] + pub destination_token: InterfaceAccount<'info, TokenAccount>, + /// CHECK: source token account authority, can be SystemAccount or PDA owned by another program + pub authority: UncheckedAccount<'info>, + /// CHECK: ExtraAccountMetaList Account, + #[account( + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: UncheckedAccount<'info>, + #[account( + seeds = [b"dummy_account", source_token.owner.as_ref()], + bump + )] + /// CHECK: dummy account. It just tests that the off-chain code correctly + /// computes and the on-chain code correctly passes on the PDA. + pub dummy_account: AccountInfo<'info>, + + #[account( + mut, + seeds = [b"counter"], + bump + )] + pub counter: Account<'info, Counter>, +} From 62b145b3c34882ef6478c9494031ec736377899c Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Mon, 8 Apr 2024 20:52:19 +0200 Subject: [PATCH 3/8] solana/transfer: use hook helper to transfer tokens TODO: the old token program still works -- is this secure? I think so but double check --- .../src/instructions/release_inbound.rs | 31 ++++++++-------- .../src/instructions/transfer.rs | 35 ++++++++++--------- .../example-native-token-transfers/src/lib.rs | 9 +++-- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs b/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs index 4d4d3d03e..e4fb201f8 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::token_interface; use ntt_messages::mode::Mode; +use spl_token_2022::onchain; use crate::{ config::*, @@ -119,8 +120,8 @@ pub struct ReleaseInboundUnlock<'info> { /// Setting this flag to `false` is useful when bundling this instruction /// together with [`crate::instructions::redeem`], so that the unlocking /// is attempted optimistically. -pub fn release_inbound_unlock( - ctx: Context, +pub fn release_inbound_unlock<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { let inbox_item = &mut ctx.accounts.common.inbox_item; @@ -138,22 +139,22 @@ pub fn release_inbound_unlock( assert!(inbox_item.release_status == ReleaseStatus::Released); match ctx.accounts.common.config.mode { Mode::Burning => Err(NTTError::InvalidMode.into()), - Mode::Locking => token_interface::transfer_checked( - CpiContext::new_with_signer( - ctx.accounts.common.token_program.to_account_info(), - token_interface::TransferChecked { - from: ctx.accounts.custody.to_account_info(), - to: ctx.accounts.common.recipient.to_account_info(), - authority: ctx.accounts.common.token_authority.clone(), - mint: ctx.accounts.common.mint.to_account_info(), - }, + Mode::Locking => { + onchain::invoke_transfer_checked( + &ctx.accounts.common.token_program.key(), + ctx.accounts.custody.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.recipient.to_account_info(), + ctx.accounts.common.token_authority.clone(), + ctx.remaining_accounts, + inbox_item.amount, + ctx.accounts.common.mint.decimals, &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], ]], - ), - inbox_item.amount, - ctx.accounts.common.mint.decimals, - ), + )?; + Ok(()) + } } } diff --git a/solana/programs/example-native-token-transfers/src/instructions/transfer.rs b/solana/programs/example-native-token-transfers/src/instructions/transfer.rs index 82891ad8d..aab502e7f 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/transfer.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/transfer.rs @@ -2,6 +2,7 @@ use anchor_lang::prelude::*; use anchor_spl::token_interface; use ntt_messages::{chain_id::ChainId, mode::Mode, trimmed_amount::TrimmedAmount}; +use spl_token_2022::onchain; use crate::{ bitmap::Bitmap, @@ -215,7 +216,10 @@ pub struct TransferLock<'info> { pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, } -pub fn transfer_lock(ctx: Context, args: TransferArgs) -> Result<()> { +pub fn transfer_lock<'info>( + ctx: Context<'_, '_, '_, 'info, TransferLock<'info>>, + args: TransferArgs, +) -> Result<()> { require_eq!( ctx.accounts.common.config.mode, Mode::Locking, @@ -240,24 +244,21 @@ pub fn transfer_lock(ctx: Context, args: TransferArgs) -> Result<( let before = accs.custody.amount; - token_interface::transfer_checked( - CpiContext::new_with_signer( - accs.common.token_program.to_account_info(), - token_interface::TransferChecked { - from: accs.common.from.to_account_info(), - to: accs.custody.to_account_info(), - authority: accs.session_authority.to_account_info(), - mint: accs.common.mint.to_account_info(), - }, - &[&[ - crate::SESSION_AUTHORITY_SEED, - accs.common.from.owner.as_ref(), - args.keccak256().as_ref(), - &[ctx.bumps.session_authority], - ]], - ), + onchain::invoke_transfer_checked( + &accs.common.token_program.key(), + accs.common.from.to_account_info(), + accs.common.mint.to_account_info(), + accs.custody.to_account_info(), + accs.session_authority.to_account_info(), + ctx.remaining_accounts, amount, accs.common.mint.decimals, + &[&[ + crate::SESSION_AUTHORITY_SEED, + accs.common.from.owner.as_ref(), + args.keccak256().as_ref(), + &[ctx.bumps.session_authority], + ]], )?; accs.custody.reload()?; diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 1d3ebc3bb..8a2704301 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -81,7 +81,10 @@ pub mod example_native_token_transfers { instructions::transfer_burn(ctx, args) } - pub fn transfer_lock(ctx: Context, args: TransferArgs) -> Result<()> { + pub fn transfer_lock<'info>( + ctx: Context<'_, '_, '_, 'info, TransferLock<'info>>, + args: TransferArgs, + ) -> Result<()> { instructions::transfer_lock(ctx, args) } @@ -96,8 +99,8 @@ pub mod example_native_token_transfers { instructions::release_inbound_mint(ctx, args) } - pub fn release_inbound_unlock( - ctx: Context, + pub fn release_inbound_unlock<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { instructions::release_inbound_unlock(ctx, args) From b17e7d2bce145aa67726b0697af07ec0944ce1bb Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Tue, 9 Apr 2024 12:34:45 +0200 Subject: [PATCH 4/8] solana/sdk: support transfer hook --- solana/tests/example-native-token-transfer.ts | 122 ++++++++++-- solana/ts/sdk/ntt.ts | 178 ++++++++++++++++-- 2 files changed, 267 insertions(+), 33 deletions(-) diff --git a/solana/tests/example-native-token-transfer.ts b/solana/tests/example-native-token-transfer.ts index 21613204b..4221419e6 100644 --- a/solana/tests/example-native-token-transfer.ts +++ b/solana/tests/example-native-token-transfer.ts @@ -16,14 +16,17 @@ import { serializePayload, deserializePayload, } from "@wormhole-foundation/sdk-definitions"; -import { NttMessage, postVaa, NTT, nttMessageLayout } from "../ts/sdk"; +import { postVaa, NTT, nttMessageLayout } from "../ts/sdk"; +import { WormholeTransceiverMessage } from "../ts/sdk/nttLayout"; + import { - NativeTokenTransfer, - TransceiverMessage, - WormholeTransceiverMessage, - nativeTokenTransferLayout, - nttManagerMessageLayout, -} from "../ts/sdk/nttLayout"; + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; + +import { DummyTransferHook } from "../target/types/dummy_transfer_hook"; export const GUARDIAN_KEY = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; @@ -48,25 +51,78 @@ describe("example-native-token-transfers", () => { const user = anchor.web3.Keypair.generate(); let tokenAccount: anchor.web3.PublicKey; - let mint: anchor.web3.PublicKey; + const mint = anchor.web3.Keypair.generate(); + + const dummyTransferHook = anchor.workspace + .DummyTransferHook as anchor.Program; + + const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + dummyTransferHook.programId + ); + + const [counterPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + dummyTransferHook.programId + ); + + async function counterValue(): Promise { + const counter = await dummyTransferHook.account.counter.fetch(counterPDA); + return counter.count + } + + it("Initialize mint", async () => { + const extensions = [spl.ExtensionType.TransferHook]; + const mintLen = spl.getMintLen(extensions); + const lamports = await connection.getMinimumBalanceForRentExemption( + mintLen + ); + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports, + programId: spl.TOKEN_2022_PROGRAM_ID, + }), + spl.createInitializeTransferHookInstruction( + mint.publicKey, + owner.publicKey, + dummyTransferHook.programId, + spl.TOKEN_2022_PROGRAM_ID + ), + spl.createInitializeMintInstruction( + mint.publicKey, + 9, + owner.publicKey, + null, + spl.TOKEN_2022_PROGRAM_ID + ) + ); - before(async () => { - // airdrop some tokens to payer - mint = await spl.createMint(connection, payer, owner.publicKey, null, 9); + await sendAndConfirmTransaction(connection, transaction, [payer, mint]); tokenAccount = await spl.createAssociatedTokenAccount( connection, payer, - mint, - user.publicKey + mint.publicKey, + user.publicKey, + undefined, + spl.TOKEN_2022_PROGRAM_ID, + spl.ASSOCIATED_TOKEN_PROGRAM_ID ); + await spl.mintTo( connection, payer, - mint, + mint.publicKey, tokenAccount, owner, - BigInt(10000000) + BigInt(10000000), + undefined, + undefined, + spl.TOKEN_2022_PROGRAM_ID ); }); @@ -75,22 +131,47 @@ describe("example-native-token-transfers", () => { expect(version).to.equal("1.0.0"); }); + it("Create ExtraAccountMetaList Account", async () => { + const initializeExtraAccountMetaListInstruction = + await dummyTransferHook.methods + .initializeExtraAccountMetaList() + .accountsStrict({ + payer: payer.publicKey, + mint: mint.publicKey, + counter: counterPDA, + extraAccountMetaList: extraAccountMetaListPDA, + tokenProgram: spl.TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add( + initializeExtraAccountMetaListInstruction + ); + + await sendAndConfirmTransaction(connection, transaction, [payer]); + }); + describe("Locking", () => { before(async () => { await spl.setAuthority( connection, payer, - mint, + mint.publicKey, owner, - 0, // mint - ntt.tokenAuthorityAddress() + spl.AuthorityType.MintTokens, + ntt.tokenAuthorityAddress(), + [], + undefined, + spl.TOKEN_2022_PROGRAM_ID ); await ntt.initialize({ payer, owner: payer, chain: "solana", - mint, + mint: mint.publicKey, outboundLimit: new BN(1000000), mode: "locking", }); @@ -173,6 +254,7 @@ describe("example-native-token-transfers", () => { // TODO: assert other stuff in the message // console.log(nttManagerMessage); + expect((await counterValue()).toString()).to.be.eq("1") }); it("Can receive tokens", async () => { @@ -230,6 +312,8 @@ describe("example-native-token-transfers", () => { }); expect(released).to.equal(true); + + expect((await counterValue()).toString()).to.be.eq("2") }); }); diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 0f1b32f48..b729c26b7 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -20,6 +20,7 @@ import { sendAndConfirmTransaction, type TransactionSignature, type Connection, + SystemProgram, TransactionMessage, VersionedTransaction } from '@solana/web3.js' @@ -232,7 +233,7 @@ export class NTT { const tokenProgram = mintInfo.owner const ix = await this.program.methods .initialize({ chainId, limit: args.outboundLimit, mode }) - .accounts({ + .accountsStrict({ payer: args.payer.publicKey, deployer: args.owner.publicKey, programData: programDataAddress(this.program.programId), @@ -241,8 +242,10 @@ export class NTT { rateLimit: this.outboxRateLimitAccountAddress(), tokenProgram, tokenAuthority: this.tokenAuthorityAddress(), - custody: await this.custodyAccountAddress(args.mint), + custody: await this.custodyAccountAddress(args.mint, tokenProgram), bpfLoaderUpgradeableProgram: BPF_LOADER_UPGRADEABLE_PROGRAM_ID, + associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, }).instruction(); return sendAndConfirmTransaction(this.program.provider.connection, new Transaction().add(ix), [args.payer, args.owner]); } @@ -298,7 +301,9 @@ export class NTT { args.from, this.sessionAuthorityAddress(args.fromAuthority.publicKey, transferArgs), args.fromAuthority.publicKey, - BigInt(args.amount.toString()) + BigInt(args.amount.toString()), + [], + config.tokenProgram ); const tx = new Transaction() tx.add(approveIx, transferIx, releaseIx) @@ -398,7 +403,7 @@ export class NTT { shouldQueue: args.shouldQueue } - return await this.program.methods + const transferIx = await this.program.methods .transferLock(transferArgs) .accounts({ common: { @@ -416,6 +421,39 @@ export class NTT { sessionAuthority: this.sessionAuthorityAddress(args.fromAuthority, transferArgs) }) .instruction() + + const mintInfo = await splToken.getMint( + this.program.provider.connection, + config.mint, + undefined, + config.tokenProgram + ) + const transferHook = splToken.getTransferHook(mintInfo) + + if (transferHook) { + const source = args.from + const mint = config.mint + const destination = await this.custodyAccountAddress(config) + const owner = this.sessionAuthorityAddress(args.fromAuthority, transferArgs) + await addExtraAccountMetasForExecute( + this.program.provider.connection, + transferIx, + transferHook.programId, + source, + mint, + destination, + owner, + // TODO(csongor): compute the amount that's passed into transfer. + // Leaving this 0 is fine unless the transfer hook accounts addresses + // depend on the amount (which is unlikely). + // If this turns out to be the case, the amount to put here is the + // untrimmed amount after removing dust. + 0, + ); + } + + return transferIx + } /** @@ -496,14 +534,15 @@ export class NTT { .releaseInboundMint({ revertOnDelay: args.revertOnDelay }) - .accounts({ + .accountsStrict({ common: { payer: args.payer, config: { config: this.configAccountAddress() }, inboxItem: this.inboxItemAccountAddress(args.chain, args.nttMessage), - recipient: getAssociatedTokenAddressSync(mint, recipientAddress), + recipient: getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram), mint, - tokenAuthority: this.tokenAuthorityAddress() + tokenAuthority: this.tokenAuthorityAddress(), + tokenProgram: config.tokenProgram } }) .instruction() @@ -551,22 +590,50 @@ export class NTT { const mint = await this.mintAccountAddress(config) - return await this.program.methods + const transferIx = await this.program.methods .releaseInboundUnlock({ revertOnDelay: args.revertOnDelay }) - .accounts({ + .accountsStrict({ common: { payer: args.payer, config: { config: this.configAccountAddress() }, inboxItem: this.inboxItemAccountAddress(args.chain, args.nttMessage), - recipient: getAssociatedTokenAddressSync(mint, recipientAddress), + recipient: getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram), mint, - tokenAuthority: this.tokenAuthorityAddress() + tokenAuthority: this.tokenAuthorityAddress(), + tokenProgram: config.tokenProgram }, custody: await this.custodyAccountAddress(config) }) .instruction() + + const mintInfo = await splToken.getMint(this.program.provider.connection, config.mint, undefined, config.tokenProgram) + const transferHook = splToken.getTransferHook(mintInfo) + + if (transferHook) { + const source = await this.custodyAccountAddress(config) + const mint = config.mint + const destination = getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram) + const owner = this.tokenAuthorityAddress() + await addExtraAccountMetasForExecute( + this.program.provider.connection, + transferIx, + transferHook.programId, + source, + mint, + destination, + owner, + // TODO(csongor): compute the amount that's passed into transfer. + // Leaving this 0 is fine unless the transfer hook accounts addresses + // depend on the amount (which is unlikely). + // If this turns out to be the case, the amount to put here is the + // untrimmed amount after removing dust. + 0, + ); + } + + return transferIx } async releaseInboundUnlock(args: { @@ -891,11 +958,11 @@ export class NTT { * (i.e. the program is initialised), the mint is derived from the config. * Otherwise, the mint must be provided. */ - async custodyAccountAddress(configOrMint: Config | PublicKey): Promise { + async custodyAccountAddress(configOrMint: Config | PublicKey, tokenProgram = splToken.TOKEN_PROGRAM_ID): Promise { if (configOrMint instanceof PublicKey) { - return associatedAddress({ mint: configOrMint, owner: this.tokenAuthorityAddress() }) + return splToken.getAssociatedTokenAddress(configOrMint, this.tokenAuthorityAddress(), true, tokenProgram) } else { - return associatedAddress({ mint: await this.mintAccountAddress(configOrMint), owner: this.tokenAuthorityAddress() }) + return splToken.getAssociatedTokenAddress(configOrMint.mint, this.tokenAuthorityAddress(), true, configOrMint.tokenProgram) } } } @@ -903,3 +970,86 @@ export class NTT { function exhaustive(_: never): A { throw new Error('Impossible') } + +/** + * TODO: this is copied from @solana/spl-token, because the most recent released + * version (0.4.3) is broken (does object equality instead of structural on the pubkey) + * + * this version fixes that error, looks like it's also fixed on main: + * https://github.com/solana-labs/solana-program-library/blob/ad4eb6914c5e4288ad845f29f0003cd3b16243e7/token/js/src/extensions/transferHook/instructions.ts#L208 + */ +async function addExtraAccountMetasForExecute( + connection: Connection, + instruction: TransactionInstruction, + programId: PublicKey, + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: number | bigint, + commitment?: Commitment +) { + const validateStatePubkey = splToken.getExtraAccountMetaAddress(mint, programId); + const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment); + if (validateStateAccount == null) { + return instruction; + } + const validateStateData = splToken.getExtraAccountMetas(validateStateAccount); + + // Check to make sure the provided keys are in the instruction + if (![source, mint, destination, owner].every((key) => instruction.keys.some((meta) => meta.pubkey.equals(key)))) { + throw new Error('Missing required account in instruction'); + } + + const executeInstruction = splToken.createExecuteInstruction( + programId, + source, + mint, + destination, + owner, + validateStatePubkey, + BigInt(amount) + ); + + for (const extraAccountMeta of validateStateData) { + executeInstruction.keys.push( + deEscalateAccountMeta( + await splToken.resolveExtraAccountMeta( + connection, + extraAccountMeta, + executeInstruction.keys, + executeInstruction.data, + executeInstruction.programId + ), + executeInstruction.keys + ) + ); + } + + // Add only the extra accounts resolved from the validation state + instruction.keys.push(...executeInstruction.keys.slice(5)); + + // Add the transfer hook program ID and the validation state account + instruction.keys.push({ pubkey: programId, isSigner: false, isWritable: false }); + instruction.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false }); +} + +// TODO: delete (see above) +function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMeta[]): AccountMeta { + const maybeHighestPrivileges = accountMetas + .filter((x) => x.pubkey.equals(accountMeta.pubkey)) + .reduce<{ isSigner: boolean; isWritable: boolean } | undefined>((acc, x) => { + if (!acc) return { isSigner: x.isSigner, isWritable: x.isWritable }; + return { isSigner: acc.isSigner || x.isSigner, isWritable: acc.isWritable || x.isWritable }; + }, undefined); + if (maybeHighestPrivileges) { + const { isSigner, isWritable } = maybeHighestPrivileges; + if (!isSigner && isSigner !== accountMeta.isSigner) { + accountMeta.isSigner = false; + } + if (!isWritable && isWritable !== accountMeta.isWritable) { + accountMeta.isWritable = false; + } + } + return accountMeta; +} From 05ba7eee80f3160d900aa36be60017e89e37fe2e Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Mon, 29 Apr 2024 17:35:09 +0100 Subject: [PATCH 5/8] solana: transfer before burn/mint to trigger hooks --- sdk/solana/src/ntt.ts | 79 +++++++---- .../json/example_native_token_transfers.json | 58 ++++++-- .../idl/ts/example_native_token_transfers.ts | 116 +++++++++++---- .../src/instructions/initialize.rs | 5 +- .../src/instructions/release_inbound.rs | 123 ++++++++++------ .../src/instructions/transfer.rs | 133 +++++++++++++----- .../example-native-token-transfers/src/lib.rs | 9 +- .../tests/sdk/instructions/transfer.rs | 3 +- solana/tests/example-native-token-transfer.ts | 30 +--- solana/ts/sdk/ntt.ts | 91 ++++++++++-- 10 files changed, 451 insertions(+), 196 deletions(-) diff --git a/sdk/solana/src/ntt.ts b/sdk/solana/src/ntt.ts index 4b5f7754e..6b3e3f60c 100644 --- a/sdk/solana/src/ntt.ts +++ b/sdk/solana/src/ntt.ts @@ -1,5 +1,4 @@ import { Program } from "@coral-xyz/anchor"; -import { associatedAddress } from "@coral-xyz/anchor/dist/cjs/utils/token.js"; import * as splToken from "@solana/spl-token"; import { createAssociatedTokenAccountInstruction, @@ -9,6 +8,7 @@ import { Connection, Keypair, PublicKey, + SystemProgram, Transaction, TransactionInstruction, TransactionMessage, @@ -236,16 +236,11 @@ export class SolanaNtt ); } - const custodyAddress = associatedAddress({ - mint: args.mint, - owner: this.pdas.tokenAuthority(), - }); - const tokenProgram = mintInfo.owner; const limit = new BN(args.outboundLimit.toString()); const ix = await this.program.methods .initialize({ chainId, limit: limit, mode }) - .accounts({ + .accountsStrict({ payer: args.payer.publicKey, deployer: args.owner.publicKey, programData: programDataAddress(this.program.programId), @@ -254,8 +249,10 @@ export class SolanaNtt rateLimit: this.pdas.outboxRateLimitAccount(), tokenProgram, tokenAuthority: this.pdas.tokenAuthority(), - custody: custodyAddress, + custody: await this.custodyAccountAddress(args.mint, tokenProgram), bpfLoaderUpgradeableProgram: BPF_LOADER_UPGRADEABLE_PROGRAM_ID, + associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, }) .instruction(); @@ -278,7 +275,7 @@ export class SolanaNtt const ix = await this.program.methods .registerTransceiver() - .accounts({ + .accountsStrict({ payer: args.payer.publicKey, owner: args.owner.publicKey, config: this.pdas.configAccount(), @@ -286,6 +283,7 @@ export class SolanaNtt registeredTransceiver: this.pdas.registeredTransceiver( args.transceiver ), + systemProgram: SystemProgram.programId, }) .instruction(); @@ -307,6 +305,7 @@ export class SolanaNtt feeCollector: whAccs.wormholeFeeCollector, sequence: whAccs.wormholeSequence, program: this.core.address, + systemProgram: SystemProgram.programId, }, }) .instruction(); @@ -337,11 +336,12 @@ export class SolanaNtt chainId: { id: toChainId(peer.chain) }, address: Array.from(peer.address.toUniversalAddress().toUint8Array()), }) - .accounts({ + .accountsStrict({ payer: sender, owner: sender, config: this.pdas.configAccount(), peer: this.pdas.transceiverPeerAccount(peer.chain), + systemProgram: SystemProgram.programId, }) .instruction(), this.program.methods @@ -390,12 +390,13 @@ export class SolanaNtt limit: new BN(inboundLimit.toString()), tokenDecimals: tokenDecimals, }) - .accounts({ + .accountsStrict({ payer: sender, owner: sender, config: this.pdas.configAccount(), peer: this.pdas.peerAccount(peer.chain), inboxRateLimit: this.pdas.inboxRateLimitAccount(peer.chain), + systemProgram: SystemProgram.programId, }) .instruction(); @@ -671,7 +672,7 @@ export class SolanaNtt ), shouldQueue: args.transferArgs.shouldQueue, }) - .accounts({ + .accountsStrict({ common: { payer: args.payer, config: { config: this.pdas.configAccount() }, @@ -680,10 +681,11 @@ export class SolanaNtt tokenProgram: config.tokenProgram, outboxItem: args.outboxItem, outboxRateLimit: this.pdas.outboxRateLimitAccount(), + systemProgram: SystemProgram.programId, + custody: config.custody, }, peer: this.pdas.peerAccount(recipientChain), inboxRateLimit: this.pdas.inboxRateLimitAccount(recipientChain), - custody: config.custody, sessionAuthority: sessionAuthority, }) .instruction(); @@ -712,7 +714,7 @@ export class SolanaNtt ), shouldQueue: args.transferArgs.shouldQueue, }) - .accounts({ + .accountsStrict({ common: { payer: args.payer, config: { config: this.pdas.configAccount() }, @@ -720,6 +722,9 @@ export class SolanaNtt from: args.from, outboxItem: args.outboxItem, outboxRateLimit: this.pdas.outboxRateLimitAccount(), + custody: config.custody, + tokenProgram: config.tokenProgram, + systemProgram: SystemProgram.programId, }, peer: this.pdas.peerAccount(recipientChain), inboxRateLimit: this.pdas.inboxRateLimitAccount(recipientChain), @@ -727,6 +732,7 @@ export class SolanaNtt args.fromAuthority, args.transferArgs ), + tokenAuthority: this.pdas.tokenAuthority(), }) .instruction(); } @@ -757,6 +763,7 @@ export class SolanaNtt feeCollector: whAccs.wormholeFeeCollector, sequence: whAccs.wormholeSequence, program: this.core.address, + systemProgram: SystemProgram.programId, }, }) .instruction(); @@ -773,7 +780,7 @@ export class SolanaNtt const emitterChain = wormholeNTT.emitterChain; return await this.program.methods .receiveWormholeMessage() - .accounts({ + .accountsStrict({ payer: payer, config: { config: this.pdas.configAccount() }, peer: this.pdas.transceiverPeerAccount(emitterChain), @@ -785,6 +792,7 @@ export class SolanaNtt emitterChain, nttMessage.id ), + systemProgram: SystemProgram.programId, }) .instruction(); } @@ -805,7 +813,7 @@ export class SolanaNtt return await this.program.methods .redeem({}) - .accounts({ + .accountsStrict({ payer: payer, config: this.pdas.configAccount(), peer: nttManagerPeer, @@ -818,6 +826,7 @@ export class SolanaNtt inboxItem, inboxRateLimit, outboxRateLimit: this.pdas.outboxRateLimitAccount(), + systemProgram: SystemProgram.programId, }) .instruction(); } @@ -847,7 +856,7 @@ export class SolanaNtt .releaseInboundMint({ revertOnDelay: args.revertOnDelay, }) - .accounts({ + .accountsStrict({ common: { payer: args.payer, config: { config: this.pdas.configAccount() }, @@ -858,6 +867,8 @@ export class SolanaNtt ), mint: config.mint, tokenAuthority: this.pdas.tokenAuthority(), + custody: config.custody, + tokenProgram: config.tokenProgram, }, }) .instruction(); @@ -888,7 +899,7 @@ export class SolanaNtt .releaseInboundUnlock({ revertOnDelay: args.revertOnDelay, }) - .accounts({ + .accountsStrict({ common: { payer: args.payer, config: { config: this.pdas.configAccount() }, @@ -899,17 +910,37 @@ export class SolanaNtt ), mint: config.mint, tokenAuthority: this.pdas.tokenAuthority(), + custody: config.custody, + tokenProgram: config.tokenProgram, }, - custody: config.custody, }) .instruction(); } - async custodyAccountAddress(mint: PublicKey): Promise { - return associatedAddress({ - mint: mint, - owner: this.pdas.tokenAuthority(), - }); + /** + * Returns the address of the custody account. If the config is available + * (i.e. the program is initialised), the mint is derived from the config. + * Otherwise, the mint must be provided. + */ + async custodyAccountAddress( + configOrMint: NttBindings.Config | PublicKey, + tokenProgram = splToken.TOKEN_PROGRAM_ID + ): Promise { + if (configOrMint instanceof PublicKey) { + return splToken.getAssociatedTokenAddress( + configOrMint, + this.pdas.tokenAuthority(), + true, + tokenProgram + ); + } else { + return splToken.getAssociatedTokenAddress( + configOrMint.mint, + this.pdas.tokenAuthority(), + true, + configOrMint.tokenProgram + ); + } } createUnsignedTx( diff --git a/solana/idl/json/example_native_token_transfers.json b/solana/idl/json/example_native_token_transfers.json index 296557228..f3a0b2089 100644 --- a/solana/idl/json/example_native_token_transfers.json +++ b/solana/idl/json/example_native_token_transfers.json @@ -45,9 +45,8 @@ "isMut": true, "isSigner": false, "docs": [ - "The custody account that holds tokens in locking mode.", - "NOTE: the account is unconditionally initialized, but not used in", - "burning mode.", + "The custody account that holds tokens in locking mode and temporarily", + "holds tokens in burning mode.", "function if the token account has already been created." ] }, @@ -139,6 +138,16 @@ "isMut": true, "isSigner": false }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, { "name": "systemProgram", "isMut": false, @@ -159,6 +168,14 @@ { "name": "sessionAuthority", "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + }, + { + "name": "tokenAuthority", + "isMut": false, "isSigner": false } ], @@ -220,6 +237,16 @@ "isMut": true, "isSigner": false }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, { "name": "systemProgram", "isMut": false, @@ -240,12 +267,10 @@ { "name": "sessionAuthority", "isMut": false, - "isSigner": false - }, - { - "name": "custody", - "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] } ], "args": [ @@ -378,6 +403,11 @@ "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false } ] } @@ -436,13 +466,13 @@ "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false } ] - }, - { - "name": "custody", - "isMut": true, - "isSigner": false } ], "args": [ diff --git a/solana/idl/ts/example_native_token_transfers.ts b/solana/idl/ts/example_native_token_transfers.ts index fa85dfa73..e44664ead 100644 --- a/solana/idl/ts/example_native_token_transfers.ts +++ b/solana/idl/ts/example_native_token_transfers.ts @@ -45,9 +45,8 @@ export type ExampleNativeTokenTransfers = { "isMut": true, "isSigner": false, "docs": [ - "The custody account that holds tokens in locking mode.", - "NOTE: the account is unconditionally initialized, but not used in", - "burning mode.", + "The custody account that holds tokens in locking mode and temporarily", + "holds tokens in burning mode.", "function if the token account has already been created." ] }, @@ -139,6 +138,16 @@ export type ExampleNativeTokenTransfers = { "isMut": true, "isSigner": false }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, { "name": "systemProgram", "isMut": false, @@ -159,6 +168,14 @@ export type ExampleNativeTokenTransfers = { { "name": "sessionAuthority", "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + }, + { + "name": "tokenAuthority", + "isMut": false, "isSigner": false } ], @@ -220,6 +237,16 @@ export type ExampleNativeTokenTransfers = { "isMut": true, "isSigner": false }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, { "name": "systemProgram", "isMut": false, @@ -240,12 +267,10 @@ export type ExampleNativeTokenTransfers = { { "name": "sessionAuthority", "isMut": false, - "isSigner": false - }, - { - "name": "custody", - "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] } ], "args": [ @@ -378,6 +403,11 @@ export type ExampleNativeTokenTransfers = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false } ] } @@ -436,13 +466,13 @@ export type ExampleNativeTokenTransfers = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false } ] - }, - { - "name": "custody", - "isMut": true, - "isSigner": false } ], "args": [ @@ -1927,9 +1957,8 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": true, "isSigner": false, "docs": [ - "The custody account that holds tokens in locking mode.", - "NOTE: the account is unconditionally initialized, but not used in", - "burning mode.", + "The custody account that holds tokens in locking mode and temporarily", + "holds tokens in burning mode.", "function if the token account has already been created." ] }, @@ -2021,6 +2050,16 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": true, "isSigner": false }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, { "name": "systemProgram", "isMut": false, @@ -2041,6 +2080,14 @@ export const IDL: ExampleNativeTokenTransfers = { { "name": "sessionAuthority", "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + }, + { + "name": "tokenAuthority", + "isMut": false, "isSigner": false } ], @@ -2102,6 +2149,16 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": true, "isSigner": false }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, { "name": "systemProgram", "isMut": false, @@ -2122,12 +2179,10 @@ export const IDL: ExampleNativeTokenTransfers = { { "name": "sessionAuthority", "isMut": false, - "isSigner": false - }, - { - "name": "custody", - "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] } ], "args": [ @@ -2260,6 +2315,11 @@ export const IDL: ExampleNativeTokenTransfers = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false } ] } @@ -2318,13 +2378,13 @@ export const IDL: ExampleNativeTokenTransfers = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false } ] - }, - { - "name": "custody", - "isMut": true, - "isSigner": false } ], "args": [ diff --git a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs index 555dfd316..9ebb9cc8b 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -67,9 +67,8 @@ pub struct Initialize<'info> { associated_token::authority = token_authority, associated_token::token_program = token_program, )] - /// The custody account that holds tokens in locking mode. - /// NOTE: the account is unconditionally initialized, but not used in - /// burning mode. + /// The custody account that holds tokens in locking mode and temporarily + /// holds tokens in burning mode. /// CHECK: Use init_if_needed here to prevent a denial-of-service of the [`initialize`] /// function if the token account has already been created. pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, diff --git a/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs b/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs index e4fb201f8..dc5b6524e 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs @@ -41,6 +41,13 @@ pub struct ReleaseInbound<'info> { pub mint: InterfaceAccount<'info, token_interface::Mint>, pub token_program: Interface<'info, token_interface::TokenInterface>, + + /// CHECK: the token program checks if this indeed the right authority for the mint + #[account( + mut, + address = config.custody + )] + pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, } #[derive(AnchorDeserialize, AnchorSerialize)] @@ -52,6 +59,9 @@ pub struct ReleaseInboundArgs { #[derive(Accounts)] pub struct ReleaseInboundMint<'info> { + #[account( + constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, + )] common: ReleaseInbound<'info>, } @@ -62,8 +72,8 @@ pub struct ReleaseInboundMint<'info> { /// Setting this flag to `false` is useful when bundling this instruction /// together with [`crate::instructions::redeem`] in a transaction, so that the minting /// is attempted optimistically. -pub fn release_inbound_mint( - ctx: Context, +pub fn release_inbound_mint<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { let inbox_item = &mut ctx.accounts.common.inbox_item; @@ -79,38 +89,65 @@ pub fn release_inbound_mint( } assert!(inbox_item.release_status == ReleaseStatus::Released); - match ctx.accounts.common.config.mode { - Mode::Burning => token_interface::mint_to( - CpiContext::new_with_signer( - ctx.accounts.common.token_program.to_account_info(), - token_interface::MintTo { - mint: ctx.accounts.common.mint.to_account_info(), - to: ctx.accounts.common.recipient.to_account_info(), - authority: ctx.accounts.common.token_authority.clone(), - }, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - ), - inbox_item.amount, + + // NOTE: minting tokens is a two-step process: + // 1. Mint tokens to the custody account + // 2. Transfer the tokens from the custody account to the recipient + // + // This is done to ensure that if the token has a transfer hook defined, it + // will be called after the tokens are minted. + // Unfortunately the Token2022 program doesn't trigger transfer hooks when + // minting tokens, so we have to do it "manually" via a transfer. + // + // If we didn't do this, transfer hooks could be bypassed by transferring + // the tokens out through NTT first, then back in to the intended recipient. + // + // The [`transfer_burn`] function operates in a similar way + // (transfer to custody from sender, *then* burn). + + // Step 1: mint tokens to the custody account + token_interface::mint_to( + CpiContext::new_with_signer( + ctx.accounts.common.token_program.to_account_info(), + token_interface::MintTo { + mint: ctx.accounts.common.mint.to_account_info(), + to: ctx.accounts.common.custody.to_account_info(), + authority: ctx.accounts.common.token_authority.clone(), + }, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], ), - Mode::Locking => Err(NTTError::InvalidMode.into()), - } + inbox_item.amount, + )?; + + // Step 2: transfer the tokens from the custody account to the recipient + onchain::invoke_transfer_checked( + &ctx.accounts.common.token_program.key(), + ctx.accounts.common.custody.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.recipient.to_account_info(), + ctx.accounts.common.token_authority.clone(), + ctx.remaining_accounts, + inbox_item.amount, + ctx.accounts.common.mint.decimals, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + )?; + Ok(()) } // Lock/unlock #[derive(Accounts)] pub struct ReleaseInboundUnlock<'info> { - common: ReleaseInbound<'info>, - - /// CHECK: the token program checks if this indeed the right authority for the mint #[account( - mut, - address = common.config.custody + constraint = common.config.mode == Mode::Locking @ NTTError::InvalidMode, )] - pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, + common: ReleaseInbound<'info>, } /// Release an inbound transfer and unlock the tokens to the recipient. @@ -136,25 +173,19 @@ pub fn release_inbound_unlock<'info>( } } - assert!(inbox_item.release_status == ReleaseStatus::Released); - match ctx.accounts.common.config.mode { - Mode::Burning => Err(NTTError::InvalidMode.into()), - Mode::Locking => { - onchain::invoke_transfer_checked( - &ctx.accounts.common.token_program.key(), - ctx.accounts.custody.to_account_info(), - ctx.accounts.common.mint.to_account_info(), - ctx.accounts.common.recipient.to_account_info(), - ctx.accounts.common.token_authority.clone(), - ctx.remaining_accounts, - inbox_item.amount, - ctx.accounts.common.mint.decimals, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - Ok(()) - } - } + onchain::invoke_transfer_checked( + &ctx.accounts.common.token_program.key(), + ctx.accounts.common.custody.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.recipient.to_account_info(), + ctx.accounts.common.token_authority.clone(), + ctx.remaining_accounts, + inbox_item.amount, + ctx.accounts.common.mint.decimals, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + )?; + Ok(()) } diff --git a/solana/programs/example-native-token-transfers/src/instructions/transfer.rs b/solana/programs/example-native-token-transfers/src/instructions/transfer.rs index aab502e7f..802e0fc37 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/transfer.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/transfer.rs @@ -1,3 +1,17 @@ +//! This module implements the transfer instruction(s). +//! There are two types of transfers: burning and locking, depending on the +//! configuration of the NTT deployment. +//! +//! Since these two operations are very similar, we abstract out as much of the +//! common account structure as possible into the `Transfer` struct. +//! Due to some unfortunate limitations of Anchor, namely that it's impossible +//! to propagate instruction data to nested account structs, there is some +//! amount of duplication between `TransferBurn` and `TransferLock` (exactly the +//! accounts whose constraints refer to the instruction data). +//! +//! See the documentation of [`crate::SESSION_AUTHORITY_SEED`] for an +//! explanation of the approval flow. + #![allow(clippy::too_many_arguments)] use anchor_lang::prelude::*; use anchor_spl::token_interface; @@ -52,6 +66,15 @@ pub struct Transfer<'info> { #[account(mut)] pub outbox_rate_limit: Account<'info, OutboxRateLimit>, + #[account( + mut, + address = config.custody + )] + /// Tokens are always transferred to the custody account first regardless of + /// the mode. + /// For an explanation, see the note in [`transfer_burn`]. + pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, + pub system_program: Program<'info, System>, } @@ -85,6 +108,9 @@ impl TransferArgs { #[derive(Accounts)] #[instruction(args: TransferArgs)] pub struct TransferBurn<'info> { + #[account( + constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, + )] pub common: Transfer<'info>, #[account( @@ -110,16 +136,20 @@ pub struct TransferBurn<'info> { ], bump, )] + /// See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow. pub session_authority: AccountInfo<'info>, -} -pub fn transfer_burn(ctx: Context, args: TransferArgs) -> Result<()> { - require_eq!( - ctx.accounts.common.config.mode, - Mode::Burning, - NTTError::InvalidMode - ); + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + pub token_authority: AccountInfo<'info>, +} +pub fn transfer_burn<'info>( + ctx: Context<'_, '_, '_, 'info, TransferBurn<'info>>, + args: TransferArgs, +) -> Result<()> { let accs = ctx.accounts; let TransferArgs { mut amount, @@ -136,30 +166,67 @@ pub fn transfer_burn(ctx: Context, args: TransferArgs) -> Result<( ) .map_err(NTTError::from)?; - let before = accs.common.from.amount; + let before = accs.common.custody.amount; + + // NOTE: burning tokens is a two-step process: + // 1. Transfer the tokens to the custody account + // 2. Burn the tokens from the custody account + // + // This is done to ensure that if the token has a transfer hook defined, it + // will be called before the tokens are burned. + // Unfortunately the Token2022 program doesn't trigger transfer hooks when + // burning tokens, so we have to do it "manually" via a transfer. + // + // If we didn't do this, transfer hooks could be bypassed by transferring + // the tokens out through NTT first, then back in to the intended recipient. + // + // The [`release_inbound_mint`] function operates in a similar way + // (mint to custody, *then* transfer to recipient). + + // Step 1: transfer to custody account + onchain::invoke_transfer_checked( + &accs.common.token_program.key(), + accs.common.from.to_account_info(), + accs.common.mint.to_account_info(), + accs.common.custody.to_account_info(), + accs.session_authority.to_account_info(), + ctx.remaining_accounts, + amount, + accs.common.mint.decimals, + &[&[ + crate::SESSION_AUTHORITY_SEED, + accs.common.from.owner.as_ref(), + args.keccak256().as_ref(), + &[ctx.bumps.session_authority], + ]], + )?; + // Step 2: burn the tokens from the custody account token_interface::burn( CpiContext::new_with_signer( accs.common.token_program.to_account_info(), token_interface::Burn { mint: accs.common.mint.to_account_info(), - from: accs.common.from.to_account_info(), - authority: accs.session_authority.to_account_info(), + from: accs.common.custody.to_account_info(), + authority: accs.token_authority.to_account_info(), }, - &[&[ - crate::SESSION_AUTHORITY_SEED, - accs.common.from.owner.as_ref(), - args.keccak256().as_ref(), - &[ctx.bumps.session_authority], - ]], + &[&[crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.token_authority]]], ), amount, )?; - accs.common.from.reload()?; - let after = accs.common.from.amount; + accs.common.custody.reload()?; + let after = accs.common.custody.amount; - if after != before - amount { + // NOTE: we currently do not support tokens with fees. Support could be + // added, but it would require the client to calculate the amount _before_ + // paying fees that results in an amount that can safely be trimmed. + // Otherwise, if the amount after paying fees has dust, then that amount + // would be lost. + // To support fee tokens, we would first transfer the amount, _then_ assert + // that the resulting amount has no dust (instead of removing dust before + // the transfer like we do now). + if after != before { return Err(NTTError::BadAmountAfterBurn.into()); } @@ -174,7 +241,9 @@ pub fn transfer_burn(ctx: Context, args: TransferArgs) -> Result<( recipient_ntt_manager, recipient_address, should_queue, - ) + )?; + + Ok(()) } // Lock/unlock @@ -182,6 +251,9 @@ pub fn transfer_burn(ctx: Context, args: TransferArgs) -> Result<( #[derive(Accounts)] #[instruction(args: TransferArgs)] pub struct TransferLock<'info> { + #[account( + constraint = common.config.mode == Mode::Locking @ NTTError::InvalidMode, + )] pub common: Transfer<'info>, #[account( @@ -207,25 +279,14 @@ pub struct TransferLock<'info> { ], bump, )] + /// See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow. pub session_authority: AccountInfo<'info>, - - #[account( - mut, - address = common.config.custody - )] - pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, } pub fn transfer_lock<'info>( ctx: Context<'_, '_, '_, 'info, TransferLock<'info>>, args: TransferArgs, ) -> Result<()> { - require_eq!( - ctx.accounts.common.config.mode, - Mode::Locking, - NTTError::InvalidMode - ); - let accs = ctx.accounts; let TransferArgs { mut amount, @@ -242,13 +303,13 @@ pub fn transfer_lock<'info>( ) .map_err(NTTError::from)?; - let before = accs.custody.amount; + let before = accs.common.custody.amount; onchain::invoke_transfer_checked( &accs.common.token_program.key(), accs.common.from.to_account_info(), accs.common.mint.to_account_info(), - accs.custody.to_account_info(), + accs.common.custody.to_account_info(), accs.session_authority.to_account_info(), ctx.remaining_accounts, amount, @@ -261,8 +322,8 @@ pub fn transfer_lock<'info>( ]], )?; - accs.custody.reload()?; - let after = accs.custody.amount; + accs.common.custody.reload()?; + let after = accs.common.custody.amount; // NOTE: we currently do not support tokens with fees. Support could be // added, but it would require the client to calculate the amount _before_ diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 8a2704301..b5bb5fbe5 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -77,7 +77,10 @@ pub mod example_native_token_transfers { Ok(VERSION.to_string()) } - pub fn transfer_burn(ctx: Context, args: TransferArgs) -> Result<()> { + pub fn transfer_burn<'info>( + ctx: Context<'_, '_, '_, 'info, TransferBurn<'info>>, + args: TransferArgs, + ) -> Result<()> { instructions::transfer_burn(ctx, args) } @@ -92,8 +95,8 @@ pub mod example_native_token_transfers { instructions::redeem(ctx, args) } - pub fn release_inbound_mint( - ctx: Context, + pub fn release_inbound_mint<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { instructions::release_inbound_mint(ctx, args) diff --git a/solana/programs/example-native-token-transfers/tests/sdk/instructions/transfer.rs b/solana/programs/example-native-token-transfers/tests/sdk/instructions/transfer.rs index 9ab3255fc..810348e04 100644 --- a/solana/programs/example-native-token-transfers/tests/sdk/instructions/transfer.rs +++ b/solana/programs/example-native-token-transfers/tests/sdk/instructions/transfer.rs @@ -33,6 +33,7 @@ pub fn transfer_burn(ntt: &NTT, transfer: Transfer, args: TransferArgs) -> Instr inbox_rate_limit: ntt.inbox_rate_limit(chain_id), peer: transfer.peer, session_authority, + token_authority: ntt.token_authority(), }; Instruction { @@ -51,7 +52,6 @@ pub fn transfer_lock(ntt: &NTT, transfer: Transfer, args: TransferArgs) -> Instr common: common(ntt, &transfer), inbox_rate_limit: ntt.inbox_rate_limit(chain_id), peer: transfer.peer, - custody: ntt.custody(&transfer.mint), session_authority, }; Instruction { @@ -90,5 +90,6 @@ fn common(ntt: &NTT, transfer: &Transfer) -> example_native_token_transfers::acc outbox_item: transfer.outbox_item, outbox_rate_limit: ntt.outbox_rate_limit(), system_program: System::id(), + custody: ntt.custody(&transfer.mint), } } diff --git a/solana/tests/example-native-token-transfer.ts b/solana/tests/example-native-token-transfer.ts index 4221419e6..9d87a84b1 100644 --- a/solana/tests/example-native-token-transfer.ts +++ b/solana/tests/example-native-token-transfer.ts @@ -153,7 +153,7 @@ describe("example-native-token-transfers", () => { await sendAndConfirmTransaction(connection, transaction, [payer]); }); - describe("Locking", () => { + describe("Burning", () => { before(async () => { await spl.setAuthority( connection, @@ -173,7 +173,7 @@ describe("example-native-token-transfers", () => { chain: "solana", mint: mint.publicKey, outboundLimit: new BN(1000000), - mode: "locking", + mode: "burning", }); await ntt.registerTransceiver({ @@ -241,19 +241,6 @@ describe("example-native-token-transfers", () => { const balance = await connection.getTokenAccountBalance(tokenAccount); expect(balance.value.amount).to.equal("9900000"); - // grab logs - // await connection.confirmTransaction(redeemTx, 'confirmed'); - // const tx = await anchor.getProvider().connection.getParsedTransaction(redeemTx, { - // commitment: "confirmed", - // }); - // console.log(tx); - - // const log = tx.meta.logMessages[1]; - // const message = log.substring(log.indexOf(':') + 1); - // console.log(message); - - // TODO: assert other stuff in the message - // console.log(nttManagerMessage); expect((await counterValue()).toString()).to.be.eq("1") }); @@ -316,17 +303,4 @@ describe("example-native-token-transfers", () => { expect((await counterValue()).toString()).to.be.eq("2") }); }); - - // describe('Burning', () => { - // beforeEach(async () => { - // await ntt.initialize({ - // payer, - // owner, - // chain: 'solana', - // mint, - // outboundLimit: new BN(1000000), - // mode: 'burning' - // }) - // }); - // }); }); diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index b729c26b7..26b6299f2 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -11,7 +11,6 @@ import { } from './nttLayout' import { derivePostedVaaKey, getWormholeDerivedAccounts } from '@certusone/wormhole-sdk/lib/cjs/solana/wormhole' import { BN, translateError, type IdlAccounts, Program, AnchorProvider, Wallet, } from '@coral-xyz/anchor' -import { associatedAddress } from '@coral-xyz/anchor/dist/cjs/utils/token' import { getAssociatedTokenAddressSync } from '@solana/spl-token' import { PublicKey, Keypair, @@ -22,7 +21,9 @@ import { type Connection, SystemProgram, TransactionMessage, - VersionedTransaction + VersionedTransaction, + Commitment, + AccountMeta } from '@solana/web3.js' import { Keccak } from 'sha3' import { type ExampleNativeTokenTransfers as RawExampleNativeTokenTransfers } from '../../idl/ts/example_native_token_transfers' @@ -208,7 +209,7 @@ export class NTT { // little endian length prefix. const buffer = Buffer.from(txSimulation.value.returnData?.data[0], 'base64') const len = buffer.readUInt32LE(0) - return buffer.slice(4, len + 4).toString() + return buffer.subarray(4, len + 4).toString() } // Instructions @@ -354,22 +355,58 @@ export class NTT { shouldQueue: args.shouldQueue } - return await this.program.methods + const transferIx = await this.program.methods .transferBurn(transferArgs) - .accounts({ + .accountsStrict({ common: { payer: args.payer, config: { config: this.configAccountAddress() }, mint, from: args.from, + tokenProgram: await this.tokenProgram(config), outboxItem: args.outboxItem, - outboxRateLimit: this.outboxRateLimitAccountAddress() + outboxRateLimit: this.outboxRateLimitAccountAddress(), + custody: await this.custodyAccountAddress(config), + systemProgram: SystemProgram.programId, }, peer: this.peerAccountAddress(args.recipientChain), inboxRateLimit: this.inboxRateLimitAccountAddress(args.recipientChain), - sessionAuthority: this.sessionAuthorityAddress(args.fromAuthority, transferArgs) + sessionAuthority: this.sessionAuthorityAddress(args.fromAuthority, transferArgs), + tokenAuthority: this.tokenAuthorityAddress() }) .instruction() + + const mintInfo = await splToken.getMint( + this.program.provider.connection, + config.mint, + undefined, + config.tokenProgram + ) + const transferHook = splToken.getTransferHook(mintInfo) + + if (transferHook) { + const source = args.from + const mint = config.mint + const destination = await this.custodyAccountAddress(config) + const owner = this.sessionAuthorityAddress(args.fromAuthority, transferArgs) + await addExtraAccountMetasForExecute( + this.program.provider.connection, + transferIx, + transferHook.programId, + source, + mint, + destination, + owner, + // TODO(csongor): compute the amount that's passed into transfer. + // Leaving this 0 is fine unless the transfer hook accounts addresses + // depend on the amount (which is unlikely). + // If this turns out to be the case, the amount to put here is the + // untrimmed amount after removing dust. + 0, + ); + } + + return transferIx } /** @@ -413,11 +450,11 @@ export class NTT { from: args.from, tokenProgram: await this.tokenProgram(config), outboxItem: args.outboxItem, - outboxRateLimit: this.outboxRateLimitAccountAddress() + outboxRateLimit: this.outboxRateLimitAccountAddress(), + custody: await this.custodyAccountAddress(config) }, peer: this.peerAccountAddress(args.recipientChain), inboxRateLimit: this.inboxRateLimitAccountAddress(args.recipientChain), - custody: await this.custodyAccountAddress(config), sessionAuthority: this.sessionAuthorityAddress(args.fromAuthority, transferArgs) }) .instruction() @@ -530,7 +567,7 @@ export class NTT { const mint = await this.mintAccountAddress(config) - return await this.program.methods + const transferIx = await this.program.methods .releaseInboundMint({ revertOnDelay: args.revertOnDelay }) @@ -542,10 +579,38 @@ export class NTT { recipient: getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram), mint, tokenAuthority: this.tokenAuthorityAddress(), - tokenProgram: config.tokenProgram + tokenProgram: config.tokenProgram, + custody: await this.custodyAccountAddress(config) } }) .instruction() + + const mintInfo = await splToken.getMint(this.program.provider.connection, config.mint, undefined, config.tokenProgram) + const transferHook = splToken.getTransferHook(mintInfo) + + if (transferHook) { + const source = await this.custodyAccountAddress(config) + const mint = config.mint + const destination = getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram) + const owner = this.tokenAuthorityAddress() + await addExtraAccountMetasForExecute( + this.program.provider.connection, + transferIx, + transferHook.programId, + source, + mint, + destination, + owner, + // TODO(csongor): compute the amount that's passed into transfer. + // Leaving this 0 is fine unless the transfer hook accounts addresses + // depend on the amount (which is unlikely). + // If this turns out to be the case, the amount to put here is the + // untrimmed amount after removing dust. + 0, + ); + } + + return transferIx } async releaseInboundMint(args: { @@ -602,9 +667,9 @@ export class NTT { recipient: getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram), mint, tokenAuthority: this.tokenAuthorityAddress(), - tokenProgram: config.tokenProgram + tokenProgram: config.tokenProgram, + custody: await this.custodyAccountAddress(config) }, - custody: await this.custodyAccountAddress(config) }) .instruction() From d0a6055e7973b56277a29e48d5b7eea3d5ce04da Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Fri, 19 Apr 2024 14:14:13 +0100 Subject: [PATCH 6/8] solana: populate an address lookup table to reduce tx size --- sdk/__tests__/utils.ts | 9 + sdk/solana/src/ntt.ts | 138 ++++++++- sdk/solana/src/utils.ts | 4 + solana/Cargo.lock | 1 + solana/Cargo.toml | 1 + .../json/example_native_token_transfers.json | 132 +++++++++ .../idl/ts/example_native_token_transfers.ts | 264 ++++++++++++++++++ .../example-native-token-transfers/Cargo.toml | 1 + .../src/instructions/luts.rs | 172 ++++++++++++ .../src/instructions/mod.rs | 2 + .../example-native-token-transfers/src/lib.rs | 4 + solana/tests/example-native-token-transfer.ts | 9 + solana/ts/sdk/ntt.ts | 160 ++++++++++- 13 files changed, 880 insertions(+), 17 deletions(-) create mode 100644 solana/programs/example-native-token-transfers/src/instructions/luts.rs diff --git a/sdk/__tests__/utils.ts b/sdk/__tests__/utils.ts index 36e60f1bf..225df1645 100644 --- a/sdk/__tests__/utils.ts +++ b/sdk/__tests__/utils.ts @@ -558,6 +558,15 @@ async function deploySolana(ctx: Ctx): Promise { await signSendWait(ctx.context, initTxs, signer); console.log("Initialized ntt at", manager.program.programId.toString()); + // NOTE: this is a hack. The next instruction will fail if we don't wait + // here, because the address lookup table is not yet available, despite + // the transaction having been confirmed. + // Looks like a bug, but I haven't investigated further. In practice, this + // won't be an issue, becase the address lookup table will have been + // created well before anyone is trying to use it, but we might want to be + // mindful in the deploy script too. + await new Promise((resolve) => setTimeout(resolve, 400)); + const registrTxs = manager.registerTransceiver({ payer: keypair, owner: keypair, diff --git a/sdk/solana/src/ntt.ts b/sdk/solana/src/ntt.ts index 6b3e3f60c..ff478826b 100644 --- a/sdk/solana/src/ntt.ts +++ b/sdk/solana/src/ntt.ts @@ -1,10 +1,12 @@ -import { Program } from "@coral-xyz/anchor"; +import { Program, web3 } from "@coral-xyz/anchor"; import * as splToken from "@solana/spl-token"; import { createAssociatedTokenAccountInstruction, getAssociatedTokenAddressSync, } from "@solana/spl-token"; import { + AddressLookupTableAccount, + AddressLookupTableProgram, Connection, Keypair, PublicKey, @@ -71,6 +73,7 @@ export class SolanaNtt program: Program; config?: NttBindings.Config; quoter?: NttQuoter; + addressLookupTable?: AddressLookupTableAccount; constructor( readonly network: N, @@ -263,6 +266,100 @@ export class SolanaNtt { transaction: tx, signers: [] }, "Ntt.Initialize" ); + + yield* this.initializeOrUpdateLUT({ payer: args.payer }); + } + + // This function should be called after each upgrade. If there's nothing to + // do, it won't actually submit a transaction, so it's cheap to call. + async *initializeOrUpdateLUT(args: { payer: Keypair }) { + // TODO: find a more robust way of fetching a recent slot + const slot = (await this.program.provider.connection.getSlot()) - 1; + + const [_, lutAddress] = web3.AddressLookupTableProgram.createLookupTable({ + authority: this.pdas.lutAuthority(), + payer: args.payer.publicKey, + recentSlot: slot, + }); + + const whAccs = utils.getWormholeDerivedAccounts( + this.program.programId, + this.core.address + ); + const config = await this.getConfig(); + + const entries = { + config: this.pdas.configAccount(), + custody: config.custody, + tokenProgram: config.tokenProgram, + mint: config.mint, + tokenAuthority: this.pdas.tokenAuthority(), + outboxRateLimit: this.pdas.outboxRateLimitAccount(), + wormhole: { + bridge: whAccs.wormholeBridge, + feeCollector: whAccs.wormholeFeeCollector, + sequence: whAccs.wormholeSequence, + program: this.core.address, + systemProgram: SystemProgram.programId, + clock: web3.SYSVAR_CLOCK_PUBKEY, + rent: web3.SYSVAR_RENT_PUBKEY, + }, + }; + + // collect all pubkeys in entries recursively + const collectPubkeys = (obj: any): Array => { + const pubkeys = new Array(); + for (const key in obj) { + const value = obj[key]; + if (value instanceof PublicKey) { + pubkeys.push(value); + } else if (typeof value === "object") { + pubkeys.push(...collectPubkeys(value)); + } + } + return pubkeys; + }; + const pubkeys = collectPubkeys(entries).map((pk) => pk.toBase58()); + + var existingLut: web3.AddressLookupTableAccount | null = null; + try { + existingLut = await this.getAddressLookupTable(false); + } catch { + // swallow errors here, it just means that lut doesn't exist + } + + if (existingLut !== null) { + const existingPubkeys = + existingLut.state.addresses?.map((a) => a.toBase58()) ?? []; + + // if pubkeys contains keys that are not in the existing LUT, we need to + // add them to the LUT + const missingPubkeys = pubkeys.filter( + (pk) => !existingPubkeys.includes(pk) + ); + + if (missingPubkeys.length === 0) { + return existingLut; + } + } + + const ix = await this.program.methods + .initializeLut(new BN(slot)) + .accountsStrict({ + payer: args.payer.publicKey, + authority: this.pdas.lutAuthority(), + lutAddress, + lut: this.pdas.lutAccount(), + lutProgram: AddressLookupTableProgram.programId, + systemProgram: SystemProgram.programId, + entries, + }) + .instruction(); + + const tx = new Transaction().add(ix); + tx.feePayer = args.payer.publicKey; + + yield this.createUnsignedTx({ transaction: tx }, "Ntt.InitializeLUT"); } async *registerTransceiver(args: { @@ -294,7 +391,7 @@ export class SolanaNtt ); const broadcastIx = await this.program.methods .broadcastWormholeId() - .accounts({ + .accountsStrict({ payer: args.payer.publicKey, config: this.pdas.configAccount(), mint: config.mint, @@ -306,6 +403,8 @@ export class SolanaNtt sequence: whAccs.wormholeSequence, program: this.core.address, systemProgram: SystemProgram.programId, + clock: web3.SYSVAR_CLOCK_PUBKEY, + rent: web3.SYSVAR_RENT_PUBKEY, }, }) .instruction(); @@ -346,7 +445,7 @@ export class SolanaNtt .instruction(), this.program.methods .broadcastWormholePeer({ chainId: toChainId(peer.chain) }) - .accounts({ + .accountsStrict({ payer: sender, config: this.pdas.configAccount(), peer: this.pdas.transceiverPeerAccount(peer.chain), @@ -357,6 +456,9 @@ export class SolanaNtt feeCollector: whAccs.wormholeFeeCollector, sequence: whAccs.wormholeSequence, program: this.core.address, + clock: web3.SYSVAR_CLOCK_PUBKEY, + rent: web3.SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, }, }) .instruction(), @@ -751,7 +853,7 @@ export class SolanaNtt .releaseWormholeOutbound({ revertOnDelay: args.revertOnDelay, }) - .accounts({ + .accountsStrict({ payer: args.payer, config: { config: this.pdas.configAccount() }, outboxItem: args.outboxItem, @@ -764,6 +866,8 @@ export class SolanaNtt sequence: whAccs.wormholeSequence, program: this.core.address, systemProgram: SystemProgram.programId, + clock: web3.SYSVAR_CLOCK_PUBKEY, + rent: web3.SYSVAR_RENT_PUBKEY, }, }) .instruction(); @@ -943,6 +1047,32 @@ export class SolanaNtt } } + async getAddressLookupTable( + useCache = true + ): Promise { + if (!useCache || !this.addressLookupTable) { + const lut = await this.program.account.lut.fetchNullable( + this.pdas.lutAccount() + ); + if (!lut) + throw new Error( + "Address lookup table not found. Did you forget to call initializeLUT?" + ); + + const response = await this.connection.getAddressLookupTable(lut.address); + if (response.value === null) throw new Error("Could not fetch LUT"); + + this.addressLookupTable = response.value; + } + + if (!this.addressLookupTable) + throw new Error( + "Address lookup table not found. Did you forget to call initializeLUT?" + ); + + return this.addressLookupTable; + } + createUnsignedTx( txReq: SolanaTransaction, description: string, diff --git a/sdk/solana/src/utils.ts b/sdk/solana/src/utils.ts index 7ea75e7e8..0e659e6bc 100644 --- a/sdk/solana/src/utils.ts +++ b/sdk/solana/src/utils.ts @@ -112,6 +112,8 @@ export const nttAddresses = (programId: PublicKeyInitData) => { 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 = (sender: PublicKey, args: TransferArgs): PublicKey => derivePda( [ @@ -142,6 +144,8 @@ export const nttAddresses = (programId: PublicKeyInitData) => { transceiverPeerAccount, transceiverMessageAccount, registeredTransceiver, + lutAccount, + lutAuthority, }; }; diff --git a/solana/Cargo.lock b/solana/Cargo.lock index 993cceb4a..541661c36 100644 --- a/solana/Cargo.lock +++ b/solana/Cargo.lock @@ -1568,6 +1568,7 @@ dependencies = [ "serde_json", "serde_wormhole", "sha3 0.10.8", + "solana-address-lookup-table-program", "solana-program", "solana-program-runtime", "solana-program-test", diff --git a/solana/Cargo.toml b/solana/Cargo.toml index 49269f5c1..5eb52e53b 100644 --- a/solana/Cargo.toml +++ b/solana/Cargo.toml @@ -47,6 +47,7 @@ anchor-spl = "0.29.0" solana-program = "=1.18.10" solana-program-runtime = "=1.18.10" solana-program-test = "=1.18.10" +solana-address-lookup-table-program = "=1.18.10" spl-token = "4.0.0" spl-token-2022 = "3.0.2" diff --git a/solana/idl/json/example_native_token_transfers.json b/solana/idl/json/example_native_token_transfers.json index f3a0b2089..0feeff52c 100644 --- a/solana/idl/json/example_native_token_transfers.json +++ b/solana/idl/json/example_native_token_transfers.json @@ -83,6 +83,122 @@ } ] }, + { + "name": "initializeLut", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "lutAddress", + "isMut": true, + "isSigner": false + }, + { + "name": "lut", + "isMut": true, + "isSigner": false + }, + { + "name": "lutProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "entries", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ] + } + ], + "args": [ + { + "name": "recentSlot", + "type": "u64" + } + ] + }, { "name": "version", "accounts": [], @@ -1129,6 +1245,22 @@ ] } }, + { + "name": "LUT", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": "publicKey" + } + ] + } + }, { "name": "NttManagerPeer", "docs": [ diff --git a/solana/idl/ts/example_native_token_transfers.ts b/solana/idl/ts/example_native_token_transfers.ts index e44664ead..a8ea1f346 100644 --- a/solana/idl/ts/example_native_token_transfers.ts +++ b/solana/idl/ts/example_native_token_transfers.ts @@ -83,6 +83,122 @@ export type ExampleNativeTokenTransfers = { } ] }, + { + "name": "initializeLut", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "lutAddress", + "isMut": true, + "isSigner": false + }, + { + "name": "lut", + "isMut": true, + "isSigner": false + }, + { + "name": "lutProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "entries", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ] + } + ], + "args": [ + { + "name": "recentSlot", + "type": "u64" + } + ] + }, { "name": "version", "accounts": [], @@ -1129,6 +1245,22 @@ export type ExampleNativeTokenTransfers = { ] } }, + { + "name": "lut", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": "publicKey" + } + ] + } + }, { "name": "validatedTransceiverMessage", "generics": [ @@ -1995,6 +2127,122 @@ export const IDL: ExampleNativeTokenTransfers = { } ] }, + { + "name": "initializeLut", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "lutAddress", + "isMut": true, + "isSigner": false + }, + { + "name": "lut", + "isMut": true, + "isSigner": false + }, + { + "name": "lutProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "entries", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ] + } + ], + "args": [ + { + "name": "recentSlot", + "type": "u64" + } + ] + }, { "name": "version", "accounts": [], @@ -3041,6 +3289,22 @@ export const IDL: ExampleNativeTokenTransfers = { ] } }, + { + "name": "lut", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": "publicKey" + } + ] + } + }, { "name": "validatedTransceiverMessage", "generics": [ diff --git a/solana/programs/example-native-token-transfers/Cargo.toml b/solana/programs/example-native-token-transfers/Cargo.toml index bc064e81d..9a131c741 100644 --- a/solana/programs/example-native-token-transfers/Cargo.toml +++ b/solana/programs/example-native-token-transfers/Cargo.toml @@ -38,6 +38,7 @@ bitmaps = "3.2.1" hex.workspace = true cfg-if.workspace = true solana-program.workspace = true +solana-address-lookup-table-program.workspace = true spl-token-2022 = { workspace = true, features = ["no-entrypoint"] } wormhole-anchor-sdk.workspace = true wormhole-io.workspace = true diff --git a/solana/programs/example-native-token-transfers/src/instructions/luts.rs b/solana/programs/example-native-token-transfers/src/instructions/luts.rs new file mode 100644 index 000000000..c9a79b527 --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/luts.rs @@ -0,0 +1,172 @@ +//! This instructions manages a canonical address lookup table (or LUT) for the +//! NTT program. +//! LUTs in general can be created permissionlessly, so support from the +//! program's side is not strictly necessary. When submitting a transaction, the +//! client could just manage its own ad-hoc lookup table. +//! Nevertheless, we provide this instruction to make it easier for the client +//! to query the lookup table from a deterministic address, and for integrators +//! to be able to fetch the accounts from the LUT in a standardised way. +//! +//! This way, the client sdk can abstract away the lookup table logic in a +//! maintanable way. +//! +//! The [`initialize_lut`] instruction can be called multiple times, each time +//! it will create a new lookup table, with the accounts defined in the +//! [`Entries`] struct. +//! An alternative would be to keep extending the existing lookup table, but +//! ensuring the instruction is idempotent (which requires ensuring no duplicate +//! entries) has O(n^2) complexity (since LUTs are append only, we can't keep it +//! sorted), and in the worst case would require ~16k checks. So we keep things +//! simple, and just create a new LUT each time. This operation won't be called +//! often, so the extra allocation is justifiable. +//! +//! Because of all the above, this instruction can be called permissionlessly. + +use anchor_lang::prelude::*; +use solana_address_lookup_table_program; +use solana_program::program::{invoke, invoke_signed}; + +use crate::{config::Config, queue::outbox::OutboxRateLimit, transceivers::wormhole::accounts::*}; + +#[account] +#[derive(InitSpace)] +pub struct LUT { + pub bump: u8, + pub address: Pubkey, +} + +#[derive(Accounts)] +#[instruction(recent_slot: u64)] +pub struct InitializeLUT<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + #[account( + seeds = [b"lut_authority"], + bump + )] + pub authority: AccountInfo<'info>, + + #[account( + mut, + seeds = [authority.key().as_ref(), &recent_slot.to_le_bytes()], + seeds::program = solana_address_lookup_table_program::id(), + bump + )] + pub lut_address: AccountInfo<'info>, + + #[account( + init_if_needed, + payer = payer, + space = 8 + LUT::INIT_SPACE, + seeds = [b"lut"], + bump + )] + pub lut: Account<'info, LUT>, + + /// CHECK: address lookup table program (checked by instruction) + #[account(executable)] + pub lut_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, + + /// These are the entries that will populate the LUT. + pub entries: Entries<'info>, +} + +#[derive(Accounts)] +pub struct Entries<'info> { + pub config: Account<'info, Config>, + + #[account( + constraint = custody.key() == config.custody, + )] + pub custody: AccountInfo<'info>, + + #[account( + constraint = token_program.key() == config.token_program, + )] + pub token_program: AccountInfo<'info>, + + #[account( + constraint = mint.key() == config.mint, + )] + pub mint: AccountInfo<'info>, + + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + pub token_authority: AccountInfo<'info>, + + pub outbox_rate_limit: Account<'info, OutboxRateLimit>, + + // NOTE: this includes the system program so we don't need to add it in the outer context + pub wormhole: WormholeAccounts<'info>, +} + +pub fn initialize_lut(ctx: Context, recent_slot: u64) -> Result<()> { + let (ix, lut_address) = solana_address_lookup_table_program::instruction::create_lookup_table( + ctx.accounts.authority.key(), + ctx.accounts.payer.key(), + recent_slot, + ); + + // just a sanity check, should never be hit, so we don't provide a custom + // error message + assert_eq!(lut_address, ctx.accounts.lut_address.key()); + + // the LUT might already exist, in which case the new one will simply + // override it. Since we don't delete the old LUTs, this is safe -- clients + // holding references to old LUTs will still be able to use them. + ctx.accounts.lut.set_inner(LUT { + bump: ctx.bumps.lut, + address: lut_address, + }); + + // NOTE: LUTs can be permissionlessly created (i.e. the authority does + // not need to sign the transaction). This means that the LUT might + // already exist (if someone frontran us). However, it's not a problem: + // AddressLookupTable::create_lookup_table checks if the LUT already + // exists and does nothing if it does. + // + // LUTs can only be created permissionlessly, but only the authority is + // authorised to actually populate the fields, so we don't have to worry + // about the frontrunner populating it with junk. The only risk of that would + // be the LUT being filled to capacity (256 addresses), with no + // possibility for us to add our own accounts -- no other security impact. + invoke( + &ix, + &[ + ctx.accounts.lut_address.to_account_info(), + ctx.accounts.authority.to_account_info(), + ctx.accounts.payer.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ], + )?; + + let entries_infos = ctx.accounts.entries.to_account_infos(); + let mut entries = Vec::with_capacity(1 + entries_infos.len()); + entries.push(crate::ID); + entries.extend(entries_infos.into_iter().map(|x| x.key)); + + let ix = solana_address_lookup_table_program::instruction::extend_lookup_table( + ctx.accounts.lut_address.key(), + ctx.accounts.authority.key(), + Some(ctx.accounts.payer.key()), + entries, + ); + + invoke_signed( + &ix, + &[ + ctx.accounts.lut_address.to_account_info(), + ctx.accounts.authority.to_account_info(), + ctx.accounts.payer.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ], + &[&[b"lut_authority", &[ctx.bumps.authority]]], + )?; + + Ok(()) +} 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 a716c567e..ed009b00c 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/mod.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/mod.rs @@ -1,11 +1,13 @@ pub mod admin; pub mod initialize; +pub mod luts; pub mod redeem; pub mod release_inbound; pub mod transfer; pub use admin::*; pub use initialize::*; +pub use luts::*; 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 b5bb5fbe5..6521acf1e 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -73,6 +73,10 @@ pub mod example_native_token_transfers { instructions::initialize(ctx, args) } + pub fn initialize_lut(ctx: Context, recent_slot: u64) -> Result<()> { + instructions::initialize_lut(ctx, recent_slot) + } + pub fn version(_ctx: Context) -> Result { Ok(VERSION.to_string()) } diff --git a/solana/tests/example-native-token-transfer.ts b/solana/tests/example-native-token-transfer.ts index 9d87a84b1..4431a5af3 100644 --- a/solana/tests/example-native-token-transfer.ts +++ b/solana/tests/example-native-token-transfer.ts @@ -176,6 +176,15 @@ describe("example-native-token-transfers", () => { mode: "burning", }); + // NOTE: this is a hack. The next instruction will fail if we don't wait + // here, because the address lookup table is not yet available, despite + // the transaction having been confirmed. + // Looks like a bug, but I haven't investigated further. In practice, this + // won't be an issue, becase the address lookup table will have been + // created well before anyone is trying to use it, but we might want to be + // mindful in the deploy script too. + await new Promise((resolve) => setTimeout(resolve, 200)); + await ntt.registerTransceiver({ payer, owner: payer, diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 26b6299f2..741815696 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -10,7 +10,7 @@ import { nativeTokenTransferLayout } from './nttLayout' import { derivePostedVaaKey, getWormholeDerivedAccounts } from '@certusone/wormhole-sdk/lib/cjs/solana/wormhole' -import { BN, translateError, type IdlAccounts, Program, AnchorProvider, Wallet, } from '@coral-xyz/anchor' +import { BN, translateError, type IdlAccounts, Program, web3, } from '@coral-xyz/anchor' import { getAssociatedTokenAddressSync } from '@solana/spl-token' import { PublicKey, Keypair, @@ -23,7 +23,9 @@ import { TransactionMessage, VersionedTransaction, Commitment, - AccountMeta + AccountMeta, + AddressLookupTableProgram, + AddressLookupTableAccount } from '@solana/web3.js' import { Keccak } from 'sha3' import { type ExampleNativeTokenTransfers as RawExampleNativeTokenTransfers } from '../../idl/ts/example_native_token_transfers' @@ -78,6 +80,7 @@ export class NTT { readonly wormholeId: PublicKey // mapping from error code to error message. Used for prettifying error messages private readonly errors: Map + addressLookupTable: web3.AddressLookupTableAccount | null = null constructor(connection: Connection, args: { nttId: NttProgramId, wormholeId: WormholeProgramId }) { // TODO: initialise a new Program here with a passed in Connection @@ -107,6 +110,14 @@ export class NTT { return this.derivePda('config') } + lutAccountAddress(): PublicKey { + return this.derivePda('lut') + } + + lutAuthorityAddress(): PublicKey { + return this.derivePda('lut_authority') + } + outboxRateLimitAccountAddress(): PublicKey { return this.derivePda('outbox_rate_limit') } @@ -221,7 +232,7 @@ export class NTT { mint: PublicKey outboundLimit: BN mode: 'burning' | 'locking' - }) { + }): Promise { const mode: any = args.mode === 'burning' ? { burning: {} } @@ -248,7 +259,97 @@ export class NTT { associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, }).instruction(); - return sendAndConfirmTransaction(this.program.provider.connection, new Transaction().add(ix), [args.payer, args.owner]); + await this.sendAndConfirmTransaction(new Transaction().add(ix), [args.payer, args.owner], false); + await this.initializeOrUpdateLUT({ payer: args.payer }) + } + + // This function should be called after each upgrade. If there's nothing to + // do, it won't actually submit a transaction, so it's cheap to call. + async initializeOrUpdateLUT(args: { + payer: Keypair + }): Promise { + // TODO: find a more robust way of fetching a recent slot + const slot = await this.program.provider.connection.getSlot() - 1 + + const [_, lutAddress] = web3.AddressLookupTableProgram.createLookupTable({ + authority: this.lutAuthorityAddress(), + payer: args.payer.publicKey, + recentSlot: slot, + }); + + const whAccs = getWormholeDerivedAccounts(this.program.programId, this.wormholeId) + const config = await this.getConfig() + + const entries = { + config: this.configAccountAddress(), + custody: await this.custodyAccountAddress(config), + tokenProgram: await this.tokenProgram(config), + mint: await this.mintAccountAddress(config), + tokenAuthority: this.tokenAuthorityAddress(), + outboxRateLimit: this.outboxRateLimitAccountAddress(), + wormhole: { + bridge: whAccs.wormholeBridge, + feeCollector: whAccs.wormholeFeeCollector, + sequence: whAccs.wormholeSequence, + program: this.wormholeId, + systemProgram: SystemProgram.programId, + clock: web3.SYSVAR_CLOCK_PUBKEY, + rent: web3.SYSVAR_RENT_PUBKEY, + } + }; + + // collect all pubkeys in entries recursively + const collectPubkeys = (obj: any): Array => { + const pubkeys = new Array() + for (const key in obj) { + const value = obj[key] + if (value instanceof PublicKey) { + pubkeys.push(value) + } else if (typeof value === 'object') { + pubkeys.push(...collectPubkeys(value, pubkeys)) + } + } + return pubkeys + } + const pubkeys = collectPubkeys(entries).map(pk => pk.toBase58()) + + var existingLut: web3.AddressLookupTableAccount | null = null + try { + existingLut = await this.getAddressLookupTable(false) + } catch { + // swallow errors here, it just means that lut doesn't exist + } + + if (existingLut !== null) { + const existingPubkeys = existingLut.state.addresses?.map(a => a.toBase58()) ?? [] + + // if pubkeys contains keys that are not in the existing LUT, we need to + // add them to the LUT + const missingPubkeys = pubkeys.filter(pk => !existingPubkeys.includes(pk)) + + if (missingPubkeys.length === 0) { + return existingLut + } + } + + const ix = await this.program.methods + .initializeLut(new BN(slot)) + .accountsStrict({ + payer: args.payer.publicKey, + authority: this.lutAuthorityAddress(), + lutAddress, + lut: this.lutAccountAddress(), + lutProgram: AddressLookupTableProgram.programId, + systemProgram: SystemProgram.programId, + entries + }).instruction(); + + const signers = [args.payer] + await this.sendAndConfirmTransaction(new Transaction().add(ix), signers, false); + + // NOTE: explicitly invalidate the cache. This is the only operation that + // modifies the LUT, so this is the only place we need to invalide. + return this.getAddressLookupTable(false) } async transfer(args: { @@ -316,9 +417,27 @@ export class NTT { /** * Like `sendAndConfirmTransaction` but parses the anchor error code. */ - private async sendAndConfirmTransaction(tx: Transaction, signers: Keypair[]): Promise { + private async sendAndConfirmTransaction(tx: Transaction, signers: Keypair[], useLut = true): Promise { + const blockhash = await this.program.provider.connection.getLatestBlockhash() + const luts: AddressLookupTableAccount[] = [] + if (useLut) { + luts.push(await this.getAddressLookupTable()) + } + try { - return await sendAndConfirmTransaction(this.program.provider.connection, tx, signers) + const messageV0 = new TransactionMessage({ + payerKey: signers[0].publicKey, + recentBlockhash: blockhash.blockhash, + instructions: tx.instructions, + }).compileToV0Message(luts) + + const transactionV0 = new VersionedTransaction(messageV0) + transactionV0.sign(signers) + + // The types for this function are wrong -- the type says it doesn't + // support version transactions, but it does 🤫 + // @ts-ignore + return await sendAndConfirmTransaction(this.program.provider.connection, transactionV0) } catch (err) { throw translateError(err, this.errors) } @@ -543,7 +662,7 @@ export class NTT { tx.add(await this.createReleaseOutboundInstruction(txArgs)) const signers = [args.payer] - return await sendAndConfirmTransaction(this.program.provider.connection, tx, signers) + return await this.sendAndConfirmTransaction(tx, signers) } // TODO: document that if recipient is provided, then the instruction can be @@ -746,7 +865,7 @@ export class NTT { peer: this.peerAccountAddress(args.chain), inboxRateLimit: this.inboxRateLimitAccountAddress(args.chain) }).instruction() - return await sendAndConfirmTransaction(this.program.provider.connection, new Transaction().add(ix), [args.payer, args.owner]) + return await this.sendAndConfirmTransaction(new Transaction().add(ix), [args.payer, args.owner]) } async setWormholeTransceiverPeer(args: { @@ -783,7 +902,7 @@ export class NTT { program: this.wormholeId } }).instruction() - return await sendAndConfirmTransaction(this.program.provider.connection, new Transaction().add(ix, broadcastIx), [args.payer, args.owner, wormholeMessage]) + return await this.sendAndConfirmTransaction(new Transaction().add(ix, broadcastIx), [args.payer, args.owner, wormholeMessage]) } async registerTransceiver(args: { @@ -816,8 +935,8 @@ export class NTT { program: this.wormholeId } }).instruction() - return await sendAndConfirmTransaction( - this.program.provider.connection, new Transaction().add(ix, broadcastIx), [args.payer, args.owner, wormholeMessage]) + return await this.sendAndConfirmTransaction( + new Transaction().add(ix, broadcastIx), [args.payer, args.owner, wormholeMessage]) } async setOutboundLimit(args: { @@ -833,7 +952,7 @@ export class NTT { config: this.configAccountAddress(), rateLimit: this.outboxRateLimitAccountAddress(), }).instruction(); - return sendAndConfirmTransaction(this.program.provider.connection, new Transaction().add(ix), [args.owner]); + return this.sendAndConfirmTransaction(new Transaction().add(ix), [args.owner]); } async setInboundLimit(args: { @@ -850,7 +969,7 @@ export class NTT { config: this.configAccountAddress(), rateLimit: this.inboxRateLimitAccountAddress(args.chain), }).instruction(); - return sendAndConfirmTransaction(this.program.provider.connection, new Transaction().add(ix), [args.owner]); + return this.sendAndConfirmTransaction(new Transaction().add(ix), [args.owner]); } async createReceiveWormholeMessageInstruction(args: { @@ -1018,6 +1137,21 @@ export class NTT { return await this.program.account.inboxItem.fetch(this.inboxItemAccountAddress(chain, nttMessage)) } + async getAddressLookupTable(useCache = true): Promise { + if (!useCache || !this.addressLookupTable) { + const lut = await this.program.account.lut.fetchNullable(this.lutAccountAddress()) + if (!lut) { + throw new Error('Address lookup table not found. Did you forget to call initializeLUT?') + } + const response = await this.program.provider.connection.getAddressLookupTable(lut.address) + this.addressLookupTable = response.value + } + if (!this.addressLookupTable) { + throw new Error('Address lookup table not found. Did you forget to call initializeLUT?') + } + return this.addressLookupTable + } + /** * Returns the address of the custody account. If the config is available * (i.e. the program is initialised), the mint is derived from the config. From 5fb81e38c9482cf7ab297db9a8c49eb847c5b187 Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Mon, 29 Apr 2024 17:04:06 +0100 Subject: [PATCH 7/8] solana: bump version to 2.0.0 add script to sync/check crate versions --- .github/workflows/solana.yml | 8 + sdk/solana/package.json | 2 +- sdk/solana/scripts/readVersion.ts | 16 - sdk/solana/src/anchor-idl/2_0_0.ts | 15 + .../anchor-idl/2_0_0/dummy_transfer_hook.json | 110 + .../anchor-idl/2_0_0/dummy_transfer_hook.ts | 221 + .../2_0_0/example_native_token_transfers.json | 1932 ++++++++ .../2_0_0/example_native_token_transfers.ts | 4087 +++++++++++++++++ .../src/anchor-idl/2_0_0/ntt_quoter.json | 588 +++ sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.ts | 1177 +++++ .../anchor-idl/2_0_0/wormhole_governance.json | 76 + .../anchor-idl/2_0_0/wormhole_governance.ts | 153 + sdk/solana/src/anchor-idl/index.ts | 1 + sdk/solana/src/bindings.ts | 9 +- solana/Cargo.lock | 10 +- solana/Makefile | 1 + solana/idl/json/dummy_transfer_hook.json | 2 +- .../json/example_native_token_transfers.json | 2 +- solana/idl/json/ntt_quoter.json | 2 +- solana/idl/json/wormhole_governance.json | 2 +- solana/idl/ts/dummy_transfer_hook.ts | 4 +- .../idl/ts/example_native_token_transfers.ts | 4 +- solana/idl/ts/ntt_quoter.ts | 4 +- solana/idl/ts/wormhole_governance.ts | 4 +- solana/modules/ntt-messages/Cargo.toml | 2 +- .../programs/dummy-transfer-hook/Cargo.toml | 2 +- .../example-native-token-transfers/Cargo.toml | 2 +- .../example-native-token-transfers/src/lib.rs | 2 +- solana/programs/ntt-quoter/Cargo.toml | 2 +- .../programs/wormhole-governance/Cargo.toml | 2 +- solana/scripts/program-version | 26 + solana/scripts/sync-versions | 39 + 32 files changed, 8463 insertions(+), 44 deletions(-) delete mode 100644 sdk/solana/scripts/readVersion.ts create mode 100644 sdk/solana/src/anchor-idl/2_0_0.ts create mode 100644 sdk/solana/src/anchor-idl/2_0_0/dummy_transfer_hook.json create mode 100644 sdk/solana/src/anchor-idl/2_0_0/dummy_transfer_hook.ts create mode 100644 sdk/solana/src/anchor-idl/2_0_0/example_native_token_transfers.json create mode 100644 sdk/solana/src/anchor-idl/2_0_0/example_native_token_transfers.ts create mode 100644 sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.json create mode 100644 sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.ts create mode 100644 sdk/solana/src/anchor-idl/2_0_0/wormhole_governance.json create mode 100644 sdk/solana/src/anchor-idl/2_0_0/wormhole_governance.ts create mode 100755 solana/scripts/program-version create mode 100755 solana/scripts/sync-versions diff --git a/.github/workflows/solana.yml b/.github/workflows/solana.yml index 5cb598de0..0faa6174e 100644 --- a/.github/workflows/solana.yml +++ b/.github/workflows/solana.yml @@ -97,6 +97,14 @@ jobs: cargo build-sbf --features "mainnet" cargo test-sbf --features "mainnet" cargo test + check-version: + name: Check version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: ./scripts/sync-versions --check + shell: bash + anchor-test: name: Anchor Test runs-on: ubuntu-latest diff --git a/sdk/solana/package.json b/sdk/solana/package.json index 7cb9ce239..2a8e1a9d4 100644 --- a/sdk/solana/package.json +++ b/sdk/solana/package.json @@ -41,7 +41,7 @@ "test:ci": "jest --config ./jest.config.ts", "copy:idl": "cp ../../solana/target/idl/*.json ./src/anchor-idl/$IDL_VERSION/", "copy:types": "cp ../../solana/target/types/*.ts ./src/anchor-idl/$IDL_VERSION/", - "generate": "export IDL_VERSION=`tsx scripts/readVersion.ts` && mkdir -p ./src/anchor-idl/$IDL_VERSION && npm run copy:idl && npm run copy:types", + "generate": "export IDL_VERSION=`../../solana/scripts/program-version | sed s/\\\\\\./_/g` && mkdir -p ./src/anchor-idl/$IDL_VERSION && npm run copy:idl && npm run copy:types", "build:contracts": "cd ../../solana && make build" }, "devDependencies": { diff --git a/sdk/solana/scripts/readVersion.ts b/sdk/solana/scripts/readVersion.ts deleted file mode 100644 index 2f2a109a7..000000000 --- a/sdk/solana/scripts/readVersion.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as fs from "fs"; - -// From the persp of `sdk/solana` -const projectRoot = "../../"; -const versionFile = "solana/programs/example-native-token-transfers/src/lib.rs"; -const versionRegex = /pub const VERSION/; -(function () { - const contents = fs.readFileSync(projectRoot + versionFile, "utf8"); - for (const line of contents.split("\n")) { - if (line.match(versionRegex)) { - const version = line.split('"')[1]; - console.log(version?.replaceAll(".", "_")); - return; - } - } -})(); diff --git a/sdk/solana/src/anchor-idl/2_0_0.ts b/sdk/solana/src/anchor-idl/2_0_0.ts new file mode 100644 index 000000000..0eaf59c94 --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0.ts @@ -0,0 +1,15 @@ +import ntt from "./2_0_0/example_native_token_transfers.json"; +import quoter from "./2_0_0/ntt_quoter.json"; +import governance from "./2_0_0/wormhole_governance.json"; + +import type { ExampleNativeTokenTransfers } from "./2_0_0/example_native_token_transfers.js"; +import type { NttQuoter } from "./2_0_0/ntt_quoter.js"; +import type { WormholeGovernance } from "./2_0_0/wormhole_governance.js"; + +export namespace _2_0_0 { + export const idl = { ntt, quoter, governance }; + + export type RawExampleNativeTokenTransfers = ExampleNativeTokenTransfers; + export type RawNttQuoter = NttQuoter; + export type RawWormholeGovernance = WormholeGovernance; +} diff --git a/sdk/solana/src/anchor-idl/2_0_0/dummy_transfer_hook.json b/sdk/solana/src/anchor-idl/2_0_0/dummy_transfer_hook.json new file mode 100644 index 000000000..2f6864c36 --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0/dummy_transfer_hook.json @@ -0,0 +1,110 @@ +{ + "version": "2.0.0", + "name": "dummy_transfer_hook", + "instructions": [ + { + "name": "initializeExtraAccountMetaList", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "extraAccountMetaList", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "counter", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "transferHook", + "accounts": [ + { + "name": "sourceToken", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "destinationToken", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "extraAccountMetaList", + "isMut": false, + "isSigner": false + }, + { + "name": "dummyAccount", + "isMut": false, + "isSigner": false, + "docs": [ + "computes and the on-chain code correctly passes on the PDA." + ] + }, + { + "name": "counter", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "Counter", + "type": { + "kind": "struct", + "fields": [ + { + "name": "count", + "type": "u64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/sdk/solana/src/anchor-idl/2_0_0/dummy_transfer_hook.ts b/sdk/solana/src/anchor-idl/2_0_0/dummy_transfer_hook.ts new file mode 100644 index 000000000..25e31bff3 --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0/dummy_transfer_hook.ts @@ -0,0 +1,221 @@ +export type DummyTransferHook = { + "version": "2.0.0", + "name": "dummy_transfer_hook", + "instructions": [ + { + "name": "initializeExtraAccountMetaList", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "extraAccountMetaList", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "counter", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "transferHook", + "accounts": [ + { + "name": "sourceToken", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "destinationToken", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "extraAccountMetaList", + "isMut": false, + "isSigner": false + }, + { + "name": "dummyAccount", + "isMut": false, + "isSigner": false, + "docs": [ + "computes and the on-chain code correctly passes on the PDA." + ] + }, + { + "name": "counter", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "counter", + "type": { + "kind": "struct", + "fields": [ + { + "name": "count", + "type": "u64" + } + ] + } + } + ] +}; + +export const IDL: DummyTransferHook = { + "version": "2.0.0", + "name": "dummy_transfer_hook", + "instructions": [ + { + "name": "initializeExtraAccountMetaList", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "extraAccountMetaList", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "counter", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "transferHook", + "accounts": [ + { + "name": "sourceToken", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "destinationToken", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "extraAccountMetaList", + "isMut": false, + "isSigner": false + }, + { + "name": "dummyAccount", + "isMut": false, + "isSigner": false, + "docs": [ + "computes and the on-chain code correctly passes on the PDA." + ] + }, + { + "name": "counter", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "counter", + "type": { + "kind": "struct", + "fields": [ + { + "name": "count", + "type": "u64" + } + ] + } + } + ] +}; diff --git a/sdk/solana/src/anchor-idl/2_0_0/example_native_token_transfers.json b/sdk/solana/src/anchor-idl/2_0_0/example_native_token_transfers.json new file mode 100644 index 000000000..82369389d --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0/example_native_token_transfers.json @@ -0,0 +1,1932 @@ +{ + "version": "2.0.0", + "name": "example_native_token_transfers", + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "deployer", + "isMut": false, + "isSigner": true + }, + { + "name": "programData", + "isMut": false, + "isSigner": false + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "The custody account that holds tokens in locking mode and temporarily", + "holds tokens in burning mode.", + "function if the token account has already been created." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "associated token account for the given mint." + ] + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "InitializeArgs" + } + } + ] + }, + { + "name": "initializeLut", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "lutAddress", + "isMut": true, + "isSigner": false + }, + { + "name": "lut", + "isMut": true, + "isSigner": false + }, + { + "name": "lutProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "entries", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ] + } + ], + "args": [ + { + "name": "recentSlot", + "type": "u64" + } + ] + }, + { + "name": "version", + "accounts": [], + "args": [], + "returns": "string" + }, + { + "name": "transferBurn", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "account can spend these tokens." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": true + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "sessionAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransferArgs" + } + } + ] + }, + { + "name": "transferLock", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "account can spend these tokens." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": true + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "sessionAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransferArgs" + } + } + ] + }, + { + "name": "redeem", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiverMessage", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false, + "docs": [ + "NOTE: This account is content-addressed (PDA seeded by the message hash).", + "This is because in a multi-transceiver configuration, the different", + "transceivers \"vote\" on messages (by delivering them). By making the inbox", + "items content-addressed, we can ensure that disagreeing votes don't", + "interfere with each other.", + "On the first call to [`redeem()`], [`InboxItem`] will be allocated and initialized with", + "default values.", + "On subsequent calls, we want to modify the `InboxItem` by \"voting\" on it. Therefore the", + "program should not fail which would occur when using the `init` constraint.", + "The [`InboxItem::init`] field is used to guard against malicious or accidental modification", + "InboxItem fields that should remain constant." + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RedeemArgs" + } + } + ] + }, + { + "name": "releaseInboundMint", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseInboundArgs" + } + } + ] + }, + { + "name": "releaseInboundUnlock", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseInboundArgs" + } + } + ] + }, + { + "name": "transferOwnership", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "newOwner", + "isMut": false, + "isSigner": false + }, + { + "name": "upgradeLock", + "isMut": false, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "claimOwnership", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "upgradeLock", + "isMut": false, + "isSigner": false + }, + { + "name": "newOwner", + "isMut": false, + "isSigner": true + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setPaused", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pause", + "type": "bool" + } + ] + }, + { + "name": "setPeer", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": true, + "isSigner": false + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetPeerArgs" + } + } + ] + }, + { + "name": "registerTransceiver", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredTransceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setOutboundLimit", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetOutboundLimitArgs" + } + } + ] + }, + { + "name": "setInboundLimit", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetInboundLimitArgs" + } + } + ] + }, + { + "name": "setWormholePeer", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "peer", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetTransceiverPeerArgs" + } + } + ] + }, + { + "name": "receiveWormholeMessage", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiverMessage", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "releaseWormholeOutbound", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": false + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseOutboundArgs" + } + } + ] + }, + { + "name": "broadcastWormholeId", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [] + }, + { + "name": "broadcastWormholePeer", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "BroadcastPeerArgs" + } + } + ] + } + ], + "accounts": [ + { + "name": "Config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "owner", + "docs": [ + "Owner of the program." + ], + "type": "publicKey" + }, + { + "name": "pendingOwner", + "docs": [ + "Pending next owner (before claiming ownership)." + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "mint", + "docs": [ + "Mint address of the token managed by this program." + ], + "type": "publicKey" + }, + { + "name": "tokenProgram", + "docs": [ + "Address of the token program (token or token22). This could always be queried", + "from the [`mint`] account's owner, but storing it here avoids an indirection", + "on the client side." + ], + "type": "publicKey" + }, + { + "name": "mode", + "docs": [ + "The mode that this program is running in. This is used to determine", + "whether the program is burning tokens or locking tokens." + ], + "type": { + "defined": "Mode" + } + }, + { + "name": "chainId", + "docs": [ + "The chain id of the chain that this program is running on. We don't", + "hardcode this so that the program is deployable on any potential SVM", + "forks." + ], + "type": { + "defined": "ChainId" + } + }, + { + "name": "nextTransceiverId", + "docs": [ + "The next transceiver id to use when registering an transceiver." + ], + "type": "u8" + }, + { + "name": "threshold", + "docs": [ + "The number of transceivers that must attest to a transfer before it is", + "accepted." + ], + "type": "u8" + }, + { + "name": "enabledTransceivers", + "docs": [ + "Bitmap of enabled transceivers.", + "The maximum number of transceivers is equal to [`Bitmap::BITS`]." + ], + "type": { + "defined": "Bitmap" + } + }, + { + "name": "paused", + "docs": [ + "Pause the program. This is useful for upgrades and other maintenance." + ], + "type": "bool" + }, + { + "name": "custody", + "docs": [ + "The custody account that holds tokens in locking mode." + ], + "type": "publicKey" + } + ] + } + }, + { + "name": "LUT", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": "publicKey" + } + ] + } + }, + { + "name": "NttManagerPeer", + "docs": [ + "A peer on another chain. Stored in a PDA seeded by the chain id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "tokenDecimals", + "type": "u8" + } + ] + } + }, + { + "name": "InboxItem", + "type": { + "kind": "struct", + "fields": [ + { + "name": "init", + "type": "bool" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipientAddress", + "type": "publicKey" + }, + { + "name": "votes", + "type": { + "defined": "Bitmap" + } + }, + { + "name": "releaseStatus", + "type": { + "defined": "ReleaseStatus" + } + } + ] + } + }, + { + "name": "InboxRateLimit", + "docs": [ + "Inbound rate limit per chain.", + "SECURITY: must check the PDA (since there are multiple PDAs, namely one for each chain.)" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "rateLimit", + "type": { + "defined": "RateLimitState" + } + } + ] + } + }, + { + "name": "OutboxItem", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": { + "defined": "TrimmedAmount" + } + }, + { + "name": "sender", + "type": "publicKey" + }, + { + "name": "recipientChain", + "type": { + "defined": "ChainId" + } + }, + { + "name": "recipientNttManager", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "recipientAddress", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "releaseTimestamp", + "type": "i64" + }, + { + "name": "released", + "type": { + "defined": "Bitmap" + } + } + ] + } + }, + { + "name": "OutboxRateLimit", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rateLimit", + "type": { + "defined": "RateLimitState" + } + } + ] + } + }, + { + "name": "RegisteredTransceiver", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "id", + "type": "u8" + }, + { + "name": "transceiverAddress", + "type": "publicKey" + } + ] + } + }, + { + "name": "TransceiverPeer", + "docs": [ + "A peer on another chain. Stored in a PDA seeded by the chain id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "BridgeData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "guardianSetIndex", + "docs": [ + "The current guardian set index, used to decide which signature sets to accept." + ], + "type": "u32" + }, + { + "name": "lastLamports", + "docs": [ + "Lamports in the collection account" + ], + "type": "u64" + }, + { + "name": "config", + "docs": [ + "Bridge configuration, which is set once upon initialization." + ], + "type": { + "defined": "BridgeConfig" + } + } + ] + } + } + ], + "types": [ + { + "name": "Bitmap", + "type": { + "kind": "struct", + "fields": [ + { + "name": "map", + "type": "u128" + } + ] + } + }, + { + "name": "SetInboundLimitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "type": "u64" + }, + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + } + ] + } + }, + { + "name": "SetOutboundLimitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "type": "u64" + } + ] + } + }, + { + "name": "SetPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "tokenDecimals", + "docs": [ + "The token decimals on the peer chain." + ], + "type": "u8" + } + ] + } + }, + { + "name": "InitializeArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "mode", + "type": { + "defined": "Mode" + } + } + ] + } + }, + { + "name": "RedeemArgs", + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "ReleaseInboundArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, + { + "name": "TransferArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipientChain", + "type": { + "defined": "ChainId" + } + }, + { + "name": "recipientAddress", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "shouldQueue", + "type": "bool" + } + ] + } + }, + { + "name": "ReleaseStatus", + "docs": [ + "The status of an InboxItem. This determines whether the tokens are minted/unlocked to the recipient. As", + "such, this must be used as a state machine that moves forward in a linear manner. A state", + "should never \"move backward\" to a previous state (e.g. should never move from `Released` to", + "`ReleaseAfter`)." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "NotApproved" + }, + { + "name": "ReleaseAfter", + "fields": [ + "i64" + ] + }, + { + "name": "Released" + } + ] + } + }, + { + "name": "RateLimitState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "docs": [ + "The maximum capacity of the rate limiter." + ], + "type": "u64" + }, + { + "name": "capacityAtLastTx", + "docs": [ + "The capacity of the rate limiter at `last_tx_timestamp`.", + "The actual current capacity is calculated in `capacity_at`, by", + "accounting for the time that has passed since `last_tx_timestamp` and", + "the refill rate." + ], + "type": "u64" + }, + { + "name": "lastTxTimestamp", + "docs": [ + "The timestamp of the last transaction that counted towards the current", + "capacity. Transactions that exceeded the capacity do not count, they are", + "just delayed." + ], + "type": "i64" + } + ] + } + }, + { + "name": "SetTransceiverPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "BroadcastPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + } + ] + } + }, + { + "name": "ReleaseOutboundArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, + { + "name": "ChainId", + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "type": "u16" + } + ] + } + }, + { + "name": "Mode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Locking" + }, + { + "name": "Burning" + } + ] + } + }, + { + "name": "TrimmedAmount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "decimals", + "type": "u8" + } + ] + } + }, + { + "name": "BridgeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "guardianSetExpirationTime", + "docs": [ + "Period for how long a guardian set is valid after it has been replaced by a new one. This", + "guarantees that VAAs issued by that set can still be submitted for a certain period. In", + "this period we still trust the old guardian set." + ], + "type": "u32" + }, + { + "name": "fee", + "docs": [ + "Amount of lamports that needs to be paid to the protocol to post a message" + ], + "type": "u64" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "CantReleaseYet", + "msg": "CantReleaseYet" + }, + { + "code": 6001, + "name": "InvalidPendingOwner", + "msg": "InvalidPendingOwner" + }, + { + "code": 6002, + "name": "InvalidChainId", + "msg": "InvalidChainId" + }, + { + "code": 6003, + "name": "InvalidRecipientAddress", + "msg": "InvalidRecipientAddress" + }, + { + "code": 6004, + "name": "InvalidTransceiverPeer", + "msg": "InvalidTransceiverPeer" + }, + { + "code": 6005, + "name": "InvalidNttManagerPeer", + "msg": "InvalidNttManagerPeer" + }, + { + "code": 6006, + "name": "InvalidRecipientNttManager", + "msg": "InvalidRecipientNttManager" + }, + { + "code": 6007, + "name": "TransferAlreadyRedeemed", + "msg": "TransferAlreadyRedeemed" + }, + { + "code": 6008, + "name": "TransferCannotBeRedeemed", + "msg": "TransferCannotBeRedeemed" + }, + { + "code": 6009, + "name": "TransferNotApproved", + "msg": "TransferNotApproved" + }, + { + "code": 6010, + "name": "MessageAlreadySent", + "msg": "MessageAlreadySent" + }, + { + "code": 6011, + "name": "InvalidMode", + "msg": "InvalidMode" + }, + { + "code": 6012, + "name": "InvalidMintAuthority", + "msg": "InvalidMintAuthority" + }, + { + "code": 6013, + "name": "TransferExceedsRateLimit", + "msg": "TransferExceedsRateLimit" + }, + { + "code": 6014, + "name": "Paused", + "msg": "Paused" + }, + { + "code": 6015, + "name": "DisabledTransceiver", + "msg": "DisabledTransceiver" + }, + { + "code": 6016, + "name": "InvalidDeployer", + "msg": "InvalidDeployer" + }, + { + "code": 6017, + "name": "BadAmountAfterTransfer", + "msg": "BadAmountAfterTransfer" + }, + { + "code": 6018, + "name": "BadAmountAfterBurn", + "msg": "BadAmountAfterBurn" + }, + { + "code": 6019, + "name": "ZeroThreshold", + "msg": "ZeroThreshold" + }, + { + "code": 6020, + "name": "OverflowExponent", + "msg": "OverflowExponent" + }, + { + "code": 6021, + "name": "OverflowScaledAmount", + "msg": "OverflowScaledAmount" + }, + { + "code": 6022, + "name": "BitmapIndexOutOfBounds", + "msg": "BitmapIndexOutOfBounds" + } + ] +} diff --git a/sdk/solana/src/anchor-idl/2_0_0/example_native_token_transfers.ts b/sdk/solana/src/anchor-idl/2_0_0/example_native_token_transfers.ts new file mode 100644 index 000000000..4a7fceeeb --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0/example_native_token_transfers.ts @@ -0,0 +1,4087 @@ +export type ExampleNativeTokenTransfers = { + "version": "2.0.0", + "name": "example_native_token_transfers", + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "deployer", + "isMut": false, + "isSigner": true + }, + { + "name": "programData", + "isMut": false, + "isSigner": false + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "The custody account that holds tokens in locking mode and temporarily", + "holds tokens in burning mode.", + "function if the token account has already been created." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "associated token account for the given mint." + ] + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "InitializeArgs" + } + } + ] + }, + { + "name": "initializeLut", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "lutAddress", + "isMut": true, + "isSigner": false + }, + { + "name": "lut", + "isMut": true, + "isSigner": false + }, + { + "name": "lutProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "entries", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ] + } + ], + "args": [ + { + "name": "recentSlot", + "type": "u64" + } + ] + }, + { + "name": "version", + "accounts": [], + "args": [], + "returns": "string" + }, + { + "name": "transferBurn", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "account can spend these tokens." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": true + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "sessionAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransferArgs" + } + } + ] + }, + { + "name": "transferLock", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "account can spend these tokens." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": true + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "sessionAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransferArgs" + } + } + ] + }, + { + "name": "redeem", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiverMessage", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false, + "docs": [ + "NOTE: This account is content-addressed (PDA seeded by the message hash).", + "This is because in a multi-transceiver configuration, the different", + "transceivers \"vote\" on messages (by delivering them). By making the inbox", + "items content-addressed, we can ensure that disagreeing votes don't", + "interfere with each other.", + "On the first call to [`redeem()`], [`InboxItem`] will be allocated and initialized with", + "default values.", + "On subsequent calls, we want to modify the `InboxItem` by \"voting\" on it. Therefore the", + "program should not fail which would occur when using the `init` constraint.", + "The [`InboxItem::init`] field is used to guard against malicious or accidental modification", + "InboxItem fields that should remain constant." + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RedeemArgs" + } + } + ] + }, + { + "name": "releaseInboundMint", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseInboundArgs" + } + } + ] + }, + { + "name": "releaseInboundUnlock", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseInboundArgs" + } + } + ] + }, + { + "name": "transferOwnership", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "newOwner", + "isMut": false, + "isSigner": false + }, + { + "name": "upgradeLock", + "isMut": false, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "claimOwnership", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "upgradeLock", + "isMut": false, + "isSigner": false + }, + { + "name": "newOwner", + "isMut": false, + "isSigner": true + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setPaused", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pause", + "type": "bool" + } + ] + }, + { + "name": "setPeer", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": true, + "isSigner": false + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetPeerArgs" + } + } + ] + }, + { + "name": "registerTransceiver", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredTransceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setOutboundLimit", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetOutboundLimitArgs" + } + } + ] + }, + { + "name": "setInboundLimit", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetInboundLimitArgs" + } + } + ] + }, + { + "name": "setWormholePeer", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "peer", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetTransceiverPeerArgs" + } + } + ] + }, + { + "name": "receiveWormholeMessage", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiverMessage", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "releaseWormholeOutbound", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": false + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseOutboundArgs" + } + } + ] + }, + { + "name": "broadcastWormholeId", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [] + }, + { + "name": "broadcastWormholePeer", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "BroadcastPeerArgs" + } + } + ] + } + ], + "accounts": [ + { + "name": "config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "owner", + "docs": [ + "Owner of the program." + ], + "type": "publicKey" + }, + { + "name": "pendingOwner", + "docs": [ + "Pending next owner (before claiming ownership)." + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "mint", + "docs": [ + "Mint address of the token managed by this program." + ], + "type": "publicKey" + }, + { + "name": "tokenProgram", + "docs": [ + "Address of the token program (token or token22). This could always be queried", + "from the [`mint`] account's owner, but storing it here avoids an indirection", + "on the client side." + ], + "type": "publicKey" + }, + { + "name": "mode", + "docs": [ + "The mode that this program is running in. This is used to determine", + "whether the program is burning tokens or locking tokens." + ], + "type": { + "defined": "Mode" + } + }, + { + "name": "chainId", + "docs": [ + "The chain id of the chain that this program is running on. We don't", + "hardcode this so that the program is deployable on any potential SVM", + "forks." + ], + "type": { + "defined": "ChainId" + } + }, + { + "name": "nextTransceiverId", + "docs": [ + "The next transceiver id to use when registering an transceiver." + ], + "type": "u8" + }, + { + "name": "threshold", + "docs": [ + "The number of transceivers that must attest to a transfer before it is", + "accepted." + ], + "type": "u8" + }, + { + "name": "enabledTransceivers", + "docs": [ + "Bitmap of enabled transceivers.", + "The maximum number of transceivers is equal to [`Bitmap::BITS`]." + ], + "type": { + "defined": "Bitmap" + } + }, + { + "name": "paused", + "docs": [ + "Pause the program. This is useful for upgrades and other maintenance." + ], + "type": "bool" + }, + { + "name": "custody", + "docs": [ + "The custody account that holds tokens in locking mode." + ], + "type": "publicKey" + } + ] + } + }, + { + "name": "lut", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": "publicKey" + } + ] + } + }, + { + "name": "validatedTransceiverMessage", + "generics": [ + "A" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "fromChain", + "type": { + "defined": "ChainId" + } + }, + { + "name": "message", + "type": { + "definedWithTypeArgs": { + "name": "TransceiverMessageData", + "args": [ + { + "type": { + "generic": "A" + } + } + ] + } + } + } + ] + } + }, + { + "name": "nttManagerPeer", + "docs": [ + "A peer on another chain. Stored in a PDA seeded by the chain id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "tokenDecimals", + "type": "u8" + } + ] + } + }, + { + "name": "inboxItem", + "type": { + "kind": "struct", + "fields": [ + { + "name": "init", + "type": "bool" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipientAddress", + "type": "publicKey" + }, + { + "name": "votes", + "type": { + "defined": "Bitmap" + } + }, + { + "name": "releaseStatus", + "type": { + "defined": "ReleaseStatus" + } + } + ] + } + }, + { + "name": "inboxRateLimit", + "docs": [ + "Inbound rate limit per chain.", + "SECURITY: must check the PDA (since there are multiple PDAs, namely one for each chain.)" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "rateLimit", + "type": { + "defined": "RateLimitState" + } + } + ] + } + }, + { + "name": "outboxItem", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": { + "defined": "TrimmedAmount" + } + }, + { + "name": "sender", + "type": "publicKey" + }, + { + "name": "recipientChain", + "type": { + "defined": "ChainId" + } + }, + { + "name": "recipientNttManager", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "recipientAddress", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "releaseTimestamp", + "type": "i64" + }, + { + "name": "released", + "type": { + "defined": "Bitmap" + } + } + ] + } + }, + { + "name": "outboxRateLimit", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rateLimit", + "type": { + "defined": "RateLimitState" + } + } + ] + } + }, + { + "name": "registeredTransceiver", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "id", + "type": "u8" + }, + { + "name": "transceiverAddress", + "type": "publicKey" + } + ] + } + }, + { + "name": "transceiverPeer", + "docs": [ + "A peer on another chain. Stored in a PDA seeded by the chain id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "bridgeData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "guardianSetIndex", + "docs": [ + "The current guardian set index, used to decide which signature sets to accept." + ], + "type": "u32" + }, + { + "name": "lastLamports", + "docs": [ + "Lamports in the collection account" + ], + "type": "u64" + }, + { + "name": "config", + "docs": [ + "Bridge configuration, which is set once upon initialization." + ], + "type": { + "defined": "BridgeConfig" + } + } + ] + } + } + ], + "types": [ + { + "name": "Bitmap", + "type": { + "kind": "struct", + "fields": [ + { + "name": "map", + "type": "u128" + } + ] + } + }, + { + "name": "SetInboundLimitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "type": "u64" + }, + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + } + ] + } + }, + { + "name": "SetOutboundLimitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "type": "u64" + } + ] + } + }, + { + "name": "SetPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "tokenDecimals", + "docs": [ + "The token decimals on the peer chain." + ], + "type": "u8" + } + ] + } + }, + { + "name": "InitializeArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "mode", + "type": { + "defined": "Mode" + } + } + ] + } + }, + { + "name": "RedeemArgs", + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "ReleaseInboundArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, + { + "name": "TransferArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipientChain", + "type": { + "defined": "ChainId" + } + }, + { + "name": "recipientAddress", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "shouldQueue", + "type": "bool" + } + ] + } + }, + { + "name": "ReleaseStatus", + "docs": [ + "The status of an InboxItem. This determines whether the tokens are minted/unlocked to the recipient. As", + "such, this must be used as a state machine that moves forward in a linear manner. A state", + "should never \"move backward\" to a previous state (e.g. should never move from `Released` to", + "`ReleaseAfter`)." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "NotApproved" + }, + { + "name": "ReleaseAfter", + "fields": [ + "i64" + ] + }, + { + "name": "Released" + } + ] + } + }, + { + "name": "RateLimitState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "docs": [ + "The maximum capacity of the rate limiter." + ], + "type": "u64" + }, + { + "name": "capacityAtLastTx", + "docs": [ + "The capacity of the rate limiter at `last_tx_timestamp`.", + "The actual current capacity is calculated in `capacity_at`, by", + "accounting for the time that has passed since `last_tx_timestamp` and", + "the refill rate." + ], + "type": "u64" + }, + { + "name": "lastTxTimestamp", + "docs": [ + "The timestamp of the last transaction that counted towards the current", + "capacity. Transactions that exceeded the capacity do not count, they are", + "just delayed." + ], + "type": "i64" + } + ] + } + }, + { + "name": "SetTransceiverPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "BroadcastPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + } + ] + } + }, + { + "name": "ReleaseOutboundArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, + { + "name": "ChainId", + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "type": "u16" + } + ] + } + }, + { + "name": "Mode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Locking" + }, + { + "name": "Burning" + } + ] + } + }, + { + "name": "NttManagerMessage", + "generics": [ + "A" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "sender", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "payload", + "type": { + "generic": "A" + } + } + ] + } + }, + { + "name": "TransceiverMessageData", + "generics": [ + "A" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "sourceNttManager", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "recipientNttManager", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nttManagerPayload", + "type": { + "definedWithTypeArgs": { + "name": "NttManagerMessage", + "args": [ + { + "type": { + "generic": "A" + } + } + ] + } + } + } + ] + } + }, + { + "name": "TrimmedAmount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "decimals", + "type": "u8" + } + ] + } + }, + { + "name": "BridgeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "guardianSetExpirationTime", + "docs": [ + "Period for how long a guardian set is valid after it has been replaced by a new one. This", + "guarantees that VAAs issued by that set can still be submitted for a certain period. In", + "this period we still trust the old guardian set." + ], + "type": "u32" + }, + { + "name": "fee", + "docs": [ + "Amount of lamports that needs to be paid to the protocol to post a message" + ], + "type": "u64" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "CantReleaseYet", + "msg": "CantReleaseYet" + }, + { + "code": 6001, + "name": "InvalidPendingOwner", + "msg": "InvalidPendingOwner" + }, + { + "code": 6002, + "name": "InvalidChainId", + "msg": "InvalidChainId" + }, + { + "code": 6003, + "name": "InvalidRecipientAddress", + "msg": "InvalidRecipientAddress" + }, + { + "code": 6004, + "name": "InvalidTransceiverPeer", + "msg": "InvalidTransceiverPeer" + }, + { + "code": 6005, + "name": "InvalidNttManagerPeer", + "msg": "InvalidNttManagerPeer" + }, + { + "code": 6006, + "name": "InvalidRecipientNttManager", + "msg": "InvalidRecipientNttManager" + }, + { + "code": 6007, + "name": "TransferAlreadyRedeemed", + "msg": "TransferAlreadyRedeemed" + }, + { + "code": 6008, + "name": "TransferCannotBeRedeemed", + "msg": "TransferCannotBeRedeemed" + }, + { + "code": 6009, + "name": "TransferNotApproved", + "msg": "TransferNotApproved" + }, + { + "code": 6010, + "name": "MessageAlreadySent", + "msg": "MessageAlreadySent" + }, + { + "code": 6011, + "name": "InvalidMode", + "msg": "InvalidMode" + }, + { + "code": 6012, + "name": "InvalidMintAuthority", + "msg": "InvalidMintAuthority" + }, + { + "code": 6013, + "name": "TransferExceedsRateLimit", + "msg": "TransferExceedsRateLimit" + }, + { + "code": 6014, + "name": "Paused", + "msg": "Paused" + }, + { + "code": 6015, + "name": "DisabledTransceiver", + "msg": "DisabledTransceiver" + }, + { + "code": 6016, + "name": "InvalidDeployer", + "msg": "InvalidDeployer" + }, + { + "code": 6017, + "name": "BadAmountAfterTransfer", + "msg": "BadAmountAfterTransfer" + }, + { + "code": 6018, + "name": "BadAmountAfterBurn", + "msg": "BadAmountAfterBurn" + }, + { + "code": 6019, + "name": "ZeroThreshold", + "msg": "ZeroThreshold" + }, + { + "code": 6020, + "name": "OverflowExponent", + "msg": "OverflowExponent" + }, + { + "code": 6021, + "name": "OverflowScaledAmount", + "msg": "OverflowScaledAmount" + }, + { + "code": 6022, + "name": "BitmapIndexOutOfBounds", + "msg": "BitmapIndexOutOfBounds" + } + ] +}; + +export const IDL: ExampleNativeTokenTransfers = { + "version": "2.0.0", + "name": "example_native_token_transfers", + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "deployer", + "isMut": false, + "isSigner": true + }, + { + "name": "programData", + "isMut": false, + "isSigner": false + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "The custody account that holds tokens in locking mode and temporarily", + "holds tokens in burning mode.", + "function if the token account has already been created." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "associated token account for the given mint." + ] + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "InitializeArgs" + } + } + ] + }, + { + "name": "initializeLut", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "lutAddress", + "isMut": true, + "isSigner": false + }, + { + "name": "lut", + "isMut": true, + "isSigner": false + }, + { + "name": "lutProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "entries", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ] + } + ], + "args": [ + { + "name": "recentSlot", + "type": "u64" + } + ] + }, + { + "name": "version", + "accounts": [], + "args": [], + "returns": "string" + }, + { + "name": "transferBurn", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "account can spend these tokens." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": true + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "sessionAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransferArgs" + } + } + ] + }, + { + "name": "transferLock", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "account can spend these tokens." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": true + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false, + "docs": [ + "Tokens are always transferred to the custody account first regardless of", + "the mode.", + "For an explanation, see the note in [`transfer_burn`]." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "sessionAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "See [`crate::SESSION_AUTHORITY_SEED`] for an explanation of the flow." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransferArgs" + } + } + ] + }, + { + "name": "redeem", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiverMessage", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false, + "docs": [ + "NOTE: This account is content-addressed (PDA seeded by the message hash).", + "This is because in a multi-transceiver configuration, the different", + "transceivers \"vote\" on messages (by delivering them). By making the inbox", + "items content-addressed, we can ensure that disagreeing votes don't", + "interfere with each other.", + "On the first call to [`redeem()`], [`InboxItem`] will be allocated and initialized with", + "default values.", + "On subsequent calls, we want to modify the `InboxItem` by \"voting\" on it. Therefore the", + "program should not fail which would occur when using the `init` constraint.", + "The [`InboxItem::init`] field is used to guard against malicious or accidental modification", + "InboxItem fields that should remain constant." + ] + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "outboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RedeemArgs" + } + } + ] + }, + { + "name": "releaseInboundMint", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseInboundArgs" + } + } + ] + }, + { + "name": "releaseInboundUnlock", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "inboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "custody", + "isMut": true, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseInboundArgs" + } + } + ] + }, + { + "name": "transferOwnership", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "newOwner", + "isMut": false, + "isSigner": false + }, + { + "name": "upgradeLock", + "isMut": false, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "claimOwnership", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "upgradeLock", + "isMut": false, + "isSigner": false + }, + { + "name": "newOwner", + "isMut": false, + "isSigner": true + }, + { + "name": "programData", + "isMut": true, + "isSigner": false + }, + { + "name": "bpfLoaderUpgradeableProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setPaused", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pause", + "type": "bool" + } + ] + }, + { + "name": "setPeer", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": true, + "isSigner": false + }, + { + "name": "inboxRateLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetPeerArgs" + } + } + ] + }, + { + "name": "registerTransceiver", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredTransceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setOutboundLimit", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetOutboundLimitArgs" + } + } + ] + }, + { + "name": "setInboundLimit", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "rateLimit", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetInboundLimitArgs" + } + } + ] + }, + { + "name": "setWormholePeer", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "peer", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SetTransceiverPeerArgs" + } + } + ] + }, + { + "name": "receiveWormholeMessage", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "transceiverMessage", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "releaseWormholeOutbound", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + } + ] + }, + { + "name": "outboxItem", + "isMut": true, + "isSigner": false + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": false + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ReleaseOutboundArgs" + } + } + ] + }, + { + "name": "broadcastWormholeId", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [] + }, + { + "name": "broadcastWormholePeer", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "peer", + "isMut": false, + "isSigner": false + }, + { + "name": "wormholeMessage", + "isMut": true, + "isSigner": true + }, + { + "name": "emitter", + "isMut": false, + "isSigner": false + }, + { + "name": "wormhole", + "accounts": [ + { + "name": "bridge", + "isMut": true, + "isSigner": false + }, + { + "name": "feeCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "sequence", + "isMut": true, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "BroadcastPeerArgs" + } + } + ] + } + ], + "accounts": [ + { + "name": "config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "owner", + "docs": [ + "Owner of the program." + ], + "type": "publicKey" + }, + { + "name": "pendingOwner", + "docs": [ + "Pending next owner (before claiming ownership)." + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "mint", + "docs": [ + "Mint address of the token managed by this program." + ], + "type": "publicKey" + }, + { + "name": "tokenProgram", + "docs": [ + "Address of the token program (token or token22). This could always be queried", + "from the [`mint`] account's owner, but storing it here avoids an indirection", + "on the client side." + ], + "type": "publicKey" + }, + { + "name": "mode", + "docs": [ + "The mode that this program is running in. This is used to determine", + "whether the program is burning tokens or locking tokens." + ], + "type": { + "defined": "Mode" + } + }, + { + "name": "chainId", + "docs": [ + "The chain id of the chain that this program is running on. We don't", + "hardcode this so that the program is deployable on any potential SVM", + "forks." + ], + "type": { + "defined": "ChainId" + } + }, + { + "name": "nextTransceiverId", + "docs": [ + "The next transceiver id to use when registering an transceiver." + ], + "type": "u8" + }, + { + "name": "threshold", + "docs": [ + "The number of transceivers that must attest to a transfer before it is", + "accepted." + ], + "type": "u8" + }, + { + "name": "enabledTransceivers", + "docs": [ + "Bitmap of enabled transceivers.", + "The maximum number of transceivers is equal to [`Bitmap::BITS`]." + ], + "type": { + "defined": "Bitmap" + } + }, + { + "name": "paused", + "docs": [ + "Pause the program. This is useful for upgrades and other maintenance." + ], + "type": "bool" + }, + { + "name": "custody", + "docs": [ + "The custody account that holds tokens in locking mode." + ], + "type": "publicKey" + } + ] + } + }, + { + "name": "lut", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": "publicKey" + } + ] + } + }, + { + "name": "validatedTransceiverMessage", + "generics": [ + "A" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "fromChain", + "type": { + "defined": "ChainId" + } + }, + { + "name": "message", + "type": { + "definedWithTypeArgs": { + "name": "TransceiverMessageData", + "args": [ + { + "type": { + "generic": "A" + } + } + ] + } + } + } + ] + } + }, + { + "name": "nttManagerPeer", + "docs": [ + "A peer on another chain. Stored in a PDA seeded by the chain id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "tokenDecimals", + "type": "u8" + } + ] + } + }, + { + "name": "inboxItem", + "type": { + "kind": "struct", + "fields": [ + { + "name": "init", + "type": "bool" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipientAddress", + "type": "publicKey" + }, + { + "name": "votes", + "type": { + "defined": "Bitmap" + } + }, + { + "name": "releaseStatus", + "type": { + "defined": "ReleaseStatus" + } + } + ] + } + }, + { + "name": "inboxRateLimit", + "docs": [ + "Inbound rate limit per chain.", + "SECURITY: must check the PDA (since there are multiple PDAs, namely one for each chain.)" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "rateLimit", + "type": { + "defined": "RateLimitState" + } + } + ] + } + }, + { + "name": "outboxItem", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": { + "defined": "TrimmedAmount" + } + }, + { + "name": "sender", + "type": "publicKey" + }, + { + "name": "recipientChain", + "type": { + "defined": "ChainId" + } + }, + { + "name": "recipientNttManager", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "recipientAddress", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "releaseTimestamp", + "type": "i64" + }, + { + "name": "released", + "type": { + "defined": "Bitmap" + } + } + ] + } + }, + { + "name": "outboxRateLimit", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rateLimit", + "type": { + "defined": "RateLimitState" + } + } + ] + } + }, + { + "name": "registeredTransceiver", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "id", + "type": "u8" + }, + { + "name": "transceiverAddress", + "type": "publicKey" + } + ] + } + }, + { + "name": "transceiverPeer", + "docs": [ + "A peer on another chain. Stored in a PDA seeded by the chain id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "bridgeData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "guardianSetIndex", + "docs": [ + "The current guardian set index, used to decide which signature sets to accept." + ], + "type": "u32" + }, + { + "name": "lastLamports", + "docs": [ + "Lamports in the collection account" + ], + "type": "u64" + }, + { + "name": "config", + "docs": [ + "Bridge configuration, which is set once upon initialization." + ], + "type": { + "defined": "BridgeConfig" + } + } + ] + } + } + ], + "types": [ + { + "name": "Bitmap", + "type": { + "kind": "struct", + "fields": [ + { + "name": "map", + "type": "u128" + } + ] + } + }, + { + "name": "SetInboundLimitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "type": "u64" + }, + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + } + ] + } + }, + { + "name": "SetOutboundLimitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "type": "u64" + } + ] + } + }, + { + "name": "SetPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "tokenDecimals", + "docs": [ + "The token decimals on the peer chain." + ], + "type": "u8" + } + ] + } + }, + { + "name": "InitializeArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "mode", + "type": { + "defined": "Mode" + } + } + ] + } + }, + { + "name": "RedeemArgs", + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "ReleaseInboundArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, + { + "name": "TransferArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipientChain", + "type": { + "defined": "ChainId" + } + }, + { + "name": "recipientAddress", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "shouldQueue", + "type": "bool" + } + ] + } + }, + { + "name": "ReleaseStatus", + "docs": [ + "The status of an InboxItem. This determines whether the tokens are minted/unlocked to the recipient. As", + "such, this must be used as a state machine that moves forward in a linear manner. A state", + "should never \"move backward\" to a previous state (e.g. should never move from `Released` to", + "`ReleaseAfter`)." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "NotApproved" + }, + { + "name": "ReleaseAfter", + "fields": [ + "i64" + ] + }, + { + "name": "Released" + } + ] + } + }, + { + "name": "RateLimitState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "limit", + "docs": [ + "The maximum capacity of the rate limiter." + ], + "type": "u64" + }, + { + "name": "capacityAtLastTx", + "docs": [ + "The capacity of the rate limiter at `last_tx_timestamp`.", + "The actual current capacity is calculated in `capacity_at`, by", + "accounting for the time that has passed since `last_tx_timestamp` and", + "the refill rate." + ], + "type": "u64" + }, + { + "name": "lastTxTimestamp", + "docs": [ + "The timestamp of the last transaction that counted towards the current", + "capacity. Transactions that exceeded the capacity do not count, they are", + "just delayed." + ], + "type": "i64" + } + ] + } + }, + { + "name": "SetTransceiverPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": { + "defined": "ChainId" + } + }, + { + "name": "address", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "BroadcastPeerArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + } + ] + } + }, + { + "name": "ReleaseOutboundArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, + { + "name": "ChainId", + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "type": "u16" + } + ] + } + }, + { + "name": "Mode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Locking" + }, + { + "name": "Burning" + } + ] + } + }, + { + "name": "NttManagerMessage", + "generics": [ + "A" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "sender", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "payload", + "type": { + "generic": "A" + } + } + ] + } + }, + { + "name": "TransceiverMessageData", + "generics": [ + "A" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "sourceNttManager", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "recipientNttManager", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nttManagerPayload", + "type": { + "definedWithTypeArgs": { + "name": "NttManagerMessage", + "args": [ + { + "type": { + "generic": "A" + } + } + ] + } + } + } + ] + } + }, + { + "name": "TrimmedAmount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "decimals", + "type": "u8" + } + ] + } + }, + { + "name": "BridgeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "guardianSetExpirationTime", + "docs": [ + "Period for how long a guardian set is valid after it has been replaced by a new one. This", + "guarantees that VAAs issued by that set can still be submitted for a certain period. In", + "this period we still trust the old guardian set." + ], + "type": "u32" + }, + { + "name": "fee", + "docs": [ + "Amount of lamports that needs to be paid to the protocol to post a message" + ], + "type": "u64" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "CantReleaseYet", + "msg": "CantReleaseYet" + }, + { + "code": 6001, + "name": "InvalidPendingOwner", + "msg": "InvalidPendingOwner" + }, + { + "code": 6002, + "name": "InvalidChainId", + "msg": "InvalidChainId" + }, + { + "code": 6003, + "name": "InvalidRecipientAddress", + "msg": "InvalidRecipientAddress" + }, + { + "code": 6004, + "name": "InvalidTransceiverPeer", + "msg": "InvalidTransceiverPeer" + }, + { + "code": 6005, + "name": "InvalidNttManagerPeer", + "msg": "InvalidNttManagerPeer" + }, + { + "code": 6006, + "name": "InvalidRecipientNttManager", + "msg": "InvalidRecipientNttManager" + }, + { + "code": 6007, + "name": "TransferAlreadyRedeemed", + "msg": "TransferAlreadyRedeemed" + }, + { + "code": 6008, + "name": "TransferCannotBeRedeemed", + "msg": "TransferCannotBeRedeemed" + }, + { + "code": 6009, + "name": "TransferNotApproved", + "msg": "TransferNotApproved" + }, + { + "code": 6010, + "name": "MessageAlreadySent", + "msg": "MessageAlreadySent" + }, + { + "code": 6011, + "name": "InvalidMode", + "msg": "InvalidMode" + }, + { + "code": 6012, + "name": "InvalidMintAuthority", + "msg": "InvalidMintAuthority" + }, + { + "code": 6013, + "name": "TransferExceedsRateLimit", + "msg": "TransferExceedsRateLimit" + }, + { + "code": 6014, + "name": "Paused", + "msg": "Paused" + }, + { + "code": 6015, + "name": "DisabledTransceiver", + "msg": "DisabledTransceiver" + }, + { + "code": 6016, + "name": "InvalidDeployer", + "msg": "InvalidDeployer" + }, + { + "code": 6017, + "name": "BadAmountAfterTransfer", + "msg": "BadAmountAfterTransfer" + }, + { + "code": 6018, + "name": "BadAmountAfterBurn", + "msg": "BadAmountAfterBurn" + }, + { + "code": 6019, + "name": "ZeroThreshold", + "msg": "ZeroThreshold" + }, + { + "code": 6020, + "name": "OverflowExponent", + "msg": "OverflowExponent" + }, + { + "code": 6021, + "name": "OverflowScaledAmount", + "msg": "OverflowScaledAmount" + }, + { + "code": 6022, + "name": "BitmapIndexOutOfBounds", + "msg": "BitmapIndexOutOfBounds" + } + ] +}; diff --git a/sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.json b/sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.json new file mode 100644 index 000000000..d6c6f2f7a --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.json @@ -0,0 +1,588 @@ +{ + "version": "2.0.0", + "name": "ntt_quoter", + "instructions": [ + { + "name": "requestRelay", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": false, + "isSigner": false, + "docs": [ + "and checking the release constraint into a single function" + ] + }, + { + "name": "relayRequest", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RequestRelayArgs" + } + } + ] + }, + { + "name": "closeRelay", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": true, + "isSigner": false + }, + { + "name": "relayRequest", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initialize", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": false, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false, + "docs": [ + "We use the program data to make sure this owner is the upgrade authority (the true owner,", + "who deployed this program)." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setAssistant", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "assistant", + "isMut": false, + "isSigner": false, + "isOptional": true + } + ], + "args": [] + }, + { + "name": "setFeeRecipient", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "registerChain", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RegisterChainArgs" + } + } + ] + }, + { + "name": "registerNtt", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RegisterNttArgs" + } + } + ] + }, + { + "name": "deregisterNtt", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "DeregisterNttArgs" + } + } + ] + }, + { + "name": "updateSolPrice", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateSolPriceArgs" + } + } + ] + }, + { + "name": "updateChainPrices", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateChainPricesArgs" + } + } + ] + }, + { + "name": "updateChainParams", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateChainParamsArgs" + } + } + ] + } + ], + "accounts": [ + { + "name": "Instance", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "publicKey" + }, + { + "name": "assistant", + "type": "publicKey" + }, + { + "name": "feeRecipient", + "type": "publicKey" + }, + { + "name": "solPrice", + "type": "u64" + } + ] + } + }, + { + "name": "RegisteredChain", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "maxGasDropoff", + "type": "u64" + }, + { + "name": "basePrice", + "type": "u64" + }, + { + "name": "nativePrice", + "type": "u64" + }, + { + "name": "gasPrice", + "type": "u64" + } + ] + } + }, + { + "name": "RegisteredNtt", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "wormholeTransceiverIndex", + "type": "u8" + }, + { + "name": "gasCost", + "type": "u32" + } + ] + } + }, + { + "name": "RelayRequest", + "type": { + "kind": "struct", + "fields": [ + { + "name": "requestedGasDropoff", + "type": "u64" + } + ] + } + } + ], + "types": [ + { + "name": "RegisterChainArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + } + ] + } + }, + { + "name": "RegisterNttArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nttProgramId", + "type": "publicKey" + }, + { + "name": "wormholeTransceiverIndex", + "type": "u8" + }, + { + "name": "gasCost", + "type": "u32" + } + ] + } + }, + { + "name": "DeregisterNttArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nttProgramId", + "type": "publicKey" + } + ] + } + }, + { + "name": "RequestRelayArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gasDropoff", + "type": "u64" + }, + { + "name": "maxFee", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateSolPriceArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "solPrice", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateChainPricesArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nativePrice", + "type": "u64" + }, + { + "name": "gasPrice", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateChainParamsArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxGasDropoff", + "type": "u64" + }, + { + "name": "basePrice", + "type": "u64" + } + ] + } + } + ], + "errors": [ + { + "code": 6001, + "name": "ExceedsUserMaxFee", + "msg": "Relay fees exceeds specified max" + }, + { + "code": 6002, + "name": "ExceedsMaxGasDropoff", + "msg": "Requested gas dropoff exceeds max allowed for chain" + }, + { + "code": 6003, + "name": "InvalidFeeRecipient", + "msg": "The specified fee recipient does not match the address in the instance accound" + }, + { + "code": 6004, + "name": "RelayingToChainDisabled", + "msg": "Relaying to the specified chain is disabled" + }, + { + "code": 6005, + "name": "OutboxItemNotReleased", + "msg": "Relaying to the specified chain is disabled" + }, + { + "code": 6006, + "name": "ScalingOverflow", + "msg": "Scaled value exceeds u64::MAX" + }, + { + "code": 6007, + "name": "DivByZero", + "msg": "Cannot divide by zero" + }, + { + "code": 6257, + "name": "FeeRecipientCannotBeDefault", + "msg": "The fee recipient cannot be the default address (0x0)" + }, + { + "code": 6258, + "name": "NotAuthorized", + "msg": "Must be owner or assistant" + }, + { + "code": 6259, + "name": "PriceCannotBeZero", + "msg": "The price cannot be zero" + } + ] +} \ No newline at end of file diff --git a/sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.ts b/sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.ts new file mode 100644 index 000000000..063f2ec30 --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0/ntt_quoter.ts @@ -0,0 +1,1177 @@ +export type NttQuoter = { + "version": "2.0.0", + "name": "ntt_quoter", + "instructions": [ + { + "name": "requestRelay", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": false, + "isSigner": false, + "docs": [ + "and checking the release constraint into a single function" + ] + }, + { + "name": "relayRequest", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RequestRelayArgs" + } + } + ] + }, + { + "name": "closeRelay", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": true, + "isSigner": false + }, + { + "name": "relayRequest", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initialize", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": false, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false, + "docs": [ + "We use the program data to make sure this owner is the upgrade authority (the true owner,", + "who deployed this program)." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setAssistant", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "assistant", + "isMut": false, + "isSigner": false, + "isOptional": true + } + ], + "args": [] + }, + { + "name": "setFeeRecipient", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "registerChain", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RegisterChainArgs" + } + } + ] + }, + { + "name": "registerNtt", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RegisterNttArgs" + } + } + ] + }, + { + "name": "deregisterNtt", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "DeregisterNttArgs" + } + } + ] + }, + { + "name": "updateSolPrice", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateSolPriceArgs" + } + } + ] + }, + { + "name": "updateChainPrices", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateChainPricesArgs" + } + } + ] + }, + { + "name": "updateChainParams", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateChainParamsArgs" + } + } + ] + } + ], + "accounts": [ + { + "name": "instance", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "publicKey" + }, + { + "name": "assistant", + "type": "publicKey" + }, + { + "name": "feeRecipient", + "type": "publicKey" + }, + { + "name": "solPrice", + "type": "u64" + } + ] + } + }, + { + "name": "registeredChain", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "maxGasDropoff", + "type": "u64" + }, + { + "name": "basePrice", + "type": "u64" + }, + { + "name": "nativePrice", + "type": "u64" + }, + { + "name": "gasPrice", + "type": "u64" + } + ] + } + }, + { + "name": "registeredNtt", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "wormholeTransceiverIndex", + "type": "u8" + }, + { + "name": "gasCost", + "type": "u32" + } + ] + } + }, + { + "name": "relayRequest", + "type": { + "kind": "struct", + "fields": [ + { + "name": "requestedGasDropoff", + "type": "u64" + } + ] + } + } + ], + "types": [ + { + "name": "RegisterChainArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + } + ] + } + }, + { + "name": "RegisterNttArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nttProgramId", + "type": "publicKey" + }, + { + "name": "wormholeTransceiverIndex", + "type": "u8" + }, + { + "name": "gasCost", + "type": "u32" + } + ] + } + }, + { + "name": "DeregisterNttArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nttProgramId", + "type": "publicKey" + } + ] + } + }, + { + "name": "RequestRelayArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gasDropoff", + "type": "u64" + }, + { + "name": "maxFee", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateSolPriceArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "solPrice", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateChainPricesArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nativePrice", + "type": "u64" + }, + { + "name": "gasPrice", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateChainParamsArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxGasDropoff", + "type": "u64" + }, + { + "name": "basePrice", + "type": "u64" + } + ] + } + } + ], + "errors": [ + { + "code": 6001, + "name": "ExceedsUserMaxFee", + "msg": "Relay fees exceeds specified max" + }, + { + "code": 6002, + "name": "ExceedsMaxGasDropoff", + "msg": "Requested gas dropoff exceeds max allowed for chain" + }, + { + "code": 6003, + "name": "InvalidFeeRecipient", + "msg": "The specified fee recipient does not match the address in the instance accound" + }, + { + "code": 6004, + "name": "RelayingToChainDisabled", + "msg": "Relaying to the specified chain is disabled" + }, + { + "code": 6005, + "name": "OutboxItemNotReleased", + "msg": "Relaying to the specified chain is disabled" + }, + { + "code": 6006, + "name": "ScalingOverflow", + "msg": "Scaled value exceeds u64::MAX" + }, + { + "code": 6007, + "name": "DivByZero", + "msg": "Cannot divide by zero" + }, + { + "code": 6257, + "name": "FeeRecipientCannotBeDefault", + "msg": "The fee recipient cannot be the default address (0x0)" + }, + { + "code": 6258, + "name": "NotAuthorized", + "msg": "Must be owner or assistant" + }, + { + "code": 6259, + "name": "PriceCannotBeZero", + "msg": "The price cannot be zero" + } + ] +}; + +export const IDL: NttQuoter = { + "version": "2.0.0", + "name": "ntt_quoter", + "instructions": [ + { + "name": "requestRelay", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": false, + "isSigner": false + }, + { + "name": "outboxItem", + "isMut": false, + "isSigner": false, + "docs": [ + "and checking the release constraint into a single function" + ] + }, + { + "name": "relayRequest", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RequestRelayArgs" + } + } + ] + }, + { + "name": "closeRelay", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": true, + "isSigner": false + }, + { + "name": "relayRequest", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initialize", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": false, + "isSigner": false + }, + { + "name": "programData", + "isMut": true, + "isSigner": false, + "docs": [ + "We use the program data to make sure this owner is the upgrade authority (the true owner,", + "who deployed this program)." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setAssistant", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "assistant", + "isMut": false, + "isSigner": false, + "isOptional": true + } + ], + "args": [] + }, + { + "name": "setFeeRecipient", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "registerChain", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RegisterChainArgs" + } + } + ] + }, + { + "name": "registerNtt", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RegisterNttArgs" + } + } + ] + }, + { + "name": "deregisterNtt", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredNtt", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "DeregisterNttArgs" + } + } + ] + }, + { + "name": "updateSolPrice", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateSolPriceArgs" + } + } + ] + }, + { + "name": "updateChainPrices", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateChainPricesArgs" + } + } + ] + }, + { + "name": "updateChainParams", + "accounts": [ + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "instance", + "isMut": false, + "isSigner": false + }, + { + "name": "registeredChain", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateChainParamsArgs" + } + } + ] + } + ], + "accounts": [ + { + "name": "instance", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "publicKey" + }, + { + "name": "assistant", + "type": "publicKey" + }, + { + "name": "feeRecipient", + "type": "publicKey" + }, + { + "name": "solPrice", + "type": "u64" + } + ] + } + }, + { + "name": "registeredChain", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "maxGasDropoff", + "type": "u64" + }, + { + "name": "basePrice", + "type": "u64" + }, + { + "name": "nativePrice", + "type": "u64" + }, + { + "name": "gasPrice", + "type": "u64" + } + ] + } + }, + { + "name": "registeredNtt", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "wormholeTransceiverIndex", + "type": "u8" + }, + { + "name": "gasCost", + "type": "u32" + } + ] + } + }, + { + "name": "relayRequest", + "type": { + "kind": "struct", + "fields": [ + { + "name": "requestedGasDropoff", + "type": "u64" + } + ] + } + } + ], + "types": [ + { + "name": "RegisterChainArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + } + ] + } + }, + { + "name": "RegisterNttArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nttProgramId", + "type": "publicKey" + }, + { + "name": "wormholeTransceiverIndex", + "type": "u8" + }, + { + "name": "gasCost", + "type": "u32" + } + ] + } + }, + { + "name": "DeregisterNttArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nttProgramId", + "type": "publicKey" + } + ] + } + }, + { + "name": "RequestRelayArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gasDropoff", + "type": "u64" + }, + { + "name": "maxFee", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateSolPriceArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "solPrice", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateChainPricesArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nativePrice", + "type": "u64" + }, + { + "name": "gasPrice", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateChainParamsArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxGasDropoff", + "type": "u64" + }, + { + "name": "basePrice", + "type": "u64" + } + ] + } + } + ], + "errors": [ + { + "code": 6001, + "name": "ExceedsUserMaxFee", + "msg": "Relay fees exceeds specified max" + }, + { + "code": 6002, + "name": "ExceedsMaxGasDropoff", + "msg": "Requested gas dropoff exceeds max allowed for chain" + }, + { + "code": 6003, + "name": "InvalidFeeRecipient", + "msg": "The specified fee recipient does not match the address in the instance accound" + }, + { + "code": 6004, + "name": "RelayingToChainDisabled", + "msg": "Relaying to the specified chain is disabled" + }, + { + "code": 6005, + "name": "OutboxItemNotReleased", + "msg": "Relaying to the specified chain is disabled" + }, + { + "code": 6006, + "name": "ScalingOverflow", + "msg": "Scaled value exceeds u64::MAX" + }, + { + "code": 6007, + "name": "DivByZero", + "msg": "Cannot divide by zero" + }, + { + "code": 6257, + "name": "FeeRecipientCannotBeDefault", + "msg": "The fee recipient cannot be the default address (0x0)" + }, + { + "code": 6258, + "name": "NotAuthorized", + "msg": "Must be owner or assistant" + }, + { + "code": 6259, + "name": "PriceCannotBeZero", + "msg": "The price cannot be zero" + } + ] +}; diff --git a/sdk/solana/src/anchor-idl/2_0_0/wormhole_governance.json b/sdk/solana/src/anchor-idl/2_0_0/wormhole_governance.json new file mode 100644 index 000000000..1561e7350 --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0/wormhole_governance.json @@ -0,0 +1,76 @@ +{ + "version": "2.0.0", + "name": "wormhole_governance", + "instructions": [ + { + "name": "governance", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "governance", + "isMut": true, + "isSigner": false, + "docs": [ + "governed program." + ] + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "replay", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "ReplayProtection", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidGovernanceChain", + "msg": "InvalidGovernanceChain" + }, + { + "code": 6001, + "name": "InvalidGovernanceEmitter", + "msg": "InvalidGovernanceEmitter" + }, + { + "code": 6002, + "name": "InvalidGovernanceProgram", + "msg": "InvalidGovernanceProgram" + } + ] +} \ No newline at end of file diff --git a/sdk/solana/src/anchor-idl/2_0_0/wormhole_governance.ts b/sdk/solana/src/anchor-idl/2_0_0/wormhole_governance.ts new file mode 100644 index 000000000..b988f040d --- /dev/null +++ b/sdk/solana/src/anchor-idl/2_0_0/wormhole_governance.ts @@ -0,0 +1,153 @@ +export type WormholeGovernance = { + "version": "2.0.0", + "name": "wormhole_governance", + "instructions": [ + { + "name": "governance", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "governance", + "isMut": true, + "isSigner": false, + "docs": [ + "governed program." + ] + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "replay", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "replayProtection", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidGovernanceChain", + "msg": "InvalidGovernanceChain" + }, + { + "code": 6001, + "name": "InvalidGovernanceEmitter", + "msg": "InvalidGovernanceEmitter" + }, + { + "code": 6002, + "name": "InvalidGovernanceProgram", + "msg": "InvalidGovernanceProgram" + } + ] +}; + +export const IDL: WormholeGovernance = { + "version": "2.0.0", + "name": "wormhole_governance", + "instructions": [ + { + "name": "governance", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "governance", + "isMut": true, + "isSigner": false, + "docs": [ + "governed program." + ] + }, + { + "name": "vaa", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "replay", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "replayProtection", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidGovernanceChain", + "msg": "InvalidGovernanceChain" + }, + { + "code": 6001, + "name": "InvalidGovernanceEmitter", + "msg": "InvalidGovernanceEmitter" + }, + { + "code": 6002, + "name": "InvalidGovernanceProgram", + "msg": "InvalidGovernanceProgram" + } + ] +}; diff --git a/sdk/solana/src/anchor-idl/index.ts b/sdk/solana/src/anchor-idl/index.ts index 6be35f4b7..926c925ce 100644 --- a/sdk/solana/src/anchor-idl/index.ts +++ b/sdk/solana/src/anchor-idl/index.ts @@ -1,4 +1,5 @@ export * from "./1_0_0.js"; +export * from "./2_0_0.js"; // This is a workaround for the fact that the anchor idl doesn't support generics // yet. This type is used to remove the generics from the idl types. diff --git a/sdk/solana/src/bindings.ts b/sdk/solana/src/bindings.ts index 20a697550..66e33c1c6 100644 --- a/sdk/solana/src/bindings.ts +++ b/sdk/solana/src/bindings.ts @@ -1,17 +1,18 @@ import { IdlAccounts, Program } from "@coral-xyz/anchor"; -import { OmitGenerics, _1_0_0 } from "./anchor-idl/index.js"; +import { OmitGenerics, _1_0_0, _2_0_0 } from "./anchor-idl/index.js"; import { Connection } from "@solana/web3.js"; export const IdlVersions = { "1.0.0": _1_0_0, - default: _1_0_0, + "2.0.0": _2_0_0, + default: _2_0_0, } as const; export type IdlVersion = keyof typeof IdlVersions; export namespace NttBindings { export type NativeTokenTransfer = - OmitGenerics<_1_0_0.RawExampleNativeTokenTransfers>; - export type Quoter = OmitGenerics<_1_0_0.RawNttQuoter>; + OmitGenerics<_2_0_0.RawExampleNativeTokenTransfers>; + export type Quoter = OmitGenerics<_2_0_0.RawNttQuoter>; export type Config = IdlAccounts["config"]; export type InboxItem = diff --git a/solana/Cargo.lock b/solana/Cargo.lock index 541661c36..4424e6667 100644 --- a/solana/Cargo.lock +++ b/solana/Cargo.lock @@ -1394,7 +1394,7 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "dummy-transfer-hook" -version = "0.1.0" +version = "2.0.0" dependencies = [ "anchor-lang", "anchor-spl", @@ -1553,7 +1553,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "example-native-token-transfers" -version = "1.0.0" +version = "2.0.0" dependencies = [ "anchor-lang", "anchor-spl", @@ -2476,7 +2476,7 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "ntt-messages" -version = "1.0.0" +version = "2.0.0" dependencies = [ "anchor-lang", "hex", @@ -2486,7 +2486,7 @@ dependencies = [ [[package]] name = "ntt-quoter" -version = "1.0.0" +version = "2.0.0" dependencies = [ "anchor-lang", "cfg-if", @@ -6392,7 +6392,7 @@ dependencies = [ [[package]] name = "wormhole-governance" -version = "1.0.0" +version = "2.0.0" dependencies = [ "anchor-lang", "hex", diff --git a/solana/Makefile b/solana/Makefile index 1fd77ae03..1389adfea 100644 --- a/solana/Makefile +++ b/solana/Makefile @@ -25,6 +25,7 @@ idl: target/idl/example_native_token_transfers.json @ mkdir -p $@/ts @ cp -r target/idl/* $@/json/ @ cp -r target/types/* $@/ts/ + @ cd ../sdk/solana && npm run generate node_modules: package-lock.json npm ci diff --git a/solana/idl/json/dummy_transfer_hook.json b/solana/idl/json/dummy_transfer_hook.json index 27f37cb16..2f6864c36 100644 --- a/solana/idl/json/dummy_transfer_hook.json +++ b/solana/idl/json/dummy_transfer_hook.json @@ -1,5 +1,5 @@ { - "version": "0.1.0", + "version": "2.0.0", "name": "dummy_transfer_hook", "instructions": [ { diff --git a/solana/idl/json/example_native_token_transfers.json b/solana/idl/json/example_native_token_transfers.json index 0feeff52c..82369389d 100644 --- a/solana/idl/json/example_native_token_transfers.json +++ b/solana/idl/json/example_native_token_transfers.json @@ -1,5 +1,5 @@ { - "version": "1.0.0", + "version": "2.0.0", "name": "example_native_token_transfers", "instructions": [ { diff --git a/solana/idl/json/ntt_quoter.json b/solana/idl/json/ntt_quoter.json index c756bd654..d6c6f2f7a 100644 --- a/solana/idl/json/ntt_quoter.json +++ b/solana/idl/json/ntt_quoter.json @@ -1,5 +1,5 @@ { - "version": "1.0.0", + "version": "2.0.0", "name": "ntt_quoter", "instructions": [ { diff --git a/solana/idl/json/wormhole_governance.json b/solana/idl/json/wormhole_governance.json index 895f58575..1561e7350 100644 --- a/solana/idl/json/wormhole_governance.json +++ b/solana/idl/json/wormhole_governance.json @@ -1,5 +1,5 @@ { - "version": "1.0.0", + "version": "2.0.0", "name": "wormhole_governance", "instructions": [ { diff --git a/solana/idl/ts/dummy_transfer_hook.ts b/solana/idl/ts/dummy_transfer_hook.ts index 4e068844b..25e31bff3 100644 --- a/solana/idl/ts/dummy_transfer_hook.ts +++ b/solana/idl/ts/dummy_transfer_hook.ts @@ -1,5 +1,5 @@ export type DummyTransferHook = { - "version": "0.1.0", + "version": "2.0.0", "name": "dummy_transfer_hook", "instructions": [ { @@ -110,7 +110,7 @@ export type DummyTransferHook = { }; export const IDL: DummyTransferHook = { - "version": "0.1.0", + "version": "2.0.0", "name": "dummy_transfer_hook", "instructions": [ { diff --git a/solana/idl/ts/example_native_token_transfers.ts b/solana/idl/ts/example_native_token_transfers.ts index a8ea1f346..4a7fceeeb 100644 --- a/solana/idl/ts/example_native_token_transfers.ts +++ b/solana/idl/ts/example_native_token_transfers.ts @@ -1,5 +1,5 @@ export type ExampleNativeTokenTransfers = { - "version": "1.0.0", + "version": "2.0.0", "name": "example_native_token_transfers", "instructions": [ { @@ -2043,7 +2043,7 @@ export type ExampleNativeTokenTransfers = { }; export const IDL: ExampleNativeTokenTransfers = { - "version": "1.0.0", + "version": "2.0.0", "name": "example_native_token_transfers", "instructions": [ { diff --git a/solana/idl/ts/ntt_quoter.ts b/solana/idl/ts/ntt_quoter.ts index 298ae9042..063f2ec30 100644 --- a/solana/idl/ts/ntt_quoter.ts +++ b/solana/idl/ts/ntt_quoter.ts @@ -1,5 +1,5 @@ export type NttQuoter = { - "version": "1.0.0", + "version": "2.0.0", "name": "ntt_quoter", "instructions": [ { @@ -588,7 +588,7 @@ export type NttQuoter = { }; export const IDL: NttQuoter = { - "version": "1.0.0", + "version": "2.0.0", "name": "ntt_quoter", "instructions": [ { diff --git a/solana/idl/ts/wormhole_governance.ts b/solana/idl/ts/wormhole_governance.ts index d3ccfd9bb..b988f040d 100644 --- a/solana/idl/ts/wormhole_governance.ts +++ b/solana/idl/ts/wormhole_governance.ts @@ -1,5 +1,5 @@ export type WormholeGovernance = { - "version": "1.0.0", + "version": "2.0.0", "name": "wormhole_governance", "instructions": [ { @@ -76,7 +76,7 @@ export type WormholeGovernance = { }; export const IDL: WormholeGovernance = { - "version": "1.0.0", + "version": "2.0.0", "name": "wormhole_governance", "instructions": [ { diff --git a/solana/modules/ntt-messages/Cargo.toml b/solana/modules/ntt-messages/Cargo.toml index cf6f5468c..b3443dd67 100644 --- a/solana/modules/ntt-messages/Cargo.toml +++ b/solana/modules/ntt-messages/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ntt-messages" -version = "1.0.0" +version = "2.0.0" edition = "2021" [features] diff --git a/solana/programs/dummy-transfer-hook/Cargo.toml b/solana/programs/dummy-transfer-hook/Cargo.toml index 6f78242db..9e408629d 100644 --- a/solana/programs/dummy-transfer-hook/Cargo.toml +++ b/solana/programs/dummy-transfer-hook/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dummy-transfer-hook" -version = "0.1.0" +version = "2.0.0" description = "Created with Anchor" edition = "2021" diff --git a/solana/programs/example-native-token-transfers/Cargo.toml b/solana/programs/example-native-token-transfers/Cargo.toml index 9a131c741..c248cb6c0 100644 --- a/solana/programs/example-native-token-transfers/Cargo.toml +++ b/solana/programs/example-native-token-transfers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "example-native-token-transfers" -version = "1.0.0" +version = "2.0.0" description = "Example implementation of native token transfer standard" edition = "2021" diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 6521acf1e..693a890be 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -62,7 +62,7 @@ pub const TOKEN_AUTHORITY_SEED: &[u8] = b"token_authority"; /// user, atomically). pub const SESSION_AUTHORITY_SEED: &[u8] = b"session_authority"; -pub const VERSION: &str = "1.0.0"; +pub const VERSION: &str = "2.0.0"; #[program] pub mod example_native_token_transfers { diff --git a/solana/programs/ntt-quoter/Cargo.toml b/solana/programs/ntt-quoter/Cargo.toml index fcc3ba281..ee06ec7ae 100644 --- a/solana/programs/ntt-quoter/Cargo.toml +++ b/solana/programs/ntt-quoter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ntt-quoter" -version = "1.0.0" +version = "2.0.0" edition = "2021" [lib] diff --git a/solana/programs/wormhole-governance/Cargo.toml b/solana/programs/wormhole-governance/Cargo.toml index 93a1b664d..0d384e87e 100644 --- a/solana/programs/wormhole-governance/Cargo.toml +++ b/solana/programs/wormhole-governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wormhole-governance" -version = "1.0.0" +version = "2.0.0" description = "Governance for programs controlled by Wormhole Guardians" edition = "2021" diff --git a/solana/scripts/program-version b/solana/scripts/program-version new file mode 100755 index 000000000..08c3e93f4 --- /dev/null +++ b/solana/scripts/program-version @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# cd to the solana root (one level up from the script location) +cd "$(dirname "$0")"/.. + +version= + +# grab all lib.rs files that export a VERSION constant +# and ensure there is exactly one +for lib in $(find programs modules -name lib.rs); do + if grep -q "pub const VERSION" $lib; then + if [[ -n $version ]]; then + echo "Error: multiple versions found" >&2 + exit 1 + fi + version=$(grep "pub const VERSION" $lib | cut -d '"' -f 2) + echo "$version" + fi +done + +if [[ -z $version ]]; then + echo "Error: version not found" >&2 + exit 1 +fi diff --git a/solana/scripts/sync-versions b/solana/scripts/sync-versions new file mode 100755 index 000000000..ac14841a4 --- /dev/null +++ b/solana/scripts/sync-versions @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# cd to the solana root (one level up from the script location) +cd "$(dirname "$0")"/.. + +# check if --check is passed (in a way that doesn't complain about unbound variables) +check=false +if [[ "${1:-}" == "--check" ]]; then + check=true +fi + +version=$(./scripts/program-version) +failed=0 + +# update Cargo.toml versions in all Cargo.toml files in the `programs` and +# `modules` directories +for cargo in $(find programs modules -name Cargo.toml); do + if $check; then + if ! grep -q "version = \"$version\"" $cargo; then + echo "Error: $cargo is out of date" >&2 + failed=$((failed + 1)) + fi + else + # NOTE: we don't use sed -i because it's not portable + cp $cargo $cargo.bak + sed "s/^version = .*/version = \"$version\"/" $cargo.bak > $cargo + if ! diff $cargo $cargo.bak > /dev/null; then + echo "Updated $cargo" + fi + rm $cargo.bak + fi +done + +if [[ $failed -gt 0 ]]; then + echo "Run $0 to update versions" >&2 + exit 1 +fi From d2a4f763e1a2b420f7127ef36d69b627dc2a63d3 Mon Sep 17 00:00:00 2001 From: Ben Guidarelli Date: Fri, 7 Jun 2024 15:30:54 -0400 Subject: [PATCH 8/8] partial update to hardcoded list with new manager addresses (#453) --- Tiltfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tiltfile b/Tiltfile index 28df548f4..3c6010134 100644 --- a/Tiltfile +++ b/Tiltfile @@ -2,7 +2,7 @@ load('ext://namespace', 'namespace_create', 'namespace_inject') load('ext://git_resource', 'git_checkout') git_checkout('https://github.com/wormhole-foundation/wormhole.git#main', '.wormhole/', unsafe_mode=True) -local(['sed','-i','/{chainId: vaa.ChainIDEthereum, addr: "000000000000000000000000855FA758c77D68a04990E992aA4dcdeF899F654A"},/i {chainId: vaa.ChainIDSolana, addr: "8bf0b547c96edc5c1d512ca25c5c1d1812a180438a0046e511d1fb61561d5cdf"},{chainId: vaa.ChainIDSolana, addr: "0a490691c21334ca173d9ce386e2a86774ce173f351db10d5d0cccc5c4875376"},{chainId: vaa.ChainIDEthereum, addr: "0000000000000000000000006f84742680311cef5ba42bc10a71a4708b4561d1"},{chainId: vaa.ChainIDEthereum, addr: "000000000000000000000000c3ef4965b788cc4b905084d01f2eb7d4b6e93abf"},{chainId: vaa.ChainIDBSC, addr: "0000000000000000000000006f84742680311cef5ba42bc10a71a4708b4561d1"},{chainId: vaa.ChainIDBSC, addr: "0000000000000000000000003f4e941ef5071a1d09c2eb4a24da1fc43f76fcff"},', '.wormhole/node/pkg/accountant/ntt_config.go']) +local(['sed','-i','/{chainId: vaa.ChainIDEthereum, addr: "000000000000000000000000855FA758c77D68a04990E992aA4dcdeF899F654A"},/i {chainId: vaa.ChainIDSolana, addr: "8bf0b547c96edc5c1d512ca25c5c1d1812a180438a0046e511d1fb61561d5cdf"},{chainId: vaa.ChainIDSolana, addr: "0a490691c21334ca173d9ce386e2a86774ce173f351db10d5d0cccc5c4875376"},{chainId: vaa.ChainIDEthereum, addr: "00000000000000000000000042D4BA5e542d9FeD87EA657f0295F1968A61c00A"},{chainId: vaa.ChainIDEthereum, addr: "000000000000000000000000c3ef4965b788cc4b905084d01f2eb7d4b6e93abf"},{chainId: vaa.ChainIDBSC, addr: "000000000000000000000000C5aFE31AE505594B190AC71EA689B58139d1C354"},{chainId: vaa.ChainIDBSC, addr: "0000000000000000000000003f4e941ef5071a1d09c2eb4a24da1fc43f76fcff"},', '.wormhole/node/pkg/accountant/ntt_config.go']) load(".wormhole/Tiltfile", "namespace", "k8s_yaml_with_ns")