# Send & Receive GMP Messages (https://docs.axelar.dev/gmp/integrations/solana/gmp-messages)



This guide walks through building a Solana program that can send and receive cross-chain messages via Axelar's General Message Passing (GMP) protocol.

For prerequisites and setup, see the [Introduction](https://docs.axelar.dev/llms.mdx/gmp/integrations/solana/intro/content.md).

## Receive Cross-Chain Messages [#receive-cross-chain-messages]

When a cross-chain message arrives on Solana, the Axelar relayer calls your program's `execute` instruction. Your program must validate the message via CPI to the Gateway and then process the payload.

### Step 1: Define Your Accounts Struct [#step-1-define-your-accounts-struct]

Use the `executable_accounts!` macro to generate the `AxelarExecuteAccounts` struct, then embed it in your own accounts struct. The argument you pass to the macro is the name of your outer struct — it can be any name you choose:

```rust
use anchor_lang::prelude::*;
use solana_axelar_gateway::{executable::*, executable_accounts};

// Generate AxelarExecuteAccounts and wire it to your struct
executable_accounts!(MyGmpExecute);

#[derive(Accounts)]
pub struct MyGmpExecute<'info> {
    // Gateway accounts for message validation (generated by the macro)
    pub executable: AxelarExecuteAccounts<'info>,

    // Your program's accounts
    #[account(mut)]
    pub my_state: Account<'info, MyState>,
}
```

The macro generates `AxelarExecuteAccounts` with:

* `incoming_message_pda` — The approved message PDA (derived from the command ID)
* `signing_pda` — Validates the message signer
* `gateway_root_pda` — Gateway configuration
* `event_authority` — For Gateway event emission
* `axelar_gateway_program` — Reference to the Gateway program

<Callout emoji="💡">
  Do **not** add `#[instruction(message: Message, payload: Vec<u8>)]` to this struct. The macro's inner struct already declares the `message` instruction parameter. Adding it again causes [an Anchor bug](https://github.com/solana-foundation/anchor/issues/2942).
</Callout>

### Step 2: Implement the Execute Handler [#step-2-implement-the-execute-handler]

Your handler must call `validate_message()` **before** processing any payload data. This performs a CPI to the Gateway to verify the message was approved and marks it as executed.

```rust
pub fn execute_handler(
    ctx: Context<MyGmpExecute>,
    message: Message,
    payload: Vec<u8>,
    encoding_scheme: ExecutablePayloadEncodingScheme,
) -> Result<()> {
    // Validate the message via CPI to the Gateway.
    // This verifies the message was approved and marks it as executed.
    validate_message(ctx.accounts, message, &payload, encoding_scheme)?;

    // The payload contains your application-specific data.
    // Decode it according to your protocol — for example, as a UTF-8 string:
    let data = std::str::from_utf8(&payload)
        .map_err(|_| ProgramError::InvalidInstructionData)?;
    // Or as a custom Borsh-encoded struct:
    // let data = MyPayload::try_from_slice(&payload)?;

    // Your business logic here...

    Ok(())
}
```

The `validate_message` function:

1. Reconstructs the full payload (data + account metadata) from the instruction accounts.
2. Verifies the payload hash matches the approved message.
3. Calls the Gateway's `validate_message` instruction via CPI, which marks the message as `executed`.

### Check the sender [#check-the-sender]

<Callout emoji="⚠️" title="Validation doesn't check who sent the message">
  The Gateway proves that a message came through Axelar. It doesn't prove that
  it came from your contract on the other chain: anyone can send a message to
  your program. Unless your program accepts messages from anyone, check the
  message's source chain and source address.
</Callout>

The source chain is `message.cc_id.chain` and the source address is `message.source_address`, a string in the source chain's format: EVM addresses arrive [EIP-55](https://eips.ethereum.org/EIPS/eip-55) checksummed. Compare them with values your program stores, e.g. before calling `validate_message`, which takes the message:

```rust
require!(
    message.cc_id.chain == ctx.accounts.my_state.trusted_chain
        && message.source_address == ctx.accounts.my_state.trusted_address,
    MyError::UntrustedSender
);
```

If your handler fails, the whole transaction reverts, including the validation, so the message stays approved and can be executed again.

### Step 3: Register the Instruction [#step-3-register-the-instruction]

The Axelar relayer looks for a specific instruction discriminator when calling your program. You must either name the instruction exactly `execute` (so Anchor generates the matching discriminator), or use the `EXECUTE_IX_DISC` constant to override the discriminator on a differently-named instruction.

**Option A:** Name it `execute`:

```rust
#[program]
pub mod my_program {
    use super::*;

    pub fn execute(
        ctx: Context<MyGmpExecute>,
        message: Message,
        payload: Vec<u8>,
        encoding_scheme: ExecutablePayloadEncodingScheme,
    ) -> Result<()> {
        instructions::execute_handler(ctx, message, payload, encoding_scheme)
    }
}
```

**Option B:** Use a custom name with the discriminator override:

```rust
use solana_axelar_gateway::executable::EXECUTE_IX_DISC;

#[instruction(discriminator = EXECUTE_IX_DISC)]
pub fn process_gmp_message(
    ctx: Context<MyGmpExecute>,
    message: Message,
    payload: Vec<u8>,
    encoding_scheme: ExecutablePayloadEncodingScheme,
) -> Result<()> {
    instructions::execute_handler(ctx, message, payload, encoding_scheme)
}
```

<Callout emoji="💡">
  The `executable_accounts!` macro is a convenience helper that generates the required account struct and trait implementations. If the macro does not fit your setup, you can copy the generated code from [`solana-axelar-gateway/src/executable.rs`](https://github.com/axelarnetwork/axelar-amplifier-solana/blob/main/programs/solana-axelar-gateway/src/executable.rs) and use it directly.
</Callout>

## Send Cross-Chain Messages [#send-cross-chain-messages]

Sending a cross-chain message from your program involves two CPI calls within a single instruction: first pay for gas, then call the Gateway. Both happen in the same instruction handler.

### Accounts [#accounts]

Your send instruction needs accounts for the Gas Service, the Gateway, and a signing PDA:

```rust
use solana_axelar_gateway::{
    cpi::accounts::CallContract,
    program::SolanaAxelarGateway,
    CallContractSigner,
};
use solana_axelar_gas_service::program::SolanaAxelarGasService;

#[derive(Accounts)]
pub struct SendMessage<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,

    /// Reference to this program
    pub my_program: Program<'info, crate::program::MyProgram>,

    /// Signing PDA derived from this program's ID
    /// CHECK: validated by the Gateway
    #[account(
        seeds = [CallContractSigner::SEED_PREFIX],
        bump,
    )]
    pub signing_pda: AccountInfo<'info>,

    // Gateway
    /// CHECK: checked by the gateway program
    pub gateway_root_pda: UncheckedAccount<'info>,
    /// CHECK: checked by the gateway program
    pub gateway_event_authority: UncheckedAccount<'info>,
    pub gateway_program: Program<'info, SolanaAxelarGateway>,

    // Gas Service
    /// CHECK: checked by the gas service program
    #[account(mut)]
    pub gas_treasury: UncheckedAccount<'info>,
    pub gas_service: Program<'info, SolanaAxelarGasService>,
    /// CHECK: checked by the gas service program
    pub gas_event_authority: UncheckedAccount<'info>,

    pub system_program: Program<'info, System>,
}
```

### Handler [#handler]

The handler pays gas first, then sends the message — both via CPI in a single instruction. `gas_amount` is in lamports; get it from an [off-chain estimate](https://docs.axelar.dev/llms.mdx/gmp/gas/estimate-and-pay/content.md#estimate-the-fee) with `solana` as the source chain. Without gas, the message isn't relayed.

```rust
pub fn send_message_handler(
    ctx: Context<SendMessage>,
    destination_chain: String,
    destination_address: String,
    payload: Vec<u8>,
    gas_amount: u64,
    refund_address: Pubkey,
) -> Result<()> {
    // 1. Pay gas
    let payload_hash = solana_keccak_hasher::hash(&payload).to_bytes();

    let gas_cpi_accounts = solana_axelar_gas_service::cpi::accounts::PayGas {
        sender: ctx.accounts.payer.to_account_info(),
        treasury: ctx.accounts.gas_treasury.to_account_info(),
        system_program: ctx.accounts.system_program.to_account_info(),
        event_authority: ctx.accounts.gas_event_authority.to_account_info(),
        program: ctx.accounts.gas_service.to_account_info(),
    };

    solana_axelar_gas_service::cpi::pay_gas(
        CpiContext::new(ctx.accounts.gas_service.to_account_info(), gas_cpi_accounts),
        destination_chain.clone(),
        destination_address.clone(),
        payload_hash,
        gas_amount,
        refund_address,
    )?;

    // 2. Send the message
    let bump = ctx.bumps.signing_pda;
    let signer_seeds = &[CallContractSigner::SEED_PREFIX, &[bump]];
    let signer_seeds = &[&signer_seeds[..]];

    let call_cpi_accounts = CallContract {
        caller: ctx.accounts.my_program.to_account_info(),
        signing_pda: Some(ctx.accounts.signing_pda.to_account_info()),
        gateway_root_pda: ctx.accounts.gateway_root_pda.to_account_info(),
        event_authority: ctx.accounts.gateway_event_authority.to_account_info(),
        program: ctx.accounts.gateway_program.to_account_info(),
    };

    solana_axelar_gateway::cpi::call_contract(
        CpiContext::new_with_signer(
            ctx.accounts.gateway_program.key(),
            call_cpi_accounts,
            signer_seeds,
        ),
        destination_chain,
        destination_address,
        payload,
        bump,
    )?;

    Ok(())
}
```

<Callout emoji="💡">
  The `signing_pda` is derived from the seed `b"gtw-call-contract"` (`CallContractSigner::SEED_PREFIX`) and your program's ID. The Gateway uses this to verify that your program authorized the call. Programs on Solana don't possess private keys, so they sign via PDAs.
</Callout>

<Callout emoji="💡">
  The Gateway also supports calling `call_contract` directly from a wallet (without a signing PDA). In that case, `signing_pda` is set to `None` and the wallet signs the transaction directly. See the [Contract Reference](https://docs.axelar.dev/llms.mdx/gmp/integrations/solana/gmp-contracts/content.md#calling-from-a-direct-wallet) for details.
</Callout>

## Payload Encoding [#payload-encoding]

Cross-chain payloads sent to Solana can include both data and account metadata. The account metadata tells the relayer which Solana accounts to pass to the destination program.

```rust
use solana_axelar_gateway::executable::{ExecutablePayload, ExecutablePayloadEncodingScheme};
use anchor_lang::solana_program::instruction::AccountMeta;

// Encode your data with account metadata
let payload = ExecutablePayload::new(
    &your_data_bytes,
    &vec![
        AccountMeta::new(some_writable_account, false),
        AccountMeta::new_readonly(some_readonly_account, false),
    ],
    ExecutablePayloadEncodingScheme::Borsh,
);

let encoded = payload.encode()?;
```

The accounts specified in the payload become `remaining_accounts` in the destination program's execute handler. Two encoding schemes are supported:

* `ExecutablePayloadEncodingScheme::Borsh` — Standard Borsh serialization.
* `ExecutablePayloadEncodingScheme::AbiEncoding` — ABI-compatible encoding for interoperability with EVM chains.

<Callout emoji="💡">
  **Coming soon:** Documentation for constructing Solana-compatible payloads from EVM source chains (ABI encoding format for account metadata).
</Callout>

## Transaction Size Limits [#transaction-size-limits]

Solana caps every transaction at **1232 bytes**. After protocol overhead, your payload and the Solana accounts that travel with it share a fixed budget.

### Receiving [#receiving]

**612 bytes** are available, shared between your payload bytes and the accounts your destination program needs. Each account costs **33 bytes**, so you trade one against the other. Anything above the 1232-byte transaction cap will fail to deliver — split the work into multiple messages or pass less data.

**Examples** (numbers assume an EVM source contract — `0x` + 40 hex. Actual limits may differ depending on the source chain: messages from chains with longer addresses, like Cosmos bech32, reduce the budget by roughly the extra address bytes):

| Accounts on the destination | Max payload |
| --------------------------: | ----------: |
|                           0 |       612 B |
|                           3 |       514 B |
|                           5 |       448 B |
|                          10 |       283 B |
|                          15 |       118 B |

### Sending [#sending]

Calling `call_contract` directly from a wallet allows up to **868 bytes** of payload.

When sending from your own program via CPI, `call_contract` is invoked inside your instruction — your wrapping instruction's accounts and data share the same 1232-byte transaction budget with the payload.

## Complete Example [#complete-example]

For a working end-to-end implementation of both sending and receiving GMP messages, see the [Memo Program](https://github.com/axelarnetwork/axelar-amplifier-solana/tree/main/programs/solana-axelar-memo):

* [Sending a message](https://github.com/axelarnetwork/axelar-amplifier-solana/blob/main/programs/solana-axelar-memo/src/instructions/send_memo.rs)
* [Receiving a message](https://github.com/axelarnetwork/axelar-amplifier-solana/blob/main/programs/solana-axelar-memo/src/instructions/execute.rs)


---

All pages of these docs, as Markdown: https://docs.axelar.dev/llms.txt