Smart Contract
FRC20TradingRecord
A.d2abb5dbf5e08666.FRC20TradingRecord
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