lz_compose_types_v2 typed discovery flow.
Prerequisites
- Familiarity with Anchor and Solana CPIs.
- Read the conceptual guide: Omnichain Composers
- Understanding of Solana OApps and the PDA used
- Understanding of the
lz_receivev2 flow (see 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.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:- Sending Application: The sender OApp sends a message with a
composeMsgattached. For composed messages to Solana, the recipient address should be set to the Composer PDA. - Receiving Application: The destination OApp’s
lz_receiveis executed, and since there is acomposeMsg, asend_composeCPI is made to the Endpoint program to send the compose message to the Composer PDA. - Composer Application: The Executor calls
lz_composeon 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:Usage
The accounts and instructions needed are similar to those outlined in lz_receive_types_v2:composer- a PDA that can be used to store static addresses needed forlz_composeexecution, and more importantly, whose address is used as the ‘Composer address’LzComposeTypesAccounts- a PDA that contains the accounts needed to calllz_compose_typeslz_compose_types_info- an instruction that provides versioning info that helps the Executor understand how to proceed withlz_compose_types/lz_compose_types_v2lz_compose_types_v2- an instruction that returns the list of accounts and execution plan needed to executelz_composelz_compose- the instruction that contains the actual business logic that must be executed for acomposeMsg
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’slib.rs:
lz_compose_types later:
LzComposeTypesAccounts
Define the struct of PDA that holds versioned compose-type discovery data, e.g.LzComposeTypesAccounts:
LzComposeTypesAccounts PDA in your init instruction:
- 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
initinstruction that was already responsible for initializinglz_receive_types_accountsis extended to also initializelz_compose_types_accounts - The
initfunction can be arbitrarily named, as long as it is called
lz_compose_types_info
Create anlz_compose_types_info instruction that returns the version and the accounts needed to construct lz_compose_types_v2:
lz_compose_types_v2
Implementlz_compose_types_v2 and return a compact execution plan including exactly one Instruction::LzCompose:
Composed Message Execution Options
Longer composed flows increase the cost of executinglz_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:
- Also set gas/value for the composer execution:
_index: the index of thecomposeMsg._gas: gas/compute budget for the destination execution._value: native value forwarded with the call if needed.
Composing an OFT
Sending an OFT from Solana with a compose message
The OFT program (Solana) supports attaching an optional compose_msg to a crosschain send.Handling a compose message for an OFT on Solana
The reference Solana OFT program’slz_receive already includes a call to Endpoint::send_composefor when a message contains a compose_msg :
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”, thesend_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. For general receive flow details, see OApp lz_receive — business logic + Endpoint::clear.
Execution notes and troubleshooting
- Ensure
lz_receive_typesandlz_compose_typesPDAs are initialized and kept in sync with your handlers. - Always call
Endpoint::clearbefore touching user state inlz_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 oflz_compose_types) vs execution handler (lz_compose) expectations.
Next steps
- Review the Solana OApp Reference for flows and PDAs.
- See EVM counterpart: Omnichain Composers (EVM) for conceptual parity.