From 30dcf2b0b2516e95e63c9793d10dfa22917b53a5 Mon Sep 17 00:00:00 2001 From: Assad Isah Date: Sat, 30 May 2026 13:57:39 +0100 Subject: [PATCH 1/4] Add admin batch ops, spender allowance index, and admin_info view --- veritixpay/contract/token/src/admin_test.rs | 50 ++++++++++++++++++- veritixpay/contract/token/src/allowance.rs | 35 ++++++++++++- .../contract/token/src/allowance_test.rs | 20 +++++++- veritixpay/contract/token/src/batch.rs | 48 ++++++++++++++++++ veritixpay/contract/token/src/contract.rs | 31 +++++++++++- veritixpay/contract/token/src/lib.rs | 1 + .../contract/token/src/storage_types.rs | 1 + 7 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 veritixpay/contract/token/src/batch.rs diff --git a/veritixpay/contract/token/src/admin_test.rs b/veritixpay/contract/token/src/admin_test.rs index 7e74db0..5cab965 100644 --- a/veritixpay/contract/token/src/admin_test.rs +++ b/veritixpay/contract/token/src/admin_test.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod admin_test { -continue use soroban_sdk::{testutils::Address as _, testutils::Events as _, Address, Env, String}; use crate::admin::{has_admin, read_admin, transfer_admin, write_admin}; @@ -143,4 +142,53 @@ continue transfer_admin(&env, admin.clone()); assert_eq!(read_admin(&env), admin); } + + #[test] + fn test_admin_info_tracks_admin_rotation() { + let env = setup_env(); + let (_admin, client) = create_initialized_client(&env); + let new_admin = Address::generate(&env); + let before = client.admin_info(); + assert_eq!(before.paused, false); + client.set_admin(&new_admin); + let after = client.admin_info(); + assert_eq!(after.admin, new_admin); + assert_eq!(after.paused, false); + } + + #[test] + fn test_freeze_batch_and_unfreeze_batch() { + let env = setup_env(); + let (_admin, client) = create_initialized_client(&env); + let a = Address::generate(&env); + let b = Address::generate(&env); + let mut targets = soroban_sdk::Vec::new(&env); + targets.push_back(a.clone()); + targets.push_back(b.clone()); + client.freeze_batch(&client.admin(), &targets); + assert!(client.is_frozen(&a)); + assert!(client.is_frozen(&b)); + client.unfreeze_batch(&client.admin(), &targets); + assert!(!client.is_frozen(&a)); + assert!(!client.is_frozen(&b)); + } + + #[test] + fn test_clawback_batch_reduces_balances_and_supply() { + let env = setup_env(); + let (_admin, client) = create_initialized_client(&env); + let admin = client.admin(); + let a = Address::generate(&env); + let b = Address::generate(&env); + client.mint(&admin, &a, &1000); + client.mint(&admin, &b, &1000); + let mut targets = soroban_sdk::Vec::new(&env); + targets.push_back((a.clone(), 200)); + targets.push_back((b.clone(), 300)); + let before_supply = client.total_supply(); + client.clawback_batch(&admin, &targets); + assert_eq!(client.balance(&a), 800); + assert_eq!(client.balance(&b), 700); + assert_eq!(client.total_supply(), before_supply - 500); + } } diff --git a/veritixpay/contract/token/src/allowance.rs b/veritixpay/contract/token/src/allowance.rs index 7628ef8..1ca1166 100644 --- a/veritixpay/contract/token/src/allowance.rs +++ b/veritixpay/contract/token/src/allowance.rs @@ -2,7 +2,7 @@ use crate::storage_types::{ AllowanceDataKey, AllowanceValue, DataKey, ALLOWANCE_BUMP_AMOUNT, ALLOWANCE_LIFETIME_THRESHOLD, }; use crate::validation::{require_current_or_future_ledger, require_non_negative_amount}; -use soroban_sdk::{Address, Env}; +use soroban_sdk::{Address, Env, Vec}; pub fn read_allowance(e: &Env, from: Address, spender: Address) -> AllowanceValue { let key = DataKey::Allowance(AllowanceDataKey { @@ -56,9 +56,35 @@ pub fn write_allowance( spender: spender.clone(), }); + let index_key = DataKey::SpenderAllowances(spender.clone()); + let mut spenders_from: Vec
= e + .storage() + .persistent() + .get(&index_key) + .unwrap_or_else(|| Vec::new(e)); + if amount == 0 { e.storage().persistent().remove(&key); + let mut updated = Vec::new(e); + for i in 0..spenders_from.len() { + let addr = spenders_from.get(i).unwrap(); + if addr != from { + updated.push_back(addr); + } + } + e.storage().persistent().set(&index_key, &updated); } else { + let mut exists = false; + for i in 0..spenders_from.len() { + if spenders_from.get(i).unwrap() == from { + exists = true; + break; + } + } + if !exists { + spenders_from.push_back(from.clone()); + e.storage().persistent().set(&index_key, &spenders_from); + } let allowance = AllowanceValue { amount, expiration_ledger, @@ -67,6 +93,13 @@ pub fn write_allowance( } } +pub fn get_allowances_for_spender(e: &Env, spender: Address) -> Vec
{ + e.storage() + .persistent() + .get(&DataKey::SpenderAllowances(spender)) + .unwrap_or_else(|| Vec::new(e)) +} + pub fn spend_allowance(e: &Env, from: Address, spender: Address, amount: i128) { let allowance = read_allowance(e, from.clone(), spender.clone()); diff --git a/veritixpay/contract/token/src/allowance_test.rs b/veritixpay/contract/token/src/allowance_test.rs index ef32c70..908df07 100644 --- a/veritixpay/contract/token/src/allowance_test.rs +++ b/veritixpay/contract/token/src/allowance_test.rs @@ -2,7 +2,7 @@ mod allowance_tests { use soroban_sdk::{Address, Env}; - use crate::allowance::{read_allowance, spend_allowance, write_allowance}; + use crate::allowance::{get_allowances_for_spender, read_allowance, spend_allowance, write_allowance}; use crate::contract::VeritixToken; fn setup_env() -> (Env, Address) { @@ -108,4 +108,22 @@ mod allowance_tests { assert_eq!(a.amount, 0); }); } + + #[test] + fn test_allowances_for_spender_index_adds_and_removes_from() { + let (e, contract_id) = setup_env(); + let from = Address::generate(&e); + let spender = Address::generate(&e); + e.as_contract(&contract_id, || { + let expiry = e.ledger().sequence() + 100; + write_allowance(&e, from.clone(), spender.clone(), 500, expiry); + let indexed = get_allowances_for_spender(&e, spender.clone()); + assert_eq!(indexed.len(), 1); + assert_eq!(indexed.get(0).unwrap(), from.clone()); + + write_allowance(&e, from.clone(), spender.clone(), 0, expiry); + let indexed_after = get_allowances_for_spender(&e, spender); + assert_eq!(indexed_after.len(), 0); + }); + } } diff --git a/veritixpay/contract/token/src/batch.rs b/veritixpay/contract/token/src/batch.rs new file mode 100644 index 0000000..9d5a0a9 --- /dev/null +++ b/veritixpay/contract/token/src/batch.rs @@ -0,0 +1,48 @@ +use crate::admin::check_admin; +use crate::balance::{decrease_supply, spend_balance}; +use crate::freeze::{freeze_account, unfreeze_account}; +use crate::validation::require_positive_amount; +use soroban_sdk::{symbol_short, Address, Env, Vec}; + +const MAX_BATCH_TARGETS: u32 = 50; + +pub fn clawback_batch(e: &Env, admin: Address, targets: Vec<(Address, i128)>) { + check_admin(e, &admin); + if targets.len() > MAX_BATCH_TARGETS { + panic!("batch too large"); + } + for i in 0..targets.len() { + let (from, amount) = targets.get(i).unwrap(); + require_positive_amount(amount); + spend_balance(e, from.clone(), amount); + decrease_supply(e, amount); + e.events() + .publish((symbol_short!("clawback"), admin.clone(), from), amount); + } +} + +pub fn freeze_batch(e: &Env, admin: Address, targets: Vec
) { + check_admin(e, &admin); + if targets.len() > MAX_BATCH_TARGETS { + panic!("batch too large"); + } + for i in 0..targets.len() { + let target = targets.get(i).unwrap(); + freeze_account(e, admin.clone(), target); + } + e.events() + .publish((symbol_short!("batch_frz"), admin), targets.len()); +} + +pub fn unfreeze_batch(e: &Env, admin: Address, targets: Vec
) { + check_admin(e, &admin); + if targets.len() > MAX_BATCH_TARGETS { + panic!("batch too large"); + } + for i in 0..targets.len() { + let target = targets.get(i).unwrap(); + unfreeze_account(e, admin.clone(), target); + } + e.events() + .publish((symbol_short!("batch_unf"), admin), targets.len()); +} diff --git a/veritixpay/contract/token/src/contract.rs b/veritixpay/contract/token/src/contract.rs index e5ac271..a851ff8 100644 --- a/veritixpay/contract/token/src/contract.rs +++ b/veritixpay/contract/token/src/contract.rs @@ -1,5 +1,6 @@ use crate::admin::{check_admin, has_admin, read_admin, transfer_admin, write_admin}; -use crate::allowance::{read_allowance, spend_allowance, write_allowance}; +use crate::allowance::{get_allowances_for_spender, read_allowance, spend_allowance, write_allowance}; +use crate::batch::{clawback_batch, freeze_batch, unfreeze_batch}; use crate::balance::{ decrease_supply, increase_supply, read_balance, read_total_supply, receive_balance, spend_balance, @@ -22,11 +23,18 @@ use crate::splitter::{ get_split as split_get, SplitRecord, SplitRecipient, }; use crate::validation::{require_not_frozen_account, require_positive_amount}; -use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String, Vec}; +use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, String, Vec}; #[contract] pub struct VeritixToken; +#[derive(Clone)] +#[contracttype] +pub struct AdminInfo { + pub admin: Address, + pub paused: bool, +} + #[contractimpl] impl VeritixToken { // --- Admin & metadata --- @@ -67,6 +75,9 @@ impl VeritixToken { e.events() .publish((symbol_short!("clawback"), admin, from), amount); } + pub fn clawback_batch(e: Env, admin: Address, targets: Vec<(Address, i128)>) { + clawback_batch(&e, admin, targets); + } // --- Freeze controls --- @@ -81,6 +92,13 @@ impl VeritixToken { check_admin(&e, &admin); unfreeze_account(&e, admin, target); } + pub fn freeze_batch(e: Env, admin: Address, targets: Vec
) { + freeze_batch(&e, admin, targets); + } + + pub fn unfreeze_batch(e: Env, admin: Address, targets: Vec
) { + unfreeze_batch(&e, admin, targets); + } // --- Mint / burn & supply tracking --- @@ -166,10 +184,19 @@ impl VeritixToken { pub fn allowance(e: Env, from: Address, spender: Address) -> i128 { read_allowance(&e, from, spender).amount } + pub fn allowances_for_spender(e: Env, spender: Address) -> Vec
{ + get_allowances_for_spender(&e, spender) + } pub fn admin(e: Env) -> Address { read_admin(&e) } + pub fn admin_info(e: Env) -> AdminInfo { + AdminInfo { + admin: read_admin(&e), + paused: false, + } + } pub fn is_frozen(e: Env, id: Address) -> bool { read_frozen_status(&e, &id) diff --git a/veritixpay/contract/token/src/lib.rs b/veritixpay/contract/token/src/lib.rs index d688869..5bf4de9 100644 --- a/veritixpay/contract/token/src/lib.rs +++ b/veritixpay/contract/token/src/lib.rs @@ -6,6 +6,7 @@ pub mod admin; pub mod allowance; +pub mod batch; pub mod balance; pub mod dispute; pub mod escrow; diff --git a/veritixpay/contract/token/src/storage_types.rs b/veritixpay/contract/token/src/storage_types.rs index b4d22c1..a08a780 100644 --- a/veritixpay/contract/token/src/storage_types.rs +++ b/veritixpay/contract/token/src/storage_types.rs @@ -29,6 +29,7 @@ pub struct AllowanceValue { pub enum DataKey { Admin, Allowance(AllowanceDataKey), + SpenderAllowances(Address), Balance(Address), Metadata, TotalSupply, From 69ffbed17b6ffc9945ab0ca81a31a4aded1abd28 Mon Sep 17 00:00:00 2001 From: yasinmuhd Date: Sat, 30 May 2026 15:07:07 +0100 Subject: [PATCH 2/4] Add CI/lint baseline, TTL bump fixes, and contract docs --- .cargo/config.toml | 2 + .github/workflows/ci.yml | 70 +++++----------------- veritixpay/contract/token/src/allowance.rs | 18 ++++++ veritixpay/contract/token/src/contract.rs | 30 ++++++++++ 4 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..0eb087e --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["-D", "warnings"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 252f6a2..63c5e06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,69 +1,29 @@ -name: Veritix Pay CI +name: CI on: - push: - branches: [ "main" ] pull_request: - branches: [ "main" ] + branches: [main] jobs: - build-and-test: - name: Build, Test, and Format + contract-ci: runs-on: ubuntu-latest - - # Set the working directory for all run steps in this job defaults: run: working-directory: veritixpay/contract/token - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.84.0" - # wasm32-unknown-unknown is needed for `cargo test` and clippy. - # `stellar contract build` uses wasm32v1-none internally and manages - # that target itself; we do not need to install it here. - targets: wasm32-unknown-unknown - components: rustfmt, clippy - - - name: Cache Cargo registry and build artifacts - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - workspaces: "veritixpay/contract/token -> target" - + targets: wasm32-unknown-unknown,wasm32v1-none - name: Install Stellar CLI - run: cargo install --locked stellar-cli - - - name: Check for orphaned source files - run: | - LIB=src/lib.rs - EXIT=0 - for f in src/*.rs; do - mod=$(basename "$f" .rs) - [ "$mod" = "lib" ] && continue - if ! grep -qE "^\s*(pub\s+)?mod\s+${mod}\s*;" "$LIB"; then - echo "ORPHAN: $f is not declared in lib.rs" - EXIT=1 - fi - done - exit $EXIT - - - name: Check Formatting - # Uses cargo fmt directly, or make fmt if it passes the args in your Makefile - run: cargo fmt -- --check - - - name: Run Clippy - run: cargo clippy --lib -- -D warnings - - - name: Compile-only check (locked) - run: cargo check --locked - - - name: Build Contract + run: cargo install stellar-cli --locked + - name: Preflight + run: make preflight + - name: Build run: make build - - - name: Run Tests + - name: Test run: make test + - name: Clippy + run: cargo clippy --all -- -D warnings + - name: Rustfmt + run: cargo fmt --all --check diff --git a/veritixpay/contract/token/src/allowance.rs b/veritixpay/contract/token/src/allowance.rs index 1ca1166..5c11738 100644 --- a/veritixpay/contract/token/src/allowance.rs +++ b/veritixpay/contract/token/src/allowance.rs @@ -1,5 +1,6 @@ use crate::storage_types::{ AllowanceDataKey, AllowanceValue, DataKey, ALLOWANCE_BUMP_AMOUNT, ALLOWANCE_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD, }; use crate::validation::{require_current_or_future_ledger, require_non_negative_amount}; use soroban_sdk::{Address, Env, Vec}; @@ -73,6 +74,12 @@ pub fn write_allowance( } } e.storage().persistent().set(&index_key, &updated); + // Keep spender index alive for long-lived delegated payment lookups. + e.storage().persistent().extend_ttl( + &index_key, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } else { let mut exists = false; for i in 0..spenders_from.len() { @@ -84,12 +91,23 @@ pub fn write_allowance( if !exists { spenders_from.push_back(from.clone()); e.storage().persistent().set(&index_key, &spenders_from); + // Keep spender index alive for long-lived delegated payment lookups. + e.storage().persistent().extend_ttl( + &index_key, + PERSISTENT_LIFETIME_THRESHOLD, + PERSISTENT_BUMP_AMOUNT, + ); } let allowance = AllowanceValue { amount, expiration_ledger, }; e.storage().persistent().set(&key, &allowance); + e.storage().persistent().extend_ttl( + &key, + ALLOWANCE_LIFETIME_THRESHOLD, + ALLOWANCE_BUMP_AMOUNT, + ); } } diff --git a/veritixpay/contract/token/src/contract.rs b/veritixpay/contract/token/src/contract.rs index a851ff8..4364ebd 100644 --- a/veritixpay/contract/token/src/contract.rs +++ b/veritixpay/contract/token/src/contract.rs @@ -75,27 +75,32 @@ impl VeritixToken { e.events() .publish((symbol_short!("clawback"), admin, from), amount); } + /// Admin-only batch clawback over `(from, amount)` tuples. pub fn clawback_batch(e: Env, admin: Address, targets: Vec<(Address, i128)>) { clawback_batch(&e, admin, targets); } // --- Freeze controls --- + /// Admin-only freeze for a single account. pub fn freeze(e: Env, target: Address) { let admin = read_admin(&e); check_admin(&e, &admin); freeze_account(&e, admin, target); } + /// Admin-only unfreeze for a single account. pub fn unfreeze(e: Env, target: Address) { let admin = read_admin(&e); check_admin(&e, &admin); unfreeze_account(&e, admin, target); } + /// Admin-only batch freeze for multiple accounts. pub fn freeze_batch(e: Env, admin: Address, targets: Vec
) { freeze_batch(&e, admin, targets); } + /// Admin-only batch unfreeze for multiple accounts. pub fn unfreeze_batch(e: Env, admin: Address, targets: Vec
) { unfreeze_batch(&e, admin, targets); } @@ -173,24 +178,30 @@ impl VeritixToken { // --- Read-only views --- + /// Returns current total token supply. pub fn total_supply(e: Env) -> i128 { read_total_supply(&e) } + /// Returns token balance for `id`. pub fn balance(e: Env, id: Address) -> i128 { read_balance(&e, id) } + /// Returns allowance from `from` to `spender`. pub fn allowance(e: Env, from: Address, spender: Address) -> i128 { read_allowance(&e, from, spender).amount } + /// Returns all owners that granted non-zero allowance to `spender`. pub fn allowances_for_spender(e: Env, spender: Address) -> Vec
{ get_allowances_for_spender(&e, spender) } + /// Returns current admin address. pub fn admin(e: Env) -> Address { read_admin(&e) } + /// Returns compact admin metadata for clients. pub fn admin_info(e: Env) -> AdminInfo { AdminInfo { admin: read_admin(&e), @@ -198,36 +209,44 @@ impl VeritixToken { } } + /// Returns freeze state for account `id`. pub fn is_frozen(e: Env, id: Address) -> bool { read_frozen_status(&e, &id) } + /// Returns token decimal precision. pub fn decimals(e: Env) -> u32 { read_decimal(&e) } + /// Returns token display name. pub fn name(e: Env) -> String { read_name(&e) } + /// Returns token symbol. pub fn symbol(e: Env) -> String { read_symbol(&e) } // --- Escrow --- + /// Creates an escrow and returns its ID. pub fn create_escrow(e: Env, depositor: Address, beneficiary: Address, amount: i128, expiry_ledger: u32) -> u32 { escrow_create(&e, depositor, beneficiary, amount, expiry_ledger) } + /// Releases escrow funds to beneficiary. pub fn release_escrow(e: Env, caller: Address, escrow_id: u32) { escrow_release(&e, caller, escrow_id) } + /// Refunds escrow funds to depositor. pub fn refund_escrow(e: Env, caller: Address, escrow_id: u32) { escrow_refund(&e, caller, escrow_id) } + /// Returns escrow record for `escrow_id`. pub fn get_escrow(e: Env, escrow_id: u32) -> EscrowRecord { escrow_get(&e, escrow_id) } @@ -246,10 +265,12 @@ impl VeritixToken { // --- Dispute --- + /// Opens a dispute for an escrow and returns dispute ID. pub fn open_dispute(e: Env, claimant: Address, escrow_id: u32, resolver: Address) -> u32 { open_dispute(&e, claimant, escrow_id, resolver) } + /// Resolves dispute by releasing to beneficiary or refunding depositor. pub fn resolve_dispute( e: Env, resolver: Address, @@ -259,6 +280,7 @@ impl VeritixToken { resolve_dispute(&e, resolver, dispute_id, release_to_beneficiary) } + /// Returns dispute record for `dispute_id`. pub fn get_dispute(e: Env, dispute_id: u32) -> DisputeRecord { dispute_get(&e, dispute_id) } @@ -271,6 +293,7 @@ impl VeritixToken { // --- Splitter --- + /// Creates a split payment plan and returns split ID. pub fn create_split( e: Env, sender: Address, @@ -280,14 +303,17 @@ impl VeritixToken { split_create(&e, sender, recipients, total_amount) } + /// Executes split distribution to recipients. pub fn distribute(e: Env, caller: Address, split_id: u32) { split_distribute(&e, caller, split_id) } + /// Cancels an active split and returns remainder to sender. pub fn cancel_split(e: Env, caller: Address, split_id: u32) { split_cancel(&e, caller, split_id) } + /// Returns split record for `split_id`. pub fn get_split(e: Env, split_id: u32) -> SplitRecord { split_get(&e, split_id) } @@ -300,6 +326,7 @@ impl VeritixToken { // --- Recurring Payments --- + /// Creates a recurring payment and returns recurring ID. pub fn setup_recurring( e: Env, payer: Address, @@ -310,14 +337,17 @@ impl VeritixToken { setup_recurring(&e, payer, payee, amount, interval) } + /// Executes one interval payment for a recurring plan. pub fn execute_recurring(e: Env, recurring_id: u32) { execute_recurring(&e, recurring_id) } + /// Cancels recurring payment plan. pub fn cancel_recurring(e: Env, caller: Address, recurring_id: u32) { cancel_recurring(&e, caller, recurring_id) } + /// Returns recurring payment record for `recurring_id`. pub fn get_recurring(e: Env, recurring_id: u32) -> RecurringRecord { get_recurring(&e, recurring_id) } From a6724df83ff13c43776b7bd9f9e5f6460ed267c2 Mon Sep 17 00:00:00 2001 From: chemicalcommando Date: Sat, 30 May 2026 15:10:22 +0100 Subject: [PATCH 3/4] Harden storage TTL bumps for split, recurring, dispute, and counters --- veritixpay/contract/token/src/dispute.rs | 8 ++++---- veritixpay/contract/token/src/recurring.rs | 12 ++++++------ veritixpay/contract/token/src/splitter.rs | 10 +++++----- veritixpay/contract/token/src/storage_types.rs | 8 ++++++++ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/veritixpay/contract/token/src/dispute.rs b/veritixpay/contract/token/src/dispute.rs index b5e3b0f..371a19c 100644 --- a/veritixpay/contract/token/src/dispute.rs +++ b/veritixpay/contract/token/src/dispute.rs @@ -1,6 +1,6 @@ use crate::balance::{receive_balance, spend_balance}; use crate::escrow::get_escrow; -use crate::storage_types::{increment_counter, write_persistent_record, DataKey, PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; +use crate::storage_types::{increment_counter, write_persistent_record, DataKey, DISPUTE_BUMP_AMOUNT, DISPUTE_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol}; #[contracttype] @@ -70,7 +70,7 @@ pub fn open_dispute(e: &Env, claimant: Address, escrow_id: u32, resolver: Addres e.storage().persistent().set(&dispute_key, &record); e.storage() .persistent() - .extend_ttl(&dispute_key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); + .extend_ttl(&dispute_key, DISPUTE_LIFETIME_THRESHOLD, DISPUTE_BUMP_AMOUNT); e.storage().persistent().set(&escrow_dispute_key, &count); e.storage() .persistent() @@ -135,7 +135,7 @@ pub fn resolve_dispute(e: &Env, resolver: Address, dispute_id: u32, release_to_b .expect("Dispute not found"); e.storage() .persistent() - .extend_ttl(&dispute_key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); + .extend_ttl(&dispute_key, DISPUTE_LIFETIME_THRESHOLD, DISPUTE_BUMP_AMOUNT); if dispute.status != DisputeStatus::Open { panic!("AlreadyResolved: This dispute has already been resolved"); @@ -177,6 +177,6 @@ pub fn get_dispute(e: &Env, dispute_id: u32) -> DisputeRecord { .expect("Dispute not found"); e.storage() .persistent() - .extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); + .extend_ttl(&key, DISPUTE_LIFETIME_THRESHOLD, DISPUTE_BUMP_AMOUNT); record } diff --git a/veritixpay/contract/token/src/recurring.rs b/veritixpay/contract/token/src/recurring.rs index 885d7a3..10ea779 100644 --- a/veritixpay/contract/token/src/recurring.rs +++ b/veritixpay/contract/token/src/recurring.rs @@ -1,5 +1,5 @@ use crate::balance::{receive_balance, spend_balance}; -use crate::storage_types::{increment_counter, DataKey, PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD}; +use crate::storage_types::{increment_counter, DataKey, RECURRING_BUMP_AMOUNT, RECURRING_LIFETIME_THRESHOLD}; use crate::validation::require_positive_amount; use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol}; @@ -55,7 +55,7 @@ pub fn setup_recurring( e.storage().persistent().set(&key, &record); e.storage() .persistent() - .extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); + .extend_ttl(&key, RECURRING_LIFETIME_THRESHOLD, RECURRING_BUMP_AMOUNT); // 4. Emit Observability Event e.events().publish( @@ -77,7 +77,7 @@ pub fn execute_recurring(e: &Env, recurring_id: u32) { .unwrap_or_else(|| panic!("recurring record not found")); e.storage() .persistent() - .extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); + .extend_ttl(&key, RECURRING_LIFETIME_THRESHOLD, RECURRING_BUMP_AMOUNT); if !record.active { panic!("recurring payment is not active"); @@ -103,7 +103,7 @@ pub fn execute_recurring(e: &Env, recurring_id: u32) { e.storage().persistent().set(&key, &record); e.storage() .persistent() - .extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); + .extend_ttl(&key, RECURRING_LIFETIME_THRESHOLD, RECURRING_BUMP_AMOUNT); e.events().publish( (symbol_short!("recurring_executed"), recurring_id), @@ -130,7 +130,7 @@ pub fn cancel_recurring(e: &Env, caller: Address, recurring_id: u32) { e.storage().persistent().set(&key, &record); e.storage() .persistent() - .extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); + .extend_ttl(&key, RECURRING_LIFETIME_THRESHOLD, RECURRING_BUMP_AMOUNT); e.events().publish( ( @@ -151,6 +151,6 @@ pub fn get_recurring(e: &Env, recurring_id: u32) -> RecurringRecord { .unwrap_or_else(|| panic!("recurring record not found")); e.storage() .persistent() - .extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT); + .extend_ttl(&key, RECURRING_LIFETIME_THRESHOLD, RECURRING_BUMP_AMOUNT); record } diff --git a/veritixpay/contract/token/src/splitter.rs b/veritixpay/contract/token/src/splitter.rs index 9cd2743..bb2305e 100644 --- a/veritixpay/contract/token/src/splitter.rs +++ b/veritixpay/contract/token/src/splitter.rs @@ -1,7 +1,7 @@ use crate::balance::{receive_balance, spend_balance}; use crate::storage_types::{ increment_counter, read_persistent_record, write_persistent_record, DataKey, - PERSISTENT_BUMP_AMOUNT, PERSISTENT_LIFETIME_THRESHOLD, + SPLIT_BUMP_AMOUNT, SPLIT_LIFETIME_THRESHOLD, }; use crate::validation::require_positive_amount; use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; @@ -95,8 +95,8 @@ pub fn distribute(e: &Env, caller: Address, split_id: u32) { .expect("split record not found"); e.storage().persistent().extend_ttl( &DataKey::Split(split_id), - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, + SPLIT_LIFETIME_THRESHOLD, + SPLIT_BUMP_AMOUNT, ); // 1. Rules: Caller must be sender, cannot distribute twice @@ -163,8 +163,8 @@ pub fn cancel_split(e: &Env, caller: Address, split_id: u32) { .expect("split record not found"); e.storage().persistent().extend_ttl( &DataKey::Split(split_id), - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_AMOUNT, + SPLIT_LIFETIME_THRESHOLD, + SPLIT_BUMP_AMOUNT, ); if record.sender != caller { diff --git a/veritixpay/contract/token/src/storage_types.rs b/veritixpay/contract/token/src/storage_types.rs index a08a780..f9d8cdc 100644 --- a/veritixpay/contract/token/src/storage_types.rs +++ b/veritixpay/contract/token/src/storage_types.rs @@ -9,6 +9,12 @@ pub const INSTANCE_BUMP_AMOUNT: u32 = 535000; /// Threshold and bump for long-lived persistent records (escrow, split, dispute, recurring, freeze). pub const PERSISTENT_LIFETIME_THRESHOLD: u32 = 518400; // ~30 days pub const PERSISTENT_BUMP_AMOUNT: u32 = 535000; +pub const SPLIT_LIFETIME_THRESHOLD: u32 = PERSISTENT_LIFETIME_THRESHOLD; +pub const SPLIT_BUMP_AMOUNT: u32 = PERSISTENT_BUMP_AMOUNT; +pub const RECURRING_LIFETIME_THRESHOLD: u32 = PERSISTENT_LIFETIME_THRESHOLD; +pub const RECURRING_BUMP_AMOUNT: u32 = PERSISTENT_BUMP_AMOUNT; +pub const DISPUTE_LIFETIME_THRESHOLD: u32 = PERSISTENT_LIFETIME_THRESHOLD; +pub const DISPUTE_BUMP_AMOUNT: u32 = PERSISTENT_BUMP_AMOUNT; #[derive(Clone)] #[contracttype] @@ -82,6 +88,8 @@ pub fn read_counter(e: &Env, key: &DataKey) -> u32 { } pub fn increment_counter(e: &Env, key: &DataKey) -> u32 { + // Keep instance storage alive whenever counters are mutated to prevent ID reset/collision. + bump_instance(e); let next = read_counter(e, key) + 1; e.storage().instance().set(key, &next); next From 5b22345abe0ca02f0f1b9f14c20810ed81d656ad Mon Sep 17 00:00:00 2001 From: rmsb-art Date: Sat, 30 May 2026 15:16:14 +0100 Subject: [PATCH 4/4] Add token_info, transfer memo, metadata update, and escrow TTL bump --- veritixpay/contract/token/src/contract.rs | 42 ++++++++++++++++++- veritixpay/contract/token/src/escrow.rs | 9 +++- veritixpay/contract/token/src/metadata.rs | 13 ++++++ .../contract/token/src/storage_types.rs | 2 + veritixpay/contract/token/src/test.rs | 28 +++++++++++++ 5 files changed, 90 insertions(+), 4 deletions(-) diff --git a/veritixpay/contract/token/src/contract.rs b/veritixpay/contract/token/src/contract.rs index 4364ebd..a90e9e5 100644 --- a/veritixpay/contract/token/src/contract.rs +++ b/veritixpay/contract/token/src/contract.rs @@ -13,7 +13,7 @@ use crate::escrow::{ }; use crate::freeze::{freeze_account, is_frozen as read_frozen_status, unfreeze_account}; use crate::metadata::{ - read_decimal, read_name, read_symbol, validate_metadata, write_metadata, TokenMetadata, + read_decimal, read_name, read_symbol, update_metadata_fields, validate_metadata, write_metadata, TokenMetadata, }; use crate::recurring::{ cancel_recurring, execute_recurring, get_recurring, setup_recurring, RecurringRecord, @@ -23,7 +23,7 @@ use crate::splitter::{ get_split as split_get, SplitRecord, SplitRecipient, }; use crate::validation::{require_not_frozen_account, require_positive_amount}; -use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, String, Vec}; +use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Bytes, Env, String, Vec}; #[contract] pub struct VeritixToken; @@ -35,6 +35,15 @@ pub struct AdminInfo { pub paused: bool, } +#[derive(Clone)] +#[contracttype] +pub struct TokenInfo { + pub name: String, + pub symbol: String, + pub decimal: u32, + pub total_supply: i128, +} + #[contractimpl] impl VeritixToken { // --- Admin & metadata --- @@ -176,6 +185,19 @@ impl VeritixToken { .publish((symbol_short!("approve"), from, spender), amount); } + pub fn transfer_with_memo(e: Env, from: Address, to: Address, amount: i128, memo: Bytes) { + if memo.len() > 64 { + panic!("memo too long"); + } + require_not_frozen_account(&e, &from); + require_positive_amount(amount); + from.require_auth(); + spend_balance(&e, from.clone(), amount); + receive_balance(&e, to.clone(), amount); + e.events() + .publish((symbol_short!("tr_memo"), from, to), (amount, memo)); + } + // --- Read-only views --- /// Returns current total token supply. @@ -229,6 +251,22 @@ impl VeritixToken { read_symbol(&e) } + pub fn token_info(e: Env) -> TokenInfo { + TokenInfo { + name: read_name(&e), + symbol: read_symbol(&e), + decimal: read_decimal(&e), + total_supply: read_total_supply(&e), + } + } + + /// Admin can update only name/symbol; decimal remains immutable. + pub fn update_metadata(e: Env, admin: Address, name: Option, symbol: Option) { + check_admin(&e, &admin); + update_metadata_fields(&e, name, symbol); + e.events().publish((symbol_short!("meta_upd"), admin), ()); + } + // --- Escrow --- /// Creates an escrow and returns its ID. diff --git a/veritixpay/contract/token/src/escrow.rs b/veritixpay/contract/token/src/escrow.rs index f913178..a018d70 100644 --- a/veritixpay/contract/token/src/escrow.rs +++ b/veritixpay/contract/token/src/escrow.rs @@ -2,6 +2,7 @@ use crate::admin::check_admin; use crate::balance::{receive_balance, spend_balance}; use crate::storage_types::{ increment_counter, read_persistent_record, write_persistent_record, DataKey, + ESCROW_BUMP_AMOUNT, ESCROW_LIFETIME_THRESHOLD, }; use crate::validation::{require_current_or_future_ledger, require_positive_amount}; use soroban_sdk::{contracttype, symbol_short, Address, Env}; @@ -160,10 +161,14 @@ pub fn get_escrow(e: &Env, escrow_id: u32) -> EscrowRecord { } pub fn try_get_escrow(e: &Env, escrow_id: u32) -> Result { - if e.storage().persistent().has(&DataKey::Escrow(escrow_id)) { + let key = DataKey::Escrow(escrow_id); + if e.storage().persistent().has(&key) { + e.storage() + .persistent() + .extend_ttl(&key, ESCROW_LIFETIME_THRESHOLD, ESCROW_BUMP_AMOUNT); Ok(read_persistent_record( e, - &DataKey::Escrow(escrow_id), + &key, "escrow not found", )) } else { diff --git a/veritixpay/contract/token/src/metadata.rs b/veritixpay/contract/token/src/metadata.rs index 6efc957..8f0162f 100644 --- a/veritixpay/contract/token/src/metadata.rs +++ b/veritixpay/contract/token/src/metadata.rs @@ -40,3 +40,16 @@ pub fn read_name(e: &Env) -> String { pub fn read_symbol(e: &Env) -> String { read_metadata(e).symbol } + +pub fn update_metadata_fields(e: &Env, name: Option, symbol: Option) { + let mut metadata = read_metadata(e); + if let Some(n) = name { + require_nonempty_string(&n, "name cannot be empty"); + metadata.name = n; + } + if let Some(s) = symbol { + require_nonempty_string(&s, "symbol cannot be empty"); + metadata.symbol = s; + } + write_metadata(e, metadata); +} diff --git a/veritixpay/contract/token/src/storage_types.rs b/veritixpay/contract/token/src/storage_types.rs index f9d8cdc..baecf11 100644 --- a/veritixpay/contract/token/src/storage_types.rs +++ b/veritixpay/contract/token/src/storage_types.rs @@ -15,6 +15,8 @@ pub const RECURRING_LIFETIME_THRESHOLD: u32 = PERSISTENT_LIFETIME_THRESHOLD; pub const RECURRING_BUMP_AMOUNT: u32 = PERSISTENT_BUMP_AMOUNT; pub const DISPUTE_LIFETIME_THRESHOLD: u32 = PERSISTENT_LIFETIME_THRESHOLD; pub const DISPUTE_BUMP_AMOUNT: u32 = PERSISTENT_BUMP_AMOUNT; +pub const ESCROW_LIFETIME_THRESHOLD: u32 = 7_884_000; +pub const ESCROW_BUMP_AMOUNT: u32 = 7_900_000; #[derive(Clone)] #[contracttype] diff --git a/veritixpay/contract/token/src/test.rs b/veritixpay/contract/token/src/test.rs index 11d56b9..c497850 100644 --- a/veritixpay/contract/token/src/test.rs +++ b/veritixpay/contract/token/src/test.rs @@ -213,6 +213,34 @@ fn test_transfer_from() { assert_eq!(client.allowance(&user, &spender), 200i128); } +#[test] +fn test_token_info_combines_metadata_and_supply() { + let (env, admin, user) = setup(); + env.mock_all_auths(); + let client = create_client(&env); + initialize_client(&client, &env, &admin, 7); + client.mint(&admin, &user, &123i128); + let info = client.token_info(); + assert_eq!(info.name, String::from_str(&env, "Veritix")); + assert_eq!(info.symbol, String::from_str(&env, "VTX")); + assert_eq!(info.decimal, 7); + assert_eq!(info.total_supply, 123); +} + +#[test] +fn test_transfer_with_memo_moves_funds() { + let (env, admin, user) = setup(); + env.mock_all_auths(); + let client = create_client(&env); + let receiver = Address::generate(&env); + initialize_client(&client, &env, &admin, 7); + client.mint(&admin, &user, &1000i128); + let memo = soroban_sdk::Bytes::from_array(&env, b"ticket-001"); + client.transfer_with_memo(&user, &receiver, &250i128, &memo); + assert_eq!(client.balance(&user), 750); + assert_eq!(client.balance(&receiver), 250); +} + #[test] fn test_approve_and_spend_allowance() { let (env, admin, user) = setup();