Smart Contract
OrdinalVendor
A.9212a87501a8a6a2.OrdinalVendor
1/*
2 OrdinalVendor.cdc
3
4 Author: Brian Min brian@flowverse.co
5*/
6
7import NonFungibleToken from 0x1d7e57aa55817448
8import FungibleToken from 0xf233dcee88fe0abe
9import FlowversePass from 0x9212a87501a8a6a2
10import FlowverseSocks from 0xce4c02539d1fabe8
11import FlowverseShirt from 0x9212a87501a8a6a2
12import Crypto
13
14access(all) contract OrdinalVendor {
15 // Entitlements
16 access(all) entitlement VendorAdmin
17
18 access(all) let AdminStoragePath: StoragePath
19
20 access(contract) var vendor: @Vendor?
21
22 // Mapping of domains to inscription numbers
23 access(contract) var domains: {String: UInt64}
24
25 // Inscription numbers for text and image type ordinals
26 access(contract) var textIDs: [UInt64]
27 access(contract) var imageIDs: [UInt64]
28
29 // Restricted inscription numbers - flagged as inappropiate
30 access(contract) var restrictedIDs: {UInt64: Bool}
31
32 access(all) event OrdinalPurchased(id: UInt64, type: String, purchaserAddress: Address, price: UFix64, salePaymentVaultType: String)
33 access(all) event OrdinalPurchasedV2(id: UInt64, type: String, size: UInt64, purchaserAddress: Address, price: UFix64, salePaymentVaultType: String)
34 access(all) event OrdinalRestricted(id: UInt64)
35
36 access(all) resource interface IMinter {
37 access(all) fun mint(creator: Address, type: String, data: String): @{NonFungibleToken.NFT}
38 }
39
40 access(all) struct PurchaseData {
41 access(all) let type: String
42 access(all) let inscriptionData: String
43 access(all) let purchaserAddress: Address
44 access(all) let purchaserCollectionRef: &{NonFungibleToken.Receiver}
45
46 init(type: String, inscriptionData: String, purchaserAddress: Address, purchaserCollectionRef: &{NonFungibleToken.Receiver}){
47 self.type = type
48 self.inscriptionData = inscriptionData
49 self.purchaserAddress = purchaserAddress
50 self.purchaserCollectionRef = purchaserCollectionRef
51 }
52 }
53
54 access(all) struct PriceData {
55 access(all) let price: {String: UFix64}
56 access(all) let type: String
57
58 init(price: {String: UFix64}, type: String){
59 self.price = price
60 self.type = type
61 }
62 }
63
64 access(all) struct AdminSignedPayload {
65 access(all) let type: String
66 access(all) let creator: Address
67 access(all) let expiration: UInt64
68
69 init(type: String, creator: Address, expiration: UInt64){
70 self.type = type
71 self.creator = creator
72 self.expiration = expiration
73 }
74
75 access(all) view fun toString(): String {
76 return self.type.concat("-")
77 .concat(self.creator.toString()).concat("-")
78 .concat(self.expiration.toString())
79 }
80 }
81
82 access(all) resource interface VendorPublic {
83 access(all) fun purchaseDomain(
84 payment: @{FungibleToken.Vault},
85 data: PurchaseData,
86 adminSignedPayload: AdminSignedPayload,
87 signature: String
88 )
89 access(all) fun purchaseText(
90 payment: @{FungibleToken.Vault},
91 data: PurchaseData,
92 adminSignedPayload: AdminSignedPayload,
93 signature: String
94 )
95 access(all) fun purchaseImage(
96 payment: @{FungibleToken.Vault},
97 data: PurchaseData,
98 adminSignedPayload: AdminSignedPayload,
99 signature: String
100 )
101 access(all) view fun getPrices(): {String: PriceData}
102 access(all) fun getPaymentReceivers(): {String: Address}
103 }
104
105 access(all) resource Vendor: VendorPublic {
106 access(self) let minterCap: Capability<&{IMinter}>
107 access(self) var prices: {String: PriceData}
108 access(self) var paymentReceiverCaps: {String: Capability<&{FungibleToken.Receiver}>}
109
110 init(
111 minterCap: Capability<&{IMinter}>,
112 prices: {String: PriceData},
113 paymentReceiverCaps: {String: Capability<&{FungibleToken.Receiver}>}
114 ) {
115 self.minterCap = minterCap
116 self.prices = prices
117 self.paymentReceiverCaps = paymentReceiverCaps
118 }
119
120 access(VendorAdmin) fun setPrice(priceData: PriceData) {
121 self.prices[priceData.type] = priceData
122 }
123
124 access(all) view fun getPrices(): {String: PriceData} {
125 return self.prices
126 }
127
128 access(VendorAdmin) fun setPaymentReceiver(salePaymentVaultType: String, paymentReceiverCap: Capability<&{FungibleToken.Receiver}>) {
129 pre {
130 paymentReceiverCap.borrow() != nil: "Could not borrow payment receiver capability"
131 }
132 self.paymentReceiverCaps[salePaymentVaultType] = paymentReceiverCap
133 }
134
135 access(all) fun getPaymentReceivers(): {String: Address} {
136 let paymentReceivers: {String: Address} = {}
137 for salePaymentVaultType in self.paymentReceiverCaps.keys {
138 let receiver = self.paymentReceiverCaps[salePaymentVaultType]!.borrow()!
139 if receiver.owner != nil {
140 paymentReceivers[salePaymentVaultType] = receiver.owner!.address
141 }
142 }
143 return paymentReceivers
144 }
145
146 access(all) view fun getPaymentReceiverAddress(salePaymentVaultType: String): Address? {
147 assert(self.paymentReceiverCaps.containsKey(salePaymentVaultType), message: "payment receiver does not exist for vault type: ".concat(salePaymentVaultType))
148 let receiver = self.paymentReceiverCaps[salePaymentVaultType]!.borrow()!
149 if receiver.owner != nil {
150 return receiver.owner!.address
151 }
152 return nil
153 }
154
155 access(self) fun verifyAdminSignedPayload(signedPayloadData: AdminSignedPayload, signature: String): Bool {
156 // Gets the Crypto.KeyList and the public key of the collection's owner
157 let keyList = Crypto.KeyList()
158 let accountKey = self.owner!.keys.get(keyIndex: 0)!.publicKey
159
160 let publicKey = PublicKey(
161 publicKey: accountKey.publicKey,
162 signatureAlgorithm: accountKey.signatureAlgorithm
163 )
164
165 return publicKey.verify(
166 signature: signature.decodeHex(),
167 signedData: signedPayloadData.toString().utf8,
168 domainSeparationTag: "FLOW-V0.0-user",
169 hashAlgorithm: HashAlgorithm.SHA3_256
170 )
171 }
172
173 access(self) fun validateDomain(domain: String, ownerAddress: Address) {
174 var i = 0
175 var index = -1
176 while i < domain.length {
177 if domain[i] == "." {
178 index = i
179 break
180 }
181 i = i + 1
182 }
183 assert(index != -1, message: "invalid domain")
184
185 assert(OrdinalVendor.checkDomainAvailability(domain: domain), message: "domain already exists")
186
187 let domainName = domain.slice(from: 0, upTo: index)
188 let domainExtension = domain.slice(from: index + 1, upTo: domain.length)
189
190 assert(
191 domainExtension == "flow" || domainExtension == "flowverse" || domainExtension == "socks" || domainExtension == "shirt",
192 message: "domain extension must be .flow, .flowverse, .socks, or .shirt"
193 )
194 assert(domainName.length <= 32, message: "domain name must be at most 32 characters long")
195
196 let forbiddenChars = "!@#$%^&*()<>? ./\\`~+=,;:'\"[]{}|_"
197 for c in forbiddenChars.utf8 {
198 assert(!domainName.utf8.contains(c), message: "domain name contains forbidden characters")
199 }
200
201 // Check if domain name is lowercase
202 assert(domainName == domainName.toLower(), message: "domain name must be lowercase")
203
204 // Check if user owns a Flowverse Pass to be eligible for .flowverse domain
205 if domainExtension == "flowverse" {
206 let mysteryPassCollectionRef = getAccount(ownerAddress).capabilities.borrow<&{NonFungibleToken.Collection}>(FlowversePass.CollectionPublicPath)
207 ?? panic("FlowversePass Collection reference not found")
208 assert(mysteryPassCollectionRef.getIDs().length > 0, message: "ineligible for .flowverse domain as user does not own a Flowverse Pass")
209 }
210
211 // Check if user owns a Flowverse Sock to be eligible for .socks domain
212 if domainExtension == "socks" {
213 let socksCollectionRef = getAccount(ownerAddress).capabilities.borrow<&{NonFungibleToken.Collection}>(FlowverseSocks.CollectionPublicPath)
214 ?? panic("FlowverseSocks Collection reference not found")
215 assert(socksCollectionRef.getIDs().length > 0, message: "ineligible for .socks domain as user does not own a Flowverse Sock")
216 }
217
218 // Check if user owns a Flowverse Shirt to be eligible for .shirt domain
219 if domainExtension == "shirt" {
220 let shirtCollectionRef = getAccount(ownerAddress).capabilities.borrow<&{NonFungibleToken.Collection}>(FlowverseShirt.CollectionPublicPath)
221 ?? panic("FlowverseShirt Collection reference not found")
222 assert(shirtCollectionRef.getIDs().length > 0, message: "ineligible for .shirt domain as user does not own a Flowverse Shirt")
223 }
224 }
225
226 access(all) fun purchaseDomain(
227 payment: @{FungibleToken.Vault},
228 data: PurchaseData,
229 adminSignedPayload: AdminSignedPayload,
230 signature: String
231 ) {
232 pre {
233 data.type == "domain": "Invalid type (must be domain)"
234 adminSignedPayload.expiration >= UInt64(getCurrentBlock().timestamp): "expired signature"
235 }
236
237 assert(
238 self.verifyAdminSignedPayload(signedPayloadData: adminSignedPayload, signature: signature) == true,
239 message: "failed to validate signature for the purchase"
240 )
241
242 self.validateDomain(domain: data.inscriptionData, ownerAddress: data.purchaserAddress)
243 self.handlePurchase(payment: <-payment, data: data)
244 }
245
246 access(all) fun purchaseText(
247 payment: @{FungibleToken.Vault},
248 data: PurchaseData,
249 adminSignedPayload: AdminSignedPayload,
250 signature: String
251 ) {
252 pre {
253 data.type == "text": "Invalid type (must be text)"
254 adminSignedPayload.expiration >= UInt64(getCurrentBlock().timestamp): "expired signature"
255 data.inscriptionData.length > 0: "text size must be greater than 0"
256 data.inscriptionData.length <= 300000: "text must be less than or equal to 300KB"
257 }
258
259 assert(
260 self.verifyAdminSignedPayload(signedPayloadData: adminSignedPayload, signature: signature) == true,
261 message: "failed to validate signature for the purchase"
262 )
263
264 self.handlePurchase(payment: <-payment, data: data)
265 }
266
267 access(all) fun purchaseImage(
268 payment: @{FungibleToken.Vault},
269 data: PurchaseData,
270 adminSignedPayload: AdminSignedPayload,
271 signature: String
272 ) {
273 pre {
274 data.type == "image" : "Invalid type (must be image)"
275 adminSignedPayload.expiration >= UInt64(getCurrentBlock().timestamp): "expired signature"
276 data.inscriptionData.length > 0: "image size must be greater than 0"
277 data.inscriptionData.length <= 300000: "image size must be less than 300KB"
278 }
279
280 assert(
281 self.verifyAdminSignedPayload(signedPayloadData: adminSignedPayload, signature: signature) == true,
282 message: "failed to validate signature for the purchase"
283 )
284
285 self.handlePurchase(payment: <-payment, data: data)
286 }
287
288 access(self) fun handlePurchase(
289 payment: @{FungibleToken.Vault},
290 data: PurchaseData
291 ) {
292 pre {
293 data.type == "image" || data.type == "text" || data.type == "domain" : "Invalid type (must be either image, text or domain)"
294 self.paymentReceiverCaps.containsKey(payment.getType().identifier): "payment receiver capability does not exist"
295 }
296
297 let salePaymentVaultType = payment.getType().identifier
298 var size = UInt64(data.inscriptionData.length)
299 if data.type == "domain" {
300 size = OrdinalVendor.getDomainSize(domain: data.inscriptionData)
301 }
302 let price = OrdinalVendor.getPrice(size: size, type: data.type, salePaymentVaultType: salePaymentVaultType)
303 assert(payment.balance == price, message: "payment vault does not contain requested price")
304
305 let receiver = self.paymentReceiverCaps[salePaymentVaultType]!.borrow()!
306 receiver.deposit(from: <- payment)
307
308 let minter = OrdinalVendor.getMinter()!
309 let ordinal <- minter.mint(creator: data.purchaserAddress, type: data.type, data: data.inscriptionData)
310 let inscriptionNumber = ordinal.id
311 data.purchaserCollectionRef.deposit(token: <-ordinal)
312
313 if data.type == "domain" {
314 OrdinalVendor.domains[data.inscriptionData] = inscriptionNumber
315 } else if data.type == "text" {
316 OrdinalVendor.textIDs.append(inscriptionNumber)
317 } else {
318 OrdinalVendor.imageIDs.append(inscriptionNumber)
319 }
320
321 emit OrdinalPurchasedV2(id: inscriptionNumber, type: data.type, size: UInt64(data.inscriptionData.length), purchaserAddress: data.purchaserAddress, price: price, salePaymentVaultType: salePaymentVaultType)
322 }
323
324 access(VendorAdmin) fun updatePaymentReceiver(salePaymentVaultType: String, paymentReceiverCap: Capability<&{FungibleToken.Receiver}>) {
325 pre {
326 paymentReceiverCap.borrow() != nil: "Could not borrow payment receiver capability"
327 }
328 self.paymentReceiverCaps[salePaymentVaultType] = paymentReceiverCap
329 }
330 }
331
332 access(all) resource Admin {
333 access(all) fun initialise(
334 minterCap: Capability<&{IMinter}>,
335 prices: {String: PriceData},
336 paymentReceiverCaps: {String: Capability<&{FungibleToken.Receiver}>}
337 ) {
338 pre {
339 minterCap.borrow() != nil: "Could not borrow minter capability"
340 }
341
342 let vendor <- create Vendor(
343 minterCap: minterCap,
344 prices: prices,
345 paymentReceiverCaps: paymentReceiverCaps
346 )
347 OrdinalVendor.vendor <-! vendor
348 }
349
350 access(all) view fun getVendor(): auth(VendorAdmin) &Vendor? {
351 if OrdinalVendor.vendor != nil {
352 return (&OrdinalVendor.vendor as auth(VendorAdmin) &Vendor?)!
353 }
354 return nil
355 }
356
357 access(all) fun addRestrictedID(id: UInt64) {
358 pre {
359 !OrdinalVendor.restrictedIDs.containsKey(id) : "ID already restricted"
360 }
361 OrdinalVendor.restrictedIDs.insert(key: id, true)
362 emit OrdinalRestricted(id: id)
363 }
364
365 access(all) fun createNewAdmin(): @Admin {
366 return <-create Admin()
367 }
368 }
369
370 access(all) fun purchaseOrdinal(
371 payment: @{FungibleToken.Vault},
372 type: String,
373 data: PurchaseData,
374 adminSignedPayload: AdminSignedPayload,
375 signature: String
376 ) {
377 pre {
378 OrdinalVendor.vendor != nil: "Vendor has not been initialised"
379 type == "image" || type == "text" || type == "domain" : "Invalid type (must be either image, text or domain)"
380 }
381 let vendor = (&OrdinalVendor.vendor as &Vendor?)!
382 if type == "domain" {
383 vendor.purchaseDomain(payment: <-payment, data: data, adminSignedPayload: adminSignedPayload, signature: signature)
384 } else if type == "text" {
385 vendor.purchaseText(payment: <-payment, data: data, adminSignedPayload: adminSignedPayload, signature: signature)
386 } else {
387 vendor.purchaseImage(payment: <-payment, data: data, adminSignedPayload: adminSignedPayload, signature: signature)
388 }
389 }
390
391 access(all) view fun getPrice(size: UInt64, type: String, salePaymentVaultType: String): UFix64 {
392 pre {
393 size > 0: "size must be greater than 0"
394 OrdinalVendor.vendor != nil: "Vendor has not been initialised"
395 }
396 let vendor = (&OrdinalVendor.vendor as &Vendor?)!
397 let prices = vendor.getPrices()
398 let priceData = prices[type] ?? panic("price does not exist")
399 let price = priceData.price[salePaymentVaultType] ?? panic("payment type not supported")
400 var multiplier = UFix64(1)
401 if type == "image" {
402 if size <= 10000 {
403 multiplier = UFix64(1)
404 } else if size <= 100000 {
405 multiplier = UFix64(2)
406 } else if size <= 200000 {
407 multiplier = UFix64(3)
408 } else {
409 multiplier = UFix64(5)
410 }
411 } else if type == "domain" {
412 if size == 1 {
413 multiplier = UFix64(16)
414 } else if size <= 2 {
415 multiplier = UFix64(10)
416 } else if size <= 3 {
417 multiplier = UFix64(6)
418 } else if size <= 4 {
419 multiplier = UFix64(4)
420 } else {
421 multiplier = UFix64(1)
422 }
423 }
424 return price * multiplier
425 }
426
427 access(all) view fun getDomainSize(domain: String): UInt64 {
428 var i = 0
429 for c in domain {
430 if c == "." {
431 break
432 }
433 i = i + 1
434 }
435 assert(i > 0, message: "invalid domain")
436 return UInt64(i)
437 }
438
439 access(all) view fun checkDomainAvailability(domain: String): Bool {
440 return OrdinalVendor.domains[domain] == nil
441 }
442
443 access(all) view fun checkOrdinalRestricted(id: UInt64): Bool {
444 return OrdinalVendor.restrictedIDs.containsKey(id)
445 }
446
447 access(all) struct OrdinalVendorInfo {
448 access(all) let domains: {String: UInt64}
449 access(all) let textIDs: [UInt64]
450 access(all) let imageIDs: [UInt64]
451 access(all) let restrictedIDs: {UInt64: Bool}
452 access(all) let prices: {String: OrdinalVendor.PriceData}
453 access(all) let paymentReceivers: {String: Address}
454
455 init(
456 domains: {String: UInt64},
457 textIDs: [UInt64],
458 imageIDs: [UInt64],
459 restrictedIDs: {UInt64: Bool},
460 prices: {String: OrdinalVendor.PriceData},
461 paymentReceivers: {String: Address} ,
462 ) {
463 self.domains = domains
464 self.textIDs = textIDs
465 self.imageIDs = imageIDs
466 self.restrictedIDs = restrictedIDs
467 self.prices = prices
468 self.paymentReceivers = paymentReceivers
469 }
470 }
471
472 access(all) fun getInfo(): OrdinalVendorInfo? {
473 if OrdinalVendor.vendor != nil {
474 let vendor = (&OrdinalVendor.vendor as &Vendor?)!
475 return OrdinalVendorInfo(
476 domains: OrdinalVendor.domains,
477 textIDs: OrdinalVendor.textIDs,
478 imageIDs: OrdinalVendor.imageIDs,
479 restrictedIDs: OrdinalVendor.restrictedIDs,
480 prices: vendor.getPrices(),
481 paymentReceivers: vendor.getPaymentReceivers()
482 )
483 }
484 return nil
485 }
486
487 access(self) fun getMinter(): &{IMinter}? {
488 return self.account.storage.borrow<&{IMinter}>(from: /storage/ordinalMinter)
489 }
490
491 init() {
492 self.AdminStoragePath = /storage/OrdinalVendorAdminStoragePath
493
494 self.vendor <- nil
495 self.domains = {}
496 self.textIDs = []
497 self.imageIDs = []
498 self.restrictedIDs = {}
499
500 self.account.storage.save<@Admin>(<- create Admin(), to: self.AdminStoragePath)
501 }
502}
503