Smart Contract
PayAsYouGoBilling
A.f3a842f7887a9bce.PayAsYouGoBilling
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}