Smart Contract

TMB2B

A.e3ac5e6a6b6c63db.TMB2B

Valid From

143,533,152

Deployed

1w ago
Feb 20, 2026, 04:51:03 AM UTC

Dependents

1 imports
1// Description: Smart Contract for Ticketmaster Business NFTs
2// SPDX-License-Identifier: UNLICENSED
3
4import NonFungibleToken from 0x1d7e57aa55817448
5import MetadataViews from 0x1d7e57aa55817448
6import ViewResolver from 0x1d7e57aa55817448
7
8access(all) contract TMB2B : NonFungibleToken {
9
10    access(all) var totalSupply: UInt64
11
12    access(all) event ContractInitialized()
13    access(all) event Withdraw(id: UInt64, from: Address?)
14    access(all) event Deposit(id: UInt64, to: Address?)
15
16    // Transfer Control Events
17    access(all) event TransferLocked(id: UInt64, reason: String)
18    access(all) event TransferUnlocked(id: UInt64)
19    access(all) event InitialTransferCompleted(id: UInt64, to: Address)
20    access(all) event BatchTransferLockChanged(nftIds: [UInt64], isLocked: Bool)
21
22    access(all) let CollectionStoragePath: StoragePath
23    access(all) let CollectionPublicPath: PublicPath
24    access(all) let MinterStoragePath: StoragePath
25
26    // TransferController Storage Path
27    access(all) fun transferControllerStoragePath(): StoragePath {
28        return /storage/TMB2BTransferController
29    }
30
31    // TransferController Management Functions
32    access(all) fun ensureTransferController(): Bool {
33        if self.account.storage.borrow<&TransferController>(
34            from: self.transferControllerStoragePath()
35        ) != nil {
36            return false
37        }
38
39        let controller <- create TransferController()
40        self.account.storage.save(<-controller, to: self.transferControllerStoragePath())
41        return true
42    }
43
44    access(contract) fun borrowTransferController(): &TransferController? {
45        return self.account.storage.borrow<&TransferController>(
46            from: self.transferControllerStoragePath()
47        )
48    }
49
50    access(all) fun isNFTLocked(nftId: UInt64): Bool {
51        self.ensureTransferController()
52        if let controller = self.borrowTransferController() {
53            return controller.isLocked(nftId: nftId)
54        }
55        return true
56    }
57
58    access(all) fun handleTransfer(nftId: UInt64, to: Address?) {
59        self.ensureTransferController()
60        let controller = self.borrowTransferController()
61            ?? panic("TransferController missing after ensureTransferController call")
62        controller.recordTransfer(nftId: nftId, to: to)
63    }
64
65    access(all) fun getTransferStatus(nftId: UInt64): {String: AnyStruct} {
66        self.ensureTransferController()
67
68        if let controller = self.borrowTransferController() {
69            return controller.getTransferStatus(nftId: nftId)
70        }
71
72        return {
73            "nftId": nftId,
74            "isTransferLocked": true,
75            "hasRestrictionRecord": false,
76            "error": "TransferController missing"
77        }
78    }
79
80    // Correct type from NonFungibleToken.INFT to NonFungibleToken.NFT
81    access(all) resource NFT : NonFungibleToken.NFT {
82        access(all) let id: UInt64
83        access(all) var link: String
84        access(all) var batch: UInt32
85        access(all) var sequence: UInt16
86        access(all) var limit: UInt16
87
88        init(
89            initID: UInt64,
90            initlink: String,
91            initbatch: UInt32,
92            initsequence: UInt16,
93            initlimit: UInt16
94        ) {
95            self.id = initID
96            self.link = initlink
97            self.batch = initbatch
98            self.sequence = initsequence
99            self.limit = initlimit
100        }
101
102        /// createEmptyCollection creates an empty Collection
103        /// and returns it to the caller so that they can own NFTs
104        /// @{NonFungibleToken.Collection}
105        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
106            return <-TMB2B.createEmptyCollection(nftType: Type<@TMB2B.NFT>())
107        }
108        access(all) view fun getViews(): [Type] {
109            return [
110                Type<MetadataViews.NFTCollectionData>()
111            ]
112        }
113
114        access(all) fun resolveView(_ view: Type): AnyStruct? {
115            switch view {
116                case Type<MetadataViews.NFTCollectionData>():
117                    return TMB2B.resolveContractView(resourceType: Type<@TMB2B.NFT>(), viewType: Type<MetadataViews.NFTCollectionData>())
118            }
119            return nil
120        }
121    }
122
123    // Ensure all type specifications conform to interface requirements
124    access(all) resource interface TMB2BCollectionPublic {
125        // Deprecated, only here for backwards compatibility
126    }
127
128    access(all) resource Collection : NonFungibleToken.Collection, TMB2BCollectionPublic {
129        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
130
131        init() {
132            self.ownedNFTs <- {}
133        }
134
135        /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts
136        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
137            let supportedTypes: {Type: Bool} = {}
138            supportedTypes[Type<@TMB2B.NFT>()] = true
139            return supportedTypes
140        }
141
142        /// Returns whether or not the given type is accepted by the collection
143        /// A collection that can accept any type should just return true by default
144        access(all) view fun isSupportedNFTType(type: Type): Bool {
145            return type == Type<@TMB2B.NFT>()
146        }
147
148        /// withdraw removes an NFT from the collection and moves it to the caller
149        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
150            // Check transfer lock before withdraw
151            let isLocked = TMB2B.isNFTLocked(nftId: withdrawID)
152            assert(!isLocked, message: "Transfer is locked for this NFT")
153
154            let token <- self.ownedNFTs.remove(key: withdrawID)
155                ?? panic("Could not withdraw an NFT with the provided ID from the collection")
156
157            return <-token
158        }
159
160        /// deposit takes a NFT and adds it to the collections dictionary
161        /// and adds the ID to the id array
162        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
163            let token <- token as! @TMB2B.NFT
164            let id = token.id
165
166            // Record transfer for auto-lock functionality
167            let recipient = self.owner?.address
168            TMB2B.handleTransfer(nftId: id, to: recipient)
169
170            // add the new token to the dictionary which removes the old one
171            let oldToken <- self.ownedNFTs[token.id] <- token
172
173            destroy oldToken
174
175        }
176
177        /// getIDs returns an array of the IDs that are in the collection
178        access(all) view fun getIDs(): [UInt64] {
179            return self.ownedNFTs.keys
180        }
181
182        /// Gets the amount of NFTs stored in the collection
183        access(all) view fun getLength(): Int {
184            return self.ownedNFTs.length
185        }
186
187        /// Borrow the view resolver for the specified NFT ID
188        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
189            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
190                return nft as &{ViewResolver.Resolver}
191            }
192            return nil
193        }
194
195        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
196            return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)
197        }
198
199        /// createEmptyCollection creates an empty Collection of the same type
200        /// and returns it to the caller
201        /// @return A an empty collection of the same type
202        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
203            return <- TMB2B.createEmptyCollection(nftType: Type<@TMB2B.NFT>())
204        }
205    }
206
207    /// createEmptyCollection creates an empty Collection for the specified NFT type
208    /// and returns it to the caller so that they can own NFTs
209    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
210        return <- create Collection()
211    }
212
213    /// Function that returns all the Metadata Views implemented by a Non Fungible Token
214    ///
215    /// @return An array of Types defining the implemented views. This value will be used by
216    ///         developers to know which parameter to pass to the resolveView() method.
217    ///
218    access(all) view fun getContractViews(resourceType: Type?): [Type] {
219        return [
220            Type<MetadataViews.NFTCollectionData>(),
221            Type<MetadataViews.NFTCollectionDisplay>(),
222            Type<MetadataViews.EVMBridgedMetadata>()
223        ]
224    }
225
226    /// Function that resolves a metadata view for this contract.
227    ///
228    /// @param view: The Type of the desired view.
229    /// @return A structure representing the requested view.
230    ///
231    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
232        switch viewType {
233            case Type<MetadataViews.NFTCollectionData>():
234                let collectionData = MetadataViews.NFTCollectionData(
235                    storagePath: self.CollectionStoragePath,
236                    publicPath: self.CollectionPublicPath,
237                    publicCollection: Type<&TMB2B.Collection>(),
238                    publicLinkedType: Type<&TMB2B.Collection>(),
239                    createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
240                        return <-TMB2B.createEmptyCollection(nftType: Type<@TMB2B.NFT>())
241                    })
242                )
243                return collectionData
244            case Type<MetadataViews.NFTCollectionDisplay>():
245                let media = MetadataViews.Media(
246                    file: MetadataViews.HTTPFile(
247                        url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg"
248                    ),
249                    mediaType: "image/svg+xml"
250                )
251                return MetadataViews.NFTCollectionDisplay(
252                    name: "TMB2B Collection",
253                    description: "This collection represents Ticketmaster Business NFTs.",
254                    externalURL: MetadataViews.ExternalURL("https://tmb2b-nft.onflow.org"),
255                    squareImage: media,
256                    bannerImage: media,
257                    socials: {
258                        "twitter": MetadataViews.ExternalURL("https://twitter.com/ticketmaster")
259                    }
260                )
261            case Type<MetadataViews.EVMBridgedMetadata>():
262                // Implementing this view gives the project control over how the bridged NFT is represented as an ERC721
263                // when bridged to EVM on Flow via the public infrastructure bridge.
264
265                // Compose the contract-level URI. In this case, the contract metadata is located on some HTTP host,
266                // but it could be IPFS, S3, a data URL containing the JSON directly, etc.
267                return MetadataViews.EVMBridgedMetadata(
268                    name: "TMB2B",
269                    symbol: "TMB2B",
270                    uri: MetadataViews.URI(
271                        baseURI: nil, // setting baseURI as nil sets the given value as the uri field value
272                        value: "https://tmb2b-nft.onflow.org/contract-metadata.json"
273                    )
274                )
275        }
276        return nil
277    }
278
279    /// Resource that an admin or something similar would own to be
280    /// able to mint new NFTs
281    ///
282    access(all) resource NFTMinter {
283
284        /// mintNFT mints a new NFT with a new ID
285        /// and returns it to the calling context
286        access(all) fun mintNFT(
287            glink: String,
288            gbatch: UInt32,
289            glimit: UInt16,
290            gsequence: UInt16
291        ): @TMB2B.NFT {
292
293            let tokenID = (UInt64(gbatch) << 32) | (UInt64(glimit) << 16) | UInt64(gsequence)
294
295            let metadata: {String: AnyStruct} = {}
296            let currentBlock = getCurrentBlock()
297            metadata["mintedBlock"] = currentBlock.height
298            metadata["mintedTime"] = currentBlock.timestamp
299
300            // this piece of metadata will be used to show embedding rarity into a trait
301            metadata["foo"] = "bar"
302
303            // create a new NFT
304            var newNFT <- create NFT(
305                initID: tokenID,
306                initlink: glink,
307                initbatch: gbatch,
308                initsequence: gsequence,
309                initlimit: glimit
310            )
311            TMB2B.totalSupply = TMB2B.totalSupply + 1
312            return <-newNFT
313        }
314    }
315
316    /// TransferController resource manages NFT transfer restrictions
317    access(all) resource TransferController {
318        access(self) var transferRestrictions: {UInt64: Bool}
319        access(self) var pendingAutoLocks: {UInt64: Bool}
320
321        init() {
322            self.transferRestrictions = {}
323            self.pendingAutoLocks = {}
324        }
325
326        access(all) fun isLocked(nftId: UInt64): Bool {
327            return self.transferRestrictions[nftId] ?? true
328        }
329
330        access(self) fun isServiceAddress(_ address: Address?): Bool {
331            return address == self.owner?.address
332        }
333
334        access(all) fun recordTransfer(nftId: UInt64, to: Address?) {
335            let current = self.transferRestrictions[nftId]
336            let isServiceRecipient = self.isServiceAddress(to)
337
338            // First transfer - set lock state
339            if current == nil {
340                if isServiceRecipient {
341                    // Minting to service account (staged mint)
342                    self.transferRestrictions[nftId] = false
343                    self.pendingAutoLocks[nftId] = true
344                } else {
345                    // Direct mint to user - lock immediately
346                    self.transferRestrictions[nftId] = true
347                    let fallback = self.owner?.address
348                    let resolvedTo = to ?? fallback ?? panic("Unable to resolve recipient for initial transfer")
349                    emit InitialTransferCompleted(id: nftId, to: resolvedTo)
350                    emit TransferLocked(id: nftId, reason: "Initial transfer completed")
351                }
352                return
353            }
354
355            // Subsequent transfers - check for pending auto-lock
356            if self.pendingAutoLocks[nftId] == true {
357                if !isServiceRecipient {
358                    // Service account -> User (staged delivery)
359                    self.transferRestrictions[nftId] = true
360                    self.pendingAutoLocks.remove(key: nftId)
361                    let fallback = self.owner?.address
362                    let resolvedTo = to ?? fallback ?? panic("Unable to resolve recipient for initial transfer")
363                    emit InitialTransferCompleted(id: nftId, to: resolvedTo)
364                    emit TransferLocked(id: nftId, reason: "Initial delivery completed")
365                }
366                return
367            }
368        }
369
370        access(all) fun setNFTTransferLock(nftId: UInt64, isLocked: Bool) {
371            let previous = self.transferRestrictions[nftId] ?? true
372            self.transferRestrictions[nftId] = isLocked
373            if !isLocked {
374                self.pendingAutoLocks.remove(key: nftId)
375            }
376
377            if isLocked {
378                if previous != true {
379                    emit TransferLocked(id: nftId, reason: "Admin locked")
380                }
381                return
382            }
383
384            if previous != false {
385                emit TransferUnlocked(id: nftId)
386            }
387        }
388
389        access(all) fun batchSetNFTTransferLock(nftIds: [UInt64], isLocked: Bool) {
390            for nftId in nftIds {
391                let previous = self.transferRestrictions[nftId] ?? true
392                self.transferRestrictions[nftId] = isLocked
393                if !isLocked {
394                    self.pendingAutoLocks.remove(key: nftId)
395                }
396
397                if isLocked {
398                    if previous != true {
399                        emit TransferLocked(id: nftId, reason: "Admin locked (batch)")
400                    }
401                    continue
402                }
403
404                if previous != false {
405                    emit TransferUnlocked(id: nftId)
406                }
407            }
408            emit BatchTransferLockChanged(nftIds: nftIds, isLocked: isLocked)
409        }
410
411        access(all) fun getLockedNFTs(): [UInt64] {
412            let lockedNFTs: [UInt64] = []
413            for nftId in self.transferRestrictions.keys {
414                if self.transferRestrictions[nftId] == true {
415                    lockedNFTs.append(nftId)
416                }
417            }
418            return lockedNFTs
419        }
420
421        access(all) fun getTransferStatus(nftId: UInt64): {String: AnyStruct} {
422            let status: {String: AnyStruct} = {}
423            status["nftId"] = nftId
424            status["isTransferLocked"] = self.transferRestrictions[nftId] ?? true
425            status["hasRestrictionRecord"] = self.transferRestrictions.containsKey(nftId)
426            status["pendingAutoLock"] = self.pendingAutoLocks[nftId] ?? false
427            return status
428        }
429    }
430
431    init() {
432        self.CollectionStoragePath = /storage/TMB2BCollection
433        self.CollectionPublicPath = /public/TMB2BCollection
434        self.MinterStoragePath = /storage/TMB2BMinter
435        self.totalSupply = 0
436
437        let collection <- create Collection()
438        self.account.storage.save(<-collection, to: self.CollectionStoragePath)
439
440        let collectionCap = self.account.capabilities.storage.issue<&TMB2B.Collection>(self.CollectionStoragePath)
441        self.account.capabilities.publish(collectionCap, at: self.CollectionPublicPath)
442
443        let minter <- create NFTMinter()
444        self.account.storage.save(<-minter, to: self.MinterStoragePath)
445
446        // Initialize TransferController
447        self.ensureTransferController()
448
449        emit ContractInitialized()
450    }
451}
452