This guide provides step-by-step instructions on how to deploy your custom Solana protocol. By following these steps, you can successfully deploy your custom Solana protocol across multiple chains. If you encounter issues, refer to the guide or reach out to us through our .
Prerequisites
Ensure you have the latest versions installed.
Before proceeding, ensure you are familiar with writing Solana programs in .
In this guide we'll be using anchor as the framework of choice, but native programs will work as well. You can follow the anchor if it's your first time using it.
Before proceeding make sure you have setup your environment first, as shown below.
agave-install init 2.1.0 # solana
avm use 0.31.0 # anchor
Contract Dependencies
To streamline development, start by adding the UIP Solana SDK dependency to the configuration file.
If you are not using Anchor, you don’t need to enable the anchor-lang feature.
programs/*/Cargo.toml
uip-solana-sdk = { version = "0.1", features = ["anchor-lang"] }
Then, if you are developing an EVM to non-EVM protocol, you will likely need to use a library for EVM ABI encoding, we recommend using :
alloy-sol-types = "0.7"
Sending a message
First, you need to declare the following accounts:
use uip_solana_sdk::UipEndpoint;
#[derive(Accounts)]
pub struct SendMessage<'info> {
#[account(mut)]
sender: Signer<'info>,
/// CHECK: checked in the CPI
endpoint_config: AccountInfo<'info>,
/// CHECK: checked in the CPI
#[account(mut)]
uts_connector: AccountInfo<'info>,
/// CHECK: checked in CPI
#[account(seeds = [b"UIP_SIGNER"], bump)]
program_signer: AccountInfo<'info>,
system_program: Program<'info, System>,
uip_program: Program<'info, UipEndpoint>,
}
sender is the payer for the operation. program_signer is a special PDA used to identify your program. And uts_connector can be retrieved using the fetchUtsConnector function from the TypeScript package.
Now, in the instruction, you can perform the Propose CPI:
total_fee - it’s the fee paid for the message, usually calculated using the TypeScript UIP SDK
dest_chain_id - chain ID of the destination network
selector - in most situations it should be omitted
dest_addr - address of the destination contract. If you’re sending to an EVM chain, make sure you left pad it so it’s 32 bytes
payload - the payload, carries any data you want to send
proposal_commitment - whether transmitters should wait for your proposal transaction to be finalized or confirmed. Skipping the finalization will reduce the transmission time by about 15 seconds, but could theoretically pose a risk if the Solana blockchain is compromised
custom_gas_limit - used on the EVM destination to limit gas usage, must not be zero
Receiving a message
First, import some helper methods.
use uip_solana_sdk::{chains::*, parse_uip_message, route_instruction, MessageDataRef};
When a message is received, the endpoint will invoke a special instruction on your contract with a specific discriminator: uip_solana_sdk::EXECUTE_DISCRIMINATOR. When using the Anchor framework, it can be declared like so:
In the function body, you should parse the incoming message:
let uip_msg_data = ctx.accounts.uip_msg.try_borrow_data()?;
let MessageDataRef {
payload,
sender_addr,
src_chain_id,
msg_hash,
..
} = parse_uip_message(&ctx.accounts.uip_msg, &uip_msg_data, &crate::ID)?;
First, you should validate that src_chain_id and sender_addr match one of the smart contracts that your protocol supports.
Then, decode the payload for further processing.
If you are using Anchor and expect different accounts for different types of messages, you can use the helper route_instruction function that can make it appear as if you are routing a real instruction, for example:
if some_condition {
let ix_data = MyIxData {
//
};
let input = MyIxInput {
//
};
route_instruction(
&crate::ID,
my_ix,
ctx.remaining_accounts,
ix_data,
input,
)?;
} else {
// route another instruction
}
Here, my_ix is defined as any other instruction, but is not made public. The first account must be payer, which is the UIP executor. The following accounts must be the custom accounts that you need. Don’t forget to perform all the required checks for these accounts.
Lastly, for the Execute instruction to be connected to the UIP network, you need to register your protocol extension.
Extension registration
Solana is different to other networks in that apart from function parameters it needs accounts to be specified. In order for the executor to know what accounts need to be passed, we utilize something called extensions, compiled to WASM and stored on IPFS. Basically, it’s a separate library that describes your Execute instruction interface.
To implement it, initialize a new library and specify the following in Cargo.toml:
Then, you need to write a function called get_api_version that returns the version of the extension API that your extension supports. Right now, 0 is the only supported version so leave it at that.
Then, you need to write the get_instruction_info function with a specific signature, that takes MessageData and writes back the required accounts, compute units, and heap frame back into InstructionInfo. If your program needs more heap than the default, you can request heap frame (it must be a multiple of 1024 less than 256 KiB), otherwise leave it at 0.
After that, record the CID of the uploaded file. You need to convert it to 36 bytes, it can be done using the multiformats package:
import { CID } from "multiformats";
Array.from(CID.parse(ipfsCid).toV1().bytes)
Your program needs to have an instruction that registers the extension using a CPI call. We recommend gating it with access control, so that only an admin or multisig can call it. You can run the instruction during initialization or upgrade of your program.
After performing RegisterExtension, your contract can now receive UIP messages!
Client-side scripts and testing
The Solana endpoint has a TypeScript package to help with client-side interaction and testing. To use it, specify the following dependency in package.json:
"@lincot/uip-solana-sdk": "^0.6.0"
Testing your contract
In order to test execution of your messages, you can mimic the cross-chain message execution on localnet. For this, you can use a mock UIP config with a predefined signer configuration:
You can put it in tests/accounts/uip_config.json. Then, you can configure Anchor to load the mock account and the other accounts necessary for UIP endpoint execution. Add this to Anchor.toml:
Sending messages that don’t fit in a single Solana transaction
We have developed a utility program to allow loading data in chunks to later be used to invoke a Solana instruction. It allows passing up to 10KB of data to an instruction, bypassing the normal limit of around 1KB per instruction.
It will be automatically used by executors when a large message is passed to Solana from another chain. However, if you want to send a large transaction from Solana, you can also use the Chunk Loader program TypeScript SDK:
"@lincot/solana-chunk-loader": "^0.4.4",
In order to pass large data to your function, you need to manually encode the function selector and arguments to a buffer, and then pass it by chunks to the Chunk Loader program. The loadByChunks instructions can be sent concurrently, and then followed by the passToCpi instruction.
To send a message you need to perform a to the endpoint.
The payer and the accounts that are specific to your protocol (specified in ) will come after uip_msg. If you only need a single set of accounts for all possible payloads, you can specify these accounts in the same structure after uip_msg. Otherwise, they will be put in the ctx.remaining_accounts variable for further routing.
Then upload target/wasm32-wasip1/release/example_extension-optimized.wasm to IPFS, using a service such as (it’s free).