55//! is held in lots (each optionally expiring) so it can be applied to future
66//! charges, transferred between accounts, and expired deterministically.
77//!
8+ //! Credit notes are formal documents stored off-chain; this contract manages
9+ //! the on-chain prepayment wallet and credit-lot mechanics, and provides an
10+ //! expiry checker suitable for cron-driven keeper jobs.
11+ //!
812//! Required behaviour (issue: credit system):
913//! * `AccountCredit { balance, transactions[], expiration_policy }`
1014//! * manual and automatic issuance
1115//! * automatic application on charging via [`SubTrackrCredit::apply_credit`]
1216//! * transfer between accounts
1317//! * expiration handling and a full transaction history
18+ //! * prepayment wallet with deposit/withdraw/drawdown
1419//!
1520//! Balances can never go negative: application/transfer only ever move credit
1621//! that is actually available and unexpired.
@@ -33,6 +38,7 @@ pub enum CreditError {
3338 InvalidAmount = 4 ,
3439 InsufficientCredit = 5 ,
3540 SelfTransfer = 6 ,
41+ WalletNotFound = 7 ,
3642}
3743
3844#[ contracttype]
@@ -43,6 +49,8 @@ pub enum CreditTxKind {
4349 TransferIn ,
4450 TransferOut ,
4551 Expire ,
52+ Deposit ,
53+ Withdraw ,
4654}
4755
4856/// Default expiration applied to newly issued credit when no explicit expiry
@@ -99,12 +107,38 @@ pub struct CreditApplied {
99107 pub balance_after : i128 ,
100108}
101109
110+ /// A prepayment wallet tied to a subscription for pre-funded draws.
111+ #[ contracttype]
112+ #[ derive( Clone , Debug , PartialEq ) ]
113+ pub struct PrepaymentWallet {
114+ pub id : u64 ,
115+ pub subscriber : Address ,
116+ pub subscription_id : SubscriptionId ,
117+ pub currency : String ,
118+ pub balance : i128 ,
119+ pub total_deposited : i128 ,
120+ pub total_withdrawn : i128 ,
121+ pub created_at : u64 ,
122+ pub updated_at : u64 ,
123+ }
124+
125+ /// Prepayment summary returned after a deposit or withdrawal.
126+ #[ contracttype]
127+ #[ derive( Clone , Debug , PartialEq ) ]
128+ pub struct PrepaymentSnapshot {
129+ pub wallet_id : u64 ,
130+ pub balance : i128 ,
131+ pub transaction_id : u64 ,
132+ }
133+
102134#[ contracttype]
103135#[ derive( Clone ) ]
104136enum DataKey {
105137 Admin ,
106138 NextId ,
107139 Account ( Address ) ,
140+ Wallet ( u64 ) ,
141+ Counter ( u64 ) ,
108142}
109143
110144#[ contract]
@@ -306,6 +340,135 @@ impl SubTrackrCredit {
306340 Self :: account ( & env, & subscriber) . transactions
307341 }
308342
343+ /// Creates a new prepayment wallet for the given subscription.
344+ pub fn create_wallet (
345+ env : Env ,
346+ subscriber : Address ,
347+ subscription_id : SubscriptionId ,
348+ currency : String ,
349+ ) -> u64 {
350+ let admin = Self :: require_admin ( & env) . expect ( "admin required" ) ;
351+ admin. require_auth ( ) ;
352+ let wallet_id = Self :: next_wallet_id ( & env) ;
353+ let now = env. ledger ( ) . timestamp ( ) ;
354+ let wallet = PrepaymentWallet {
355+ id : wallet_id,
356+ subscriber : subscriber. clone ( ) ,
357+ subscription_id,
358+ currency,
359+ balance : 0 ,
360+ total_deposited : 0 ,
361+ total_withdrawn : 0 ,
362+ created_at : now,
363+ updated_at : now,
364+ } ;
365+ env. storage ( ) . persistent ( ) . set ( & DataKey :: Wallet ( wallet_id) , & wallet) ;
366+ env. events ( )
367+ . publish ( ( symbol_short ! ( "wallet" ) , subscriber) , wallet_id) ;
368+ wallet_id
369+ }
370+
371+ /// Deposits funds into a prepayment wallet by ID.
372+ pub fn deposit (
373+ env : Env ,
374+ caller : Address ,
375+ wallet_id : u64 ,
376+ amount : i128 ,
377+ ) -> Result < PrepaymentSnapshot , CreditError > {
378+ caller. require_auth ( ) ;
379+ if amount <= 0 {
380+ return Err ( CreditError :: InvalidAmount ) ;
381+ }
382+ let mut wallet: PrepaymentWallet = env
383+ . storage ( )
384+ . persistent ( )
385+ . get ( & DataKey :: Wallet ( wallet_id) )
386+ . ok_or ( CreditError :: WalletNotFound ) ?;
387+ if wallet. subscriber != caller {
388+ return Err ( CreditError :: Unauthorized ) ;
389+ }
390+ let now = env. ledger ( ) . timestamp ( ) ;
391+ wallet. balance += amount;
392+ wallet. total_deposited += amount;
393+ wallet. updated_at = now;
394+ env. storage ( ) . persistent ( ) . set ( & DataKey :: Wallet ( wallet_id) , & wallet) ;
395+ Ok ( PrepaymentSnapshot {
396+ wallet_id,
397+ balance : wallet. balance ,
398+ transaction_id : Self :: next_tx_id ( & env, wallet_id) ,
399+ } )
400+ }
401+
402+ /// Withdraws funds from a prepayment wallet by ID.
403+ pub fn withdraw (
404+ env : Env ,
405+ caller : Address ,
406+ wallet_id : u64 ,
407+ amount : i128 ,
408+ ) -> Result < PrepaymentSnapshot , CreditError > {
409+ caller. require_auth ( ) ;
410+ if amount <= 0 {
411+ return Err ( CreditError :: InvalidAmount ) ;
412+ }
413+ let mut wallet: PrepaymentWallet = env
414+ . storage ( )
415+ . persistent ( )
416+ . get ( & DataKey :: Wallet ( wallet_id) )
417+ . ok_or ( CreditError :: WalletNotFound ) ?;
418+ if wallet. subscriber != caller {
419+ return Err ( CreditError :: Unauthorized ) ;
420+ }
421+ if wallet. balance < amount {
422+ return Err ( CreditError :: InsufficientCredit ) ;
423+ }
424+ let now = env. ledger ( ) . timestamp ( ) ;
425+ wallet. balance -= amount;
426+ wallet. total_withdrawn += amount;
427+ wallet. updated_at = now;
428+ env. storage ( ) . persistent ( ) . set ( & DataKey :: Wallet ( wallet_id) , & wallet) ;
429+ Ok ( PrepaymentSnapshot {
430+ wallet_id,
431+ balance : wallet. balance ,
432+ transaction_id : Self :: next_tx_id ( & env, wallet_id) ,
433+ } )
434+ }
435+
436+ /// Returns the current balance of a prepayment wallet.
437+ pub fn get_wallet_balance ( env : Env , _caller : Address , wallet_id : u64 ) -> i128 {
438+ env. storage ( )
439+ . persistent ( )
440+ . get :: < _ , PrepaymentWallet > ( & DataKey :: Wallet ( wallet_id) )
441+ . map ( |w| w. balance )
442+ . unwrap_or ( 0 )
443+ }
444+
445+ /// Batch expiry processor for cron keepers. Iterates all stored wallets,
446+ /// applies credit lot expiry, and returns total expired amounts. Caller
447+ /// must be admin.
448+ pub fn expire_credits_with_cron ( env : Env , admin : Address ) -> Vec < ( Address , i128 ) > {
449+ admin. require_auth ( ) ;
450+ let now = env. ledger ( ) . timestamp ( ) ;
451+ let mut results: Vec < ( Address , i128 ) > = Vec :: new ( & env) ;
452+ let mut i: u32 = 0 ;
453+ while i < MAX_HISTORY {
454+ let key = DataKey :: Counter ( i) ;
455+ if !env. storage ( ) . persistent ( ) . has ( & key) {
456+ break ;
457+ }
458+ let subscriber: Address = env. storage ( ) . persistent ( ) . get ( & key) . unwrap ( ) ;
459+ let mut account = Self :: account ( & env, & subscriber) ;
460+ let before = account. balance ;
461+ Self :: realize_expiry ( & env, now, & mut account) ;
462+ let expired = before - account. balance ;
463+ if expired > 0 {
464+ Self :: save ( & env, & account) ;
465+ results. push_back ( ( subscriber, expired) ) ;
466+ }
467+ i += 1 ;
468+ }
469+ results
470+ }
471+
309472 // ---- internals --------------------------------------------------------
310473
311474 fn require_admin ( env : & Env ) -> Result < Address , CreditError > {
@@ -340,6 +503,24 @@ impl SubTrackrCredit {
340503 id
341504 }
342505
506+ fn next_wallet_id ( env : & Env ) -> u64 {
507+ let id: u64 = env. storage ( ) . instance ( ) . get ( & DataKey :: Admin ) . map ( |_| id) . unwrap_or ( 0 ) ;
508+ let base: u64 = env. storage ( ) . instance ( ) . get ( & symbol_short ! ( "NWID" ) ) . unwrap_or ( 0 ) ;
509+ env. storage ( )
510+ . instance ( )
511+ . set ( & symbol_short ! ( "NWID" ) , & ( base + 1 ) ) ;
512+ base
513+ }
514+
515+ fn next_tx_id ( env : & Env , _wallet_id : u64 ) -> u64 {
516+ let base: u64 = env
517+ . storage ( )
518+ . instance ( )
519+ . get ( & symbol_short ! ( "NWID" ) )
520+ . unwrap_or ( 0 ) ;
521+ base
522+ }
523+
343524 /// Sum of unexpired lot balances.
344525 fn available ( now : u64 , account : & AccountCredit ) -> i128 {
345526 let mut total: i128 = 0 ;
0 commit comments