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: 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
- feat: batch refund — refund_batch for multiple invoices — Adds refund_batch(caller, ids) to refund multiple deadline-p
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-150%20passing-00D4AA)
![Tests](https://img.shields.io/badge/tests-153%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 @@ -81,6 +81,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**
- **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
- **Batch refund** — `refund_batch(caller, ids)` refund up to 10 deadline-passed invoices in one tx
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 @@ -272,3 +272,10 @@ pub struct InvoiceMetadataUpdatedEvent { pub invoice_id: u64, pub updater: Addre
pub fn invoice_metadata_updated(env: &Env, invoice_id: u64, updater: &Address) {
env.events().publish((symbol_short!("imeta"),), InvoiceMetadataUpdatedEvent { invoice_id, updater: updater.clone() });
}

#[contracttype]
#[derive(Clone)]
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 });
}
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, SplitRule,
InvoicePayment, InvoiceStats, InvoiceStatus, InvoiceTags, InvoiceExtraMemo, Payment, InvoiceMetadata, DiscountConfig, 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 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 @@ -1064,6 +1065,21 @@ impl SharpyContract {
pub fn get_invoice_metadata(env: Env, invoice_id: u64) -> Option<InvoiceMetadata> {
env.storage().persistent().get(&invoice_metadata_key(invoice_id))
}

pub fn set_discount(env: Env, caller: Address, invoice_id: u64, discount_bps: u32) {
require_not_paused(&env); caller.require_auth();
let invoice = load_invoice(&env, invoice_id);
assert!(invoice.creator == caller, "only creator can set discount");
assert!(discount_bps <= 10000, "discount exceeds 100%");
let cfg = DiscountConfig { discount_bps, updated_at: env.ledger().timestamp() };
env.storage().persistent().set(&discount_key(invoice_id), &cfg);
env.storage().persistent().extend_ttl(&discount_key(invoice_id), 100_000, 6_307_200);
append_audit(&env, invoice_id, symbol_short!("disc"), &caller);
events::discount_updated(&env, invoice_id, discount_bps);
}
pub fn get_discount(env: Env, invoice_id: u64) -> Option<DiscountConfig> {
env.storage().persistent().get(&discount_key(invoice_id))
}
}

/// 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 @@ -3680,3 +3680,28 @@ mod test_invoice_metadata {
}
}


#[cfg(test)]
mod test_discount {
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) }
fn no_rules(env: &Env) -> crate::types::InvoiceOptions { crate::types::InvoiceOptions{escrow_enabled:false, escrow_release_delay:None, split_rules:Vec::new(env), auto_resolve_rules:Vec::new(env), arbitrator:None} }
#[test] fn test_discount_set_get() {
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, &no_rules(&env));
assert!(client.get_discount(&id).is_none());
client.set_discount(&creator, &id, &1000u32);
assert_eq!(client.get_discount(&id).unwrap().discount_bps, 1000u32);
}
#[test] #[should_panic(expected="discount exceeds 100%")] fn test_discount_too_high() {
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, &no_rules(&env));
client.set_discount(&creator, &id, &10001u32);
}
#[test] #[should_panic(expected="only creator can set discount")] fn test_discount_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_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok]), &dl, &no_rules(&env));
client.set_discount(&s, &id, &500u32);
}
}
10 changes: 10 additions & 0 deletions contracts/sharpy/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,13 @@ pub struct InvoiceMetadata {
/// Last update timestamp.
pub updated_at: u64,
}

/// Discount configuration per invoice — basis points off total.
#[contracttype]
#[derive(Clone, Debug)]
pub struct DiscountConfig {
/// Discount in basis points (0-10000, 1000=10%).
pub discount_bps: u32,
/// Timestamp set.
pub updated_at: u64,
}
Loading