use steel::*;
use crate::{
consts::{
BASIS_POINTS_DENOMINATOR, DEFAULT_FEE_BPS, PAYMENT_STATE_FUNDED, PAYMENT_STATE_REFUNDED,
PAYMENT_STATE_RELEASED,
},
error::EscrowError,
instruction::*,
state::{bank_pda, escrow_pda, normalize_payment_uid, payment_pda, sol_storage_pda, Payment},
};
pub struct EscrowSdk;
impl EscrowSdk {
pub fn program_id() -> Pubkey {
crate::ID
}
pub fn bank_pda() -> (Pubkey, u8) {
bank_pda()
}
pub fn authority_transfer_pda(bank: Pubkey) -> (Pubkey, u8) {
crate::state::authority_transfer_pda(bank)
}
pub fn config_pda() -> (Pubkey, u8) {
crate::state::config_pda()
}
pub fn escrow_pda(mint: Pubkey) -> (Pubkey, u8) {
let (bank_pda, _) = Self::bank_pda();
escrow_pda(mint, bank_pda)
}
pub fn payment_pda(payment_uid: &str, bank: Pubkey) -> (Pubkey, u8) {
payment_pda(payment_uid, bank)
}
pub fn sol_storage_pda(mint: Pubkey, bank: Pubkey, escrow: Pubkey) -> (Pubkey, u8) {
sol_storage_pda(mint, bank, escrow)
}
pub fn associated_token_account(wallet: Pubkey, mint: Pubkey) -> Pubkey {
Self::associated_token_account_with_program(wallet, mint, spl_token::ID)
}
pub fn associated_token_account_with_program(
wallet: Pubkey,
mint: Pubkey,
token_program: Pubkey,
) -> Pubkey {
spl_associated_token_account::get_associated_token_address_with_program_id(
&wallet,
&mint,
&token_program,
)
}
pub fn escrow_token_account(mint: Pubkey) -> Pubkey {
Self::escrow_token_account_with_program(mint, spl_token::ID)
}
pub fn escrow_token_account_with_program(mint: Pubkey, token_program: Pubkey) -> Pubkey {
let (escrow_pda, _) = Self::escrow_pda(mint);
Self::associated_token_account_with_program(escrow_pda, mint, token_program)
}
pub fn calculate_gross_quote(
desired_net: u64,
fee_bps: u16,
min_fee_amount: u64,
oracle_fee_bps: u16,
) -> u64 {
let numerator_a =
(desired_net as u128 + min_fee_amount as u128).saturating_mul(BASIS_POINTS_DENOMINATOR);
let denominator_a = (BASIS_POINTS_DENOMINATOR - oracle_fee_bps as u128).max(1);
let amount_a = (numerator_a.saturating_add(denominator_a - 1) / denominator_a) as u64;
let numerator_b = (desired_net as u128).saturating_mul(BASIS_POINTS_DENOMINATOR);
let denominator_b =
(BASIS_POINTS_DENOMINATOR - fee_bps as u128 - oracle_fee_bps as u128).max(1);
let amount_b = (numerator_b.saturating_add(denominator_b - 1) / denominator_b) as u64;
amount_a.max(amount_b)
}
pub fn initialize(signer: Pubkey, fee_bps: Option<u16>) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(signer, true),
AccountMeta::new(bank_pda, false),
AccountMeta::new(config_pda, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
],
data: Initialize {
fee_bps: (fee_bps.unwrap_or(DEFAULT_FEE_BPS)).to_le_bytes(),
_padding: [0; 6], }
.to_bytes(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn open_escrow(
signer: Pubkey,
payer: Pubkey,
mint: Pubkey,
min_payment_amount: u64,
max_payment_amount: u64,
min_fee_amount: u64,
fee_bps: u16,
oracle_fee_bps: u16,
) -> Instruction {
Self::open_escrow_with_token_program(
signer,
payer,
mint,
min_payment_amount,
max_payment_amount,
min_fee_amount,
fee_bps,
oracle_fee_bps,
spl_token::ID,
)
}
#[allow(clippy::too_many_arguments)]
pub fn open_escrow_with_token_program(
signer: Pubkey,
payer: Pubkey,
mint: Pubkey,
min_payment_amount: u64,
max_payment_amount: u64,
min_fee_amount: u64,
fee_bps: u16,
oracle_fee_bps: u16,
token_program: Pubkey,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let escrow_tokens = if mint == Pubkey::default() {
escrow_pda
} else {
Self::escrow_token_account_with_program(mint, token_program)
};
let mut accounts = vec![
AccountMeta::new(signer, true),
AccountMeta::new(payer, true),
AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new(escrow_pda, false),
AccountMeta::new(escrow_tokens, false),
AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(token_program, false),
AccountMeta::new_readonly(spl_associated_token_account::ID, false),
AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
];
if mint == Pubkey::default() {
let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
accounts.push(AccountMeta::new(sol_storage_pda, false));
}
Instruction {
program_id: Self::program_id(),
accounts,
data: OpenEscrow {
min_payment_amount: min_payment_amount.to_le_bytes(),
max_payment_amount: max_payment_amount.to_le_bytes(),
min_fee_amount: min_fee_amount.to_le_bytes(),
fee_bps: fee_bps.to_le_bytes(),
oracle_fee_bps: oracle_fee_bps.to_le_bytes(),
_padding: [0; 4],
}
.to_bytes(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn fund_payment(
buyer: Pubkey,
buyer_tokens: Option<Pubkey>, seller: Pubkey,
mint: Pubkey,
amount: u64,
ttl_seconds: i64,
payment_uid: &str,
sla_hash: [u8; 32],
oracle_authority: Pubkey,
) -> Instruction {
Self::fund_payment_with_token_program(
buyer,
buyer_tokens,
seller,
mint,
amount,
ttl_seconds,
payment_uid,
sla_hash,
oracle_authority,
spl_token::ID,
)
}
#[allow(clippy::too_many_arguments)]
pub fn fund_payment_with_token_program(
buyer: Pubkey,
buyer_tokens: Option<Pubkey>,
seller: Pubkey,
mint: Pubkey,
amount: u64,
ttl_seconds: i64,
payment_uid: &str,
sla_hash: [u8; 32],
oracle_authority: Pubkey,
token_program: Pubkey,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let (payment_pda, _) = crate::state::payment_pda(payment_uid, bank_pda);
let is_sol = mint == Pubkey::default();
let mut accounts = vec![
AccountMeta::new(buyer, true),
AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new(escrow_pda, false),
AccountMeta::new(payment_pda, false),
AccountMeta::new_readonly(mint, false),
];
if is_sol {
let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
accounts.push(AccountMeta::new(sol_storage_pda, false));
accounts.push(AccountMeta::new_readonly(
solana_program::system_program::ID,
false,
));
} else {
let buyer_tokens = buyer_tokens.expect("buyer_tokens required for SPL tokens");
let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
accounts.push(AccountMeta::new(escrow_tokens, false));
accounts.push(AccountMeta::new(buyer_tokens, false));
accounts.push(AccountMeta::new_readonly(token_program, false));
accounts.push(AccountMeta::new_readonly(
solana_program::system_program::ID,
false,
));
}
Instruction {
program_id: Self::program_id(),
accounts,
data: FundPayment {
seller,
mint,
oracle_authority,
payment_uid: normalize_payment_uid(payment_uid),
sla_hash,
amount: amount.to_le_bytes(),
ttl_seconds: ttl_seconds.to_le_bytes(),
}
.to_bytes(),
}
}
pub fn release_payment(
caller: Pubkey,
seller_tokens: Option<Pubkey>, seller: Option<Pubkey>, mint: Pubkey,
payment_uid: &str,
oracle_tokens: Option<Pubkey>,
oracle_authority: Option<Pubkey>,
) -> Instruction {
Self::release_payment_with_token_program(
caller,
seller_tokens,
seller,
mint,
payment_uid,
oracle_tokens,
oracle_authority,
spl_token::ID,
)
}
#[allow(clippy::too_many_arguments)]
pub fn release_payment_with_token_program(
caller: Pubkey,
seller_tokens: Option<Pubkey>,
seller: Option<Pubkey>,
mint: Pubkey,
payment_uid: &str,
oracle_tokens: Option<Pubkey>,
oracle_authority: Option<Pubkey>,
token_program: Pubkey,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
let is_sol = mint == Pubkey::default();
let mut accounts = vec![
AccountMeta::new(caller, true),
AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new(escrow_pda, false),
AccountMeta::new(payment_pda, false),
AccountMeta::new_readonly(mint, false),
];
if is_sol {
let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
accounts.push(AccountMeta::new(sol_storage_pda, false));
accounts.push(AccountMeta::new(
seller.expect("seller required for SOL"),
false,
));
accounts.push(AccountMeta::new_readonly(
solana_program::system_program::ID,
false,
));
if let Some(oracle_authority) = oracle_authority {
accounts.push(AccountMeta::new(oracle_authority, false));
}
} else {
let seller_tokens = seller_tokens.expect("seller_tokens required for SPL tokens");
let seller = seller.expect("seller account required for SPL tokens");
let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
accounts.push(AccountMeta::new(escrow_tokens, false));
accounts.push(AccountMeta::new(seller_tokens, false));
accounts.push(AccountMeta::new(seller, false));
accounts.push(AccountMeta::new_readonly(token_program, false));
accounts.push(AccountMeta::new_readonly(
spl_associated_token_account::ID,
false,
));
accounts.push(AccountMeta::new_readonly(
solana_program::system_program::ID,
false,
));
if let (Some(oracle_tokens), Some(oracle_authority)) = (oracle_tokens, oracle_authority)
{
accounts.push(AccountMeta::new(oracle_tokens, false));
accounts.push(AccountMeta::new(oracle_authority, false));
}
}
Instruction {
program_id: Self::program_id(),
accounts,
data: ReleasePayment {}.to_bytes(),
}
}
pub fn refund_payment(
caller: Pubkey,
buyer_tokens: Option<Pubkey>, mint: Pubkey,
payment_uid: &str,
oracle_tokens: Option<Pubkey>,
oracle_authority: Option<Pubkey>,
) -> Instruction {
Self::refund_payment_with_token_program(
caller,
buyer_tokens,
mint,
payment_uid,
oracle_tokens,
oracle_authority,
spl_token::ID,
)
}
#[allow(clippy::too_many_arguments)]
pub fn refund_payment_with_token_program(
caller: Pubkey,
buyer_tokens: Option<Pubkey>,
mint: Pubkey,
payment_uid: &str,
oracle_tokens: Option<Pubkey>,
oracle_authority: Option<Pubkey>,
token_program: Pubkey,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
let is_sol = mint == Pubkey::default();
let mut accounts = vec![
AccountMeta::new(caller, true),
AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new(escrow_pda, false),
AccountMeta::new(payment_pda, false),
AccountMeta::new_readonly(mint, false),
];
if is_sol {
let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
accounts.push(AccountMeta::new(sol_storage_pda, false));
accounts.push(AccountMeta::new(
buyer_tokens.expect("buyer required for SOL"),
false,
));
accounts.push(AccountMeta::new_readonly(
solana_program::system_program::ID,
false,
));
if let Some(oracle_authority) = oracle_authority {
accounts.push(AccountMeta::new(oracle_authority, false));
}
} else {
let buyer_tokens = buyer_tokens.expect("buyer_tokens required for SPL tokens");
let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
accounts.push(AccountMeta::new(escrow_tokens, false));
accounts.push(AccountMeta::new(buyer_tokens, false));
accounts.push(AccountMeta::new_readonly(token_program, false));
if let (Some(oracle_tokens), Some(oracle_authority)) = (oracle_tokens, oracle_authority)
{
accounts.push(AccountMeta::new(oracle_tokens, false));
accounts.push(AccountMeta::new(oracle_authority, false));
accounts.push(AccountMeta::new_readonly(
spl_associated_token_account::ID,
false,
));
accounts.push(AccountMeta::new_readonly(
solana_program::system_program::ID,
false,
));
}
}
Instruction {
program_id: Self::program_id(),
accounts,
data: RefundPayment {}.to_bytes(),
}
}
pub fn submit_delivery(
caller: Pubkey,
mint: Pubkey,
payment_uid: &str,
delivery_hash: [u8; 32],
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(caller, true), AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new_readonly(escrow_pda, false),
AccountMeta::new(payment_pda, false),
],
data: SubmitDelivery { delivery_hash }.to_bytes(),
}
}
pub fn close_payment(
caller: Pubkey,
buyer: Pubkey,
mint: Pubkey,
payment_uid: &str,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(caller, true), AccountMeta::new(buyer, false), AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new(escrow_pda, false),
AccountMeta::new(payment_pda, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
],
data: ClosePayment {}.to_bytes(),
}
}
pub fn withdraw_fees(
authority: Pubkey,
beneficiary: Pubkey,
mint: Pubkey,
amount: u64,
) -> Instruction {
Self::withdraw_fees_with_token_program(authority, beneficiary, mint, amount, spl_token::ID)
}
pub fn withdraw_fees_with_token_program(
authority: Pubkey,
beneficiary: Pubkey,
mint: Pubkey,
amount: u64,
token_program: Pubkey,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let is_sol = mint == Pubkey::default();
let mut accounts = vec![
AccountMeta::new(authority, true),
AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new(escrow_pda, false),
AccountMeta::new(beneficiary, false),
];
if is_sol {
let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
accounts.push(AccountMeta::new(sol_storage_pda, false));
accounts.push(AccountMeta::new_readonly(
solana_program::system_program::ID,
false,
));
} else {
let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
let beneficiary_tokens =
Self::associated_token_account_with_program(beneficiary, mint, token_program);
accounts[4] = AccountMeta::new(escrow_tokens, false);
accounts.push(AccountMeta::new(beneficiary_tokens, false));
accounts.push(AccountMeta::new_readonly(mint, false));
accounts.push(AccountMeta::new_readonly(token_program, false));
}
Instruction {
program_id: Self::program_id(),
accounts,
data: WithdrawFees {
amount: amount.to_le_bytes(),
}
.to_bytes(),
}
}
pub fn extend_payment_ttl(
caller: Pubkey,
mint: Pubkey,
payment_uid: &str,
additional_seconds: i64,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(caller, true),
AccountMeta::new_readonly(bank_pda, false), AccountMeta::new_readonly(config_pda, false),
AccountMeta::new_readonly(escrow_pda, false),
AccountMeta::new(payment_pda, false), AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
],
data: ExtendPaymentTTL {
additional_seconds: additional_seconds.to_le_bytes(),
}
.to_bytes(),
}
}
pub fn close_escrow(authority: Pubkey, recipient: Pubkey, mint: Pubkey) -> Instruction {
Self::close_escrow_with_token_program(authority, recipient, mint, spl_token::ID)
}
pub fn close_escrow_with_token_program(
authority: Pubkey,
recipient: Pubkey,
mint: Pubkey,
token_program: Pubkey,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let escrow_tokens = if mint == Pubkey::default() {
let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
sol_storage_pda
} else {
Self::escrow_token_account_with_program(mint, token_program)
};
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(authority, true),
AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new(escrow_pda, false),
AccountMeta::new(escrow_tokens, false),
AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(token_program, false),
AccountMeta::new(recipient, false),
],
data: CloseEscrow {}.to_bytes(),
}
}
pub fn update_escrow_settings(
authority: Pubkey,
escrow: Pubkey,
fee_bps: u16,
min_payment_amount: u64,
max_payment_amount: u64,
min_fee_amount: u64,
oracle_fee_bps: u16,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(authority, true),
AccountMeta::new_readonly(bank_pda, false), AccountMeta::new_readonly(config_pda, false),
AccountMeta::new(escrow, false), AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
],
data: UpdateEscrowSettings {
min_payment_amount: min_payment_amount.to_le_bytes(),
max_payment_amount: max_payment_amount.to_le_bytes(),
min_fee_amount: min_fee_amount.to_le_bytes(),
new_fee_bps: fee_bps.to_le_bytes(),
new_oracle_fee_bps: oracle_fee_bps.to_le_bytes(),
_padding: [0; 4],
}
.to_bytes(),
}
}
pub fn pause_escrow(authority: Pubkey, mint: Pubkey, pause: bool) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(authority, true),
AccountMeta::new_readonly(bank_pda, false), AccountMeta::new_readonly(config_pda, false),
AccountMeta::new(escrow_pda, false), AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
],
data: PauseEscrow {
pause: if pause { 1 } else { 0 },
}
.to_bytes(),
}
}
pub fn update_authority(current_authority: Pubkey, new_authority: Pubkey) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (transfer_pda, _) = Self::authority_transfer_pda(bank_pda);
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(current_authority, true),
AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new(transfer_pda, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
],
data: UpdateAuthority { new_authority }.to_bytes(),
}
}
pub fn accept_authority(new_authority: Pubkey) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (transfer_pda, _) = Self::authority_transfer_pda(bank_pda);
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(new_authority, true), AccountMeta::new(bank_pda, false), AccountMeta::new(transfer_pda, false), ],
data: AcceptAuthority {}.to_bytes(),
}
}
pub fn cancel_authority_proposal(current_authority: Pubkey) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (transfer_pda, _) = Self::authority_transfer_pda(bank_pda);
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(current_authority, true), AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new(transfer_pda, false), ],
data: CancelAuthorityProposal {}.to_bytes(),
}
}
pub fn update_config(
admin: Pubkey,
closure_delay_seconds: i64,
refund_cooldown_seconds: i64,
delivery_cutoff_seconds: i64,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(admin, true),
AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new(config_pda, false),
],
data: UpdateConfig {
closure_delay_seconds: closure_delay_seconds.to_le_bytes(),
refund_cooldown_seconds: refund_cooldown_seconds.to_le_bytes(),
delivery_cutoff_seconds: delivery_cutoff_seconds.to_le_bytes(),
}
.to_bytes(),
}
}
pub fn confirm_oracle(
oracle_authority: Pubkey,
mint: Pubkey,
payment_uid: &str,
delivery_hash: [u8; 32],
resolution_hash: [u8; 32],
resolution_state: u8,
resolution_reason: u16,
) -> Instruction {
let (bank_pda, _) = Self::bank_pda();
let (config_pda, _) = Self::config_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
Instruction {
program_id: Self::program_id(),
accounts: vec![
AccountMeta::new(oracle_authority, true), AccountMeta::new_readonly(bank_pda, false),
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new_readonly(escrow_pda, false),
AccountMeta::new(payment_pda, false), ],
data: ConfirmOracle {
delivery_hash,
resolution_hash,
resolution_reason: resolution_reason.to_le_bytes(),
resolution_state,
_padding: [0; 5],
}
.to_bytes(),
}
}
pub fn validate_payment_for_funding(payment: &Payment) -> Result<(), EscrowError> {
if payment.state != PAYMENT_STATE_FUNDED {
return Err(EscrowError::InvalidPaymentState);
}
Ok(())
}
pub fn validate_payment_for_release(payment: &Payment) -> Result<(), EscrowError> {
if payment.state != PAYMENT_STATE_FUNDED {
return Err(EscrowError::InvalidPaymentState);
}
Ok(())
}
pub fn validate_payment_for_refund(payment: &Payment) -> Result<(), EscrowError> {
if payment.state != PAYMENT_STATE_FUNDED {
return Err(EscrowError::InvalidPaymentState);
}
Ok(())
}
pub fn calculate_fee(amount: u64, fee_bps: u16, min_fee_amount: u64) -> u64 {
let fee_bps = fee_bps as u128;
let amount_128 = amount as u128;
let calculated_fee = ((amount_128 * fee_bps) / BASIS_POINTS_DENOMINATOR) as u64;
calculated_fee.max(min_fee_amount).min(amount)
}
pub fn calculate_payout(amount: u64, fee_bps: u16, min_fee_amount: u64) -> u64 {
let fee = Self::calculate_fee(amount, fee_bps, min_fee_amount);
amount.saturating_sub(fee)
}
pub fn is_payment_expired(payment: &Payment) -> bool {
let clock = solana_program::clock::Clock::get().unwrap_or_default();
clock.unix_timestamp > payment.expires_at
}
pub fn get_payment_state_string(state: u8) -> &'static str {
match state {
PAYMENT_STATE_FUNDED => "Funded",
PAYMENT_STATE_RELEASED => "Released",
PAYMENT_STATE_REFUNDED => "Refunded",
_ => "Unknown",
}
}
pub fn get_escrow_state_string(paused: u8) -> &'static str {
match paused {
0 => "Active",
1 => "Paused",
_ => "Unknown",
}
}
pub fn get_payment_accounts(
buyer: Pubkey,
seller: Pubkey,
mint: Pubkey,
payment_uid: &str,
) -> PaymentAccounts {
Self::get_payment_accounts_with_token_program(
buyer,
seller,
mint,
payment_uid,
spl_token::ID,
)
}
pub fn get_payment_accounts_with_token_program(
buyer: Pubkey,
seller: Pubkey,
mint: Pubkey,
payment_uid: &str,
token_program: Pubkey,
) -> PaymentAccounts {
let (bank_pda, _) = Self::bank_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
PaymentAccounts {
buyer,
seller,
mint,
bank: bank_pda,
escrow: escrow_pda,
payment: payment_pda,
escrow_tokens,
}
}
pub fn get_escrow_accounts(mint: Pubkey) -> EscrowAccounts {
Self::get_escrow_accounts_with_token_program(mint, spl_token::ID)
}
pub fn get_escrow_accounts_with_token_program(
mint: Pubkey,
token_program: Pubkey,
) -> EscrowAccounts {
let (bank_pda, _) = Self::bank_pda();
let (escrow_pda, _) = Self::escrow_pda(mint);
let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
EscrowAccounts {
mint,
bank: bank_pda,
escrow: escrow_pda,
escrow_tokens,
}
}
}
#[derive(Debug, Clone)]
pub struct PaymentAccounts {
pub buyer: Pubkey,
pub seller: Pubkey,
pub mint: Pubkey,
pub bank: Pubkey,
pub escrow: Pubkey,
pub payment: Pubkey,
pub escrow_tokens: Pubkey,
}
#[derive(Debug, Clone)]
pub struct EscrowAccounts {
pub mint: Pubkey,
pub bank: Pubkey,
pub escrow: Pubkey,
pub escrow_tokens: Pubkey,
}
pub struct PaymentBuilder {
buyer: Option<Pubkey>,
buyer_tokens: Option<Pubkey>,
seller: Option<Pubkey>,
mint: Option<Pubkey>,
token_program: Option<Pubkey>,
amount: Option<u64>,
ttl_seconds: Option<i64>,
payment_uid: Option<String>,
sla_hash: Option<[u8; 32]>,
oracle_authority: Option<Pubkey>,
}
impl PaymentBuilder {
pub fn new() -> Self {
Self {
buyer: None,
buyer_tokens: None,
seller: None,
mint: None,
token_program: None,
amount: None,
ttl_seconds: None,
payment_uid: None,
sla_hash: None,
oracle_authority: None,
}
}
pub fn buyer(mut self, buyer: Pubkey) -> Self {
self.buyer = Some(buyer);
self
}
pub fn buyer_tokens(mut self, buyer_tokens: Pubkey) -> Self {
self.buyer_tokens = Some(buyer_tokens);
self
}
pub fn seller(mut self, seller: Pubkey) -> Self {
self.seller = Some(seller);
self
}
pub fn mint(mut self, mint: Pubkey) -> Self {
self.mint = Some(mint);
self
}
pub fn token_program(mut self, token_program: Pubkey) -> Self {
self.token_program = Some(token_program);
self
}
pub fn amount(mut self, amount: u64) -> Self {
self.amount = Some(amount);
self
}
pub fn ttl_seconds(mut self, ttl_seconds: i64) -> Self {
self.ttl_seconds = Some(ttl_seconds);
self
}
pub fn payment_uid(mut self, payment_uid: &str) -> Self {
self.payment_uid = Some(payment_uid.to_string());
self
}
pub fn sla_hash(mut self, sla_hash: [u8; 32]) -> Self {
self.sla_hash = Some(sla_hash);
self
}
pub fn oracle_authority(mut self, oracle_authority: Pubkey) -> Self {
self.oracle_authority = Some(oracle_authority);
self
}
pub fn build(self) -> Result<Instruction, EscrowError> {
let buyer = self.buyer.ok_or(EscrowError::MissingRequiredField)?;
let buyer_tokens = self.buyer_tokens; let seller = self.seller.ok_or(EscrowError::MissingRequiredField)?;
let mint = self.mint.ok_or(EscrowError::MissingRequiredField)?;
let token_program = self.token_program.unwrap_or(spl_token::ID);
let amount = self.amount.ok_or(EscrowError::MissingRequiredField)?;
let ttl_seconds = self.ttl_seconds.ok_or(EscrowError::MissingRequiredField)?;
let payment_uid = self.payment_uid.ok_or(EscrowError::MissingRequiredField)?;
let sla_hash = self.sla_hash.unwrap_or([0; 32]);
let oracle_authority = self.oracle_authority.unwrap_or_default();
Ok(EscrowSdk::fund_payment_with_token_program(
buyer,
buyer_tokens,
seller,
mint,
amount,
ttl_seconds,
&payment_uid,
sla_hash,
oracle_authority,
token_program,
))
}
}
impl Default for PaymentBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct EscrowBuilder {
authority: Option<Pubkey>,
funder: Option<Pubkey>,
mint: Option<Pubkey>,
token_program: Option<Pubkey>,
min_payment_amount: Option<u64>,
max_payment_amount: Option<u64>,
min_fee_amount: Option<u64>,
fee_bps: Option<u16>,
oracle_fee_bps: Option<u16>,
}
impl EscrowBuilder {
pub fn new() -> Self {
Self {
authority: None,
funder: None,
mint: None,
token_program: None,
min_payment_amount: None,
max_payment_amount: None,
min_fee_amount: None,
fee_bps: None,
oracle_fee_bps: None,
}
}
pub fn authority(mut self, authority: Pubkey) -> Self {
self.authority = Some(authority);
self
}
pub fn funder(mut self, funder: Pubkey) -> Self {
self.funder = Some(funder);
self
}
pub fn mint(mut self, mint: Pubkey) -> Self {
self.mint = Some(mint);
self
}
pub fn token_program(mut self, token_program: Pubkey) -> Self {
self.token_program = Some(token_program);
self
}
pub fn min_payment_amount(mut self, min_payment_amount: u64) -> Self {
self.min_payment_amount = Some(min_payment_amount);
self
}
pub fn max_payment_amount(mut self, max_payment_amount: u64) -> Self {
self.max_payment_amount = Some(max_payment_amount);
self
}
pub fn min_fee_amount(mut self, min_fee_amount: u64) -> Self {
self.min_fee_amount = Some(min_fee_amount);
self
}
pub fn fee_bps(mut self, fee_bps: u16) -> Self {
self.fee_bps = Some(fee_bps);
self
}
pub fn oracle_fee_bps(mut self, oracle_fee_bps: u16) -> Self {
self.oracle_fee_bps = Some(oracle_fee_bps);
self
}
pub fn create_escrow(self) -> Result<Instruction, EscrowError> {
let authority = self.authority.ok_or(EscrowError::MissingRequiredField)?;
let funder = self.funder.ok_or(EscrowError::MissingRequiredField)?;
let mint = self.mint.ok_or(EscrowError::MissingRequiredField)?;
let token_program = self.token_program.unwrap_or(spl_token::ID);
let min_payment_amount = self
.min_payment_amount
.unwrap_or(crate::consts::DEFAULT_MIN_PAYMENT_RAW_USDC_DECIMALS_6);
let max_payment_amount = self.max_payment_amount.unwrap_or(u64::MAX); let min_fee_amount = self
.min_fee_amount
.unwrap_or(crate::consts::DEFAULT_MIN_FEE_RAW_USDC_DECIMALS_6);
let fee_bps = self.fee_bps.unwrap_or(u16::MAX);
let oracle_fee_bps = self.oracle_fee_bps.unwrap_or(0);
Ok(EscrowSdk::open_escrow_with_token_program(
authority,
funder,
mint,
min_payment_amount,
max_payment_amount,
min_fee_amount,
fee_bps,
oracle_fee_bps,
token_program,
))
}
pub fn pause_escrow(self, paused: bool) -> Result<Instruction, EscrowError> {
let authority = self.authority.ok_or(EscrowError::MissingRequiredField)?;
let mint = self.mint.ok_or(EscrowError::MissingRequiredField)?;
Ok(EscrowSdk::pause_escrow(authority, mint, paused))
}
}
impl Default for EscrowBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_program_aware_ata_derivation_changes_address() {
let wallet = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let legacy = EscrowSdk::associated_token_account(wallet, mint);
let token2022 = EscrowSdk::associated_token_account_with_program(
wallet,
mint,
crate::consts::TOKEN_2022_PROGRAM_ID,
);
assert_ne!(legacy, token2022);
}
#[test]
fn close_escrow_uses_supplied_token_program() {
let authority = Pubkey::new_unique();
let recipient = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let instruction = EscrowSdk::close_escrow_with_token_program(
authority,
recipient,
mint,
crate::consts::TOKEN_2022_PROGRAM_ID,
);
assert_eq!(
instruction.accounts[3].pubkey,
EscrowSdk::escrow_token_account_with_program(
mint,
crate::consts::TOKEN_2022_PROGRAM_ID,
)
);
assert_eq!(
instruction.accounts[5].pubkey,
crate::consts::TOKEN_2022_PROGRAM_ID
);
}
}
pub mod tokens {
use solana_program::pubkey::Pubkey;
pub const USDC: Pubkey =
solana_program::pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
pub const USDT: Pubkey =
solana_program::pubkey!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");
pub const WSOL: Pubkey = solana_program::pubkey!("So11111111111111111111111111111111111111112");
pub const MARS: Pubkey =
solana_program::pubkey!("7RAV5UPRTzxn46kLeA8MiJsdNy9VKc5fip8FWEgTpTHh");
pub const MIRACLE: Pubkey =
solana_program::pubkey!("Mirab4SFVff6sCuK48PPnSUj7PNpDDrBWY6FkJmuifG");
pub const TESTCOIN: Pubkey =
solana_program::pubkey!("2gNCDGj8Xi9Zs7LNQTPWf4pfZvAM7UHusY4xhKNYg6W6");
}
pub mod payment_states {
use crate::consts::{PAYMENT_STATE_FUNDED, PAYMENT_STATE_REFUNDED, PAYMENT_STATE_RELEASED};
pub const FUNDED: u8 = PAYMENT_STATE_FUNDED;
pub const RELEASED: u8 = PAYMENT_STATE_RELEASED;
pub const REFUNDED: u8 = PAYMENT_STATE_REFUNDED;
}
pub mod escrow_states {
pub const ACTIVE: u8 = 0;
pub const PAUSED: u8 = 1;
}