Smart Contract

NameVoucher

A.097bafa4e0b48eef.NameVoucher

Deployed

2d ago
Feb 26, 2026, 03:12:51 AM UTC

Dependents

1 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import NonFungibleToken from 0x1d7e57aa55817448
4import MetadataViews from 0x1d7e57aa55817448
5import ViewResolver from 0x1d7e57aa55817448
6import FungibleTokenSwitchboard from 0xf233dcee88fe0abe
7import Profile from 0x097bafa4e0b48eef
8import FIND from 0x097bafa4e0b48eef
9import FindViews from 0x097bafa4e0b48eef
10import FindAirdropper from 0x097bafa4e0b48eef
11
12access(all) contract NameVoucher :NonFungibleToken{
13
14    access(all) var totalSupply: UInt64
15
16    access(all) event ContractInitialized()
17    access(all) event Withdraw(id: UInt64, from: Address?)
18    access(all) event Deposit(id: UInt64, to: Address?)
19    access(all) event Minted(id:UInt64, address:Address, minCharLength: UInt64)
20
21    access(all) event Destroyed(id: UInt64, address: Address?, minCharLength: UInt64)
22    access(all) event Redeemed(id: UInt64, address: Address?, minCharLength: UInt64, findName: String, action: String)
23
24    access(all) let CollectionStoragePath: StoragePath
25    access(all) let CollectionPublicPath: PublicPath
26
27    access(all) var royalties : [MetadataViews.Royalty]
28    access(all) var thumbnail : {MetadataViews.File}
29
30    access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver {
31        access(all) let id:UInt64
32        access(all) var nounce:UInt64
33        // 3 characters voucher should be able to claim name with at LEAST 3 char and so on
34        access(all) let minCharLength:UInt64
35
36        init(
37            minCharLength: UInt64
38        ) {
39            self.nounce=0
40            self.minCharLength=minCharLength
41            self.id=self.uuid
42        }
43
44        access(all) view fun getViews(): [Type] {
45            return  [
46            Type<MetadataViews.Display>(),
47            Type<MetadataViews.Royalties>(),
48            Type<MetadataViews.ExternalURL>(),
49            Type<MetadataViews.NFTCollectionData>(),
50            Type<MetadataViews.NFTCollectionDisplay>(),
51            Type<MetadataViews.Traits>()
52            ]
53        }
54
55        access(all) view fun getID() : UInt64 {
56            return self.id
57        }
58
59        access(all) fun resolveView(_ view: Type): AnyStruct? {
60
61            var imageFile=NameVoucher.thumbnail
62
63            switch self.minCharLength {
64            case 3 :
65                imageFile = MetadataViews.IPFSFile(cid: "QmYMtXfFcgpJgm3Mhy68r6cuHTCMMcucVUpYTVeSRTWLTh", path: nil)
66
67            case 4 :
68                imageFile = MetadataViews.IPFSFile(cid: "QmWpQRvGudYrkZw6rKKTrkghkYKs4wt3KQGzxcXJ8JmuSc", path: nil)
69            }
70
71            let name= self.minCharLength.toString().concat("-characters .find name voucher")
72            let description ="This voucher entitles the holder to claim or extend any available or owned .find name with ".concat(self.minCharLength.toString()).concat(" characters or more. It is valid for one-time use only and will be voided after the successful registration or extension of a .find name.\n\nIf you received this voucher via airdrop, check your inbox to claim it. Once claimed, it will be added to your collection. To use the voucher, follow these steps:\nLog in to your account.\nNavigate to the Collection page and locate the voucher you wish to use.\nClick the “Use Voucher” button and follow the on-screen instructions to register a new .find name or extend an existing one.\nUpon successful completion, the voucher will be invalidated, and the chosen .find name will be registered or extended under your account.")
73
74            switch view {
75            case Type<MetadataViews.Display>():
76                return MetadataViews.Display(
77                    name: name,
78                    description: description,
79                    thumbnail: imageFile
80                )
81
82            case Type<MetadataViews.ExternalURL>():
83                return MetadataViews.ExternalURL("https://find.xyz/".concat(self.owner!.address.toString()).concat("/collection/nameVoucher/").concat(self.id.toString()))
84
85            case Type<MetadataViews.Royalties>():
86                return MetadataViews.Royalties(NameVoucher.royalties)
87
88            case Type<MetadataViews.NFTCollectionDisplay>():
89                let externalURL = MetadataViews.ExternalURL("https://find.xyz/")
90                let squareImage = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://pbs.twimg.com/profile_images/1467546091780550658/R1uc6dcq_400x400.jpg"), mediaType: "image")
91                let bannerImage = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://pbs.twimg.com/profile_banners/1448245049666510848/1674733461/1500x500"), mediaType: "image")
92                let desc = "Name Vouchers can be used to claim or extend any available .find name of 3-characters or more, depending on voucher rarity. Vouchers can be used only once and will be destroyed after use. Enjoy!"
93                return MetadataViews.NFTCollectionDisplay(
94                    name: "NameVoucher",
95                    description: desc,
96                    externalURL: externalURL,
97                    squareImage: squareImage,
98                    bannerImage: bannerImage,
99                    socials: {
100                        "discord": MetadataViews.ExternalURL("https://discord.gg/findonflow"),
101                        "twitter" : MetadataViews.ExternalURL("https://twitter.com/findonflow")
102                    }
103                )
104
105            case Type<MetadataViews.NFTCollectionData>():
106                return NameVoucher.resolveContractView(resourceType: Type<@NameVoucher.NFT>(), viewType: Type<MetadataViews.NFTCollectionData>())
107            case Type<MetadataViews.Traits>():
108                return MetadataViews.Traits([
109                MetadataViews.Trait(
110                    name: "Minimum number of characters",
111                    value: self.minCharLength,
112                    displayType: "number",
113                    rarity: nil
114                )
115                ])
116            }
117            return nil
118        }
119
120        access(contract) fun increaseNounce() {
121            self.nounce=self.nounce+1
122        }
123
124        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
125            return <-NameVoucher.createEmptyCollection(nftType:Type<@NameVoucher.NFT>())
126        }
127    }
128
129    access(all) view fun getContractViews(resourceType: Type?): [Type] {
130        return [
131        Type<MetadataViews.NFTCollectionData>(),
132        Type<MetadataViews.NFTCollectionDisplay>()
133        ]
134    }
135
136    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
137        switch viewType {
138        case Type<MetadataViews.NFTCollectionData>():
139            let collectionRef = self.account.storage.borrow<&NameVoucher.Collection>(
140                from: NameVoucher.CollectionStoragePath
141            ) ?? panic("Could not borrow a reference to the stored collection")
142            let collectionData = MetadataViews.NFTCollectionData(
143                storagePath: NameVoucher.CollectionStoragePath,
144                publicPath: NameVoucher.CollectionPublicPath,
145                publicCollection: Type<&NameVoucher.Collection>(),
146                publicLinkedType: Type<&NameVoucher.Collection>(),
147                createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
148                    return <-NameVoucher.createEmptyCollection(nftType:Type<@NameVoucher.NFT>())
149                })
150            )
151            return collectionData
152        }
153        return nil
154    }
155
156    access(all) entitlement Owner
157
158    access(all) resource Collection: NonFungibleToken.Collection, ViewResolver.ResolverCollection {
159        // dictionary of NFT conforming tokens
160        // NFT is a resource type with an `UInt64` ID field
161        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
162
163        init () {
164            self.ownedNFTs <- {}
165        }
166
167        // withdraw removes an NFT from the collection and moves it to the caller
168        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
169            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
170
171            emit Withdraw(id: token.id, from: self.owner?.address)
172
173            return <-token
174        }
175
176        // deposit takes a NFT and adds it to the collections dictionary
177        // and adds the ID to the id array
178        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
179            let token <- token as! @NFT
180
181            let id: UInt64 = token.id
182
183            token.increaseNounce()
184            // add the new token to the dictionary which removes the old one
185            let oldToken <- self.ownedNFTs[id] <- token
186
187            emit Deposit(id: id, to: self.owner?.address)
188
189
190            destroy oldToken
191        }
192        // getIDs returns an array of the IDs that are in the collection
193        access(all) view fun getIDs(): [UInt64] {
194            return self.ownedNFTs.keys
195        }
196
197        access(all) fun contains(_ id: UInt64) : Bool {
198            return self.ownedNFTs.containsKey(id)
199        }
200
201        // borrowNFT gets a reference to an NFT in the collection
202        // so that the caller can read its metadata and call its methods
203        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
204            return &self.ownedNFTs[id] 
205        }
206
207
208        /// Borrow the view resolver for the specified NFT ID
209        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
210            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
211                return nft as &{ViewResolver.Resolver}
212            }
213            return nil
214        }
215
216        access(Owner) fun redeem(id: UInt64, name: String) {
217            let nft <- self.ownedNFTs.remove(key: id) ?? panic("Cannot find voucher with ID ".concat(id.toString()))
218            let typedNFT <- nft as! @NameVoucher.NFT
219            let nameLength = UInt64(name.length)
220            let minLength = typedNFT.minCharLength
221
222            // Assert that the name voucher is valid for claiming name with this length
223            assert(nameLength >= minLength, message: "You are trying to register a ".concat(nameLength.toString()).concat(" character name, but the voucher can only support names with minimun character of ").concat(minLength.toString()))
224            destroy typedNFT
225
226            // get All the paths here for registration
227            let network = NameVoucher.account.storage.borrow<&FIND.Network>(from: FIND.NetworkStoragePath) ?? panic("Cannot borrow find network for registration")
228            let status = FIND.status(name)
229
230            // If the lease is free, we register it
231            if status.status == FIND.LeaseStatus.FREE {
232                let profile = self.owner!.capabilities.get<&{Profile.Public}>(Profile.publicPath)!
233                let lease = self.owner!.capabilities.get<&{FIND.LeaseCollectionPublic}>(FIND.LeasePublicPath)!
234                network.internal_register(name: name, profile: profile,  leases: lease)
235                emit Redeemed(id: id, address: self.owner?.address, minCharLength: minLength, findName: name, action: "register")
236                return
237            }
238
239            // If the lease is already taken / locked, we check if that's under the name of the voucher owner, then extend it
240            if status.owner != nil && status.owner! == self.owner!.address {
241                network.internal_renew(name: name)
242                emit Redeemed(id: id, address: self.owner?.address, minCharLength: minLength, findName: name, action: "renew")
243                return
244            }
245
246            panic("Name is already taken by others ".concat(status.owner!.toString()))
247        }
248
249        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
250            let supportedTypes: {Type: Bool} = {}
251            return supportedTypes
252        }
253
254        /// Returns whether or not the given type is accepted by the collection
255        access(all) view fun isSupportedNFTType(type: Type): Bool {
256            return type == Type<@NameVoucher.NFT>()
257        }
258
259        /// Gets the amount of NFTs stored in the collection
260        access(all) view fun getLength(): Int {
261            return self.ownedNFTs.length
262        }
263
264        /// getIDsWithTypes returns a list of IDs that are in the collection, keyed by type
265        /// Should only be used by collections that can store multiple NFT types
266        access(all) view fun getIDsWithTypes(): {Type: [UInt64]} {
267            let ids: {Type: [UInt64]} = {}
268            return ids
269        }
270
271        // public function that anyone can call to create a new empty collection
272        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
273            return <- NameVoucher.createEmptyCollection(nftType:Type<@NameVoucher.NFT>())
274        }
275
276        access(all) view fun getDefaultStoragePath() : StoragePath {
277            return NameVoucher.CollectionStoragePath
278        }
279
280        access(all) view fun getDefaultPublicPath() : PublicPath {
281            return NameVoucher.CollectionPublicPath
282        }
283
284    }
285
286    // Internal mint NFT is used inside the contract as a helper function
287    // It DOES NOT emit events so the admin function calling this should emit that
288    access(account) fun mintNFT(
289        recipient: &{NonFungibleToken.Receiver},
290        minCharLength: UInt64
291    ) : UInt64 {
292        pre {
293            recipient.owner != nil : "Recipients NFT collection is not owned"
294        }
295
296        NameVoucher.totalSupply = NameVoucher.totalSupply + 1
297        // create a new NFT
298        var newNFT <- create NFT(
299            minCharLength: minCharLength
300        )
301
302        let id = newNFT.id
303        recipient.deposit(token: <-newNFT)
304        emit Minted(id: id, address: recipient.owner!.address, minCharLength: minCharLength)
305        return id
306    }
307
308    access(account) fun setRoyaltycut(_ cutInfo: [MetadataViews.Royalty]) {
309        NameVoucher.royalties = cutInfo
310    }
311
312    // public function that anyone can call to create a new empty collection
313    access(all) fun createEmptyCollection(nftType:Type): @{NonFungibleToken.Collection} {
314        return <- create Collection()
315    }
316
317    init() {
318        // Initialize the total supply
319        self.totalSupply = 0
320
321        // Check if the account already has a Switchboard resource
322        if self.account.storage.borrow<&FungibleTokenSwitchboard.Switchboard>(from: FungibleTokenSwitchboard.StoragePath) == nil {
323            // Create a new Switchboard resource and put it into storage
324            self.account.storage.save(
325                <- FungibleTokenSwitchboard.createSwitchboard(),
326                to: FungibleTokenSwitchboard.StoragePath
327            )
328
329            // Clear existing Capabilities at canonical paths
330            self.account.capabilities.unpublish(FungibleTokenSwitchboard.ReceiverPublicPath)
331            self.account.capabilities.unpublish(FungibleTokenSwitchboard.PublicPath)
332
333            // Create a public capability to the Switchboard exposing the deposit
334            // function through the {FungibleToken.Receiver} interface
335            let receiverCap = self.account.capabilities.storage.issue<&{FungibleToken.Receiver}>(
336                FungibleTokenSwitchboard.StoragePath
337            )
338            self.account.capabilities.publish(receiverCap, at: FungibleTokenSwitchboard.ReceiverPublicPath)
339
340            // Create a public capability to the Switchboard exposing both the
341            // {FungibleTokenSwitchboard.SwitchboardPublic} and the 
342            // {FungibleToken.Receiver} interfaces
343            let switchboardPublicCap = self.account.capabilities.storage.issue<&{FungibleTokenSwitchboard.SwitchboardPublic, FungibleToken.Receiver}>(
344                FungibleTokenSwitchboard.StoragePath
345            )
346            self.account.capabilities.publish(switchboardPublicCap, at: FungibleTokenSwitchboard.PublicPath)
347        }
348
349        // Set Royalty cuts in a transaction
350
351        self.royalties = [
352        MetadataViews.Royalty(
353            receiver: NameVoucher.account.capabilities.get<&{FungibleToken.Receiver}>(FungibleTokenSwitchboard.ReceiverPublicPath)!	,
354            cut: 0.025,
355            description: "network"
356        )
357        ]
358
359        // 5 - letter Thumbnail
360        self.thumbnail = MetadataViews.IPFSFile(cid: "QmWj3bwRfksGXvFQYoWtjdycD68cp4xRGMJonnDibsN6Rz", path: nil)
361
362        // Set the named paths
363        self.CollectionStoragePath = /storage/nameVoucher
364        self.CollectionPublicPath = /public/nameVoucher
365
366        self.account.storage.save<@{NonFungibleToken.Collection}>(<- NameVoucher.createEmptyCollection(nftType:Type<@NameVoucher.NFT>()) , to: NameVoucher.CollectionStoragePath)
367        let collectionCap = self.account.capabilities.storage.issue<&NameVoucher.Collection>(NameVoucher.CollectionStoragePath)
368        self.account.capabilities.publish(collectionCap, at: self.CollectionPublicPath)
369
370        emit ContractInitialized()
371    }
372}
373