Smart Contract
NFTLocker
A.b6f2481eba4df97b.NFTLocker
1import NonFungibleToken from 0x1d7e57aa55817448
2
3
4/// A contract to lock NFT for a given duration
5/// Locked NFT are stored in a user owned collection
6/// The collection owner can unlock the NFT after duration has been exceeded
7///
8access(all) contract NFTLocker {
9
10 /// Contract events
11 ///
12 access(all) event Withdraw(id: UInt64, from: Address?)
13 access(all) event Deposit(id: UInt64, to: Address?)
14 access(all) event NFTLocked(
15 id: UInt64,
16 to: Address?,
17 lockedAt: UInt64,
18 lockedUntil: UInt64,
19 duration: UInt64,
20 nftType: Type
21 )
22 access(all) event NFTUnlocked(
23 id: UInt64,
24 from: Address?,
25 nftType: Type,
26 receiverName: String?, // receiver name if unlocked with authorized deposit, nil otherwise
27 lockedUntilBeforeEarlyUnlock: UInt64? // lockedUntil if unlocked with authorized deposit, nil otherwise
28 )
29 access(all) event ReceiverAdded(name: String, eligibleNFTTypes: {Type: Bool})
30 access(all) event ReceiverRemoved(name: String, eligibleNFTTypes: {Type: Bool})
31
32 /// Named Paths
33 ///
34 access(all) let CollectionStoragePath: StoragePath
35 access(all) let CollectionPublicPath: PublicPath
36
37 /// Contract variables
38 ///
39 access(all) var totalLockedTokens: UInt64
40
41 /// Metadata Dictionaries
42 ///
43 access(self) let lockedTokens: {Type: {UInt64: LockedData}}
44
45 /// Entitlement that grants the ability to operate authorized functions
46 ///
47 access(all) entitlement Operate
48
49 /// Data describing characteristics of the locked NFT
50 ///
51 access(all) struct LockedData {
52 access(all) let id: UInt64
53 access(all) let owner: Address
54 access(all) let lockedAt: UInt64
55 access(all) let lockedUntil: UInt64
56 access(all) let duration: UInt64
57 access(all) let nftType: Type
58 access(all) let extension: {String: AnyStruct}
59
60 view init (id: UInt64, owner: Address, duration: UInt64, nftType: Type) {
61 if let lockedToken = (NFTLocker.lockedTokens[nftType]!)[id] {
62 self.id = id
63 self.owner = lockedToken.owner
64 self.lockedAt = lockedToken.lockedAt
65 self.lockedUntil = lockedToken.lockedUntil
66 self.duration = lockedToken.duration
67 self.nftType = lockedToken.nftType
68 self.extension = lockedToken.extension
69 } else {
70 self.id = id
71 self.owner = owner
72 self.lockedAt = UInt64(getCurrentBlock().timestamp)
73 self.lockedUntil = self.lockedAt + duration
74 self.duration = duration
75 self.nftType = nftType
76 self.extension = {}
77 }
78 }
79 }
80
81 /// Get the details of a locked NFT
82 ///
83 access(all) view fun getNFTLockerDetails(id: UInt64, nftType: Type): NFTLocker.LockedData? {
84 return (NFTLocker.lockedTokens[nftType]!)[id]
85 }
86
87 /// Determine if NFT can be unlocked
88 ///
89 access(all) view fun canUnlockToken(id: UInt64, nftType: Type): Bool {
90 if let lockedTokens = &NFTLocker.lockedTokens[nftType] as &{UInt64: NFTLocker.LockedData}? {
91 if let lockedToken = lockedTokens[id] {
92 if lockedToken.lockedUntil <= UInt64(getCurrentBlock().timestamp) {
93 return true
94 }
95 }
96 }
97 return false
98 }
99
100 /// The path to the Admin resource belonging to the account where this contract is deployed
101 ///
102 access(all) view fun GetAdminStoragePath(): StoragePath {
103 return /storage/NFTLockerAdmin
104 }
105
106 /// The path to the ReceiverCollector resource belonging to the account where this contract is deployed
107 ///
108 access(all) view fun getReceiverCollectorStoragePath(): StoragePath {
109 return /storage/NFTLockerAdminReceiverCollector
110 }
111
112 /// Return an unauthorized reference to the admin's ReceiverCollector resource if it exists
113 ///
114 access(all) view fun borrowAdminReceiverCollectorPublic(): &ReceiverCollector? {
115 return self.account.storage.borrow<&ReceiverCollector>(from: NFTLocker.getReceiverCollectorStoragePath())
116 }
117
118 /// Interface for depositing NFTs to authorized receivers
119 ///
120 access(all) struct interface IAuthorizedDepositHandler {
121 access(all) fun deposit(nft: @{NonFungibleToken.NFT}, ownerAddress: Address, passThruParams: {String: AnyStruct})
122 }
123
124 /// Struct that defines a Receiver
125 ///
126 /// Receivers are entities that can receive locked NFTs and deposit them using a specific deposit method
127 ///
128 access(all) struct Receiver {
129 /// Handler for depositing NFTs for the receiver
130 ///
131 access(all) var authorizedDepositHandler: {IAuthorizedDepositHandler}
132
133 /// The eligible NFT types for the receiver
134 ///
135 access(all) let eligibleNFTTypes: {Type: Bool}
136
137 /// Extension map for additional data
138 ///
139 access(all) let metadata: {String: AnyStruct}
140
141 /// Initialize Receiver struct
142 ///
143 view init(
144 authorizedDepositHandler: {IAuthorizedDepositHandler},
145 eligibleNFTTypes: {Type: Bool}
146 ) {
147 self.authorizedDepositHandler = authorizedDepositHandler
148 self.eligibleNFTTypes = eligibleNFTTypes
149 self.metadata = {}
150 }
151 }
152
153 /// ReceiverCollector resource
154 ///
155 /// Note: This resource is used to store receivers and corresponding authorized deposit handlers; currently,
156 /// only the admin account can add or remove receivers - in the future, a ReceiverProvider resource could
157 /// be added to provide this capability to separate authorized accounts.
158 ///
159 access(all) resource ReceiverCollector {
160 /// Map of receivers by name
161 ///
162 access(self) let receiversByName: {String: Receiver}
163
164 /// Map of receiver names by NFT type for lookup
165 ///
166 access(self) let receiverNamesByNFTType: {Type: {String: Bool}}
167
168 /// Extension map for additional data
169 ///
170 access(self) let metadata: {String: AnyStruct}
171
172 /// Add a deposit handler for given NFT types
173 ///
174 access(Operate) fun addReceiver(
175 name: String,
176 authorizedDepositHandler: {IAuthorizedDepositHandler},
177 eligibleNFTTypes: {Type: Bool}
178 ) {
179 pre {
180 !self.receiversByName.containsKey(name): "Receiver with the same name already exists"
181 }
182
183 // Add the receiver
184 self.receiversByName[name] = Receiver(
185 authorizedDepositHandler: authorizedDepositHandler,
186 eligibleNFTTypes: eligibleNFTTypes
187 )
188
189 // Add the receiver to the lookup map
190 for nftType in eligibleNFTTypes.keys {
191 if let namesMap = self.receiverNamesByNFTType[nftType] {
192 namesMap[name] = true
193 self.receiverNamesByNFTType[nftType] = namesMap
194 } else {
195 self.receiverNamesByNFTType[nftType] = {name: true}
196 }
197 }
198
199 // Emit event
200 emit ReceiverAdded(name: name, eligibleNFTTypes: eligibleNFTTypes)
201 }
202
203 /// Remove a deposit method for a given NFT type
204 ///
205 access(Operate) fun removeReceiver(name: String) {
206 // Get the receiver
207 let receiver = self.receiversByName[name]
208 ?? panic("Receiver with the given name does not exist")
209
210 // Remove the receiver from the lookup map
211 for nftType in receiver.eligibleNFTTypes.keys {
212 if self.receiverNamesByNFTType.containsKey(nftType) {
213 self.receiverNamesByNFTType[nftType]!.remove(key: name)
214 }
215 }
216
217 // Remove the receiver
218 self.receiversByName.remove(key: name)
219
220 // Emit event
221 emit ReceiverRemoved(name: name, eligibleNFTTypes: receiver.eligibleNFTTypes)
222 }
223
224 /// Get the receiver for the given name if it exists
225 ///
226 access(all) view fun getReceiver(name: String): Receiver? {
227 return self.receiversByName[name]
228 }
229
230 /// Get the receiver names for the given NFT type if it exists
231 ///
232 access(all) view fun getReceiverNamesByNFTType(nftType: Type): {String: Bool}? {
233 return self.receiverNamesByNFTType[nftType]
234 }
235
236 /// Initialize ReceiverCollector struct
237 ///
238 view init() {
239 self.receiversByName = {}
240 self.receiverNamesByNFTType = {}
241 self.metadata = {}
242 }
243 }
244
245 /// Admin resource
246 ///
247 access(all) resource Admin {
248 /// Expire lock
249 ///
250 access(all) fun expireLock(id: UInt64, nftType: Type) {
251 NFTLocker.expireLock(id: id, nftType: nftType)
252 }
253
254 /// Create and return a ReceiverCollector resource
255 ///
256 access(all) fun createReceiverCollector(): @ReceiverCollector {
257 return <- create ReceiverCollector()
258 }
259 }
260
261 /// Expire lock
262 ///
263 /// This can be called either by the admin or by the user unlockWithAuthorizedDeposit, if the locked NFT
264 /// type is eligible.
265 ///
266 access(contract) fun expireLock(id: UInt64, nftType: Type) {
267 if let locker = &NFTLocker.lockedTokens[nftType] as auth(Mutate) &{UInt64: NFTLocker.LockedData}?{
268 if locker[id] != nil {
269 // Update locked data's duration to 0
270 if let oldLockedData = locker.remove(key: id){
271 locker.insert(
272 key: id,
273 LockedData(
274 id: id,
275 owner: oldLockedData.owner,
276 duration: 0,
277 nftType: nftType
278 )
279 )
280 }
281 }
282 }
283 }
284
285
286 /// A public collection interface that requires the ability to lock and unlock NFTs and return the ids
287 /// of NFTs locked for a given type
288 ///
289 access(all) resource interface LockedCollection {
290 access(all) view fun getIDs(nftType: Type): [UInt64]?
291 access(Operate) fun lock(token: @{NonFungibleToken.NFT}, duration: UInt64)
292 access(Operate) fun unlock(id: UInt64, nftType: Type): @{NonFungibleToken.NFT}
293 access(Operate) fun unlockWithAuthorizedDeposit(
294 id: UInt64,
295 nftType: Type,
296 receiverName: String,
297 passThruParams: {String: AnyStruct}
298 )
299 }
300
301 /// Deprecated in favor of Operate entitlement
302 ///
303 access(all) resource interface LockProvider: LockedCollection {}
304
305 /// An NFT Collection
306 ///
307 access(all) resource Collection: LockedCollection, LockProvider {
308 /// This collection's locked NFTs
309 ///
310 access(all) var lockedNFTs: @{Type: {UInt64: {NonFungibleToken.NFT}}}
311
312 /// Unlock an NFT of a given type
313 ///
314 access(Operate) fun unlock(id: UInt64, nftType: Type): @{NonFungibleToken.NFT} {
315 pre {
316 NFTLocker.canUnlockToken(id: id, nftType: nftType): "locked duration has not been met"
317 }
318
319 return <- self.withdrawFromLockedNFTs(id: id, nftType: nftType, receiverName: nil, lockedUntilBeforeEarlyUnlock: nil)
320 }
321
322 /// Force unlock the NFT with the given id and type, and deposit it using the receiver's deposit method;
323 /// additional function parameters may be required by the receiver's deposit method and are passed in the
324 /// passThruParams map.
325 ///
326 access(Operate) fun unlockWithAuthorizedDeposit(
327 id: UInt64,
328 nftType: Type,
329 receiverName: String,
330 passThruParams: {String: AnyStruct}
331 ) {
332 pre {
333 !NFTLocker.canUnlockToken(id: id, nftType: nftType): "locked duration has been met, use unlock() instead"
334 }
335
336 // Get the locked token details, panic if it doesn't exist
337 let lockedTokenDetails = NFTLocker.getNFTLockerDetails(id: id, nftType: nftType)
338 ?? panic("No locked token found for the given id and NFT type")
339
340 // Get a public reference to the admin's receiver collector, panic if it doesn't exist
341 let receiverCollector = NFTLocker.borrowAdminReceiverCollectorPublic()
342 ?? panic("No receiver collector found")
343
344 // Get the receiver names for the given NFT type, panic if there is no record
345 let nftTypeReceivers = receiverCollector.getReceiverNamesByNFTType(nftType: nftType)
346 ?? panic("No authorized receiver for the given NFT type")
347
348 // Verify that the receiver with the given name is authorized
349 assert(
350 nftTypeReceivers[receiverName] == true,
351 message: "Provided receiver does not exist or is not authorized for the given NFT type"
352 )
353
354 // Expire the NFT's lock
355 NFTLocker.expireLock(id: id, nftType: nftType)
356
357 // Unlock and deposit the NFT using the receiver's deposit method
358 receiverCollector.getReceiver(name: receiverName)!.authorizedDepositHandler.deposit(
359 nft: <- self.withdrawFromLockedNFTs(
360 id: id,
361 nftType: nftType,
362 receiverName: receiverName,
363 lockedUntilBeforeEarlyUnlock: lockedTokenDetails.lockedUntil
364 ),
365 ownerAddress: lockedTokenDetails.owner,
366 passThruParams: passThruParams,
367 )
368 }
369
370 /// Withdraw the NFT with the given id and type, used in the unlock and unlockWithAuthorizedDeposit functions
371 ///
372 access(self) fun withdrawFromLockedNFTs(id: UInt64, nftType: Type, receiverName: String?, lockedUntilBeforeEarlyUnlock: UInt64?): @{NonFungibleToken.NFT} {
373 // Remove the token's locked data
374 if let lockedTokens = &NFTLocker.lockedTokens[nftType] as auth(Remove) &{UInt64: NFTLocker.LockedData}? {
375 lockedTokens.remove(key: id)
376 }
377
378 // Decrement the locked tokens count
379 NFTLocker.totalLockedTokens = NFTLocker.totalLockedTokens - 1
380
381 // Emit events
382 emit NFTUnlocked(
383 id: id,
384 from: self.owner?.address,
385 nftType: nftType,
386 receiverName: receiverName,
387 lockedUntilBeforeEarlyUnlock: lockedUntilBeforeEarlyUnlock
388 )
389 emit Withdraw(id: id, from: self.owner?.address)
390
391 return <- self.lockedNFTs[nftType]?.remove(key: id)!!
392 }
393
394 /// Lock the given NFT for the specified duration
395 ///
396 access(Operate) fun lock(token: @{NonFungibleToken.NFT}, duration: UInt64) {
397 // Get the NFT's id and type
398 let nftId: UInt64 = token.id
399 let nftType: Type = token.getType()
400
401 // Initialize the collection's locked NFTs for the given type if it doesn't exist
402 if self.lockedNFTs[nftType] == nil {
403 self.lockedNFTs[nftType] <-! {}
404 }
405
406 // Initialize the contract's locked tokens data for the given type if it doesn't exist
407 if NFTLocker.lockedTokens[nftType] == nil {
408 NFTLocker.lockedTokens[nftType] = {}
409 }
410
411 // Get a reference to this collection's locked NFTs map
412 let collectionLockedNFTsRef = &self.lockedNFTs[nftType] as auth(Insert) &{UInt64: {NonFungibleToken.NFT}}?
413
414 // Deposit the provided NFT in this collection's locked NFTs map - Cadence design requires destroying the resource-typed return value
415 destroy <- collectionLockedNFTsRef!.insert(key: nftId, <- token)
416
417 // Get a reference to the contract's nested map containing locked tokens data
418 let lockedTokensDataRef = &NFTLocker.lockedTokens[nftType] as auth(Insert) &{UInt64: NFTLocker.LockedData}?
419 ?? panic("Could not get a reference to the locked tokens data")
420
421 // Create locked data
422 let lockedData = NFTLocker.LockedData(
423 id: nftId,
424 owner: self.owner!.address,
425 duration: duration,
426 nftType: nftType
427 )
428
429 // Insert the locked data
430 lockedTokensDataRef.insert(key: nftId, lockedData)
431
432 // Increment the total locked tokens
433 NFTLocker.totalLockedTokens = NFTLocker.totalLockedTokens + 1
434
435 // Emit events
436 emit NFTLocked(
437 id: nftId,
438 to: self.owner?.address,
439 lockedAt: lockedData.lockedAt,
440 lockedUntil: lockedData.lockedUntil,
441 duration: lockedData.duration,
442 nftType: nftType
443 )
444
445 emit Deposit(id: nftId, to: self.owner?.address)
446 }
447
448 /// Get the ids of NFTs locked for a given type
449 ///
450 access(all) view fun getIDs(nftType: Type): [UInt64]? {
451 return self.lockedNFTs[nftType]?.keys
452 }
453
454 /// Initialize Collection resource
455 ///
456 view init() {
457 self.lockedNFTs <- {}
458 }
459 }
460
461 /// Create and return an empty collection
462 ///
463 access(all) fun createEmptyCollection(): @Collection {
464 return <- create Collection()
465 }
466
467 /// Contract initializer
468 ///
469 init() {
470 // Set paths
471 self.CollectionStoragePath = /storage/NFTLockerCollection
472 self.CollectionPublicPath = /public/NFTLockerCollection
473
474 // Create and save the admin resource
475 self.account.storage.save(<- create Admin(), to: NFTLocker.GetAdminStoragePath())
476
477 // Set contract variables
478 self.totalLockedTokens = 0
479 self.lockedTokens = {}
480 }
481}