@flowtyio/flow-contracts 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contracts/dapper/TopShot.cdc +1686 -0
- package/contracts/dapper/TopShotLocking.cdc +165 -0
- package/contracts/dapper/offers/DapperOffersV2.cdc +238 -0
- package/contracts/dapper/offers/OffersV2.cdc +344 -0
- package/contracts/dapper/offers/Resolver.cdc +70 -0
- package/contracts/flow-utils/AddressUtils.cdc +109 -0
- package/contracts/flow-utils/ArrayUtils.cdc +69 -0
- package/contracts/flow-utils/StringUtils.cdc +97 -0
- package/flow.json +73 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1686 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Description: Central Smart Contract for NBA TopShot
|
|
3
|
+
|
|
4
|
+
This smart contract contains the core functionality for
|
|
5
|
+
NBA Top Shot, created by Dapper Labs
|
|
6
|
+
|
|
7
|
+
The contract manages the data associated with all the plays and sets
|
|
8
|
+
that are used as templates for the Moment NFTs
|
|
9
|
+
|
|
10
|
+
When a new Play wants to be added to the records, an Admin creates
|
|
11
|
+
a new Play struct that is stored in the smart contract.
|
|
12
|
+
|
|
13
|
+
Then an Admin can create new Sets. Sets consist of a public struct that
|
|
14
|
+
contains public information about a set, and a private resource used
|
|
15
|
+
to mint new moments based off of plays that have been linked to the Set.
|
|
16
|
+
|
|
17
|
+
The admin resource has the power to do all of the important actions
|
|
18
|
+
in the smart contract. When admins want to call functions in a set,
|
|
19
|
+
they call their borrowSet function to get a reference
|
|
20
|
+
to a set in the contract. Then, they can call functions on the set using that reference.
|
|
21
|
+
|
|
22
|
+
In this way, the smart contract and its defined resources interact
|
|
23
|
+
with great teamwork, just like the Indiana Pacers, the greatest NBA team
|
|
24
|
+
of all time.
|
|
25
|
+
|
|
26
|
+
When moments are minted, they are initialized with a MomentData struct and
|
|
27
|
+
are returned by the minter.
|
|
28
|
+
|
|
29
|
+
The contract also defines a Collection resource. This is an object that
|
|
30
|
+
every TopShot NFT owner will store in their account
|
|
31
|
+
to manage their NFT collection.
|
|
32
|
+
|
|
33
|
+
The main Top Shot account will also have its own Moment collections
|
|
34
|
+
it can use to hold its own moments that have not yet been sent to a user.
|
|
35
|
+
|
|
36
|
+
Note: All state changing functions will panic if an invalid argument is
|
|
37
|
+
provided or one of its pre-conditions or post conditions aren't met.
|
|
38
|
+
Functions that don't modify state will simply return 0 or nil
|
|
39
|
+
and those cases need to be handled by the caller.
|
|
40
|
+
|
|
41
|
+
It is also important to remember that
|
|
42
|
+
The Golden State Warriors blew a 3-1 lead in the 2016 NBA finals.
|
|
43
|
+
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import "FungibleToken"
|
|
47
|
+
import "NonFungibleToken"
|
|
48
|
+
import "MetadataViews"
|
|
49
|
+
import "TopShotLocking"
|
|
50
|
+
|
|
51
|
+
pub contract TopShot: NonFungibleToken {
|
|
52
|
+
// -----------------------------------------------------------------------
|
|
53
|
+
// TopShot deployment variables
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
// The network the contract is deployed on
|
|
57
|
+
pub fun Network() : String { return "emulator" }
|
|
58
|
+
|
|
59
|
+
// The address to which royalties should be deposited
|
|
60
|
+
pub fun RoyaltyAddress() : Address { return TopShot.account.address }
|
|
61
|
+
|
|
62
|
+
// The path to the Subedition Admin resource belonging to the Account
|
|
63
|
+
// which the contract is deployed on
|
|
64
|
+
pub fun SubeditionAdminStoragePath() : StoragePath { return /storage/TopShotSubeditionAdmin}
|
|
65
|
+
|
|
66
|
+
// -----------------------------------------------------------------------
|
|
67
|
+
// TopShot contract Events
|
|
68
|
+
// -----------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
// Emitted when the TopShot contract is created
|
|
71
|
+
pub event ContractInitialized()
|
|
72
|
+
|
|
73
|
+
// Emitted when a new Play struct is created
|
|
74
|
+
pub event PlayCreated(id: UInt32, metadata: {String:String})
|
|
75
|
+
// Emitted when a new series has been triggered by an admin
|
|
76
|
+
pub event NewSeriesStarted(newCurrentSeries: UInt32)
|
|
77
|
+
|
|
78
|
+
// Events for Set-Related actions
|
|
79
|
+
//
|
|
80
|
+
// Emitted when a new Set is created
|
|
81
|
+
pub event SetCreated(setID: UInt32, series: UInt32)
|
|
82
|
+
// Emitted when a new Play is added to a Set
|
|
83
|
+
pub event PlayAddedToSet(setID: UInt32, playID: UInt32)
|
|
84
|
+
// Emitted when a Play is retired from a Set and cannot be used to mint
|
|
85
|
+
pub event PlayRetiredFromSet(setID: UInt32, playID: UInt32, numMoments: UInt32)
|
|
86
|
+
// Emitted when a Set is locked, meaning Plays cannot be added
|
|
87
|
+
pub event SetLocked(setID: UInt32)
|
|
88
|
+
// Emitted when a Moment is minted from a Set
|
|
89
|
+
pub event MomentMinted(momentID: UInt64, playID: UInt32, setID: UInt32, serialNumber: UInt32, subeditionID: UInt32)
|
|
90
|
+
|
|
91
|
+
// Events for Collection-related actions
|
|
92
|
+
//
|
|
93
|
+
// Emitted when a moment is withdrawn from a Collection
|
|
94
|
+
pub event Withdraw(id: UInt64, from: Address?)
|
|
95
|
+
// Emitted when a moment is deposited into a Collection
|
|
96
|
+
pub event Deposit(id: UInt64, to: Address?)
|
|
97
|
+
|
|
98
|
+
// Emitted when a Moment is destroyed
|
|
99
|
+
pub event MomentDestroyed(id: UInt64)
|
|
100
|
+
|
|
101
|
+
// Emitted when a Subedition is created
|
|
102
|
+
pub event SubeditionCreated(subeditionID: UInt32, name: String, metadata: {String:String})
|
|
103
|
+
|
|
104
|
+
// Emitted when a Subedition is linked to the specific Moment
|
|
105
|
+
pub event SubeditionAddedToMoment(momentID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32)
|
|
106
|
+
|
|
107
|
+
// -----------------------------------------------------------------------
|
|
108
|
+
// TopShot contract-level fields.
|
|
109
|
+
// These contain actual values that are stored in the smart contract.
|
|
110
|
+
// -----------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
// Series that this Set belongs to.
|
|
113
|
+
// Series is a concept that indicates a group of Sets through time.
|
|
114
|
+
// Many Sets can exist at a time, but only one series.
|
|
115
|
+
pub var currentSeries: UInt32
|
|
116
|
+
|
|
117
|
+
// Variable size dictionary of Play structs
|
|
118
|
+
access(self) var playDatas: {UInt32: Play}
|
|
119
|
+
|
|
120
|
+
// Variable size dictionary of SetData structs
|
|
121
|
+
access(self) var setDatas: {UInt32: SetData}
|
|
122
|
+
|
|
123
|
+
// Variable size dictionary of Set resources
|
|
124
|
+
access(self) var sets: @{UInt32: Set}
|
|
125
|
+
|
|
126
|
+
// The ID that is used to create Plays.
|
|
127
|
+
// Every time a Play is created, playID is assigned
|
|
128
|
+
// to the new Play's ID and then is incremented by 1.
|
|
129
|
+
pub var nextPlayID: UInt32
|
|
130
|
+
|
|
131
|
+
// The ID that is used to create Sets. Every time a Set is created
|
|
132
|
+
// setID is assigned to the new set's ID and then is incremented by 1.
|
|
133
|
+
pub var nextSetID: UInt32
|
|
134
|
+
|
|
135
|
+
// The total number of Top shot Moment NFTs that have been created
|
|
136
|
+
// Because NFTs can be destroyed, it doesn't necessarily mean that this
|
|
137
|
+
// reflects the total number of NFTs in existence, just the number that
|
|
138
|
+
// have been minted to date. Also used as global moment IDs for minting.
|
|
139
|
+
pub var totalSupply: UInt64
|
|
140
|
+
|
|
141
|
+
// -----------------------------------------------------------------------
|
|
142
|
+
// TopShot contract-level Composite Type definitions
|
|
143
|
+
// -----------------------------------------------------------------------
|
|
144
|
+
// These are just *definitions* for Types that this contract
|
|
145
|
+
// and other accounts can use. These definitions do not contain
|
|
146
|
+
// actual stored values, but an instance (or object) of one of these Types
|
|
147
|
+
// can be created by this contract that contains stored values.
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
// Play is a Struct that holds metadata associated
|
|
151
|
+
// with a specific NBA play, like the legendary moment when
|
|
152
|
+
// Ray Allen hit the 3 to tie the Heat and Spurs in the 2013 finals game 6
|
|
153
|
+
// or when Lance Stephenson blew in the ear of Lebron James.
|
|
154
|
+
//
|
|
155
|
+
// Moment NFTs will all reference a single play as the owner of
|
|
156
|
+
// its metadata. The plays are publicly accessible, so anyone can
|
|
157
|
+
// read the metadata associated with a specific play ID
|
|
158
|
+
//
|
|
159
|
+
pub struct Play {
|
|
160
|
+
|
|
161
|
+
// The unique ID for the Play
|
|
162
|
+
pub let playID: UInt32
|
|
163
|
+
|
|
164
|
+
// Stores all the metadata about the play as a string mapping
|
|
165
|
+
// This is not the long term way NFT metadata will be stored. It's a temporary
|
|
166
|
+
// construct while we figure out a better way to do metadata.
|
|
167
|
+
//
|
|
168
|
+
pub let metadata: {String: String}
|
|
169
|
+
|
|
170
|
+
init(metadata: {String: String}) {
|
|
171
|
+
pre {
|
|
172
|
+
metadata.length != 0: "New Play metadata cannot be empty"
|
|
173
|
+
}
|
|
174
|
+
self.playID = TopShot.nextPlayID
|
|
175
|
+
self.metadata = metadata
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// This function is intended to backfill the Play on blockchain with a more detailed
|
|
179
|
+
/// description of the Play. The benefit of having the description is that anyone would
|
|
180
|
+
/// be able to know the story of the Play directly from Flow
|
|
181
|
+
access(contract) fun updateTagline(tagline: String): UInt32 {
|
|
182
|
+
self.metadata["Tagline"] = tagline
|
|
183
|
+
|
|
184
|
+
TopShot.playDatas[self.playID] = self
|
|
185
|
+
return self.playID
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// A Set is a grouping of Plays that have occured in the real world
|
|
190
|
+
// that make up a related group of collectibles, like sets of baseball
|
|
191
|
+
// or Magic cards. A Play can exist in multiple different sets.
|
|
192
|
+
//
|
|
193
|
+
// SetData is a struct that is stored in a field of the contract.
|
|
194
|
+
// Anyone can query the constant information
|
|
195
|
+
// about a set by calling various getters located
|
|
196
|
+
// at the end of the contract. Only the admin has the ability
|
|
197
|
+
// to modify any data in the private Set resource.
|
|
198
|
+
//
|
|
199
|
+
pub struct SetData {
|
|
200
|
+
|
|
201
|
+
// Unique ID for the Set
|
|
202
|
+
pub let setID: UInt32
|
|
203
|
+
|
|
204
|
+
// Name of the Set
|
|
205
|
+
// ex. "Times when the Toronto Raptors choked in the playoffs"
|
|
206
|
+
pub let name: String
|
|
207
|
+
|
|
208
|
+
// Series that this Set belongs to.
|
|
209
|
+
// Series is a concept that indicates a group of Sets through time.
|
|
210
|
+
// Many Sets can exist at a time, but only one series.
|
|
211
|
+
pub let series: UInt32
|
|
212
|
+
|
|
213
|
+
init(name: String) {
|
|
214
|
+
pre {
|
|
215
|
+
name.length > 0: "New Set name cannot be empty"
|
|
216
|
+
}
|
|
217
|
+
self.setID = TopShot.nextSetID
|
|
218
|
+
self.name = name
|
|
219
|
+
self.series = TopShot.currentSeries
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Set is a resource type that contains the functions to add and remove
|
|
224
|
+
// Plays from a set and mint Moments.
|
|
225
|
+
//
|
|
226
|
+
// It is stored in a private field in the contract so that
|
|
227
|
+
// the admin resource can call its methods.
|
|
228
|
+
//
|
|
229
|
+
// The admin can add Plays to a Set so that the set can mint Moments
|
|
230
|
+
// that reference that playdata.
|
|
231
|
+
// The Moments that are minted by a Set will be listed as belonging to
|
|
232
|
+
// the Set that minted it, as well as the Play it references.
|
|
233
|
+
//
|
|
234
|
+
// Admin can also retire Plays from the Set, meaning that the retired
|
|
235
|
+
// Play can no longer have Moments minted from it.
|
|
236
|
+
//
|
|
237
|
+
// If the admin locks the Set, no more Plays can be added to it, but
|
|
238
|
+
// Moments can still be minted.
|
|
239
|
+
//
|
|
240
|
+
// If retireAll() and lock() are called back-to-back,
|
|
241
|
+
// the Set is closed off forever and nothing more can be done with it.
|
|
242
|
+
pub resource Set {
|
|
243
|
+
|
|
244
|
+
// Unique ID for the set
|
|
245
|
+
pub let setID: UInt32
|
|
246
|
+
|
|
247
|
+
// Array of plays that are a part of this set.
|
|
248
|
+
// When a play is added to the set, its ID gets appended here.
|
|
249
|
+
// The ID does not get removed from this array when a Play is retired.
|
|
250
|
+
access(contract) var plays: [UInt32]
|
|
251
|
+
|
|
252
|
+
// Map of Play IDs that Indicates if a Play in this Set can be minted.
|
|
253
|
+
// When a Play is added to a Set, it is mapped to false (not retired).
|
|
254
|
+
// When a Play is retired, this is set to true and cannot be changed.
|
|
255
|
+
access(contract) var retired: {UInt32: Bool}
|
|
256
|
+
|
|
257
|
+
// Indicates if the Set is currently locked.
|
|
258
|
+
// When a Set is created, it is unlocked
|
|
259
|
+
// and Plays are allowed to be added to it.
|
|
260
|
+
// When a set is locked, Plays cannot be added.
|
|
261
|
+
// A Set can never be changed from locked to unlocked,
|
|
262
|
+
// the decision to lock a Set it is final.
|
|
263
|
+
// If a Set is locked, Plays cannot be added, but
|
|
264
|
+
// Moments can still be minted from Plays
|
|
265
|
+
// that exist in the Set.
|
|
266
|
+
pub var locked: Bool
|
|
267
|
+
|
|
268
|
+
// Mapping of Play IDs that indicates the number of Moments
|
|
269
|
+
// that have been minted for specific Plays in this Set.
|
|
270
|
+
// When a Moment is minted, this value is stored in the Moment to
|
|
271
|
+
// show its place in the Set, eg. 13 of 60.
|
|
272
|
+
access(contract) var numberMintedPerPlay: {UInt32: UInt32}
|
|
273
|
+
|
|
274
|
+
init(name: String) {
|
|
275
|
+
self.setID = TopShot.nextSetID
|
|
276
|
+
self.plays = []
|
|
277
|
+
self.retired = {}
|
|
278
|
+
self.locked = false
|
|
279
|
+
self.numberMintedPerPlay = {}
|
|
280
|
+
|
|
281
|
+
// Create a new SetData for this Set and store it in contract storage
|
|
282
|
+
TopShot.setDatas[self.setID] = SetData(name: name)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// addPlay adds a play to the set
|
|
286
|
+
//
|
|
287
|
+
// Parameters: playID: The ID of the Play that is being added
|
|
288
|
+
//
|
|
289
|
+
// Pre-Conditions:
|
|
290
|
+
// The Play needs to be an existing play
|
|
291
|
+
// The Set needs to be not locked
|
|
292
|
+
// The Play can't have already been added to the Set
|
|
293
|
+
//
|
|
294
|
+
pub fun addPlay(playID: UInt32) {
|
|
295
|
+
pre {
|
|
296
|
+
TopShot.playDatas[playID] != nil: "Cannot add the Play to Set: Play doesn't exist."
|
|
297
|
+
!self.locked: "Cannot add the play to the Set after the set has been locked."
|
|
298
|
+
self.numberMintedPerPlay[playID] == nil: "The play has already beed added to the set."
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Add the Play to the array of Plays
|
|
302
|
+
self.plays.append(playID)
|
|
303
|
+
|
|
304
|
+
// Open the Play up for minting
|
|
305
|
+
self.retired[playID] = false
|
|
306
|
+
|
|
307
|
+
// Initialize the Moment count to zero
|
|
308
|
+
self.numberMintedPerPlay[playID] = 0
|
|
309
|
+
|
|
310
|
+
emit PlayAddedToSet(setID: self.setID, playID: playID)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// addPlays adds multiple Plays to the Set
|
|
314
|
+
//
|
|
315
|
+
// Parameters: playIDs: The IDs of the Plays that are being added
|
|
316
|
+
// as an array
|
|
317
|
+
//
|
|
318
|
+
pub fun addPlays(playIDs: [UInt32]) {
|
|
319
|
+
for play in playIDs {
|
|
320
|
+
self.addPlay(playID: play)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// retirePlay retires a Play from the Set so that it can't mint new Moments
|
|
325
|
+
//
|
|
326
|
+
// Parameters: playID: The ID of the Play that is being retired
|
|
327
|
+
//
|
|
328
|
+
// Pre-Conditions:
|
|
329
|
+
// The Play is part of the Set and not retired (available for minting).
|
|
330
|
+
//
|
|
331
|
+
pub fun retirePlay(playID: UInt32) {
|
|
332
|
+
pre {
|
|
333
|
+
self.retired[playID] != nil: "Cannot retire the Play: Play doesn't exist in this set!"
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if !self.retired[playID]! {
|
|
337
|
+
self.retired[playID] = true
|
|
338
|
+
|
|
339
|
+
emit PlayRetiredFromSet(setID: self.setID, playID: playID, numMoments: self.numberMintedPerPlay[playID]!)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// retireAll retires all the plays in the Set
|
|
344
|
+
// Afterwards, none of the retired Plays will be able to mint new Moments
|
|
345
|
+
//
|
|
346
|
+
pub fun retireAll() {
|
|
347
|
+
for play in self.plays {
|
|
348
|
+
self.retirePlay(playID: play)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// lock() locks the Set so that no more Plays can be added to it
|
|
353
|
+
//
|
|
354
|
+
// Pre-Conditions:
|
|
355
|
+
// The Set should not be locked
|
|
356
|
+
pub fun lock() {
|
|
357
|
+
if !self.locked {
|
|
358
|
+
self.locked = true
|
|
359
|
+
emit SetLocked(setID: self.setID)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// mintMoment mints a new Moment and returns the newly minted Moment
|
|
364
|
+
//
|
|
365
|
+
// Parameters: playID: The ID of the Play that the Moment references
|
|
366
|
+
//
|
|
367
|
+
// Pre-Conditions:
|
|
368
|
+
// The Play must exist in the Set and be allowed to mint new Moments
|
|
369
|
+
//
|
|
370
|
+
// Returns: The NFT that was minted
|
|
371
|
+
//
|
|
372
|
+
pub fun mintMoment(playID: UInt32): @NFT {
|
|
373
|
+
pre {
|
|
374
|
+
self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist."
|
|
375
|
+
!self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired."
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Gets the number of Moments that have been minted for this Play
|
|
379
|
+
// to use as this Moment's serial number
|
|
380
|
+
let numInPlay = self.numberMintedPerPlay[playID]!
|
|
381
|
+
|
|
382
|
+
// Mint the new moment
|
|
383
|
+
let newMoment: @NFT <- create NFT(serialNumber: numInPlay + UInt32(1),
|
|
384
|
+
playID: playID,
|
|
385
|
+
setID: self.setID,
|
|
386
|
+
subeditionID: 0)
|
|
387
|
+
|
|
388
|
+
// Increment the count of Moments minted for this Play
|
|
389
|
+
self.numberMintedPerPlay[playID] = numInPlay + UInt32(1)
|
|
390
|
+
|
|
391
|
+
return <-newMoment
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// batchMintMoment mints an arbitrary quantity of Moments
|
|
395
|
+
// and returns them as a Collection
|
|
396
|
+
//
|
|
397
|
+
// Parameters: playID: the ID of the Play that the Moments are minted for
|
|
398
|
+
// quantity: The quantity of Moments to be minted
|
|
399
|
+
//
|
|
400
|
+
// Returns: Collection object that contains all the Moments that were minted
|
|
401
|
+
//
|
|
402
|
+
pub fun batchMintMoment(playID: UInt32, quantity: UInt64): @Collection {
|
|
403
|
+
let newCollection <- create Collection()
|
|
404
|
+
|
|
405
|
+
var i: UInt64 = 0
|
|
406
|
+
while i < quantity {
|
|
407
|
+
newCollection.deposit(token: <-self.mintMoment(playID: playID))
|
|
408
|
+
i = i + UInt64(1)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return <-newCollection
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// mintMomentWithSubedition mints a new Moment with subedition and returns the newly minted Moment
|
|
415
|
+
//
|
|
416
|
+
// Parameters: playID: The ID of the Play that the Moment references
|
|
417
|
+
// subeditionID: The ID of the subedition within Edition that the Moment references
|
|
418
|
+
//
|
|
419
|
+
// Pre-Conditions:
|
|
420
|
+
// The Play must exist in the Set and be allowed to mint new Moments
|
|
421
|
+
//
|
|
422
|
+
// Returns: The NFT that was minted
|
|
423
|
+
//
|
|
424
|
+
pub fun mintMomentWithSubedition(playID: UInt32, subeditionID: UInt32): @NFT {
|
|
425
|
+
pre {
|
|
426
|
+
self.retired[playID] != nil: "Cannot mint the moment: This play doesn't exist."
|
|
427
|
+
!self.retired[playID]!: "Cannot mint the moment from this play: This play has been retired."
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Gets the number of Moments that have been minted for this subedition
|
|
431
|
+
// to use as this Moment's serial number
|
|
432
|
+
let subeditionRef = TopShot.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
|
|
433
|
+
?? panic("No subedition admin resource in storage")
|
|
434
|
+
|
|
435
|
+
let numInSubedition = subeditionRef.getNumberMintedPerSubedition(setID: self.setID,
|
|
436
|
+
playID: playID,
|
|
437
|
+
subeditionID: subeditionID)
|
|
438
|
+
|
|
439
|
+
// Mint the new moment
|
|
440
|
+
let newMoment: @NFT <- create NFT(serialNumber: numInSubedition + UInt32(1),
|
|
441
|
+
playID: playID,
|
|
442
|
+
setID: self.setID,
|
|
443
|
+
subeditionID: subeditionID)
|
|
444
|
+
|
|
445
|
+
// Increment the count of Moments minted for this subedition
|
|
446
|
+
subeditionRef.addToNumberMintedPerSubedition(setID: self.setID,
|
|
447
|
+
playID: playID,
|
|
448
|
+
subeditionID: subeditionID)
|
|
449
|
+
|
|
450
|
+
subeditionRef.setMomentsSubedition(nftID: newMoment.id, subeditionID: subeditionID, setID: self.setID, playID: playID)
|
|
451
|
+
|
|
452
|
+
self.numberMintedPerPlay[playID] = self.numberMintedPerPlay[playID]! + UInt32(1)
|
|
453
|
+
|
|
454
|
+
return <-newMoment
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// batchMintMomentWithSubedition mints an arbitrary quantity of Moments with subedition
|
|
458
|
+
// and returns them as a Collection
|
|
459
|
+
//
|
|
460
|
+
// Parameters: playID: the ID of the Play that the Moments are minted for
|
|
461
|
+
// quantity: The quantity of Moments to be minted
|
|
462
|
+
// subeditionID: The ID of the subedition within Edition that the Moments references
|
|
463
|
+
//
|
|
464
|
+
// Returns: Collection object that contains all the Moments that were minted
|
|
465
|
+
//
|
|
466
|
+
pub fun batchMintMomentWithSubedition(playID: UInt32, quantity: UInt64, subeditionID: UInt32): @Collection {
|
|
467
|
+
let newCollection <- create Collection()
|
|
468
|
+
|
|
469
|
+
var i: UInt64 = 0
|
|
470
|
+
while i < quantity {
|
|
471
|
+
newCollection.deposit(token: <-self.mintMomentWithSubedition(playID: playID,
|
|
472
|
+
subeditionID: subeditionID))
|
|
473
|
+
i = i + UInt64(1)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return <-newCollection
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
pub fun getPlays(): [UInt32] {
|
|
480
|
+
return self.plays
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
pub fun getRetired(): {UInt32: Bool} {
|
|
484
|
+
return self.retired
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
pub fun getNumMintedPerPlay(): {UInt32: UInt32} {
|
|
488
|
+
return self.numberMintedPerPlay
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Struct that contains all of the important data about a set
|
|
493
|
+
// Can be easily queried by instantiating the `QuerySetData` object
|
|
494
|
+
// with the desired set ID
|
|
495
|
+
// let setData = TopShot.QuerySetData(setID: 12)
|
|
496
|
+
//
|
|
497
|
+
pub struct QuerySetData {
|
|
498
|
+
pub let setID: UInt32
|
|
499
|
+
pub let name: String
|
|
500
|
+
pub let series: UInt32
|
|
501
|
+
access(self) var plays: [UInt32]
|
|
502
|
+
access(self) var retired: {UInt32: Bool}
|
|
503
|
+
pub var locked: Bool
|
|
504
|
+
access(self) var numberMintedPerPlay: {UInt32: UInt32}
|
|
505
|
+
|
|
506
|
+
init(setID: UInt32) {
|
|
507
|
+
pre {
|
|
508
|
+
TopShot.sets[setID] != nil: "The set with the provided ID does not exist"
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
let set = (&TopShot.sets[setID] as &Set?)!
|
|
512
|
+
let setData = TopShot.setDatas[setID]!
|
|
513
|
+
|
|
514
|
+
self.setID = setID
|
|
515
|
+
self.name = setData.name
|
|
516
|
+
self.series = setData.series
|
|
517
|
+
self.plays = set.plays
|
|
518
|
+
self.retired = set.retired
|
|
519
|
+
self.locked = set.locked
|
|
520
|
+
self.numberMintedPerPlay = set.numberMintedPerPlay
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
pub fun getPlays(): [UInt32] {
|
|
524
|
+
return self.plays
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
pub fun getRetired(): {UInt32: Bool} {
|
|
528
|
+
return self.retired
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
pub fun getNumberMintedPerPlay(): {UInt32: UInt32} {
|
|
532
|
+
return self.numberMintedPerPlay
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
pub struct MomentData {
|
|
537
|
+
|
|
538
|
+
// The ID of the Set that the Moment comes from
|
|
539
|
+
pub let setID: UInt32
|
|
540
|
+
|
|
541
|
+
// The ID of the Play that the Moment references
|
|
542
|
+
pub let playID: UInt32
|
|
543
|
+
|
|
544
|
+
// The place in the edition that this Moment was minted
|
|
545
|
+
// Otherwise know as the serial number
|
|
546
|
+
pub let serialNumber: UInt32
|
|
547
|
+
|
|
548
|
+
init(setID: UInt32, playID: UInt32, serialNumber: UInt32) {
|
|
549
|
+
self.setID = setID
|
|
550
|
+
self.playID = playID
|
|
551
|
+
self.serialNumber = serialNumber
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// This is an implementation of a custom metadata view for Top Shot.
|
|
557
|
+
// This view contains the play metadata.
|
|
558
|
+
//
|
|
559
|
+
pub struct TopShotMomentMetadataView {
|
|
560
|
+
|
|
561
|
+
pub let fullName: String?
|
|
562
|
+
pub let firstName: String?
|
|
563
|
+
pub let lastName: String?
|
|
564
|
+
pub let birthdate: String?
|
|
565
|
+
pub let birthplace: String?
|
|
566
|
+
pub let jerseyNumber: String?
|
|
567
|
+
pub let draftTeam: String?
|
|
568
|
+
pub let draftYear: String?
|
|
569
|
+
pub let draftSelection: String?
|
|
570
|
+
pub let draftRound: String?
|
|
571
|
+
pub let teamAtMomentNBAID: String?
|
|
572
|
+
pub let teamAtMoment: String?
|
|
573
|
+
pub let primaryPosition: String?
|
|
574
|
+
pub let height: String?
|
|
575
|
+
pub let weight: String?
|
|
576
|
+
pub let totalYearsExperience: String?
|
|
577
|
+
pub let nbaSeason: String?
|
|
578
|
+
pub let dateOfMoment: String?
|
|
579
|
+
pub let playCategory: String?
|
|
580
|
+
pub let playType: String?
|
|
581
|
+
pub let homeTeamName: String?
|
|
582
|
+
pub let awayTeamName: String?
|
|
583
|
+
pub let homeTeamScore: String?
|
|
584
|
+
pub let awayTeamScore: String?
|
|
585
|
+
pub let seriesNumber: UInt32?
|
|
586
|
+
pub let setName: String?
|
|
587
|
+
pub let serialNumber: UInt32
|
|
588
|
+
pub let playID: UInt32
|
|
589
|
+
pub let setID: UInt32
|
|
590
|
+
pub let numMomentsInEdition: UInt32?
|
|
591
|
+
|
|
592
|
+
init(
|
|
593
|
+
fullName: String?,
|
|
594
|
+
firstName: String?,
|
|
595
|
+
lastName: String?,
|
|
596
|
+
birthdate: String?,
|
|
597
|
+
birthplace: String?,
|
|
598
|
+
jerseyNumber: String?,
|
|
599
|
+
draftTeam: String?,
|
|
600
|
+
draftYear: String?,
|
|
601
|
+
draftSelection: String?,
|
|
602
|
+
draftRound: String?,
|
|
603
|
+
teamAtMomentNBAID: String?,
|
|
604
|
+
teamAtMoment: String?,
|
|
605
|
+
primaryPosition: String?,
|
|
606
|
+
height: String?,
|
|
607
|
+
weight: String?,
|
|
608
|
+
totalYearsExperience: String?,
|
|
609
|
+
nbaSeason: String?,
|
|
610
|
+
dateOfMoment: String?,
|
|
611
|
+
playCategory: String?,
|
|
612
|
+
playType: String?,
|
|
613
|
+
homeTeamName: String?,
|
|
614
|
+
awayTeamName: String?,
|
|
615
|
+
homeTeamScore: String?,
|
|
616
|
+
awayTeamScore: String?,
|
|
617
|
+
seriesNumber: UInt32?,
|
|
618
|
+
setName: String?,
|
|
619
|
+
serialNumber: UInt32,
|
|
620
|
+
playID: UInt32,
|
|
621
|
+
setID: UInt32,
|
|
622
|
+
numMomentsInEdition: UInt32?
|
|
623
|
+
) {
|
|
624
|
+
self.fullName = fullName
|
|
625
|
+
self.firstName = firstName
|
|
626
|
+
self.lastName = lastName
|
|
627
|
+
self.birthdate = birthdate
|
|
628
|
+
self.birthplace = birthplace
|
|
629
|
+
self.jerseyNumber = jerseyNumber
|
|
630
|
+
self.draftTeam = draftTeam
|
|
631
|
+
self.draftYear = draftYear
|
|
632
|
+
self.draftSelection = draftSelection
|
|
633
|
+
self.draftRound = draftRound
|
|
634
|
+
self.teamAtMomentNBAID = teamAtMomentNBAID
|
|
635
|
+
self.teamAtMoment = teamAtMoment
|
|
636
|
+
self.primaryPosition = primaryPosition
|
|
637
|
+
self.height = height
|
|
638
|
+
self.weight = weight
|
|
639
|
+
self.totalYearsExperience = totalYearsExperience
|
|
640
|
+
self.nbaSeason = nbaSeason
|
|
641
|
+
self.dateOfMoment= dateOfMoment
|
|
642
|
+
self.playCategory = playCategory
|
|
643
|
+
self.playType = playType
|
|
644
|
+
self.homeTeamName = homeTeamName
|
|
645
|
+
self.awayTeamName = awayTeamName
|
|
646
|
+
self.homeTeamScore = homeTeamScore
|
|
647
|
+
self.awayTeamScore = awayTeamScore
|
|
648
|
+
self.seriesNumber = seriesNumber
|
|
649
|
+
self.setName = setName
|
|
650
|
+
self.serialNumber = serialNumber
|
|
651
|
+
self.playID = playID
|
|
652
|
+
self.setID = setID
|
|
653
|
+
self.numMomentsInEdition = numMomentsInEdition
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// The resource that represents the Moment NFTs
|
|
658
|
+
//
|
|
659
|
+
pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
|
|
660
|
+
|
|
661
|
+
// Global unique moment ID
|
|
662
|
+
pub let id: UInt64
|
|
663
|
+
|
|
664
|
+
// Struct of Moment metadata
|
|
665
|
+
pub let data: MomentData
|
|
666
|
+
|
|
667
|
+
init(serialNumber: UInt32, playID: UInt32, setID: UInt32, subeditionID: UInt32) {
|
|
668
|
+
// Increment the global Moment IDs
|
|
669
|
+
TopShot.totalSupply = TopShot.totalSupply + UInt64(1)
|
|
670
|
+
|
|
671
|
+
self.id = TopShot.totalSupply
|
|
672
|
+
|
|
673
|
+
// Set the metadata struct
|
|
674
|
+
self.data = MomentData(setID: setID, playID: playID, serialNumber: serialNumber)
|
|
675
|
+
|
|
676
|
+
emit MomentMinted(momentID: self.id,
|
|
677
|
+
playID: playID,
|
|
678
|
+
setID: self.data.setID,
|
|
679
|
+
serialNumber: self.data.serialNumber,
|
|
680
|
+
subeditionID: subeditionID)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// If the Moment is destroyed, emit an event to indicate
|
|
684
|
+
// to outside ovbservers that it has been destroyed
|
|
685
|
+
destroy() {
|
|
686
|
+
emit MomentDestroyed(id: self.id)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
pub fun name(): String {
|
|
690
|
+
let fullName: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName") ?? ""
|
|
691
|
+
let playType: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType") ?? ""
|
|
692
|
+
return fullName
|
|
693
|
+
.concat(" ")
|
|
694
|
+
.concat(playType)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
access(self) fun buildDescString(): String {
|
|
698
|
+
let setName: String = TopShot.getSetName(setID: self.data.setID) ?? ""
|
|
699
|
+
let serialNumber: String = self.data.serialNumber.toString()
|
|
700
|
+
let seriesNumber: String = TopShot.getSetSeries(setID: self.data.setID)?.toString() ?? ""
|
|
701
|
+
return "A series "
|
|
702
|
+
.concat(seriesNumber)
|
|
703
|
+
.concat(" ")
|
|
704
|
+
.concat(setName)
|
|
705
|
+
.concat(" moment with serial number ")
|
|
706
|
+
.concat(serialNumber)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/// The description of the Moment. If Tagline property of the play is empty, compose it using the buildDescString function
|
|
710
|
+
/// If the Tagline property is not empty, use that as the description
|
|
711
|
+
pub fun description(): String {
|
|
712
|
+
let playDesc: String = TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Tagline") ?? ""
|
|
713
|
+
|
|
714
|
+
return playDesc.length > 0 ? playDesc : self.buildDescString()
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// All supported metadata views for the Moment including the Core NFT Views
|
|
718
|
+
pub fun getViews(): [Type] {
|
|
719
|
+
return [
|
|
720
|
+
Type<MetadataViews.Display>(),
|
|
721
|
+
Type<TopShotMomentMetadataView>(),
|
|
722
|
+
Type<MetadataViews.Royalties>(),
|
|
723
|
+
Type<MetadataViews.Editions>(),
|
|
724
|
+
Type<MetadataViews.ExternalURL>(),
|
|
725
|
+
Type<MetadataViews.NFTCollectionData>(),
|
|
726
|
+
Type<MetadataViews.NFTCollectionDisplay>(),
|
|
727
|
+
Type<MetadataViews.Serial>(),
|
|
728
|
+
Type<MetadataViews.Traits>(),
|
|
729
|
+
Type<MetadataViews.Medias>()
|
|
730
|
+
]
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
pub fun resolveView(_ view: Type): AnyStruct? {
|
|
736
|
+
switch view {
|
|
737
|
+
case Type<MetadataViews.Display>():
|
|
738
|
+
return MetadataViews.Display(
|
|
739
|
+
name: self.name(),
|
|
740
|
+
description: self.description(),
|
|
741
|
+
thumbnail: MetadataViews.HTTPFile(url: self.thumbnail())
|
|
742
|
+
)
|
|
743
|
+
// Custom metadata view unique to TopShot Moments
|
|
744
|
+
case Type<TopShotMomentMetadataView>():
|
|
745
|
+
return TopShotMomentMetadataView(
|
|
746
|
+
fullName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FullName"),
|
|
747
|
+
firstName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "FirstName"),
|
|
748
|
+
lastName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "LastName"),
|
|
749
|
+
birthdate: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Birthdate"),
|
|
750
|
+
birthplace: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Birthplace"),
|
|
751
|
+
jerseyNumber: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "JerseyNumber"),
|
|
752
|
+
draftTeam: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftTeam"),
|
|
753
|
+
draftYear: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftYear"),
|
|
754
|
+
draftSelection: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftSelection"),
|
|
755
|
+
draftRound: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DraftRound"),
|
|
756
|
+
teamAtMomentNBAID: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TeamAtMomentNBAID"),
|
|
757
|
+
teamAtMoment: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TeamAtMoment"),
|
|
758
|
+
primaryPosition: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PrimaryPosition"),
|
|
759
|
+
height: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Height"),
|
|
760
|
+
weight: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "Weight"),
|
|
761
|
+
totalYearsExperience: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "TotalYearsExperience"),
|
|
762
|
+
nbaSeason: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "NbaSeason"),
|
|
763
|
+
dateOfMoment: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "DateOfMoment"),
|
|
764
|
+
playCategory: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayCategory"),
|
|
765
|
+
playType: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "PlayType"),
|
|
766
|
+
homeTeamName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "HomeTeamName"),
|
|
767
|
+
awayTeamName: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "AwayTeamName"),
|
|
768
|
+
homeTeamScore: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "HomeTeamScore"),
|
|
769
|
+
awayTeamScore: TopShot.getPlayMetaDataByField(playID: self.data.playID, field: "AwayTeamScore"),
|
|
770
|
+
seriesNumber: TopShot.getSetSeries(setID: self.data.setID),
|
|
771
|
+
setName: TopShot.getSetName(setID: self.data.setID),
|
|
772
|
+
serialNumber: self.data.serialNumber,
|
|
773
|
+
playID: self.data.playID,
|
|
774
|
+
setID: self.data.setID,
|
|
775
|
+
numMomentsInEdition: TopShot.getNumMomentsInEdition(setID: self.data.setID, playID: self.data.playID)
|
|
776
|
+
)
|
|
777
|
+
case Type<MetadataViews.Editions>():
|
|
778
|
+
let name = self.getEditionName()
|
|
779
|
+
let max = TopShot.getNumMomentsInEdition(setID: self.data.setID, playID: self.data.playID) ?? 0
|
|
780
|
+
let editionInfo = MetadataViews.Edition(name: name, number: UInt64(self.data.serialNumber), max: max > 0 ? UInt64(max) : nil)
|
|
781
|
+
let editionList: [MetadataViews.Edition] = [editionInfo]
|
|
782
|
+
return MetadataViews.Editions(
|
|
783
|
+
editionList
|
|
784
|
+
)
|
|
785
|
+
case Type<MetadataViews.Serial>():
|
|
786
|
+
return MetadataViews.Serial(
|
|
787
|
+
UInt64(self.data.serialNumber)
|
|
788
|
+
)
|
|
789
|
+
case Type<MetadataViews.Royalties>():
|
|
790
|
+
let royaltyReceiver: Capability<&{FungibleToken.Receiver}> =
|
|
791
|
+
getAccount(TopShot.RoyaltyAddress()).getCapability<&AnyResource{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath())
|
|
792
|
+
return MetadataViews.Royalties(
|
|
793
|
+
royalties: [
|
|
794
|
+
MetadataViews.Royalty(
|
|
795
|
+
receiver: royaltyReceiver,
|
|
796
|
+
cut: 0.05,
|
|
797
|
+
description: "NBATopShot marketplace royalty"
|
|
798
|
+
)
|
|
799
|
+
]
|
|
800
|
+
)
|
|
801
|
+
case Type<MetadataViews.ExternalURL>():
|
|
802
|
+
return MetadataViews.ExternalURL(self.getMomentURL())
|
|
803
|
+
case Type<MetadataViews.NFTCollectionData>():
|
|
804
|
+
return MetadataViews.NFTCollectionData(
|
|
805
|
+
storagePath: /storage/MomentCollection,
|
|
806
|
+
publicPath: /public/MomentCollection,
|
|
807
|
+
providerPath: /private/MomentCollection,
|
|
808
|
+
publicCollection: Type<&TopShot.Collection{TopShot.MomentCollectionPublic}>(),
|
|
809
|
+
publicLinkedType: Type<&TopShot.Collection{TopShot.MomentCollectionPublic,NonFungibleToken.Receiver,NonFungibleToken.CollectionPublic,MetadataViews.ResolverCollection}>(),
|
|
810
|
+
providerLinkedType: Type<&TopShot.Collection{NonFungibleToken.Provider,TopShot.MomentCollectionPublic,NonFungibleToken.Receiver,NonFungibleToken.CollectionPublic,MetadataViews.ResolverCollection}>(),
|
|
811
|
+
createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
|
|
812
|
+
return <-TopShot.createEmptyCollection()
|
|
813
|
+
})
|
|
814
|
+
)
|
|
815
|
+
case Type<MetadataViews.NFTCollectionDisplay>():
|
|
816
|
+
let bannerImage = MetadataViews.Media(
|
|
817
|
+
file: MetadataViews.HTTPFile(
|
|
818
|
+
url: "https://nbatopshot.com/static/img/top-shot-logo-horizontal-white.svg"
|
|
819
|
+
),
|
|
820
|
+
mediaType: "image/svg+xml"
|
|
821
|
+
)
|
|
822
|
+
let squareImage = MetadataViews.Media(
|
|
823
|
+
file: MetadataViews.HTTPFile(
|
|
824
|
+
url: "https://nbatopshot.com/static/img/og/og.png"
|
|
825
|
+
),
|
|
826
|
+
mediaType: "image/png"
|
|
827
|
+
)
|
|
828
|
+
return MetadataViews.NFTCollectionDisplay(
|
|
829
|
+
name: "NBA-Top-Shot",
|
|
830
|
+
description: "NBA Top Shot is your chance to own, sell, and trade official digital collectibles of the NBA and WNBA's greatest plays and players",
|
|
831
|
+
externalURL: MetadataViews.ExternalURL("https://nbatopshot.com"),
|
|
832
|
+
squareImage: squareImage,
|
|
833
|
+
bannerImage: bannerImage,
|
|
834
|
+
socials: {
|
|
835
|
+
"twitter": MetadataViews.ExternalURL("https://twitter.com/nbatopshot"),
|
|
836
|
+
"discord": MetadataViews.ExternalURL("https://discord.com/invite/nbatopshot"),
|
|
837
|
+
"instagram": MetadataViews.ExternalURL("https://www.instagram.com/nbatopshot")
|
|
838
|
+
}
|
|
839
|
+
)
|
|
840
|
+
case Type<MetadataViews.Traits>():
|
|
841
|
+
// sports radar team id
|
|
842
|
+
let excludedNames: [String] = ["TeamAtMomentNBAID"]
|
|
843
|
+
// non play specific traits
|
|
844
|
+
let traitDictionary: {String: AnyStruct} = {
|
|
845
|
+
"SeriesNumber": TopShot.getSetSeries(setID: self.data.setID),
|
|
846
|
+
"SetName": TopShot.getSetName(setID: self.data.setID),
|
|
847
|
+
"SerialNumber": self.data.serialNumber
|
|
848
|
+
}
|
|
849
|
+
// add play specific data
|
|
850
|
+
let fullDictionary = self.mapPlayData(dict: traitDictionary)
|
|
851
|
+
return MetadataViews.dictToTraits(dict: fullDictionary, excludedNames: excludedNames)
|
|
852
|
+
case Type<MetadataViews.Medias>():
|
|
853
|
+
return MetadataViews.Medias(
|
|
854
|
+
items: [
|
|
855
|
+
MetadataViews.Media(
|
|
856
|
+
file: MetadataViews.HTTPFile(
|
|
857
|
+
url: self.mediumimage()
|
|
858
|
+
),
|
|
859
|
+
mediaType: "image/jpeg"
|
|
860
|
+
),
|
|
861
|
+
MetadataViews.Media(
|
|
862
|
+
file: MetadataViews.HTTPFile(
|
|
863
|
+
url: self.video()
|
|
864
|
+
),
|
|
865
|
+
mediaType: "video/mp4"
|
|
866
|
+
)
|
|
867
|
+
]
|
|
868
|
+
)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return nil
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Functions used for computing MetadataViews
|
|
875
|
+
|
|
876
|
+
// mapPlayData helps build our trait map from play metadata
|
|
877
|
+
// Returns: The trait map with all non-empty fields from play data added
|
|
878
|
+
pub fun mapPlayData(dict: {String: AnyStruct}) : {String: AnyStruct} {
|
|
879
|
+
let playMetadata = TopShot.getPlayMetaData(playID: self.data.playID) ?? {}
|
|
880
|
+
for name in playMetadata.keys {
|
|
881
|
+
let value = playMetadata[name] ?? ""
|
|
882
|
+
if value != "" {
|
|
883
|
+
dict.insert(key: name, value)
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return dict
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// getMomentURL
|
|
890
|
+
// Returns: The computed external url of the moment
|
|
891
|
+
pub fun getMomentURL(): String {
|
|
892
|
+
return "https://nbatopshot.com/moment/".concat(self.id.toString())
|
|
893
|
+
}
|
|
894
|
+
// getEditionName Moment's edition name is a combination of the Moment's setName and playID
|
|
895
|
+
// `setName: #playID`
|
|
896
|
+
pub fun getEditionName() : String {
|
|
897
|
+
let setName: String = TopShot.getSetName(setID: self.data.setID) ?? ""
|
|
898
|
+
let editionName = setName.concat(": #").concat(self.data.playID.toString())
|
|
899
|
+
return editionName
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
pub fun assetPath(): String {
|
|
903
|
+
return "https://assets.nbatopshot.com/media/".concat(self.id.toString())
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// returns a url to display an medium sized image
|
|
907
|
+
pub fun mediumimage(): String {
|
|
908
|
+
let url = self.assetPath().concat("?width=512")
|
|
909
|
+
return self.appendOptionalParams(url: url, firstDelim: "&")
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// a url to display a thumbnail associated with the moment
|
|
913
|
+
pub fun thumbnail(): String {
|
|
914
|
+
let url = self.assetPath().concat("?width=256")
|
|
915
|
+
return self.appendOptionalParams(url: url, firstDelim: "&")
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// a url to display a video associated with the moment
|
|
919
|
+
pub fun video(): String {
|
|
920
|
+
let url = self.assetPath().concat("/video")
|
|
921
|
+
return self.appendOptionalParams(url: url, firstDelim: "?")
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// appends and optional network param needed to resolve the media
|
|
925
|
+
pub fun appendOptionalParams(url: String, firstDelim: String): String {
|
|
926
|
+
if (TopShot.Network() == "testnet") {
|
|
927
|
+
return url.concat(firstDelim).concat("testnet")
|
|
928
|
+
}
|
|
929
|
+
return url
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Admin is a special authorization resource that
|
|
934
|
+
// allows the owner to perform important functions to modify the
|
|
935
|
+
// various aspects of the Plays, Sets, and Moments
|
|
936
|
+
//
|
|
937
|
+
pub resource Admin {
|
|
938
|
+
|
|
939
|
+
// createPlay creates a new Play struct
|
|
940
|
+
// and stores it in the Plays dictionary in the TopShot smart contract
|
|
941
|
+
//
|
|
942
|
+
// Parameters: metadata: A dictionary mapping metadata titles to their data
|
|
943
|
+
// example: {"Player Name": "Kevin Durant", "Height": "7 feet"}
|
|
944
|
+
// (because we all know Kevin Durant is not 6'9")
|
|
945
|
+
//
|
|
946
|
+
// Returns: the ID of the new Play object
|
|
947
|
+
//
|
|
948
|
+
pub fun createPlay(metadata: {String: String}): UInt32 {
|
|
949
|
+
// Create the new Play
|
|
950
|
+
var newPlay = Play(metadata: metadata)
|
|
951
|
+
let newID = newPlay.playID
|
|
952
|
+
|
|
953
|
+
// Increment the ID so that it isn't used again
|
|
954
|
+
TopShot.nextPlayID = TopShot.nextPlayID + UInt32(1)
|
|
955
|
+
|
|
956
|
+
emit PlayCreated(id: newPlay.playID, metadata: metadata)
|
|
957
|
+
|
|
958
|
+
// Store it in the contract storage
|
|
959
|
+
TopShot.playDatas[newID] = newPlay
|
|
960
|
+
|
|
961
|
+
return newID
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/// Temporarily enabled so the description of the play can be backfilled
|
|
965
|
+
/// Parameters: playID: The ID of the play to update
|
|
966
|
+
/// tagline: A string to be used as the tagline for the play
|
|
967
|
+
/// Returns: The ID of the play
|
|
968
|
+
pub fun updatePlayTagline(playID: UInt32, tagline: String): UInt32 {
|
|
969
|
+
let tmpPlay = TopShot.playDatas[playID] ?? panic("playID does not exist")
|
|
970
|
+
tmpPlay.updateTagline(tagline: tagline)
|
|
971
|
+
return playID
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// createSet creates a new Set resource and stores it
|
|
975
|
+
// in the sets mapping in the TopShot contract
|
|
976
|
+
//
|
|
977
|
+
// Parameters: name: The name of the Set
|
|
978
|
+
//
|
|
979
|
+
// Returns: The ID of the created set
|
|
980
|
+
pub fun createSet(name: String): UInt32 {
|
|
981
|
+
|
|
982
|
+
// Create the new Set
|
|
983
|
+
var newSet <- create Set(name: name)
|
|
984
|
+
|
|
985
|
+
// Increment the setID so that it isn't used again
|
|
986
|
+
TopShot.nextSetID = TopShot.nextSetID + UInt32(1)
|
|
987
|
+
|
|
988
|
+
let newID = newSet.setID
|
|
989
|
+
|
|
990
|
+
emit SetCreated(setID: newSet.setID, series: TopShot.currentSeries)
|
|
991
|
+
|
|
992
|
+
// Store it in the sets mapping field
|
|
993
|
+
TopShot.sets[newID] <-! newSet
|
|
994
|
+
|
|
995
|
+
return newID
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// borrowSet returns a reference to a set in the TopShot
|
|
999
|
+
// contract so that the admin can call methods on it
|
|
1000
|
+
//
|
|
1001
|
+
// Parameters: setID: The ID of the Set that you want to
|
|
1002
|
+
// get a reference to
|
|
1003
|
+
//
|
|
1004
|
+
// Returns: A reference to the Set with all of the fields
|
|
1005
|
+
// and methods exposed
|
|
1006
|
+
//
|
|
1007
|
+
pub fun borrowSet(setID: UInt32): &Set {
|
|
1008
|
+
pre {
|
|
1009
|
+
TopShot.sets[setID] != nil: "Cannot borrow Set: The Set doesn't exist"
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Get a reference to the Set and return it
|
|
1013
|
+
// use `&` to indicate the reference to the object and type
|
|
1014
|
+
return (&TopShot.sets[setID] as &Set?)!
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// startNewSeries ends the current series by incrementing
|
|
1018
|
+
// the series number, meaning that Moments minted after this
|
|
1019
|
+
// will use the new series number
|
|
1020
|
+
//
|
|
1021
|
+
// Returns: The new series number
|
|
1022
|
+
//
|
|
1023
|
+
pub fun startNewSeries(): UInt32 {
|
|
1024
|
+
// End the current series and start a new one
|
|
1025
|
+
// by incrementing the TopShot series number
|
|
1026
|
+
TopShot.currentSeries = TopShot.currentSeries + UInt32(1)
|
|
1027
|
+
|
|
1028
|
+
emit NewSeriesStarted(newCurrentSeries: TopShot.currentSeries)
|
|
1029
|
+
|
|
1030
|
+
return TopShot.currentSeries
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// createSubeditionResource creates new SubeditionMap resource that
|
|
1034
|
+
// will be used to mint Moments with Subeditions
|
|
1035
|
+
pub fun createSubeditionAdminResource() {
|
|
1036
|
+
TopShot.account.save<@SubeditionAdmin>(<- create SubeditionAdmin(), to: TopShot.SubeditionAdminStoragePath())
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// setMomentsSubedition saves which Subedition the Moment belongs to
|
|
1040
|
+
//
|
|
1041
|
+
// Parameters: nftID: The ID of the NFT
|
|
1042
|
+
// subeditionID: The ID of the Subedition the Moment belongs to
|
|
1043
|
+
// setID: The ID of the Set that the Moment references
|
|
1044
|
+
// playID: The ID of the Play that the Moment references
|
|
1045
|
+
//
|
|
1046
|
+
pub fun setMomentsSubedition(nftID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32) {
|
|
1047
|
+
let subeditionAdmin = TopShot.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
|
|
1048
|
+
?? panic("No subedition admin resource in storage")
|
|
1049
|
+
|
|
1050
|
+
subeditionAdmin.setMomentsSubedition(nftID: nftID, subeditionID: subeditionID, setID: setID, playID: playID)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// createSubedition creates a new Subedition struct
|
|
1054
|
+
// and stores it in the Subeditions dictionary in the SubeditionAdmin resource
|
|
1055
|
+
//
|
|
1056
|
+
// Parameters: name: The name of the Subedition
|
|
1057
|
+
// metadata: A dictionary mapping metadata titles to their data
|
|
1058
|
+
//
|
|
1059
|
+
// Returns: the ID of the new Subedition object
|
|
1060
|
+
//
|
|
1061
|
+
pub fun createSubedition(name:String, metadata:{String:String}): UInt32 {
|
|
1062
|
+
let subeditionAdmin = TopShot.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
|
|
1063
|
+
?? panic("No subedition admin resource in storage")
|
|
1064
|
+
|
|
1065
|
+
return subeditionAdmin.createSubedition(name:name, metadata:metadata)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// createNewAdmin creates a new Admin resource
|
|
1069
|
+
//
|
|
1070
|
+
pub fun createNewAdmin(): @Admin {
|
|
1071
|
+
return <-create Admin()
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// This is the interface that users can cast their Moment Collection as
|
|
1076
|
+
// to allow others to deposit Moments into their Collection. It also allows for reading
|
|
1077
|
+
// the IDs of Moments in the Collection.
|
|
1078
|
+
pub resource interface MomentCollectionPublic {
|
|
1079
|
+
pub fun deposit(token: @NonFungibleToken.NFT)
|
|
1080
|
+
pub fun batchDeposit(tokens: @NonFungibleToken.Collection)
|
|
1081
|
+
pub fun getIDs(): [UInt64]
|
|
1082
|
+
pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
|
|
1083
|
+
pub fun borrowMoment(id: UInt64): &TopShot.NFT? {
|
|
1084
|
+
// If the result isn't nil, the id of the returned reference
|
|
1085
|
+
// should be the same as the argument to the function
|
|
1086
|
+
post {
|
|
1087
|
+
(result == nil) || (result?.id == id):
|
|
1088
|
+
"Cannot borrow Moment reference: The ID of the returned reference is incorrect"
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Collection is a resource that every user who owns NFTs
|
|
1094
|
+
// will store in their account to manage their NFTS
|
|
1095
|
+
//
|
|
1096
|
+
pub resource Collection: MomentCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
|
|
1097
|
+
// Dictionary of Moment conforming tokens
|
|
1098
|
+
// NFT is a resource type with a UInt64 ID field
|
|
1099
|
+
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
|
|
1100
|
+
|
|
1101
|
+
init() {
|
|
1102
|
+
self.ownedNFTs <- {}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// withdraw removes an Moment from the Collection and moves it to the caller
|
|
1106
|
+
//
|
|
1107
|
+
// Parameters: withdrawID: The ID of the NFT
|
|
1108
|
+
// that is to be removed from the Collection
|
|
1109
|
+
//
|
|
1110
|
+
// returns: @NonFungibleToken.NFT the token that was withdrawn
|
|
1111
|
+
pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
|
|
1112
|
+
|
|
1113
|
+
// Borrow nft and check if locked
|
|
1114
|
+
let nft = self.borrowNFT(id: withdrawID)
|
|
1115
|
+
if TopShotLocking.isLocked(nftRef: nft) {
|
|
1116
|
+
panic("Cannot withdraw: Moment is locked")
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Remove the nft from the Collection
|
|
1120
|
+
let token <- self.ownedNFTs.remove(key: withdrawID)
|
|
1121
|
+
?? panic("Cannot withdraw: Moment does not exist in the collection")
|
|
1122
|
+
|
|
1123
|
+
emit Withdraw(id: token.id, from: self.owner?.address)
|
|
1124
|
+
|
|
1125
|
+
// Return the withdrawn token
|
|
1126
|
+
return <-token
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// batchWithdraw withdraws multiple tokens and returns them as a Collection
|
|
1130
|
+
//
|
|
1131
|
+
// Parameters: ids: An array of IDs to withdraw
|
|
1132
|
+
//
|
|
1133
|
+
// Returns: @NonFungibleToken.Collection: A collection that contains
|
|
1134
|
+
// the withdrawn moments
|
|
1135
|
+
//
|
|
1136
|
+
pub fun batchWithdraw(ids: [UInt64]): @NonFungibleToken.Collection {
|
|
1137
|
+
// Create a new empty Collection
|
|
1138
|
+
var batchCollection <- create Collection()
|
|
1139
|
+
|
|
1140
|
+
// Iterate through the ids and withdraw them from the Collection
|
|
1141
|
+
for id in ids {
|
|
1142
|
+
batchCollection.deposit(token: <-self.withdraw(withdrawID: id))
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Return the withdrawn tokens
|
|
1146
|
+
return <-batchCollection
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// deposit takes a Moment and adds it to the Collections dictionary
|
|
1150
|
+
//
|
|
1151
|
+
// Paramters: token: the NFT to be deposited in the collection
|
|
1152
|
+
//
|
|
1153
|
+
pub fun deposit(token: @NonFungibleToken.NFT) {
|
|
1154
|
+
|
|
1155
|
+
// Cast the deposited token as a TopShot NFT to make sure
|
|
1156
|
+
// it is the correct type
|
|
1157
|
+
let token <- token as! @TopShot.NFT
|
|
1158
|
+
|
|
1159
|
+
// Get the token's ID
|
|
1160
|
+
let id = token.id
|
|
1161
|
+
|
|
1162
|
+
// Add the new token to the dictionary
|
|
1163
|
+
let oldToken <- self.ownedNFTs[id] <- token
|
|
1164
|
+
|
|
1165
|
+
// Only emit a deposit event if the Collection
|
|
1166
|
+
// is in an account's storage
|
|
1167
|
+
if self.owner?.address != nil {
|
|
1168
|
+
emit Deposit(id: id, to: self.owner?.address)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Destroy the empty old token that was "removed"
|
|
1172
|
+
destroy oldToken
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// batchDeposit takes a Collection object as an argument
|
|
1176
|
+
// and deposits each contained NFT into this Collection
|
|
1177
|
+
pub fun batchDeposit(tokens: @NonFungibleToken.Collection) {
|
|
1178
|
+
|
|
1179
|
+
// Get an array of the IDs to be deposited
|
|
1180
|
+
let keys = tokens.getIDs()
|
|
1181
|
+
|
|
1182
|
+
// Iterate through the keys in the collection and deposit each one
|
|
1183
|
+
for key in keys {
|
|
1184
|
+
self.deposit(token: <-tokens.withdraw(withdrawID: key))
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Destroy the empty Collection
|
|
1188
|
+
destroy tokens
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// lock takes a token id and a duration in seconds and locks
|
|
1192
|
+
// the moment for that duration
|
|
1193
|
+
pub fun lock(id: UInt64, duration: UFix64) {
|
|
1194
|
+
// Remove the nft from the Collection
|
|
1195
|
+
let token <- self.ownedNFTs.remove(key: id)
|
|
1196
|
+
?? panic("Cannot lock: Moment does not exist in the collection")
|
|
1197
|
+
|
|
1198
|
+
// pass the token to the locking contract
|
|
1199
|
+
// store it again after it comes back
|
|
1200
|
+
let oldToken <- self.ownedNFTs[id] <- TopShotLocking.lockNFT(nft: <- token, duration: duration)
|
|
1201
|
+
|
|
1202
|
+
destroy oldToken
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// batchLock takes an array of token ids and a duration in seconds
|
|
1206
|
+
// it iterates through the ids and locks each for the specified duration
|
|
1207
|
+
pub fun batchLock(ids: [UInt64], duration: UFix64) {
|
|
1208
|
+
// Iterate through the ids and lock them
|
|
1209
|
+
for id in ids {
|
|
1210
|
+
self.lock(id: id, duration: duration)
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// unlock takes a token id and attempts to unlock it
|
|
1215
|
+
// TopShotLocking.unlockNFT contains business logic around unlock eligibility
|
|
1216
|
+
pub fun unlock(id: UInt64) {
|
|
1217
|
+
// Remove the nft from the Collection
|
|
1218
|
+
let token <- self.ownedNFTs.remove(key: id)
|
|
1219
|
+
?? panic("Cannot lock: Moment does not exist in the collection")
|
|
1220
|
+
|
|
1221
|
+
// Pass the token to the TopShotLocking contract then get it back
|
|
1222
|
+
// Store it back to the ownedNFTs dictionary
|
|
1223
|
+
let oldToken <- self.ownedNFTs[id] <- TopShotLocking.unlockNFT(nft: <- token)
|
|
1224
|
+
|
|
1225
|
+
destroy oldToken
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// batchUnlock takes an array of token ids
|
|
1229
|
+
// it iterates through the ids and unlocks each if they are eligible
|
|
1230
|
+
pub fun batchUnlock(ids: [UInt64]) {
|
|
1231
|
+
// Iterate through the ids and unlocks them
|
|
1232
|
+
for id in ids {
|
|
1233
|
+
self.unlock(id: id)
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// getIDs returns an array of the IDs that are in the Collection
|
|
1238
|
+
pub fun getIDs(): [UInt64] {
|
|
1239
|
+
return self.ownedNFTs.keys
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// borrowNFT Returns a borrowed reference to a Moment in the Collection
|
|
1243
|
+
// so that the caller can read its ID
|
|
1244
|
+
//
|
|
1245
|
+
// Parameters: id: The ID of the NFT to get the reference for
|
|
1246
|
+
//
|
|
1247
|
+
// Returns: A reference to the NFT
|
|
1248
|
+
//
|
|
1249
|
+
// Note: This only allows the caller to read the ID of the NFT,
|
|
1250
|
+
// not any topshot specific data. Please use borrowMoment to
|
|
1251
|
+
// read Moment data.
|
|
1252
|
+
//
|
|
1253
|
+
pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
|
|
1254
|
+
return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Safe way to borrow a reference to an NFT that does not panic
|
|
1258
|
+
// Also now part of the NonFungibleToken.PublicCollection interface
|
|
1259
|
+
//
|
|
1260
|
+
// Parameters: id: The ID of the NFT to get the reference for
|
|
1261
|
+
//
|
|
1262
|
+
// Returns: An optional reference to the desired NFT, will be nil if the passed ID does not exist
|
|
1263
|
+
pub fun borrowNFTSafe(id: UInt64): &NonFungibleToken.NFT? {
|
|
1264
|
+
if let nftRef = &self.ownedNFTs[id] as &NonFungibleToken.NFT? {
|
|
1265
|
+
return nftRef
|
|
1266
|
+
}
|
|
1267
|
+
return nil
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// borrowMoment returns a borrowed reference to a Moment
|
|
1271
|
+
// so that the caller can read data and call methods from it.
|
|
1272
|
+
// They can use this to read its setID, playID, serialNumber,
|
|
1273
|
+
// or any of the setData or Play data associated with it by
|
|
1274
|
+
// getting the setID or playID and reading those fields from
|
|
1275
|
+
// the smart contract.
|
|
1276
|
+
//
|
|
1277
|
+
// Parameters: id: The ID of the NFT to get the reference for
|
|
1278
|
+
//
|
|
1279
|
+
// Returns: A reference to the NFT
|
|
1280
|
+
pub fun borrowMoment(id: UInt64): &TopShot.NFT? {
|
|
1281
|
+
if self.ownedNFTs[id] != nil {
|
|
1282
|
+
let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
|
|
1283
|
+
return ref as! &TopShot.NFT
|
|
1284
|
+
} else {
|
|
1285
|
+
return nil
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
|
|
1290
|
+
let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
|
|
1291
|
+
let topShotNFT = nft as! &TopShot.NFT
|
|
1292
|
+
return topShotNFT as &AnyResource{MetadataViews.Resolver}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// If a transaction destroys the Collection object,
|
|
1296
|
+
// All the NFTs contained within are also destroyed!
|
|
1297
|
+
// Much like when Damian Lillard destroys the hopes and
|
|
1298
|
+
// dreams of the entire city of Houston.
|
|
1299
|
+
//
|
|
1300
|
+
destroy() {
|
|
1301
|
+
destroy self.ownedNFTs
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// -----------------------------------------------------------------------
|
|
1306
|
+
// TopShot contract-level function definitions
|
|
1307
|
+
// -----------------------------------------------------------------------
|
|
1308
|
+
|
|
1309
|
+
// createEmptyCollection creates a new, empty Collection object so that
|
|
1310
|
+
// a user can store it in their account storage.
|
|
1311
|
+
// Once they have a Collection in their storage, they are able to receive
|
|
1312
|
+
// Moments in transactions.
|
|
1313
|
+
//
|
|
1314
|
+
pub fun createEmptyCollection(): @NonFungibleToken.Collection {
|
|
1315
|
+
return <-create TopShot.Collection()
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// getAllPlays returns all the plays in topshot
|
|
1319
|
+
//
|
|
1320
|
+
// Returns: An array of all the plays that have been created
|
|
1321
|
+
pub fun getAllPlays(): [TopShot.Play] {
|
|
1322
|
+
return TopShot.playDatas.values
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// getPlayMetaData returns all the metadata associated with a specific Play
|
|
1326
|
+
//
|
|
1327
|
+
// Parameters: playID: The id of the Play that is being searched
|
|
1328
|
+
//
|
|
1329
|
+
// Returns: The metadata as a String to String mapping optional
|
|
1330
|
+
pub fun getPlayMetaData(playID: UInt32): {String: String}? {
|
|
1331
|
+
return self.playDatas[playID]?.metadata
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// getPlayMetaDataByField returns the metadata associated with a
|
|
1335
|
+
// specific field of the metadata
|
|
1336
|
+
// Ex: field: "Team" will return something
|
|
1337
|
+
// like "Memphis Grizzlies"
|
|
1338
|
+
//
|
|
1339
|
+
// Parameters: playID: The id of the Play that is being searched
|
|
1340
|
+
// field: The field to search for
|
|
1341
|
+
//
|
|
1342
|
+
// Returns: The metadata field as a String Optional
|
|
1343
|
+
pub fun getPlayMetaDataByField(playID: UInt32, field: String): String? {
|
|
1344
|
+
// Don't force a revert if the playID or field is invalid
|
|
1345
|
+
if let play = TopShot.playDatas[playID] {
|
|
1346
|
+
return play.metadata[field]
|
|
1347
|
+
} else {
|
|
1348
|
+
return nil
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// getSetData returns the data that the specified Set
|
|
1353
|
+
// is associated with.
|
|
1354
|
+
//
|
|
1355
|
+
// Parameters: setID: The id of the Set that is being searched
|
|
1356
|
+
//
|
|
1357
|
+
// Returns: The QuerySetData struct that has all the important information about the set
|
|
1358
|
+
pub fun getSetData(setID: UInt32): QuerySetData? {
|
|
1359
|
+
if TopShot.sets[setID] == nil {
|
|
1360
|
+
return nil
|
|
1361
|
+
} else {
|
|
1362
|
+
return QuerySetData(setID: setID)
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// getSetName returns the name that the specified Set
|
|
1367
|
+
// is associated with.
|
|
1368
|
+
//
|
|
1369
|
+
// Parameters: setID: The id of the Set that is being searched
|
|
1370
|
+
//
|
|
1371
|
+
// Returns: The name of the Set
|
|
1372
|
+
pub fun getSetName(setID: UInt32): String? {
|
|
1373
|
+
// Don't force a revert if the setID is invalid
|
|
1374
|
+
return TopShot.setDatas[setID]?.name
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// getSetSeries returns the series that the specified Set
|
|
1378
|
+
// is associated with.
|
|
1379
|
+
//
|
|
1380
|
+
// Parameters: setID: The id of the Set that is being searched
|
|
1381
|
+
//
|
|
1382
|
+
// Returns: The series that the Set belongs to
|
|
1383
|
+
pub fun getSetSeries(setID: UInt32): UInt32? {
|
|
1384
|
+
// Don't force a revert if the setID is invalid
|
|
1385
|
+
return TopShot.setDatas[setID]?.series
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// getSetIDsByName returns the IDs that the specified Set name
|
|
1389
|
+
// is associated with.
|
|
1390
|
+
//
|
|
1391
|
+
// Parameters: setName: The name of the Set that is being searched
|
|
1392
|
+
//
|
|
1393
|
+
// Returns: An array of the IDs of the Set if it exists, or nil if doesn't
|
|
1394
|
+
pub fun getSetIDsByName(setName: String): [UInt32]? {
|
|
1395
|
+
var setIDs: [UInt32] = []
|
|
1396
|
+
|
|
1397
|
+
// Iterate through all the setDatas and search for the name
|
|
1398
|
+
for setData in TopShot.setDatas.values {
|
|
1399
|
+
if setName == setData.name {
|
|
1400
|
+
// If the name is found, return the ID
|
|
1401
|
+
setIDs.append(setData.setID)
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// If the name isn't found, return nil
|
|
1406
|
+
// Don't force a revert if the setName is invalid
|
|
1407
|
+
if setIDs.length == 0 {
|
|
1408
|
+
return nil
|
|
1409
|
+
} else {
|
|
1410
|
+
return setIDs
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// getPlaysInSet returns the list of Play IDs that are in the Set
|
|
1415
|
+
//
|
|
1416
|
+
// Parameters: setID: The id of the Set that is being searched
|
|
1417
|
+
//
|
|
1418
|
+
// Returns: An array of Play IDs
|
|
1419
|
+
pub fun getPlaysInSet(setID: UInt32): [UInt32]? {
|
|
1420
|
+
// Don't force a revert if the setID is invalid
|
|
1421
|
+
return TopShot.sets[setID]?.plays
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// isEditionRetired returns a boolean that indicates if a Set/Play combo
|
|
1425
|
+
// (otherwise known as an edition) is retired.
|
|
1426
|
+
// If an edition is retired, it still remains in the Set,
|
|
1427
|
+
// but Moments can no longer be minted from it.
|
|
1428
|
+
//
|
|
1429
|
+
// Parameters: setID: The id of the Set that is being searched
|
|
1430
|
+
// playID: The id of the Play that is being searched
|
|
1431
|
+
//
|
|
1432
|
+
// Returns: Boolean indicating if the edition is retired or not
|
|
1433
|
+
pub fun isEditionRetired(setID: UInt32, playID: UInt32): Bool? {
|
|
1434
|
+
|
|
1435
|
+
if let setdata = self.getSetData(setID: setID) {
|
|
1436
|
+
|
|
1437
|
+
// See if the Play is retired from this Set
|
|
1438
|
+
let retired = setdata.getRetired()[playID]
|
|
1439
|
+
|
|
1440
|
+
// Return the retired status
|
|
1441
|
+
return retired
|
|
1442
|
+
} else {
|
|
1443
|
+
|
|
1444
|
+
// If the Set wasn't found, return nil
|
|
1445
|
+
return nil
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// isSetLocked returns a boolean that indicates if a Set
|
|
1450
|
+
// is locked. If it's locked,
|
|
1451
|
+
// new Plays can no longer be added to it,
|
|
1452
|
+
// but Moments can still be minted from Plays the set contains.
|
|
1453
|
+
//
|
|
1454
|
+
// Parameters: setID: The id of the Set that is being searched
|
|
1455
|
+
//
|
|
1456
|
+
// Returns: Boolean indicating if the Set is locked or not
|
|
1457
|
+
pub fun isSetLocked(setID: UInt32): Bool? {
|
|
1458
|
+
// Don't force a revert if the setID is invalid
|
|
1459
|
+
return TopShot.sets[setID]?.locked
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// getNumMomentsInEdition return the number of Moments that have been
|
|
1463
|
+
// minted from a certain edition.
|
|
1464
|
+
//
|
|
1465
|
+
// Parameters: setID: The id of the Set that is being searched
|
|
1466
|
+
// playID: The id of the Play that is being searched
|
|
1467
|
+
//
|
|
1468
|
+
// Returns: The total number of Moments
|
|
1469
|
+
// that have been minted from an edition
|
|
1470
|
+
pub fun getNumMomentsInEdition(setID: UInt32, playID: UInt32): UInt32? {
|
|
1471
|
+
if let setdata = self.getSetData(setID: setID) {
|
|
1472
|
+
|
|
1473
|
+
// Read the numMintedPerPlay
|
|
1474
|
+
let amount = setdata.getNumberMintedPerPlay()[playID]
|
|
1475
|
+
|
|
1476
|
+
return amount
|
|
1477
|
+
} else {
|
|
1478
|
+
// If the set wasn't found return nil
|
|
1479
|
+
return nil
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// getMomentsSubedition returns the Subedition the Moment belongs to
|
|
1484
|
+
//
|
|
1485
|
+
// Parameters: nftID: The ID of the NFT
|
|
1486
|
+
//
|
|
1487
|
+
// returns: UInt32? Subedition's ID if exists
|
|
1488
|
+
//
|
|
1489
|
+
pub fun getMomentsSubedition(nftID: UInt64):UInt32? {
|
|
1490
|
+
let subeditionAdmin = self.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
|
|
1491
|
+
?? panic("No subedition admin resource in storage")
|
|
1492
|
+
|
|
1493
|
+
return subeditionAdmin.getMomentsSubedition(nftID: nftID)
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// getAllSubeditions returns all the subeditions in topshot subeditionAdmin resource
|
|
1497
|
+
//
|
|
1498
|
+
// Returns: An array of all the subeditions that have been created
|
|
1499
|
+
pub fun getAllSubeditions():[TopShot.Subedition] {
|
|
1500
|
+
let subeditionAdmin = self.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
|
|
1501
|
+
?? panic("No subedition admin resource in storage")
|
|
1502
|
+
return subeditionAdmin.subeditionDatas.values
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// getSubeditionByID returns the subedition struct entity
|
|
1506
|
+
//
|
|
1507
|
+
// Parameters: subeditionID: The id of the Subedition that is being searched
|
|
1508
|
+
//
|
|
1509
|
+
// Returns: The Subedition struct
|
|
1510
|
+
pub fun getSubeditionByID(subeditionID: UInt32):TopShot.Subedition {
|
|
1511
|
+
let subeditionAdmin = self.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
|
|
1512
|
+
?? panic("No subedition admin resource in storage")
|
|
1513
|
+
return subeditionAdmin.subeditionDatas[subeditionID]!
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// This script reads the public nextSubeditionID from the SubeditionAdmin resource and
|
|
1517
|
+
// returns that number to the caller
|
|
1518
|
+
//
|
|
1519
|
+
// Returns: UInt32
|
|
1520
|
+
// the next number in nextSubeditionID from the SubeditionAdmin resource
|
|
1521
|
+
pub fun getNextSubeditionID():UInt32 {
|
|
1522
|
+
let subeditionAdmin = self.account.borrow<&SubeditionAdmin>(from: TopShot.SubeditionAdminStoragePath())
|
|
1523
|
+
?? panic("No subedition admin resource in storage")
|
|
1524
|
+
return subeditionAdmin.nextSubeditionID
|
|
1525
|
+
}
|
|
1526
|
+
// SubeditionAdmin is a resource that allows Set to mint Moments with Subeditions
|
|
1527
|
+
//
|
|
1528
|
+
pub struct Subedition {
|
|
1529
|
+
pub let subeditionID: UInt32
|
|
1530
|
+
|
|
1531
|
+
pub let name: String
|
|
1532
|
+
|
|
1533
|
+
pub let metadata: {String: String}
|
|
1534
|
+
|
|
1535
|
+
init(subeditionID: UInt32, name: String, metadata: {String: String}) {
|
|
1536
|
+
pre {
|
|
1537
|
+
name.length != 0: "New Subedition name cannot be empty"
|
|
1538
|
+
}
|
|
1539
|
+
self.subeditionID = subeditionID
|
|
1540
|
+
self.name = name
|
|
1541
|
+
self.metadata = metadata
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
pub resource SubeditionAdmin {
|
|
1546
|
+
|
|
1547
|
+
// Map of number of already minted Moments using Subedition.
|
|
1548
|
+
// When a new Moment with Subedition is minted, 1 is added to the
|
|
1549
|
+
// number in this map by the key, formed by concatinating of
|
|
1550
|
+
// SetID, PlayID and SubeditionID
|
|
1551
|
+
access(contract) let numberMintedPerSubedition: {String:UInt32}
|
|
1552
|
+
|
|
1553
|
+
// Map of Subedition which the Moment belongs to.
|
|
1554
|
+
// This map updates after each minting.
|
|
1555
|
+
access(contract) let momentsSubedition: {UInt64:UInt32}
|
|
1556
|
+
|
|
1557
|
+
// The ID that is used to create Subeditions.
|
|
1558
|
+
// Every time a Subeditions is created, subeditionID is assigned
|
|
1559
|
+
// to the new Subedition's ID and then is incremented by 1.
|
|
1560
|
+
access(contract) var nextSubeditionID: UInt32
|
|
1561
|
+
|
|
1562
|
+
// Variable size dictionary of Subedition structs
|
|
1563
|
+
access(contract) let subeditionDatas: {UInt32: Subedition}
|
|
1564
|
+
|
|
1565
|
+
// createSubedition creates a new Subedition struct
|
|
1566
|
+
// and stores it in the Subeditions dictionary in the SubeditionAdmin resource
|
|
1567
|
+
//
|
|
1568
|
+
// Parameters: name: The name of the Subedition
|
|
1569
|
+
// metadata: A dictionary mapping metadata titles to their data
|
|
1570
|
+
//
|
|
1571
|
+
// Returns: the ID of the new Subedition object
|
|
1572
|
+
//
|
|
1573
|
+
pub fun createSubedition(name:String, metadata:{String:String}): UInt32 {
|
|
1574
|
+
|
|
1575
|
+
let newID = self.nextSubeditionID
|
|
1576
|
+
|
|
1577
|
+
var newSubedition = Subedition(subeditionID: newID, name: name, metadata: metadata)
|
|
1578
|
+
|
|
1579
|
+
self.nextSubeditionID = self.nextSubeditionID + UInt32(1)
|
|
1580
|
+
|
|
1581
|
+
self.subeditionDatas[newID] = newSubedition
|
|
1582
|
+
|
|
1583
|
+
emit SubeditionCreated(subeditionID: newID, name: name, metadata: metadata)
|
|
1584
|
+
|
|
1585
|
+
return newID
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// getMomentsSubedition function that return's wich Subedition the Moment belongs to
|
|
1589
|
+
//
|
|
1590
|
+
// Parameters: nftID: The ID of the NFT
|
|
1591
|
+
//
|
|
1592
|
+
// returns: UInt32? Subedition's ID if exists
|
|
1593
|
+
//
|
|
1594
|
+
pub fun getMomentsSubedition(nftID: UInt64):UInt32? {
|
|
1595
|
+
return self.momentsSubedition[nftID]
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// getNumberMintedPerSubedition function that return's
|
|
1599
|
+
// the number of Moments that have been minted for this subedition
|
|
1600
|
+
// to use as this Moment's serial number
|
|
1601
|
+
//
|
|
1602
|
+
// Parameters: setID: The ID of the Set Moment will be minted from
|
|
1603
|
+
// playID: The ID of the Play Moment will be minted from
|
|
1604
|
+
// subeditionID: The ID of the Subedition using which moment will be minted
|
|
1605
|
+
//
|
|
1606
|
+
// returns: UInt32 Number of Moments, already minted for this Subedition
|
|
1607
|
+
//
|
|
1608
|
+
pub fun getNumberMintedPerSubedition(setID: UInt32, playID: UInt32, subeditionID: UInt32): UInt32 {
|
|
1609
|
+
let setPlaySubedition = setID.toString().concat(playID.toString()).concat(subeditionID.toString())
|
|
1610
|
+
if !self.numberMintedPerSubedition.containsKey(setPlaySubedition) {
|
|
1611
|
+
self.numberMintedPerSubedition.insert(key: setPlaySubedition,UInt32(0))
|
|
1612
|
+
return UInt32(0)
|
|
1613
|
+
}
|
|
1614
|
+
return self.numberMintedPerSubedition[setPlaySubedition]!
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// addToNumberMintedPerSubedition function that increments 1 to the
|
|
1618
|
+
// number of Moments that have been minted for this subedition
|
|
1619
|
+
//
|
|
1620
|
+
// Parameters: setID: The ID of the Set Moment will be minted from
|
|
1621
|
+
// playID: The ID of the Play Moment will be minted from
|
|
1622
|
+
// subeditionID: The ID of the Subedition using which moment will be minted
|
|
1623
|
+
//
|
|
1624
|
+
//
|
|
1625
|
+
pub fun addToNumberMintedPerSubedition(setID: UInt32, playID: UInt32, subeditionID: UInt32) {
|
|
1626
|
+
let setPlaySubedition = setID.toString().concat(playID.toString()).concat(subeditionID.toString())
|
|
1627
|
+
|
|
1628
|
+
if !self.numberMintedPerSubedition.containsKey(setPlaySubedition) {
|
|
1629
|
+
panic("Could not find specified Subedition!")
|
|
1630
|
+
}
|
|
1631
|
+
self.numberMintedPerSubedition[setPlaySubedition] = self.numberMintedPerSubedition[setPlaySubedition]! + UInt32(1)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
// setMomentsSubedition saves which Subedition the Moment belongs to
|
|
1636
|
+
//
|
|
1637
|
+
// Parameters: nftID: The ID of the NFT
|
|
1638
|
+
// subeditionID: The ID of the Subedition the Moment belongs to
|
|
1639
|
+
// setID: The ID of the Set that the Moment references
|
|
1640
|
+
// playID: The ID of the Play that the Moment references
|
|
1641
|
+
//
|
|
1642
|
+
pub fun setMomentsSubedition(nftID: UInt64, subeditionID: UInt32, setID: UInt32, playID: UInt32){
|
|
1643
|
+
pre {
|
|
1644
|
+
!self.momentsSubedition.containsKey(nftID) : "Subedition for this moment already exists!"
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
self.momentsSubedition.insert(key: nftID, subeditionID)
|
|
1648
|
+
|
|
1649
|
+
emit SubeditionAddedToMoment(momentID: nftID, subeditionID: subeditionID, setID: setID, playID: playID)
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
init() {
|
|
1653
|
+
self.momentsSubedition = {}
|
|
1654
|
+
self.numberMintedPerSubedition = {}
|
|
1655
|
+
self.subeditionDatas = {}
|
|
1656
|
+
self.nextSubeditionID = 1
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
// -----------------------------------------------------------------------
|
|
1662
|
+
// TopShot initialization function
|
|
1663
|
+
// -----------------------------------------------------------------------
|
|
1664
|
+
//
|
|
1665
|
+
init() {
|
|
1666
|
+
// Initialize contract fields
|
|
1667
|
+
self.currentSeries = 0
|
|
1668
|
+
self.playDatas = {}
|
|
1669
|
+
self.setDatas = {}
|
|
1670
|
+
self.sets <- {}
|
|
1671
|
+
self.nextPlayID = 1
|
|
1672
|
+
self.nextSetID = 1
|
|
1673
|
+
self.totalSupply = 0
|
|
1674
|
+
|
|
1675
|
+
// Put a new Collection in storage
|
|
1676
|
+
self.account.save<@Collection>(<- create Collection(), to: /storage/MomentCollection)
|
|
1677
|
+
|
|
1678
|
+
// Create a public capability for the Collection
|
|
1679
|
+
self.account.link<&{MomentCollectionPublic}>(/public/MomentCollection, target: /storage/MomentCollection)
|
|
1680
|
+
|
|
1681
|
+
// Put the Minter in storage
|
|
1682
|
+
self.account.save<@Admin>(<- create Admin(), to: /storage/TopShotAdmin)
|
|
1683
|
+
|
|
1684
|
+
emit ContractInitialized()
|
|
1685
|
+
}
|
|
1686
|
+
}
|