Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
All notable changes to the Sharpy smart contract are documented here.

## [Unreleased]
- feat: recurring pause — pause/resume recurring chain — Adds pause_recurring / resume_recurring and is_recurring_pau
- feat: discount config — set/get_discount — feat/discount-config
- feat: invoice metadata — set/get_invoice_metadata — Adds InvoiceMetadata (key-value map) via set/get with creato
- feat: deadline extension — extend_deadline for creators — Adds extend_deadline(caller, id, new_deadline) allowing crea
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![Soroban](https://img.shields.io/badge/Soroban-Protocol%2027-6C63FF?logo=stellar)
![Rust](https://img.shields.io/badge/Rust-stable-orange?logo=rust)
![Tests](https://img.shields.io/badge/tests-153%20passing-00D4AA)
![Tests](https://img.shields.io/badge/tests-156%20passing-00D4AA)
![License](https://img.shields.io/badge/license-MIT-green)
![Version](https://img.shields.io/badge/version-0.2.0-6C63FF)
[![Demo](https://img.shields.io/badge/Demo-Watch%20on%20Loom-00D4AA?logo=loom)](https://www.loom.com/share/09aa4a78e0c944dcab866a7036fde24d)
Expand Down Expand Up @@ -85,6 +85,7 @@ graph TD
- **Freeze control** — `freeze_invoice()`/`unfreeze_invoice()` admin blocks/re-enables `pay` (frozen field)
- **Invoice notes** — `set_invoice_notes()`/`get_invoice_notes()` free-text `InvoiceNotes { text, updated_at }`
- **Invoice tags**
- **Recurring pause** — `pause_recurring`/`resume_recurring`/`is_recurring_paused`
- **Discount config** — `set/get_discount` `DiscountConfig { discount_bps, updated_at }`
- **Invoice metadata** — `set/get_invoice_metadata` `InvoiceMetadata { entries, updated_at }`
- **Deadline extension** — `extend_deadline(caller, id, new_deadline)` creator can push deadline forward
Expand Down
7 changes: 7 additions & 0 deletions contracts/sharpy/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,10 @@ pub struct DiscountUpdatedEvent { pub invoice_id: u64, pub discount_bps: u32 }
pub fn discount_updated(env: &Env, invoice_id: u64, discount_bps: u32) {
env.events().publish((symbol_short!("disc"),), DiscountUpdatedEvent { invoice_id, discount_bps });
}

#[contracttype]
#[derive(Clone)]
pub struct RecurringPausedEvent { pub invoice_id: u64, pub paused: bool }
pub fn recurring_paused(env: &Env, invoice_id: u64, paused: bool) {
env.events().publish((symbol_short!("rpause"),), RecurringPausedEvent { invoice_id, paused });
}
29 changes: 28 additions & 1 deletion contracts/sharpy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mod test;
use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, Bytes, Env, Map, String, Symbol, Vec};
use types::{
AuditEntry, CreateInvoiceParams, DisputeState, Invoice, InvoiceNotes, InvoiceOptions,
InvoicePayment, InvoiceStats, InvoiceStatus, InvoiceTags, InvoiceExtraMemo, Payment, InvoiceMetadata, DiscountConfig, SplitRule,
InvoicePayment, InvoiceStats, InvoiceStatus, InvoiceTags, InvoiceExtraMemo, Payment, InvoiceMetadata, DiscountConfig, RecurringPauseState, SplitRule,
SubscriptionParams,
};

Expand All @@ -42,6 +42,7 @@ fn account_balance_key(account: &Address, token: &Address) -> (Symbol, Address,
}
fn invoice_notes_key(id: u64) -> (Symbol, u64) { (symbol_short!("notes"), id) }
fn invoice_tags_key(id: u64) -> (Symbol, u64) { (symbol_short!("itags"), id) }
fn recurring_pause_key(id: u64) -> (Symbol, u64) { (symbol_short!("rpause"), id) }
fn discount_key(id: u64) -> (Symbol, u64) { (symbol_short!("disc"), id) }
fn invoice_metadata_key(id: u64) -> (Symbol, u64) { (symbol_short!("imeta"), id) }
fn invoice_memo_ext_key(id: u64) -> (Symbol, u64) { (symbol_short!("imemo"), id) }
Expand Down Expand Up @@ -1080,6 +1081,32 @@ impl SharpyContract {
pub fn get_discount(env: Env, invoice_id: u64) -> Option<DiscountConfig> {
env.storage().persistent().get(&discount_key(invoice_id))
}

pub fn pause_recurring(env: Env, caller: Address, invoice_id: u64) {
caller.require_auth();
let invoice = load_invoice(&env, invoice_id);
assert!(invoice.creator == caller, "only creator can pause recurring");
let params: SubscriptionParams = env.storage().persistent().get(&recurring_params_key(invoice_id)).expect("not recurring");
let _ = params;
let state = RecurringPauseState { paused: true, updated_at: env.ledger().timestamp() };
env.storage().persistent().set(&recurring_pause_key(invoice_id), &state);
append_audit(&env, invoice_id, symbol_short!("rpause"), &caller);
events::recurring_paused(&env, invoice_id, true);
}
pub fn resume_recurring(env: Env, caller: Address, invoice_id: u64) {
caller.require_auth();
let invoice = load_invoice(&env, invoice_id);
assert!(invoice.creator == caller, "only creator can resume recurring");
let state: RecurringPauseState = env.storage().persistent().get(&recurring_pause_key(invoice_id)).expect("not paused");
assert!(state.paused, "not paused");
let new_state = RecurringPauseState { paused: false, updated_at: env.ledger().timestamp() };
env.storage().persistent().set(&recurring_pause_key(invoice_id), &new_state);
append_audit(&env, invoice_id, symbol_short!("resume"), &caller);
events::recurring_paused(&env, invoice_id, false);
}
pub fn is_recurring_paused(env: Env, invoice_id: u64) -> bool {
env.storage().persistent().get::<(Symbol,u64), RecurringPauseState>(&recurring_pause_key(invoice_id)).map(|s| s.paused).unwrap_or(false)
}
}

/// Validates that a token address is not the zero address.
Expand Down
25 changes: 25 additions & 0 deletions contracts/sharpy/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3705,3 +3705,28 @@ mod test_discount {
client.set_discount(&s, &id, &500u32);
}
}

#[cfg(test)]
mod test_recurring_pause {
use soroban_sdk::{testutils::Address as _, Address, Env, Vec};
use crate::SharpyContractClient;
fn setup() -> (Env, SharpyContractClient<'static>) { let env=Env::default(); env.mock_all_auths(); let cid=env.register(crate::SharpyContract, ()); let c=SharpyContractClient::new(&env,&cid); let a=Address::generate(&env); let t=Address::generate(&env); c.initialize(&a,&t); (env,c) }
#[test] fn test_pause_resume() {
let (env, client)=setup(); let creator=Address::generate(&env); let r=Address::generate(&env); let tok=Address::generate(&env); let dl=env.ledger().timestamp()+86400;
let id=client.create_recurring(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok]), &dl, &86400u64, &3u32);
assert!(!client.is_recurring_paused(&id));
client.pause_recurring(&creator, &id); assert!(client.is_recurring_paused(&id));
client.resume_recurring(&creator, &id); assert!(!client.is_recurring_paused(&id));
}
#[test] #[should_panic(expected="only creator can pause recurring")] fn test_pause_non_creator() {
let (env, client)=setup(); let creator=Address::generate(&env); let s=Address::generate(&env); let r=Address::generate(&env); let tok=Address::generate(&env); let dl=env.ledger().timestamp()+86400;
let id=client.create_recurring(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok]), &dl, &86400u64, &3u32);
client.pause_recurring(&s, &id);
}
#[test] #[should_panic(expected="not recurring")] fn test_pause_non_recurring_panics() {
let (env, client)=setup(); let creator=Address::generate(&env); let r=Address::generate(&env); let tok=Address::generate(&env); let dl=env.ledger().timestamp()+86400;
let id=client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok]), &dl, &crate::types::InvoiceOptions{escrow_enabled:false, escrow_release_delay:None, split_rules:Vec::new(&env), auto_resolve_rules:Vec::new(&env), arbitrator:None});
client.pause_recurring(&creator, &id);
}
}

5 changes: 5 additions & 0 deletions contracts/sharpy/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,8 @@ pub struct DiscountConfig {
/// Timestamp set.
pub updated_at: u64,
}

/// Pause state for recurring invoices.
#[contracttype]
#[derive(Clone, Debug)]
pub struct RecurringPauseState { pub paused: bool, pub updated_at: u64 }
Loading