Smart Contract

PayAsYouGoBilling

A.f3a842f7887a9bce.PayAsYouGoBilling

Valid From

123,173,933

Deployed

6d ago
Feb 21, 2026, 10:18:33 PM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2
3access(all) contract PayAsYouGoBilling {
4    /// Storage and public paths for user allowances
5    access(all) let AllowanceStoragePath: StoragePath
6    access(all) let AllowancePublicPath: PublicPath
7
8    /// Minimal interface exposed to providers for charging
9            access(all) resource interface Charge {
10        access(all) fun charge(amount: UFix64)
11        access(all) view fun getRemainingCapacity(): UFix64
12        access(all) view fun getConfig(): PayAsYouGoBilling.Config
13    }
14
15    /// Immutable configuration for an allowance
16    access(all) struct Config {
17        access(all) let minCharge: UFix64
18        access(all) let monthlyMax: UFix64
19        access(all) let periodSeconds: UFix64
20        access(all) let vaultType: Type
21        access(all) let user: Address
22        access(all) let provider: Address
23
24        init(
25            minCharge: UFix64,
26            monthlyMax: UFix64,
27            periodSeconds: UFix64,
28            vaultType: Type,
29            user: Address,
30            provider: Address
31        ) {
32            pre {
33                minCharge > 0.0: "minCharge must be > 0"
34                monthlyMax >= minCharge: "monthlyMax must be >= minCharge"
35                periodSeconds > 0.0: "periodSeconds must be > 0"
36            }
37            self.minCharge = minCharge
38            self.monthlyMax = monthlyMax
39            self.periodSeconds = periodSeconds
40            self.vaultType = vaultType
41            self.user = user
42            self.provider = provider
43        }
44    }
45
46    /// Allowance resource held by the USER, callable by the PROVIDER through a public capability
47    access(all) resource Allowance: Charge {
48        /// Capability to withdraw from the user's vault of the configured token
49        access(contract) let userVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>
50        /// Capability for the provider's receiver to receive the configured token
51        access(contract) let providerReceiverCap: Capability<&{FungibleToken.Receiver}>
52        /// Configuration snapshot
53        access(all) let config: PayAsYouGoBilling.Config
54        /// Rolling period tracking
55        access(contract) var periodStartTimestamp: UFix64
56        access(contract) var chargedThisPeriod: UFix64
57
58        init(
59            userVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>,
60            providerReceiverCap: Capability<&{FungibleToken.Receiver}>,
61            config: PayAsYouGoBilling.Config
62        ) {
63            // Validate caps and type matching
64            let userVault = userVaultCap.borrow()
65                ?? panic("Invalid user vault capability")
66            let providerReceiver = providerReceiverCap.borrow()
67                ?? panic("Invalid provider receiver capability")
68            if userVault.getType().identifier != providerReceiver.getType().identifier {
69                panic("Mismatched token types")
70            }
71            if userVault.getType().identifier != config.vaultType.identifier {
72                panic("Config vault type mismatch")
73            }
74            self.userVaultCap = userVaultCap
75            self.providerReceiverCap = providerReceiverCap
76            self.config = config
77            self.periodStartTimestamp = getCurrentBlock().timestamp
78            self.chargedThisPeriod = 0.0
79        }
80
81        /// Ensure the rolling period is correctly maintained and reset when elapsed
82        access(contract) fun ensurePeriodFresh() {
83            let now: UFix64 = getCurrentBlock().timestamp
84            if now >= self.periodStartTimestamp + self.config.periodSeconds {
85                self.periodStartTimestamp = now
86                self.chargedThisPeriod = 0.0
87            }
88        }
89
90        /// Returns the remaining capacity for this period (after refreshing the window)
91        access(all) view fun getRemainingCapacity(): UFix64 {
92            let now: UFix64 = getCurrentBlock().timestamp
93            let periodEnd: UFix64 = self.periodStartTimestamp + self.config.periodSeconds
94            if now >= periodEnd {
95                return self.config.monthlyMax
96            }
97            if self.chargedThisPeriod >= self.config.monthlyMax {
98                return 0.0
99            }
100            return self.config.monthlyMax - self.chargedThisPeriod
101        }
102
103        /// Returns immutable configuration
104        access(all) view fun getConfig(): PayAsYouGoBilling.Config {
105            return self.config
106        }
107
108        /// Provider-triggered charge respecting min per-charge and monthly cap
109        access(all) fun charge(amount: UFix64) {
110            // Validate against current period capacity without mutating
111            if amount < self.config.minCharge {
112                panic("Amount below minCharge")
113            }
114            if amount > self.getRemainingCapacity() {
115                panic("Amount exceeds remaining capacity for this period")
116            }
117            // Refresh period and perform the charge
118            self.ensurePeriodFresh()
119            let vault <- self.userVaultCap.borrow()!.withdraw(amount: amount)
120            self.providerReceiverCap.borrow()!.deposit(from: <-vault)
121            self.chargedThisPeriod = self.chargedThisPeriod + amount
122        }
123    }
124
125    /// Create a new allowance. Caller should save it and link a public capability restricted to Charge
126    access(all) fun createAllowance(
127        userVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>,
128        providerReceiverCap: Capability<&{FungibleToken.Receiver}>,
129        minCharge: UFix64,
130        monthlyMax: UFix64,
131        periodSeconds: UFix64,
132        user: Address,
133        provider: Address
134    ): @PayAsYouGoBilling.Allowance {
135        let vt = userVaultCap.borrow()
136            ?? panic("Invalid user vault cap when creating allowance")
137        let config = PayAsYouGoBilling.Config(
138            minCharge: minCharge,
139            monthlyMax: monthlyMax,
140            periodSeconds: periodSeconds,
141            vaultType: vt.getType(),
142            user: user,
143            provider: provider
144        )
145        return <- create PayAsYouGoBilling.Allowance(
146            userVaultCap: userVaultCap,
147            providerReceiverCap: providerReceiverCap,
148            config: config
149        )
150    }
151
152    init() {
153        self.AllowanceStoragePath = StoragePath(identifier: "paygAllowance")!
154        self.AllowancePublicPath = PublicPath(identifier: "paygAllowance")!
155    }
156}