Smart Contract

CoatCheck

A.5c57f79c6694797f.CoatCheck

Deployed

2d ago
Feb 25, 2026, 11:48:26 PM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3
4
5// CoatCheck
6//
7// A Smart contract Meant to hold items for another address should they not be able to receive them.
8// this contract is essentially an escrow that no one holds the keys to. It will support holding any Fungible and NonFungible Tokens
9// with a specified address that is allowed to claim them. In this way, dapps which need to ensure that accounts are able to "Receive" assets
10// have a way of putting them into a holding resource that the intended receiver can redeem.
11pub contract CoatCheck {
12
13    // | ---------------- Events ----------------- |
14    pub event Initialized()
15
16    pub event TicketCreated(ticketResourceID: UInt64, redeemer: Address, fungibleTokenTypes: [Type], nonFungibleTokenTypes: [Type])
17
18    pub event TicketRedeemed(ticketResourceID: UInt64, redeemer: Address)
19
20    pub event ValetDestroyed(resourceID: UInt64)
21
22    access(account) let valet: @CoatCheck.Valet
23
24    // A CoatCheck has been initialized. This resource can now be watched for deposited items that accounts can redeem
25    pub event CoatCheckInitialized(resourceID: UInt64)
26
27    pub resource interface TicketPublic {
28        pub fun redeem(fungibleTokenReceiver: &{FungibleToken.Receiver}?, nonFungibleTokenReceiver: &{NonFungibleToken.CollectionPublic}?)
29        pub fun getDetails(): CoatCheck.TicketDetails
30    }
31
32    pub struct TicketDetails {
33        pub let nftTypes: [Type]
34        pub let vaultTypes: [Type]
35        pub let redeemer: Address
36
37        init(
38            redeemer: Address,
39            nftTypes: [Type],
40            vaultTypes: [Type]
41        ) {
42            self.redeemer = redeemer
43            self.nftTypes = nftTypes
44            self.vaultTypes = vaultTypes
45        }
46    }
47
48    // Tickets are used to fungible tokens, non-fungible tokens, or both.
49    // A ticket can be created by the CoatCheck valet, and can be redeemed only by
50    // capabilities owned by the designated redeemer of a ticket
51    pub resource Ticket: TicketPublic {
52        // a ticket can have Fungible Tokens AND NonFungibleTokens
53        access(self) var fungibleTokenVaults: @[FungibleToken.Vault]?
54        access(self) var nonFungibleTokens: @[NonFungibleToken.NFT]?
55        
56        pub let details: TicketDetails
57
58        // The following variables are maintained by the CoatCheck contract
59        access(self) var redeemed: Bool // a ticket can only be redeemed once. It also cannot be destroyed unless redeemed is set to true
60        access(self) var storageFee: UFix64 // the storage fee taken to hold this ticket in storage. It is returned when the ticket is redeemed.
61
62        init(
63            fungibleTokenVaults: @[FungibleToken.Vault]?,
64            nonFungibleTokens: @[NonFungibleToken.NFT]?,
65            redeemer: Address
66        ) {
67            assert(
68                fungibleTokenVaults != nil || nonFungibleTokens != nil, 
69                message: "must provide either FungibleToken vaults or NonFungibleToken NFTs"
70            )
71
72            let ftTypes: [Type] = []
73            let nftTypes: [Type] = []
74
75            let ticketVaults: @[FungibleToken.Vault] <- []
76            let ticketTokens: @[NonFungibleToken.NFT] <- []
77
78            if fungibleTokenVaults != nil {
79                while fungibleTokenVaults?.length! > 0 {
80                    let vault <- fungibleTokenVaults?.removeFirst()                  
81                    ftTypes.append(vault?.getType()!)
82                    ticketVaults.append(<-vault!)
83                }
84            }
85
86            if nonFungibleTokens != nil {
87                while nonFungibleTokens?.length! > 0 {
88                    let token <- nonFungibleTokens?.removeFirst()
89                    nftTypes.append(token?.getType()!)
90                    ticketTokens.append(<-token!)
91                }
92            }
93
94            if ftTypes.length > 0 {
95                self.fungibleTokenVaults <- ticketVaults
96            } else {
97                self.fungibleTokenVaults <- nil
98                destroy ticketVaults
99            }
100
101            if nftTypes.length > 0 {
102                self.nonFungibleTokens <- ticketTokens
103            } else {
104                self.nonFungibleTokens <- nil
105                destroy ticketTokens
106            }
107
108            self.details = TicketDetails(redeemer: redeemer, nftTypes: nftTypes, vaultTypes: ftTypes)
109        
110            self.redeemed = false
111            self.storageFee = 0.0
112
113            emit TicketCreated(ticketResourceID: self.uuid, redeemer: redeemer, fungibleTokenTypes: ftTypes, nonFungibleTokenTypes: nftTypes)
114
115            destroy fungibleTokenVaults
116            destroy nonFungibleTokens
117        }
118
119        // redeem the ticket using an optional receiver for fungible tokens and non-fungible tokens. The supplied receivers must be
120        // owned by the redeemer of this ticket.
121        pub fun redeem(fungibleTokenReceiver: &{FungibleToken.Receiver}?, nonFungibleTokenReceiver: &{NonFungibleToken.CollectionPublic}?) {
122            pre {
123                fungibleTokenReceiver == nil || (fungibleTokenReceiver!.owner!.address == self.details.redeemer) : "incorrect owner"
124                nonFungibleTokenReceiver == nil || (nonFungibleTokenReceiver!.owner!.address == self.details.redeemer) : "incorrect owner"
125                self.fungibleTokenVaults == nil || fungibleTokenReceiver != nil: "must provide fungibleTokenReceiver when there is a vault to claim"
126                self.nonFungibleTokens == nil || nonFungibleTokenReceiver != nil: "must provide nonFungibleTokenReceiver when there is a vault to claim"
127            }
128
129            self.redeemed = true
130
131            let vaults <- self.fungibleTokenVaults <- nil
132            let tokens <- self.nonFungibleTokens <- nil
133
134            // do we have vaults to distribute?
135            if vaults != nil && vaults?.length! > 0 {
136                while vaults?.length! > 0 {
137                    // pop them off our list of vaults one by one and deposit them
138                    let vault <- vaults?.remove(at: 0)!
139                    fungibleTokenReceiver!.deposit(from: <-vault)
140                }
141            }
142
143            // do we have nfts to distribute?
144            if tokens != nil && tokens?.length! > 0 {
145                while tokens?.length! > 0 {
146                    // pop them off our list of tokens one by one and deposit them
147                    let token <- tokens?.remove(at: 0)!
148                    nonFungibleTokenReceiver!.deposit(token: <-token)
149                }
150            }
151
152            emit TicketRedeemed(ticketResourceID: self.uuid, redeemer: self.details.redeemer)
153
154            destroy vaults
155            destroy tokens
156        }
157
158        pub fun getDetails(): CoatCheck.TicketDetails {
159            return self.details
160        }
161
162        destroy () {
163            pre {
164                self.redeemed : "not redeemed"
165            }
166        
167            destroy self.fungibleTokenVaults
168            destroy self.nonFungibleTokens
169        }
170    }
171
172    // ValetPublic contains our main entry-point methods that
173    // anyone can use to make/redeem tickets.
174    pub resource interface ValetPublic {
175        // redeem a ticket, supplying an optional receiver to use for depositing
176        // any fts or nfts in the ticket
177        pub fun redeemTicket(
178            ticketID: UInt64, 
179            fungibleTokenReceiver: &{FungibleToken.Receiver}?,
180            nonFungibleTokenReceiver: &{NonFungibleToken.CollectionPublic}?,
181        )
182        pub fun borrowTicket(ticketID: UInt64): &Ticket{TicketPublic}?
183        
184    }
185
186    pub resource Valet: ValetPublic {
187        access(self) var tickets: @{UInt64: Ticket}
188
189        // we store the fees taken when a ticket is made so that the exact amount is withdrawn
190        // when a ticket is redeemed
191        access(self) var feesByTicketID: {UInt64: UFix64}
192
193        init() {
194            self.tickets <- {}
195            self.feesByTicketID = {}
196        }
197
198        // Create a new ticket containing a list of vaults, nfts, or both.
199        // The creator of a ticket must also include a vault to pay fees with,
200        // and a receiver to refund the fee taken once a ticket is redeemed.
201        // Any extra tokens sent for the storage fee are sent back when the ticket is made
202        access(account) fun createTicket(
203            redeemer: Address, 
204            vaults: @[FungibleToken.Vault]?, 
205            tokens: @[NonFungibleToken.NFT]?, 
206        ) {
207            let ticket <- create Ticket(
208                fungibleTokenVaults: <-vaults,
209                nonFungibleTokens: <-tokens,
210                redeemer: redeemer,
211            )
212
213            let ticketID = ticket.uuid
214            let oldTicket <- self.tickets[ticketID] <- ticket
215            destroy oldTicket
216        }
217
218        pub fun borrowTicket(ticketID: UInt64): &Ticket{TicketPublic}? {
219             if self.tickets[ticketID] != nil {
220                return &self.tickets[ticketID] as! &Ticket{TicketPublic}?
221            } else {
222                return nil
223            }
224        }
225
226        // redeem the ticket using supplied receivers.
227        // if a ticket has fungible tokens, the fungibleTokenReceiver is required.
228        // if a ticket has nfts, the nonFungibleTokenReceiver is required.
229        pub fun redeemTicket(
230            ticketID: UInt64, 
231            fungibleTokenReceiver: &{FungibleToken.Receiver}?,
232            nonFungibleTokenReceiver: &{NonFungibleToken.CollectionPublic}?
233        ) {
234            pre {
235                self.tickets[ticketID] != nil : "ticket does not exist"
236            }
237            // take the ticket out of storage and redeem it
238            let ticket <-! self.tickets[ticketID] <- nil
239            ticket?.redeem(fungibleTokenReceiver: fungibleTokenReceiver, nonFungibleTokenReceiver: nonFungibleTokenReceiver)
240            destroy ticket
241        }
242
243        destroy () {
244            emit ValetDestroyed(resourceID: self.uuid)
245            destroy self.tickets
246        }
247    }
248
249    pub fun getValetPublic(): &CoatCheck.Valet{CoatCheck.ValetPublic} {
250        return &self.valet as &CoatCheck.Valet{CoatCheck.ValetPublic}
251    }
252
253    access(account) fun getValet(): &CoatCheck.Valet {
254        return &self.valet as &CoatCheck.Valet
255    }
256
257    init() {
258        self.valet <- create Valet()
259    }
260}
261