Smart Contract
TopShotEscrowV2
A.4eded0de73020ca5.TopShotEscrowV2
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import TopShot from 0x0b2a3299cc857e29
4
5pub contract TopShotEscrowV2 {
6
7 // -----------------------------------------------------------------------
8 // TopShotEscrowV2 contract Events
9 // -----------------------------------------------------------------------
10
11 pub event ContractInitialized()
12 pub event Escrowed(id: UInt64, owner: Address, NFTIds: [UInt64], duration: UFix64, startTime: UFix64)
13 pub event Redeemed(id: UInt64, owner: Address, NFTIds: [UInt64], partial: Bool, time: UFix64)
14 pub event EscrowCancelled(id: UInt64, owner: Address, NFTIds: [UInt64], partial: Bool, time: UFix64)
15 pub event EscrowWithdraw(id: UInt64, from: Address?)
16 pub event EscrowUpdated(id: UInt64, owner: Address, NFTIds: [UInt64])
17
18 // -----------------------------------------------------------------------
19 // TopShotEscrowV2 contract-level fields.
20 // These contain actual values that are stored in the smart contract.
21 // -----------------------------------------------------------------------
22
23 // The total amount of EscrowItems that have been created
24 pub var totalEscrows: UInt64
25
26 // Escrow Storage Path
27 pub let escrowStoragePath: StoragePath
28
29 /// Escrow Public Path
30 pub let escrowPublicPath: PublicPath
31
32 // -----------------------------------------------------------------------
33 // TopShotEscrowV2 contract-level Composite Type definitions
34 // -----------------------------------------------------------------------
35 // These are just *definitions* for Types that this contract
36 // and other accounts can use. These definitions do not contain
37 // actual stored values, but an instance (or object) of one of these Types
38 // can be created by this contract that contains stored values.
39 // -----------------------------------------------------------------------
40
41 // This struct contains the status of the escrow
42 // and is exposed so websites can use escrow information
43 pub struct EscrowDetails {
44 pub let owner: Address
45 pub let escrowID: UInt64
46 pub let NFTIds: [UInt64]?
47 pub let starTime: UFix64
48 pub let duration : UFix64
49 pub let isRedeemable : Bool
50
51 init(_owner: Address,
52 _escrowID: UInt64,
53 _NFTIds: [UInt64]?,
54 _startTime: UFix64,
55 _duration: UFix64,
56 _isRedeemable: Bool
57 ) {
58 self.owner = _owner
59 self.escrowID = _escrowID
60 self.NFTIds = _NFTIds
61 self.starTime = _startTime
62 self.duration = _duration
63 self.isRedeemable = _isRedeemable
64 }
65 }
66
67 // An interface that exposes public fields and functions
68 // of the EscrowItem resource
69 pub resource interface EscrowItemPublic {
70 pub let escrowID: UInt64
71 pub var redeemed: Bool
72 pub fun hasBeenRedeemed(): Bool
73 pub fun isRedeemable(): Bool
74 pub fun getEscrowDetails(): EscrowDetails
75 pub fun redeem(NFTIds: [UInt64])
76 pub fun addNFTs(NFTCollection: @TopShot.Collection)
77 }
78
79 // EscrowItem contains a NFT Collection (single or several NFTs) for a single escrow
80 // Fields and functions are defined as private by default
81 // to access escrow details, one can call getEscrowDetails()
82 pub resource EscrowItem: EscrowItemPublic {
83
84 // The id of this individual escrow
85 pub let escrowID: UInt64
86 access(self) var NFTCollection: [UInt64]?
87 pub let startTime: UFix64
88 access(self) var duration: UFix64
89 pub var redeemed: Bool
90 access(self) let receiverCap : Capability<&{NonFungibleToken.Receiver}>
91 access(self) var lock: Bool
92
93 init(
94 _NFTCollection: @TopShot.Collection,
95 _duration: UFix64,
96 _receiverCap : Capability<&{NonFungibleToken.Receiver}>
97 ) {
98 TopShotEscrowV2.totalEscrows = TopShotEscrowV2.totalEscrows + 1
99 self.escrowID = TopShotEscrowV2.totalEscrows
100 self.NFTCollection = _NFTCollection.getIDs()
101 assert(self.NFTCollection != nil, message: "NFT Collection is empty")
102 self.startTime = getCurrentBlock().timestamp
103 self.duration = _duration
104 assert(_receiverCap.borrow() != nil, message: "Cannot borrow receiver")
105 self.receiverCap = _receiverCap
106 self.redeemed = false
107 self.lock = false
108
109 let adminTopShotReceiverRef = TopShotEscrowV2.account.getCapability(/public/MomentCollection).borrow<&{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, TopShot.MomentCollectionPublic}>()
110 ?? panic("Cannot borrow collection")
111
112 for tokenId in self.NFTCollection! {
113 let token <- _NFTCollection.withdraw(withdrawID: tokenId)
114 adminTopShotReceiverRef.deposit(token: <-token)
115 }
116
117 assert(_NFTCollection.getIDs().length == 0, message: "can't destroy resources")
118 destroy _NFTCollection
119 }
120
121 pub fun isRedeemable(): Bool {
122 return getCurrentBlock().timestamp > self.startTime + self.duration
123 }
124
125 pub fun hasBeenRedeemed(): Bool {
126 return self.redeemed
127 }
128
129
130 pub fun redeem(NFTIds: [UInt64]) {
131
132 pre {
133 !self.lock: "Reentrant call"
134 self.isRedeemable() : "Not redeemable yet"
135 !self.hasBeenRedeemed() : "Has already been redeemed"
136 }
137 post {
138 !self.lock: "Lock not released"
139 }
140 self.lock = true
141
142 let collectionRef = self.receiverCap.borrow()
143 ?? panic("Cannot borrow receiver")
144
145 let providerTopShotProviderRef: &TopShot.Collection? = TopShotEscrowV2.account.borrow<&TopShot.Collection>(from: /storage/MomentCollection)
146 ?? panic("Cannot borrow collection")
147
148 if (NFTIds.length == 0 || NFTIds.length == self.NFTCollection?.length){
149 // Iterate through the keys in the collection and deposit each one
150 for tokenId in self.NFTCollection! {
151 let token <- providerTopShotProviderRef?.withdraw(withdrawID: tokenId)!
152 collectionRef.deposit(token: <-token)
153 }
154
155 self.redeemed = true;
156
157 emit Redeemed(id: self.escrowID, owner: self.receiverCap.address, NFTIds: self.NFTCollection!, partial: false, time: getCurrentBlock().timestamp)
158 } else {
159 for NFTId in NFTIds {
160 let token <- providerTopShotProviderRef?.withdraw(withdrawID: NFTId)!
161 collectionRef.deposit(token: <-token)
162 let index = self.NFTCollection?.firstIndex(of: NFTId) ?? panic("NFT ID not found")
163 let removedId = self.NFTCollection?.remove(at: index !) ?? panic("NFT ID not found")
164 assert(removedId == NFTId, message: "NFT ID mismatch")
165 }
166
167 if (self.NFTCollection?.length == 0){
168 self.redeemed = true;
169 }
170
171 emit Redeemed(id: self.escrowID, owner: self.receiverCap.address, NFTIds: NFTIds, partial: !self.redeemed, time: getCurrentBlock().timestamp)
172 }
173
174 self.lock = false
175 }
176
177 pub fun getEscrowDetails(): EscrowDetails{
178 return EscrowDetails(
179 _owner: self.receiverCap.address,
180 _escrowID: self.escrowID,
181 _NFTIds: self.NFTCollection,
182 _startTime: self.startTime,
183 _duration: self.duration,
184 _isRedeemable: self.isRedeemable()
185 )
186 }
187
188 pub fun setEscrowDuration(_ newDuration: UFix64) {
189 post {
190 newDuration < self.duration : "Can only decrease duration"
191 }
192 self.duration = newDuration
193 }
194
195 pub fun cancelEscrow(NFTIds: [UInt64]) {
196 pre {
197 !self.hasBeenRedeemed() : "Has already been redeemed"
198 }
199 let collectionRef = self.receiverCap.borrow()
200 ?? panic("Cannot borrow receiver")
201
202 let providerTopShotProviderRef: &TopShot.Collection? = TopShotEscrowV2.account.borrow<&TopShot.Collection>(from: /storage/MomentCollection)
203 ?? panic("Cannot borrow collection")
204
205 if (NFTIds.length == 0 || NFTIds.length == self.NFTCollection?.length){
206 self.redeemed = true;
207
208 for tokenId in self.NFTCollection! {
209 let token <- providerTopShotProviderRef?.withdraw(withdrawID: tokenId)!
210 collectionRef.deposit(token: <-token)
211 }
212
213 emit EscrowCancelled(id: self.escrowID, owner: self.receiverCap.address, NFTIds: self.NFTCollection!, partial: false, time: getCurrentBlock().timestamp)
214 } else {
215 for NFTId in NFTIds {
216 let token <- providerTopShotProviderRef?.withdraw(withdrawID: NFTId)!
217 collectionRef.deposit(token: <-token)
218 let index = self.NFTCollection?.firstIndex(of: NFTId) ?? panic("NFT ID not found")
219 let removedId = self.NFTCollection?.remove(at: index !) ?? panic("NFT ID not found")
220 assert(removedId == NFTId, message: "NFT ID mismatch")
221 }
222
223 if (self.NFTCollection?.length == 0){
224 self.redeemed = true;
225 }
226
227 emit EscrowCancelled(id: self.escrowID, owner: self.receiverCap.address, NFTIds: NFTIds, partial: !self.redeemed, time: getCurrentBlock().timestamp)
228 }
229 }
230
231 pub fun addNFTs(NFTCollection: @TopShot.Collection) {
232 pre {
233 !self.hasBeenRedeemed() : "Has already been redeemed"
234 }
235 let NFTIds = NFTCollection.getIDs()
236 let adminTopShotReceiverRef = TopShotEscrowV2.account.getCapability(/public/MomentCollection).borrow<&{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, TopShot.MomentCollectionPublic}>()
237 ?? panic("Cannot borrow collection")
238
239 for NFTId in NFTIds {
240 let token <- NFTCollection.withdraw(withdrawID: NFTId)
241 adminTopShotReceiverRef.deposit(token: <-token)
242 }
243
244 assert(NFTCollection.getIDs().length == 0, message: "can't destroy resources")
245 destroy NFTCollection
246
247 self.NFTCollection!.appendAll(NFTIds)
248 emit EscrowUpdated(id: self.escrowID, owner: self.receiverCap.address, NFTIds: self.NFTCollection!)
249 }
250
251 destroy() {
252 assert(self.redeemed, message: "Escrow not redeemed")
253 }
254 }
255
256 // An interface to interact publicly with the Escrow Collection
257 pub resource interface EscrowCollectionPublic {
258 pub fun createEscrow(
259 NFTCollection: @TopShot.Collection,
260 duration: UFix64,
261 receiverCap : Capability<&{NonFungibleToken.Receiver}>)
262
263 pub fun borrowEscrow(escrowID: UInt64): &EscrowItem{EscrowItemPublic}?
264 pub fun getEscrowIDs() : [UInt64]
265 }
266
267 // EscrowCollection contains a dictionary of EscrowItems
268 // and provides methods for manipulating the EscrowItems
269 pub resource EscrowCollection: EscrowCollectionPublic {
270
271 // Escrow Items
272 access(self) var escrowItems: @{UInt64: EscrowItem}
273
274 // withdraw
275 // Removes an escrow from the collection and moves it to the caller
276 pub fun withdraw(escrowID: UInt64): @TopShotEscrowV2.EscrowItem {
277 let escrow <- self.escrowItems.remove(key: escrowID) ?? panic("missing NFT")
278
279 emit EscrowWithdraw(id: escrow.escrowID, from: self.owner?.address)
280
281 return <-escrow
282 }
283
284
285 init() {
286 self.escrowItems <- {}
287 }
288
289 pub fun getEscrowIDs() : [UInt64] {
290 return self.escrowItems.keys
291 }
292
293 pub fun createEscrow(
294 NFTCollection: @TopShot.Collection,
295 duration: UFix64,
296 receiverCap : Capability<&{NonFungibleToken.Receiver}>) {
297
298 let TopShotIds = NFTCollection.getIDs()
299
300 assert(receiverCap.check(), message : "Non Valid Receiver Capability")
301
302 // create a new escrow item resource container
303 let item <- create EscrowItem(
304 _NFTCollection: <- NFTCollection,
305 _duration: duration,
306 _receiverCap: receiverCap)
307
308 let escrowID = item.escrowID
309 let startTime = item.startTime
310 // update the escrow items dictionary with the new resources
311 let oldItem <- self.escrowItems[escrowID] <- item
312 destroy oldItem
313
314 let owner = receiverCap.address
315
316 emit Escrowed(id: escrowID, owner: owner, NFTIds: TopShotIds, duration: duration, startTime: startTime)
317 }
318
319 pub fun borrowEscrow(escrowID: UInt64): &EscrowItem{EscrowItemPublic}? {
320 // Get the escrow item resources
321 if let escrowRef = (&self.escrowItems[escrowID] as &EscrowItem{EscrowItemPublic}?) {
322 return escrowRef
323 }
324 return nil
325 }
326
327 pub fun createEscrowRef(escrowID: UInt64): &EscrowItem {
328 // Get the escrow item resources
329 let escrowRef = (&self.escrowItems[escrowID] as &EscrowItem?)!
330 return escrowRef
331 }
332
333 destroy() {
334 assert(self.escrowItems.length == 0, message: "Escrow items still exist")
335 destroy self.escrowItems
336 }
337 }
338
339 // -----------------------------------------------------------------------
340 // TopShotEscrowV2 contract-level function definitions
341 // -----------------------------------------------------------------------
342
343 // createEscrowCollection returns a new EscrowCollection resource to the caller
344 pub fun createEscrowCollection(): @EscrowCollection {
345 let escrowCollection <- create EscrowCollection()
346
347 return <- escrowCollection
348 }
349
350 // createEscrow
351 pub fun createEscrow(
352 _ NFTCollection: @TopShot.Collection,
353 _ duration: UFix64,
354 _ receiverCap : Capability<&{NonFungibleToken.Receiver}>) {
355 let escrowCollectionRef = self.account.borrow<&TopShotEscrowV2.EscrowCollection>(from: self.escrowStoragePath) ??
356 panic("Couldn't borrow escrow collection")
357 escrowCollectionRef.createEscrow(NFTCollection: <- NFTCollection, duration: duration, receiverCap: receiverCap)
358 }
359
360 // redeem tokens
361 pub fun redeem(_ escrowID: UInt64, _ NFTIds: [UInt64]) {
362 let escrowCollectionRef = self.account.borrow<&TopShotEscrowV2.EscrowCollection>(from: self.escrowStoragePath) ??
363 panic("Couldn't borrow escrow collection")
364 let escrowRef = escrowCollectionRef.borrowEscrow(escrowID: escrowID)!
365 escrowRef.redeem(NFTIds: NFTIds)
366 if (escrowRef.redeemed){
367 destroy <- escrowCollectionRef.withdraw(escrowID: escrowID)
368 }
369 }
370
371 // batch redeem tokens
372 pub fun batchRedeem(_ escrowIDs: [UInt64]) {
373
374 for escrowID in escrowIDs {
375 let escrowCollectionRef = self.account.borrow<&TopShotEscrowV2.EscrowCollection>(from: self.escrowStoragePath) ??
376 panic("Couldn't borrow escrow collection")
377 let escrowRef = escrowCollectionRef.borrowEscrow(escrowID: escrowID)!
378 escrowRef.redeem(NFTIds: [])
379 if (escrowRef.redeemed){
380 destroy <- escrowCollectionRef.withdraw(escrowID: escrowID)
381 }
382 }
383 }
384
385 // -----------------------------------------------------------------------
386 // TopShotEscrowV2 initialization function
387 // -----------------------------------------------------------------------
388 //
389
390 init() {
391 self.totalEscrows = 0
392 self.escrowStoragePath= /storage/TopShotEscrowV2
393 self.escrowPublicPath= /public/TopShotEscrowV2
394
395 // Setup collection onto Deployer's account
396 let escrowCollection <- self.createEscrowCollection()
397 self.account.save(<- escrowCollection, to: self.escrowStoragePath)
398 self.account.link<&TopShotEscrowV2.EscrowCollection{TopShotEscrowV2.EscrowCollectionPublic}>(self.escrowPublicPath, target: self.escrowStoragePath)
399 }
400}
401