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: invoice templates — reusable invoice configs — Adds InvoiceTemplate struct and create/get_template function
- 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
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-156%20passing-00D4AA)
![Tests](https://img.shields.io/badge/tests-159%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**
- **Invoice templates** — `create_template`/`get_template` reusable configs `InvoiceTemplate`
- **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 }`
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 @@ -286,3 +286,10 @@ 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 });
}

#[contracttype]
#[derive(Clone)]
pub struct TemplateCreatedEvent { pub template_id: u64, pub creator: Address }
pub fn template_created(env: &Env, template_id: u64, creator: &Address) {
env.events().publish((symbol_short!("tmpl"),), TemplateCreatedEvent { template_id, creator: creator.clone() });
}
18 changes: 17 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, RecurringPauseState, SplitRule,
InvoicePayment, InvoiceStats, InvoiceStatus, InvoiceTags, InvoiceExtraMemo, Payment, InvoiceMetadata, DiscountConfig, RecurringPauseState, InvoiceTemplate, 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 template_key(id: u64) -> (Symbol, u64) { (symbol_short!("tmpl"), id) } fn template_counter_key() -> Symbol { symbol_short!("tmpl_ctr") }
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) }
Expand Down Expand Up @@ -1107,6 +1108,21 @@ impl SharpyContract {
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)
}

pub fn create_template(env: Env, creator: Address, name: String, recipients: Vec<Address>, amounts: Vec<i128>) -> u64 {
creator.require_auth();
assert!(!recipients.is_empty(), "recipients empty");
assert_eq!(recipients.len(), amounts.len(), "length mismatch");
let ctr: u64 = env.storage().persistent().get(&template_counter_key()).unwrap_or(0) + 1;
env.storage().persistent().set(&template_counter_key(), &ctr);
let tmpl = InvoiceTemplate { name: name.clone(), recipients: recipients.clone(), amounts: amounts.clone(), template_id: ctr };
env.storage().persistent().set(&template_key(ctr), &tmpl);
events::template_created(&env, ctr, &creator);
ctr
}
pub fn get_template(env: Env, template_id: u64) -> Option<InvoiceTemplate> {
env.storage().persistent().get(&template_key(template_id))
}
}

/// Validates that a token address is not the zero address.
Expand Down
22 changes: 22 additions & 0 deletions contracts/sharpy/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3730,3 +3730,25 @@ mod test_recurring_pause {
}
}

#[cfg(test)]
mod test_template {
use soroban_sdk::{testutils::Address as _, Address, Env, String, 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_create_get_template() {
let (env, client)=setup(); let creator=Address::generate(&env); let r=Address::generate(&env);
let id=client.create_template(&creator, &String::from_str(&env, "standard"), &Vec::from_array(&env, [r.clone()]), &Vec::from_array(&env, [100i128]));
let tmpl=client.get_template(&id).unwrap(); assert_eq!(tmpl.name, String::from_str(&env, "standard")); assert_eq!(tmpl.recipients.get(0).unwrap(), r);
}
#[test] fn test_template_counter_increments() {
let (env, client)=setup(); let creator=Address::generate(&env); let r=Address::generate(&env);
let id1=client.create_template(&creator, &String::from_str(&env, "a"), &Vec::from_array(&env, [r.clone()]), &Vec::from_array(&env, [10i128]));
let id2=client.create_template(&creator, &String::from_str(&env, "b"), &Vec::from_array(&env, [r]), &Vec::from_array(&env, [20i128]));
assert_eq!(id2, id1+1);
}
#[test] #[should_panic(expected="recipients empty")] fn test_template_empty_panics() {
let (env, client)=setup(); let creator=Address::generate(&env);
client.create_template(&creator, &String::from_str(&env, "empty"), &Vec::new(&env), &Vec::new(&env));
}
}

10 changes: 10 additions & 0 deletions contracts/sharpy/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,13 @@ pub struct DiscountConfig {
#[contracttype]
#[derive(Clone, Debug)]
pub struct RecurringPauseState { pub paused: bool, pub updated_at: u64 }

/// Reusable invoice template stored on-chain.
#[contracttype]
#[derive(Clone, Debug)]
pub struct InvoiceTemplate {
pub name: soroban_sdk::String,
pub recipients: soroban_sdk::Vec<Address>,
pub amounts: soroban_sdk::Vec<i128>,
pub template_id: u64,
}
Loading