Smart Contract
PDS
A.b6f2481eba4df97b.PDS
1import NonFungibleToken from 0x1d7e57aa55817448
2import IPackNFT from 0x18ddf0823a55a0ee
3
4/// The Pack Distribution Service (PDS) contract is responsible for creating and managing distributions of packs.
5///
6access(all) contract PDS{
7 /// Entitlement that grants the ability to operate PDS functionalities.
8 ///
9 access(all) entitlement Operate
10
11 access(all) var version: String
12 access(all) let PackIssuerStoragePath: StoragePath
13 access(all) let PackIssuerCapRecv: PublicPath
14 access(all) let DistCreatorStoragePath: StoragePath
15 access(all) let DistManagerStoragePath: StoragePath
16
17 /// The next distribution ID to be used.
18 ///
19 access(all) var nextDistId: UInt64
20
21 /// Dictionary that stores distribution IDs to distribution details in the contract state.
22 ///
23 access(contract) let Distributions: {UInt64: DistInfo}
24
25 /// Dictionary that stores distribution IDs to shared capabilities in the contract state.
26 ///
27 access(contract) let DistSharedCap: @{UInt64: SharedCapabilities}
28
29 /// Emitted when an issuer has created a distribution.
30 ///
31 access(all) event DistributionCreated(DistId: UInt64, title: String, metadata: {String: String}, state: UInt8)
32
33 /// Emitted when an issuer has updated a distribution.
34 ///
35 access(all) event DistributionUpdated(DistId: UInt64, title: String, metadata: {String: String}, state: UInt8)
36
37 /// Emmitted when a distribution manager has updated a distribution state.
38 ///
39 access(all) event DistributionStateUpdated(DistId: UInt64, state: UInt8)
40
41 /// Enum that defines the status of a Distribution.
42 ///
43 access(all) enum DistState: UInt8 {
44 access(all) case Initialized
45 access(all) case Invalid
46 access(all) case Complete
47 }
48
49 /// Struct that defines the details of a Distribution.
50 ///
51 access(all) struct DistInfo {
52 access(all) var title: String
53 access(all) var metadata: {String: String}
54 access(all) var state: PDS.DistState
55
56 access(contract) fun setState(newState: PDS.DistState) {
57 self.state = newState
58 }
59
60 access(contract) fun update(title: String, metadata: {String: String}) {
61 self.title = title
62 self.metadata = metadata
63 }
64
65 /// DistInfo struct initializer.
66 ///
67 view init(title: String, metadata: {String: String}) {
68 self.title = title
69 self.metadata = metadata
70 self.state = PDS.DistState.Initialized
71 }
72 }
73
74 /// Struct that defines a Collectible.
75 ///
76 access(all) struct Collectible: IPackNFT.Collectible {
77 access(all) let address: Address
78 access(all) let contractName: String
79 access(all) let id: UInt64
80
81 // returning in string so that it is more readable and anyone can check the hash
82 access(all) view fun hashString(): String {
83 // address string is 16 characters long with 0x as prefix (for 8 bytes in hex)
84 // example: ,f3fcd2c1a78f5ee.ExampleNFT.12
85 let c = "A."
86 var a = ""
87 let addrStr = self.address.toString()
88 if addrStr.length < 18 {
89 let padding = 18 - addrStr.length
90 let p = "0"
91 var i = 0
92 a = addrStr.slice(from: 2, upTo: addrStr.length)
93 while i < padding {
94 a = p.concat(a)
95 i = i + 1
96 }
97 } else {
98 a = addrStr.slice(from: 2, upTo: 18)
99 }
100 return c.concat(a).concat(".").concat(self.contractName).concat(".").concat(self.id.toString())
101 }
102
103 /// Collectible struct initializer.
104 ///
105 view init(address: Address, contractName: String, id: UInt64) {
106 self.address = address
107 self.contractName = contractName
108 self.id = id
109 }
110 }
111
112 /// Resource that defines the shared capabilities required for creating and managing Pack NFTs.
113 ///
114 access(all) resource SharedCapabilities {
115 /// Capability to withdraw NFTs from the issuer.
116 ///
117 access(self) let withdrawCap: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>
118
119 /// Capability to mint, reveal, and open Pack NFTs.
120 ///
121 access(self) let operatorCap: Capability<auth(IPackNFT.Operate) &{IPackNFT.IOperator}>
122
123 /// Withdraw an NFT from the issuer.
124 ///
125 access(contract) fun withdrawFromIssuer(withdrawID: UInt64): @{NonFungibleToken.NFT} {
126 let c = self.withdrawCap.borrow() ?? panic("no such cap")
127 return <- c.withdraw(withdrawID: withdrawID)
128 }
129
130 /// Mint Pack NFTs.
131 ///
132 access(contract) fun mintPackNFT(distId: UInt64, commitHashes: [String], issuer: Address, recvCap: &{NonFungibleToken.CollectionPublic}) {
133 var i = 0
134 let c = self.operatorCap.borrow() ?? panic("no such cap")
135 while i < commitHashes.length{
136 let nft <- c.mint(distId: distId, commitHash: commitHashes[i], issuer: issuer)
137 i = i + 1
138 let n <- nft
139 recvCap.deposit(token: <- n)
140 }
141 }
142
143 /// Reveal Pack NFTs.
144 ///
145 access(contract) fun revealPackNFT(packId: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) {
146 let c = self.operatorCap.borrow() ?? panic("no such cap")
147 c.reveal(id: packId, nfts: nfts, salt: salt)
148 }
149
150 /// Open Pack NFTs.
151 ///
152 access(contract) fun openPackNFT(packId: UInt64, nfts: [{IPackNFT.Collectible}], recvCap: &{NonFungibleToken.CollectionPublic}, collectionStoragePath: StoragePath?) {
153 let c = self.operatorCap.borrow() ?? panic("no such cap")
154 let toReleaseNFTs: [UInt64] = []
155 var i = 0
156 while i < nfts.length {
157 toReleaseNFTs.append(nfts[i].id)
158 i = i + 1
159 }
160 c.open(id: packId, nfts: nfts)
161 if collectionStoragePath == nil {
162 self.fulfillFromIssuer(nftIds: toReleaseNFTs, recvCap: recvCap)
163 } else {
164 self.releaseEscrow(nftIds: toReleaseNFTs, recvCap: recvCap , collectionStoragePath: collectionStoragePath!)
165 }
166 }
167
168 /// Release escrowed NFTs to the receiver.
169 ///
170 access(contract) fun releaseEscrow(nftIds: [UInt64], recvCap: &{NonFungibleToken.CollectionPublic}, collectionStoragePath: StoragePath) {
171 let pdsCollection = PDS.account.storage.borrow<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>(from: collectionStoragePath)
172 ?? panic("Unable to borrow PDS collection provider capability from private path")
173 var i = 0
174 while i < nftIds.length {
175 recvCap.deposit(token: <- pdsCollection.withdraw(withdrawID: nftIds[i]))
176 i = i + 1
177 }
178 }
179
180 /// Release NFTs from the issuer to the receiver.
181 ///
182 access(contract) fun fulfillFromIssuer(nftIds: [UInt64], recvCap: &{NonFungibleToken.CollectionPublic}) {
183 let issuerCollection = self.withdrawCap.borrow() ?? panic("Unable to borrow withdrawCap")
184 var i = 0
185 while i < nftIds.length {
186 recvCap.deposit(token: <- issuerCollection.withdraw(withdrawID: nftIds[i]))
187 i = i + 1
188 }
189 }
190
191 /// SharedCapabilities resource initializer.
192 ///
193 view init(
194 withdrawCap: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>,
195 operatorCap: Capability<auth(IPackNFT.Operate) &{IPackNFT.IOperator}>
196 ) {
197 self.withdrawCap = withdrawCap
198 self.operatorCap = operatorCap
199 }
200 }
201
202
203 // Included for backwards compatibility.
204 access(all) resource interface PackIssuerCapReciever {}
205
206 /// Resource that defines the issuer of a pack.
207 ///
208 access(all) resource PackIssuer: PackIssuerCapReciever {
209 access(self) var cap: Capability<&DistributionCreator>?
210
211 /// Set the capability to create a distribution; the function is publicly accessible but requires a capability argument to a DistrubutionCreator admin resource.
212 ///
213 access(all) fun setDistCap(cap: Capability<&DistributionCreator>) {
214 pre {
215 cap.check(): "Invalid capability"
216 }
217 self.cap = cap
218 }
219
220 access(Operate) fun createDist(sharedCap: @SharedCapabilities, title: String, metadata: {String: String}) {
221 assert(title.length > 0, message: "Title must not be empty")
222 let c = self.cap!.borrow()!
223 c.createNewDist(sharedCap: <- sharedCap, title: title, metadata: metadata)
224 }
225
226 /// PackIssuer resource initializer.
227 ///
228 view init() {
229 self.cap = nil
230 }
231 }
232
233 // Included for backwards compatibility.
234 access(all) resource interface IDistCreator {}
235
236 /// Resource that defines the creator of a distribution.
237 ///
238 access(all) resource DistributionCreator: IDistCreator {
239 access(contract) fun createNewDist(sharedCap: @SharedCapabilities, title: String, metadata: {String: String}) {
240 let currentId = PDS.nextDistId
241 PDS.DistSharedCap[currentId] <-! sharedCap
242 PDS.Distributions[currentId] = DistInfo(title: title, metadata: metadata)
243 PDS.nextDistId = currentId + 1
244 emit DistributionCreated(DistId: currentId, title: title, metadata: metadata, state: 0)
245 }
246 }
247
248 /// Resource that defines the manager of a distribution.
249 ///
250 access(all) resource DistributionManager {
251 access(Operate) fun updateDistState(distId: UInt64, state: PDS.DistState) {
252 let d = PDS.Distributions.remove(key: distId) ?? panic ("No such distribution")
253 d.setState(newState: state)
254 PDS.Distributions.insert(key: distId, d)
255 emit DistributionStateUpdated(DistId: distId, state: state.rawValue)
256 }
257
258 access(Operate) fun updateDist(distId: UInt64, title: String, metadata: {String: String}) {
259 pre {
260 title.length > 0: "Title must not be empty"
261 }
262 let d = PDS.Distributions.remove(key: distId) ?? panic ("No such distribution")
263 d.update(title: title, metadata: metadata)
264 PDS.Distributions.insert(key: distId, d)
265 emit DistributionUpdated(DistId: distId, title: title, metadata: metadata, state: d.state.rawValue)
266 }
267
268 access(Operate) fun withdraw(distId: UInt64, nftIDs: [UInt64], escrowCollectionPublic: PublicPath) {
269 assert(PDS.DistSharedCap.containsKey(distId), message: "No such distribution")
270 let d <- PDS.DistSharedCap.remove(key: distId)!
271 let pdsCollection = PDS.getManagerCollectionCap(escrowCollectionPublic: escrowCollectionPublic).borrow()!
272 var i = 0
273 while i < nftIDs.length {
274 let nft <- d.withdrawFromIssuer(withdrawID: nftIDs[i])
275 pdsCollection.deposit(token:<-nft)
276 i = i + 1
277 }
278 PDS.DistSharedCap[distId] <-! d
279 }
280
281 access(Operate) fun mintPackNFT(distId: UInt64, commitHashes: [String], issuer: Address, recvCap: &{NonFungibleToken.CollectionPublic}) {
282 assert(PDS.DistSharedCap.containsKey(distId), message: "No such distribution")
283 let d <- PDS.DistSharedCap.remove(key: distId)!
284 d.mintPackNFT(distId: distId, commitHashes: commitHashes, issuer: issuer, recvCap: recvCap)
285 PDS.DistSharedCap[distId] <-! d
286 }
287
288 access(Operate) fun revealPackNFT(distId: UInt64, packId: UInt64, nftContractAddrs: [Address], nftContractNames: [String], nftIds: [UInt64], salt: String) {
289 assert(PDS.DistSharedCap.containsKey(distId), message: "No such distribution")
290 assert(
291 nftContractAddrs.length == nftContractNames.length &&
292 nftContractNames.length == nftIds.length,
293 message: "NFTs must be fully described"
294 )
295 let d <- PDS.DistSharedCap.remove(key: distId)!
296 let arr: [{IPackNFT.Collectible}] = []
297 var i = 0
298 while i < nftContractAddrs.length {
299 let s = Collectible(address: nftContractAddrs[i], contractName: nftContractNames[i], id: nftIds[i])
300 arr.append(s)
301 i = i + 1
302 }
303 d.revealPackNFT(packId: packId, nfts: arr, salt: salt)
304 PDS.DistSharedCap[distId] <-! d
305 }
306
307 access(Operate) fun openPackNFT(
308 distId: UInt64,
309 packId: UInt64,
310 nftContractAddrs: [Address],
311 nftContractNames: [String],
312 nftIds: [UInt64],
313 recvCap: &{NonFungibleToken.CollectionPublic},
314 collectionStoragePath: StoragePath?
315 ) {
316 assert(PDS.DistSharedCap.containsKey(distId), message: "No such distribution")
317 let d <- PDS.DistSharedCap.remove(key: distId)!
318 let arr: [{IPackNFT.Collectible}] = []
319 var i = 0
320 while i < nftContractAddrs.length {
321 let s = Collectible(address: nftContractAddrs[i], contractName: nftContractNames[i], id: nftIds[i])
322 arr.append(s)
323 i = i + 1
324 }
325 d.openPackNFT(packId: packId, nfts: arr, recvCap: recvCap, collectionStoragePath: collectionStoragePath)
326 PDS.DistSharedCap[distId] <-! d
327 }
328
329 }
330
331 /// Returns the manager collection capability to receive NFTs to be escrowed.
332 ///
333 access(contract) view fun getManagerCollectionCap(escrowCollectionPublic: PublicPath): Capability<&{NonFungibleToken.CollectionPublic}> {
334 let pdsCollection = self.account.capabilities.get<&{NonFungibleToken.CollectionPublic}>(escrowCollectionPublic)!
335 assert(pdsCollection.check(), message: "Please ensure PDS has created and linked a Collection for recieving escrows")
336 return pdsCollection
337 }
338
339
340
341 /// Create a PackIssuer resource and return it to the caller.
342 access(all) fun createPackIssuer(): @PackIssuer{
343 return <- create PackIssuer()
344 }
345
346 /// Create a SharedCapabilities resource and return it to the caller.
347 ///
348 access(all) fun createSharedCapabilities(
349 withdrawCap: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>,
350 operatorCap: Capability<auth(IPackNFT.Operate) &{IPackNFT.IOperator}>
351 ): @SharedCapabilities {
352 return <- create SharedCapabilities(
353 withdrawCap: withdrawCap,
354 operatorCap: operatorCap
355 )
356 }
357
358 /// Returns the details of a distribution if it exists, nil otherwise.
359 ///
360 access(all) view fun getDistInfo(distId: UInt64): DistInfo? {
361 return PDS.Distributions[distId]
362 }
363
364 /// PDS contract initializer.
365 ///
366 init(
367 PackIssuerStoragePath: StoragePath,
368 PackIssuerCapRecv: PublicPath,
369 DistCreatorStoragePath: StoragePath,
370 DistManagerStoragePath: StoragePath,
371 version: String
372 ) {
373 self.nextDistId = 1
374 self.DistSharedCap <- {}
375 self.Distributions = {}
376 self.PackIssuerStoragePath = PackIssuerStoragePath
377 self.PackIssuerCapRecv = PackIssuerCapRecv
378 self.DistCreatorStoragePath = DistCreatorStoragePath
379 self.DistManagerStoragePath = DistManagerStoragePath
380 self.version = version
381
382 // Create a DistributionCreator resource to share create capability with PackIssuer.
383 self.account.storage.save(<- create DistributionCreator(), to: self.DistCreatorStoragePath)
384
385 // Create a DistributionManager resource to manager distributions (withdraw for escrow, mint PackNFT todo: reveal / transfer).
386 self.account.storage.save(<- create DistributionManager(), to: self.DistManagerStoragePath)
387 }
388}
389