Smart Contract
NameVoucher
A.097bafa4e0b48eef.NameVoucher
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