Smart Contract

OrdinalVendor

A.9212a87501a8a6a2.OrdinalVendor

Valid From

86,528,101

Deployed

4d ago
Feb 24, 2026, 02:13:43 PM UTC

Dependents

3 imports
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