Smart Contract
StakedStarlyCard
A.29fcd0b5e444242a.StakedStarlyCard
1import Burner from 0xf233dcee88fe0abe
2import FungibleToken from 0xf233dcee88fe0abe
3import NonFungibleToken from 0x1d7e57aa55817448
4import MetadataViews from 0x1d7e57aa55817448
5import ViewResolver from 0x1d7e57aa55817448
6import StarlyCard from 0x5b82f21c0edf76e3
7import StarlyCardStaking from 0x29fcd0b5e444242a
8import StarlyIDParser from 0x5b82f21c0edf76e3
9import StarlyMetadata from 0x5b82f21c0edf76e3
10import StarlyToken from 0x142fa6570b62fd97
11
12access(all) contract StakedStarlyCard: NonFungibleToken {
13
14 access(all) event CardStaked(
15 id: UInt64,
16 starlyID: String,
17 beneficiary: Address,
18 stakeTimestamp: UFix64,
19 remainingResourceAtStakeTimestamp: UFix64)
20 access(all) event CardUnstaked(
21 id: UInt64,
22 starlyID: String,
23 beneficiary: Address,
24 stakeTimestamp: UFix64,
25 unstakeTimestamp: UFix64,
26 remainingResourceAtUnstakeTimestamp: UFix64)
27 access(all) event StakeBurned(id: UInt64, starlyID: String)
28 access(all) event Withdraw(id: UInt64, from: Address?)
29 access(all) event Deposit(id: UInt64, to: Address?)
30 access(all) event ContractInitialized()
31
32 access(all) var stakingEnabled: Bool
33 access(all) var unstakingEnabled: Bool
34 access(all) var totalSupply: UInt64
35
36 access(all) let CollectionStoragePath: StoragePath
37 access(all) let CollectionPublicPath: PublicPath
38 access(all) let AdminStoragePath: StoragePath
39 access(all) let MinterStoragePath: StoragePath
40 access(all) let BurnerStoragePath: StoragePath
41
42 access(all) resource interface StakePublic {
43 access(all) fun getStarlyID(): String
44 access(all) fun getBeneficiary(): Address
45 access(all) fun getStakeTimestamp(): UFix64
46 access(all) fun getRemainingResourceAtStakeTimestamp(): UFix64
47 access(all) fun getUnlockedResource(): UFix64
48 access(all) fun borrowStarlyCard(): &StarlyCard.NFT
49 }
50
51 access(all) struct StakeMetadataView {
52 access(all) let id: UInt64
53 access(all) let starlyID: String
54 access(all) let stakeTimestamp: UFix64
55 access(all) let remainingResource: UFix64
56 access(all) let remainingResourceAtStakeTimestamp: UFix64
57
58 init(
59 id: UInt64,
60 starlyID: String,
61 stakeTimestamp: UFix64,
62 remainingResource: UFix64,
63 remainingResourceAtStakeTimestamp: UFix64) {
64 self.id = id
65 self.starlyID = starlyID
66 self.stakeTimestamp = stakeTimestamp
67 self.remainingResource = remainingResource
68 self.remainingResourceAtStakeTimestamp = remainingResourceAtStakeTimestamp
69 }
70 }
71
72 access(all) resource NFT: NonFungibleToken.NFT, Burner.Burnable, StakePublic {
73 access(all) let id: UInt64
74 access(contract) let starlyCard: @StarlyCard.NFT
75 access(all) let beneficiary: Address
76 access(all) let stakeTimestamp: UFix64
77 access(all) let remainingResourceAtStakeTimestamp: UFix64
78
79 init(
80 id: UInt64,
81 starlyCard: @StarlyCard.NFT,
82 beneficiary: Address,
83 stakeTimestamp: UFix64,
84 remainingResourceAtStakeTimestamp: UFix64) {
85 self.id = id
86 self.starlyCard <-starlyCard
87 self.beneficiary = beneficiary
88 self.stakeTimestamp = stakeTimestamp
89 self.remainingResourceAtStakeTimestamp = remainingResourceAtStakeTimestamp
90 }
91
92 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
93 return <-StakedStarlyCard.createEmptyCollection(nftType: Type<@StakedStarlyCard.NFT>())
94 }
95
96 access(contract) fun burnCallback() {
97 let starlyID = self.starlyCard.starlyID
98 let collectionRef = getAccount(self.beneficiary).capabilities.borrow<&{NonFungibleToken.CollectionPublic}>(StarlyCard.CollectionPublicPath)!
99 let minterProxy = StakedStarlyCard.account.storage.borrow<&StarlyCard.MinterProxy>(from: StarlyCard.MinterProxyStoragePath)
100 ?? panic("Could not borrow a reference to the NFT minter proxy")
101 let clonedNFT <-minterProxy.cloneNFT(id: self.starlyCard.id, starlyID: self.starlyCard.starlyID)
102 collectionRef.deposit(token: <-clonedNFT)
103 emit StakeBurned(id: self.id, starlyID: starlyID)
104 }
105
106 access(all) view fun getViews(): [Type] {
107 return [
108 Type<MetadataViews.Display>(),
109 Type<StakeMetadataView>()
110 ]
111 }
112
113 access(all) fun resolveView(_ view: Type): AnyStruct? {
114 switch view {
115 case Type<MetadataViews.Display>():
116 return MetadataViews.Display(
117 name: "StakedStarlyCard #".concat(self.id.toString()),
118 description: "id: ".concat(self.id.toString())
119 .concat(", stakeTimestamp: ").concat(UInt64(self.stakeTimestamp).toString()),
120 thumbnail: MetadataViews.HTTPFile(url: ""))
121 case Type<StakeMetadataView>():
122 return StakeMetadataView(
123 id: self.id,
124 starlyID: self.starlyCard.starlyID,
125 stakeTimestamp: self.stakeTimestamp,
126 remainingResource: StarlyCardStaking.getRemainingResourceWithDefault(starlyID: self.starlyCard.starlyID),
127 remainingResourceAtStakeTimestamp: self.remainingResourceAtStakeTimestamp)
128 }
129 return nil
130 }
131
132 access(all) fun getStarlyID(): String {
133 return self.starlyCard.starlyID
134 }
135
136 access(all) fun getBeneficiary(): Address {
137 return self.beneficiary
138 }
139
140 access(all) fun getStakeTimestamp(): UFix64 {
141 return self.stakeTimestamp
142 }
143
144 access(all) fun getRemainingResourceAtStakeTimestamp(): UFix64 {
145 return self.remainingResourceAtStakeTimestamp
146 }
147
148 access(all) fun getUnlockedResource(): UFix64 {
149 let starlyID = self.starlyCard.starlyID
150 let stakeTimestamp = self.stakeTimestamp
151 let remainingResourceAtStakeTimestamp = self.remainingResourceAtStakeTimestamp
152 let stakedSeconds = getCurrentBlock().timestamp - stakeTimestamp
153
154 let metadata = StarlyMetadata.getCardEdition(starlyID: starlyID) ?? panic("Missing metadata")
155 let collectionID = metadata.collection.id
156 let initialResource = metadata.score ?? 0.0
157 let claimedResourceBeforeStaking = initialResource - remainingResourceAtStakeTimestamp
158 let remainingResource = StarlyCardStaking.getRemainingResource(collectionID: collectionID, starlyID: starlyID) ?? initialResource
159 if remainingResource <= 0.0 {
160 return 0.0
161 }
162
163 let claimedResource = remainingResourceAtStakeTimestamp - remainingResource
164 let claimResourcePerSecond = initialResource / 0.31556952
165 let unlockedResource = stakedSeconds / 10000.0 * claimResourcePerSecond / 10000.0 - claimedResource
166 return unlockedResource > remainingResource ? remainingResource : unlockedResource
167 }
168
169 access(all) fun borrowStarlyCard(): &StarlyCard.NFT {
170 let ref = &self.starlyCard as &{NonFungibleToken.NFT}
171 return ref as! &StarlyCard.NFT
172 }
173 }
174
175 access(all) view fun getContractViews(resourceType: Type?): [Type] {
176 return [
177 Type<MetadataViews.NFTCollectionData>()
178 ]
179 }
180
181 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
182 switch viewType {
183 case Type<MetadataViews.NFTCollectionData>():
184 return MetadataViews.NFTCollectionData(
185 storagePath: StakedStarlyCard.CollectionStoragePath,
186 publicPath: StakedStarlyCard.CollectionPublicPath,
187 publicCollection: Type<&StakedStarlyCard.Collection>(),
188 publicLinkedType: Type<&StakedStarlyCard.Collection>(),
189 createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} {
190 return <-StakedStarlyCard.createEmptyCollection(nftType: Type<@StakedStarlyCard.NFT>())
191 }))
192 }
193 return nil
194 }
195
196 access(all) resource NFTMinter {
197
198 access(all) fun mintStake(
199 starlyCard: @StarlyCard.NFT,
200 beneficiary: Address,
201 stakeTimestamp: UFix64): @StakedStarlyCard.NFT {
202
203 pre {
204 StakedStarlyCard.stakingEnabled: "Staking is disabled"
205 }
206
207 let starlyID = starlyCard.starlyID
208 let remainingResource = StarlyCardStaking.getRemainingResourceWithDefault(starlyID: starlyID)
209 let stake <- create NFT(
210 id: StakedStarlyCard.totalSupply,
211 starlyCard: <-starlyCard,
212 beneficiary: beneficiary,
213 stakeTimestamp: stakeTimestamp,
214 remainingResourceAtStakeTimestamp: remainingResource)
215 StakedStarlyCard.totalSupply = StakedStarlyCard.totalSupply + (1 as UInt64)
216 emit CardStaked(
217 id: stake.id,
218 starlyID: starlyID,
219 beneficiary: beneficiary,
220 stakeTimestamp: stakeTimestamp,
221 remainingResourceAtStakeTimestamp: remainingResource)
222 return <-stake
223 }
224 }
225
226 access(all) resource NFTBurner {
227
228 access(all) fun burnStake(stake: @StakedStarlyCard.NFT) {
229 pre {
230 StakedStarlyCard.unstakingEnabled: "Unstaking is disabled"
231 stake.stakeTimestamp < getCurrentBlock().timestamp: "Cannot unstake stake with stakeTimestamp more or equal to current timestamp"
232 }
233
234 let id = stake.id
235 let starlyID = stake.starlyCard.starlyID
236 let beneficiary = stake.beneficiary
237 let stakeTimestamp = stake.stakeTimestamp
238 let timestamp = getCurrentBlock().timestamp
239 let seconds = timestamp - stake.stakeTimestamp
240 Burner.burn(<-stake)
241
242 let remainingResource = StarlyCardStaking.getRemainingResourceWithDefault(starlyID: starlyID)
243 emit CardUnstaked(
244 id: id,
245 starlyID: starlyID,
246 beneficiary: beneficiary,
247 stakeTimestamp: stakeTimestamp,
248 unstakeTimestamp: timestamp,
249 remainingResourceAtUnstakeTimestamp: remainingResource)
250 }
251 }
252
253 access(all) resource interface CollectionPublic {
254 access(all) view fun getIDs(): [UInt64]
255 access(all) view fun borrowStakePublic(id: UInt64): &StakedStarlyCard.NFT
256 }
257
258 access(all) resource interface CollectionPrivate {
259 access(all) fun borrowStakePrivate(id: UInt64): &StakedStarlyCard.NFT
260 access(all) fun stake(starlyCard: @StarlyCard.NFT, beneficiary: Address)
261 access(NonFungibleToken.Withdraw) fun unstake(id: UInt64)
262 access(all) fun claimAll(limit: Int)
263 }
264
265 access(all) resource Collection:
266 NonFungibleToken.Collection,
267 CollectionPublic,
268 CollectionPrivate {
269
270 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
271
272 init () {
273 self.ownedNFTs <- {}
274 }
275
276 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
277 let supportedTypes: {Type: Bool} = {}
278 supportedTypes[Type<@StakedStarlyCard.NFT>()] = true
279 return supportedTypes
280 }
281
282 access(all) view fun isSupportedNFTType(type: Type): Bool {
283 if type == Type<@StakedStarlyCard.NFT>() {
284 return true
285 } else {
286 return false
287 }
288 }
289
290 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
291 let stake <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
292 emit Withdraw(id: stake.id, from: self.owner?.address)
293 return <-stake
294 }
295
296 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
297 let token <- token as! @StakedStarlyCard.NFT
298 let oldToken <- self.ownedNFTs[token.id] <- token
299 destroy oldToken
300 }
301
302 access(all) view fun getIDs(): [UInt64] {
303 return self.ownedNFTs.keys
304 }
305
306 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
307 return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)
308 }
309
310 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
311 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
312 return nft as &{ViewResolver.Resolver}
313 }
314 return nil
315 }
316
317 access(all) view fun borrowStakePublic(id: UInt64): &StakedStarlyCard.NFT {
318 let stakeRef = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
319 let intermediateRef = stakeRef as! &StakedStarlyCard.NFT
320 return intermediateRef as &StakedStarlyCard.NFT
321 }
322
323 access(all) fun stake(starlyCard: @StarlyCard.NFT, beneficiary: Address) {
324 let minter = StakedStarlyCard.account.storage.borrow<&NFTMinter>(from: StakedStarlyCard.MinterStoragePath)!
325 let stake <- minter.mintStake(
326 starlyCard: <-starlyCard,
327 beneficiary: beneficiary,
328 stakeTimestamp: getCurrentBlock().timestamp)
329 self.deposit(token: <-stake)
330 }
331
332 access(NonFungibleToken.Withdraw) fun unstake(id: UInt64) {
333 let burner = StakedStarlyCard.account.storage.borrow<&NFTBurner>(from: StakedStarlyCard.BurnerStoragePath)!
334 let stake <- self.withdraw(withdrawID: id) as! @StakedStarlyCard.NFT
335 burner.burnStake(stake: <-stake)
336 }
337
338 access(all) fun borrowStakePrivate(id: UInt64): &StakedStarlyCard.NFT {
339 let stakePassRef = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
340 return stakePassRef as! &StakedStarlyCard.NFT
341 }
342
343 access(all) fun claimAll(limit: Int) {
344 var i = 0
345 let stakeIDs = self.getIDs()
346 for stakeID in stakeIDs {
347 let stakeRef = self.borrowStakePrivate(id: stakeID)
348 let starlyID = stakeRef.starlyCard.starlyID
349 let parsedStarlyID = StarlyIDParser.parse(starlyID: starlyID)
350 let collectionID = parsedStarlyID.collectionID
351 let remainingResource = StarlyCardStaking.getRemainingResource(collectionID: collectionID, starlyID: starlyID)
352 if (i > limit) {
353 return
354 }
355 i = i + 1
356 }
357 }
358
359 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
360 return <-StarlyCard.createEmptyCollection(nftType: Type<@StakedStarlyCard.NFT>())
361 }
362 }
363
364 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
365 return <- create Collection()
366 }
367
368 access(all) resource Admin {
369 access(all) fun setStakingEnabled(_ enabled: Bool) {
370 StakedStarlyCard.stakingEnabled = enabled
371 }
372
373 access(all) fun setUnstakingEnabled(_ enabled: Bool) {
374 StakedStarlyCard.unstakingEnabled = enabled
375 }
376
377 access(all) fun createNFTMinter(): @NFTMinter {
378 return <-create NFTMinter()
379 }
380
381 access(all) fun createNFTBurner(): @NFTBurner {
382 return <-create NFTBurner()
383 }
384 }
385
386 init() {
387 self.stakingEnabled = true
388 self.unstakingEnabled = true
389 self.totalSupply = 0
390
391 self.CollectionStoragePath = /storage/stakedStarlyCardCollection
392 self.CollectionPublicPath = /public/stakedStarlyCardCollection
393 self.AdminStoragePath = /storage/stakedStarlyCardAdmin
394 self.MinterStoragePath = /storage/stakedStarlyCardMinter
395 self.BurnerStoragePath = /storage/stakedStarlyCardBurner
396
397 let admin <- create Admin()
398 let minter <- admin.createNFTMinter()
399 let burner <- admin.createNFTBurner()
400 self.account.storage.save(<-admin, to: self.AdminStoragePath)
401 self.account.storage.save(<-minter, to: self.MinterStoragePath)
402 self.account.storage.save(<-burner, to: self.BurnerStoragePath)
403
404 emit ContractInitialized()
405 }
406}
407
408