use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::pubkey::Pubkey;
use crate::error::{Result, ZeraError};
use super::super::constants::{
ACTION_MINT_WRAPPED, ACTION_MINT_WRAPPED_EXISTING, ATA_PROGRAM_ID, CORE_PROGRAM_ID,
DEFAULT_EVENT_INDEX, DEFAULT_VAA_EXPIRY, DEFAULT_VAA_VERSION, METADATA_PROGRAM_ID,
SYSTEM_PROGRAM_ID, SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_RENT_PUBKEY, TOKEN_BRIDGE_PROGRAM_ID,
TOKEN_PROGRAM_ID,
};
use super::super::rpc::SolanaRpc;
use super::super::types::{
MintWrappedAccounts, MintWrappedExistingAccounts, MintWrappedExistingOptions,
MintWrappedExistingResult, MintWrappedOptions, MintWrappedResult,
};
use super::super::utils::{
concat_bytes, derive_bridge_info_pda, derive_metadata_pda, derive_rate_limit_state_pda,
derive_released_transfer_pda, derive_router_config_pda, derive_token_registration_pda,
derive_verified_transfer_pda, derive_wrapped_mint_authority_pda, derive_wrapped_mint_pda,
encode_u16_be, encode_u32_le, encode_u64_be, encode_u64_le, generate_discriminator, get_ata,
};
use super::helpers::{
build_transaction, compute_unit_limit_instruction, create_ed25519_verify_instruction,
parse_hex_32, parse_pubkey, parse_signatures, payload_length_suffix, resolve_recent_blockhash,
};
fn validate_u16_len(bytes: &[u8], field_name: &str) -> Result<u16> {
u16::try_from(bytes.len()).map_err(|_| {
ZeraError::Validation(format!(
"{field_name} exceeds maximum encodable length of 65535 bytes"
))
})
}
fn validate_u8_len(bytes: &[u8], field_name: &str) -> Result<u8> {
u8::try_from(bytes.len()).map_err(|_| {
ZeraError::Validation(format!(
"{field_name} exceeds maximum encodable length of 255 bytes"
))
})
}
pub async fn build_mint_wrapped_transaction(
options: MintWrappedOptions,
payer: &Pubkey,
rpc: Option<&dyn SolanaRpc>,
) -> Result<MintWrappedResult> {
let recipient = parse_pubkey(&options.recipient, "recipient")?;
let txn_hash = parse_hex_32(&options.txn_id, "txnId")?;
let expected_hash = parse_hex_32(&options.expected_hash, "expectedHash")?;
let contract_id = options.contract_id.into_bytes();
let name = options.name.into_bytes();
let symbol = options.symbol.into_bytes();
let uri = options.uri.into_bytes();
let contract_id_len_u16 = validate_u16_len(&contract_id, "contractId")?;
let name_len_u8 = validate_u8_len(&name, "name")?;
let symbol_len_u8 = validate_u8_len(&symbol, "symbol")?;
let uri_len_u16 = validate_u16_len(&uri, "uri")?;
let (router_config, _) = derive_router_config_pda();
let (used_marker, _) = derive_verified_transfer_pda(&expected_hash);
let (redeemed_marker, _) = derive_released_transfer_pda(&expected_hash);
let (wrapped_mint, _) =
derive_wrapped_mint_pda(std::str::from_utf8(&contract_id).unwrap_or_default());
let (mint_authority, _) = derive_wrapped_mint_authority_pda(&wrapped_mint);
let (metadata, _) = derive_metadata_pda(&wrapped_mint);
let (bridge_info, _) = derive_bridge_info_pda(&wrapped_mint);
let (rate_limit_state, _) = derive_rate_limit_state_pda();
let (token_registration, _) = derive_token_registration_pda(&wrapped_mint);
let recipient_ata = get_ata(&recipient, &wrapped_mint);
let (signatures, public_keys) = parse_signatures(&options.signatures)?;
let mut signature_instructions = Vec::with_capacity(signatures.len());
for (signature, public_key) in signatures.iter().zip(public_keys.iter()) {
signature_instructions.push(create_ed25519_verify_instruction(
&expected_hash,
signature,
public_key,
)?);
}
let version = [DEFAULT_VAA_VERSION];
let action = [ACTION_MINT_WRAPPED];
let expiry = encode_u64_le(DEFAULT_VAA_EXPIRY);
let event_index = encode_u32_le(DEFAULT_EVENT_INDEX);
let timestamp = encode_u64_le(options.timestamp);
let amount_be = encode_u64_be(options.amount);
let amount_le = encode_u64_le(options.amount);
let contract_id_len_be = encode_u16_be(contract_id_len_u16);
let contract_id_len_le = encode_u32_le(contract_id.len() as u32);
let name_len = encode_u32_le(name.len() as u32);
let symbol_len = encode_u32_le(symbol.len() as u32);
let uri_len_be = encode_u16_be(uri_len_u16);
let uri_len_le = encode_u32_le(uri.len() as u32);
let usd_price_nano_be = encode_u64_be(options.usd_price_nano);
let usd_price_nano_le = encode_u64_le(options.usd_price_nano);
let liquidity_usd_nano_be = encode_u64_be(options.liquidity_usd_nano);
let liquidity_usd_nano_le = encode_u64_le(options.liquidity_usd_nano);
let tier = [options.tier];
let decimals = [options.decimals];
let payload = concat_bytes(&[
amount_be.as_ref(),
recipient.as_ref(),
contract_id_len_be.as_ref(),
contract_id.as_slice(),
decimals.as_ref(),
&[name_len_u8],
name.as_slice(),
&[symbol_len_u8],
symbol.as_slice(),
uri_len_be.as_ref(),
uri.as_slice(),
usd_price_nano_be.as_ref(),
liquidity_usd_nano_be.as_ref(),
tier.as_ref(),
]);
let payload_len = encode_u32_le(payload.len() as u32);
let payload_tail = payload_length_suffix(&payload);
let core_discriminator = generate_discriminator("global:post_verified_transfer");
let core_data = concat_bytes(&[
core_discriminator.as_ref(),
version.as_ref(),
action.as_ref(),
timestamp.as_ref(),
expiry.as_ref(),
txn_hash.as_slice(),
event_index.as_ref(),
payload_len.as_ref(),
payload.as_slice(),
payload_tail.as_ref(),
]);
let core_instruction = Instruction {
program_id: *CORE_PROGRAM_ID,
accounts: vec![
AccountMeta::new(router_config, false),
AccountMeta::new_readonly(*SYSVAR_INSTRUCTIONS_PUBKEY, false),
AccountMeta::new(used_marker, false),
AccountMeta::new(*payer, true),
AccountMeta::new_readonly(*SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(*TOKEN_BRIDGE_PROGRAM_ID, false),
],
data: core_data,
};
let token_discriminator = generate_discriminator("global:mint_wrapped");
let token_data = concat_bytes(&[
token_discriminator.as_ref(),
version.as_ref(),
timestamp.as_ref(),
expiry.as_ref(),
txn_hash.as_slice(),
event_index.as_ref(),
amount_le.as_ref(),
contract_id_len_le.as_ref(),
contract_id.as_slice(),
&[1, options.decimals],
&[1],
name_len.as_ref(),
name.as_slice(),
&[1],
symbol_len.as_ref(),
symbol.as_slice(),
&[1],
uri_len_le.as_ref(),
uri.as_slice(),
usd_price_nano_le.as_ref(),
liquidity_usd_nano_le.as_ref(),
tier.as_ref(),
]);
let token_instruction = Instruction {
program_id: *TOKEN_BRIDGE_PROGRAM_ID,
accounts: vec![
AccountMeta::new_readonly(*CORE_PROGRAM_ID, false),
AccountMeta::new_readonly(router_config, false),
AccountMeta::new(*payer, true),
AccountMeta::new(wrapped_mint, false),
AccountMeta::new_readonly(mint_authority, false),
AccountMeta::new(metadata, false),
AccountMeta::new(bridge_info, false),
AccountMeta::new_readonly(recipient, false),
AccountMeta::new(recipient_ata, false),
AccountMeta::new_readonly(used_marker, false),
AccountMeta::new(redeemed_marker, false),
AccountMeta::new_readonly(*TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(*ATA_PROGRAM_ID, false),
AccountMeta::new_readonly(*METADATA_PROGRAM_ID, false),
AccountMeta::new_readonly(*SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(*SYSVAR_RENT_PUBKEY, false),
AccountMeta::new_readonly(*SYSVAR_INSTRUCTIONS_PUBKEY, false),
AccountMeta::new(rate_limit_state, false),
AccountMeta::new(token_registration, false),
AccountMeta::new_readonly(*TOKEN_BRIDGE_PROGRAM_ID, false),
],
data: token_data,
};
let recent_blockhash = resolve_recent_blockhash(rpc).await?;
let mut verify_instructions = Vec::new();
if !signature_instructions.is_empty() {
verify_instructions.push(compute_unit_limit_instruction(1_400_000));
}
verify_instructions.extend(signature_instructions.iter().cloned());
verify_instructions.push(core_instruction.clone());
let verify_transaction = build_transaction(payer, verify_instructions, recent_blockhash);
let mint_transaction = build_transaction(
payer,
vec![
compute_unit_limit_instruction(1_400_000),
token_instruction.clone(),
],
recent_blockhash,
);
let mut combined_instructions = Vec::new();
if !signature_instructions.is_empty() {
combined_instructions.push(compute_unit_limit_instruction(1_400_000));
}
combined_instructions.extend(signature_instructions.iter().cloned());
combined_instructions.push(core_instruction.clone());
combined_instructions.push(token_instruction.clone());
let transaction = build_transaction(payer, combined_instructions, recent_blockhash);
Ok(MintWrappedResult {
core_instruction,
token_instruction,
signature_instructions,
transaction,
verify_transaction,
mint_transaction,
accounts: MintWrappedAccounts {
wrapped_mint,
mint_authority,
recipient_ata,
metadata,
bridge_info,
used_marker,
redeemed_marker,
},
})
}
pub async fn build_mint_wrapped_existing_transaction(
options: MintWrappedExistingOptions,
payer: &Pubkey,
rpc: Option<&dyn SolanaRpc>,
) -> Result<MintWrappedExistingResult> {
let recipient = parse_pubkey(&options.recipient, "recipient")?;
let txn_hash = parse_hex_32(&options.txn_id, "txnId")?;
let expected_hash = parse_hex_32(&options.expected_hash, "expectedHash")?;
let contract_id = options.contract_id.into_bytes();
let contract_id_len_u16 = validate_u16_len(&contract_id, "contractId")?;
let contract_id_str = std::str::from_utf8(&contract_id).unwrap_or_default();
let (router_config, _) = derive_router_config_pda();
let (used_marker, _) = derive_verified_transfer_pda(&expected_hash);
let (redeemed_marker, _) = derive_released_transfer_pda(&expected_hash);
let (wrapped_mint, _) = derive_wrapped_mint_pda(contract_id_str);
let (mint_authority, _) = derive_wrapped_mint_authority_pda(&wrapped_mint);
let (metadata, _) = derive_metadata_pda(&wrapped_mint);
let (bridge_info, _) = derive_bridge_info_pda(&wrapped_mint);
let (rate_limit_state, _) = derive_rate_limit_state_pda();
let (token_registration, _) = derive_token_registration_pda(&wrapped_mint);
let recipient_ata = get_ata(&recipient, &wrapped_mint);
let (signatures, public_keys) = parse_signatures(&options.signatures)?;
let mut signature_instructions = Vec::with_capacity(signatures.len());
for (signature, public_key) in signatures.iter().zip(public_keys.iter()) {
signature_instructions.push(create_ed25519_verify_instruction(
&expected_hash,
signature,
public_key,
)?);
}
let version = [DEFAULT_VAA_VERSION];
let action = [ACTION_MINT_WRAPPED_EXISTING];
let expiry = encode_u64_le(DEFAULT_VAA_EXPIRY);
let event_index = encode_u32_le(DEFAULT_EVENT_INDEX);
let timestamp = encode_u64_le(options.timestamp);
let amount_be = encode_u64_be(options.amount);
let amount_le = encode_u64_le(options.amount);
let contract_id_len_be = encode_u16_be(contract_id_len_u16);
let contract_id_len_le = encode_u32_le(contract_id.len() as u32);
let usd_price_nano_be = encode_u64_be(options.usd_price_nano);
let usd_price_nano_le = encode_u64_le(options.usd_price_nano);
let liquidity_usd_nano_be = encode_u64_be(options.liquidity_usd_nano);
let liquidity_usd_nano_le = encode_u64_le(options.liquidity_usd_nano);
let tier = [options.tier];
let payload = concat_bytes(&[
amount_be.as_ref(),
recipient.as_ref(),
contract_id_len_be.as_ref(),
contract_id.as_slice(),
usd_price_nano_be.as_ref(),
liquidity_usd_nano_be.as_ref(),
tier.as_ref(),
]);
let payload_len = encode_u32_le(payload.len() as u32);
let payload_tail = payload_length_suffix(&payload);
let core_discriminator = generate_discriminator("global:post_verified_transfer");
let core_data = concat_bytes(&[
core_discriminator.as_ref(),
version.as_ref(),
action.as_ref(),
timestamp.as_ref(),
expiry.as_ref(),
txn_hash.as_slice(),
event_index.as_ref(),
payload_len.as_ref(),
payload.as_slice(),
payload_tail.as_ref(),
]);
let core_instruction = Instruction {
program_id: *CORE_PROGRAM_ID,
accounts: vec![
AccountMeta::new(router_config, false),
AccountMeta::new_readonly(*SYSVAR_INSTRUCTIONS_PUBKEY, false),
AccountMeta::new(used_marker, false),
AccountMeta::new(*payer, true),
AccountMeta::new_readonly(*SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(*TOKEN_BRIDGE_PROGRAM_ID, false),
],
data: core_data,
};
let token_discriminator = generate_discriminator("global:mint_wrapped");
let token_data = concat_bytes(&[
token_discriminator.as_ref(),
version.as_ref(),
timestamp.as_ref(),
expiry.as_ref(),
txn_hash.as_slice(),
event_index.as_ref(),
amount_le.as_ref(),
contract_id_len_le.as_ref(),
contract_id.as_slice(),
&[0],
&[0],
&[0],
&[0],
usd_price_nano_le.as_ref(),
liquidity_usd_nano_le.as_ref(),
tier.as_ref(),
]);
let token_instruction = Instruction {
program_id: *TOKEN_BRIDGE_PROGRAM_ID,
accounts: vec![
AccountMeta::new_readonly(*CORE_PROGRAM_ID, false),
AccountMeta::new_readonly(router_config, false),
AccountMeta::new(*payer, true),
AccountMeta::new(wrapped_mint, false),
AccountMeta::new_readonly(mint_authority, false),
AccountMeta::new(metadata, false),
AccountMeta::new(bridge_info, false),
AccountMeta::new_readonly(recipient, false),
AccountMeta::new(recipient_ata, false),
AccountMeta::new_readonly(used_marker, false),
AccountMeta::new(redeemed_marker, false),
AccountMeta::new_readonly(*TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(*ATA_PROGRAM_ID, false),
AccountMeta::new_readonly(*METADATA_PROGRAM_ID, false),
AccountMeta::new_readonly(*SYSTEM_PROGRAM_ID, false),
AccountMeta::new_readonly(*SYSVAR_RENT_PUBKEY, false),
AccountMeta::new_readonly(*SYSVAR_INSTRUCTIONS_PUBKEY, false),
AccountMeta::new(rate_limit_state, false),
AccountMeta::new(token_registration, false),
AccountMeta::new_readonly(*TOKEN_BRIDGE_PROGRAM_ID, false),
],
data: token_data,
};
let recent_blockhash = resolve_recent_blockhash(rpc).await?;
let mut verify_instructions = Vec::new();
if !signature_instructions.is_empty() {
verify_instructions.push(compute_unit_limit_instruction(1_400_000));
}
verify_instructions.extend(signature_instructions.iter().cloned());
verify_instructions.push(core_instruction.clone());
let verify_transaction = build_transaction(payer, verify_instructions, recent_blockhash);
let mint_transaction = build_transaction(
payer,
vec![
compute_unit_limit_instruction(1_400_000),
token_instruction.clone(),
],
recent_blockhash,
);
let mut combined_instructions = Vec::new();
if !signature_instructions.is_empty() {
combined_instructions.push(compute_unit_limit_instruction(1_400_000));
}
combined_instructions.extend(signature_instructions.iter().cloned());
combined_instructions.push(core_instruction.clone());
combined_instructions.push(token_instruction.clone());
let transaction = build_transaction(payer, combined_instructions, recent_blockhash);
Ok(MintWrappedExistingResult {
core_instruction,
token_instruction,
signature_instructions,
transaction,
verify_transaction,
mint_transaction,
accounts: MintWrappedExistingAccounts {
wrapped_mint,
mint_authority,
recipient_ata,
used_marker,
redeemed_marker,
},
})
}