Smart Contract
TSHOTExchange
A.05b67ba314000b2d.TSHOTExchange
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import TopShot from 0x0b2a3299cc857e29
4import TSHOT from 0x05b67ba314000b2d
5import TopShotTiers from 0xb1788d64d512026d
6import TopShotShardedCollectionV2 from 0xb1788d64d512026d
7import RandomConsumer from 0x45caec600164c9e6
8import Burner from 0xf233dcee88fe0abe
9import Xorshift128plus from 0x45caec600164c9e6
10
11access(all) contract TSHOTExchange {
12
13 /* ─────────────────────────── Events ─────────────────────────── */
14 access(all) event NFTToTSHOTSwapCompleted(
15 payer: Address,
16 recipient: Address,
17 amountMinted: UFix64,
18 numNFTs: UInt64
19 )
20
21 access(all) event TSHOTToNFTSwapInitiated(
22 payer: Address,
23 amountCommitted: UFix64,
24 receiptID: UInt64
25 )
26
27 access(all) event TSHOTToNFTSwapCompleted(
28 payer: Address,
29 recipient: Address,
30 amountBurned: UFix64,
31 receiptID: UInt64,
32 numNFTs: UInt64
33 )
34
35 /* ───────────────────── Paths & state ───────────────────── */
36 access(all) let nftCollectionPath: StoragePath
37 access(all) let tshotAdminPath: StoragePath
38 access(self) let reserve: @TSHOT.Vault
39 access(all) let ReceiptStoragePath: StoragePath
40 access(self) let consumer: @RandomConsumer.Consumer
41
42 /* ───────────────────── Helper ───────────────────── */
43 access(all) fun validateNFT(nft: &TopShot.NFT): Bool {
44 let filter: {TopShotShardedCollectionV2.MomentFilter} =
45 TopShotShardedCollectionV2.CommonFandomFilter()
46 return filter.isSupported(moment: nft)
47 }
48
49 /* ───────────────── Receipt (unchanged) ───────────────── */
50 access(all) resource Receipt : RandomConsumer.RequestWrapper {
51 access(all) let betAmount: UFix64
52 access(all) var request: @RandomConsumer.Request?
53
54 init(betAmount: UFix64, request: @RandomConsumer.Request) {
55 self.betAmount = betAmount
56 self.request <- request
57 }
58 }
59
60 /* ─────────── NFT → TSHOT ─────────── */
61 access(all) fun swapNFTsForTSHOT(
62 payer: Address,
63 nftIDs: @[TopShot.NFT],
64 recipient: Address
65 ) {
66 pre { nftIDs.length > 0: "No NFTs provided." }
67
68 let numDeposited: UInt64 = UInt64(nftIDs.length)
69
70 let adminCollection = self.account
71 .storage
72 .borrow<&TopShotShardedCollectionV2.ShardedCollection>(
73 from: self.nftCollectionPath
74 ) ?? panic("admin collection")
75
76 let receiverRef = getAccount(recipient)
77 .capabilities
78 .get<&{FungibleToken.Receiver}>(/public/TSHOTTokenReceiver)
79 .borrow()
80 ?? panic("recipient TSHOT receiver")
81
82 let adminRef = self.account
83 .storage
84 .borrow<auth(TSHOT.AdminEntitlement) & TSHOT.Admin>(
85 from: self.tshotAdminPath
86 ) ?? panic("admin resource")
87
88 var minted: UFix64 = 0.0
89
90 // O(n) loop
91 while nftIDs.length > 0 {
92 let nft <- nftIDs.removeLast()
93 if !self.validateNFT(nft: &nft as &TopShot.NFT) {
94 panic("Moment tier invalid for TSHOT")
95 }
96
97 adminCollection.deposit(token: <- nft)
98
99 let vault <- adminRef.mintTokens(amount: 1.0)
100 receiverRef.deposit(from: <- vault)
101 minted = minted + 1.0
102 }
103 destroy nftIDs
104
105 emit NFTToTSHOTSwapCompleted(
106 payer: payer,
107 recipient: recipient,
108 amountMinted: minted,
109 numNFTs: numDeposited
110 )
111 }
112
113 /* ─────────── Commit ─────────── */
114 access(all) fun commitSwap(
115 payer: Address,
116 bet: @{FungibleToken.Vault}
117 ): @Receipt {
118 pre {
119 bet.balance > 0.0 : "Bet is zero."
120 bet.balance <= 50.0 : "Max 50 TSHOT per commit."
121 bet.getType() == Type<@TSHOT.Vault>(): "Vault must be TSHOT."
122 bet.balance % 1.0 == 0.0 : "Bet must be whole tokens."
123 }
124
125 let req <- self.consumer.requestRandomness()
126 let receipt <- create Receipt(
127 betAmount: bet.balance,
128 request: <- req
129 )
130
131 self.reserve.deposit(from: <- bet)
132
133 emit TSHOTToNFTSwapInitiated(
134 payer: payer,
135 amountCommitted: receipt.betAmount,
136 receiptID: receipt.uuid
137 )
138
139 return <- receipt
140 }
141
142 /* ─────────── Reveal ─────────── */
143 access(all) fun swapTSHOTForNFTs(
144 payer: Address,
145 recipient: Address,
146 receipt: @Receipt
147 ) {
148 pre {
149 receipt.request != nil :
150 "Receipt already used."
151 receipt.getRequestBlock()! < getCurrentBlock().height :
152 "Commit block has not passed."
153 }
154
155 let tokenAmount = receipt.betAmount
156 let receiptID = receipt.uuid
157
158 // Burn committed TSHOT
159 let burnVault <- self.reserve.withdraw(amount: tokenAmount) as! @TSHOT.Vault
160 TSHOT.burnTokens(from: <- burnVault)
161
162 // Fulfil randomness
163 let prg = self.consumer.fulfillWithPRG(
164 request: <- receipt.popRequest()
165 )
166 let prgRef: &Xorshift128plus.PRG = &prg
167
168 /* --- pick shard & IDs --- */
169 let adminCollection = self.account
170 .storage
171 .borrow<auth(NonFungibleToken.Withdraw) & TopShotShardedCollectionV2.ShardedCollection>(
172 from: self.nftCollectionPath
173 ) ?? panic("admin collection")
174
175 let bucketCount = adminCollection.getNumBuckets()
176 if bucketCount == 0 { panic("No shards available.") }
177
178 let shardIndex = RandomConsumer.getNumberInRange(
179 prg: prgRef, min: 0, max: bucketCount - 1
180 )
181
182 var ids: [UInt64] = adminCollection.getShardIDs(shardIndex: shardIndex)
183
184 if ids.length == 0 { panic("Selected shard empty.") }
185 if UInt64(tokenAmount) > UInt64(ids.length) {
186 panic("Not enough NFTs in shard.")
187 }
188
189 var selected: [UInt64] = []
190 var needed: UInt64 = UInt64(tokenAmount)
191 var upper = ids.length - 1
192
193 while needed > 0 {
194 let idx = RandomConsumer.getNumberInRange(
195 prg: prgRef,
196 min: 0,
197 max: UInt64(upper)
198 )
199 // manual swap chosen → end
200 let temp = ids[Int(idx)]
201 ids[Int(idx)] = ids[upper]
202 ids[upper] = temp
203
204 selected.append(ids.removeLast())
205 upper = upper - 1
206 needed = needed - 1
207 }
208
209 assert(
210 UInt64(selected.length) == UInt64(tokenAmount),
211 message: "Selection count mismatch"
212 )
213
214 let nfts <- adminCollection.batchWithdrawFromShard(
215 shardIndex: shardIndex,
216 ids: selected
217 )
218
219 let receiver = getAccount(recipient)
220 .capabilities
221 .get<&TopShot.Collection>(/public/MomentCollection)
222 .borrow()
223 ?? panic("recipient collection")
224
225 receiver.batchDeposit(tokens: <- nfts)
226
227 emit TSHOTToNFTSwapCompleted(
228 payer: payer,
229 recipient: recipient,
230 amountBurned: tokenAmount,
231 receiptID: receiptID,
232 numNFTs: UInt64(tokenAmount)
233 )
234
235 Burner.burn(<- receipt)
236 }
237
238 /* ─────────── Init ─────────── */
239 init() {
240 self.nftCollectionPath = /storage/ShardedMomentCollection
241 self.tshotAdminPath = /storage/TSHOTAdmin
242
243 self.reserve <- TSHOT.createEmptyVault(vaultType: Type<@TSHOT.Vault>())
244 self.consumer <- RandomConsumer.createConsumer()
245
246 self.ReceiptStoragePath =
247 StoragePath(identifier:
248 "TSHOTReceipt_".concat(self.account.address.toString())
249 )!
250 }
251}
252