Smart Contract

StakedStarlyCard

A.29fcd0b5e444242a.StakedStarlyCard

Deployed

2d ago
Feb 25, 2026, 02:55:55 AM UTC

Dependents

7 imports
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