Smart Contract

NFTLocker

A.b6f2481eba4df97b.NFTLocker

Valid From

86,001,982

Deployed

2w ago
Feb 11, 2026, 05:26:40 PM UTC

Dependents

156359 imports
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}