Smart Contract
TitStakingX0X
A.fdfe39186c5e3b90.TitStakingX0X
1import NonFungibleToken from 0x1d7e57aa55817448
2import TitToken from 0x66b60643244a7738 // Replace with your actual TitToken address
3import FungibleToken from 0xf233dcee88fe0abe
4import PagesX from 0xfdfe39186c5e3b90
5import CourtiersX from 0xfdfe39186c5e3b90
6
7
8access(all) contract TitStakingX0X {
9
10 access(all) let StakingStoragePath: StoragePath
11 access(all) let StakingPublicPath: PublicPath
12 access(all) let StakingPrivatePath: PrivatePath
13 access(all) let AdminStoragePath: StoragePath
14
15 // Minimum staking duration (in seconds) before rewards begin accruing.
16 access(all) let minStakeDuration: UFix64
17
18 // Reward rates (Tit Tokens per second):
19 access(all) let rewardRatePages: UFix64
20 access(all) let rewardRateCourtiers: UFix64
21
22 // Treasury vault holding Tit Tokens used for rewards.
23 access(all) var treasuryVault: @TitToken.Vault
24
25 // Staked NFT metadata storage
26 access(all) var stakedNFTMetadata: {Address: {UInt64: StakedNFTMetadata}}
27
28 // ✅ FIX: Use `{Address: {UInt64: @{NonFungibleToken.NFT}}}` to store actual NFTs
29 access(all) var stakedNFTAssets: @{Address: {UInt64: {NonFungibleToken.NFT}}}
30
31 // Store reward history for each NFT that was unstaked
32 access(all) var rewardHistory: {Address: {UInt64: UFix64}}
33
34
35 // Struct to hold staked NFT metadata.
36 access(all) struct StakedNFTMetadata {
37 access(all) let nftID: UInt64
38 access(all) let nftType: String
39 access(all) let nftName: String
40 access(self) var stakeTimestamp: UFix64 // Private to struct, mutable within the struct
41 access(all) var level: UInt64
42
43 init(nftID: UInt64, nftType: String, nftName: String, stakeTimestamp: UFix64, level: UInt64) {
44 self.nftID = nftID
45 self.nftType = nftType
46 self.nftName = nftName
47 self.stakeTimestamp = stakeTimestamp
48 self.level = level
49 }
50
51 // Setter function to modify stakeTimestamp
52 access(all) fun updateStakeTimestamp(newTimestamp: UFix64) {
53 self.stakeTimestamp = newTimestamp
54 }
55
56 // ✅ Getter function to access stakeTimestamp
57 access(all) fun getStakeTimestamp(): UFix64 {
58 return self.stakeTimestamp
59 }
60
61 // Setter function to update the level
62 access(all) fun setLevel(newLevel: UInt64) {
63 self.level = newLevel
64 }
65
66 }
67
68
69 // Events
70
71 access(all) event NFTStaked(owner: Address, nftID: UInt64, nftType: String, nftName: String, stakeTimestamp: UFix64)
72 access(all) event NFTUnstaked(owner: Address, nftID: UInt64)
73 access(all) event RewardsClaimed(owner: Address, totalReward: UFix64)
74 access(all) event TreasuryFunded(newBalance: UFix64)
75
76 access(all) resource interface TitStakePublic {
77 access(all) fun stakeNFT(nft: @{NonFungibleToken.NFT}, nftID: UInt64, nftType: String, nftName: String, level: UInt64, owner: Address)
78 access(all) fun calculateRewards(owner: Address): UFix64
79 access(all) fun getStakedNFTDetails(owner: Address): [StakedNFTMetadata]
80 }
81
82 // ✅ Private interface for owner-only actions
83 // access(self) resource interface TitStakePrivate {
84 // access(all) fun unstakeNFT(nftID: UInt64): @{NonFungibleToken.NFT}
85 // access(all) fun claimRewards(recipient: &{FungibleToken.Receiver}): @TitToken.Vault
86 // }
87
88 // ✅ Staking resource with proper access control
89 access(all) resource TitStake: TitStakePublic {
90
91
92
93 // 🔒 Secure stake function (No owner parameter)
94 access(all) fun stakeNFT(
95 nft: @{NonFungibleToken.NFT},
96 nftID: UInt64,
97 nftType: String,
98 nftName: String,
99 level: UInt64, // Level is now passed in directly
100 owner: Address
101 ) {
102 let currentTime = getCurrentBlock().timestamp
103
104 // ✅ Store the level directly from the argument
105 var metadataDict = TitStakingX0X.stakedNFTMetadata[owner] ?? {}
106 metadataDict[nftID] = StakedNFTMetadata(
107 nftID: nftID,
108 nftType: nftType,
109 nftName: nftName,
110 stakeTimestamp: currentTime,
111 level: level // Store the level here directly
112 )
113 TitStakingX0X.stakedNFTMetadata[owner] = metadataDict
114
115 let maybeAssetsDict <- TitStakingX0X.stakedNFTAssets.remove(key: owner)
116 ?? panic("No existing staked assets found for this owner.")
117
118 var assetsDict <- maybeAssetsDict
119 assetsDict[nftID] <-! nft
120 TitStakingX0X.stakedNFTAssets[owner] <-! assetsDict
121
122 emit NFTStaked(owner: owner, nftID: nftID, nftType: nftType, nftName: nftName, stakeTimestamp: currentTime)
123 }
124
125
126
127 // 🔒 Secure unstake function (Only owner can call)
128
129 //WE NEED TO ADD A SECTION FOR NFT TYPES IN HERE, NOT THAT THEY HAVE THE SAME ID FOR TWO TYPES AND THE CODE DOESN'T KNOW WHO TO REMOVE
130
131
132 access(all) fun unstakeNFT(nftID: UInt64, nftType: String, owner: Address): @{NonFungibleToken.NFT} {
133 let maybeAssetsDict <- TitStakingX0X.stakedNFTAssets.remove(key: owner)
134 ?? panic("No staked NFT assets found for owner")
135 var assetsDict <- maybeAssetsDict
136 let nft <- assetsDict.remove(key: nftID)
137 ?? panic("NFT asset not found")
138
139 if assetsDict.length > 0 {
140 TitStakingX0X.stakedNFTAssets[owner] <-! assetsDict
141 } else {
142 destroy assetsDict
143 }
144
145 var metadataDict = TitStakingX0X.stakedNFTMetadata[owner]
146 ?? panic("No staked NFT metadata found for owner")
147 let nftMetadata = metadataDict[nftID]
148 ?? panic("NFT metadata not found")
149
150 // Check if the nftType matches
151 if nftMetadata.nftType != nftType {
152 panic("NFT type mismatch during unstaking")
153 }
154
155 // Calculate and store the accumulated rewards for this NFT
156 let elapsed = getCurrentBlock().timestamp - nftMetadata.getStakeTimestamp()
157
158 var baseReward: UFix64 = 0.0
159
160 // ✅ Fixed: Using standard if-else block for baseReward calculation
161 if nftMetadata.nftType == "pages" {
162 baseReward = elapsed * TitStakingX0X.rewardRatePages
163 } else {
164 baseReward = elapsed * TitStakingX0X.rewardRateCourtiers
165 }
166
167 let multiplier = 1.0 + (UFix64(nftMetadata.level) / 15.0)
168 let totalReward = baseReward * multiplier
169
170 // Get the existing reward history for the owner
171 var historyDict = TitStakingX0X.rewardHistory[owner] ?? {}
172
173 // Update or add the reward for this specific NFT
174 historyDict[nftID] = (historyDict[nftID] ?? 0.0) + totalReward
175
176 // Save the updated reward history back to the contract state
177 TitStakingX0X.rewardHistory[owner] = historyDict
178
179
180 metadataDict.remove(key: nftID)
181 TitStakingX0X.stakedNFTMetadata[owner] = metadataDict
182
183 emit NFTUnstaked(owner: owner, nftID: nftID)
184 return <- nft
185 }
186
187 // 🔒 Secure claim rewards function (Only owner can call)
188 access(all) fun claimRewards(recipient: &{FungibleToken.Receiver}, owner: Address): @TitToken.Vault {
189 let currentTime = getCurrentBlock().timestamp
190 var totalReward: UFix64 = 0.0
191
192 // Fetch staked NFT metadata
193 var metadataDict = TitStakingX0X.stakedNFTMetadata[owner]
194 ?? panic("No staked NFT metadata found for owner")
195
196 var updatedMetadataDict: {UInt64: StakedNFTMetadata} = {}
197
198 for nftID in metadataDict.keys {
199 var metadata = metadataDict[nftID]!
200 let elapsed = currentTime - metadata.getStakeTimestamp()
201
202 if elapsed >= TitStakingX0X.minStakeDuration {
203 var baseReward: UFix64 = 0.0
204 if metadata.nftType == "pages" {
205 baseReward = elapsed * TitStakingX0X.rewardRatePages
206 } else if metadata.nftType == "courtiers" {
207 baseReward = elapsed * TitStakingX0X.rewardRateCourtiers
208 }
209 let multiplier = 1.0 + (UFix64(metadata.level) / 15.0)
210 totalReward = totalReward + (baseReward * multiplier)
211
212 // ✅ ✅ ✅ Recreate the struct and reinsert it into the dictionary
213 let newMetadata = StakedNFTMetadata(
214 nftID: metadata.nftID,
215 nftType: metadata.nftType,
216 nftName: metadata.nftName,
217 stakeTimestamp: currentTime, // 🔥 SET NEW TIMESTAMP
218 level: metadata.level
219 )
220
221 updatedMetadataDict[nftID] = newMetadata
222 } else {
223 updatedMetadataDict[nftID] = metadata
224 }
225 }
226
227 // ✅ Overwrite storage with the updated dictionary
228 TitStakingX0X.stakedNFTMetadata[owner] = updatedMetadataDict
229
230 // ✅ Also include rewards from unstaked NFTs
231 let historyDict = TitStakingX0X.rewardHistory[owner] ?? {}
232 for reward in historyDict.values {
233 totalReward = totalReward + reward
234 }
235
236 // 🔥 Clear reward history after successfully claiming
237 if totalReward > 0.0 {
238 TitStakingX0X.rewardHistory[owner] = {}
239 }
240
241 // Handle the token transfer
242 if totalReward <= 0.0 {
243 panic("No rewards available or minimum staking duration not met")
244 }
245 if TitStakingX0X.treasuryVault.balance < totalReward {
246 panic("Insufficient funds in treasury")
247 }
248
249 let rewardVault <- TitStakingX0X.treasuryVault.withdraw(amount: totalReward)
250
251 emit RewardsClaimed(owner: owner, totalReward: totalReward)
252 recipient.deposit(from: <- rewardVault)
253
254 return <- TitToken.createEmptyVault(vaultType: Type<@TitToken.Vault>())
255 }
256
257
258 // 🔒 Secure reward calculation
259 access(all) fun calculateRewards(owner: Address): UFix64 {
260 let currentTime = getCurrentBlock().timestamp
261 var totalReward: UFix64 = 0.0
262
263 // Calculate rewards for currently staked NFTs
264 let metadataDict = TitStakingX0X.stakedNFTMetadata[owner]
265 ?? panic("No staked NFT metadata for owner")
266
267 for metadata in metadataDict.values {
268 let elapsed = currentTime - metadata.getStakeTimestamp()
269 if elapsed >= TitStakingX0X.minStakeDuration {
270 var baseReward: UFix64 = 0.0
271 if metadata.nftType == "pages" {
272 baseReward = elapsed * TitStakingX0X.rewardRatePages
273 } else if metadata.nftType == "courtiers" {
274 baseReward = elapsed * TitStakingX0X.rewardRateCourtiers
275 }
276 // Compute multiplier: ranges linearly from 1 (level 0) to 2 (level 15)
277 let multiplier = 1.0 + (UFix64(metadata.level) / 15.0)
278 totalReward = totalReward + (baseReward * multiplier)
279 }
280 }
281
282 // ✅ Include rewards from previously unstaked NFTs
283 let historyDict = TitStakingX0X.rewardHistory[owner] ?? {}
284 for reward in historyDict.values {
285 totalReward = totalReward + reward
286 }
287
288 return totalReward
289 }
290
291
292 // Retrieve details of all staked NFTs for a given owner.
293 access(all) fun getStakedNFTDetails(owner: Address): [StakedNFTMetadata] {
294 let metadataDict = TitStakingX0X.stakedNFTMetadata[owner] ?? {}
295 var details: [StakedNFTMetadata] = []
296 for metadata in metadataDict.values {
297 details.append(metadata)
298 }
299 return details
300 }
301
302 access(all) fun createTitStaking(): @TitStake {
303 return <- create TitStakingX0X.TitStake()
304 }
305
306
307
308 }
309
310
311 access(all) fun createTitStaking(owner: Address): @TitStake {
312 return <- create TitStake()
313 }
314
315
316
317 // Initialization
318 init(
319
320 ) {
321 self.StakingStoragePath = /storage/titStakingXStorage
322 self.StakingPublicPath = /public/titStakingXPublic
323 self.StakingPrivatePath = /private/titStakingXPrivate
324 self.AdminStoragePath = /storage/titStakingXAdmin
325
326 // Use the no-argument version of createEmptyVault()
327 self.treasuryVault <- TitToken.createEmptyVault(vaultType: Type<@TitToken.Vault>())
328 self.minStakeDuration = 1.0
329 self.rewardRatePages = 0.00000579
330 self.rewardRateCourtiers = 0.00001158
331 self.rewardHistory = {}
332 self.stakedNFTMetadata = {}
333 self.stakedNFTAssets <- {} as @{Address: {UInt64: {NonFungibleToken.NFT}}}
334 }
335
336
337 access(all) fun depositToTreasury(from: @TitToken.Vault) {
338 self.treasuryVault.deposit(from: <- from)
339 emit TreasuryFunded(newBalance: self.treasuryVault.balance)
340 }
341
342 access(all) fun hasAssets(owner: Address): Bool {
343 return self.stakedNFTAssets[owner] != nil
344 }
345
346
347 access(all) fun createAssetsDictionary(owner: Address) {
348 // Overwrite any existing dictionary by forcefully setting a new empty one
349 self.stakedNFTAssets[owner] <-! {} as @{UInt64: {NonFungibleToken.NFT}}
350 }
351
352
353
354}
355