use anyhow::Context as _;
use clap::{Args as ClapArgs, Parser};
use cow_settlement_client::{
cow_settlement_interface::{
data::intent::{Asset, Flags, OrderIntent, OrderKind, TokenAsset},
pda::order::find_order_pda,
},
instruction::CreateOrder,
};
use solana_sdk::{signature::Signer, transaction::Transaction};
use std::num::NonZeroU64;
use std::time::{SystemTime, UNIX_EPOCH};
use super::Context;
use crate::utils::{self, output::print_summary, token::TokenAccountDetails};
#[derive(ClapArgs)]
struct CommonArgs {
#[arg(long, default_value_t = valid_to_in(300))]
valid_to: u32,
#[arg(long)]
partially_fillable: bool,
}
#[derive(Parser)]
#[command(long_about = "\
Create an order to sell or buy exactly N token, per the subcommand used \
(`cow sell` / `cow buy`). This only creates the order on-chain; it does not \
itself execute the swap. Supported forms:
cow sell 1.0 SOL USDC sell exactly 1.0 SOL, receive any USDC
cow sell 1.0 SOL 50.0 USDC sell exactly 1.0 SOL, receive ≥ 50.0 USDC
cow sell 1.0 SOL for 50.0 USDC same as before, but more english
cow sell 1.0 USDC sell 1.0 USDC into SOL (SOL implied as buy token)
cow buy 1.0 SOL 100.0 USDC buy exactly 1.0 SOL, spend at most 100.0 USDC
cow buy 1.0 SOL for 100.0 USDC same as before, but more english
cow buy 1.0 SOL USDC buy exactly 1.0 SOL, spending any USDC
cow buy 1.0 USDC buy 1.0 USDC, selling any amount of SOL (implied)
Tokens can be a builtin symbol (SOL, WSOL, USDC), a mint address, or a token-account address.")]
pub struct BuyOrSellArgs {
#[arg(num_args = 2..=5)]
terms: Vec<String>,
#[command(flatten)]
common: CommonArgs,
}
pub fn run_sell(ctx: Context, args: BuyOrSellArgs) -> anyhow::Result<()> {
let BuyOrSellArgs { terms, common } = args;
let parsed = parse(&ctx, OrderKind::Sell, &terms)?;
execute(ctx, parsed, common)
}
pub fn run_buy(ctx: Context, args: BuyOrSellArgs) -> anyhow::Result<()> {
let BuyOrSellArgs { terms, common } = args;
let parsed = parse(&ctx, OrderKind::Buy, &terms)?;
execute(ctx, parsed, common)
}
struct ParsedOrder {
kind: OrderKind,
sell: TokenAccountDetails,
sell_amount: NonZeroU64,
sell_is_sol: bool,
buy: TokenAccountDetails,
buy_amount: NonZeroU64,
}
fn resolve_amount(amount: Option<&str>, decimals: u8) -> anyhow::Result<NonZeroU64> {
let Some(amount) = amount else {
return Ok(NonZeroU64::MIN);
};
let raw = spl_token::try_ui_amount_into_amount(amount.to_string(), decimals)
.map_err(|_| anyhow::anyhow!("invalid value {amount:?}"))?;
NonZeroU64::new(raw).context("must be non-zero")
}
fn parse(ctx: &Context, kind: OrderKind, terms: &[String]) -> anyhow::Result<ParsedOrder> {
let term_refs: Vec<&str> = terms.iter().map(String::as_str).collect();
let terms = &term_refs[..];
let [a_amount, a_tok, ..] = terms else {
anyhow::bail!(
"must provide at least an amount and a token; run `cow sell --help` for usage"
);
};
let (b_tok, b_amount): (&str, Option<&str>) = match terms[2..] {
[] => ("SOL", None),
["for" | "with", b] => (b, None),
["for" | "with", buy_amount, b] => (b, Some(buy_amount)),
[b] => (b, None),
[buy_amount, b] => (b, Some(buy_amount)),
_ => anyhow::bail!(
"cannot interpret {:?}; run `cow sell --help` for usage",
terms
),
};
let (sell_tok, sell_amount_str, buy_tok, buy_amount_str) = match kind {
OrderKind::Sell => (*a_tok, Some(*a_amount), b_tok, b_amount),
OrderKind::Buy => (b_tok, b_amount, *a_tok, Some(*a_amount)),
};
let sell = utils::token::resolve(&ctx.rpc, &ctx.payer.pubkey(), sell_tok)?;
let buy = utils::token::resolve(&ctx.rpc, &ctx.payer.pubkey(), buy_tok)?;
let sell_amount =
resolve_amount(sell_amount_str, sell.mint_data.decimals).context("sell amount")?;
let buy_amount =
resolve_amount(buy_amount_str, buy.mint_data.decimals).context("buy amount")?;
Ok(ParsedOrder {
kind,
sell_is_sol: sell_tok.eq_ignore_ascii_case("sol"),
sell,
sell_amount,
buy,
buy_amount,
})
}
fn execute(ctx: Context, parsed: ParsedOrder, common: CommonArgs) -> anyhow::Result<()> {
let ParsedOrder {
kind,
sell,
sell_amount,
sell_is_sol,
buy,
buy_amount,
} = parsed;
let mut ixs = Vec::new();
if sell_is_sol {
let (wsol_ata, wrap_ixs) =
utils::spl_instructions::wrap_sol(&ctx.rpc, &ctx.payer.pubkey(), sell_amount.get())?;
assert_eq!(wsol_ata, sell.handle.account, "resolved WSOL ATA mismatch");
ixs.extend(wrap_ixs);
}
ixs.extend(buy.create_ata_ix(&ctx.payer.pubkey()));
ixs.push(utils::spl_instructions::approve(
&ctx.program_id,
&sell.handle,
&ctx.payer.pubkey(),
sell_amount.get(),
)?);
let intent = OrderIntent {
owner: ctx.payer.pubkey(),
sell: TokenAsset {
mint: sell.handle.mint,
token_account: sell.handle.account,
},
buy: Asset::try_from(TokenAsset {
mint: buy.handle.mint,
token_account: buy.handle.account,
})
.map_err(|_| anyhow::anyhow!("buy mint {} is the native SOL marker", buy.handle.mint))?,
sell_amount,
buy_amount,
valid_to: common.valid_to,
flags: Flags {
kind,
partially_fillable: common.partially_fillable,
},
app_data: [0u8; 32],
};
let uid = intent.uid();
let (order_pda, _) = find_order_pda(&ctx.program_id, &uid);
let create_order_ix = CreateOrder {
program_id: ctx.program_id,
owner: ctx.payer.pubkey(),
created_by: ctx.payer.pubkey(),
intent: &intent,
};
ixs.push(create_order_ix.into());
let blockhash = ctx
.rpc
.get_latest_blockhash()
.context("failed to fetch blockhash")?;
let tx = Transaction::new_signed_with_payer(
&ixs,
Some(&ctx.payer.pubkey()),
&[&ctx.payer],
blockhash,
);
let sig = ctx
.rpc
.send_and_confirm_transaction(&tx)
.context("transaction failed")?;
let uid_hex: String = uid.as_ref().iter().map(|b| format!("{b:02x}")).collect();
print_summary(&[
("signature", &sig),
("orderPda", &order_pda),
("orderUid", &uid_hex),
]);
Ok(())
}
fn valid_to_in(secs_from_now: u64) -> u32 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
u32::try_from(now.saturating_add(secs_from_now)).unwrap_or(u32::MAX)
}