Smart Contract

PDS

A.b6f2481eba4df97b.PDS

Deployed

1w ago
Feb 19, 2026, 10:36:10 AM UTC

Dependents

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