Smart Contract

FRC20Indexer

A.d2abb5dbf5e08666.FRC20Indexer

Valid From

86,128,658

Deployed

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

Dependents

22 imports
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FRC20 Indexer
5
6This the main contract of FRC20, it is used to deploy and manage the FRC20 tokens.
7
8*/
9// Third-party imports
10import MetadataViews from 0x1d7e57aa55817448
11import ViewResolver from 0x1d7e57aa55817448
12import FungibleToken from 0xf233dcee88fe0abe
13import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
14import FlowToken from 0x1654653399040a61
15import Burner from 0xf233dcee88fe0abe
16import BlackHole from 0x4396883a58c3a2d1
17// Fixes imports
18import Fixes from 0xd2abb5dbf5e08666
19import FixesInscriptionFactory from 0xd2abb5dbf5e08666
20import FRC20FTShared from 0xd2abb5dbf5e08666
21
22access(all) contract FRC20Indexer {
23
24    access(all) entitlement Admin
25
26    /* --- Events --- */
27    /// Event emitted when the contract is initialized
28    access(all) event ContractInitialized()
29
30    /// Event emitted when the admin calls the sponsorship method
31    access(all) event PlatformTreasurySponsorship(amount: UFix64, to: Address, forTick: String)
32    /// Event emitted when the Treasury Withdrawn invoked
33    access(all) event TokenTreasuryWithdrawn(tick: String, amount: UFix64, byInsId: UInt64, reason: String)
34
35    /// Event emitted when a FRC20 token is deployed
36    access(all) event FRC20Deployed(tick: String, max: UFix64, limit: UFix64, deployer: Address)
37    /// Event emitted when a FRC20 token is minted
38    access(all) event FRC20Minted(tick: String, amount: UFix64, to: Address)
39    /// Event emitted when the owner of an inscription is updated
40    access(all) event FRC20Transfer(tick: String, from: Address, to: Address, amount: UFix64)
41    /// Event emitted when a FRC20 token is burned
42    access(all) event FRC20Burned(tick: String, amount: UFix64, from: Address, flowExtracted: UFix64)
43    /// Event emitted when a FRC20 token is withdrawn as change
44    access(all) event FRC20WithdrawnAsChange(tick: String, amount: UFix64, from: Address)
45    /// Event emitted when a FRC20 token is deposited from change
46    access(all) event FRC20DepositedFromChange(tick: String, amount: UFix64, to: Address, from: Address)
47    /// Event emitted when a FRC20 token is set to be burnable
48    access(all) event FRC20BurnableSet(tick: String, burnable: Bool)
49    /// Event emitted when a FRC20 token is burned unsupplied tokens
50    access(all) event FRC20UnsuppliedBurned(tick: String, amount: UFix64)
51
52    /* --- Variable, Enums and Structs --- */
53    access(all)
54    let IndexerStoragePath: StoragePath
55    access(all)
56    let IndexerPublicPath: PublicPath
57
58    /* --- Interfaces & Resources --- */
59
60    /// The meta-info of a FRC20 token
61    access(all) struct FRC20Meta {
62        access(all) let tick: String
63        access(all) let max: UFix64
64        access(all) let limit: UFix64
65        access(all) let deployAt: UFix64
66        access(all) let deployer: Address
67        access(all) var burnable: Bool
68        access(all) var supplied: UFix64
69        access(all) var burned: UFix64
70
71        init(
72            tick: String,
73            max: UFix64,
74            limit: UFix64,
75            deployAt: UFix64,
76            deployer: Address,
77            supplied: UFix64,
78            burned: UFix64,
79            burnable: Bool
80        ) {
81            self.tick = tick
82            self.max = max
83            self.limit = limit
84            self.deployAt = deployAt
85            self.deployer = deployer
86            self.supplied = supplied
87            self.burned = burned
88            self.burnable = burnable
89        }
90
91        access(all)
92        fun updateSupplied(_ amt: UFix64) {
93            self.supplied = amt
94        }
95
96        access(all)
97        fun updateBurned(_ amt: UFix64) {
98            self.burned = amt
99        }
100
101        access(all)
102        fun setBurnable(_ burnable: Bool) {
103            self.burnable = burnable
104        }
105    }
106
107    access(all) resource interface IndexerPublic {
108        /* --- read-only --- */
109        /// Get all the tokens
110        access(all)
111        view fun getTokens(): [String]
112        /// Get the meta-info of a token
113        access(all)
114        view fun getTokenMeta(tick: String): FRC20Meta?
115        /// Get the token display info
116        access(all)
117        view fun getTokenDisplay(tick: String): FungibleTokenMetadataViews.FTDisplay?
118        /// Check if an inscription is a valid FRC20 inscription
119        access(all)
120        view fun isValidFRC20Inscription(ins: &Fixes.Inscription): Bool
121        /// Get the balance of a FRC20 token
122        access(all)
123        view fun getBalance(tick: String, addr: Address): UFix64
124        /// Get all balances of some address
125        access(all)
126        view fun getBalances(addr: Address): {String: UFix64}
127        /// Get the holders of a FRC20 token
128        access(all)
129        view fun getHolders(tick: String): [Address]
130        /// Get the amount of holders of a FRC20 token
131        access(all)
132        view fun getHoldersAmount(tick: String): UInt64
133        /// Get the pool balance of a FRC20 token
134        access(all)
135        view fun getPoolBalance(tick: String): UFix64
136        /// Get the benchmark value of a FRC20 token
137        access(all)
138        view fun getBenchmarkValue(tick: String): UFix64
139        /// Get the pool balance of platform treasury
140        access(all)
141        view fun getPlatformTreasuryBalance(): UFix64
142        /** ---- borrow public interface ---- */
143        /// Borrow the token's treasury $FLOW receiver
144        access(all)
145        view fun borrowTokenTreasuryReceiver(tick: String): &{FungibleToken.Receiver}
146        /// Borrow the platform treasury $FLOW receiver
147        access(all)
148        view fun borowPlatformTreasuryReceiver(): &{FungibleToken.Receiver}
149        /* --- write --- */
150        /// Deploy a new FRC20 token
151        access(all)
152        fun deploy(ins: auth(Fixes.Extractable) &Fixes.Inscription)
153        /// Mint a FRC20 token
154        access(all)
155        fun mint(ins: auth(Fixes.Extractable) &Fixes.Inscription)
156        /// Transfer a FRC20 token
157        access(all)
158        fun transfer(ins: auth(Fixes.Extractable) &Fixes.Inscription)
159        /// Burn a FRC20 token
160        access(all)
161        fun burn(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault
162        /** ---- Account Methods for listing ---- */
163        /// Ensure the balance of an address exists
164        access(account)
165        fun ensureBalanceExists(tick: String, addr: Address)
166        /// Building a selling FRC20 Token order with the sale cut from a FRC20 inscription
167        /// This method will not extract all value of the inscription
168        access(account)
169        fun buildBuyNowListing(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.ValidFrozenOrder
170        /// Building a buying FRC20 Token order with the sale cut from a FRC20 inscription
171        /// This method will not extract all value of the inscription
172        access(account)
173        fun buildSellNowListing(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.ValidFrozenOrder
174        /// Extract a part of the inscription's value to a FRC20 token change
175        access(account)
176        fun extractFlowVaultChangeFromInscription(
177            _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
178            amount: UFix64
179        ): @FRC20FTShared.Change
180        /// Apply a listed order, maker and taker should be the same token and the same amount
181        access(account)
182        fun applyBuyNowOrder(
183            makerIns: auth(Fixes.Extractable) &Fixes.Inscription,
184            takerIns: auth(Fixes.Extractable) &Fixes.Inscription,
185            maxAmount: UFix64,
186            change: @FRC20FTShared.Change
187        ): @FRC20FTShared.Change
188        /// Apply a listed order, maker and taker should be the same token and the same amount
189        access(account)
190        fun applySellNowOrder(
191            makerIns: auth(Fixes.Extractable) &Fixes.Inscription,
192            takerIns: auth(Fixes.Extractable) &Fixes.Inscription,
193            maxAmount: UFix64,
194            change: @FRC20FTShared.Change,
195            _ distributeFlowTokenFunc: (fun (UFix64, @FlowToken.Vault): Bool)
196        ): @FRC20FTShared.Change
197        /// Cancel a listed order
198        access(account)
199        fun cancelListing(listedIns: auth(Fixes.Extractable) &Fixes.Inscription, change: @FRC20FTShared.Change)
200        /// Withdraw amount of a FRC20 token by a FRC20 inscription
201        access(account)
202        fun withdrawChange(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.Change
203        /// Deposit a FRC20 token change to indexer
204        access(account)
205        fun depositChange(ins: auth(Fixes.Extractable) &Fixes.Inscription, change: @FRC20FTShared.Change)
206        /// Return the change of a FRC20 order back to the owner
207        access(account)
208        fun returnChange(change: @FRC20FTShared.Change)
209        /// Burn a FRC20 token and withdraw $FLOW by SystemBurner
210        access(account)
211        fun burnFromTreasury(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault
212        /** ---- Account Methods for command inscriptions ---- */
213        /// Set a FRC20 token to be burnable
214        access(account)
215        fun setBurnable(ins: auth(Fixes.Extractable) &Fixes.Inscription)
216        // Burn unsupplied frc20 tokens
217        access(account)
218        fun burnUnsupplied(ins: auth(Fixes.Extractable) &Fixes.Inscription)
219        /// Withdraw some $FLOW from the treasury
220        access(account)
221        fun withdrawFromTreasury(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.Change
222        /// Allocate the tokens to some address
223        access(account)
224        fun allocate(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault
225        /// Extract the ins and ensure this ins is owned by the deployer
226        access(account)
227        fun executeByDeployer(ins: auth(Fixes.Extractable) &Fixes.Inscription): Bool
228    }
229
230    /// The resource that stores the inscriptions mapping
231    ///
232    access(all) resource InscriptionIndexer: IndexerPublic {
233        /// The mapping of tokens
234        access(self)
235        let tokens: {String: FRC20Meta}
236        /// The mapping of balances
237        access(self)
238        let balances: {String: {Address: UFix64}}
239        /// The extracted balance pool of the indexer
240        access(self)
241        let pool: @{String: FlowToken.Vault}
242        /// The treasury of the indexer
243        access(self)
244        let treasury: @FlowToken.Vault
245
246        init() {
247            self.tokens = {}
248            self.balances = {}
249            self.pool <- {}
250            self.treasury <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
251        }
252
253        /* ---- Public methds ---- */
254
255        /// Get all the tokens
256        ///
257        access(all)
258        view fun getTokens(): [String] {
259            return self.tokens.keys
260        }
261
262        /// Get the meta-info of a token
263        ///
264        access(all)
265        view fun getTokenMeta(tick: String): FRC20Meta? {
266            return self.tokens[tick.toLower()]
267        }
268
269        /// Get the token display info
270        ///
271        access(all)
272        view fun getTokenDisplay(tick: String): FungibleTokenMetadataViews.FTDisplay? {
273            let ticker = tick.toLower()
274            if self.tokens[ticker] == nil {
275                return nil
276            }
277            let tickNameSize = 80 + (10 - ticker.length > 0 ? 10 - ticker.length : 0) * 12
278            let svgStr = "data:image/svg+xml;utf8,"
279                .concat("%3Csvg%20xmlns%3D%5C%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%5C%22%20viewBox%3D%5C%22-256%20-256%20512%20512%5C%22%20width%3D%5C%22512%5C%22%20height%3D%5C%22512%5C%22%3E")
280                .concat("%3Cdefs%3E%3ClinearGradient%20gradientUnits%3D%5C%22userSpaceOnUse%5C%22%20x1%3D%5C%220%5C%22%20y1%3D%5C%22-240%5C%22%20x2%3D%5C%220%5C%22%20y2%3D%5C%22240%5C%22%20id%3D%5C%22gradient-0%5C%22%20gradientTransform%3D%5C%22matrix(0.908427%2C%20-0.41805%2C%200.320369%2C%200.696163%2C%20-69.267567%2C%20-90.441103)%5C%22%3E%3Cstop%20offset%3D%5C%220%5C%22%20style%3D%5C%22stop-color%3A%20rgb(244%2C%20246%2C%20246)%3B%5C%22%3E%3C%2Fstop%3E%3Cstop%20offset%3D%5C%221%5C%22%20style%3D%5C%22stop-color%3A%20rgb(35%2C%20133%2C%2091)%3B%5C%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E")
281                .concat("%3Cellipse%20style%3D%5C%22fill%3A%20rgb(149%2C%20225%2C%20192)%3B%20stroke-width%3A%201rem%3B%20paint-order%3A%20fill%3B%20stroke%3A%20url(%23gradient-0)%3B%5C%22%20ry%3D%5C%22240%5C%22%20rx%3D%5C%22240%5C%22%3E%3C%2Fellipse%3E")
282                .concat("%3Ctext%20style%3D%5C%22dominant-baseline%3A%20middle%3B%20fill%3A%20rgb(80%2C%20213%2C%20155)%3B%20font-family%3A%20system-ui%2C%20sans-serif%3B%20text-anchor%3A%20middle%3B%5C%22%20fill-opacity%3D%5C%220.2%5C%22%20y%3D%5C%22-12%5C%22%20font-size%3D%5C%22420%5C%22%3E%F0%9D%94%89%3C%2Ftext%3E")
283                .concat("%3Ctext%20style%3D%5C%22dominant-baseline%3A%20middle%3B%20fill%3A%20rgb(244%2C%20246%2C%20246)%3B%20font-family%3A%20system-ui%2C%20sans-serif%3B%20text-anchor%3A%20middle%3B%20font-style%3A%20italic%3B%20font-weight%3A%20700%3B%5C%22%20y%3D%5C%2212%5C%22%20font-size%3D%5C%22").concat(tickNameSize.toString()).concat("%5C%22%3E")
284                .concat(ticker).concat("%3C%2Ftext%3E%3C%2Fsvg%3E")
285            let medias = MetadataViews.Medias([MetadataViews.Media(
286                file: MetadataViews.HTTPFile(url: svgStr),
287                mediaType: "image/svg+xml"
288            )])
289            return FungibleTokenMetadataViews.FTDisplay(
290                name: "FIXeS FRC20 - ".concat(ticker),
291                symbol: ticker,
292                description: "This is a FRC20 Fungible Token created by [FIXeS](https://fixes.world/).",
293                externalURL: MetadataViews.ExternalURL("https://fixes.world/"),
294                logos: medias,
295                socials: {
296                    "twitter": MetadataViews.ExternalURL("https://twitter.com/flowOnFlow")
297                }
298            )
299        }
300
301        /// Get the balance of a FRC20 token
302        ///
303        access(all)
304        view fun getBalance(tick: String, addr: Address): UFix64 {
305            let balancesRef = self._borrowBalancesRef(tick: tick)
306            return balancesRef[addr] ?? 0.0
307        }
308
309        /// Get all balances of some address
310        ///
311        access(all)
312        view fun getBalances(addr: Address): {String: UFix64} {
313            let ret: {String: UFix64} = {}
314            for tick in self.tokens.keys {
315                let balancesRef = self._borrowBalancesRef(tick: tick)
316                let balance = balancesRef[addr] ?? 0.0
317                if balance > 0.0 {
318                    ret[tick] = balance
319                }
320            }
321            return ret
322        }
323
324        /// Get the holders of a FRC20 token
325        access(all)
326        view fun getHolders(tick: String): [Address] {
327            let balancesRef = self._borrowBalancesRef(tick: tick.toLower())
328            return balancesRef.keys.slice(from: 0, upTo: balancesRef.length)
329        }
330
331        /// Get the amount of holders of a FRC20 token
332        access(all)
333        view fun getHoldersAmount(tick: String): UInt64 {
334            let balancesRef = self._borrowBalancesRef(tick: tick.toLower())
335            return UInt64(balancesRef.length)
336        }
337
338        /// Get the pool balance of a FRC20 token
339        ///
340        access(all)
341        view fun getPoolBalance(tick: String): UFix64 {
342            let pool = self._borrowTokenTreasury(tick: tick)
343            return pool.balance
344        }
345
346        /// Get the benchmark value of a FRC20 token
347        access(all)
348        view fun getBenchmarkValue(tick: String): UFix64 {
349            let pool = self._borrowTokenTreasury(tick: tick)
350            let meta = self.borrowTokenMeta(tick: tick)
351            let totalExisting = meta.supplied.saturatingSubtract(meta.burned)
352            if totalExisting > 0.0 {
353                return pool.balance / totalExisting
354            } else {
355                return 0.0
356            }
357        }
358
359        /// Get the pool balance of global
360        ///
361        access(all)
362        view fun getPlatformTreasuryBalance(): UFix64 {
363            return self.treasury.balance
364        }
365
366        /// Check if an inscription is a valid FRC20 inscription
367        ///
368        access(all)
369        view fun isValidFRC20Inscription(ins: &Fixes.Inscription): Bool {
370            let p = ins.getMetaProtocol()
371            return ins.getMimeType() == "text/plain" &&
372                (p == "FRC20" || p == "frc20" || p == "frc-20" || p == "FRC-20")
373        }
374
375        /** ---- borrow public interface ---- */
376
377        /// Borrow the token's treasury $FLOW receiver
378        ///
379        access(all)
380        view fun borrowTokenTreasuryReceiver(tick: String): &{FungibleToken.Receiver} {
381            let pool = self._borrowTokenTreasury(tick: tick)
382            return pool
383        }
384
385        /// Borrow the platform treasury $FLOW receiver
386        ///
387        access(all)
388        view fun borowPlatformTreasuryReceiver(): &{FungibleToken.Receiver} {
389            let pool = self._borrowPlatformTreasury()
390            return pool
391        }
392
393        // ---- Admin Methods ----
394
395        /// Extract some $FLOW from global pool to sponsor the tick deployer
396        ///
397        access(Admin)
398        fun sponsorship(
399            amount: UFix64,
400            to: Capability<&{FungibleToken.Receiver}>,
401            forTick: String,
402        ) {
403            pre {
404                amount > 0.0: "The amount should be greater than 0.0"
405                to.check() != nil: "The receiver should be a valid capability"
406            }
407
408            let recipient = to.address
409            let meta = self.borrowTokenMeta(tick: forTick)
410            // The receiver should be the deployer of the token
411            assert(
412                recipient == meta.deployer,
413                message: "The receiver should be the deployer of the token"
414            )
415
416            let platformPool = self._borrowPlatformTreasury()
417            // check the balance
418            assert(
419                platformPool.balance >= amount,
420                message: "The platform treasury does not have enough balance"
421            )
422
423            let flowReceiver = to.borrow()
424                ?? panic("The receiver should be a valid capability")
425            let supportedTypes = flowReceiver.getSupportedVaultTypes()
426            assert(
427                supportedTypes[Type<@FlowToken.Vault>()] == true,
428                message: "The receiver should support the $FLOW vault"
429            )
430
431            let flowExtracted <- platformPool.withdraw(amount: amount)
432            let sponsorAmt = flowExtracted.balance
433            flowReceiver.deposit(from: <- (flowExtracted as! @FlowToken.Vault))
434
435            emit PlatformTreasurySponsorship(
436                amount: sponsorAmt,
437                to: recipient,
438                forTick: forTick
439            )
440        }
441
442        /** ------ Functionality ------  */
443
444        /// Deploy a new FRC20 token
445        ///
446        access(all)
447        fun deploy(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
448            pre {
449                ins.isExtractable(): "The inscription is not extractable"
450                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
451            }
452            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
453            assert(
454                meta["op"] == "deploy" && meta["tick"] != nil && meta["max"] != nil && meta["lim"] != nil,
455                message: "The inscription is not a valid FRC20 inscription for deployment"
456            )
457
458            let tick = meta["tick"]!.toLower()
459            assert(
460                tick.length >= 3 && tick.length <= 10,
461                message: "The token tick should be between 3 and 10 characters"
462            )
463            // ensure the first letter is lowercase a-z
464            let firstLetterUTF8 = tick[0].utf8
465            assert(
466                 firstLetterUTF8.length == 1 && firstLetterUTF8[0] >= 97 && firstLetterUTF8[0] <= 122,
467                message: "The token tick should start with a lowercase letter"
468            )
469            assert(
470                self.tokens[tick] == nil && self.balances[tick] == nil && self.pool[tick] == nil,
471                message: "The token has already been deployed"
472            )
473            let max = UFix64.fromString(meta["max"]!) ?? panic("The max supply is not a valid UFix64")
474            let limit = UFix64.fromString(meta["lim"]!) ?? panic("The limit is not a valid UFix64")
475            let deployer = ins.owner!.address
476            let burnable = meta["burnable"] == "true" || meta["burnable"] == "1" // default to false
477            self.tokens[tick] = FRC20Meta(
478                tick: tick,
479                max: max,
480                limit: limit,
481                deployAt: getCurrentBlock().timestamp,
482                deployer: deployer,
483                supplied: 0.0,
484                burned: 0.0,
485                burnable: burnable
486            )
487            self.balances[tick] = {} // init the balance mapping
488            self.pool[tick] <-! FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) // init the pool
489
490            // emit event
491            emit FRC20Deployed(
492                tick: tick,
493                max: max,
494                limit: limit,
495                deployer: deployer
496            )
497
498            // extract inscription
499            self._extractInscription(tick: tick, ins: ins)
500        }
501
502        /// Mint a FRC20 token
503        ///
504        access(all)
505        fun mint(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
506            pre {
507                ins.isExtractable(): "The inscription is not extractable"
508                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
509            }
510            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
511            assert(
512                meta["op"] == "mint" && meta["tick"] != nil && meta["amt"] != nil,
513                message: "The inscription is not a valid FRC20 inscription for minting"
514            )
515
516            let tick = self._parseTickerName(meta)
517            let tokenMeta = self.borrowTokenMeta(tick: tick)
518            assert(
519                tokenMeta.supplied < tokenMeta.max,
520                message: "The token has reached the max supply"
521            )
522            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
523            assert(
524                amt > 0.0 && amt <= tokenMeta.limit,
525                message: "The amount should be greater than 0.0 and less than the limit"
526            )
527            let fromAddr = ins.owner!.address
528
529            // get the balance mapping
530            let balancesRef = self._borrowBalancesRef(tick: tick)
531
532            // check the limit
533            var amtToAdd = amt
534            if tokenMeta.supplied + amt > tokenMeta.max {
535                amtToAdd = tokenMeta.max.saturatingSubtract(tokenMeta.supplied)
536            }
537            assert(
538                amtToAdd > 0.0,
539                message: "The amount should be greater than 0.0"
540            )
541
542            // update the balance
543            self.ensureBalanceExists(tick: tick, addr: fromAddr)
544            let oldBalance = balancesRef[fromAddr] ?? panic("Failed to refer to balance")
545            balancesRef[fromAddr] = oldBalance.saturatingAdd(amtToAdd)
546
547            tokenMeta.updateSupplied(tokenMeta.supplied + amtToAdd)
548
549            // emit event
550            emit FRC20Minted(
551                tick: tick,
552                amount: amtToAdd,
553                to: fromAddr
554            )
555
556            // extract inscription
557            self._extractInscription(tick: tick, ins: ins)
558        }
559
560        /// Transfer a FRC20 token
561        ///
562        access(all)
563        fun transfer(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
564            pre {
565                ins.isExtractable(): "The inscription is not extractable"
566                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
567            }
568            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
569            assert(
570                meta["op"] == "transfer" && meta["tick"] != nil && meta["amt"] != nil && meta["to"] != nil,
571                message: "The inscription is not a valid FRC20 inscription for transfer"
572            )
573
574            let tick = self._parseTickerName(meta)
575            let tokenMeta = self.borrowTokenMeta(tick: tick)
576            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
577            let to = Address.fromString(meta["to"]!) ?? panic("The receiver is not a valid address")
578            let fromAddr = ins.owner!.address
579
580            // call the internal transfer method
581            self._transferToken(tick: tick, fromAddr: fromAddr, to: to, amt: amt)
582
583            // extract inscription
584            self._extractInscription(tick: tick, ins: ins)
585        }
586
587        /// Burn a FRC20 token
588        /// it is public so it can be called by the frc20 holders
589        ///
590        access(all)
591        fun burn(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault {
592            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
593            let tick = self._parseTickerName(meta)
594            let tokenMeta = self.borrowTokenMeta(tick: tick)
595            // this method is public, so the token should be burnable
596            assert(
597                tokenMeta.burnable,
598                message: "The token is not burnable"
599            )
600            return <- self.burnFromTreasury(ins: ins)
601        }
602
603        /// Burn a FRC20 token and withdraw $FLOW
604        /// This method is account access, so it can be called by the indexer or other contracts in the account
605        ///
606        access(account)
607        fun burnFromTreasury(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault {
608            pre {
609                ins.isExtractable(): "The inscription is not extractable"
610                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
611            }
612            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
613            assert(
614                meta["op"] == "burn" && meta["tick"] != nil && meta["amt"] != nil,
615                message: "The inscription is not a valid FRC20 inscription for burning"
616            )
617
618            let tick = self._parseTickerName(meta)
619            let tokenMeta = self.borrowTokenMeta(tick: tick)
620            assert(
621                tokenMeta.supplied > tokenMeta.burned,
622                message: "The token has been burned out"
623            )
624            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
625            let fromAddr = ins.owner!.address
626
627            // get the balance mapping
628            let balancesRef = self._borrowBalancesRef(tick: tick)
629
630            // check the amount for from address
631            let fromBalance = balancesRef[fromAddr] ?? panic("The from address does not have a balance")
632            assert(
633                fromBalance >= amt && amt > 0.0,
634                message: "The from address does not have enough balance"
635            )
636
637            let oldBurned = tokenMeta.burned
638            balancesRef[fromAddr] = fromBalance.saturatingSubtract(amt)
639            self._burnTokenInternal(tick: tick, amountToBurn: amt)
640
641            // extract inscription
642            self._extractInscription(tick: tick, ins: ins)
643
644            // extract flow from pool
645            let flowPool = self._borrowTokenTreasury(tick: tick)
646            let restAmt = tokenMeta.supplied.saturatingSubtract(oldBurned)
647            if restAmt > 0.0 {
648                let flowTokenToExtract = flowPool.balance * amt / restAmt
649                let flowExtracted <- flowPool.withdraw(amount: flowTokenToExtract)
650                // emit event
651                emit FRC20Burned(
652                    tick: tick,
653                    amount: amt,
654                    from: fromAddr,
655                    flowExtracted: flowExtracted.balance
656                )
657                return <- (flowExtracted as! @FlowToken.Vault)
658            } else {
659                return <- (FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()))
660            }
661        }
662
663        // ---- Account Methods for command inscriptions ----
664
665        /// Set a FRC20 token to be burnable
666        ///
667        access(account)
668        fun setBurnable(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
669            pre {
670                ins.isExtractable(): "The inscription is not extractable"
671                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
672                // The command inscriptions should be only executed by the indexer
673                self._isOwnedByIndexer(ins): "The inscription is not owned by the indexer"
674            }
675            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
676            assert(
677                meta["op"] == "burnable" && meta["tick"] != nil && meta["v"] != nil,
678                message: "The inscription is not a valid FRC20 inscription for setting burnable"
679            )
680
681            let tick = self._parseTickerName(meta)
682            let tokenMeta = self.borrowTokenMeta(tick: tick)
683            let isTrue = meta["v"]! == "true" || meta["v"]! == "1"
684            tokenMeta.setBurnable(isTrue)
685
686            // emit event
687            emit FRC20BurnableSet(
688                tick: tick,
689                burnable: isTrue
690            )
691
692            // extract inscription
693            self._extractInscription(tick: tick, ins: ins)
694        }
695
696        /// Burn unsupplied frc20 tokens
697        ///
698        access(account)
699        fun burnUnsupplied(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
700            pre {
701                ins.isExtractable(): "The inscription is not extractable"
702                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
703                // The command inscriptions should be only executed by the indexer
704                self._isOwnedByIndexer(ins): "The inscription is not owned by the indexer"
705            }
706            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
707            assert(
708                meta["op"] == "burnUnsup" && meta["tick"] != nil && meta["perc"] != nil,
709                message: "The inscription is not a valid FRC20 inscription for burning unsupplied tokens"
710            )
711
712            let tick = self._parseTickerName(meta)
713            let tokenMeta = self.borrowTokenMeta(tick: tick)
714            // check the burnable
715            assert(
716                tokenMeta.burnable,
717                message: "The token is not burnable"
718            )
719            // check the supplied, should be less than the max
720            assert(
721                tokenMeta.supplied < tokenMeta.max,
722                message: "The token has reached the max supply"
723            )
724
725            let perc = UFix64.fromString(meta["perc"]!) ?? panic("The percentage is not a valid UFix64")
726            // check the percentage
727            assert(
728                perc > 0.0 && perc <= 1.0,
729                message: "The percentage should be greater than 0.0 and less than or equal to 1.0"
730            )
731
732            // update the burned amount
733            let totalUnsupplied = tokenMeta.max.saturatingSubtract(tokenMeta.supplied)
734            let amtToBurn = totalUnsupplied * perc
735            // update the meta-info: supplied and burned
736            tokenMeta.updateSupplied(tokenMeta.supplied.saturatingAdd(amtToBurn))
737            self._burnTokenInternal(tick: tick, amountToBurn: amtToBurn)
738
739            // emit event
740            emit FRC20UnsuppliedBurned(
741                tick: tick,
742                amount: amtToBurn
743            )
744
745            // extract inscription
746            self._extractInscription(tick: tick, ins: ins)
747        }
748
749        /// Burn unsupplied frc20 tokens
750        ///
751        access(account)
752        fun withdrawFromTreasury(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.Change {
753            pre {
754                ins.isExtractable(): "The inscription is not extractable"
755                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
756                // The command inscriptions should be only executed by the indexer
757                self._isOwnedByIndexer(ins): "The inscription is not owned by the indexer"
758            }
759            post {
760                result.isBackedByFlowTokenVault(): "The result should be backed by a FlowToken.Vault"
761                result.getBalance() > 0.0: "The result should have a positive balance"
762            }
763
764            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
765            assert(
766                meta["op"] == "withdrawFromTreasury" && meta["tick"] != nil && meta["amt"] != nil && meta["usage"] != nil,
767                message: "The inscription is not a valid FRC20 inscription for withdrawing from treasury"
768            )
769
770            let tick = self._parseTickerName(meta)
771            let tokenMeta = self.borrowTokenMeta(tick: tick)
772            let amtToWithdraw = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
773            let usage = meta["usage"]!
774            assert(
775                usage == "lottery" || usage == "staking",
776                message: "The usage should be 'lottery'"
777            )
778
779            let treasury = self._borrowTokenTreasury(tick: tick)
780            assert(
781                treasury.balance >= amtToWithdraw,
782                message: "The treasury does not have enough balance"
783            )
784
785            let ret <- FRC20FTShared.wrapFungibleVaultChange(
786                ftVault: <- treasury.withdraw(amount: amtToWithdraw),
787                from: FRC20Indexer.getAddress()
788            )
789
790            assert(
791                ret.getBalance() == amtToWithdraw,
792                message: "The result should have the same balance as the amount to withdraw"
793            )
794
795            // emit event
796            emit TokenTreasuryWithdrawn(
797                tick: tick,
798                amount: amtToWithdraw,
799                byInsId: ins.getId(),
800                reason: usage
801            )
802
803            // extract inscription
804            self._extractInscription(tick: tick, ins: ins)
805
806            return <- ret
807        }
808
809        /// Allocate the tokens to some address
810        ///
811        access(account)
812        fun allocate(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FlowToken.Vault {
813            pre {
814                ins.isExtractable(): "The inscription is not extractable"
815                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
816            }
817
818            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
819            assert(
820                meta["op"] == "alloc" && meta["tick"] != nil && meta["amt"] != nil && meta["to"] != nil,
821                message: "The inscription is not a valid FRC20 inscription for allocating"
822            )
823
824            let tick = self._parseTickerName(meta)
825            let tokenMeta = self.borrowTokenMeta(tick: tick)
826            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
827            let to = Address.fromString(meta["to"]!) ?? panic("The receiver is not a valid address")
828            let fromAddr = FRC20Indexer.getAddress()
829
830            // call the internal transfer method
831            self._transferToken(tick: tick, fromAddr: fromAddr, to: to, amt: amt)
832
833            return <- ins.extract()
834        }
835
836        /// Extract the ins and ensure this ins is owned by the deployer
837        ///
838        access(account)
839        fun executeByDeployer(ins: auth(Fixes.Extractable) &Fixes.Inscription): Bool {
840            pre {
841                ins.isExtractable(): "The inscription is not extractable"
842                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
843            }
844
845            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
846            // only check the tick property
847            assert(
848                meta["tick"] != nil,
849                message: "The inscription is not a valid FRC20 inscription for deployer execution"
850            )
851
852            // only the deployer can execute the inscription
853            let tick = self._parseTickerName(meta)
854            let tokenMeta = self.borrowTokenMeta(tick: tick)
855
856            assert(
857                ins.owner!.address == tokenMeta.deployer,
858                message: "The inscription is not owned by the deployer"
859            )
860
861            // extract inscription
862            self._extractInscription(tick: tick, ins: ins)
863
864            return true
865        }
866
867        /** ---- Account Methods without Inscription extrasction ---- */
868
869        /// Building a selling FRC20 Token order with the sale cut from a FRC20 inscription
870        ///
871        access(account)
872        fun buildBuyNowListing(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.ValidFrozenOrder {
873            pre {
874                ins.isExtractable(): "The inscription is not extractable"
875                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
876            }
877
878            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
879            assert(
880                meta["op"] == "list-buynow" && meta["tick"] != nil && meta["amt"] != nil && meta["price"] != nil,
881                message: "The inscription is not a valid FRC20 inscription for listing"
882            )
883
884            let tick = self._parseTickerName(meta)
885            let tokenMeta = self.borrowTokenMeta(tick: tick)
886            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
887            assert(
888                amt > 0.0,
889                message: "The amount should be greater than 0.0"
890            )
891
892            // the price here means the total price
893            let totalPrice = UFix64.fromString(meta["price"]!) ?? panic("The price is not a valid UFix64")
894
895            let benchmarkValue = self.getBenchmarkValue(tick: tick)
896            let benchmarkPrice = benchmarkValue * amt
897            assert(
898                totalPrice >= benchmarkPrice,
899                message: "The price should be greater than or equal to the benchmark value: ".concat(benchmarkValue.toString())
900            )
901            // from address
902            let fromAddr = ins.owner!.address
903
904            // create the valid frozen order
905            let order <- FRC20FTShared.createValidFrozenOrder(
906                tick: tick,
907                amount: amt,
908                totalPrice: totalPrice,
909                cuts: self._buildFRC20SaleCuts(sellerAddress: fromAddr),
910                // withdraw the token to change
911                change: <- self._withdrawToTokenChange(tick: tick, fromAddr: fromAddr, amt: amt),
912            )
913            assert(
914                order.change != nil && order.change?.isBackedByVault() == false,
915                message: "The 'BuyNow' listing change should not be backed by a vault"
916            )
917            assert(
918                order.change?.getBalance() == amt,
919                message: "The change amount should be same as the amount"
920            )
921            return <- order
922        }
923
924        /// Building a buying FRC20 Token order with the sale cut from a FRC20 inscription
925        ///
926        access(account)
927        fun buildSellNowListing(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.ValidFrozenOrder {
928            pre {
929                ins.isExtractable(): "The inscription is not extractable"
930                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
931            }
932
933            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
934            assert(
935                meta["op"] == "list-sellnow" && meta["tick"] != nil && meta["amt"] != nil && meta["price"] != nil,
936                message: "The inscription is not a valid FRC20 inscription for listing"
937            )
938
939            let tick = self._parseTickerName(meta)
940            let tokenMeta = self.borrowTokenMeta(tick: tick)
941            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
942            assert(
943                amt > 0.0,
944                message: "The amount should be greater than 0.0"
945            )
946
947            // the price here means the total price
948            let totalPrice = UFix64.fromString(meta["price"]!) ?? panic("The price is not a valid UFix64")
949
950            let benchmarkValue = self.getBenchmarkValue(tick: tick)
951            let benchmarkPrice = benchmarkValue * amt
952            assert(
953                totalPrice >= benchmarkValue,
954                message: "The price should be greater than or equal to the benchmark value: ".concat(benchmarkValue.toString())
955            )
956
957            // create the valid frozen order
958            let order <- FRC20FTShared.createValidFrozenOrder(
959                tick: tick,
960                amount: amt,
961                totalPrice: totalPrice,
962                cuts: self._buildFRC20SaleCuts(sellerAddress: nil),
963                change: <- self.extractFlowVaultChangeFromInscription(ins, amount: totalPrice),
964            )
965            assert(
966                order.change != nil && order.change?.isBackedByFlowTokenVault() == true,
967                message: "The 'SellNow' listing change should be backed by a vault"
968            )
969            assert(
970                order.change?.getBalance() == totalPrice,
971                message: "The 'SellNow' listing change amount should be same as the amount"
972            )
973            return <- order
974        }
975
976        /// Apply a listed order, maker and taker should be the same token and the same amount
977        access(account)
978        fun applyBuyNowOrder(
979            makerIns: auth(Fixes.Extractable) &Fixes.Inscription,
980            takerIns: auth(Fixes.Extractable) &Fixes.Inscription,
981            maxAmount: UFix64,
982            change: @FRC20FTShared.Change
983        ): @FRC20FTShared.Change {
984            pre {
985                makerIns.isExtractable(): "The MAKER inscription is not extractable"
986                takerIns.isExtractable(): "The TAKER inscription is not extractable"
987                self.isValidFRC20Inscription(ins: makerIns): "The MAKER inscription is not a valid FRC20 inscription"
988                self.isValidFRC20Inscription(ins: takerIns): "The TAKER inscription is not a valid FRC20 inscription"
989                change.isBackedByVault() == false: "The change should not be backed by a vault"
990                maxAmount > 0.0: "No Enough amount to transact"
991                maxAmount <= change.getBalance(): "The max amount should be less than or equal to the change balance"
992            }
993
994            let makerMeta = FixesInscriptionFactory.parseMetadata(&makerIns.getData() as &Fixes.InscriptionData)
995            let takerMeta = FixesInscriptionFactory.parseMetadata(&takerIns.getData() as &Fixes.InscriptionData)
996
997            assert(
998                makerMeta["op"] == "list-buynow" && makerMeta["tick"] != nil && makerMeta["amt"] != nil && makerMeta["price"] != nil,
999                message: "The MAKER inscription is not a valid FRC20 inscription for listing"
1000            )
1001            assert(
1002                takerMeta["op"] == "list-take-buynow" && takerMeta["tick"] != nil && takerMeta["amt"] != nil,
1003                message: "The TAKER inscription is not a valid FRC20 inscription for taking listing"
1004            )
1005
1006            let tick = self._parseTickerName(takerMeta)
1007            assert(
1008                makerMeta["tick"]!.toLower() == tick && change.tick == tick,
1009                message: "The MAKER and TAKER should be the same token"
1010            )
1011            let takerAmt = UFix64.fromString(takerMeta["amt"]!) ?? panic("The amount is not a valid UFix64")
1012            let makerAmt = UFix64.fromString(makerMeta["amt"]!) ?? panic("The amount is not a valid UFix64")
1013
1014            // the max amount should be less than or equal to the maker amount
1015            assert(
1016                maxAmount <= makerAmt,
1017                message: "The max takeable amount should be less than or equal to the maker amount"
1018            )
1019            // set the transact amount, max
1020            let transactAmount = takerAmt > maxAmount ? maxAmount : takerAmt
1021            assert(
1022                transactAmount > 0.0 && transactAmount <= change.getBalance(),
1023                message: "The transact amount should be greater than 0.0 and less than or equal to the change balance"
1024            )
1025
1026            let makerAddr = makerIns.owner!.address
1027            let takerAddr = takerIns.owner!.address
1028            assert(
1029                makerAddr != takerAddr,
1030                message: "The MAKER and TAKER should be different address"
1031            )
1032            assert(
1033                makerAddr == change.from,
1034                message: "The MAKER should be the same address as the change from address"
1035            )
1036
1037            // withdraw the token from the maker by given amount
1038            let tokenToTransfer <- change.withdrawAsChange(amount: transactAmount)
1039
1040            // deposit the token change to the taker
1041            self._depositFromTokenChange(change: <- tokenToTransfer, to: takerAddr)
1042
1043            // extract taker's inscription
1044            self._extractInscription(tick: tick, ins: takerIns)
1045            // check rest balance in the change, if empty, extract the maker's inscription
1046            if change.isEmpty() {
1047                self._extractInscription(tick: tick, ins: makerIns)
1048            }
1049            return <- change
1050        }
1051
1052        /// Apply a listed order, maker and taker should be the same token and the same amount
1053        access(account)
1054        fun applySellNowOrder(
1055            makerIns: auth(Fixes.Extractable) &Fixes.Inscription,
1056            takerIns: auth(Fixes.Extractable) &Fixes.Inscription,
1057            maxAmount: UFix64,
1058            change: @FRC20FTShared.Change,
1059            _ distributeFlowTokenFunc: (fun (UFix64, @FlowToken.Vault): Bool)
1060        ): @FRC20FTShared.Change {
1061            pre {
1062                makerIns.isExtractable(): "The MAKER inscription is not extractable"
1063                takerIns.isExtractable(): "The TAKER inscription is not extractable"
1064                self.isValidFRC20Inscription(ins: makerIns): "The MAKER inscription is not a valid FRC20 inscription"
1065                self.isValidFRC20Inscription(ins: takerIns): "The TAKER inscription is not a valid FRC20 inscription"
1066                maxAmount > 0.0: "No Enough amount to transact"
1067                change.isBackedByFlowTokenVault() == true: "The change should be backed by a flow vault"
1068            }
1069
1070            let makerMeta = FixesInscriptionFactory.parseMetadata(&makerIns.getData() as &Fixes.InscriptionData)
1071            let takerMeta = FixesInscriptionFactory.parseMetadata(&takerIns.getData() as &Fixes.InscriptionData)
1072
1073            assert(
1074                makerMeta["op"] == "list-sellnow" && makerMeta["tick"] != nil && makerMeta["amt"] != nil && makerMeta["price"] != nil,
1075                message: "The MAKER inscription is not a valid FRC20 inscription for listing"
1076            )
1077            assert(
1078                takerMeta["op"] == "list-take-sellnow" && takerMeta["tick"] != nil && takerMeta["amt"] != nil,
1079                message: "The TAKER inscription is not a valid FRC20 inscription for taking listing"
1080            )
1081
1082            let tick = self._parseTickerName(takerMeta)
1083            assert(
1084                makerMeta["tick"]!.toLower() == tick,
1085                message: "The MAKER and TAKER should be the same token"
1086            )
1087            let takerAmt = UFix64.fromString(takerMeta["amt"]!) ?? panic("The amount is not a valid UFix64")
1088            let makerAmt = UFix64.fromString(makerMeta["amt"]!) ?? panic("The amount is not a valid UFix64")
1089
1090            // the max amount should be less than or equal to the maker amount
1091            assert(
1092                maxAmount <= makerAmt,
1093                message: "The MAKER and TAKER should be the same amount"
1094            )
1095            // set the transact amount, max
1096            let transactAmount = takerAmt > maxAmount ? maxAmount : takerAmt
1097
1098            let makerAddr = makerIns.owner!.address
1099            let takerAddr = takerIns.owner!.address
1100            assert(
1101                makerAddr != takerAddr,
1102                message: "The MAKER and TAKER should be different address"
1103            )
1104            assert(
1105                makerAddr == change.from,
1106                message: "The MAKER should be the same address as the change from address"
1107            )
1108
1109            // the price here means the total price
1110            let totalPrice = UFix64.fromString(makerMeta["price"]!) ?? panic("The price is not a valid UFix64")
1111            let partialPrice = transactAmount / makerAmt * totalPrice
1112
1113            // transfer token from taker to maker
1114            self._transferToken(tick: tick, fromAddr: takerAddr, to: makerAddr, amt: transactAmount)
1115
1116            // withdraw the token from the maker by given amount
1117            let tokenToTransfer <- change.withdrawAsVault(amount: partialPrice)
1118            let isCompleted = distributeFlowTokenFunc(transactAmount, <- (tokenToTransfer as! @FlowToken.Vault))
1119
1120            // extract inscription
1121            self._extractInscription(tick: tick, ins: takerIns)
1122            // check rest balance in the change, if empty, extract the maker's inscription
1123            if change.isEmpty() || isCompleted {
1124                self._extractInscription(tick: tick, ins: makerIns)
1125            }
1126            return <- change
1127        }
1128
1129        /// Cancel a listed order
1130        access(account)
1131        fun cancelListing(listedIns: auth(Fixes.Extractable) &Fixes.Inscription, change: @FRC20FTShared.Change) {
1132            pre {
1133                listedIns.isExtractable(): "The inscription is not extractable"
1134                self.isValidFRC20Inscription(ins: listedIns): "The inscription is not a valid FRC20 inscription"
1135            }
1136
1137            let meta = FixesInscriptionFactory.parseMetadata(&listedIns.getData() as &Fixes.InscriptionData)
1138            assert(
1139                meta["op"]?.slice(from: 0, upTo: 5) == "list-" && meta["tick"] != nil && meta["amt"] != nil && meta["price"] != nil,
1140                message: "The inscription is not a valid FRC20 inscription for listing"
1141            )
1142            let fromAddr = listedIns.owner!.address
1143            assert(
1144                fromAddr == change.from,
1145                message: "The listed owner should be the same as the change from address"
1146            )
1147
1148            // deposit the token change return to change's from address
1149            let flowReceiver = Fixes.borrowFlowTokenReceiver(fromAddr)
1150                ?? panic("The flow receiver no found")
1151            let supportedTypes = flowReceiver.getSupportedVaultTypes()
1152            assert(
1153                supportedTypes[Type<@FlowToken.Vault>()] == true,
1154                message: "The receiver should support the $FLOW vault"
1155            )
1156            // extract inscription and return flow in the inscription to the owner
1157            flowReceiver.deposit(from: <- listedIns.extract())
1158
1159            // call the return change method
1160            self.returnChange(change: <- change)
1161        }
1162
1163        /// Withdraw amount of a FRC20 token by a FRC20 inscription
1164        ///
1165        access(account)
1166        fun withdrawChange(ins: auth(Fixes.Extractable) &Fixes.Inscription): @FRC20FTShared.Change {
1167            pre {
1168                ins.isExtractable(): "The inscription is not extractable"
1169                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
1170            }
1171
1172            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
1173            assert(
1174                meta["op"] == "withdraw" && meta["tick"] != nil && meta["amt"] != nil && meta["usage"] != nil,
1175                message: "The inscription is not a valid FRC20 inscription for transfer"
1176            )
1177
1178            let tick = self._parseTickerName(meta)
1179            let tokenMeta = self.borrowTokenMeta(tick: tick)
1180            let usage = meta["usage"]!
1181            assert(
1182                usage == "staking" || usage == "donate" || usage == "lottery" || usage == "convert" || usage == "lock" || usage == "empty",
1183                message: "The usage should be 'staking', 'donate', 'lottery', 'convert', 'lock' or 'empty'"
1184            )
1185
1186            // The from address: the owner of the inscription
1187            let fromAddr = ins.owner!.address
1188
1189            // amount can be zero, which means withdraw nothing, just execute the inscription
1190            var retChange: @FRC20FTShared.Change? <- nil
1191            let amt = UFix64.fromString(meta["amt"]!) ?? panic("The amount is not a valid UFix64")
1192            if amt == 0.0 {
1193                // ensure usage is empty
1194                assert(
1195                    usage == "empty",
1196                    message: "The usage should be 'empty' if the amount is zero"
1197                )
1198                retChange <-! FRC20FTShared.createEmptyChange(tick: tick, from: fromAddr)
1199            } else {
1200                // withdraw the token to change
1201                retChange <-! self._withdrawToTokenChange(
1202                    tick: tick,
1203                    fromAddr: fromAddr,
1204                    amt: amt
1205                )
1206            }
1207
1208            // extract inscription
1209            self._extractInscription(tick: tick, ins: ins)
1210
1211            return <- retChange!
1212        }
1213
1214        /// Deposit a FRC20 token change to indexer
1215        ///
1216        access(account)
1217        fun depositChange(ins: auth(Fixes.Extractable) &Fixes.Inscription, change: @FRC20FTShared.Change) {
1218            pre {
1219                ins.isExtractable(): "The inscription is not extractable"
1220                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
1221                change.isBackedByVault() == false: "The change should not be backed by a vault"
1222            }
1223
1224            let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
1225            assert(
1226                meta["op"] == "deposit" && meta["tick"] != nil,
1227                message: "The inscription is not a valid FRC20 inscription for transfer"
1228            )
1229
1230            let tick = self._parseTickerName(meta)
1231            let tokenMeta = self.borrowTokenMeta(tick: tick)
1232            assert(
1233                tokenMeta.tick == change.tick,
1234                message: "The token should be the same as the change"
1235            )
1236            let fromAddr = ins.owner!.address
1237
1238            self._depositFromTokenChange(change: <- change, to: fromAddr)
1239
1240            // extract inscription
1241            self._extractInscription(tick: tick, ins: ins)
1242        }
1243
1244        /// Return the change of a FRC20 order back to the owner
1245        ///
1246        access(account)
1247        fun returnChange(change: @FRC20FTShared.Change) {
1248            // if the change is empty, destroy it and return
1249            if change.getBalance() == 0.0 {
1250                Burner.burn(<- change)
1251                return
1252            }
1253            let fromAddr = change.from
1254
1255            if !change.isBackedByVault() {
1256                self._depositFromTokenChange(change: <- change, to: fromAddr)
1257            } else {
1258                let vault <- change.extractAsVault()
1259                let vaultType = vault.getType()
1260                if vaultType == Type<@FlowToken.Vault>() {
1261                    // deposit the token change return to change's from address
1262                    let flowReceiver = Fixes.borrowFlowTokenReceiver(fromAddr)
1263                        ?? panic("The flow receiver no found")
1264                    assert(
1265                        flowReceiver.isSupportedVaultType(type: vaultType),
1266                        message: "The receiver should support the $FLOW vault"
1267                    )
1268                    flowReceiver.deposit(from: <- vault)
1269                } else {
1270                    let ftVaultData = FRC20FTShared.resolveFTVaultDataByTicker(change.getOriginalTick())
1271                        ?? panic("Failed to resolve ticker")
1272                    if let ftReceiver = getAccount(fromAddr).capabilities
1273                        .borrow<&{FungibleToken.Vault}>(ftVaultData.receiverPath) {
1274                        assert(
1275                            ftReceiver.isSupportedVaultType(type: vaultType),
1276                            message: "The receiver should support the $FLOW vault"
1277                        )
1278                        // Transfer to the Receiver
1279                        ftReceiver.deposit(from: <- vault)
1280                    } else {
1281                        // IF receiver does not exist, just vanish the vault.
1282                        BlackHole.vanish(<- vault)
1283                    }
1284                }
1285                Burner.burn(<- change)
1286            }
1287        }
1288
1289        /// Ensure the balance of an address exists
1290        ///
1291        access(account)
1292        fun ensureBalanceExists(tick: String, addr: Address) {
1293            let balancesRef = self._borrowBalancesRef(tick: tick)
1294            if balancesRef[addr] == nil {
1295                balancesRef[addr] = 0.0
1296            }
1297        }
1298
1299        /// Extract a part of the inscription's value to a FRC20 token change
1300        ///
1301        access(account)
1302        fun extractFlowVaultChangeFromInscription(
1303            _ ins: auth(Fixes.Extractable) &Fixes.Inscription,
1304            amount: UFix64
1305        ): @FRC20FTShared.Change {
1306            pre {
1307                ins.isExtractable(): "The inscription is not extractable"
1308                self.isValidFRC20Inscription(ins: ins): "The inscription is not a valid FRC20 inscription"
1309            }
1310            post {
1311                ins.isExtractable() && ins.isValueValid(): "The inscription should be extractable and the value should be valid after partial extraction"
1312            }
1313            // extract payment from the buyer's inscription, the payment should be a FLOW token
1314            // the payment should be equal to the total price and the payer should be the buyer
1315            // in the partialExtract method, the inscription will be extracted if the payment is enough
1316            // and the inscription will be still extractable.
1317            let vault <- ins.partialExtract(amount)
1318            assert(
1319                vault.balance == amount,
1320                message: "The amount should be equal to the balance of the vault"
1321            )
1322            // return the change
1323            return <- FRC20FTShared.wrapFungibleVaultChange(
1324                ftVault: <- vault,
1325                from: ins.owner!.address, // Pay $FLOW to the buyer and get the FRC20 token
1326            )
1327        }
1328
1329        /** ----- Private methods ----- */
1330
1331        /// Build the sale cuts for a FRC20 order
1332        /// - Parameters:
1333        ///   - sellerAddress: The seller address, if it is nil, then it is a buy order
1334        ///
1335        access(self)
1336        fun _buildFRC20SaleCuts(sellerAddress: Address?): [FRC20FTShared.SaleCut] {
1337            let ret: [FRC20FTShared.SaleCut] = []
1338
1339            // use the shared store to get the sale fee
1340            let sharedStore = FRC20FTShared.borrowGlobalStoreRef()
1341            // Default sales fee, 2% of the total price
1342            let salesFee = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesFee) as! UFix64?) ?? 0.02
1343            assert(
1344                salesFee > 0.0 && salesFee <= 1.0,
1345                message: "The sales fee should be greater than 0.0 and less than or equal to 1.0"
1346            )
1347
1348            // Default 40% of sales fee to the token treasury pool
1349            let treasuryPoolCut = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesCutTreasuryPoolRatio) as! UFix64?) ?? 0.4
1350            // Default 25% of sales fee to the platform pool
1351            let platformTreasuryCut = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesCutPlatformPoolRatio) as! UFix64?) ?? 0.25
1352            // Default 25% of sales fee to the stakers pool
1353            let platformStakersCut = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesCutPlatformStakersRatio) as! UFix64?) ?? 0.25
1354            // Default 10% of sales fee to the marketplace portion cut
1355            let marketplacePortionCut = (sharedStore.getByEnum(FRC20FTShared.ConfigType.PlatformSalesCutMarketRatio) as! UFix64?) ?? 0.1
1356
1357            // sum of all the cuts should be 1.0
1358            let totalCutsRatio = treasuryPoolCut + platformTreasuryCut + platformStakersCut + marketplacePortionCut
1359            assert(
1360                totalCutsRatio == 1.0,
1361                message: "The sum of all the cuts should be 1.0"
1362            )
1363
1364            // add to the sale cuts
1365            // The first cut is the token treasury cut to ensure residualReceiver will be this
1366            ret.append(FRC20FTShared.SaleCut(
1367                type: FRC20FTShared.SaleCutType.TokenTreasury,
1368                ratio: salesFee * treasuryPoolCut,
1369                receiver: nil
1370            ))
1371            ret.append(FRC20FTShared.SaleCut(
1372                type: FRC20FTShared.SaleCutType.PlatformTreasury,
1373                ratio: salesFee * platformTreasuryCut,
1374                receiver: nil
1375            ))
1376            ret.append(FRC20FTShared.SaleCut(
1377                type: FRC20FTShared.SaleCutType.PlatformStakers,
1378                ratio: salesFee * platformStakersCut,
1379                receiver: nil
1380            ))
1381            ret.append(FRC20FTShared.SaleCut(
1382                type: FRC20FTShared.SaleCutType.MarketplacePortion,
1383                ratio: salesFee * marketplacePortionCut,
1384                receiver: nil
1385            ))
1386
1387            // add the seller or buyer cut
1388            if sellerAddress != nil {
1389                // borrow the receiver reference
1390                let flowTokenReceiver = getAccount(sellerAddress!)
1391                    .capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
1392                assert(
1393                    flowTokenReceiver.check(),
1394                    message: "Could not borrow receiver reference to the seller's Vault"
1395                )
1396                ret.append(FRC20FTShared.SaleCut(
1397                    type: FRC20FTShared.SaleCutType.SellMaker,
1398                    ratio: (1.0 - salesFee),
1399                    // recevier is the FlowToken Vault of the seller
1400                    receiver: flowTokenReceiver
1401                ))
1402            } else {
1403                ret.append(FRC20FTShared.SaleCut(
1404                    type: FRC20FTShared.SaleCutType.BuyTaker,
1405                    ratio: (1.0 - salesFee),
1406                    receiver: nil
1407                ))
1408            }
1409
1410            // check cuts amount, should be same as the total price
1411            var totalRatio: UFix64 = 0.0
1412            for cut in ret {
1413                totalRatio = totalRatio.saturatingAdd(cut.ratio)
1414            }
1415            assert(
1416                totalRatio == 1.0,
1417                message: "The sum of all the cuts should be 1.0"
1418            )
1419            // return the sale cuts
1420            return ret
1421        }
1422
1423        /// Internal Transfer a FRC20 token
1424        ///
1425        access(self)
1426        fun _transferToken(
1427            tick: String,
1428            fromAddr: Address,
1429            to: Address,
1430            amt: UFix64
1431        ) {
1432            let change <- self._withdrawToTokenChange(tick: tick, fromAddr: fromAddr, amt: amt)
1433            self._depositFromTokenChange(change: <- change, to: to)
1434
1435            // emit event
1436            emit FRC20Transfer(
1437                tick: tick,
1438                from: fromAddr,
1439                to: to,
1440                amount: amt
1441            )
1442        }
1443
1444        /// Internal Build a FRC20 token change
1445        ///
1446        access(self)
1447        fun _withdrawToTokenChange(
1448            tick: String,
1449            fromAddr: Address,
1450            amt: UFix64
1451        ): @FRC20FTShared.Change {
1452            post {
1453                result.isBackedByVault() == false: "The change should not be backed by a vault"
1454                result.getBalance() == amt: "The change balance should be same as the amount"
1455                self.getBalance(tick: tick, addr: fromAddr) == before(self.getBalance(tick: tick, addr: fromAddr)) - amt
1456                    : "The from address balance should be decreased by the amount"
1457            }
1458            // borrow the balance mapping
1459            let balancesRef = self._borrowBalancesRef(tick: tick)
1460
1461            // check the amount for from address
1462            let fromBalance = balancesRef[fromAddr] ?? panic("The from address does not have a balance")
1463            assert(
1464                fromBalance >= amt && amt > 0.0,
1465                message: "The from address does not have enough balance"
1466            )
1467
1468            balancesRef[fromAddr] = fromBalance.saturatingSubtract(amt)
1469
1470            // emit event
1471            emit FRC20WithdrawnAsChange(
1472                tick: tick,
1473                amount: amt,
1474                from: fromAddr
1475            )
1476
1477            // create the frc20 token change
1478            return <- FRC20FTShared.createChange(
1479                tick: tick,
1480                from: fromAddr,
1481                balance: amt,
1482                ftVault: nil
1483            )
1484        }
1485
1486        /// Internal Deposit a FRC20 token change
1487        ///
1488        access(self)
1489        fun _depositFromTokenChange(
1490            change: @FRC20FTShared.Change,
1491            to: Address
1492        ) {
1493            pre {
1494                change.isBackedByVault() == false: "The change should not be backed by a vault"
1495            }
1496            let tick = change.tick
1497            let amt = change.extract()
1498
1499            // ensure the balance exists
1500            self.ensureBalanceExists(tick: tick, addr: to)
1501            // borrow the balance mapping
1502            let balancesRef = self._borrowBalancesRef(tick: tick)
1503            // update the balance
1504            let oldBalance = balancesRef[to] ?? panic("Failed to refer to balance")
1505            balancesRef[to] = oldBalance.saturatingAdd(amt)
1506
1507            // emit event
1508            emit FRC20DepositedFromChange(
1509                tick: tick,
1510                amount: amt,
1511                to: to,
1512                from: change.from
1513            )
1514
1515            // destroy the empty change
1516            Burner.burn(<- change)
1517        }
1518
1519        /// Internal Burn a FRC20 token
1520        ///
1521        access(self)
1522        fun _burnTokenInternal(tick: String, amountToBurn: UFix64) {
1523            let meta = self.borrowTokenMeta(tick: tick)
1524            let oldBurned = meta.burned
1525            meta.updateBurned(oldBurned.saturatingAdd(amountToBurn))
1526        }
1527
1528        /// Extract the $FLOW from inscription
1529        ///
1530        access(self)
1531        fun _extractInscription(tick: String, ins: auth(Fixes.Extractable) &Fixes.Inscription) {
1532            pre {
1533                ins.isExtractable(): "The inscription is not extractable"
1534                self.pool[tick] != nil: "The token has not been deployed"
1535            }
1536
1537            // extract the tokens
1538            let token <- ins.extract()
1539            // 5% of the extracted tokens will be sent to the treasury
1540            let amtToTreasury = token.balance * 0.05
1541            // withdraw the tokens to the treasury
1542            let tokenToTreasuryVault <- token.withdraw(amount: amtToTreasury)
1543
1544            // deposit the tokens to pool and treasury
1545            let pool = self._borrowTokenTreasury(tick: tick)
1546            let treasury = self._borrowPlatformTreasury()
1547
1548            pool.deposit(from: <- token)
1549            treasury.deposit(from: <- tokenToTreasuryVault)
1550        }
1551
1552        /// Check if an inscription is owned by the indexer
1553        ///
1554        access(self)
1555        view fun _isOwnedByIndexer(_ ins: &Fixes.Inscription): Bool {
1556            return ins.owner?.address == FRC20Indexer.getAddress()
1557        }
1558
1559        /// Parse the ticker name from the meta-info of a FRC20 inscription
1560        ///
1561        access(self)
1562        view fun _parseTickerName(_ meta: {String: String}): String {
1563            let tick = meta["tick"]?.toLower() ?? panic("The token tick is not found")
1564            assert(
1565                self.tokens[tick] != nil && self.balances[tick] != nil && self.pool[tick] != nil,
1566                message: "The token has not been deployed"
1567            )
1568            return tick
1569        }
1570
1571        /// Borrow the meta-info of a token
1572        ///
1573        access(self)
1574        view fun borrowTokenMeta(tick: String): auth(Mutate) &FRC20Meta {
1575            let meta = &self.tokens[tick.toLower()] as auth(Mutate) &FRC20Meta?
1576            return meta ?? panic("The token meta is not found")
1577        }
1578
1579        /// Borrow the balance mapping of a token
1580        ///
1581        access(self)
1582        view fun _borrowBalancesRef(tick: String): auth(Mutate) &{Address: UFix64} {
1583            let balancesRef = &self.balances[tick.toLower()] as auth(Mutate) &{Address: UFix64}?
1584            return balancesRef ?? panic("The token balance is not found")
1585        }
1586
1587        /// Borrow the token's treasury $FLOW receiver
1588        ///
1589        access(self)
1590        view fun _borrowTokenTreasury(tick: String): auth(FungibleToken.Withdraw) &FlowToken.Vault {
1591            let pool = &self.pool[tick.toLower()] as auth(FungibleToken.Withdraw) &FlowToken.Vault?
1592            return pool ?? panic("The token pool is not found")
1593        }
1594
1595        /// Borrow the platform treasury $FLOW receiver
1596        ///
1597        access(self)
1598        view fun _borrowPlatformTreasury(): auth(FungibleToken.Withdraw) &FlowToken.Vault {
1599            return &self.treasury
1600        }
1601    }
1602
1603    /* --- Public Methods --- */
1604
1605    /// Get the address of the indexer
1606    ///
1607    access(all)
1608    view fun getAddress(): Address {
1609        return self.account.address
1610    }
1611
1612    /// Get the inscription indexer
1613    ///
1614    access(all)
1615    view fun getIndexer(): &{IndexerPublic} {
1616        let addr = self.account.address
1617        let cap = getAccount(addr)
1618            .capabilities.get<&{IndexerPublic}>(self.IndexerPublicPath)
1619            .borrow()
1620        return cap ?? panic("Could not borrow InscriptionIndexer")
1621    }
1622
1623    init() {
1624        let identifier = "FRC20Indexer_".concat(self.account.address.toString())
1625        self.IndexerStoragePath = StoragePath(identifier: identifier)!
1626        self.IndexerPublicPath = PublicPath(identifier: identifier)!
1627        // create the indexer
1628        self.account.storage.save(<- create InscriptionIndexer(), to: self.IndexerStoragePath)
1629        self.account.capabilities.publish(
1630            self.account.capabilities.storage.issue<&{IndexerPublic}>(self.IndexerStoragePath),
1631            at: self.IndexerPublicPath
1632        )
1633    }
1634}
1635