Smart Contract
FixesFungibleToken
A.282cd67844f046cf.FixesFungibleToken
1/**
2
3> Author: Fixes Lab <https://github.com/fixes-world/>
4
5# FixesFungibleToken
6
7This is the fungible token contract for a Mintable Fungible tokens with FixesAssetMeta.
8It is the template contract that is used to deploy.
9
10*/
11
12import FungibleToken from 0xf233dcee88fe0abe
13import ViewResolver from 0x1d7e57aa55817448
14import MetadataViews from 0x1d7e57aa55817448
15import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
16import BlackHole from 0x4396883a58c3a2d1
17import TokenList from 0x15a918087ab12d86
18import Burner from 0xf233dcee88fe0abe
19// Fixes imports
20import Fixes from 0xd2abb5dbf5e08666
21import FixesInscriptionFactory from 0xd2abb5dbf5e08666
22import FixesFungibleTokenInterface from 0xd2abb5dbf5e08666
23import FixesTraits from 0xd2abb5dbf5e08666
24import FixesAssetMeta from 0xd2abb5dbf5e08666
25import FRC20FTShared from 0xd2abb5dbf5e08666
26import FRC20AccountsPool from 0xd2abb5dbf5e08666
27
28/// This is the template source for a Fixes Fungible Token
29/// The contract is deployed in the child account of the FRC20AccountsPool
30/// The Token is issued by Minter
31access(all) contract FixesFungibleToken: FixesFungibleTokenInterface, FungibleToken, ViewResolver {
32 // ------ Events -------
33
34 /// The event that is emitted when the contract is created
35 access(all) event TokensInitialized(initialSupply: UFix64)
36
37 /// The event that is emitted when new tokens are minted
38 access(all) event TokensMinted(amount: UFix64)
39
40 /// The event that is emitted when tokens are destroyed
41 access(all) event TokensBurned(amount: UFix64)
42
43 /// -------- Parameters --------
44
45 /// Total supply of FixesFungibleToken in existence
46 /// This value is only a record of the quantity existing in the form of Flow Fungible Tokens.
47 /// It does not represent the total quantity of the token that has been minted.
48 /// The total quantity of the token that has been minted is loaded from the FRC20Indexer.
49 access(all)
50 var totalSupply: UFix64
51
52 /// -------- Resources and Interfaces --------
53
54 /// Each user stores an instance of only the Vault in their storage
55 /// The functions in the Vault and governed by the pre and post conditions
56 /// in FungibleToken when they are called.
57 /// The checks happen at runtime whenever a function is called.
58 ///
59 /// Resources can only be created in the context of the contract that they
60 /// are defined in, so there is no way for a malicious user to create Vaults
61 /// out of thin air. A special Minter resource needs to be defined to mint
62 /// new tokens.
63 ///
64 access(all) resource Vault: FixesFungibleTokenInterface.Vault, FungibleToken.Vault {
65 /// The total balance of this vault
66 access(all)
67 var balance: UFix64
68 /// Metadata: Type of MergeableData => MergeableData
69 access(contract)
70 let metadata: {Type: {FixesTraits.MergeableData}}
71
72 /// Initialize the balance at resource creation time
73 init(balance: UFix64) {
74 self.balance = balance
75 self.metadata = {}
76
77 // init with ExclusiveMeta, this meta will be removed if the initialize method is called
78 let initmeta = FixesAssetMeta.ExclusiveMeta()
79 self.metadata[initmeta.getType()] = initmeta
80 }
81
82 /// Called when a fungible token is burned via the `Burner.burn()` method
83 ///
84 access(contract) fun burnCallback() {
85 if self.balance > 0.0 {
86 // update the total supply for the FungibleToken
87 FixesFungibleToken.totalSupply = FixesFungibleToken.totalSupply - self.balance
88 }
89 self.balance = 0.0
90 }
91
92 /// createEmptyVault
93 ///
94 /// Function that creates a new Vault with a balance of zero
95 /// and returns it to the calling context. A user must call this function
96 /// and store the returned Vault in their storage in order to allow their
97 /// account to be able to receive deposits of this token type.
98 ///
99 access(all) fun createEmptyVault(): @Vault {
100 return <- FixesFungibleToken.createEmptyVault(vaultType: Type<@Vault>())
101 }
102
103 /// ----- Internal Methods -----
104
105 /// The initialize method for the Vault
106 ///
107 access(contract)
108 fun initialize(
109 _ ins: &Fixes.Inscription?,
110 _ owner: Address?,
111 ) {
112 pre {
113 ins != nil || owner != nil: "The inscription or owner must be provided"
114 }
115 let fromAddr = (ins != nil ? ins!.owner?.address : owner)
116 ?? panic("Failed to get the owner address")
117
118 // remove the ExclusiveMeta
119 let exclusiveMetaType = Type<FixesAssetMeta.ExclusiveMeta>()
120 if self.metadata[exclusiveMetaType] != nil {
121 self.metadata.remove(key: exclusiveMetaType)
122 }
123
124 /// init DNA to the metadata
125 self.initializeMetadata(FixesAssetMeta.DNA(
126 self.getDNAIdentifier(),
127 fromAddr,
128 // if inscription exists, init the DNA with the mutatable attempts
129 ins != nil ? self.getMaxGenerateGeneAttempts() : 0
130 ))
131 }
132
133 /// Set the metadata by key
134 ///
135 access(contract)
136 fun initializeMetadata(_ data: {FixesTraits.MergeableData}) {
137 let key = data.getType()
138 self.metadata[key] = data
139 }
140
141 /// --------- Implement Metadata --------- ///
142
143 /// Get the symbol of the token
144 access(all)
145 view fun getSymbol(): String {
146 return FixesFungibleToken.getSymbol()
147 }
148
149 /// DNA charging
150 /// One inscription can activate DNA mutatable attempts.
151 ///
152 access(all)
153 fun chargeDNAMutatableAttempts(_ ins: auth(Fixes.Extractable) &Fixes.Inscription) {
154 let insOwner = ins.owner?.address ?? panic("The owner of the inscription is not found")
155 assert(
156 insOwner == self.owner?.address,
157 message: "The owner of the inscription is not matched"
158 )
159
160 if !self.isValidVault() {
161 // initialize the vault with the DNA metadata
162 self.initialize(ins, nil)
163 } else {
164 // borrow the DNA metadata
165 let dnaRef = self.borrowMergeableDataRef(Type<FixesAssetMeta.DNA>())
166 ?? panic("The DNA metadata is not found")
167 let oldValue = (dnaRef.getValue("mutatableAmount") as! UInt64?) ?? 0
168 // update the DNA mutatable amount
169 let addAmt = self.getMaxGenerateGeneAttempts()
170 dnaRef.setValue("mutatableAmount", oldValue + addAmt)
171 }
172
173 // execute the inscription
174 FixesFungibleToken.executeInscription(ins: ins, usage: "charge")
175 }
176
177 /// --------- Implement FungibleToken.Provider --------- ///
178
179 /// Asks if the amount can be withdrawn from this vault
180 ///
181 access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool {
182 return amount <= self.balance
183 }
184
185 /// Function that takes an amount as an argument
186 /// and withdraws that amount from the Vault.
187 /// It creates a new temporary Vault that is used to hold
188 /// the money that is being transferred. It returns the newly
189 /// created Vault to the context that called so it can be deposited
190 /// elsewhere.
191 ///
192 /// @param amount: The amount of tokens to be withdrawn from the vault
193 /// @return The Vault resource containing the withdrawn funds
194 ///
195 access(FungibleToken.Withdraw)
196 fun withdraw(amount: UFix64): @{FungibleToken.Vault} {
197 pre {
198 self.isAvailableToWithdraw(amount: amount): "The amount withdrawn must be less than or equal to the balance"
199 }
200 let oldBalance = self.balance
201 // update the balance
202 self.balance = self.balance - amount
203
204 // initialize the new vault with the amount
205 let newVault <- create Vault(balance: amount)
206
207 // if current vault is valid vault(with DNA)
208 if self.isValidVault() {
209 newVault.initialize(nil, self.getDNAOwner())
210 }
211
212 // check if the vault has PureVault
213 let isPureVault = self.borrowMergeableDataRef(Type<FixesAssetMeta.ExclusiveMeta>()) != nil
214 // update metadata if the vault is not PureVault
215 if !isPureVault {
216 // setup mergeable data, split from withdraw percentage
217 let percentage = amount / oldBalance
218 let mergeableKeys = self.getMergeableKeys()
219 for key in mergeableKeys {
220 if let dataRef = self.borrowMergeableDataRef(key) {
221 let splitData = dataRef.split(percentage)
222 newVault.metadata[key] = splitData
223
224 // emit the event
225 FixesFungibleToken.emitMetadataUpdated(
226 &self as auth(FixesFungibleTokenInterface.MetadataUpdate) &{FixesFungibleTokenInterface.Vault},
227 dataRef
228 )
229 }
230 }
231
232 // Is the vault has DNA, then attempt to generate a new gene
233 self._attemptGenerateGene(amount, oldBalance)
234 }
235
236 // update the balance ranking
237 let ownerAddr = self.owner?.address
238 if ownerAddr != nil {
239 let adminRef = FixesFungibleToken.borrowAdminPublic()
240 let lastTopHolder = adminRef.getLastTopHolder()
241 if lastTopHolder != ownerAddr || lastTopHolder == nil {
242 let lastTopBalance = lastTopHolder != nil ? FixesFungibleToken.getTokenBalance(lastTopHolder!) : 0.0
243 if lastTopBalance == 0.0 || (lastTopBalance > self.balance && adminRef.isInTop100(ownerAddr!)) {
244 adminRef.onBalanceChanged(ownerAddr!)
245 }
246 }
247 // if balance is greater than 0, then update the token holder
248 if self.balance > 0.0 && !adminRef.isTokenHolder(ownerAddr!) {
249 adminRef.onTokenDeposited(ownerAddr!)
250 }
251 }
252
253 return <- newVault
254 }
255
256 /// --------- Implement FungibleToken.Receiver --------- ///
257
258 /// Function that takes a Vault object as an argument and adds
259 /// its balance to the balance of the owners Vault.
260 /// It is allowed to Burner.burn(the sent Vault because the Vault
261 /// was a temporary holder of the tokens. The Vault's balance has
262 /// been consumed and therefore can be destroyed.
263 ///
264 /// @param from: The Vault resource containing the funds that will be deposited
265 ///
266 access(all)
267 fun deposit(from: @{FungibleToken.Vault}) {
268 // the interface ensured that the vault is of the same type
269 // so we can safely cast it
270 let vault <- from as! @FixesFungibleToken.Vault
271
272 // try to get the current owner of the DNA
273 let selfOwner = self.owner?.address
274 let dnaOwner = vault.getDNAOwner()
275 let currentOwner = selfOwner ?? dnaOwner
276
277 // initialize current vault if it is not initialized
278 if !self.isValidVault() && currentOwner != nil {
279 self.initialize(nil, currentOwner!)
280 }
281
282 // check if the vault has PureVault
283 let isPureVault = self.borrowMergeableDataRef(Type<FixesAssetMeta.ExclusiveMeta>()) != nil
284 // update metadata if the vault is not PureVault
285 if !isPureVault {
286 // merge the metadata
287 let keys = vault.getMergeableKeys()
288 for key in keys {
289 let data = vault.getMergeableData(key)!
290 if let selfData = self.borrowMergeableDataRef(key) {
291 selfData.merge(data)
292 } else {
293 self.metadata[key] = data
294 }
295
296 let dataRef = self.borrowMergeableDataRef(key)!
297 // emit the event
298 FixesFungibleToken.emitMetadataUpdated(
299 &self as auth(FixesFungibleTokenInterface.MetadataUpdate) &{FixesFungibleTokenInterface.Vault},
300 dataRef
301 )
302 }
303 }
304
305 // record the deposited balance
306 let depositedBalance = vault.balance
307
308 // update the balance
309 self.balance = self.balance + vault.balance
310 // reset the balance of the from vault
311 vault.balance = 0.0
312 // vault is useless now
313 Burner.burn(<- vault)
314
315 // update metadata if the vault is not PureVault
316 if !isPureVault {
317 // Is the vault has DNA, then attempt to generate a new gene
318 self._attemptGenerateGene(depositedBalance, self.balance)
319 }
320
321 // update the balance ranking
322 let ownerAddr = self.owner?.address
323 if ownerAddr != nil {
324 let adminRef = FixesFungibleToken.borrowAdminPublic()
325 let lastTopHolder = adminRef.getLastTopHolder()
326 if lastTopHolder != ownerAddr || lastTopHolder == nil {
327 let lastTopBalance = lastTopHolder != nil ? FixesFungibleToken.getTokenBalance(lastTopHolder!) : 0.0
328 if lastTopBalance < self.balance {
329 adminRef.onBalanceChanged(ownerAddr!)
330 }
331 }
332 }
333 }
334
335 /// --------- Implement ViewResolver.Resolver --------- ///
336
337 /// The way of getting all the Metadata Views implemented by FixesFungibleToken
338 ///
339 /// @return An array of Types defining the implemented views. This value will be used by
340 /// developers to know which parameter to pass to the resolveView() method.
341 ///
342 access(all)
343 view fun getViews(): [Type] {
344 let contractViews = FixesFungibleToken.getContractViews(resourceType: Type<@Vault>())
345 return contractViews
346 }
347
348 /// The way of getting a Metadata View out of the FixesFungibleToken
349 ///
350 /// @param view: The Type of the desired view.
351 /// @return A structure representing the requested view.
352 ///
353 access(all)
354 fun resolveView(_ view: Type): AnyStruct? {
355 return FixesFungibleToken.resolveContractView(resourceType: nil, viewType: view)
356 }
357
358 /// --------- Internal Methods --------- ///
359
360 /// Borrow the mergeable data by key
361 ///
362 access(contract)
363 view fun borrowMergeableDataRef(_ type: Type): auth(FixesTraits.Write) &{FixesTraits.MergeableData}? {
364 return &self.metadata[type]
365 }
366
367 /// Attempt to generate a new gene, Max attempts is 10
368 ///
369 access(self)
370 fun _attemptGenerateGene(_ transactedAmt: UFix64, _ totalAmt: UFix64) {
371 log("-> Attempt to generate gene:"
372 .concat(" Owner: ").concat(self.owner?.address?.toString() ?? "Unknown")
373 .concat(" Valid: ").concat(self.isValidVault() ? "true" : "false")
374 .concat(" DNA: ").concat(self.getDNAIdentifier())
375 .concat(" Mutatable: ").concat(self.getDNAMutatableAmount().toString())
376 .concat(" Transacted: ").concat(transactedAmt.toString())
377 .concat(" Total: ").concat(totalAmt.toString()))
378
379 let remainingAmt = self.getDNAMutatableAmount()
380 if !self.isValidVault() || remainingAmt == 0 || transactedAmt == 0.0 || totalAmt == 0.0 {
381 return
382 }
383 // every 10% of balance change will get a new attempt to mutate the DNA
384 // attempt to generate a new gene
385 self.attemptGenerateGene(remainingAmt)
386 }
387 }
388
389 /// The interface for the FungibleTokenAdmin's global internal writable methods
390 ///
391 access(all) resource interface GlobalInternalWritable {
392 /// update the token holder
393 access(contract)
394 fun onTokenDeposited(_ address: Address): Bool
395
396 /// update the balance ranking
397 access(contract)
398 fun onBalanceChanged(_ address: Address): Bool
399 }
400
401 /// The admin resource for the FRC20 FT
402 ///
403 access(all) resource FungibleTokenAdmin: GlobalInternalWritable, FixesFungibleTokenInterface.IGlobalPublic, FixesFungibleTokenInterface.IMinterHolder, FixesFungibleTokenInterface.IAdminWritable {
404 access(self)
405 let minter: @Minter
406 /// The amount of tokens that all created minters are allowed to mint
407 access(self)
408 var grantedMintableAmount: UFix64
409 /// The top 100 accounts sorted by balance
410 access(self)
411 let top100Accounts: [Address]
412 /// All token holders
413 access(self)
414 let tokenHolders: {Address: Bool}
415 /// All authorized users
416 access(self)
417 let authorizedUsers: {Address: Bool}
418
419 init() {
420 self.minter <- create Minter(allowedAmount: nil)
421 self.grantedMintableAmount = 0.0
422 self.top100Accounts = []
423 self.authorizedUsers = {}
424 self.tokenHolders = {}
425 }
426
427 // ----- Implement AdminInterface -----
428
429 access(all)
430 view fun isAuthorizedUser(_ addr: Address): Bool {
431 return self.authorizedUsers[addr] == true
432 }
433
434 /// Mint new tokens
435 ///
436 access(all)
437 view fun getGrantedMintableAmount(): UFix64 {
438 return self.grantedMintableAmount
439 }
440
441 /// Get the top 100 sorted array of holders, descending by balance
442 ///
443 access(all)
444 view fun getEstimatedTop100Holders(): [Address]? {
445 return self.top100Accounts
446 }
447
448 /// Get the top 1 holder
449 ///
450 access(all)
451 view fun getTop1Holder(): Address? {
452 if self.top100Accounts.length > 0 {
453 return self.top100Accounts[0]
454 }
455 return nil
456 }
457
458 /// Get the last top holder
459 ///
460 access(all)
461 view fun getLastTopHolder(): Address? {
462 if self.top100Accounts.length > 0 {
463 return self.top100Accounts[self.top100Accounts.length - 1]
464 }
465 return nil
466 }
467
468 /// Check if the address is in the top 100
469 ///
470 access(all)
471 view fun isInTop100(_ address: Address): Bool {
472 return self.top100Accounts.contains(address)
473 }
474
475 /// Check if the address is the token holder
476 access(all)
477 view fun isTokenHolder(_ addr: Address): Bool {
478 return self.tokenHolders[addr] == true
479 }
480
481 /// Get the holders amount
482 access(all)
483 view fun getHoldersAmount(): UInt64 {
484 return UInt64(self.tokenHolders.length)
485 }
486
487 /// update the token holder
488 access(contract)
489 fun onTokenDeposited(_ address: Address): Bool {
490 var isUpdated = false
491 if self.tokenHolders[address] == nil {
492 self.tokenHolders[address] = true
493 isUpdated = true
494 }
495 return isUpdated
496 }
497
498 /// update the balance ranking
499 ///
500 access(contract)
501 fun onBalanceChanged(_ address: Address): Bool {
502 log("onBalanceChanged - Start - ".concat(address.toString()))
503 // remove the address from the top 100
504 if let idx = self.top100Accounts.firstIndex(of: address) {
505 self.top100Accounts.remove(at: idx)
506 }
507 // now address is not in the top 100, we need to check balance and insert it
508 let balance = FixesFungibleToken.getTokenBalance(address)
509 var highBalanceIdx = 0
510 var lowBalanceIdx = self.top100Accounts.length - 1
511 // use binary search to find the position
512 while lowBalanceIdx >= highBalanceIdx {
513 let mid = (lowBalanceIdx + highBalanceIdx) / 2
514 let midBalance = FixesFungibleToken.getTokenBalance(self.top100Accounts[mid])
515 // find the position
516 if balance > midBalance {
517 lowBalanceIdx = mid - 1
518 } else if balance < midBalance {
519 highBalanceIdx = mid + 1
520 } else {
521 break
522 }
523 }
524 // insert the address
525 self.top100Accounts.insert(at: highBalanceIdx, address)
526 log("onBalanceChanged - End - ".concat(address.toString())
527 .concat(" balance: ").concat(balance.toString())
528 .concat(" rank: ").concat(highBalanceIdx.toString()))
529 // remove the last one if the length is greater than 100
530 if self.top100Accounts.length > 100 {
531 self.top100Accounts.removeLast()
532 }
533 return true
534 }
535
536 // ----- Implement IMinterHolder -----
537
538 /// Borrow the minter reference
539 ///
540 access(contract)
541 view fun borrowMinter(): auth(FixesFungibleTokenInterface.Manage) &Minter {
542 return self.borrowSuperMinter()
543 }
544
545 // ------ Implement IAdminWritable ------
546
547 /// Create a new Minter resource
548 ///
549 access(FixesFungibleTokenInterface.Manage)
550 fun createMinter(allowedAmount: UFix64): @Minter {
551 let minter <- create Minter(allowedAmount: allowedAmount)
552 self.grantedMintableAmount = self.grantedMintableAmount + allowedAmount
553 return <- minter
554 }
555
556 /// Update the authorized users
557 ///
558 access(FixesFungibleTokenInterface.Manage)
559 fun updateAuthorizedUsers(_ addr: Address, _ isAdd: Bool) {
560 self.authorizedUsers[addr] = isAdd
561 }
562
563 /// Borrow the super minter resource
564 ///
565 access(FixesFungibleTokenInterface.Manage)
566 view fun borrowSuperMinter(): auth(FixesFungibleTokenInterface.Manage) &Minter {
567 return &self.minter
568 }
569 }
570
571 /// Resource object that token admin accounts can hold to mint new tokens.
572 ///
573 access(all) resource Minter: FixesFungibleTokenInterface.IMinter {
574 /// The total allowed amount of the minting token, if nil means unlimited
575 access(all)
576 let totalAllowedAmount: UFix64?
577 /// The amount of tokens that the minter is allowed to mint
578 access(all)
579 var allowedAmount: UFix64?
580
581 init(allowedAmount: UFix64?) {
582 self.totalAllowedAmount = allowedAmount
583 self.allowedAmount = allowedAmount
584 }
585
586 // ----- Implement IMinter -----
587
588 /// Get the symbol of the minting token
589 ///
590 access(all)
591 view fun getSymbol(): String {
592 return FixesFungibleToken.getSymbol()
593 }
594
595 /// Get the type of the minting token
596 access(all)
597 view fun getTokenType(): Type {
598 return Type<@FixesFungibleToken.Vault>()
599 }
600
601 /// Get the key in the accounts pool
602 access(all)
603 view fun getAccountsPoolKey(): String? {
604 return "$".concat(self.getSymbol())
605 }
606
607 /// Get the contract address of the minting token
608 access(all)
609 view fun getContractAddress(): Address {
610 return FixesFungibleToken.getAccountAddress()
611 }
612
613 /// Get the max supply of the minting token
614 access(all)
615 view fun getMaxSupply(): UFix64 {
616 return FixesFungibleToken.getMaxSupply() ?? UFix64.max
617 }
618
619 /// Get the total supply of the minting token
620 ///
621 access(all)
622 view fun getTotalSupply(): UFix64 {
623 return FixesFungibleToken.getTotalSupply()
624 }
625
626 /// Get the current mintable amount
627 ///
628 access(all)
629 view fun getCurrentMintableAmount(): UFix64 {
630 return self.allowedAmount ?? self.getUnsuppliedAmount()
631 }
632
633 /// Get the total allowed mintable amount
634 ///
635 access(all)
636 view fun getTotalAllowedMintableAmount(): UFix64 {
637 return self.totalAllowedAmount ?? self.getMaxSupply()
638 }
639
640 /// Get the vault data of the minting token
641 ///
642 access(all)
643 fun getVaultData(): FungibleTokenMetadataViews.FTVaultData {
644 return FixesFungibleToken.resolveContractView(resourceType: nil, viewType: Type<FungibleTokenMetadataViews.FTVaultData>()) as! FungibleTokenMetadataViews.FTVaultData?
645 ?? panic("The vault data is not found")
646 }
647
648 /// Function that mints new tokens, adds them to the total supply,
649 /// and returns them to the calling context.
650 ///
651 /// @param amount: The quantity of tokens to mint
652 /// @return The Vault resource containing the minted tokens
653 ///
654 access(FixesFungibleTokenInterface.Manage)
655 fun mintTokens(amount: UFix64): @FixesFungibleToken.Vault {
656 pre {
657 amount > 0.0: "Amount minted must be greater than zero"
658 self.allowedAmount == nil || amount <= self.allowedAmount!: "Amount minted must be less than the allowed amount"
659 }
660 if self.allowedAmount != nil {
661 self.allowedAmount = self.allowedAmount! - amount
662 }
663
664 let newVault <- create Vault(balance: amount)
665 FixesFungibleToken.totalSupply = FixesFungibleToken.totalSupply + amount
666 emit TokensMinted(amount: amount)
667 return <- newVault
668 }
669
670 /// Mint tokens with user's inscription
671 ///
672 access(FixesFungibleTokenInterface.Manage)
673 fun initializeVaultByInscription(
674 vault: @{FungibleToken.Vault},
675 ins: auth(Fixes.Extractable) &Fixes.Inscription
676 ): @{FungibleToken.Vault} {
677 pre {
678 vault.isInstance(Type<@FixesFungibleToken.Vault>()): "The vault must be an instance of FixesFungibleToken.Vault"
679 }
680 post {
681 before(vault.balance) == result.balance: "The vault balance must be the same"
682 }
683 let typedVault <- vault as! @FixesFungibleToken.Vault
684 // ensure vault is initialized
685 if !typedVault.isValidVault() {
686 if ins.isExtractable() {
687 typedVault.initialize(ins, nil)
688 // execute the inscription
689 FixesFungibleToken.executeInscription(ins: ins, usage: "init")
690 } else {
691 typedVault.initialize(nil, ins.owner?.address)
692 }
693 }
694 return <- typedVault
695 }
696
697 /// Burn tokens with user's inscription
698 ///
699 access(FixesFungibleTokenInterface.Manage)
700 fun burnTokenWithInscription(
701 vault: @{FungibleToken.Vault},
702 ins: auth(Fixes.Extractable) &Fixes.Inscription
703 ) {
704 pre {
705 vault.isInstance(Type<@FixesFungibleToken.Vault>()): "The vault must be an instance of FixesFungibleToken.Vault"
706 }
707 // execute the inscription
708 if ins.isExtractable() {
709 FixesFungibleToken.executeInscription(ins: ins, usage: "burn")
710 }
711 Burner.burn(<- vault)
712 }
713 }
714
715 /// ------------ Internal Methods ------------
716
717 /// Exuecte and extract FlowToken in the inscription
718 ///
719 access(contract)
720 fun executeInscription(ins: auth(Fixes.Extractable) &Fixes.Inscription, usage:String) {
721 let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
722 assert(
723 meta["usage"] == usage || meta["usage"] == "*",
724 message: "The inscription usage must be ".concat(usage)
725 )
726 let tick = meta["tick"] ?? panic("The ticker name is not found")
727 assert(
728 tick.length > 0 && tick[0] == "$" && tick == "$".concat(self.getSymbol()),
729 message: "The ticker name is not matched"
730 )
731
732 // register token to the tokenlist
733 TokenList.ensureFungibleTokenRegistered(self.account.address, "FixesFungibleToken")
734
735 // execute the inscription
736 let acctsPool = FRC20AccountsPool.borrowAccountsPool()
737 acctsPool.executeInscription(type: FRC20AccountsPool.ChildAccountType.FungibleToken, ins)
738 }
739
740 /// ------------ General Functions ------------
741
742 /// Function that creates a new Vault with a balance of zero
743 /// and returns it to the calling context. A user must call this function
744 /// and store the returned Vault in their storage in order to allow their
745 /// account to be able to receive deposits of this token type.
746 ///
747 /// @return The new Vault resource
748 ///
749 access(all)
750 fun createEmptyVault(vaultType: Type): @Vault {
751 return <-create Vault(balance: 0.0)
752 }
753
754 /// Function that resolves a metadata view for this contract.
755 ///
756 /// @param view: The Type of the desired view.
757 /// @return A structure representing the requested view.
758 ///
759 access(all)
760 fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
761 // external url
762 let externalUrl = FixesFungibleToken.getExternalUrl()
763 switch viewType {
764 case Type<MetadataViews.ExternalURL>():
765 return externalUrl != nil
766 ? MetadataViews.ExternalURL(externalUrl!)
767 : MetadataViews.ExternalURL("https://fixes.world/")
768 case Type<FungibleTokenMetadataViews.FTView>():
769 return FungibleTokenMetadataViews.FTView(
770 ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type<FungibleTokenMetadataViews.FTDisplay>()) as! FungibleTokenMetadataViews.FTDisplay?,
771 ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type<FungibleTokenMetadataViews.FTVaultData>()) as! FungibleTokenMetadataViews.FTVaultData?
772 )
773 case Type<FungibleTokenMetadataViews.FTDisplay>():
774 let store = FixesFungibleToken.borrowSharedStore()
775 let tick = FixesFungibleToken.getSymbol()
776
777 // medias
778 let medias: [MetadataViews.Media] = []
779 let logoKey = store.getKeyByEnum(FRC20FTShared.ConfigType.FungibleTokenLogoPrefix)!
780 if let iconUrl = store.get(logoKey) as! String? {
781 medias.append(MetadataViews.Media(
782 file: MetadataViews.HTTPFile(url: iconUrl),
783 mediaType: "image/png" // default is png
784 ))
785 }
786 if let iconUrl = store.get(logoKey.concat("png")) as! String? {
787 medias.append(MetadataViews.Media(
788 file: MetadataViews.HTTPFile(url: iconUrl),
789 mediaType: "image/png"
790 ))
791 }
792 if let iconUrl = store.get(logoKey.concat("svg")) as! String? {
793 medias.append(MetadataViews.Media(
794 file: MetadataViews.HTTPFile(url: iconUrl),
795 mediaType: "image/svg+xml"
796 ))
797 }
798 if let iconUrl = store.get(logoKey.concat("jpg")) as! String? {
799 medias.append(MetadataViews.Media(
800 file: MetadataViews.HTTPFile(url: iconUrl),
801 mediaType: "image/jpeg"
802 ))
803 }
804 if let iconUrl = store.get(logoKey.concat("gif")) as! String? {
805 medias.append(MetadataViews.Media(
806 file: MetadataViews.HTTPFile(url: iconUrl),
807 mediaType: "image/gif"
808 ))
809 }
810 // socials
811 let socialDict: {String: MetadataViews.ExternalURL} = {}
812 let socialKey = store.getKeyByEnum(FRC20FTShared.ConfigType.FungibleTokenSocialPrefix)!
813 // load social infos
814 if let socialUrl = store.get(socialKey.concat("twitter")) {
815 socialDict["twitter"] = MetadataViews.ExternalURL((socialUrl as! String?)!)
816 }
817 if let socialUrl = store.get(socialKey.concat("telegram")) {
818 socialDict["telegram"] = MetadataViews.ExternalURL((socialUrl as! String?)!)
819 }
820 if let socialUrl = store.get(socialKey.concat("discord")) {
821 socialDict["discord"] = MetadataViews.ExternalURL((socialUrl as! String?)!)
822 }
823 if let socialUrl = store.get(socialKey.concat("github")) {
824 socialDict["github"] = MetadataViews.ExternalURL((socialUrl as! String?)!)
825 }
826 // override all customized fields
827 return FungibleTokenMetadataViews.FTDisplay(
828 name: FixesFungibleToken.getDisplayName() ?? tick,
829 symbol: tick,
830 description: FixesFungibleToken.getTokenDescription() ?? "No description",
831 externalURL: externalUrl != nil
832 ? MetadataViews.ExternalURL(externalUrl!)
833 : MetadataViews.ExternalURL("https://linktr.ee/fixes.world/"),
834 logos: medias.length > 0
835 ? MetadataViews.Medias(medias)
836 : MetadataViews.Medias([]),
837 socials: socialDict
838 )
839 case Type<FungibleTokenMetadataViews.FTVaultData>():
840 let prefix = FixesFungibleToken.getPathPrefix()
841 return FungibleTokenMetadataViews.FTVaultData(
842 storagePath: FixesFungibleToken.getVaultStoragePath(),
843 receiverPath: FixesFungibleToken.getReceiverPublicPath(),
844 metadataPath: FixesFungibleToken.getVaultPublicPath(),
845 receiverLinkedType: Type<&{FungibleToken.Receiver}>(),
846 metadataLinkedType: Type<&FixesFungibleToken.Vault>(),
847 createEmptyVaultFunction: (fun (): @FixesFungibleToken.Vault {
848 return <-FixesFungibleToken.createEmptyVault(vaultType: Type<@Vault>())
849 })
850 )
851 case Type<FungibleTokenMetadataViews.TotalSupply>():
852 return FungibleTokenMetadataViews.TotalSupply(
853 totalSupply: FixesFungibleToken.totalSupply
854 )
855 }
856 return nil
857 }
858
859 /// Function that returns all the Metadata Views implemented by a Fungible Token
860 ///
861 /// @return An array of Types defining the implemented views. This value will be used by
862 /// developers to know which parameter to pass to the resolveView() method.
863 ///
864 access(all)
865 view fun getContractViews(resourceType: Type?): [Type] {
866 return [
867 Type<MetadataViews.ExternalURL>(),
868 Type<FungibleTokenMetadataViews.TotalSupply>(),
869 Type<FungibleTokenMetadataViews.FTView>(),
870 Type<FungibleTokenMetadataViews.FTDisplay>(),
871 Type<FungibleTokenMetadataViews.FTVaultData>()
872 ]
873 }
874
875 /// the real total supply is loaded from the FRC20Indexer
876 ///
877 access(all)
878 view fun getTotalSupply(): UFix64 {
879 return self.totalSupply
880 }
881
882 /// Get the prefix for the storage paths
883 ///
884 access(all)
885 view fun getPathPrefix(): String {
886 return "FixsStandardFT_".concat(self.account.address.toString()).concat(self.getSymbol()).concat("_")
887 }
888
889 /// Borrow the admin public reference
890 ///
891 access(contract)
892 view fun borrowAdminPublic(): &FungibleTokenAdmin {
893 return self.account
894 .capabilities.get<&FungibleTokenAdmin>(self.getAdminPublicPath())
895 .borrow() ?? panic("The FungibleToken Admin is not found")
896 }
897
898 /// Initialize the contract with ticker name
899 ///
900 init() {
901 // Initialize
902 self.totalSupply = 0.0
903
904 // Emit an event that shows that the contract was initialized
905 emit TokensInitialized(initialSupply: self.totalSupply)
906
907 // Singleton resources
908 let globalStore = FRC20FTShared.borrowGlobalStoreRef()
909 let acctsPool = FRC20AccountsPool.borrowAccountsPool()
910
911 let isSysmtemDeploy = self.account.address == globalStore.owner?.address
912 if isSysmtemDeploy {
913 // DO NOTHING, It will be a template contract for deploying new FRC20 Fungible tokens
914 return
915 }
916
917 // Step.0 Ensure shared store exists
918
919 // Initialize the shared store
920 if self.account.storage.borrow<&AnyResource>(from: FRC20FTShared.SharedStoreStoragePath) == nil {
921 let sharedStore <- FRC20FTShared.createSharedStore()
922 self.account.storage.save(<- sharedStore, to: FRC20FTShared.SharedStoreStoragePath)
923 }
924 // link the resource to the public path
925 if self.account
926 .capabilities.get<&FRC20FTShared.SharedStore>(FRC20FTShared.SharedStorePublicPath)
927 .borrow() == nil {
928 self.account.capabilities.unpublish(FRC20FTShared.SharedStorePublicPath)
929 self.account.capabilities.publish(
930 self.account.capabilities.storage.issue<&FRC20FTShared.SharedStore>(FRC20FTShared.SharedStoreStoragePath),
931 at: FRC20FTShared.SharedStorePublicPath
932 )
933 }
934 // borrow the shared store
935 let store = self.account.storage
936 .borrow<auth(FRC20FTShared.Write) &FRC20FTShared.SharedStore>(from: FRC20FTShared.SharedStoreStoragePath)
937 ?? panic("The shared store is not found")
938
939 // Step.1 Try get the ticker name from the shared store
940 var tickerName = store.getByEnum(FRC20FTShared.ConfigType.FungibleTokenSymbol) as! String?
941 if tickerName == nil {
942 var fixesFTKey: String? = nil
943 // try load the ticker name from AccountPools
944 let addrDict = acctsPool.getAddresses(type: FRC20AccountsPool.ChildAccountType.FungibleToken)
945 let contractAddr = self.account.address
946 addrDict.forEachKey(fun (key: String): Bool {
947 if let addr = addrDict[key] {
948 if addr == contractAddr {
949 fixesFTKey = key
950 return false
951 }
952 }
953 return true
954 })
955
956 // set the ticker name
957 if let foundKey = fixesFTKey {
958 assert(
959 foundKey[0] == "$",
960 message: "The ticker name is invalid."
961 )
962 tickerName = foundKey.slice(from: 1, upTo: foundKey.length)
963 store.setByEnum(FRC20FTShared.ConfigType.FungibleTokenSymbol, value: tickerName!)
964 }
965 }
966
967 assert(
968 tickerName != nil,
969 message: "The ticker name is not found"
970 )
971 log("Init the Fungible Token symbol:".concat(tickerName!))
972
973 // setup admin resource
974 let admin <- create FungibleTokenAdmin()
975 let adminStoragePath = self.getAdminStoragePath()
976 self.account.storage.save(<-admin, to: adminStoragePath)
977 // link the admin resource to the public path
978 let adminPublicPath = self.getAdminPublicPath()
979 self.account.capabilities.publish(
980 self.account.capabilities.storage.issue<&FungibleTokenAdmin>(adminStoragePath),
981 at: adminPublicPath
982 )
983
984 // ensure the admin resource exists
985 let adminRef = self.account.storage
986 .borrow<auth(FixesFungibleTokenInterface.Manage) &FungibleTokenAdmin>(from: adminStoragePath)
987 ?? panic("The FungibleToken Admin is not found")
988 let deployer = self.getDeployerAddress()
989 // add the deployer as the authorized user
990 adminRef.updateAuthorizedUsers(deployer, true)
991
992 // Step.2 Setup the vault and receiver for the contract account
993
994 let storagePath = self.getVaultStoragePath()
995 let publicPath = self.getVaultPublicPath()
996 let receiverPath = self.getReceiverPublicPath()
997
998 // Create the Vault with the total supply of tokens and save it in storage.
999 let vault <- create Vault(balance: self.totalSupply)
1000 self.account.storage.save(<-vault, to: storagePath)
1001
1002 // Create a public capability to the stored Vault that exposes
1003 // the `deposit` method through the `Receiver` interface.
1004 self.account.capabilities.publish(
1005 self.account.capabilities.storage.issue<&{FungibleToken.Receiver}>(storagePath),
1006 at: receiverPath
1007 )
1008 // Create a public capability to the stored Vault that only exposes
1009 // the `balance` field and the `resolveView` method through the `Balance` interface
1010 self.account.capabilities.publish(
1011 self.account.capabilities.storage.issue<&FixesFungibleToken.Vault>(storagePath),
1012 at: publicPath
1013 )
1014 }
1015}
1016