Smart Contract

Questing

A.35f9e7ac86111b22.Questing

Deployed

1d ago
Feb 26, 2026, 09:43:59 PM UTC

Dependents

0 imports
1import QuestReward from 0x35f9e7ac86111b22
2import NonFungibleToken from 0x1d7e57aa55817448
3import RewardAlgorithm from 0x35f9e7ac86111b22
4
5access(all)
6contract Questing{ 
7	
8	// -----------------------------------------------------------------------
9	// Events
10	// -----------------------------------------------------------------------
11	access(all)
12	event ContractInitialized()
13	
14	access(all)
15	event QuestStarted(
16		questID: UInt64,
17		typeIdentifier: String,
18		questingResourceID: UInt64,
19		quester: Address
20	)
21	
22	access(all)
23	event QuestEnded(
24		questID: UInt64,
25		typeIdentifier: String,
26		questingResourceID: UInt64,
27		quester: Address?
28	)
29	
30	access(all)
31	event RewardAdded(
32		questID: UInt64,
33		typeIdentifier: String,
34		questingResourceID: UInt64,
35		rewardID: UInt64,
36		rewardTemplateID: UInt32,
37		rewardTemplate: QuestReward.RewardTemplate
38	)
39	
40	access(all)
41	event RewardBurned(
42		questID: UInt64,
43		typeIdentifier: String,
44		questingResourceID: UInt64,
45		rewardID: UInt64,
46		rewardTemplateID: UInt32,
47		minterID: UInt64
48	)
49	
50	access(all)
51	event RewardMoved(
52		questID: UInt64,
53		typeIdentifier: String,
54		fromQuestingResourceID: UInt64,
55		toQuestingResourceID: UInt64,
56		rewardID: UInt64,
57		rewardTemplateID: UInt32,
58		minterID: UInt64
59	)
60	
61	access(all)
62	event RewardPerSecondChanged(questID: UInt64, typeIdentifier: String, rewardPerSecond: UFix64)
63	
64	access(all)
65	event RewardRevealed(
66		questID: UInt64,
67		typeIdentifier: String,
68		questingResourceID: UInt64,
69		questRewardID: UInt64,
70		rewardTemplateID: UInt32
71	)
72	
73	access(all)
74	event AdjustedQuestingStartDateUpdated(
75		questID: UInt64,
76		typeIdentifier: String,
77		questingResourceID: UInt64,
78		newAdjustedQuestingStartDate: UFix64
79	)
80	
81	access(all)
82	event QuestCreated(questID: UInt64, type: Type, questCreator: Address)
83	
84	access(all)
85	event QuestDeposited(
86		questID: UInt64,
87		type: Type,
88		questCreator: Address,
89		questReceiver: Address?
90	)
91	
92	access(all)
93	event QuestWithdrawn(
94		questID: UInt64,
95		type: Type,
96		questCreator: Address,
97		questProvider: Address?
98	)
99	
100	access(all)
101	event QuestDestroyed(questID: UInt64, type: Type, questCreator: Address)
102	
103	access(all)
104	event MinterDeposited(minterID: UInt64, name: String, receiverAddress: Address?)
105	
106	access(all)
107	event MinterWithdrawn(minterID: UInt64, name: String, providerAddress: Address?)
108	
109	// -----------------------------------------------------------------------
110	// Paths
111	// -----------------------------------------------------------------------
112	access(all)
113	let QuestManagerStoragePath: StoragePath
114	
115	access(all)
116	let QuestManagerPublicPath: PublicPath
117	
118	access(all)
119	let QuestManagerPrivatePath: PrivatePath
120	
121	access(all)
122	let QuesterStoragePath: StoragePath
123	
124	// -----------------------------------------------------------------------
125	// Contract Fields
126	// -----------------------------------------------------------------------
127	access(all)
128	var totalSupply: UInt64
129	
130	// -----------------------------------------------------------------------
131	// Future Contract Extensions
132	// -----------------------------------------------------------------------
133	access(self)
134	var metadata:{ String: AnyStruct}
135	
136	access(self)
137	var resources: @{String: AnyResource}
138	
139	access(all)
140	resource interface Public{ 
141		access(all)
142		let id: UInt64
143		
144		access(all)
145		let type: Type
146		
147		access(all)
148		let questCreator: Address
149		
150		access(all)
151		var rewardPerSecond: UFix64
152		
153		access(contract)
154		fun quest(questingResource: @AnyResource, address: Address): @AnyResource
155		
156		access(contract)
157		fun unquest(questingResource: @AnyResource, address: Address): @AnyResource
158		
159		access(contract)
160		fun revealQuestReward(questingResource: @AnyResource, questRewardID: UInt64): @AnyResource
161		
162		access(all)
163		fun getQuesters(): [Address]
164		
165		access(all)
166		fun getAllQuestingStartDates():{ UInt64: UFix64}
167		
168		access(all)
169		fun getQuestingStartDate(questingResourceID: UInt64): UFix64?
170		
171		access(all)
172		fun getAllAdjustedQuestingStartDates():{ UInt64: UFix64}
173		
174		access(all)
175		fun getAdjustedQuestingStartDate(questingResourceID: UInt64): UFix64?
176		
177		access(all)
178		fun getQuestingResourceIDs(): [UInt64]
179		
180		access(all)
181		fun borrowRewardCollection(questingResourceID: UInt64): &QuestReward.Collection?
182	}
183	
184	access(all)
185	resource Quest: Public{ 
186		access(all)
187		let id: UInt64
188		
189		access(all)
190		let type: Type
191		
192		access(all)
193		let questCreator: Address
194		
195		access(self)
196		var questers: [Address]
197		
198		access(self)
199		var questingStartDates:{ UInt64: UFix64}
200		
201		access(self)
202		var adjustedQuestingStartDates:{ UInt64: UFix64}
203		
204		/*
205					Questing Rewards
206				*/
207		
208		access(all)
209		var rewardPerSecond: UFix64
210		
211		access(self)
212		var rewards: @{UInt64: QuestReward.Collection}
213		
214		/*
215					Future extensions
216				*/
217		
218		access(self)
219		var metadata:{ String: AnyStruct}
220		
221		access(self)
222		var resources: @{String: AnyResource}
223		
224		init(type: Type, questCreator: Address){ 
225			self.id = self.uuid
226			self.type = type
227			self.questCreator = questCreator
228			self.questers = []
229			self.questingStartDates ={} 
230			self.adjustedQuestingStartDates ={} 
231			self.rewardPerSecond = 604800.0
232			self.rewards <-{} 
233			self.metadata ={} 
234			self.resources <-{} 
235			Questing.totalSupply = Questing.totalSupply + 1
236			emit QuestCreated(questID: self.id, type: type, questCreator: questCreator)
237		}
238		
239		// access(contract) to make sure user verifies with their actual address using the quest function from the quester resource
240		access(contract)
241		fun quest(questingResource: @AnyResource, address: Address): @AnyResource{ 
242			pre{ 
243				questingResource.getType() == self.type:
244					"Cannot quest: questingResource type does not match type required by quest"
245			}
246			var uuid: UInt64? = nil
247			var container: @{UInt64: AnyResource} <-{} 
248			container[0] <-! questingResource
249			if container[0]?.isInstance(Type<@{NonFungibleToken.NFT}>()) == true{ 
250				let ref = &container[0] as &AnyResource?
251				let _resource = (ref as! &{NonFungibleToken.NFT}?)!
252				uuid = _resource.uuid
253			}
254			
255			// ensure we always have a UUID by this point
256			assert(uuid != nil, message: "UUID should not be nil")
257			
258			// check if already questing
259			assert(!self.questingStartDates.keys.contains(uuid!), message: "Cannot quest: questingResource is already questing")
260			
261			// add quester to the list of questers
262			if !self.questers.contains(address){ 
263				self.questers.append(address)
264			}
265			
266			// add timers
267			self.questingStartDates[uuid!] = getCurrentBlock().timestamp
268			self.adjustedQuestingStartDates[uuid!] = getCurrentBlock().timestamp
269			emit QuestStarted(questID: self.id, typeIdentifier: self.type.identifier, questingResourceID: uuid!, quester: address)
270			let returnResource <- container.remove(key: 0)!
271			destroy container
272			return <-returnResource
273		}
274		
275		access(contract)
276		fun unquest(questingResource: @AnyResource, address: Address): @AnyResource{ 
277			pre{ 
278				questingResource.getType() == self.type:
279					"Cannot unquest: questingResource type does not match type required by quest"
280			}
281			var uuid: UInt64? = nil
282			var container: @{UInt64: AnyResource} <-{} 
283			container[0] <-! questingResource
284			if container[0]?.isInstance(Type<@{NonFungibleToken.NFT}>()) == true{ 
285				let ref = &container[0] as &AnyResource?
286				let _resource = (ref as! &{NonFungibleToken.NFT}?)!
287				uuid = _resource.uuid
288			}
289			
290			// ensure we always have a UUID by this point
291			assert(uuid != nil, message: "UUID should not be nil")
292			
293			// check if questingResource is questing
294			assert(self.questingStartDates.keys.contains(uuid!), message: "Cannot unquest: questingResource is not currently questing")
295			self.unquestResource(questingResourceID: uuid!)
296			let returnResource <- container.remove(key: 0)!
297			destroy container
298			return <-returnResource
299		}
300		
301		access(contract)
302		fun revealQuestReward(questingResource: @AnyResource, questRewardID: UInt64): @AnyResource{ 
303			var uuid: UInt64? = nil
304			var container: @{UInt64: AnyResource} <-{} 
305			container[0] <-! questingResource
306			if container[0]?.isInstance(Type<@{NonFungibleToken.NFT}>()) == true{ 
307				let ref = &container[0] as &AnyResource?
308				let _resource = (ref as! &{NonFungibleToken.NFT}?)!
309				uuid = _resource.uuid
310			}
311			
312			// ensure we always have a UUID by this point
313			assert(uuid != nil, message: "UUID should not be nil")
314			self.revealReward(questingResourceID: uuid!, questRewardID: questRewardID)
315			let returnResource <- container.remove(key: 0)!
316			destroy container
317			return <-returnResource
318		}
319		
320		/*
321					Public functions
322				*/
323		
324		access(all)
325		fun getQuesters(): [Address]{ 
326			return self.questers
327		}
328		
329		access(all)
330		fun getAllQuestingStartDates():{ UInt64: UFix64}{ 
331			return self.questingStartDates
332		}
333		
334		access(all)
335		fun getQuestingStartDate(questingResourceID: UInt64): UFix64?{ 
336			return self.questingStartDates[questingResourceID]
337		}
338		
339		access(all)
340		fun getAllAdjustedQuestingStartDates():{ UInt64: UFix64}{ 
341			return self.adjustedQuestingStartDates
342		}
343		
344		access(all)
345		fun getAdjustedQuestingStartDate(questingResourceID: UInt64): UFix64?{ 
346			return self.adjustedQuestingStartDates[questingResourceID]
347		}
348		
349		access(all)
350		fun getQuestingResourceIDs(): [UInt64]{ 
351			return self.rewards.keys
352		}
353		
354		access(all)
355		fun borrowRewardCollection(questingResourceID: UInt64): &QuestReward.Collection?{ 
356			return &self.rewards[questingResourceID] as &QuestReward.Collection?
357		}
358		
359		/*
360					QuestManager functions
361				*/
362		
363		access(all)
364		fun unquestResource(questingResourceID: UInt64){ 
365			// remove timers
366			self.questingStartDates.remove(key: questingResourceID)
367			self.adjustedQuestingStartDates.remove(key: questingResourceID)
368			emit QuestEnded(questID: self.id, typeIdentifier: self.type.identifier, questingResourceID: questingResourceID, quester: nil)
369		}
370		
371		access(all)
372		fun addReward(questingResourceID: UInt64, minter: &QuestReward.Minter, rewardAlgo: &{RewardAlgorithm.Algorithm}, rewardMapping:{ Int: UInt32}){ 
373			//check if resource is questing
374			if let adjustedQuestingStartDate = self.adjustedQuestingStartDates[questingResourceID]{ 
375				let timeQuested = getCurrentBlock().timestamp - adjustedQuestingStartDate
376				
377				//check if resource is eligible for reward
378				if timeQuested >= self.rewardPerSecond{ 
379					let rewardTemplateID = rewardMapping[rewardAlgo.randomAlgorithm()] ?? panic("RewardMapping does not contain a reward for the random algorithm")
380					var newReward <- minter.mintReward(rewardTemplateID: rewardTemplateID)
381					let rewardID = newReward.id
382					let toRef: &QuestReward.Collection? = &self.rewards[questingResourceID] as &QuestReward.Collection?
383					if toRef == nil{ 
384						let newCollection <- QuestReward.createEmptyCollection(nftType: Type<@QuestReward.Collection>())
385						newCollection.deposit(token: <-newReward)
386						self.rewards[questingResourceID] <-! newCollection as! @QuestReward.Collection
387					} else{ 
388						(toRef!).deposit(token: <-newReward)
389					}
390					self.updateAdjustedQuestingStartDate(questingResourceID: questingResourceID, rewardPerSecond: self.rewardPerSecond)
391					emit RewardAdded(questID: self.id, typeIdentifier: self.type.identifier, questingResourceID: questingResourceID, rewardID: rewardID, rewardTemplateID: rewardTemplateID, rewardTemplate: minter.getRewardTemplate(id: rewardTemplateID)!)
392				}
393			}
394		}
395		
396		access(all)
397		fun burnReward(questingResourceID: UInt64, rewardID: UInt64){ 
398			let collectionRef = &self.rewards[questingResourceID] as auth(NonFungibleToken.Withdraw) &QuestReward.Collection?
399    		assert(collectionRef != nil, message: "Cannot burn reward: questingResource does not have any rewards")
400			let reward <- (collectionRef!).withdraw(withdrawID: rewardID) as! @QuestReward.NFT
401			emit RewardBurned(questID: self.id, typeIdentifier: self.type.identifier, questingResourceID: questingResourceID, rewardID: rewardID, rewardTemplateID: reward.rewardTemplateID, minterID: reward.minterID)
402			destroy reward
403		}
404		
405		access(all)
406		fun moveReward(fromID: UInt64, toID: UInt64, rewardID: UInt64){ 
407			let fromRef = &self.rewards[fromID] as auth(NonFungibleToken.Withdraw) &QuestReward.Collection?
408			assert(fromRef != nil, message: "Cannot move reward: fromID does not have any rewards")
409			let toRef: &QuestReward.Collection? = &self.rewards[toID]
410			assert(toRef != nil, message: "Cannot move reward: toID does not have any rewards")
411			let reward <- (fromRef!).withdraw(withdrawID: rewardID) as! @QuestReward.NFT
412			emit RewardMoved(questID: self.id, typeIdentifier: self.type.identifier, fromQuestingResourceID: fromID, toQuestingResourceID: toID, rewardID: rewardID, rewardTemplateID: reward.rewardTemplateID, minterID: reward.minterID)
413			(toRef!).deposit(token: <-reward)
414		}
415		
416		access(all)
417		fun changeRewardPerSecond(seconds: UFix64){ 
418			self.rewardPerSecond = seconds
419			emit RewardPerSecondChanged(questID: self.id, typeIdentifier: self.type.identifier, rewardPerSecond: seconds)
420		}
421		
422		access(all)
423		fun revealReward(questingResourceID: UInt64, questRewardID: UInt64){ 
424			let collectionRef = &self.rewards[questingResourceID] as &QuestReward.Collection?
425			assert(collectionRef != nil, message: "Cannot reveal reward: questingResource does not have any rewards")
426			let rewardRef = (collectionRef!).borrowEntireQuestReward(id: questRewardID)!
427			rewardRef.reveal()
428			emit RewardRevealed(questID: self.id, typeIdentifier: self.type.identifier, questingResourceID: questingResourceID, questRewardID: questRewardID, rewardTemplateID: rewardRef.rewardTemplateID)
429		}
430		
431		access(contract)
432		fun updateAdjustedQuestingStartDate(questingResourceID: UInt64, rewardPerSecond: UFix64){ 
433			if self.adjustedQuestingStartDates[questingResourceID] != nil{ 
434				self.adjustedQuestingStartDates[questingResourceID] = self.adjustedQuestingStartDates[questingResourceID]! + rewardPerSecond
435				emit AdjustedQuestingStartDateUpdated(questID: self.id, typeIdentifier: self.type.identifier, questingResourceID: questingResourceID, newAdjustedQuestingStartDate: self.adjustedQuestingStartDates[questingResourceID]!)
436			}
437		}
438	}
439	
440	access(all)
441	resource interface MinterReceiver{ 
442		access(all)
443		fun depositMinter(minter: @QuestReward.Minter)
444	}
445	
446	access(all)
447	resource interface QuestReceiver{ 
448		access(all)
449		fun depositQuest(quest: @Quest)
450	}
451	
452	access(all)
453	resource interface QuestManagerPublic{ 
454		access(all)
455		fun getIDs(): [UInt64]
456		
457		access(all)
458		fun getMinterIDs(): [UInt64]
459		
460		access(all)
461		fun borrowQuest(id: UInt64): &Quest?{ 
462			post{ 
463				result == nil || result?.id == id:
464					"Cannot borrow Quest reference: The ID of the returned reference is incorrect"
465			}
466		}
467		
468		access(all)
469		fun borrowMinter(id: UInt64): &QuestReward.Minter?{ 
470			post{ 
471				result == nil || result?.id == id:
472					"Cannot borrow Minter reference: The ID of the returned reference is incorrect"
473			}
474		}
475	}
476	
477	access(all)
478	resource QuestManager: QuestManagerPublic, QuestReceiver, MinterReceiver{ 
479		access(self)
480		var quests: @{UInt64: Quest}
481		
482		access(self)
483		var minters: @{UInt64: QuestReward.Minter}
484		
485		init(){ 
486			self.quests <-{} 
487			self.minters <-{} 
488		}
489		
490		access(all)
491		fun getIDs(): [UInt64]{ 
492			return self.quests.keys
493		}
494		
495		access(all)
496		fun getMinterIDs(): [UInt64]{ 
497			return self.minters.keys
498		}
499		
500		access(all)
501		fun createQuest(type: Type){ 
502			let quest <- create Quest(type: type, questCreator: (self.owner!).address)
503			let id = quest.id
504			self.quests[id] <-! quest
505		}
506		
507		access(all)
508		fun depositQuest(quest: @Quest){ 
509			emit QuestDeposited(questID: quest.id, type: quest.type, questCreator: quest.questCreator, questReceiver: self.owner?.address)
510			self.quests[quest.id] <-! quest
511		}
512		
513		access(all)
514		fun withdrawQuest(id: UInt64): @Quest{ 
515			let quest <- self.quests.remove(key: id) ?? panic("Quest does not exist")
516			emit QuestWithdrawn(questID: id, type: quest.type, questCreator: quest.questCreator, questProvider: self.owner?.address)
517			return <-quest
518		}
519		
520		access(all)
521		fun destroyQuest(id: UInt64){ 
522			let quest <- self.quests.remove(key: id) ?? panic("Quest does not exist")
523			emit QuestDestroyed(questID: id, type: quest.type, questCreator: quest.questCreator)
524			destroy quest
525		}
526		
527		access(all)
528		fun borrowQuest(id: UInt64): &Questing.Quest?{ 
529			return &self.quests[id] as &Questing.Quest?
530		}
531		
532		access(all)
533		fun borrowEntireQuest(id: UInt64): &Questing.Quest?{ 
534			return &self.quests[id] as &Questing.Quest?
535		}
536		
537		access(all)
538		fun depositMinter(minter: @QuestReward.Minter){ 
539			emit MinterDeposited(minterID: minter.id, name: minter.name, receiverAddress: self.owner?.address)
540			self.minters[minter.id] <-! minter
541		}
542		
543		access(all)
544		fun withdrawMinter(id: UInt64): @QuestReward.Minter{ 
545			let minter <- self.minters.remove(key: id) ?? panic("Minter does not exist")
546			emit MinterWithdrawn(minterID: id, name: minter.name, providerAddress: self.owner?.address)
547			return <-minter
548		}
549		
550		access(all)
551		fun borrowMinter(id: UInt64): &QuestReward.Minter?{ 
552			return &self.minters[id] as &QuestReward.Minter?
553		}
554		
555		access(all)
556		fun borrowEntireMinter(id: UInt64): &QuestReward.Minter?{ 
557			return &self.minters[id] as &QuestReward.Minter?
558		}
559	}
560	
561	access(all)
562	resource Quester{ 
563		access(all)
564		fun quest(
565			questManager: Address,
566			questID: UInt64,
567			questingResource: @AnyResource
568		): @AnyResource{ 
569			let questRef = Questing.getQuest(questManager: questManager, id: questID)
570			assert(questRef != nil, message: "Quest reference should not be nil")
571			return <-(questRef!).quest(
572				questingResource: <-questingResource,
573				address: (self.owner!).address
574			)
575		}
576		
577		access(all)
578		fun unquest(
579			questManager: Address,
580			questID: UInt64,
581			questingResource: @AnyResource
582		): @AnyResource{ 
583			let questRef = Questing.getQuest(questManager: questManager, id: questID)
584			assert(questRef != nil, message: "Quest reference should not be nil")
585			return <-(questRef!).unquest(
586				questingResource: <-questingResource,
587				address: (self.owner!).address
588			)
589		}
590		
591		access(all)
592		fun revealReward(
593			questManager: Address,
594			questID: UInt64,
595			questingResource: @AnyResource,
596			questRewardID: UInt64
597		): @AnyResource{ 
598			let questRef = Questing.getQuest(questManager: questManager, id: questID)
599			assert(questRef != nil, message: "Quest reference should not be nil")
600			return <-(questRef!).revealQuestReward(
601				questingResource: <-questingResource,
602				questRewardID: questRewardID
603			)
604		}
605	}
606	
607	access(all)
608	fun getQuest(questManager: Address, id: UInt64): &Quest?{ 
609		if let questManagerRef =
610			getAccount(questManager).capabilities.get<&QuestManager>(
611				Questing.QuestManagerPublicPath
612			).borrow(){ 
613			return questManagerRef.borrowQuest(id: id)
614		} else{ 
615			return nil
616		}
617	}
618	
619	access(all)
620	fun createQuestManager(): @QuestManager{ 
621		return <-create QuestManager()
622	}
623	
624	access(all)
625	fun createQuester(): @Quester{ 
626		return <-create Quester()
627	}
628	
629	init(){ 
630		self.QuestManagerStoragePath = /storage/WonderlandQuestManager_2
631		self.QuestManagerPublicPath = /public/WonderlandQuestManager_2
632		self.QuestManagerPrivatePath = /private/WonderlandQuestManager_2
633		self.QuesterStoragePath = /storage/WonderlandQuester_2
634		self.totalSupply = 0
635		self.metadata ={} 
636		self.resources <-{} 
637		emit ContractInitialized()
638	}
639}
640