Quickstart
Example
For the step-by-step instructions on how to build, deploy and wire a Solana OApp, view the Solana OApp example.Scaffold
Spin up a new Solana OApp project (based on the example) in seconds:OApp (Solana) and proceed with the installation.
The example contains a string-passing OApp that works across Solana and EVM.
Follow the provided README instructions to deploy the example and make your first crosschain message between Solana and an EVM chain.
The following sections will highlight the several code excerpts of the Solana OApp that are essential to it functioning.
Developing Solana OApps vs EVM OApps
Due to VM and programming paradigm differences, developing OApps on Solana works differently from doing the same on EVM chains. On EVM chains, OApps can simply inherit the OApp contract standard to unlock crosschain functionality. On Solana, there is no similar inheritance. The table below outlines the main differences when developing:High-level message flow
Due to the need for the inclusion of accounts, the flow of an inbound message on Solana is different from that on an EVM chain:Accounts and Instructions Discovery
The discovery of accounts and instructions is handled bylz_receive_types_v2.
What lz_receive_types_v2 introduces
lz_receive_types_v2 has the following key features:
- Support for multiple ALTs: Expands the capacity for account inclusion by allowing multiple Address Lookup Tables, increasing the number of accounts accessible within a transaction.
- Compact and flexible account reference model: Implements the AddressLocator, enabling OApps to reference accounts with a leaner, more efficient structure while maintaining adaptability for future upgrades.
- Context account from the Executor: Provides runtime metadata at execution, ensuring that OApps have contextual awareness without requiring redundant account fetching or manual setup. In the code, this is referred to as the ExecutionContext.
- Explicit support for multiple EOA signers: Enables dynamic data account initialization by allowing multiple externally owned accounts (EOAs) to participate in signing and setup. This improves flexibility for multi-party or multi-step workflows.
- Multi-instruction execution model: Empowers OApps to compose complex workflows within a single atomic transaction, combining several instructions while preserving consistency and rollback guarantees.
How lz_receive_types_v2 works
Overall, lz_receive_types_v2 has the following execution flow:
lz_receive_types_info- requires two accounts in this exact order (must not be changed):
oapp_account- the OApp identity/account. In the code examples here, theoapp_accountis thestoreaccount.lz_receive_types_accounts- PDA derived withseeds = [LZ_RECEIVE_TYPES_SEED, &oapp_account.key().to_bytes()].
- returns
(version, versioned_data)version: u8— A protocol-defined version identifier for theLzReceiveTypelogic and return type, starting from 2.versioned_data: Any— A value of typeAny, representing a version-specific structure. The Executor decodes this payload based on the version and uses it to construct the full set of accounts needed to invokelz_receive_types_v2(LzReceiveTypesV2Accounts).
- requires two accounts in this exact order (must not be changed):
lz_receive_types_v2- this instruction is called withLzReceiveTypesV2Accountsas the supplied accounts and returns:- the
context_version - ALTs used (if any)
- The full list of instructions for
lz_receive
- the
build and submit transaction- now the Executor can prepare the full transaction and submit it based on what was returned bylz_receive_types_v2. Before submitting, the Executor will also prepend (pre_execute) and append (post_execute) instructions to prepare the execution context and ensure safety:- pre_execute - Initializes a fee-limited execution context at the start of the transaction (allowing up to two ComputeBudget instructions), records the payer’s starting balance, and ensures a matching PostExecute will end the transaction.
- post_execute - validates pairing with PreExecute, enforces the fee limit against the payer’s balance change, checks signer invariants, and resets the context.
All the instructions above are called by the Executor. As the OApp developer, you only need to ensure that you implement the required instructions and PDAs in your OApp program.
Required PDAs
The following are the PDAs that are required for a Solana OApp.
Note that except for the Store PDA, the PDA seeds are not customizable.
Peer Config PDAs are initialized by the wiring step.
Required Instructions
Initialize the OApp PDA
Thisinit instruction initializes 2 required PDAs: OApp Store and lz_receive_types_accounts.
- You must call
oapp::endpoint_cpi::register_oappto register your OApp - Registration is one-time; afterwards the Executor knows this Store PDA = OApp.
- You do not pass a “trusted remote” mapping here—that’s what the
PeerPDAs enforce. - You can extend your OApp’s store PDA with other fields required by your use case.
Implement lz_receive — business logic + Endpoint::clear
The lz_receive instruction is where your OApp’s business logic is defined and also where the call to Endpoint::clear is made. Additionally, it is also where compose messages are handled, if implemented.
- Call
clear()before touching any user state—this burns the nonce and prevents re-entry. - Use
ctx.remaining_accountsinstead of hard-wiring anything—keepslz_receive_types_v2andlz_receiveperfectly in sync. - Don’t forget
is_signer: truezero-pubkey placeholders for ATA init or rent payer.
- Validate the
Peeraccount first (constraint = params.sender == peer.address). - Store the Endpoint ID inside state (
store.endpoint_program) and assert it every CPI.
Implement lz_receive_types_v2
Define LzReceiveTypesAccounts anywhere in your state module tree:
Create anstorehere is the OApp account referred to asoapp_accountin the flow description.
lz_receive_types_info instruction:
lz_receive_types_v2:
lib.rs:
OApp-Specific Message Codec
Since at its core, the OApp Standard simply gives you the interface for generic message passing (raw bytes), you need to implement for yourself how the raw bytes are interpreted. In the Solana OApp example and also in the OFT implementation, this logic is encapusalated in amsg_codec.rs file.
- Every OApp would have its own Message Codec implementation
- The above Message Codec example involves an OApp that expects the message to contain only a
lengthand the actualstring - If sending across VMs, ensure the codec on the other VM matches.