Smart Contract
FishNFT
A.44100f14f70e3f78.FishNFT
1import NonFungibleToken from 0x1d7e57aa55817448
2import MetadataViews from 0x1d7e57aa55817448
3import Xorshift128plus from 0x45caec600164c9e6
4
5access(all) contract FishNFT: NonFungibleToken {
6
7 // DESIGN NOTES:
8 // This NFT contract implements strict immutability - once a catch is minted,
9 // none of its data can be modified. This ensures:
10 // 1. Data integrity of verified catches
11 // 2. Fair competition and prize distribution
12 // 3. Permanent historical record of catches
13 // 4. Trust in the DerbyFish ecosystem
14 //
15 // All catch data, including:
16 // - Core catch details
17 // - Competition results
18 // - Location data
19 // - Environmental conditions
20 // - Verification status
21 // Must be provided at mint time and cannot be updated.
22
23 // SPECIES COIN INTEGRATION - Simple registry approach
24 access(all) var speciesRegistry: {String: Address} // speciesCode -> contract address
25 access(all) var totalFishCaught: UInt64
26
27 // Storage paths
28 access(all) let CollectionStoragePath: StoragePath
29 access(all) let CollectionPublicPath: PublicPath
30 access(all) let MinterStoragePath: StoragePath
31
32 // FishCard storage paths
33 access(all) let FishCardCollectionStoragePath: StoragePath
34 access(all) let FishCardCollectionPublicPath: PublicPath
35 access(all) let FishCardMinterStoragePath: StoragePath
36
37 // Events
38 access(all) event SpeciesRegistered(speciesCode: String, contractAddress: Address)
39
40 access(all) event FishMinted(
41 id: UInt64,
42 recipient: Address,
43 species: String,
44 scientific: String,
45 length: UFix64,
46 latitude: Fix64,
47 longitude: Fix64,
48 timestamp: UFix64,
49 speciesCode: String
50 )
51
52 // FishCard events
53 access(all) event FishCardMinted(
54 id: UInt64,
55 fishNFTId: UInt64,
56 recipient: Address,
57 species: String,
58 revealedFields: [String]
59 )
60
61 access(all) event FishCardCommitted(
62 commitId: UInt64,
63 fishNFTId: UInt64,
64 committer: Address,
65 commitBlock: UInt64,
66 revealBlock: UInt64
67 )
68
69 access(all) event FishCardRevealed(
70 commitId: UInt64,
71 fishCardId: UInt64,
72 revealedFields: [String]
73 )
74
75 // FishCard state
76 access(all) var totalFishCards: UInt64
77 access(all) var nextCommitId: UInt64
78
79 // Active commits for commit-reveal scheme
80 access(self) var activeCommits: {UInt64: FishCardCommit}
81
82 // FishCard Receipt storage path
83 access(all) let FishCardReceiptStoragePath: StoragePath
84
85 // Simple commit structure
86 access(all) struct FishCardCommit {
87 access(all) let id: UInt64
88 access(all) let fishNFTId: UInt64
89 access(all) let fishNFTOwner: Address
90 access(all) let recipient: Address
91 access(all) let commitBlock: UInt64
92 access(all) let userSalt: [UInt8]
93
94 init(id: UInt64, fishNFTId: UInt64, fishNFTOwner: Address, recipient: Address, userSalt: [UInt8]) {
95 self.id = id
96 self.fishNFTId = fishNFTId
97 self.fishNFTOwner = fishNFTOwner
98 self.recipient = recipient
99 self.commitBlock = getCurrentBlock().height
100 self.userSalt = userSalt
101 }
102 }
103
104 access(all) struct FishMetadata {
105 // PUBLIC CORE DATA - Always visible to everyone
106 access(all) let owner: Address // Owner of the NFT
107 access(all) let species: String // Common name
108 access(all) let scientific: String // Scientific name
109 access(all) let length: UFix64 // Length in inches
110 access(all) let weight: UFix64? // Weight in pounds if measured
111 access(all) let timestamp: UFix64 // When caught
112 access(all) let speciesCode: String // Required species code for minting
113 access(all) let hasRelease: Bool // Whether fish was released
114 access(all) let qualityScore: UFix64? // DerbyFish quality score (1-100)
115 access(all) let waterBody: String? // Lake, river, ocean name from location API
116 access(all) var allowFishCards: Bool // Whether this fish can be used to mint FishCards (one-way switch)
117
118 // COMPETITION DATA - Public competition results
119 access(all) let competitions: {UInt64: {String: UInt64}} // Map of derbyId -> {leaderboardId: placement}
120 access(all) let prizesWon: [String] // List of prizes won with this catch
121 access(all) let totalPrizeValue: UFix64? // Total value of prizes won (if applicable)
122
123 // DERBYFISH SANCTIONING BODY DATA - Added during verification
124 access(all) let verificationLevel: String // Level of verification (basic, enhanced, tournament)
125 access(all) let verifiedBy: Address // Address of verifier
126 access(all) let verifiedAt: UFix64 // When verified
127 access(all) let competitionId: String? // If caught during competition
128 access(all) let recordStatus: String? // If this is a record catch (state, lake, etc)
129 access(all) let certificationLevel: String? // Certification level of the catch
130
131 // REQUIRED MEDIA - For verification
132 access(all) let bumpShotUrl: String // Bump board measurement photo
133 access(all) let heroShotUrl: String // Beauty shot of catch
134 access(all) let bumpHash: String // Hash of bump shot
135 access(all) let heroHash: String // Hash of hero shot
136 access(all) let releaseVideoUrl: String? // Optional release video URL
137 access(all) let releaseHash: String? // Release video hash if exists
138
139 // PRIVATE LOCATION DATA - Only visible to owner and contract admin
140 access(contract) let longitude: Fix64 // Exact longitude from GPS
141 access(contract) let latitude: Fix64 // Exact latitude from GPS
142 access(contract) let waterTemp: UFix64? // Temperature in Fahrenheit from weather API
143 access(contract) let airTemp: UFix64? // Temperature in Fahrenheit from weather API
144 access(contract) let weather: String? // Weather conditions from API
145 access(contract) let moonPhase: String? // Moon phase calculated by app
146 access(contract) let tide: String? // Tide data if coastal location
147 access(contract) let barometricPressure: UFix64? // Barometric pressure from weather API
148 access(contract) let windSpeed: UFix64? // Wind speed from weather API
149 access(contract) let windDirection: String? // Wind direction from weather API
150 access(contract) let skyConditions: String? // Weather conditions from API
151 access(contract) let waterDepth: UFix64? // Depth from bathymetric data if available
152 access(contract) let structureType: String? // Structure type from location data
153 access(contract) let bottomType: String? // Bottom composition from location data
154
155 // PRIVATE ANGLER DATA - Only visible to owner and contract admin
156 access(contract) let location: String? // Named spot/location
157 access(contract) let waterClarity: String? // Water visibility/clarity
158 access(contract) let currentStrength: String? // Water current strength
159 access(contract) let gear: String? // Equipment used
160 access(contract) let baitLure: String? // Specific bait or lure
161 access(contract) let fightDuration: UFix64? // Fight duration in seconds
162 access(contract) let technique: String? // Fishing technique used
163 access(contract) let girth: UFix64? // Girth in inches if measured
164 access(contract) let rodType: String? // Type of rod used
165 access(contract) let reelType: String? // Type of reel used
166 access(contract) let lineType: String? // Type of line used
167 access(contract) let leaderType: String? // Type of leader used
168 access(contract) let hookType: String? // Type of hook used
169 access(contract) let presentation: String? // How the bait/lure was presented
170 access(contract) let retrieveSpeed: String? // Speed of retrieve
171 access(contract) let catchDepth: UFix64? // Depth fish was hooked at
172
173 // Function to get private data if caller is authorized
174 access(all) fun getPrivateData(caller: Address): {String: AnyStruct}? {
175 // Only allow the NFT owner or contract admin to access private data
176 if caller != self.owner && caller != FishNFT.account.address {
177 return nil
178 }
179
180 return {
181 // Location data
182 "longitude": self.longitude,
183 "latitude": self.latitude,
184 "waterTemp": self.waterTemp,
185 "airTemp": self.airTemp,
186 "weather": self.weather,
187 "moonPhase": self.moonPhase,
188 "tide": self.tide,
189 "barometricPressure": self.barometricPressure,
190 "windSpeed": self.windSpeed,
191 "windDirection": self.windDirection,
192 "skyConditions": self.skyConditions,
193 "waterDepth": self.waterDepth,
194 "structureType": self.structureType,
195 "bottomType": self.bottomType,
196
197 // Angler data
198 "location": self.location,
199 "waterClarity": self.waterClarity,
200 "currentStrength": self.currentStrength,
201 "gear": self.gear,
202 "baitLure": self.baitLure,
203 "fightDuration": self.fightDuration,
204 "technique": self.technique,
205 "girth": self.girth,
206 "rodType": self.rodType,
207 "reelType": self.reelType,
208 "lineType": self.lineType,
209 "leaderType": self.leaderType,
210 "hookType": self.hookType,
211 "presentation": self.presentation,
212 "retrieveSpeed": self.retrieveSpeed,
213 "catchDepth": self.catchDepth
214 }
215 }
216
217 // Function to enable fish card minting - can only be called once
218 access(all) fun enableFishCards() {
219 pre {
220 !self.allowFishCards: "Fish card minting has already been enabled"
221 }
222 self.allowFishCards = true
223 }
224
225 init(
226 // PUBLIC CORE DATA
227 owner: Address,
228 species: String,
229 scientific: String,
230 length: UFix64,
231 weight: UFix64?,
232 timestamp: UFix64,
233 speciesCode: String,
234 hasRelease: Bool,
235
236 // DERBY & COMPETITION DATA
237 competitions: {UInt64: {String: UInt64}},
238 prizesWon: [String],
239 totalPrizeValue: UFix64?,
240
241 // DERBYFISH SANCTIONING BODY DATA
242 verificationLevel: String,
243 verifiedBy: Address,
244 verifiedAt: UFix64,
245 competitionId: String?,
246 recordStatus: String?,
247 certificationLevel: String?,
248 qualityScore: UFix64?,
249
250 // LOCATION & ENVIRONMENTAL DATA
251 longitude: Fix64,
252 latitude: Fix64,
253 waterTemp: UFix64?,
254 airTemp: UFix64?,
255 weather: String?,
256 moonPhase: String?,
257 tide: String?,
258 waterBody: String?,
259 barometricPressure: UFix64?,
260 windSpeed: UFix64?,
261 windDirection: String?,
262 skyConditions: String?,
263 waterDepth: UFix64?,
264 structureType: String?,
265 bottomType: String?,
266
267 // MEDIA
268 bumpShotUrl: String,
269 heroShotUrl: String,
270 bumpHash: String,
271 heroHash: String,
272 releaseVideoUrl: String?,
273 releaseHash: String?,
274
275 // ANGLER DATA
276 location: String?,
277 waterClarity: String?,
278 currentStrength: String?,
279 gear: String?,
280 baitLure: String?,
281 fightDuration: UFix64?,
282 technique: String?,
283 girth: UFix64?,
284 rodType: String?,
285 reelType: String?,
286 lineType: String?,
287 leaderType: String?,
288 hookType: String?,
289 presentation: String?,
290 retrieveSpeed: String?,
291 catchDepth: UFix64?
292 ) {
293 self.owner = owner
294 self.species = species
295 self.scientific = scientific
296 self.length = length
297 self.weight = weight
298 self.timestamp = timestamp
299 self.speciesCode = speciesCode
300 self.hasRelease = hasRelease
301 self.allowFishCards = false // Disabled by default
302
303 // Competition data
304 self.competitions = competitions
305 self.prizesWon = prizesWon
306 self.totalPrizeValue = totalPrizeValue
307
308 // Verification data
309 self.verificationLevel = verificationLevel
310 self.verifiedBy = verifiedBy
311 self.verifiedAt = verifiedAt
312 self.competitionId = competitionId
313 self.recordStatus = recordStatus
314 self.certificationLevel = certificationLevel
315 self.qualityScore = qualityScore
316
317 // Location data
318 self.longitude = longitude
319 self.latitude = latitude
320 self.waterTemp = waterTemp
321 self.airTemp = airTemp
322 self.weather = weather
323 self.moonPhase = moonPhase
324 self.tide = tide
325 self.waterBody = waterBody
326 self.barometricPressure = barometricPressure
327 self.windSpeed = windSpeed
328 self.windDirection = windDirection
329 self.skyConditions = skyConditions
330 self.waterDepth = waterDepth
331 self.structureType = structureType
332 self.bottomType = bottomType
333
334 // Media
335 self.bumpShotUrl = bumpShotUrl
336 self.heroShotUrl = heroShotUrl
337 self.bumpHash = bumpHash
338 self.heroHash = heroHash
339 self.releaseVideoUrl = releaseVideoUrl
340 self.releaseHash = releaseHash
341
342 // Angler data
343 self.location = location
344 self.waterClarity = waterClarity
345 self.currentStrength = currentStrength
346 self.gear = gear
347 self.baitLure = baitLure
348 self.fightDuration = fightDuration
349 self.technique = technique
350 self.girth = girth
351 self.rodType = rodType
352 self.reelType = reelType
353 self.lineType = lineType
354 self.leaderType = leaderType
355 self.hookType = hookType
356 self.presentation = presentation
357 self.retrieveSpeed = retrieveSpeed
358 self.catchDepth = catchDepth
359 }
360 }
361
362 // FishCard metadata structure - contains selectively revealed data from Fish NFT
363 access(all) struct FishCardMetadata {
364 // CORE FIELDS - Always included from original Fish NFT
365 access(all) let fishNFTId: UInt64
366 access(all) let originalOwner: Address
367 access(all) let species: String
368 access(all) let scientific: String
369 access(all) let length: UFix64
370 access(all) let timestamp: UFix64
371 access(all) let speciesCode: String
372 access(all) let hasRelease: Bool
373
374 // SELECTIVELY REVEALED FIELDS - Based on VRF coin flips
375 access(all) let weight: UFix64?
376 access(all) let qualityScore: UFix64?
377 access(all) let waterBody: String?
378 access(all) let verificationLevel: String?
379 access(all) let bumpShotUrl: String?
380 access(all) let heroShotUrl: String?
381
382 // REVEALED LOCATION DATA - Based on coin flips
383 access(all) let longitude: Fix64?
384 access(all) let latitude: Fix64?
385 access(all) let waterTemp: UFix64?
386 access(all) let airTemp: UFix64?
387 access(all) let weather: String?
388 access(all) let moonPhase: String?
389 access(all) let tide: String?
390
391 // REVEALED ANGLER DATA - Based on coin flips
392 access(all) let location: String?
393 access(all) let gear: String?
394 access(all) let baitLure: String?
395 access(all) let technique: String?
396 access(all) let girth: UFix64?
397 access(all) let fightDuration: UFix64?
398
399 // METADATA
400 access(all) let revealedFields: [String]
401 access(all) let cardRarity: String // Based on number of revealed fields
402
403 init(
404 fishNFTId: UInt64,
405 originalOwner: Address,
406 species: String,
407 scientific: String,
408 length: UFix64,
409 timestamp: UFix64,
410 speciesCode: String,
411 hasRelease: Bool,
412 weight: UFix64?,
413 qualityScore: UFix64?,
414 waterBody: String?,
415 verificationLevel: String?,
416 bumpShotUrl: String?,
417 heroShotUrl: String?,
418 longitude: Fix64?,
419 latitude: Fix64?,
420 waterTemp: UFix64?,
421 airTemp: UFix64?,
422 weather: String?,
423 moonPhase: String?,
424 tide: String?,
425 location: String?,
426 gear: String?,
427 baitLure: String?,
428 technique: String?,
429 girth: UFix64?,
430 fightDuration: UFix64?,
431 revealedFields: [String]
432 ) {
433 self.fishNFTId = fishNFTId
434 self.originalOwner = originalOwner
435 self.species = species
436 self.scientific = scientific
437 self.length = length
438 self.timestamp = timestamp
439 self.speciesCode = speciesCode
440 self.hasRelease = hasRelease
441 self.weight = weight
442 self.qualityScore = qualityScore
443 self.waterBody = waterBody
444 self.verificationLevel = verificationLevel
445 self.bumpShotUrl = bumpShotUrl
446 self.heroShotUrl = heroShotUrl
447 self.longitude = longitude
448 self.latitude = latitude
449 self.waterTemp = waterTemp
450 self.airTemp = airTemp
451 self.weather = weather
452 self.moonPhase = moonPhase
453 self.tide = tide
454 self.location = location
455 self.gear = gear
456 self.baitLure = baitLure
457 self.technique = technique
458 self.girth = girth
459 self.fightDuration = fightDuration
460 self.revealedFields = revealedFields
461
462 // Calculate rarity based on revealed fields
463 let revealCount = revealedFields.length
464 if revealCount <= 3 {
465 self.cardRarity = "Common"
466 } else if revealCount <= 6 {
467 self.cardRarity = "Uncommon"
468 } else if revealCount <= 9 {
469 self.cardRarity = "Rare"
470 } else if revealCount <= 12 {
471 self.cardRarity = "Epic"
472 } else {
473 self.cardRarity = "Legendary"
474 }
475 }
476 }
477
478 access(all) resource NFT: NonFungibleToken.NFT {
479 access(all) let id: UInt64
480 access(all) let metadata: FishMetadata
481 access(all) let mintedBy: Address
482
483 // Convenience accessors for common fields
484 access(all) view fun getSpecies(): String {
485 return self.metadata.species
486 }
487
488 access(all) view fun getSpeciesCode(): String {
489 return self.metadata.speciesCode
490 }
491
492 access(all) view fun getLength(): UFix64 {
493 return self.metadata.length
494 }
495
496 access(all) view fun getWeight(): UFix64? {
497 return self.metadata.weight
498 }
499
500 access(all) view fun getTimestamp(): UFix64 {
501 return self.metadata.timestamp
502 }
503
504 access(all) view fun getHasRelease(): Bool {
505 return self.metadata.hasRelease
506 }
507
508 access(all) view fun getOwner(): Address {
509 return self.metadata.owner
510 }
511
512 // Access private data (only for owner or admin)
513 access(all) fun getPrivateData(caller: Address): {String: AnyStruct}? {
514 return self.metadata.getPrivateData(caller: caller)
515 }
516
517 // Enable fish card minting
518 access(all) fun enableFishCards() {
519 self.metadata.enableFishCards()
520 }
521
522 access(all) view fun canMintFishCards(): Bool {
523 return self.metadata.allowFishCards
524 }
525
526 // Views implementation
527 access(all) view fun getViews(): [Type] {
528 return [
529 Type<MetadataViews.Display>(),
530 Type<MetadataViews.Royalties>(),
531 Type<MetadataViews.ExternalURL>(),
532 Type<MetadataViews.NFTCollectionData>(),
533 Type<MetadataViews.NFTCollectionDisplay>(),
534 Type<MetadataViews.Serial>(),
535 Type<MetadataViews.Traits>()
536 ]
537 }
538
539 access(all) fun resolveView(_ view: Type): AnyStruct? {
540 switch view {
541 case Type<MetadataViews.Display>():
542 return MetadataViews.Display(
543 name: self.metadata.species.concat(" - ").concat(self.metadata.length.toString()).concat("\""),
544 description: "A verified ".concat(self.metadata.species).concat(" catch from DerbyFish"),
545 thumbnail: MetadataViews.HTTPFile(
546 url: self.metadata.heroShotUrl
547 )
548 )
549 case Type<MetadataViews.Serial>():
550 return MetadataViews.Serial(
551 self.id
552 )
553 case Type<MetadataViews.Royalties>():
554 return MetadataViews.Royalties([])
555 case Type<MetadataViews.ExternalURL>():
556 return MetadataViews.ExternalURL("https://derby.fish/catch/".concat(self.id.toString()))
557 case Type<MetadataViews.NFTCollectionData>():
558 return MetadataViews.NFTCollectionData(
559 storagePath: FishNFT.CollectionStoragePath,
560 publicPath: FishNFT.CollectionPublicPath,
561 publicCollection: Type<&FishNFT.Collection>(),
562 publicLinkedType: Type<&FishNFT.Collection>(),
563 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
564 return <-FishNFT.createEmptyCollection(nftType: Type<@FishNFT.NFT>())
565 })
566 )
567 case Type<MetadataViews.NFTCollectionDisplay>():
568 return MetadataViews.NFTCollectionDisplay(
569 name: "DerbyFish Catches",
570 description: "Verified fishing catches in the DerbyFish ecosystem",
571 externalURL: MetadataViews.ExternalURL("https://derby.fish"),
572 squareImage: MetadataViews.Media(
573 file: MetadataViews.HTTPFile(url: "https://derby.fish/images/logo-square.png"),
574 mediaType: "image/png"
575 ),
576 bannerImage: MetadataViews.Media(
577 file: MetadataViews.HTTPFile(url: "https://derby.fish/images/banner.png"),
578 mediaType: "image/png"
579 ),
580 socials: {
581 "website": MetadataViews.ExternalURL("https://derby.fish"),
582 "twitter": MetadataViews.ExternalURL("https://twitter.com/derby_fish")
583 }
584 )
585 case Type<MetadataViews.Traits>():
586 return MetadataViews.Traits([
587 MetadataViews.Trait(name: "species", value: self.metadata.species, displayType: nil, rarity: nil),
588 MetadataViews.Trait(name: "scientific", value: self.metadata.scientific, displayType: nil, rarity: nil),
589 MetadataViews.Trait(name: "length", value: self.metadata.length, displayType: "Number", rarity: nil),
590 MetadataViews.Trait(name: "hasRelease", value: self.metadata.hasRelease, displayType: nil, rarity: nil),
591 MetadataViews.Trait(name: "speciesCode", value: self.metadata.speciesCode, displayType: nil, rarity: nil)
592 ])
593 }
594 return nil
595 }
596
597 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
598 return <-create Collection()
599 }
600
601 init(id: UInt64, metadata: FishMetadata, mintedBy: Address) {
602 self.id = id
603 self.metadata = metadata
604 self.mintedBy = mintedBy
605 }
606 }
607
608 // FishCard NFT Resource
609 access(all) resource FishCard: NonFungibleToken.NFT {
610 access(all) let id: UInt64
611 access(all) let metadata: FishCardMetadata
612 access(all) let mintedBy: Address
613 access(all) let mintedAt: UFix64
614
615 // Convenience accessors
616 access(all) view fun getSpecies(): String {
617 return self.metadata.species
618 }
619
620 access(all) view fun getFishNFTId(): UInt64 {
621 return self.metadata.fishNFTId
622 }
623
624 access(all) view fun getRarity(): String {
625 return self.metadata.cardRarity
626 }
627
628 access(all) view fun getRevealedFields(): [String] {
629 return self.metadata.revealedFields
630 }
631
632 // Views implementation for FishCard
633 access(all) view fun getViews(): [Type] {
634 return [
635 Type<MetadataViews.Display>(),
636 Type<MetadataViews.Serial>(),
637 Type<MetadataViews.Traits>()
638 ]
639 }
640
641 access(all) fun resolveView(_ view: Type): AnyStruct? {
642 switch view {
643 case Type<MetadataViews.Display>():
644 return MetadataViews.Display(
645 name: self.metadata.species.concat(" Card #").concat(self.id.toString()),
646 description: "A trading card featuring a ".concat(self.metadata.species).concat(" catch - ").concat(self.metadata.cardRarity).concat(" rarity"),
647 thumbnail: MetadataViews.HTTPFile(
648 url: self.metadata.heroShotUrl ?? "https://derby.fish/images/card-placeholder.png"
649 )
650 )
651 case Type<MetadataViews.Serial>():
652 return MetadataViews.Serial(self.id)
653 case Type<MetadataViews.Traits>():
654 let traits: [MetadataViews.Trait] = []
655 traits.append(MetadataViews.Trait(name: "species", value: self.metadata.species, displayType: nil, rarity: nil))
656 traits.append(MetadataViews.Trait(name: "rarity", value: self.metadata.cardRarity, displayType: nil, rarity: nil))
657 traits.append(MetadataViews.Trait(name: "revealedFields", value: self.metadata.revealedFields.length, displayType: "Number", rarity: nil))
658 traits.append(MetadataViews.Trait(name: "fishNFTId", value: self.metadata.fishNFTId, displayType: "Number", rarity: nil))
659 return MetadataViews.Traits(traits)
660 }
661 return nil
662 }
663
664 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
665 return <-create FishCardCollection()
666 }
667
668 init(id: UInt64, metadata: FishCardMetadata, mintedBy: Address) {
669 self.id = id
670 self.metadata = metadata
671 self.mintedBy = mintedBy
672 self.mintedAt = getCurrentBlock().timestamp
673 }
674 }
675
676 access(all) resource Collection: NonFungibleToken.Collection {
677 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
678
679 access(all) view fun getLength(): Int {
680 return self.ownedNFTs.length
681 }
682
683 access(all) view fun getIDs(): [UInt64] {
684 return self.ownedNFTs.keys
685 }
686
687 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
688 return &self.ownedNFTs[id]
689 }
690
691 access(all) fun borrowEntireNFT(id: UInt64): &FishNFT.NFT? {
692 if self.ownedNFTs[id] != nil {
693 let ref = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}?
694 return ref as! &FishNFT.NFT
695 }
696 return nil
697 }
698
699 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
700 let token <- token as! @FishNFT.NFT
701 let id = token.id
702 let oldToken <- self.ownedNFTs[id] <- token
703 destroy oldToken
704 }
705
706 // Override withdraw to prevent transfers
707 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
708 panic("FishNFTs are non-transferable")
709 }
710
711 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
712 let supportedTypes: {Type: Bool} = {}
713 supportedTypes[Type<@FishNFT.NFT>()] = true
714 return supportedTypes
715 }
716
717 access(all) view fun isSupportedNFTType(type: Type): Bool {
718 return type == Type<@FishNFT.NFT>()
719 }
720
721 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
722 return <-create Collection()
723 }
724
725 init() {
726 self.ownedNFTs <- {}
727 }
728 }
729
730 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
731 return <- create Collection()
732 }
733
734 // FishCard Collection Resource - Remains transferable
735 access(all) resource FishCardCollection: NonFungibleToken.Collection {
736 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
737
738 access(all) view fun getLength(): Int {
739 return self.ownedNFTs.length
740 }
741
742 access(all) view fun getIDs(): [UInt64] {
743 return self.ownedNFTs.keys
744 }
745
746 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
747 return &self.ownedNFTs[id]
748 }
749
750 access(all) fun borrowFishCard(id: UInt64): &FishNFT.FishCard? {
751 if self.ownedNFTs[id] != nil {
752 let ref = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}?
753 return ref as! &FishNFT.FishCard
754 }
755 return nil
756 }
757
758 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
759 let token <- token as! @FishNFT.FishCard
760 let id = token.id
761 let oldToken <- self.ownedNFTs[id] <- token
762 destroy oldToken
763 }
764
765 // Keep withdraw enabled for FishCards
766 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
767 let token <- self.ownedNFTs.remove(key: withdrawID)
768 ?? panic("Could not withdraw a FishCard with the specified ID")
769 return <-token
770 }
771
772 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
773 let supportedTypes: {Type: Bool} = {}
774 supportedTypes[Type<@FishNFT.FishCard>()] = true
775 return supportedTypes
776 }
777
778 access(all) view fun isSupportedNFTType(type: Type): Bool {
779 return type == Type<@FishNFT.FishCard>()
780 }
781
782 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
783 return <-create FishCardCollection()
784 }
785
786 init() {
787 self.ownedNFTs <- {}
788 }
789 }
790
791 access(all) fun createEmptyFishCardCollection(): @{NonFungibleToken.Collection} {
792 return <- create FishCardCollection()
793 }
794
795 // Simple FishCard Receipt for commit-reveal scheme
796 access(all) resource FishCardReceipt {
797 access(all) let commitId: UInt64
798
799 init(commitId: UInt64) {
800 self.commitId = commitId
801 }
802 }
803
804 access(all) resource NFTMinter {
805 access(all) var nextID: UInt64
806
807 init() {
808 self.nextID = 1
809 }
810
811 access(all) fun mintNFT(
812 recipient: Address,
813 metadata: FishMetadata
814 ): @FishNFT.NFT {
815 let newNFT <- create FishNFT.NFT(
816 id: self.nextID,
817 metadata: metadata,
818 mintedBy: FishNFT.account.address
819 )
820
821 emit FishMinted(
822 id: self.nextID,
823 recipient: recipient,
824 species: metadata.species,
825 scientific: metadata.scientific,
826 length: metadata.length,
827 latitude: metadata.latitude,
828 longitude: metadata.longitude,
829 timestamp: metadata.timestamp,
830 speciesCode: metadata.speciesCode
831 )
832
833 // Update total fish caught counter
834 FishNFT.totalFishCaught = FishNFT.totalFishCaught + 1
835
836 self.nextID = self.nextID + 1
837
838 return <-newNFT
839 }
840
841 // Enhanced mint function with species validation
842 access(all) fun mintNFTWithSpeciesValidation(
843 // REQUIRED CORE DATA
844 recipient: Address,
845 species: String,
846 scientific: String,
847 length: UFix64,
848 weight: UFix64?,
849 timestamp: UFix64,
850 speciesCode: String,
851 hasRelease: Bool,
852
853 // REQUIRED MEDIA
854 bumpShotUrl: String,
855 heroShotUrl: String,
856 bumpHash: String,
857 heroHash: String,
858 releaseVideoUrl: String?,
859 releaseHash: String?,
860
861 // LOCATION DATA
862 longitude: Fix64,
863 latitude: Fix64,
864 waterBody: String?,
865
866 // OPTIONAL ENVIRONMENTAL DATA
867 waterTemp: UFix64?,
868 airTemp: UFix64?,
869 weather: String?,
870 moonPhase: String?,
871 tide: String?,
872 barometricPressure: UFix64?,
873 windSpeed: UFix64?,
874 windDirection: String?,
875 skyConditions: String?,
876 waterDepth: UFix64?,
877 structureType: String?,
878 bottomType: String?,
879
880 // OPTIONAL ANGLER DATA
881 location: String?,
882 waterClarity: String?,
883 currentStrength: String?,
884 gear: String?,
885 baitLure: String?,
886 fightDuration: UFix64?,
887 technique: String?,
888 girth: UFix64?,
889 rodType: String?,
890 reelType: String?,
891 lineType: String?,
892 leaderType: String?,
893 hookType: String?,
894 presentation: String?,
895 retrieveSpeed: String?,
896 catchDepth: UFix64?
897 ): @FishNFT.NFT {
898
899 // Create metadata with all provided fields
900 let metadata = FishMetadata(
901 // PUBLIC CORE DATA
902 owner: recipient,
903 species: species,
904 scientific: scientific,
905 length: length,
906 weight: weight,
907 timestamp: timestamp,
908 speciesCode: speciesCode,
909 hasRelease: hasRelease,
910
911 // COMPETITION DATA - Empty initially
912 competitions: {},
913 prizesWon: [],
914 totalPrizeValue: nil,
915
916 // VERIFICATION DATA - Basic verification initially
917 verificationLevel: "basic",
918 verifiedBy: FishNFT.account.address,
919 verifiedAt: getCurrentBlock().timestamp,
920 competitionId: nil,
921 recordStatus: nil,
922 certificationLevel: nil,
923 qualityScore: nil,
924
925 // LOCATION & ENVIRONMENTAL DATA
926 longitude: longitude,
927 latitude: latitude,
928 waterTemp: waterTemp,
929 airTemp: airTemp,
930 weather: weather,
931 moonPhase: moonPhase,
932 tide: tide,
933 waterBody: waterBody,
934 barometricPressure: barometricPressure,
935 windSpeed: windSpeed,
936 windDirection: windDirection,
937 skyConditions: skyConditions,
938 waterDepth: waterDepth,
939 structureType: structureType,
940 bottomType: bottomType,
941
942 // MEDIA
943 bumpShotUrl: bumpShotUrl,
944 heroShotUrl: heroShotUrl,
945 bumpHash: bumpHash,
946 heroHash: heroHash,
947 releaseVideoUrl: releaseVideoUrl,
948 releaseHash: releaseHash,
949
950 // ANGLER DATA
951 location: location,
952 waterClarity: waterClarity,
953 currentStrength: currentStrength,
954 gear: gear,
955 baitLure: baitLure,
956 fightDuration: fightDuration,
957 technique: technique,
958 girth: girth,
959 rodType: rodType,
960 reelType: reelType,
961 lineType: lineType,
962 leaderType: leaderType,
963 hookType: hookType,
964 presentation: presentation,
965 retrieveSpeed: retrieveSpeed,
966 catchDepth: catchDepth
967 )
968
969 return <- self.mintNFT(recipient: recipient, metadata: metadata)
970 }
971
972 access(all) fun mintNFTToCollection(
973 recipient: &{NonFungibleToken.Collection},
974 metadata: FishMetadata
975 ) {
976 let nft <- self.mintNFT(
977 recipient: recipient.owner?.address ?? FishNFT.account.address,
978 metadata: metadata
979 )
980 recipient.deposit(token: <-nft)
981 }
982 }
983
984 // FishCard Minter Resource with RandomConsumer commit-reveal pattern
985 access(all) resource FishCardMinter {
986 access(all) var nextCardID: UInt64
987
988 init() {
989 self.nextCardID = 1
990 }
991
992 // COMMIT PHASE: Create receipt for future FishCard minting
993 access(all) fun commitFishCard(
994 fishNFTId: UInt64,
995 fishNFTOwner: Address,
996 recipient: Address,
997 userSalt: [UInt8]
998 ): @FishCardReceipt {
999 // Verify Fish NFT exists and can mint cards
1000 let ownerAccount = getAccount(fishNFTOwner)
1001 let collectionRef = ownerAccount.capabilities.borrow<&FishNFT.Collection>(FishNFT.CollectionPublicPath)
1002 ?? panic("Could not borrow Fish NFT collection from owner")
1003
1004 let fishNFT = collectionRef.borrowEntireNFT(id: fishNFTId)
1005 ?? panic("Could not borrow Fish NFT with ID: ".concat(fishNFTId.toString()))
1006
1007 assert(fishNFT.canMintFishCards(), message: "FishCard minting not enabled for this Fish NFT")
1008
1009 // Create commit
1010 let commitId = FishNFT.nextCommitId
1011 let commit = FishCardCommit(
1012 id: commitId,
1013 fishNFTId: fishNFTId,
1014 fishNFTOwner: fishNFTOwner,
1015 recipient: recipient,
1016 userSalt: userSalt
1017 )
1018
1019 // Store the commit
1020 FishNFT.activeCommits[commitId] = commit
1021 FishNFT.nextCommitId = FishNFT.nextCommitId + 1
1022
1023 // Create and return receipt
1024 let receipt <- create FishCardReceipt(commitId: commitId)
1025
1026 emit FishCardCommitted(
1027 commitId: commitId,
1028 fishNFTId: fishNFTId,
1029 committer: recipient,
1030 commitBlock: commit.commitBlock,
1031 revealBlock: commit.commitBlock + 1
1032 )
1033
1034 return <-receipt
1035 }
1036
1037 // REVEAL PHASE: Use committed randomness to mint FishCard
1038 access(all) fun revealFishCard(receipt: @FishCardReceipt): @FishNFT.FishCard {
1039 // Get commit data
1040 let commit = FishNFT.activeCommits[receipt.commitId]
1041 ?? panic("Commit not found")
1042
1043 assert(getCurrentBlock().height > commit.commitBlock, message: "Must wait at least 1 block to reveal")
1044
1045 // Get Fish NFT
1046 let ownerAccount = getAccount(commit.fishNFTOwner)
1047 let collectionRef = ownerAccount.capabilities.borrow<&FishNFT.Collection>(FishNFT.CollectionPublicPath)
1048 ?? panic("Could not borrow Fish NFT collection from owner")
1049
1050 let fishNFT = collectionRef.borrowEntireNFT(id: commit.fishNFTId)
1051 ?? panic("Could not borrow Fish NFT")
1052
1053 // Get private data from Fish NFT
1054 let privateData = fishNFT.getPrivateData(caller: FishNFT.account.address)
1055 let fishMetadata = fishNFT.metadata
1056
1057 // Create randomness source using current block + user salt
1058 let blockHash = getCurrentBlock().id
1059 var blockHashArray: [UInt8] = []
1060 var i = 0
1061 while i < blockHash.length {
1062 blockHashArray.append(blockHash[i])
1063 i = i + 1
1064 }
1065 let randomSource = commit.userSalt.concat(blockHashArray)
1066 var prg = Xorshift128plus.PRG(sourceOfRandomness: randomSource, salt: commit.fishNFTId.toBigEndianBytes())
1067
1068 // Perform coin flips for each non-core field using PRG
1069 let revealedFields: [String] = []
1070
1071 // Non-core fields to coin flip
1072 let weight = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) ? fishMetadata.weight : nil
1073 if weight != nil { revealedFields.append("weight") }
1074
1075 let qualityScore = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) ? fishMetadata.qualityScore : nil
1076 if qualityScore != nil { revealedFields.append("qualityScore") }
1077
1078 let waterBody = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) ? fishMetadata.waterBody : nil
1079 if waterBody != nil { revealedFields.append("waterBody") }
1080
1081 let verificationLevel = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) ? fishMetadata.verificationLevel : nil
1082 if verificationLevel != nil { revealedFields.append("verificationLevel") }
1083
1084 let bumpShotUrl = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) ? fishMetadata.bumpShotUrl : nil
1085 if bumpShotUrl != nil { revealedFields.append("bumpShotUrl") }
1086
1087 let heroShotUrl = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) ? fishMetadata.heroShotUrl : nil
1088 if heroShotUrl != nil { revealedFields.append("heroShotUrl") }
1089
1090 // Private location data coin flips
1091 let longitude = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["longitude"] as! Fix64?) : nil
1092 if longitude != nil { revealedFields.append("longitude") }
1093
1094 let latitude = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["latitude"] as! Fix64?) : nil
1095 if latitude != nil { revealedFields.append("latitude") }
1096
1097 let waterTemp = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["waterTemp"] as! UFix64?) : nil
1098 if waterTemp != nil { revealedFields.append("waterTemp") }
1099
1100 let airTemp = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["airTemp"] as! UFix64?) : nil
1101 if airTemp != nil { revealedFields.append("airTemp") }
1102
1103 let weather = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["weather"] as! String?) : nil
1104 if weather != nil { revealedFields.append("weather") }
1105
1106 let moonPhase = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["moonPhase"] as! String?) : nil
1107 if moonPhase != nil { revealedFields.append("moonPhase") }
1108
1109 let tide = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["tide"] as! String?) : nil
1110 if tide != nil { revealedFields.append("tide") }
1111
1112 // Private angler data coin flips
1113 let location = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["location"] as! String?) : nil
1114 if location != nil { revealedFields.append("location") }
1115
1116 let gear = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["gear"] as! String?) : nil
1117 if gear != nil { revealedFields.append("gear") }
1118
1119 let baitLure = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["baitLure"] as! String?) : nil
1120 if baitLure != nil { revealedFields.append("baitLure") }
1121
1122 let technique = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["technique"] as! String?) : nil
1123 if technique != nil { revealedFields.append("technique") }
1124
1125 let girth = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["girth"] as! UFix64?) : nil
1126 if girth != nil { revealedFields.append("girth") }
1127
1128 let fightDuration = self.coinFlip(prg: &prg as &Xorshift128plus.PRG) && privateData != nil ? (privateData!["fightDuration"] as! UFix64?) : nil
1129 if fightDuration != nil { revealedFields.append("fightDuration") }
1130
1131 // Create FishCard metadata
1132 let cardMetadata = FishCardMetadata(
1133 fishNFTId: fishNFT.id,
1134 originalOwner: fishMetadata.owner,
1135 species: fishMetadata.species,
1136 scientific: fishMetadata.scientific,
1137 length: fishMetadata.length,
1138 timestamp: fishMetadata.timestamp,
1139 speciesCode: fishMetadata.speciesCode,
1140 hasRelease: fishMetadata.hasRelease,
1141 weight: weight,
1142 qualityScore: qualityScore,
1143 waterBody: waterBody,
1144 verificationLevel: verificationLevel,
1145 bumpShotUrl: bumpShotUrl,
1146 heroShotUrl: heroShotUrl,
1147 longitude: longitude,
1148 latitude: latitude,
1149 waterTemp: waterTemp,
1150 airTemp: airTemp,
1151 weather: weather,
1152 moonPhase: moonPhase,
1153 tide: tide,
1154 location: location,
1155 gear: gear,
1156 baitLure: baitLure,
1157 technique: technique,
1158 girth: girth,
1159 fightDuration: fightDuration,
1160 revealedFields: revealedFields
1161 )
1162
1163 // Create and return the FishCard
1164 let newFishCard <- create FishCard(
1165 id: self.nextCardID,
1166 metadata: cardMetadata,
1167 mintedBy: FishNFT.account.address
1168 )
1169
1170 emit FishCardMinted(
1171 id: self.nextCardID,
1172 fishNFTId: fishNFT.id,
1173 recipient: commit.recipient,
1174 species: fishMetadata.species,
1175 revealedFields: revealedFields
1176 )
1177
1178 emit FishCardRevealed(
1179 commitId: receipt.commitId,
1180 fishCardId: self.nextCardID,
1181 revealedFields: revealedFields
1182 )
1183
1184 // Update counts and cleanup
1185 FishNFT.totalFishCards = FishNFT.totalFishCards + 1
1186 self.nextCardID = self.nextCardID + 1
1187 FishNFT.activeCommits.remove(key: receipt.commitId)
1188
1189 // Clean up receipt
1190 destroy receipt
1191
1192 return <-newFishCard
1193 }
1194
1195 // Simple coin flip using PRG
1196 access(contract) fun coinFlip(prg: &Xorshift128plus.PRG): Bool {
1197 let randomValue = prg.nextUInt64()
1198 return randomValue % 2 == 0
1199 }
1200 }
1201
1202 // Public query functions
1203 access(all) view fun getTotalFishCaught(): UInt64 {
1204 return self.totalFishCaught
1205 }
1206
1207 access(all) view fun getTotalFishCards(): UInt64 {
1208 return self.totalFishCards
1209 }
1210
1211 // Public FishCard commit function - anyone can call to start the process
1212 access(all) fun commitFishCard(
1213 fishNFTId: UInt64,
1214 fishNFTOwner: Address,
1215 recipient: Address,
1216 userSalt: [UInt8]
1217 ): @FishCardReceipt {
1218 // Get minter reference
1219 let minterRef = self.account.storage.borrow<&FishCardMinter>(from: self.FishCardMinterStoragePath)
1220 ?? panic("Could not borrow FishCard minter")
1221
1222 // Commit the card request
1223 return <- minterRef.commitFishCard(
1224 fishNFTId: fishNFTId,
1225 fishNFTOwner: fishNFTOwner,
1226 recipient: recipient,
1227 userSalt: userSalt
1228 )
1229 }
1230
1231 // Public FishCard reveal function - reveals the card from a receipt
1232 access(all) fun revealFishCard(receipt: @FishCardReceipt): @FishNFT.FishCard {
1233 // Get minter reference
1234 let minterRef = self.account.storage.borrow<&FishCardMinter>(from: self.FishCardMinterStoragePath)
1235 ?? panic("Could not borrow FishCard minter")
1236
1237 // Reveal the card
1238 return <- minterRef.revealFishCard(receipt: <-receipt)
1239 }
1240
1241 // Contract Views (required by NonFungibleToken interface)
1242 access(all) view fun getContractViews(resourceType: Type?): [Type] {
1243 return [
1244 Type<MetadataViews.NFTCollectionData>(),
1245 Type<MetadataViews.NFTCollectionDisplay>()
1246 ]
1247 }
1248
1249 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
1250 switch viewType {
1251 case Type<MetadataViews.NFTCollectionData>():
1252 return MetadataViews.NFTCollectionData(
1253 storagePath: self.CollectionStoragePath,
1254 publicPath: self.CollectionPublicPath,
1255 publicCollection: Type<&FishNFT.Collection>(),
1256 publicLinkedType: Type<&FishNFT.Collection>(),
1257 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
1258 return <-FishNFT.createEmptyCollection(nftType: Type<@FishNFT.NFT>())
1259 })
1260 )
1261 case Type<MetadataViews.NFTCollectionDisplay>():
1262 return MetadataViews.NFTCollectionDisplay(
1263 name: "DerbyFish Catches",
1264 description: "Verified fishing catches in the DerbyFish ecosystem",
1265 externalURL: MetadataViews.ExternalURL("https://derby.fish"),
1266 squareImage: MetadataViews.Media(
1267 file: MetadataViews.HTTPFile(url: "https://derby.fish/images/logo-square.png"),
1268 mediaType: "image/png"
1269 ),
1270 bannerImage: MetadataViews.Media(
1271 file: MetadataViews.HTTPFile(url: "https://derby.fish/images/banner.png"),
1272 mediaType: "image/png"
1273 ),
1274 socials: {
1275 "website": MetadataViews.ExternalURL("https://derby.fish"),
1276 "twitter": MetadataViews.ExternalURL("https://twitter.com/derby_fish")
1277 }
1278 )
1279 }
1280 return nil
1281 }
1282
1283 // Simple species registry functions
1284 access(all) fun registerSpecies(speciesCode: String, contractAddress: Address) {
1285 self.speciesRegistry[speciesCode] = contractAddress
1286 emit SpeciesRegistered(speciesCode: speciesCode, contractAddress: contractAddress)
1287 }
1288
1289 access(all) fun getSpeciesAddress(speciesCode: String): Address? {
1290 return self.speciesRegistry[speciesCode]
1291 }
1292
1293 access(all) view fun getAllRegisteredSpecies(): {String: Address} {
1294 return self.speciesRegistry
1295 }
1296
1297 init() {
1298 self.CollectionStoragePath = /storage/FishNFTCollection
1299 self.CollectionPublicPath = /public/FishNFTCollection
1300 self.MinterStoragePath = /storage/FishNFTMinter
1301
1302 // Initialize FishCard storage paths
1303 self.FishCardCollectionStoragePath = /storage/FishCardCollection
1304 self.FishCardCollectionPublicPath = /public/FishCardCollection
1305 self.FishCardMinterStoragePath = /storage/FishCardMinter
1306
1307 // Initialize species integration variables
1308 self.speciesRegistry = {}
1309 self.totalFishCaught = 0
1310 self.totalFishCards = 0
1311 self.nextCommitId = 0
1312 self.activeCommits = {}
1313
1314 // Set FishCard Receipt storage path
1315 self.FishCardReceiptStoragePath = StoragePath(identifier: "FishCardReceipt_".concat(self.account.address.toString()))!
1316
1317 // Create Fish NFT collection and minter
1318 let collection <- create Collection()
1319 self.account.storage.save(<-collection, to: self.CollectionStoragePath)
1320
1321 let collectionCap = self.account.capabilities.storage.issue<&FishNFT.Collection>(self.CollectionStoragePath)
1322 self.account.capabilities.publish(collectionCap, at: self.CollectionPublicPath)
1323
1324 let minter <- create NFTMinter()
1325 self.account.storage.save(<-minter, to: self.MinterStoragePath)
1326
1327 // Create FishCard collection and minter
1328 let fishCardCollection <- create FishCardCollection()
1329 self.account.storage.save(<-fishCardCollection, to: self.FishCardCollectionStoragePath)
1330
1331 let fishCardCollectionCap = self.account.capabilities.storage.issue<&FishNFT.FishCardCollection>(self.FishCardCollectionStoragePath)
1332 self.account.capabilities.publish(fishCardCollectionCap, at: self.FishCardCollectionPublicPath)
1333
1334 let fishCardMinter <- create FishCardMinter()
1335 self.account.storage.save(<-fishCardMinter, to: self.FishCardMinterStoragePath)
1336 }
1337}