Install Dependencies
cargo add heaven-integration-sdk --git https://github.com/heavenxyz/heaven-integration-sdk.git -rev 0c61785e24f92226e1b3a0b288cfa21ade3672ef
cargo add solana-sdk
cargo add solana-client
cargo add spl-token --features no-entrypoint
cargo add spl-associated-token-account --features no-entrypoint
cargo add spl-token-2022 --features no-entrypoint
Create an RPC Connection
let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com");
Create a Pool
// 1. Set up an RPC client pointing at mainnet-beta.
let rpc_client =
solana_client::rpc_client::RpcClient::new("https://api.mainnet-beta.solana.com");
// 2. Define the fee-payer address (must be funded) and a fresh creator keypair.
let payer = pubkey!("2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S");
let creator = Pubkey::new_unique();
// 3. Specify which version of the protocol config to use, and the program ID.
// Permissionless pool creation is only supported in version 2 and should be used for development and testing purposes.
// Until version 1 is updated to support permissionless pool creation, it is recommended to use version 2.
let protocol_config_version = 2;
let program_id = heaven::ID;
// 4. Derive the PDA for the protocol config state, then fetch & parse it.
let protocol_config_key =
derive_protocol_config_state(protocol_config_version, &program_id).0;
let protocol_config_account = rpc_client
.get_account(&protocol_config_key)
.expect("Failed to get protocol config account");
let protocol_config = parse_protocol_config(&protocol_config_account.data)
.expect("Failed to parse protocol config");
// ─── Prepare a new SPL-Token 2022 mint for token A ────────────────────────────
// 5. Generate a new keypair for the mint; this account will hold mint metadata.
let mint_signer = Keypair::new();
let token_a_mint = mint_signer.pubkey();
// Use the SPL-Token 2022 program for extended features (transfer fees).
let token_a_program = spl_token_2022::ID;
let token_a_mint_authority = creator; // creator will initially have minting rights
// 6. Compute how much space the mint needs to include the transfer-fee extension.
let mint_space =
ExtensionType::try_calculate_account_len::<spl_token_2022::state::Mint>(&vec![
ExtensionType::TransferFeeConfig,
])
.unwrap();
// 7. Query minimum lamports for rent exemption for that account size.
let rent_exempt = rpc_client
.get_minimum_balance_for_rent_exemption(mint_space as usize)
.expect("Failed to get rent exemption");
// 8. Build the CPI to initialize the transfer-fee config on the mint.
let initialize_transfer_fee_ix =
spl_token_2022::extension::transfer_fee::instruction::initialize_transfer_fee_config(
&spl_token_2022::ID,
&token_a_mint,
Some(&protocol_config_key), // fee config authority
Some(&protocol_config_key), // withdraw authority
0, // transfer fee numerator
u64::MAX, // transfer fee denominator (max precision)
)
.unwrap();
// 9. Build the CPI to initialize the mint itself (decimals from protocol config).
let initialize_mint_ix = spl_token_2022::instruction::initialize_mint(
&spl_token_2022::ID,
&token_a_mint,
&token_a_mint_authority,
None, // no freeze authority
protocol_config.token_a_decimals, // decimals as configured
)
.unwrap();
// 10. Build the system instruction to create the mint account on-chain.
let create_mint_ix = system_instruction::create_account(
&payer,
&token_a_mint,
rent_exempt,
mint_space as u64,
&token_a_program,
);
// ─── Create and fund the creator’s ATA, then mint tokens ──────────────────────
// 11. Derive the Associated Token Account (ATA) address for (creator, token A).
let ata_key =
get_associated_token_address_with_program_id(&creator, &token_a_mint, &token_a_program);
// Build the idempotent CPI to create the ATA (no-op if it already exists).
let create_ata_ix = create_associated_token_account_idempotent(
&payer,
&creator,
&token_a_mint,
&token_a_program,
);
// Mint the initial token A supply into the creator’s ATA.
let mint_tokens_ix = spl_token_2022::instruction::mint_to(
&spl_token_2022::ID,
&token_a_mint,
&ata_key,
&token_a_mint_authority,
&[], // no multisig signers
protocol_config.initial_token_a_amount,
)
.unwrap();
// 12. Revoke mint authority so no further minting is possible.
let revoke_mint_authority_ix = spl_token_2022::instruction::set_authority(
&spl_token_2022::ID,
&token_a_mint,
None, // new authority = none (lock forever)
spl_token_2022::instruction::AuthorityType::MintTokens,
&token_a_mint_authority,
&[],
)
.unwrap();
// ─── Quote how much SOL is needed for the pool’s initial purchase ───────────────
// 13. Convert a human-readable UI amount into raw token amount.
let initial_purchase_amount =
ui_amount_to_amount(10_000_000.0, protocol_config.token_a_decimals);
// 14. Ask the bonding-curve logic how much SOL to spend for that many tokens.
let max_sol_spend =
quote_initial_purchase_amount(&protocol_config, initial_purchase_amount)
.expect("Failed to quote initial purchase amount");
// ─── Build the Heaven AMM “create pool” instruction ───────────────────────────
// 15. Construct the CPI to call our on-chain program and create the pool.
let create_pool_ix = create_initialize_pool_ix(
&payer,
&creator,
initial_purchase_amount,
max_sol_spend,
&token_a_program,
&token_a_mint,
protocol_config_version,
&program_id,
)
.unwrap();
// ─── (Optional) Bump compute-unit limits for heavy CPI workloads ────────────────
let set_compute_unit_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000);
let set_compute_unit_price_ix = ComputeBudgetInstruction::set_compute_unit_price(1_000_000);
// ─── Aggregate all instructions into one transaction ──────────────────────────
let ixs = vec![
set_compute_unit_limit_ix,
set_compute_unit_price_ix,
create_mint_ix,
initialize_transfer_fee_ix,
initialize_mint_ix,
create_ata_ix,
mint_tokens_ix,
revoke_mint_authority_ix,
create_pool_ix,
];
// ─── Compile, sign, and simulate the transaction ──────────────────────────────
// 16. For simplicity, use a dummy recent blockhash.
let recent_blockhash = Hash::default();
// 17. Build a versioned Message from payer + all instructions.
let message = Message::try_compile(&payer, &ixs.as_ref(), &[], recent_blockhash)
.expect("Failed to compile message");
let versioned_message = VersionedMessage::V0(message);
// 18. Collect the required signers: payer, creator, and the mint keypair.
let signers: [&dyn Signer; 3] = [
&NullSigner::new(&payer),
&NullSigner::new(&creator),
&mint_signer,
];
// 19. Create the VersionedTransaction object.
let versioned_tx = VersionedTransaction::try_new(versioned_message, &signers)
.expect("Failed to create versioned transaction");
// 20. Submit the transaction to the RPC and check for errors.
// ...
Fetch Pool State
let token_mint = pubkey!("88aUGeGXFNaEyzL48fkzSPWUPhJr3gWrMDD8EH8tCb1");
let program_id = heaven::ID;
let pool_key = derive_standard_liquidity_pool_state(&token_mint, &spl_token::native_mint::ID, &program_id)
.0;
let pool_account = rpc_client.get_account(&pool_key).unwrap();
let pool_state = parse_liquidity_pool_state(&pool_account.data).unwrap();
Fetch Protocol Configuration
let protocol_config_version = 1;
let protocol_config_key = derive_protocol_config_state(protocol_config_version, &program_id).0;
let protocol_config_account = rpc_client.get_account(&protocol_config_key).unwrap();
let protocol_config = parse_protocol_config(&protocol_config_account.data).unwrap();
Initialize the Heaven Client
let amm = HeavenAmm {
state: &pool_state,
program_id: &program_id,
protocol_config: &protocol_config,
};
Quote Buy
quote_buy
method. This will return the amount of tokens you will receive, the fee, and the fee percentage.let max_sol_spend = (0.01 * 1e9) as u64; // 0.1 SOL in lamports
let ata = get_associated_token_address_with_program_id(
&user,
&pool_state.token_a.mint,
&pool_state.token_a.owner,
);
let current_balance = rpc_client
.get_token_account_balance(&ata)
.map(|balance| balance.amount.parse::<u64>().unwrap_or(0))
.unwrap_or_default();
let (token_output, fee, fee_pct) = amm
.quote_buy(
max_sol_spend,
current_slot,
current_balance,
current_sol_usd_price,
)
.unwrap();
println!(
"Buy Quote: Output: {}, Fee: {}, Fee Percentage: {}%",
amount_to_ui_amount(token_output, pool_state.token_a_decimals()),
fee,
fee_pct * 100.0
);
Create Buy Instruction
max_sol_spend
is the maximum amount of SOL you are willing to spend, and minimum_token_output
is the minimum amount of tokens you want to receive.let minimum_token_output = token_output;
let buy_ix = amm
.buy_ix(max_sol_spend, minimum_token_output, &user)
.unwrap();
Quote Sell
quote_sell
method. This will return the amount of SOL you will receive, the fee, and the fee percentage.let token_amount = token_output;
let (sol_output, fee, fee_pct) = amm
.quote_sell(token_amount, current_slot, current_sol_usd_price)
.unwrap();
Create Sell Instruction
let sell_ix = amm
.sell_ix(token_amount, sol_output, &user)
.unwrap();
use heaven_integration_sdk::heaven::{
self,
amm::{
HeavenAmm, create_close_wrapped_sol_ix, derive_protocol_config_state,
derive_standard_liquidity_pool_state, parse_liquidity_pool_state, parse_protocol_config,
wrap_sol_ixs,
},
oracle::{CHAINLINK_SOL_USD_FEED, state::get_latest_sol_usd_price},
};
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_sdk::{
compute_budget::ComputeBudgetInstruction,
hash::Hash,
message::{VersionedMessage, v0::Message},
pubkey,
signature::NullSigner,
transaction::VersionedTransaction,
};
use spl_associated_token_account::{
get_associated_token_address_with_program_id,
instruction::create_associated_token_account_idempotent,
};
use spl_token::amount_to_ui_amount;
fn main() {
let rpc_client =
solana_client::rpc_client::RpcClient::new("https://api.mainnet-beta.solana.com");
let account = rpc_client.get_account(&CHAINLINK_SOL_USD_FEED).unwrap();
let data = account.data;
let token_mint = pubkey!("88aUGeGXFNaEyzL48fkzSPWUPhJr3gWrMDD8EH8tCb1");
let protocol_config_version = 1;
let user = pubkey!("FrXg2ceCwbpSCcW3eLMuzt2xqBp727zqoSC93TkRixvC");
let program_id = heaven::ID;
let current_sol_usd_price = get_latest_sol_usd_price(&data).unwrap();
let pool_key =
derive_standard_liquidity_pool_state(&token_mint, &spl_token::native_mint::ID, &program_id)
.0;
let pool_account = rpc_client.get_account(&pool_key).unwrap();
let pool_state =
parse_liquidity_pool_state(&pool_account.data).expect("Failed to parse pool state");
let protocol_config_key = derive_protocol_config_state(protocol_config_version, &program_id).0;
let protocol_config_account = rpc_client
.get_account(&protocol_config_key)
.expect("Failed to get protocol config account");
let protocol_config = parse_protocol_config(&protocol_config_account.data)
.expect("Failed to parse protocol config");
let amm = HeavenAmm {
state: &pool_state,
program_id: &program_id,
protocol_config: &protocol_config,
};
let current_slot = rpc_client.get_slot().unwrap();
// Calculate buy quote
let max_sol_spend = (0.01 * 1e9) as u64; // 0.1 SOL in lamports
let ata = get_associated_token_address_with_program_id(
&user,
&pool_state.token_a.mint,
&pool_state.token_a.owner,
);
let current_balance = rpc_client
.get_token_account_balance(&ata)
.map(|balance| balance.amount.parse::<u64>().unwrap_or(0))
.unwrap_or_default();
let (token_output, fee, fee_pct) = amm
.quote_buy(
max_sol_spend,
current_slot,
current_balance,
current_sol_usd_price,
)
.expect("Failed to get buy quote");
println!(
"Buy Quote: Output: {}, Fee: {}, Fee Percentage: {}%",
amount_to_ui_amount(token_output, pool_state.token_a_decimals()),
fee,
fee_pct * 100.0
);
assert!(token_output > 0);
let (sol_output, fee, fee_pct) = amm
.quote_sell(token_output, current_slot, current_sol_usd_price)
.expect("Failed to get sell quote");
println!(
"Sell Quote: Output: {}, Fee: {}, Fee Percentage: {}%",
amount_to_ui_amount(sol_output, pool_state.token_b_decimals()),
fee,
fee_pct * 100.0
);
// Create buy instruction based on the quote
let buy_ix = amm
.buy_ix(max_sol_spend, token_output, &user)
.expect("Failed to create buy instruction");
let sell_ix = amm
.sell_ix(token_output, sol_output, &user)
.expect("Failed to create sell instruction");
let set_compute_unit_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000);
let set_compute_unit_price_ix = ComputeBudgetInstruction::set_compute_unit_price(1000_000);
let (mut ixs, ata) = wrap_sol_ixs(max_sol_spend, &user);
let create_ata_ix = create_associated_token_account_idempotent(
&user,
&user,
&pool_state.token_a.mint,
&spl_token_2022::ID,
);
ixs.insert(0, create_ata_ix);
ixs.insert(0, set_compute_unit_price_ix);
ixs.insert(0, set_compute_unit_limit_ix);
ixs.push(buy_ix);
ixs.push(sell_ix);
let close_wrapped_sol_ix = create_close_wrapped_sol_ix(&user, &ata);
ixs.push(close_wrapped_sol_ix);
let recent_blockhash = Hash::default();
let message = Message::try_compile(&user, &ixs.as_ref(), &[], recent_blockhash)
.expect("Failed to compile message");
let versioned_message = VersionedMessage::V0(message);
let versioned_tx = VersionedTransaction::try_new(versioned_message, &[NullSigner::new(&user)])
.expect("Failed to create versioned transaction");
let logs = rpc_client.simulate_transaction_with_config(
&versioned_tx,
RpcSimulateTransactionConfig {
replace_recent_blockhash: true,
..Default::default()
},
);
assert!(logs.is_ok());
assert!(
logs.as_ref().unwrap().value.err.is_none(),
"Simulation error: {:#?}",
logs.unwrap().value.err
);
}