Smart Contract

TSHOTExchange

A.05b67ba314000b2d.TSHOTExchange

Deployed

1w ago
Feb 19, 2026, 09:18:49 AM UTC

Dependents

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