From 0fa2b49880b378a7837f2d36f644b7204f356366 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:22:59 +0200 Subject: [PATCH] chore: add helper examples to calculate program PDAs These is needed because SPEL currently doesn't support PDA calculation the way our programs do (we wrap our seeds in SHA256) --- programs/amm/examples/amm_pdas.rs | 90 +++++++++++++++++++ programs/ata/examples/ata_pdas.rs | 61 +++++++++++++ .../twap_oracle/examples/twap_oracle_pdas.rs | 82 +++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 programs/amm/examples/amm_pdas.rs create mode 100644 programs/ata/examples/ata_pdas.rs create mode 100644 programs/twap_oracle/examples/twap_oracle_pdas.rs diff --git a/programs/amm/examples/amm_pdas.rs b/programs/amm/examples/amm_pdas.rs new file mode 100644 index 0000000..06d1783 --- /dev/null +++ b/programs/amm/examples/amm_pdas.rs @@ -0,0 +1,90 @@ +//! Print the AMM PDAs for a deployment (and, given a token pair, a pool's PDAs). +//! +//! Usage: +//! cargo run -q -p amm_program --example amm_pdas -- [ ] +//! +//! `*_pid` are ProgramIds as 8 comma-separated u32 limbs (as printed by `spel program-id`); +//! `defA`/`defB` are base58 token-definition account ids. With only `` it prints the +//! singleton config PDA; with all four args it also prints the pool/vault/LP/lock/tick PDAs. + +use std::str::FromStr; + +use amm_core::{ + compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda, + compute_vault_pda, +}; +use nssa_core::{account::AccountId, program::ProgramId}; +use twap_oracle_core::compute_current_tick_account_pda; + +// Accepts a ProgramId as 8 comma-separated u32 limbs, a 64-char ImageID hex, or a base58 +// ImageID. Hex/base58 are decoded as the 32 ImageID bytes read little-endian per u32 word, +// matching how `spel program-id` maps the ImageID to limbs. +fn parse_pid(s: &str) -> ProgramId { + if s.contains(',') { + let limbs: Vec = s + .split(',') + .map(|x| x.trim().parse().expect("ProgramId limb must be a u32")) + .collect(); + assert_eq!(limbs.len(), 8, "ProgramId must be 8 u32 limbs"); + let mut pid: ProgramId = [0u32; 8]; + pid.copy_from_slice(&limbs); + return pid; + } + let bytes: [u8; 32] = if s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()) { + let mut out = [0u8; 32]; + for (byte, pair) in out.iter_mut().zip(s.as_bytes().chunks_exact(2)) { + let pair: [u8; 2] = pair.try_into().expect("hex pair"); + let hex = std::str::from_utf8(&pair).expect("ascii hex"); + *byte = u8::from_str_radix(hex, 16).expect("invalid hex digit"); + } + out + } else { + AccountId::from_str(s) + .expect("ProgramId must be 8 u32 limbs, a 64-char hex ImageID, or base58") + .into_value() + }; + let mut pid: ProgramId = [0u32; 8]; + for (limb, chunk) in pid.iter_mut().zip(bytes.chunks_exact(4)) { + *limb = u32::from_le_bytes(chunk.try_into().expect("4-byte chunk")); + } + pid +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let Some((amm_s, rest)) = args.split_first() else { + eprintln!("usage: amm_pdas [ ]"); + std::process::exit(1); + }; + let amm = parse_pid(amm_s); + let config = compute_config_pda(amm); + println!("config {config}"); + + if let [twap_s, def_a_s, def_b_s] = rest { + let twap = parse_pid(twap_s); + let def_a = AccountId::from_str(def_a_s).expect("defA must be base58"); + let def_b = AccountId::from_str(def_b_s).expect("defB must be base58"); + let pool = compute_pool_pda(amm, def_a, def_b); + println!("pool {pool}"); + println!( + "vault_a {}", + compute_vault_pda(amm, pool, def_a) + ); + println!( + "vault_b {}", + compute_vault_pda(amm, pool, def_b) + ); + println!( + "pool_definition_lp {}", + compute_liquidity_token_pda(amm, pool) + ); + println!( + "lp_lock_holding {}", + compute_lp_lock_holding_pda(amm, pool) + ); + println!( + "current_tick_account {}", + compute_current_tick_account_pda(twap, pool) + ); + } +} diff --git a/programs/ata/examples/ata_pdas.rs b/programs/ata/examples/ata_pdas.rs new file mode 100644 index 0000000..1b7481d --- /dev/null +++ b/programs/ata/examples/ata_pdas.rs @@ -0,0 +1,61 @@ +//! Print the Associated Token Account (ATA) address for an owner + token definition. +//! +//! Usage: +//! cargo run -q -p ata_program --example ata_pdas -- +//! +//! `*_pid` are ProgramIds as 8 comma-separated u32 limbs (as printed by `spel program-id`); +//! `owner` and `definition` are base58 account ids. + +use std::str::FromStr; + +use ata_core::{compute_ata_seed, get_associated_token_account_id}; +use nssa_core::{account::AccountId, program::ProgramId}; + +// Accepts a ProgramId as 8 comma-separated u32 limbs, a 64-char ImageID hex, or a base58 +// ImageID. Hex/base58 are decoded as the 32 ImageID bytes read little-endian per u32 word, +// matching how `spel program-id` maps the ImageID to limbs. +fn parse_pid(s: &str) -> ProgramId { + if s.contains(',') { + let limbs: Vec = s + .split(',') + .map(|x| x.trim().parse().expect("ProgramId limb must be a u32")) + .collect(); + assert_eq!(limbs.len(), 8, "ProgramId must be 8 u32 limbs"); + let mut pid: ProgramId = [0u32; 8]; + pid.copy_from_slice(&limbs); + return pid; + } + let bytes: [u8; 32] = if s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()) { + let mut out = [0u8; 32]; + for (byte, pair) in out.iter_mut().zip(s.as_bytes().chunks_exact(2)) { + let pair: [u8; 2] = pair.try_into().expect("hex pair"); + let hex = std::str::from_utf8(&pair).expect("ascii hex"); + *byte = u8::from_str_radix(hex, 16).expect("invalid hex digit"); + } + out + } else { + AccountId::from_str(s) + .expect("ProgramId must be 8 u32 limbs, a 64-char hex ImageID, or base58") + .into_value() + }; + let mut pid: ProgramId = [0u32; 8]; + for (limb, chunk) in pid.iter_mut().zip(bytes.chunks_exact(4)) { + *limb = u32::from_le_bytes(chunk.try_into().expect("4-byte chunk")); + } + pid +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let [ata_s, token_s, owner_s, def_s] = args.as_slice() else { + eprintln!("usage: ata_pdas "); + std::process::exit(1); + }; + let ata = parse_pid(ata_s); + let token = parse_pid(token_s); + let owner = AccountId::from_str(owner_s).expect("owner must be base58"); + let definition = AccountId::from_str(def_s).expect("definition must be base58"); + + let seed = compute_ata_seed(token, owner, definition); + println!("ata {}", get_associated_token_account_id(&ata, &seed)); +} diff --git a/programs/twap_oracle/examples/twap_oracle_pdas.rs b/programs/twap_oracle/examples/twap_oracle_pdas.rs new file mode 100644 index 0000000..d62c843 --- /dev/null +++ b/programs/twap_oracle/examples/twap_oracle_pdas.rs @@ -0,0 +1,82 @@ +//! Print the TWAP oracle PDAs for a price source (current-tick always; the windowed +//! price-observations / oracle-price accounts when a window duration is given). +//! +//! Usage: +//! cargo run -q -p twap_oracle_program --example twap_oracle_pdas -- +//! [] +//! +//! `oracle_pid` is a ProgramId as 8 comma-separated u32 limbs (as printed by `spel program-id`); +//! `price_source` is a base58 account id (e.g. an AMM pool); `window_duration` is a u64. + +use std::str::FromStr; + +use nssa_core::{account::AccountId, program::ProgramId}; +use twap_oracle_core::{ + compute_current_tick_account_pda, compute_oracle_price_account_pda, + compute_price_observations_pda, +}; + +// Accepts a ProgramId as 8 comma-separated u32 limbs, a 64-char ImageID hex, or a base58 +// ImageID. Hex/base58 are decoded as the 32 ImageID bytes read little-endian per u32 word, +// matching how `spel program-id` maps the ImageID to limbs. +fn parse_pid(s: &str) -> ProgramId { + if s.contains(',') { + let limbs: Vec = s + .split(',') + .map(|x| x.trim().parse().expect("ProgramId limb must be a u32")) + .collect(); + assert_eq!(limbs.len(), 8, "ProgramId must be 8 u32 limbs"); + let mut pid: ProgramId = [0u32; 8]; + pid.copy_from_slice(&limbs); + return pid; + } + let bytes: [u8; 32] = if s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()) { + let mut out = [0u8; 32]; + for (byte, pair) in out.iter_mut().zip(s.as_bytes().chunks_exact(2)) { + let pair: [u8; 2] = pair.try_into().expect("hex pair"); + let hex = std::str::from_utf8(&pair).expect("ascii hex"); + *byte = u8::from_str_radix(hex, 16).expect("invalid hex digit"); + } + out + } else { + AccountId::from_str(s) + .expect("ProgramId must be 8 u32 limbs, a 64-char hex ImageID, or base58") + .into_value() + }; + let mut pid: ProgramId = [0u32; 8]; + for (limb, chunk) in pid.iter_mut().zip(bytes.chunks_exact(4)) { + *limb = u32::from_le_bytes(chunk.try_into().expect("4-byte chunk")); + } + pid +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let (oracle_s, source_s, window_s) = match args.as_slice() { + [oracle, source] => (oracle, source, None), + [oracle, source, window] => (oracle, source, Some(window)), + _ => { + eprintln!("usage: twap_oracle_pdas []"); + std::process::exit(1); + } + }; + let oracle = parse_pid(oracle_s); + let source = AccountId::from_str(source_s).expect("price source must be base58"); + + println!( + "current_tick_account {}", + compute_current_tick_account_pda(oracle, source) + ); + + if let Some(window_s) = window_s { + let window: u64 = window_s.parse().expect("window_duration must be a u64"); + println!( + "price_observations {}", + compute_price_observations_pda(oracle, source, window) + ); + println!( + "oracle_price_account {}", + compute_oracle_price_account_pda(oracle, source, window) + ); + } +}