Skip to content
Open
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
181 changes: 181 additions & 0 deletions contracts/credit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
//! is held in lots (each optionally expiring) so it can be applied to future
//! charges, transferred between accounts, and expired deterministically.
//!
//! Credit notes are formal documents stored off-chain; this contract manages
//! the on-chain prepayment wallet and credit-lot mechanics, and provides an
//! expiry checker suitable for cron-driven keeper jobs.
//!
//! Required behaviour (issue: credit system):
//! * `AccountCredit { balance, transactions[], expiration_policy }`
//! * manual and automatic issuance
//! * automatic application on charging via [`SubTrackrCredit::apply_credit`]
//! * transfer between accounts
//! * expiration handling and a full transaction history
//! * prepayment wallet with deposit/withdraw/drawdown
//!
//! Balances can never go negative: application/transfer only ever move credit
//! that is actually available and unexpired.
Expand All @@ -33,6 +38,7 @@ pub enum CreditError {
InvalidAmount = 4,
InsufficientCredit = 5,
SelfTransfer = 6,
WalletNotFound = 7,
}

#[contracttype]
Expand All @@ -43,6 +49,8 @@ pub enum CreditTxKind {
TransferIn,
TransferOut,
Expire,
Deposit,
Withdraw,
}

/// Default expiration applied to newly issued credit when no explicit expiry
Expand Down Expand Up @@ -99,12 +107,38 @@ pub struct CreditApplied {
pub balance_after: i128,
}

/// A prepayment wallet tied to a subscription for pre-funded draws.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct PrepaymentWallet {
pub id: u64,
pub subscriber: Address,
pub subscription_id: SubscriptionId,
pub currency: String,
pub balance: i128,
pub total_deposited: i128,
pub total_withdrawn: i128,
pub created_at: u64,
pub updated_at: u64,
}

/// Prepayment summary returned after a deposit or withdrawal.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct PrepaymentSnapshot {
pub wallet_id: u64,
pub balance: i128,
pub transaction_id: u64,
}

#[contracttype]
#[derive(Clone)]
enum DataKey {
Admin,
NextId,
Account(Address),
Wallet(u64),
Counter(u64),
}

#[contract]
Expand Down Expand Up @@ -306,6 +340,135 @@ impl SubTrackrCredit {
Self::account(&env, &subscriber).transactions
}

/// Creates a new prepayment wallet for the given subscription.
pub fn create_wallet(
env: Env,
subscriber: Address,
subscription_id: SubscriptionId,
currency: String,
) -> u64 {
let admin = Self::require_admin(&env).expect("admin required");
admin.require_auth();
let wallet_id = Self::next_wallet_id(&env);
let now = env.ledger().timestamp();
let wallet = PrepaymentWallet {
id: wallet_id,
subscriber: subscriber.clone(),
subscription_id,
currency,
balance: 0,
total_deposited: 0,
total_withdrawn: 0,
created_at: now,
updated_at: now,
};
env.storage().persistent().set(&DataKey::Wallet(wallet_id), &wallet);
env.events()
.publish((symbol_short!("wallet"), subscriber), wallet_id);
wallet_id
}

/// Deposits funds into a prepayment wallet by ID.
pub fn deposit(
env: Env,
caller: Address,
wallet_id: u64,
amount: i128,
) -> Result<PrepaymentSnapshot, CreditError> {
caller.require_auth();
if amount <= 0 {
return Err(CreditError::InvalidAmount);
}
let mut wallet: PrepaymentWallet = env
.storage()
.persistent()
.get(&DataKey::Wallet(wallet_id))
.ok_or(CreditError::WalletNotFound)?;
if wallet.subscriber != caller {
return Err(CreditError::Unauthorized);
}
let now = env.ledger().timestamp();
wallet.balance += amount;
wallet.total_deposited += amount;
wallet.updated_at = now;
env.storage().persistent().set(&DataKey::Wallet(wallet_id), &wallet);
Ok(PrepaymentSnapshot {
wallet_id,
balance: wallet.balance,
transaction_id: Self::next_tx_id(&env, wallet_id),
})
}

/// Withdraws funds from a prepayment wallet by ID.
pub fn withdraw(
env: Env,
caller: Address,
wallet_id: u64,
amount: i128,
) -> Result<PrepaymentSnapshot, CreditError> {
caller.require_auth();
if amount <= 0 {
return Err(CreditError::InvalidAmount);
}
let mut wallet: PrepaymentWallet = env
.storage()
.persistent()
.get(&DataKey::Wallet(wallet_id))
.ok_or(CreditError::WalletNotFound)?;
if wallet.subscriber != caller {
return Err(CreditError::Unauthorized);
}
if wallet.balance < amount {
return Err(CreditError::InsufficientCredit);
}
let now = env.ledger().timestamp();
wallet.balance -= amount;
wallet.total_withdrawn += amount;
wallet.updated_at = now;
env.storage().persistent().set(&DataKey::Wallet(wallet_id), &wallet);
Ok(PrepaymentSnapshot {
wallet_id,
balance: wallet.balance,
transaction_id: Self::next_tx_id(&env, wallet_id),
})
}

/// Returns the current balance of a prepayment wallet.
pub fn get_wallet_balance(env: Env, _caller: Address, wallet_id: u64) -> i128 {
env.storage()
.persistent()
.get::<_, PrepaymentWallet>(&DataKey::Wallet(wallet_id))
.map(|w| w.balance)
.unwrap_or(0)
}

/// Batch expiry processor for cron keepers. Iterates all stored wallets,
/// applies credit lot expiry, and returns total expired amounts. Caller
/// must be admin.
pub fn expire_credits_with_cron(env: Env, admin: Address) -> Vec<(Address, i128)> {
admin.require_auth();
let now = env.ledger().timestamp();
let mut results: Vec<(Address, i128)> = Vec::new(&env);
let mut i: u32 = 0;
while i < MAX_HISTORY {
let key = DataKey::Counter(i);
if !env.storage().persistent().has(&key) {
break;
}
let subscriber: Address = env.storage().persistent().get(&key).unwrap();
let mut account = Self::account(&env, &subscriber);
let before = account.balance;
Self::realize_expiry(&env, now, &mut account);
let expired = before - account.balance;
if expired > 0 {
Self::save(&env, &account);
results.push_back((subscriber, expired));
}
i += 1;
}
results
}

// ---- internals --------------------------------------------------------

fn require_admin(env: &Env) -> Result<Address, CreditError> {
Expand Down Expand Up @@ -340,6 +503,24 @@ impl SubTrackrCredit {
id
}

fn next_wallet_id(env: &Env) -> u64 {
let id: u64 = env.storage().instance().get(&DataKey::Admin).map(|_| id).unwrap_or(0);
let base: u64 = env.storage().instance().get(&symbol_short!("NWID")).unwrap_or(0);
env.storage()
.instance()
.set(&symbol_short!("NWID"), &(base + 1));
base
}

fn next_tx_id(env: &Env, _wallet_id: u64) -> u64 {
let base: u64 = env
.storage()
.instance()
.get(&symbol_short!("NWID"))
.unwrap_or(0);
base
}

/// Sum of unexpired lot balances.
fn available(now: u64, account: &AccountCredit) -> i128 {
let mut total: i128 = 0;
Expand Down
6 changes: 6 additions & 0 deletions src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const AdminDashboardScreen = lazyScreen(() => import('../screens/AdminDashboardS
const FraudDashboard = lazyScreen(() => import('../screens/FraudDashboard'));
const GroupManagementScreen = lazyScreen(() => import('../screens/GroupManagementScreen'));
const TaxSettingsScreen = lazyScreen(() => import('../screens/TaxSettingsScreen'));
const CreditsAndPrepaymentsScreen = lazyScreen(() => import('../screens/CreditsAndPrepaymentsScreen'));
const SupportDashboardScreen = lazyScreen(() => import('../screens/SupportDashboardScreen'));
const SegmentManagementScreen = lazyScreen(() =>
import('../screens/SegmentManagementScreen').then((m) => ({ default: m.SegmentManagementScreen }))
Expand Down Expand Up @@ -290,6 +291,11 @@ const SettingsStack = () => (
component={TaxSettingsScreen}
options={{ title: 'Tax Settings', headerShown: true }}
/>
<Stack.Screen
name="CreditsAndPrepayments"
component={CreditsAndPrepaymentsScreen}
options={{ title: 'Credits & Prepayments', headerShown: true }}
/>
<Stack.Screen
name="SupportDashboard"
component={SupportDashboardScreen}
Expand Down
1 change: 1 addition & 0 deletions src/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type RootStackParamList = {
FraudDashboard: undefined;
GroupManagement: undefined;
TaxSettings: undefined;
CreditsAndPrepayments: undefined;
SupportDashboard: undefined;
UsageDashboard: undefined;
DeveloperPortal: undefined;
Expand Down
Loading