Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,315 changes: 2,702 additions & 1,613 deletions rust/Cargo.lock

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,16 @@ qrcode = { version = "0.14", default-features = false }
mime_guess = { version = "2.0", default-features = false, features = ["rev-mappings"] }

# Solana MPP (includes keypair loading + charge + subscription + authenticate
# builders). Tracks pay-kit `main`; PR #154 merged the SIWMPP work so the
# stable branch is the source of truth again.
solana-mpp = { git = "https://github.com/solana-foundation/pay-kit", branch = "main", default-features = false, features = [
# builders). Tracks the confidential-transfer + solana-4.0 branch (pay-kit
# PR #181) until it merges to main.
solana-mpp = { git = "https://github.com/solana-foundation/pay-kit", branch = "feat/confidential-transfers", default-features = false, features = [
"client",
"server",
"confidential",
] }

# Solana x402 (lives in the pay-kit workspace alongside solana-mpp)
solana-x402 = { git = "https://github.com/solana-foundation/pay-kit", branch = "main", package = "solana-x402", default-features = false, features = [
solana-x402 = { git = "https://github.com/solana-foundation/pay-kit", branch = "feat/confidential-transfers", package = "solana-x402", default-features = false, features = [
"client",
] }
Comment on lines +108 to 117

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Mutable branch references for production dependencies

Both solana-mpp and solana-x402 are pinned to branch = "feat/confidential-transfers" rather than a fixed rev =. The Cargo.lock does capture the commit hash at lock time, but any cargo update will silently pull whatever is at the HEAD of that branch at that moment — including force-pushed rewrites or last-minute breaking changes. Given the feature branch is still in review (PR #181), this is a concrete risk window. Consider locking to the current HEAD rev instead, e.g., rev = "abc1234", until the upstream branch stabilises or merges.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional and temporary: solana-mpp/solana-x402 track pay-kit PR #181's feature branch until it merges, then move to a pinned release/rev. Noted in the Cargo.toml comment and the dev-shims section of confidential-transfers.md.


Expand All @@ -126,8 +127,9 @@ solana-signature = "3.2"
solana-system-interface = "2.0"
solana-transaction = "3.1"

# Local override for end-to-end testing of unreleased pay-kit changes.
# Uncomment to redirect both crates to a local pay-kit checkout.
# [patch."https://github.com/solana-foundation/pay-kit"]
# solana-x402 = { path = "/Users/ludo/Coding/pay-kit/rust/crates/x402" }
# solana-mpp = { path = "/Users/ludo/Coding/pay-kit/rust/crates/mpp" }
# litesvm fork (solana-address pin loosened) so litesvm 0.13 — pulled by
# surfpool-sdk 1.4 — can coexist with the confidential proof crates (zk-sdk 7).
# Pending upstream PR; tracks the fork branch for now.
[patch.crates-io]
litesvm = { git = "https://github.com/lgalabru/litesvm.git", branch = "loosen-solana-address-constraint" }
litesvm-token = { git = "https://github.com/lgalabru/litesvm.git", branch = "loosen-solana-address-constraint" }
Comment on lines +133 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Personal fork pinned to a mutable branch in [patch.crates-io]

The litesvm / litesvm-token patch points to lgalabru/litesvm.git on a personal fork with branch = "loosen-solana-address-constraint". Personal forks can be renamed, deleted, or go stale; using a rev = pin would at least guarantee the exact commit is fetched regardless of branch mutations. This affects test builds (surfpool_tests, session_surfpool_sdk_tests).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporary dev shim: the fork only loosens litesvm's solana-address pin so litesvm can coexist with the zk-sdk 7 proof crates (no newer litesvm release exists). Pending the upstream litesvm PR; documented in confidential-transfers.md §6.1.

35 changes: 5 additions & 30 deletions rust/crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1196,36 +1196,11 @@ fn pay_session_and_retry(
header
}
Some(SessionPullVoucherStrategy::OperatedVoucher) => {
if verbose && !is_json {
eprintln!(
"{}",
format!(
"Opening pull operated-voucher session (deposit {} µUSDC, operator {})…",
deposit,
&request.operator[..8.min(request.operator.len())]
)
.dimmed()
);
}

let (_handle, header) = pay_core::session::open_pull_session_header(
challenge,
request,
&store,
network_override,
account_override,
deposit,
sandbox,
)?;

if verbose && !is_json {
eprintln!(
"{}",
"Pull operated-voucher session ready — delegation txs built, sending request…\n"
.dimmed()
);
}
header
return Err(pay_core::Error::Mpp(
"operated-voucher pull sessions are no longer supported; \
use a client-voucher payment-channel session instead"
.to_string(),
));
}
None => {
return Err(pay_core::Error::Mpp(
Expand Down
7 changes: 7 additions & 0 deletions rust/crates/cli/src/commands/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ pub struct SendCommand {
/// This is implied when AMOUNT is "max".
#[arg(long)]
pub fee_within: bool,

/// Send as a confidential transfer (amount hidden on-chain). Requires a
/// Token-2022 mint with the Confidential Transfer extension; the gateway
/// issues a confidential challenge and the payment settles as a bundle.
#[arg(long)]
pub confidential: bool,
}

impl SendCommand {
Expand Down Expand Up @@ -101,6 +107,7 @@ impl SendCommand {
account_override,
memo: memo.as_deref(),
fee_within,
confidential: self.confidential,
rpc_url,
},
)?;
Expand Down
241 changes: 6 additions & 235 deletions rust/crates/cli/src/commands/server/start.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! `pay server start` — start a payment gateway proxy.

use std::process::Command as ProcessCommand;
use std::str::FromStr;
use std::sync::Arc;

Expand Down Expand Up @@ -575,9 +574,8 @@ impl StartCommand {

// ── Create session MPP server (if session config present) ──
let session_mpp: Option<Arc<SessionMpp>> = if let Some(ref sess) = api.session {
use pay_core::server::session::{PullVoucherStrategy, RpcMultiDelegateChain};
use pay_core::server::session::PullVoucherStrategy;
use pay_types::metering::SessionPullVoucherStrategy as ConfigPullVoucherStrategy;
use solana_mpp::program::multi_delegator::MULTI_DELEGATOR_PROGRAM_ID;
use solana_mpp::server::session::SessionConfig;
use solana_mpp::{SessionMode, SessionPullVoucherStrategy};
use std::str::FromStr;
Expand All @@ -600,7 +598,11 @@ impl StartCommand {
ConfigPullVoucherStrategy::Disabled => PullVoucherStrategy::Disabled,
ConfigPullVoucherStrategy::ClientVoucher => PullVoucherStrategy::ClientVoucher,
ConfigPullVoucherStrategy::OperatedVoucher => {
PullVoucherStrategy::OperatedVoucher
return Err(pay_core::Error::Config(
"session.pull_voucher_strategy = operated_voucher is no longer \
supported; use client_voucher or disabled"
.to_string(),
));
}
};
let mut modes = requested_modes.clone();
Expand All @@ -621,14 +623,10 @@ impl StartCommand {
PullVoucherStrategy::ClientVoucher => {
Some(SessionPullVoucherStrategy::ClientVoucher)
}
PullVoucherStrategy::OperatedVoucher => {
Some(SessionPullVoucherStrategy::OperatedVoucher)
}
}
} else {
None
};
let using_local_rpc = rpc_url.contains("localhost") || rpc_url.contains("127.0.0.1");
let channel_program_id = std::env::var("PAY_PAYMENT_CHANNELS_PROGRAM_ID")
.or_else(|_| std::env::var("PAY_FIBER_PROGRAM_ID"))
.ok()
Expand Down Expand Up @@ -685,98 +683,7 @@ impl StartCommand {
smpp = smpp.with_payment_channel_payer_signer(channel_payer_signer);
}

// Operated-voucher pull sessions use multi-delegate setup.
if modes.contains(&SessionMode::Pull)
&& pull_voucher_strategy == PullVoucherStrategy::OperatedVoucher
{
let program_id = solana_pubkey::Pubkey::from_str(MULTI_DELEGATOR_PROGRAM_ID)
.expect("valid multi-delegator program ID");
let mint = solana_pubkey::Pubkey::from_str(&session_mpp_currency)
.unwrap_or_else(|_| {
// fallback: mainnet USDC
solana_pubkey::Pubkey::from_str(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
)
.unwrap()
});
let operator_pk = solana_pubkey::Pubkey::from_str(&session_operator)
.unwrap_or_else(|_| solana_pubkey::Pubkey::default());

if using_local_rpc {
ensure_local_multi_delegator_program(&rpc_url, MULTI_DELEGATOR_PROGRAM_ID)?;
}

if sandbox && !using_local_rpc {
// Sandbox: record submitted txs and return a stub sig so
// the full HTTP flow works without a live multi-delegator
// program on Surfpool.
use pay_core::server::session::MultiDelegateChain;
use solana_mpp::program::multi_delegator::MultiDelegateOnChainState;
use std::future::Future;
use std::pin::Pin;

struct SandboxChain;

impl MultiDelegateChain for SandboxChain {
fn fetch_state<'a>(
&'a self,
owner: &'a str,
) -> Pin<
Box<
dyn Future<
Output = pay_core::Result<
MultiDelegateOnChainState,
>,
> + Send
+ 'a,
>,
> {
let _ = owner;
Box::pin(async {
Ok(MultiDelegateOnChainState {
multi_delegate_exists: false,
existing_delegation_cap: None,
})
})
}

fn submit_tx<'a>(
&'a self,
tx_base64: &'a str,
) -> Pin<
Box<
dyn Future<Output = pay_core::Result<String>>
+ Send
+ 'a,
>,
> {
let preview = &tx_base64[..40.min(tx_base64.len())];
eprintln!(" {} {preview}…", "[sandbox] submit_tx".dimmed());
Box::pin(async { Ok("sandbox_stub_sig".to_string()) })
}
}

smpp = smpp.with_multi_delegate_chain(Box::new(SandboxChain));
} else {
smpp = smpp.with_multi_delegate_chain(Box::new(RpcMultiDelegateChain {
rpc_url: rpc_url.clone(),
program_id,
mint,
operator: operator_pk,
delegation_nonce: 0,
}));
}

tracing::info!(
channel_program_id = %channel_program_id,
"enabled payment-channel session runtime"
);
}

let smpp = Arc::new(smpp);
smpp.set_open_channel_batch_interval(Duration::from_millis(
sess.batch_open_interval_ms,
));
smpp.start_lifecycle_runloop(Duration::from_millis(sess.close_delay_ms));
Some(smpp)
} else {
Expand Down Expand Up @@ -2040,142 +1947,6 @@ fn format_price(price: f64) -> String {
}
}

fn ensure_local_multi_delegator_program(rpc_url: &str, program_id: &str) -> pay_core::Result<()> {
if local_program_is_executable(rpc_url, program_id) {
eprintln!(
" {}",
"multi-delegator program already deployed locally".dimmed()
);
return Ok(());
}

let repo = std::env::var("PAY_MULTI_DELEGATOR_REPO")
.unwrap_or_else(|_| "/Users/ludo/Coding/solana-program/multi-delegator".to_string());
let repo_path = std::path::Path::new(&repo);
let keypair_path = repo_path.join("keys/multi_delegator-keypair.json");
let deploy_dir = repo_path.join("target/deploy");
let deploy_keypair_path = deploy_dir.join("multi_delegator-keypair.json");
let program_so_path = deploy_dir.join("multi_delegator.so");

if !keypair_path.exists() {
return Err(pay_core::Error::Config(format!(
"multi-delegator keypair not found at {}",
keypair_path.display()
)));
}

std::fs::create_dir_all(&deploy_dir).map_err(|e| {
pay_core::Error::Config(format!("failed to create {}: {e}", deploy_dir.display()))
})?;
std::fs::copy(&keypair_path, &deploy_keypair_path).map_err(|e| {
pay_core::Error::Config(format!(
"failed to copy deploy keypair to {}: {e}",
deploy_keypair_path.display()
))
})?;

if !program_so_path.exists() {
eprintln!(" {}", "building local multi-delegator program...".dimmed());
let status = ProcessCommand::new("cargo")
.arg("build-sbf")
.current_dir(repo_path.join("programs/multi_delegator"))
.status()
.map_err(|e| {
pay_core::Error::Config(format!(
"failed to invoke cargo build-sbf for multi-delegator: {e}"
))
})?;
if !status.success() {
return Err(pay_core::Error::Config(
"cargo build-sbf for multi-delegator failed".to_string(),
));
}
}

let payer_keypair = localnet_fee_payer_keypair_file()?;
eprintln!(
" {}",
"deploying multi-delegator program to local Surfpool...".dimmed()
);
let status = ProcessCommand::new("solana")
.arg("program")
.arg("deploy")
.arg("--url")
.arg(rpc_url)
.arg("--keypair")
.arg(payer_keypair.path())
.arg("--fee-payer")
.arg(payer_keypair.path())
.arg("--program-id")
.arg(&deploy_keypair_path)
.arg(&program_so_path)
.status()
.map_err(|e| {
pay_core::Error::Config(format!(
"failed to invoke solana program deploy for multi-delegator: {e}"
))
})?;
if !status.success() {
return Err(pay_core::Error::Config(
"solana program deploy for multi-delegator failed".to_string(),
));
}

if !local_program_is_executable(rpc_url, program_id) {
return Err(pay_core::Error::Config(format!(
"multi-delegator program {program_id} still not executable after deploy"
)));
}

eprintln!(" {}", "multi-delegator program deployed locally".green());
Ok(())
}

fn local_program_is_executable(rpc_url: &str, program_id: &str) -> bool {
let output = ProcessCommand::new("curl")
.arg("-s")
.arg("-X")
.arg("POST")
.arg(rpc_url)
.arg("-H")
.arg("Content-Type: application/json")
.arg("-d")
.arg(format!(
"{{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getAccountInfo\",\"params\":[\"{program_id}\",{{\"encoding\":\"base64\"}}]}}"
))
.output();

let Ok(output) = output else {
return false;
};
let body = String::from_utf8_lossy(&output.stdout);
body.contains("\"executable\":true")
}

fn localnet_fee_payer_keypair_file() -> pay_core::Result<tempfile::NamedTempFile> {
use std::io::Write;

let accounts = pay_core::accounts::AccountsFile::load()?;
let (_, account) = accounts.account_for_network("localnet").ok_or_else(|| {
pay_core::Error::Config(
"no localnet account configured in ~/.config/pay/accounts.yml".to_string(),
)
})?;
let bytes = account.ephemeral_keypair_bytes().ok_or_else(|| {
pay_core::Error::Config(
"localnet account is not ephemeral or missing secret_key_b58".to_string(),
)
})?;

let mut file = tempfile::NamedTempFile::new().map_err(|e| {
pay_core::Error::Config(format!("failed to create temp fee payer keypair file: {e}"))
})?;
write!(file, "{}", serde_json::to_string(&bytes).unwrap()).map_err(|e| {
pay_core::Error::Config(format!("failed to write temp fee payer keypair file: {e}"))
})?;
Ok(file)
}

/// Emit an OSC 8 clickable hyperlink for terminals that support it.

// ── Gateway verify endpoint ──
Expand Down
Loading
Loading