Smart Contract

FRC20TradingRecord

A.d2abb5dbf5e08666.FRC20TradingRecord

Valid From

86,128,698

Deployed

2d ago
Feb 24, 2026, 11:54:28 PM UTC

Dependents

16 imports
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FRC20TradingRecord
5
6The contract is used to record the trading records of the fungible tokens.
7
8*/
9import SwapConfig from 0xb78ef7afa52ff906
10// Fixes Imports
11import Fixes from 0xd2abb5dbf5e08666
12import FRC20Indexer from 0xd2abb5dbf5e08666
13import FRC20FTShared from 0xd2abb5dbf5e08666
14
15access(all) contract FRC20TradingRecord {
16    /* --- Events --- */
17
18    /// Event emitted when the contract is initialized
19    access(all) event ContractInitialized()
20
21    /// Event emitted when a record is created
22    access(all) event RecordCreated(
23        recorder: Address,
24        storefront: Address,
25        buyer: Address,
26        seller: Address,
27        tick: String,
28        dealAmount: UFix64,
29        dealPrice: UFix64,
30        dealPricePerMint: UFix64,
31    )
32
33    /* --- Variable, Enums and Structs --- */
34    access(all)
35    let TradingRecordsStoragePath: StoragePath
36    access(all)
37    let TradingRecordsPublicPath: PublicPath
38
39    /* --- Interfaces & Resources --- */
40
41    /// The struct containing the transaction record
42    ///
43    access(all) struct TransactionRecord {
44        access(all)
45        let storefront: Address
46        access(all)
47        let buyer: Address
48        access(all)
49        let seller: Address
50        access(all)
51        let tick: String
52        access(all)
53        let dealAmount: UFix64
54        access(all)
55        let dealPrice: UFix64
56        access(all)
57        let dealPricePerMint: UFix64
58        access(all)
59        let timestamp: UInt64
60
61        init(
62            storefront: Address,
63            buyer: Address,
64            seller: Address,
65            tick: String,
66            dealAmount: UFix64,
67            dealPrice: UFix64,
68            dealPricePerMint: UFix64,
69        ) {
70            self.storefront = storefront
71            self.buyer = buyer
72            self.seller = seller
73            self.tick = tick
74            self.dealAmount = dealAmount
75            self.dealPrice = dealPrice
76            self.dealPricePerMint = dealPricePerMint
77            self.timestamp = UInt64(getCurrentBlock().timestamp)
78        }
79
80        access(all)
81        view fun getDealPricePerToken(): UFix64 {
82            if self.dealAmount == 0.0 {
83                return 0.0
84            }
85            return self.dealPrice / self.dealAmount
86        }
87    }
88
89    /// The struct containing the trading status
90    ///
91    access(all) struct TradingStatus {
92        access(all)
93        var dealFloorPricePerToken: UFix64
94        access(all)
95        var dealFloorPricePerMint: UFix64
96        access(all)
97        var dealCeilingPricePerToken: UFix64
98        access(all)
99        var dealCeilingPricePerMint: UFix64
100        access(all)
101        var dealAmount: UFix64
102        access(all)
103        var volume: UFix64
104        access(all)
105        var sales: UInt64
106
107        init() {
108            self.dealFloorPricePerToken = 0.0
109            self.dealFloorPricePerMint = 0.0
110            self.dealCeilingPricePerToken = 0.0
111            self.dealCeilingPricePerMint = 0.0
112            self.dealAmount = 0.0
113            self.volume = 0.0
114            self.sales = 0
115        }
116
117        access(contract)
118        fun updateByNewRecord(
119            _ recordRef: &TransactionRecord
120        ) {
121            // update the trading price
122            let dealPricePerToken = recordRef.getDealPricePerToken()
123            let dealPricePerMint = recordRef.dealPricePerMint
124
125            // update the floor price per token
126            if self.dealFloorPricePerToken == 0.0 || dealPricePerToken < self.dealFloorPricePerToken {
127                self.dealFloorPricePerToken = dealPricePerToken
128            }
129            // update the floor price per mint
130            if self.dealFloorPricePerMint == 0.0 || dealPricePerMint < self.dealFloorPricePerMint {
131                self.dealFloorPricePerMint = dealPricePerMint
132            }
133            // update the ceiling price per token
134            if dealPricePerToken > self.dealCeilingPricePerToken {
135                self.dealCeilingPricePerToken = dealPricePerToken
136            }
137            // update the ceiling price per mint
138            if dealPricePerMint > self.dealCeilingPricePerMint {
139                self.dealCeilingPricePerMint = dealPricePerMint
140            }
141            // update the deal amount
142            self.dealAmount = self.dealAmount + recordRef.dealAmount
143            // update the volume
144            self.volume = self.volume + recordRef.dealPrice
145            // update the sales
146            self.sales = self.sales + 1
147        }
148    }
149
150    /// The interface for viewing the trading status
151    ///
152    access(all) resource interface TradingStatusViewer {
153        access(all)
154        view fun getStatus(): TradingStatus
155    }
156
157    /// The resource containing the trading status
158    ///
159    access(all) resource BasicRecord: TradingStatusViewer {
160        access(all)
161        let status: TradingStatus
162
163        init() {
164            self.status = TradingStatus()
165        }
166
167        access(all)
168        view fun getStatus(): TradingStatus {
169            return self.status
170        }
171
172        access(contract)
173        fun updateByNewRecord(
174            _ recordRef: &TransactionRecord
175        ) {
176            self.status.updateByNewRecord(recordRef)
177        }
178    }
179
180    /// The interface for viewing the daily records
181    ///
182    access(all) resource interface DailyRecordsPublic {
183        /// Get the length of the records
184        access(all)
185        view fun getRecordLength(): UInt64
186        /// Get the records of the page
187        access(all)
188        view fun getRecords(page: Int, pageSize: Int, offset: Int?): [TransactionRecord]
189        /// Available minutes
190        access(all)
191        view fun getMintesWithStatus(): [UInt64]
192        /// Get the trading status
193        access(all)
194        view fun borrowMinutesStatus(_ time: UInt64): &BasicRecord?
195        /// Get the buyer addresses
196        access(all)
197        view fun getBuyerAddresses(): [Address]
198        /// Get the trading volume of the address
199        access(all)
200        view fun getAddressBuyVolume(_ addr: Address): UFix64?
201        /// Get the seller addresses
202        access(all)
203        view fun getSellerAddresses(): [Address]
204        /// Get the trading volume of the address
205        access(all)
206        view fun getAddressSellVolume(_ addr: Address): UFix64?
207    }
208
209    /// The resource containing the daily records
210    //
211    access(all) resource DailyRecords: DailyRecordsPublic, TradingStatusViewer {
212        /// The date of the records, in seconds
213        access(all)
214        let date: UInt64
215        /// The trading status of the day
216        access(all)
217        let status: TradingStatus
218        /// Deal records, sorted by timestamp, descending
219        access(contract)
220        let records: [TransactionRecord]
221        /// Minute => TradingStatus
222        access(self)
223        let minutes: @{UInt64: BasicRecord}
224        // Address => TradingVolume
225        access(self)
226        let buyerVolumes: {Address: UFix64}
227        // Address => TradingVolume
228        access(self)
229        let sellerVolumes: {Address: UFix64}
230
231        init(date: UInt64) {
232            self.date = date
233            self.status = TradingStatus()
234            self.records = []
235            self.minutes <- {}
236            self.buyerVolumes = {}
237            self.sellerVolumes = {}
238        }
239
240        /** Public methods */
241
242        access(all)
243        view fun getRecordLength(): UInt64 {
244            return UInt64(self.records.length)
245        }
246
247        access(all)
248        view fun getRecords(page: Int, pageSize: Int, offset: Int?): [TransactionRecord] {
249            var start = page * pageSize + (offset ?? 0)
250            if start < 0 {
251                start = 0
252            } else if start > self.records.length {
253                return []
254            }
255            var end = start + pageSize
256            if end > self.records.length {
257                end = self.records.length
258            }
259            return self.records.slice(from: start, upTo: end)
260        }
261
262        access(all)
263        view fun getStatus(): TradingStatus {
264            return self.status
265        }
266
267        /// Available minutes
268        ///
269        access(all)
270        view fun getMintesWithStatus(): [UInt64] {
271            return self.minutes.keys
272        }
273
274        /// Get the trading status
275        ///
276        access(all)
277        view fun borrowMinutesStatus(_ time: UInt64): &BasicRecord? {
278            let minuteTime = self.convertToMinute(time)
279            return &self.minutes[minuteTime]
280        }
281
282        /// Get the buyer addresses
283        access(all)
284        view fun getBuyerAddresses(): [Address] {
285            return self.buyerVolumes.keys
286        }
287
288        /// Get the trading volume of the address
289        access(all)
290        view fun getAddressBuyVolume(_ addr: Address): UFix64? {
291            return self.buyerVolumes[addr]
292        }
293
294        /// Get the seller addresses
295        access(all)
296        view fun getSellerAddresses(): [Address] {
297            return self.sellerVolumes.keys
298        }
299
300        /// Get the trading volume of the address
301        access(all)
302        view fun getAddressSellVolume(_ addr: Address): UFix64? {
303            return self.sellerVolumes[addr]
304        }
305
306        /** Internal Methods */
307
308        access(contract)
309        fun addRecord(record: TransactionRecord) {
310            // timestamp is in seconds, not milliseconds
311            let timestamp = record.timestamp
312            // ensure the timestamp is in the same day
313            if timestamp / 86400 != self.date / 86400 {
314                return // DO NOT PANIC
315            }
316            let recorder = self.owner?.address
317            if recorder == nil {
318                return // DO NOT PANIC
319            }
320            let recordRef = &record as &TransactionRecord
321            // update the trading status
322            let statusRef = self.borrowStatus()
323            statusRef.updateByNewRecord(recordRef)
324
325            // update the minutes
326            let minuteTime = self.convertToMinute(timestamp)
327            var minuteRecordsRef = self.borrowMinute(minuteTime)
328            if minuteRecordsRef == nil {
329                self.minutes[minuteTime] <-! create BasicRecord()
330                minuteRecordsRef = self.borrowMinute(minuteTime)
331            }
332            if minuteRecordsRef != nil {
333                minuteRecordsRef!.updateByNewRecord(recordRef)
334            }
335
336            // record detailed trading volume
337            self.buyerVolumes[record.buyer] = (self.buyerVolumes[record.buyer] ?? 0.0) + record.dealPrice
338            self.sellerVolumes[record.seller] = (self.sellerVolumes[record.seller] ?? 0.0) + record.dealPrice
339
340            // add the record, sorted by timestamp, descending
341            self.records.insert(at: 0, record)
342
343            // emit the event
344            emit RecordCreated(
345                recorder: recorder!,
346                storefront: record.storefront,
347                buyer: record.buyer,
348                seller: record.seller,
349                tick: record.tick,
350                dealAmount: record.dealAmount,
351                dealPrice: record.dealPrice,
352                dealPricePerMint: record.dealPricePerMint
353            )
354        }
355
356        access(self)
357        view fun convertToMinute(_ time: UInt64): UInt64 {
358            return time - time % 60
359        }
360
361        access(self)
362        view fun borrowStatus(): &TradingStatus {
363            return &self.status
364        }
365
366        access(self)
367        view fun borrowMinute(_ time: UInt64): &BasicRecord? {
368            let minuteTime = self.convertToMinute(time)
369            return &self.minutes[minuteTime]
370        }
371    }
372
373    access(all) resource interface TradingRecordsPublic {
374        access(all)
375        view fun borrowDailyRecords(_ date: UInt64): &DailyRecords?
376        // ---- 2x Traders Points ----
377        access(all)
378        view fun getTraders(): [Address]
379        access(all)
380        view fun getTradersPoints(_ addr: Address): UFix64
381        // ---- 10x Traders Points ----
382        access(all)
383        view fun get10xTraders(): [Address]
384        access(all)
385        view fun get10xTradersPoints(_ addr: Address): UFix64
386        // ---- 100x Traders  Points ----
387        access(all)
388        view fun get100xTraders(): [Address]
389        access(all)
390        view fun get100xTradersPoints(_ addr: Address): UFix64
391    }
392
393    /// The resource containing the trading volume
394    ///
395    access(all) resource TradingRecords: TradingRecordsPublic, TradingStatusViewer, FRC20FTShared.TransactionHook {
396        access(self)
397        let tick: String?
398        /// Trading status
399        access(self)
400        let status: TradingStatus
401        /// Date => DailyRecords
402        access(self)
403        let dailyRecords: @{UInt64: DailyRecords}
404        // > 2x traders address => Points
405        access(self)
406        let traderPoints: {Address: UFix64}
407        /// > 10x traders address => Points
408        access(self)
409        let traders10xBenchmark: {Address: UFix64}
410        /// > 100x traders address => Points
411        access(self)
412        let traders100xBenchmark: {Address: UFix64}
413
414        init(
415            _ tick: String?
416        ) {
417            self.tick = tick
418            self.dailyRecords <- {}
419            self.status = TradingStatus()
420            self.traderPoints = {}
421            self.traders10xBenchmark = {}
422            self.traders100xBenchmark = {}
423        }
424
425        access(all)
426        view fun getStatus(): TradingStatus {
427            return self.status
428        }
429
430        /// Get the public daily records
431        ///
432        access(all)
433        view fun borrowDailyRecords(_ date: UInt64): &DailyRecords? {
434            let date = self.convertToDate(date)
435            return &self.dailyRecords[date]
436        }
437
438        // ---- Traders Points ----
439
440        /// Get the 2x traders
441        ///
442        access(all)
443        view fun getTraders(): [Address] {
444            return self.traderPoints.keys
445        }
446
447        /// Get the 2x traders points
448        ///
449        access(all)
450        view fun getTradersPoints(_ addr: Address): UFix64 {
451            return self.traderPoints[addr] ?? 0.0
452        }
453
454        /// Get the 10x traders
455        ///
456        access(all)
457        view fun get10xTraders(): [Address] {
458            return self.traders10xBenchmark.keys
459        }
460
461        /// Get the 10x traders points
462        ///
463        access(all)
464        view fun get10xTradersPoints(_ addr: Address): UFix64 {
465            return self.traders10xBenchmark[addr] ?? 0.0
466        }
467
468        /// Get the 100x traders
469        ///
470        access(all)
471        view fun get100xTraders(): [Address] {
472            return self.traders100xBenchmark.keys
473        }
474
475        /// Get the 100x traders points
476        ///
477        access(all)
478        view fun get100xTradersPoints(_ addr: Address): UFix64 {
479            return self.traders100xBenchmark[addr] ?? 0.0
480        }
481
482        // --- FRC20FTShared.TransactionHook ---
483
484        /// The method that is invoked when the transaction is executed
485        /// Before try-catch is deployed, please ensure that there will be no panic inside the method.
486        ///
487        access(account)
488        fun onDeal(
489            seller: Address,
490            buyer: Address,
491            tick: String,
492            dealAmount: UFix64,
493            dealPrice: UFix64,
494            storefront: Address,
495            listingId: UInt64?,
496        ) {
497            if self.owner == nil {
498                return // DO NOT PANIC
499            }
500
501            var dealPricePerMint = 0.0
502            // for frc20
503            let frc20Indexer = FRC20Indexer.getIndexer()
504            if let meta = frc20Indexer.getTokenMeta(tick: tick) {
505                dealPricePerMint = SwapConfig.ScaledUInt256ToUFix64(
506                    SwapConfig.UFix64ToScaledUInt256(meta.limit) / SwapConfig.UFix64ToScaledUInt256(dealAmount) * SwapConfig.UFix64ToScaledUInt256(dealPrice)
507                )
508            }
509            let newRecord = TransactionRecord(
510                storefront: storefront,
511                buyer: buyer,
512                seller: seller,
513                tick: tick,
514                dealAmount: dealAmount,
515                dealPrice: dealPrice,
516                dealPricePerMint: dealPricePerMint
517            )
518
519            self.addRecord(record: newRecord)
520        }
521
522        /** Internal Methods */
523
524        access(contract)
525        fun addRecord(record: TransactionRecord) {
526            // timestamp is in seconds, not milliseconds
527            let timestamp = record.timestamp
528            let date = self.convertToDate(timestamp)
529
530            var dailyRecordsRef = self.borrowDailyRecords(date)
531            if dailyRecordsRef == nil {
532                self.dailyRecords[date] <-! create DailyRecords(date: date)
533                dailyRecordsRef = self.borrowDailyRecords(date)
534            }
535            if dailyRecordsRef == nil {
536                return // DO NOT PANIC
537            }
538
539            // update the trading status
540            let statusRef = self.borrowStatus()
541            statusRef.updateByNewRecord(&record as &TransactionRecord)
542
543            // add to the daily records
544            dailyRecordsRef!.addRecord(record: record)
545
546            /// calculate the traders points for frc20
547            let frcIndexer = FRC20Indexer.getIndexer()
548            /// Here is the points for frc20 traders
549            if let tokenMeta = frcIndexer.getTokenMeta(tick: record.tick) {
550                // get the benchmark value
551                var benchmarkValue = frcIndexer.getBenchmarkValue(tick: record.tick)
552                if benchmarkValue == 0.0 {
553                    benchmarkValue = 0.00000001
554                }
555                let benchmarkPrice = benchmarkValue * record.dealAmount
556                let mintAmount = record.dealAmount / tokenMeta.limit
557                // Check if buyer / seller are an 2x traders
558                if record.dealPrice > benchmarkPrice * 2.0 {
559                    let points = 1.0 * record.dealPrice / benchmarkPrice * mintAmount
560                    // earn trading points = 2x points
561                    self.traderPoints[record.buyer] = (self.traderPoints[record.buyer] ?? 0.0) + points
562                    self.traderPoints[record.seller] = (self.traderPoints[record.seller] ?? 0.0) + points
563                }
564                // Check if buyer / seller are an 10x traders, if yes, add extra points
565                if record.dealPrice > benchmarkPrice * 10.0 {
566                    let points = 5.0 * (record.dealPrice - benchmarkPrice * 10.0) / benchmarkPrice * mintAmount
567                    // earn trading points = 10x points + 2x points
568                    self.traders10xBenchmark[record.buyer] = (self.traders10xBenchmark[record.buyer] ?? 0.0) + points
569                    self.traders10xBenchmark[record.seller] = (self.traders10xBenchmark[record.seller] ?? 0.0) + points
570                    // add to the 2x traders points
571                    self.traderPoints[record.buyer] = (self.traderPoints[record.buyer] ?? 0.0) + points
572                    self.traderPoints[record.seller] = (self.traderPoints[record.seller] ?? 0.0) + points
573                }
574                // Check if buyer / seller are an 100x traders, if yes, add extra points
575                if record.dealPrice > benchmarkPrice * 100.0 {
576                    let points = 10.0 * (record.dealPrice - benchmarkPrice * 100.0) / benchmarkPrice * mintAmount
577                    // earn trading points = 100x points + 10x points + 2x points
578                    self.traders100xBenchmark[record.buyer] = (self.traders100xBenchmark[record.buyer] ?? 0.0) + points
579                    self.traders100xBenchmark[record.seller] = (self.traders100xBenchmark[record.seller] ?? 0.0) + points
580                    // add to the 10x traders points
581                    self.traders10xBenchmark[record.buyer] = (self.traders10xBenchmark[record.buyer] ?? 0.0) + points
582                    self.traders10xBenchmark[record.seller] = (self.traders10xBenchmark[record.seller] ?? 0.0) + points
583                    // add to the 2x traders points
584                    self.traderPoints[record.buyer] = (self.traderPoints[record.buyer] ?? 0.0) + points
585                    self.traderPoints[record.seller] = (self.traderPoints[record.seller] ?? 0.0) + points
586                }
587            } else {
588                // Here is the points for non-frc20 traders
589                // 1 $FLOW = 10 Point
590                let points = record.dealPrice * 10.0
591                if record.buyer != record.storefront {
592                    self.traderPoints[record.buyer] = (self.traderPoints[record.buyer] ?? 0.0) + points
593                }
594                if record.seller != record.storefront {
595                    self.traderPoints[record.seller] = (self.traderPoints[record.seller] ?? 0.0) + points
596                }
597            }
598        }
599
600        access(contract)
601        view fun borrowStatus(): &TradingStatus {
602            return &self.status
603        }
604
605        access(self)
606        view fun convertToDate(_ time: UInt64): UInt64 {
607            // date is up to the timestamp of UTC 00:00:00
608            return time - time % 86400
609        }
610    }
611
612    /** ---– Public methods ---- */
613
614    /// The helper method to get the market resource reference
615    ///
616    access(all)
617    fun borrowTradingRecords(_ addr: Address): &{TradingRecordsPublic, TradingStatusViewer}? {
618        return getAccount(addr)
619            .capabilities.get<&{TradingRecordsPublic, TradingStatusViewer}>(self.TradingRecordsPublicPath)
620            .borrow()
621    }
622
623    /// Create a trading records resource
624    ///
625    access(all)
626    fun createTradingRecords(_ tick: String?): @TradingRecords {
627        return <-create TradingRecords(tick)
628    }
629
630    init() {
631        let recordsIdentifier = "FRC20TradingRecords_".concat(self.account.address.toString())
632        self.TradingRecordsStoragePath = StoragePath(identifier: recordsIdentifier)!
633        self.TradingRecordsPublicPath = PublicPath(identifier: recordsIdentifier)!
634
635        // Register the hooks
636        FRC20FTShared.registerHookType(Type<@FRC20TradingRecord.TradingRecords>())
637
638        emit ContractInitialized()
639    }
640}
641