Smart Contract
TMB2B
A.e3ac5e6a6b6c63db.TMB2B
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