diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2fd1631..1c4041c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,3 +34,16 @@ jobs: - name: Run Unit Tests run: cargo test + + - name: Install tarpaulin + run: cargo install cargo-tarpaulin + + - name: Run Code Coverage + run: | + cargo tarpaulin --out Lcov --output-dir coverage + + - name: Upload Coverage report + uses: actions/upload-artifact@v3 + with: + name: code-coverage-report + path: coverage/lcov.info diff --git a/Cargo.lock b/Cargo.lock index b88f6e5..a5c0689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "arbitrary" version = "1.3.2" @@ -257,6 +307,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "const-oid" version = "0.9.6" @@ -311,6 +407,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "ctor" version = "0.5.0" @@ -484,6 +601,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -573,6 +711,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.2" @@ -700,6 +844,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -783,6 +933,23 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -829,6 +996,12 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.181" @@ -841,6 +1014,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + [[package]] name = "log" version = "0.4.29" @@ -915,6 +1097,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "p256" version = "0.13.2" @@ -968,6 +1156,20 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -1025,6 +1227,17 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -1070,6 +1283,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "schemars" version = "0.8.22" @@ -1508,6 +1727,14 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "substream-simulator" +version = "0.1.0" +dependencies = [ + "clap", + "prettytable-rs", +] + [[package]] name = "substream_contracts" version = "0.0.0" @@ -1543,6 +1770,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1606,6 +1844,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" @@ -1711,6 +1961,28 @@ dependencies = [ "indexmap-nostd", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -1770,6 +2042,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "zerocopy" version = "0.8.39" diff --git a/Cargo.toml b/Cargo.toml index ae69fd7..650c716 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "contracts/*", + "simulator", ] [workspace.dependencies] diff --git a/contracts/substream_contracts/src/lib.rs b/contracts/substream_contracts/src/lib.rs index fe3cbbd..99ccfe6 100644 --- a/contracts/substream_contracts/src/lib.rs +++ b/contracts/substream_contracts/src/lib.rs @@ -50,6 +50,8 @@ pub enum DataKey { GiftsReceived(Address), CreatorSplit(Address), ContractAdmin, + ProtocolFeeBps, + Moderator(Address), VerifiedCreator(Address), } @@ -130,13 +132,34 @@ impl SubStreamContract { env.storage().persistent().set(&DataKey::ContractAdmin, &admin); } - pub fn verify_creator(env: Env, admin: Address, creator: Address) { + pub fn verify_creator(env: Env, caller: Address, creator: Address) { + caller.require_auth(); + let admin: Address = env.storage().persistent().get(&DataKey::ContractAdmin).expect("not initialized"); + let is_mod = env.storage().persistent().get(&DataKey::Moderator(caller.clone())).unwrap_or(false); + + if caller != admin && !is_mod { + panic!("unauthorized: admin or moderator required"); + } + + env.storage().persistent().set(&DataKey::VerifiedCreator(creator.clone()), &true); + CreatorVerified { creator, verified_by: caller }.publish(&env); + } + + pub fn set_moderator(env: Env, admin: Address, moderator: Address, status: bool) { admin.require_auth(); let stored_admin: Address = env.storage().persistent().get(&DataKey::ContractAdmin).expect("not initialized"); if admin != stored_admin { panic!("admin only"); } - env.storage().persistent().set(&DataKey::VerifiedCreator(creator.clone()), &true); - CreatorVerified { creator, verified_by: admin }.publish(&env); + env.storage().persistent().set(&DataKey::Moderator(moderator), &status); + } + + pub fn set_protocol_fee(env: Env, admin: Address, fee_bps: u32) { + admin.require_auth(); + let stored_admin: Address = env.storage().persistent().get(&DataKey::ContractAdmin).expect("not initialized"); + if admin != stored_admin { panic!("admin only"); } + if fee_bps > 10000 { panic!("invalid fee bps"); } + + env.storage().persistent().set(&DataKey::ProtocolFeeBps, &fee_bps); } pub fn is_creator_verified(env: Env, creator: Address) -> bool { diff --git a/scripts/generate_bindings.ps1 b/scripts/generate_bindings.ps1 new file mode 100644 index 0000000..e39f13f --- /dev/null +++ b/scripts/generate_bindings.ps1 @@ -0,0 +1,26 @@ +# Generate TypeScript bindings for the SubStream Soroban contract (PowerShell) + +$CONTRACT_WASM = "./target/wasm32-unknown-unknown/release/substream_contracts.wasm" +$OUTPUT_DIR = "./bindings/substream" + +# 1. Build contract if wasm doesn't exist +if (-not (Test-Path $CONTRACT_WASM)) { + Write-Host "WASM not found. Building contract..." -ForegroundColor Yellow + cargo build --target wasm32-unknown-unknown --release +} + +# 2. Check for stellar-cli +if (-not (Get-Command "stellar" -ErrorAction SilentlyContinue)) { + Write-Host "stellar-cli could not be found. Please install it with 'cargo install --locked stellar-cli'" -ForegroundColor Red + exit +} + +# 3. Generate bindings +Write-Host "Generating TypeScript bindings for $CONTRACT_WASM..." -ForegroundColor Cyan +if (-not (Test-Path $OUTPUT_DIR)) { + New-Item -ItemType Directory -Path $OUTPUT_DIR -Force | Out-Null +} + +stellar contract bindings typescript --wasm $CONTRACT_WASM --output-dir $OUTPUT_DIR --overwrite + +Write-Host "Bindings generated in $OUTPUT_DIR" -ForegroundColor Green diff --git a/scripts/generate_bindings.sh b/scripts/generate_bindings.sh new file mode 100644 index 0000000..0fc2179 --- /dev/null +++ b/scripts/generate_bindings.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Generate TypeScript bindings for the SubStream Soroban contract + +CONTRACT_WASM="./target/wasm32-unknown-unknown/release/substream_contracts.wasm" +OUTPUT_DIR="./bindings/substream" + +# 1. Build contract if wasm doesn't exist +if [ ! -f "$CONTRACT_WASM" ]; then + echo "WASM not found. Building contract..." + cargo build --target wasm32-unknown-unknown --release +fi + +# 2. Check for stellar-cli +if ! command -v stellar &> /dev/null +then + echo "stellar-cli could not be found. Please install it with 'cargo install --locked stellar-cli'" + exit 1 +fi + +# 3. Generate bindings +echo "Generating TypeScript bindings for $CONTRACT_WASM..." +mkdir -p $OUTPUT_DIR +stellar contract bindings typescript --wasm $CONTRACT_WASM --output-dir $OUTPUT_DIR --overwrite + +echo "Bindings generated in $OUTPUT_DIR" diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml new file mode 100644 index 0000000..5d08a6d --- /dev/null +++ b/simulator/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "substream-simulator" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.0", features = ["derive"] } +prettytable-rs = "0.10" diff --git a/simulator/src/main.rs b/simulator/src/main.rs new file mode 100644 index 0000000..1c6068b --- /dev/null +++ b/simulator/src/main.rs @@ -0,0 +1,63 @@ +use clap::Parser; +use prettytable::{Table, row}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Initial number of subscribers + #[arg(short, long, default_value_t = 100)] + subscribers: u32, + + /// Monthly growth rate in percentage (e.g., 10 for 10%) + #[arg(short, long, default_value_t = 15.0)] + growth: f64, + + /// Monthly churn rate in percentage (e.g., 5 for 5%) + #[arg(short, long, default_value_t = 5.0)] + churn: f64, + + /// Monthly subscription fee in dollars + #[arg(short, long, default_value_t = 10.0)] + fee: f64, + + /// Number of months to simulate + #[arg(short, long, default_value_t = 12)] + months: u32, +} + +fn main() { + let args = Args::parse(); + + let mut current_subs = args.subscribers as f64; + let growth_rate = args.growth / 100.0; + let churn_rate = args.churn / 100.0; + + let mut table = Table::new(); + table.add_row(row!["Month", "New Subs", "Lost Subs", "Net Subs", "Revenue ($)"]); + + for month in 1..=args.months { + let newcomers = current_subs * growth_rate; + let churners = current_subs * churn_rate; + let net_growth = newcomers - churners; + current_subs += net_growth; + + let monthly_revenue = current_subs * args.fee; + + table.add_row(row![ + month, + format!("{:.1}", newcomers), + format!("{:.1}", churners), + format!("{:.0}", current_subs.max(0.0)), + format!("{:.2}", monthly_revenue.max(0.0)) + ]); + } + + println!("\nSubStream Creator Revenue Simulator"); + println!("----------------------------------"); + println!("Initial Subs: {}", args.subscribers); + println!("Monthly Growth: {}%", args.growth); + println!("Monthly Churn: {}%", args.churn); + println!("Subscription Fee: ${}", args.fee); + println!("\nProjection:"); + table.printstd(); +}