> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Solana Composers

> Overview of Solana Composers on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure crosschain messaging.

Crosschain composability enables multi-step workflows that span multiple chains. LayerZero V2 supports composing follow-up actions as separate messages, improving reliability and flexibility for complex flows.

This page explains how to implement composability for Solana programs using the `lz_compose_types_v2` typed discovery flow.

## Prerequisites

* Familiarity with [Anchor](https://www.anchor-lang.com/) and Solana CPIs.
* Read the conceptual guide: [Omnichain Composers](/v2/concepts/applications/composer-standard)
* Understanding of [Solana OApps](/v2/developers/solana/oapp/overview) and the PDA used
* Understanding of the `lz_receive` v2 flow (see [lz\_receive\_types\_v2](/v2/developers/solana/oapp/overview#lz_receive_types_v2))

## Why composability matters

For a clear, high-level explanation of both the how and the why behind composed messaging, see the conceptual guide: [Omnichain Composers](/v2/concepts/applications/composer-standard).

## How composability works

A message may or may not contain a compose message. When it does, we refer to it as a **composed message**. The following is the workflow for a composed message when the destination chain is Solana.

A composed flow is split into distinct steps across messages:

1. **Sending Application**: The sender OApp sends a message with a `composeMsg` attached. For composed messages to Solana, the recipient address should be set to the **Composer PDA**.
2. **Receiving Application**: The destination OApp's `lz_receive` is executed, and since there is a `composeMsg`, a `send_compose` CPI is made to the Endpoint program to send the compose message to the Composer PDA.
3. **Composer Application**: The Executor calls `lz_compose` on the Composer Program (the program that owns the Composer PDA).

> A **Composer** is the smart contract that is responsible for executing a compose message.

This separation reduces call-stack complexity and allows non-critical reverts in the composed step without rolling back the initial receive.

## Installation

You can either extend your existing OApp or OFT program to turn it into a Composer, or create a standalone program to serve as the Composer.

In this example, we will scaffold a basic Solana OApp, and extend it to also be a Composer.

Use the CLI to scaffold a Solana OApp project you can extend with compose:

```bash wrap theme={null}
LZ_ENABLE_SOLANA_OAPP_EXAMPLE=1 npx create-lz-oapp@latest --example oapp-solana
```

## Usage

The accounts and instructions needed are similar to those outlined in [lz\_receive\_types\_v2](/v2/developers/solana/oapp/overview#lz_receive_types_v2):

* `composer` - a PDA that can be used to store static addresses needed for `lz_compose` execution, and more importantly, whose address is used as the 'Composer address'
* `LzComposeTypesAccounts` - a PDA that contains the accounts needed to call `lz_compose_types`
* `lz_compose_types_info`- an instruction that provides versioning info that helps the Executor understand how to proceed with `lz_compose_types`/`lz_compose_types_v2`
* `lz_compose_types_v2` - an instruction that returns the list of accounts and execution plan needed to execute `lz_compose`
* `lz_compose` - the instruction that contains the actual business logic that must be executed for a `composeMsg`

## `lz_compose_types_v2`

`lz_compose_types_v2` achieves similar goals to `lz_receive_types_v2` (supports more accounts, multiple instructions, multiple signers) but for compose messages. The flow is similar: discover versioned accounts via `lz_compose_types_info`, return a compact, ALT-aware execution plan via `lz_compose_types_v2`, then the Executor builds and submits the transaction that includes the `lz_compose` instruction.

Note that for this example, we will assume that the Composer is integrated into the OApp program itself. Whether you adopt this design as well depends on your use case. You may also choose to have a standalone Composer program.

### Implementing `lz_compose_types_v2`

#### Composer PDA

Define the composer PDA seed in your program's `lib.rs`:

```rust wrap theme={null}
const COMPOSER_SEED: &[u8] = b"Composer";
```

Define the struct of the PDA that will hold the static addresses or fields that will be used in `lz_compose_types` later:

```rust wrap theme={null}
#[account]
pub struct Composer {
    pub endpoint_program: Pubkey,
    // if you need to namespace your Composer PDA, you can add the identifier here
    // you can add other fields as needed
    pub bump: u8,
}

impl Composer {
    // 8 (discriminator) + 1 Pubkey + 1 bump
    pub const SIZE: usize = 8 + 1 * 32 + 1;
}

```

#### LzComposeTypesAccounts

Define the struct of PDA that holds versioned compose-type discovery data, e.g. `LzComposeTypesAccounts`:

```rust wrap theme={null}
/// LzComposeTypesAccounts includes accounts that are used in the LzComposeTypesV2 instruction.
#[account]
#[derive(InitSpace)]
pub struct LzComposeTypesAccounts {
    pub composer: Pubkey, // Note: The Composer PDA.
    // Example: You can also store a single ALT (or change to Vec<Pubkey> for many)
    pub alt: Pubkey,
    // You may add more Pubkeys here per your use case
    pub bump: u8,
}
```

Initialize the `LzComposeTypesAccounts` PDA in your `init` instruction:

```rust wrap theme={null}
use crate::{
    state::LzComposeTypesAccounts,
    STORE_SEED,
};
use anchor_lang::prelude::*;
use anchor_lang::solana_program::address_lookup_table::program::ID as ALT_PROGRAM_ID;
use oapp::{
    endpoint::{instructions::RegisterOAppParams},
    LZ_COMPOSE_TYPES_SEED,
};

#[derive(Accounts)]
pub struct Init<'info> {
    // .. existing accounts including payer, store, lz_receive_types_accounts and its ALT
    /// PDA holding all the static pubkeys for lz_compose execution
    #[account(
        init,
        payer = payer,
        space  = Composer::SIZE,
        seeds  = [COMPOSER_SEED], // Note: if the composer needs to be namespaced, add the identifier into the seed here
        bump
    )]
    pub composer: Account<'info, Composer>,
    #[account(
        init,
        payer = payer,
        space  = 8 + LzComposeTypesAccounts::INIT_SPACE,
        seeds  = [LZ_COMPOSE_TYPES_SEED, composer.key().as_ref()],
        bump
    )]
    pub lz_compose_types_accounts: Account<'info, LzComposeTypesAccounts>,
    // Note: For simplicity, we will use the same ALT for both lz_receive_types_accounts and lz_compose_types_accounts, but you can also accept two separate ALTs if you want the ability to specify them separately.
    // If you choose to specify them separately, then you can name them something like lz_receive_alt and lz_compose_alt, and just modify the instruction handler below to use the right account keys
    #[account(owner = ALT_PROGRAM_ID)]
    pub alt: Option<UncheckedAccount<'info>>, // optional.
    pub system_program: Program<'info, System>,
}

impl Init<'_> {
    pub fn apply(ctx: &mut Context<Init>, params: &InitParams) -> Result<()> {
        // existing code
        ctx.accounts.composer.endpoint_program = params.endpoint_program;
        ctx.accounts.composer.bump = ctx.bumps.composer;

        ctx.accounts.lz_compose_types_accounts.composer = ctx.accounts.composer.key();
        ctx.accounts.lz_compose_types_accounts.alt = ctx.accounts.alt.as_ref().map(|a| a.key()).unwrap_or_default();
        ctx.accounts.lz_compose_types_accounts.bump = ctx.bumps.lz_compose_types_accounts;
        // existing code
    }
}
```

* If you need the Composer PDA to be namespaced by a certain identifier, you can add it into its `seeds`. The convention is to also store that value in the Composer PDA itself, which requires amending its struct that was defined earlier.
* The above assumes that the existing `init` instruction that was already responsible for initializing `lz_receive_types_accounts` is extended to also initialize `lz_compose_types_accounts`
* The `init` function can be arbitrarily named, as long as it is called

#### lz\_compose\_types\_info

Create an `lz_compose_types_info` instruction that returns the version and the accounts needed to construct `lz_compose_types_v2`:

```rust wrap theme={null}
use oapp::{
    lz_compose_types_v2::{LzComposeTypesV2Accounts, LZ_COMPOSE_TYPES_VERSION},
    LzComposeParams, LZ_COMPOSE_TYPES_SEED,
};

use crate::*;

#[derive(Accounts)]
pub struct LzComposeTypesInfo<'info> {
    #[account(seeds = [COMPOSER_SEED], bump = composer.bump)]
    pub composer: Account<'info, Composer>,
    #[account(seeds = [LZ_COMPOSE_TYPES_SEED, composer.key().as_ref()], bump = lz_compose_types_accounts.bump)]
    pub lz_compose_types_accounts: Account<'info, LzComposeTypesAccounts>,
}

impl LzComposeTypesInfo<'_> {
    /// Returns (version, versioned_data) used by the Executor
    pub fn apply(
        ctx: &Context<LzComposeTypesInfo>,
        _params: &LzComposeParams,
    ) -> Result<(u8, LzComposeTypesV2Accounts)> {
        let composer = &ctx.accounts.composer;
        let compose_types_account = &ctx.accounts.lz_compose_types_accounts;
        let required_accounts = if compose_types_account.alt == Pubkey::default() {
            vec![
                // 1) composer PDA
                compose_types_account.composer
            ]
        } else {
            vec![
                // 1) composer PDA
                compose_types_account.composer,
                // ALT will be passed under remaining_accounts
                compose_types_account.alt
            ]
        };
        Ok((LZ_COMPOSE_TYPES_VERSION, LzComposeTypesV2Accounts { accounts: required_accounts }))
    }
}
```

#### lz\_compose\_types\_v2

Implement `lz_compose_types_v2` and return a compact execution plan including exactly one `Instruction::LzCompose`:

```rust wrap theme={null}
use crate::*;
use oapp::{
    common::{compact_accounts_with_alts, AccountMetaRef, AddressLocator, EXECUTION_CONTEXT_VERSION_1},
    lz_compose_types_v2::{get_accounts_for_clear_compose, Instruction, LzComposeTypesV2Result},
    LzComposeParams,
};

#[derive(Accounts)]
#[instruction(params: LzComposeParams)]
pub struct LzComposeTypesV2<'info> {
    // 1) Composer PDA
    #[account(seeds = [COMPOSER_SEED], bump = composer.bump)]
    pub composer: Account<'info, Composer>,
}

impl LzComposeTypesV2<'_> {
    /// Returns the execution plan for lz_compose with a minimal account set.
    pub fn apply(
        ctx: &Context<LzComposeTypesV2>,
        params: &LzComposeParams,
    ) -> Result<LzComposeTypesV2Result> {
        let mut accounts = vec![
            // 0) payer
            AccountMetaRef { pubkey: AddressLocator::Payer, is_writable: true },
            // 1) endpoint program
            AccountMetaRef { pubkey: ctx.accounts.composer.endpoint_program, is_writable: false },
            // 2) composer PDA
            AccountMetaRef { pubkey: ctx.accounts.composer.key().into(), is_writable: false },
        ];

        // Endpoint helper accounts for compose
        let accounts_for_composing = get_accounts_for_clear_compose(
            ctx.accounts.composer.endpoint_program,
            &params.from,
            &ctx.accounts.composer.key(),
            &params.guid,
            params.index,
            &params.message,
        );
        accounts.extend(accounts_for_composing);

        Ok(LzComposeTypesV2Result {
            context_version: EXECUTION_CONTEXT_VERSION_1,
            alts: ctx.remaining_accounts.iter().map(|alt| alt.key()).collect(),
            instructions: vec![
                Instruction::LzCompose {
                    // In this example, ALTs are passed in via remaining_accounts
                    // This decision allows for flexibility in terms of passing in any number of ALTs without needing to change the accounts struct
                    // However, if you need stronger schema guarantees and require only a single ALT, you may opt to have it passed in explicitly via ctx.accounts.alt (or similar)
                    accounts: compact_accounts_with_alts(&ctx.remaining_accounts, accounts)?,
                },
            ],
        })
    }
}
```

### Composed Message Execution Options

Longer composed flows increase the cost of executing `lz_receive` on the destination and the follow-up `lz_compose` call. You must set sufficient gas and value in your Message Options for both steps.

* Add extra gas for the lzReceive step:

```ts wrap theme={null}
// addExecutorLzReceiveOption(uint128 _gas, uint128 _value)
Options.newOptions().addExecutorLzReceiveOption(50000, 0);
```

* Also set gas/value for the composer execution:

```ts wrap theme={null}
// addExecutorLzComposeOption(uint16 _index, uint128 _gas, uint128 _value)
Options.newOptions().addExecutorLzReceiveOption(50000, 0).addExecutorLzComposeOption(0, 30000, 0);
```

Parameters:

* `_index`: the index of the `composeMsg`.
* `_gas`: gas/compute budget for the destination execution.
* `_value`: native value forwarded with the call if needed.

If insufficient limits are provided, execution will not proceed and a manual retry with higher limits will be required.

For an overview of how options work, see [Message Options](/v2/concepts/message-options).

### Composing an OFT

#### Sending an OFT from Solana with a compose message

The [OFT program (Solana)](/v2/developers/solana/oft/overview) supports attaching an optional [compose\_msg](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/instructions/send.rs#L172) to a crosschain send.

```rust wrap theme={null}
pub struct SendParams {
    pub dst_eid: u32,
    pub to: [u8; 32],
    pub amount_ld: u64,
    pub min_amount_ld: u64,
    pub options: Vec<u8>,
    pub compose_msg: Option<Vec<u8>>, // <---- Optional compose_msg param
    pub native_fee: u64,
    pub lz_token_fee: u64,
}

```

#### Handling a compose message for an OFT on Solana

The reference [Solana OFT program](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana/programs/oft)'s `lz_receive` already [includes a call to `Endpoint::send_compose`for when a message contains a `compose_msg`](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/instructions/lz_receive.rs#L155-L173) :

```rust wrap theme={null}
if let Some(message) = msg_codec::compose_msg(&params.message) {
    oapp::endpoint_cpi::send_compose(
        ctx.accounts.oft_store.endpoint_program,
        ctx.accounts.oft_store.key(),
    &ctx.remaining_accounts[Clear::MIN_ACCOUNTS_LEN..],
    seeds,
    SendComposeParams {
            to: ctx.accounts.to_address.key(),
            guid: params.guid,
            index: 0, // only 1 compose msg per lzReceive
            message: compose_msg_codec::encode(
                params.nonce,
                params.src_eid,
                amount_received_ld,
                &message,
            ),
    },
)?;
}
```

The OFT Program comes with a default [compose\_msg\_codec](https://github.com/LayerZero-Labs/devtools/blob/main/examples/oft-solana/programs/oft/src/compose_msg_codec.rs) that's used for sending and receiving composed messages:

```rust wrap theme={null}
const NONCE_OFFSET: usize = 0;
const SRC_EID_OFFSET: usize = 8;
const AMOUNT_LD_OFFSET: usize = 12;
const COMPOSE_FROM_OFFSET: usize = 20;
const COMPOSE_MSG_OFFSET: usize = 52;

pub fn encode(
    nonce: u64,
    src_eid: u32,
    amount_ld: u64,
    compose_msg: &Vec<u8>, // [composeFrom][composeMsg]
) -> Vec<u8> {
    let mut encoded = Vec::with_capacity(20 + compose_msg.len()); // 8 + 4 + 8
    encoded.extend_from_slice(&nonce.to_be_bytes());
    encoded.extend_from_slice(&src_eid.to_be_bytes());
    encoded.extend_from_slice(&amount_ld.to_be_bytes());
    encoded.extend_from_slice(&compose_msg);
    encoded
}

pub fn nonce(message: &[u8]) -> u64 {
    let mut nonce_bytes = [0; 8];
    nonce_bytes.copy_from_slice(&message[NONCE_OFFSET..SRC_EID_OFFSET]);
    u64::from_be_bytes(nonce_bytes)
}

pub fn src_eid(message: &[u8]) -> u32 {
    let mut src_eid_bytes = [0; 4];
    src_eid_bytes.copy_from_slice(&message[SRC_EID_OFFSET..AMOUNT_LD_OFFSET]);
    u32::from_be_bytes(src_eid_bytes)
}

pub fn amount_ld(message: &[u8]) -> u64 {
    let mut amount_ld_bytes = [0; 8];
    amount_ld_bytes.copy_from_slice(&message[AMOUNT_LD_OFFSET..COMPOSE_FROM_OFFSET]);
    u64::from_be_bytes(amount_ld_bytes)
}

pub fn compose_from(message: &[u8]) -> [u8; 32] {
    let mut compose_from = [0; 32];
    compose_from.copy_from_slice(&message[COMPOSE_FROM_OFFSET..COMPOSE_MSG_OFFSET]);
    compose_from
}

pub fn compose_msg(message: &[u8]) -> Vec<u8> {
    if message.len() > COMPOSE_MSG_OFFSET {
        message[COMPOSE_MSG_OFFSET..].to_vec()
    } else {
        Vec::new()
    }
}
```

The reference [Solana OFT program](https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana/programs/oft), however, does not include a dedicated `lz_compose` handler as the decision of whether to extend the OFT program or have a separate Composer program is for the developer to decide.

If your product needs compose flows, use `lz_compose_types_v2` to describe execution and choose one of these patterns:

* Create a separate composer program (recommended for separation of concerns).
* Modify the OFT program to also act as the composer (tighter coupling).

### Composing an OApp

As shown above in ["Composing an OFT"](#composing-an-oft), the `send_compose` call is issued from within `lz_receive` after `Endpoint::clear`. Custom OApps follow the same pattern: detect a compose payload, then forward it via `send_compose` to your Composer PDA (or destination PDA). Ensure your discovery accounts stay in sync with the execution plan described by [`lz_compose_types_v2`](#lz_compose_types_v2). For general receive flow details, see OApp [lz\_receive — business logic + Endpoint::clear](/v2/developers/solana/oapp/overview).

## Execution notes and troubleshooting

* Ensure `lz_receive_types` and `lz_compose_types` PDAs are initialized and kept in sync with your handlers.
* Always call `Endpoint::clear` before touching user state in `lz_receive`.
* The Executor enforces a fee-limited execution context; keep instruction counts and compute within limits.
* If account ordering mismatches, you may see `AccountNotWritable`/`InvalidProgramId`—double-check discovery (return of `lz_compose_types`) vs execution handler (`lz_compose`) expectations.

## Next steps

* Review the [Solana OApp Reference](/v2/developers/solana/oapp/overview) for flows and PDAs.
* See EVM counterpart: [Omnichain Composers (EVM)](/v2/developers/evm/composer/overview) for conceptual parity.
