Discovery of Cadence composability by Flowns and Lilico

Tags
Cadence
Flow
Tech
Published
Jul 11, 2022 01:22 PM
Language
EN
notion image
 
In the last two months, Flowns and Lilico have been trying to integrate the Flowns domain name service into lilico's non-custodial wallet to help improve user experience and make asset transferring more smoothly.
Lilico wallet uses the Flowns domain name as an Inbox of user assets, the sender and receiver of assets do not need to worry about the account initialization resources, and can turn on the functionality for the account to accept any types of FT and NFT assets like the user in Ethereum.

Why do we need to do this

The core difference between Cadence and other smart Contract languages is the resource-oriented programming concept, which is also its advantage in implement more complex and flexible asset scenarios through resource composable.
It is also because of its Resource-oriented nature, users cannot receive asset transfers from the sender without initializing asset resources, so it is very necessary to cache assets through an all-in-one inbox.
At the beginning of design, Flowns envisioned domain NFT as a container of data and assets on-chain, and hoped to meet the needs of more scenarios of product composability and scalability through the composability of resources. One typical problem is helping users cache uninitialized asset resources.
Of course, to solve this problem, we must be able to do the following first:
  • Guarantee that the owner of domain name assets is also the owner of the composable assets stored in the inbox;
  • Composable assets can be deposited from externally and cannot be withdrawn by anyone but the owner;
  • Composable assets can only be withdrawn by the owner, and the resources required for asset storage need to be initialized at the same time;
So it seems that the asset composability of Flowns domain fits better in such scenarios, and it comes with additional features:
  • The ownership of composable assets can be transferred with the transfer of domain name.
  • The composable assets can still be withdrawn when the domain name got expired.
  • Even if the domain name is invalid or the same domain name is registered by others after the expiration, Inbox is still stored under the original owner's account, and no one else has accessibility to the domain resource.

What have Lilico and Flowns done

Lilico creates an inbox for users that can accept any asset transfer through the composability of the Flowns domain name. For lilico users, there's a fallback option of asset transfer process. If the receiver does not initialize resources, the function of the inbox will be enabled to help users accept uninitialized resources as long as she/he owns the flows domain name.
The whole process follows:
  • The user generates mnemonic words offline through lilico wallet and sets the user name of lilico service
  • Lilico helps users create Flow address on the mainnet and the testnet at same time
  • User can claim the .meow root domain, gets the Flowns domain name
    • Domain mint trasaction Initiated by the user, signed by Lilico server and authorized by Flowns, the three parties together complete the authorization, multi-sig and mint .meow domain name to users
    • After getting the domain name, users have the ability to open inbox
  • Any third-party asset sender will call a Cadence script with inbox deposit to send the asset to the account of the uninitialized receiving resource user
  • If the asset is received by inbox, the user can see the asset information by querying the domain name info, and they can transfer the asset from the domain to the account resource through the Cadence script with initialization function.
It seems to be a relatively simple integration of functionalities, right ? It also involves many technical details that need to be solved, such as how to unlock the inbox function for new users without domain names, how to authenticate the first domain name distribution through the signatures of three parties, how to dynamically support a variety of assets in the future according to FT and NFT standards, and how to prevent invalid registrations and sybil attack.
If you are interested in the technical details, please see here (Chinese version)

Coding time

We have listed some technical processes clearly, now we need to understand how to implement the features of asset composability and inbox based on Cadence code details. Here are several parts:
  • NFT asset composability based on Flowns domain name NFT
  • Implementation details of the fallback Cadence scripts of inbox transaction

Flowns domain NFT composability

We can see the code in Domains contract.
Let's see the code and comments
pub resource NFT: DomainPublic, DomainPrivate, NonFungibleToken.INFT{ pub let id: UInt64 pub let name: String pub let nameHash: String pub let createdAt: UFix64 pub let parent: String pub var subdomainCount: UInt64 pub var receivable: Bool // .... // // Fungible Token composable access(self) var vaults: @{String: FungibleToken.Vault} // NonFungible Token composable access(self) var collections: @{String: NonFungibleToken.Collection} init(id: UInt64, name: String, nameHash: String, parent: String) { // ..../ } // .... //
Here we can see that two dictionary resource structures vaults and collections defined in domains NFT resources. These two structures are the basis for our implementation of resource composability.
It's noteworthy that the modifier here uses access(self) to prevent the security risks of some public fields, and only allows the modification of mapping content through the default interface of the NFT resource itself.
The mapping resource structure uses the form of @{identifier : resource} to define the type of resource storage, and at the same time, the location of resource storage is spotted according to the 'resource type identifier' of the storage resource.
For more infomation of ‘identifier’, you can see here In general, cadence will define the asset type according to the identifier of the resource, which is used as the ID of the resource type to distinguish it from others.
So the key of vaults dictionary will be like Vault.getType().identifier -> A.0ae53cb6e3f42a79.FlowToken.Vault The key of collections dictionary will be like Collection.getType().identifier -> A.0afe396ebc8eee65.FLOAT.Collection
This is beneficial for third-party developers, who can easily identify the ownership of the assets by directly knowing the address, contract, and resource type of the currently composable resources through the information contained in the key of dictionary.
Now that there is a storage structure, of course, there are withdraw methods.

Deposit and withdrawal of FT assets

// Deposit any type of FT assets into domain NFT resources pub fun depositVault(from: @FungibleToken.Vault, senderRef: &{FungibleToken.Receiver}?) { pre { // Make sure domain name service is normal !Domains.isExpired(self.nameHash) : Domains.domainExpiredTip !Domains.isDeprecated(nameHash: self.nameHash, domainId: self.id) : Domains.domainDeprecatedTip // Users can manually turn on or off inbox, which is on by default self.receivable : "Domain is not receivable" } // Get resource identifier let typeKey = from.getType().identifier let amount = from.balance let address = from.owner?.address // According to the key, judge whether there are resources and multiple reception if self.vaults[typeKey] == nil { // first deposit self.vaults[typeKey] <-! from } else { let vault = (&self.vaults[typeKey] as &FungibleToken.Vault?)! // multiple deposit vault.deposit(from: <- from) } emit DomainVaultDeposited(nameHash: self.nameHash, vaultType: typeKey, amount: amount, from: senderRef?.owner?.address ) } // Withdraw the assets in the domain NFT resource pub fun withdrawVault(key: String, amount: UFix64): @FungibleToken.Vault { pre { self.vaults[key] != nil : "Vault not exsit..." } // Get vault reference let vaultRef = (&self.vaults[key] as &FungibleToken.Vault?)! let balance = vaultRef.balance var withdrawAmount = amount // if amount is 0 so withdraw all if amount == 0.0 { withdrawAmount = balance } emit DomainVaultWithdrawn(nameHash: self.nameHash, vaultType: key, amount: balance, from: Domains.getRecords(self.nameHash)) // Return Vault return <- vaultRef.withdraw(amount: withdrawAmount) }
The code above is the detailed of deposit and withdrawal of FT. Both methods here are prefixed with pub fun, but there seems to be a permission problem here, which will be explained in detail later.
Because the deposit method here is publicly available, using the resource to obtain identifier when making deposits can avoid the risk of attack or fraud, and also ensure that the mapping keys are unique and will not be overwritten.

Deposit and withdrawal of NFT assets

// Initialize NFT collection resources to Domain NFT pub fun addCollection(collection: @NonFungibleToken.Collection) { // some pre conditions pre { !Domains.isExpired(self.nameHash) : Domains.domainExpiredTip !Domains.isDeprecated(nameHash: self.nameHash, domainId: self.id) : Domains.domainDeprecatedTip self.receivable : "Domain is not receivable" } // Get Collection resource identifier let typeKey = collection.getType().identifier // Disable the domain itself composability if collection.isInstance(Type<@Domains.Collection>()) { panic("Do not nest domain resource") } let address = collection.owner?.address // First init resource if self.collections[typeKey] == nil { self.collections[typeKey] <-! collection emit DomainCollectionAdded(nameHash: self.nameHash, collectionType: typeKey) } else { // Init collection resource must empty if collection.getIDs().length > 0 { panic("collection not empty ") } // destroy collection destroy collection } } // Deposit NFT assets to domain NFT pub fun depositNFT(key: String, token: @NonFungibleToken.NFT, senderRef: &{NonFungibleToken.CollectionPublic}?) { pre { self.collections[key] != nil : "Cannot find NFT collection..." !Domains.isExpired(self.nameHash) : Domains.domainExpiredTip !Domains.isDeprecated(nameHash: self.nameHash, domainId: self.id) : Domains.domainDeprecatedTip } // Get collection reference let collectionRef = (&self.collections[key] as &NonFungibleToken.Collection?)! emit DomainCollectionDeposited(nameHash: self.nameHash, collectionType: key, itemId: token.id, from: senderRef?.owner?.address) // Deposit NFT, where the key and NFT resource types will be checked at runtime collectionRef.deposit(token: <- token) } // Withdraw NFT assets from domain NFT pub fun withdrawNFT(key: String, itemId: UInt64): @NonFungibleToken.NFT { pre { self.collections[key] != nil : "Cannot find NFT collection..." } let collectionRef = (&self.collections[key] as &NonFungibleToken.Collection?)! emit DomainCollectionWithdrawn(nameHash: self.nameHash, collectionType: key, itemId: itemId, from: Domains.getRecords(self.nameHash)) // Withdraw NFT from domain return <- collectionRef.withdraw(withdrawID: itemId) }
NFT deposit and withdrawal here are slightly different from FT, because of the differences between FT and NFT contract standards, NFT needs to be stored into the collection resources, so we schedule the storage operation into two steps, one is to initialize the collection, the other is to deposit NFT to the collection. In fact, here we can also merge the two steps into one step in the Cadence script.
For safety, depositNFT uses identifier of Collection resource type as key, however, there is no verification here. In fact, a type comparison will be performed when calling the collection deposit function. If the key input by an external user does not match the actual NFT resource, cadence will throw an execution exception.
The implementation of the function completes, now follows another problem to solve - how to control the permission of pub fun functions?
Actually, we already separated the permissions while defined the resource interface:
Public access
// Exposed publicly accessible functions by link DomainPublic to public path pub resource interface DomainPublic { // ... pub fun depositVault(from: @FungibleToken.Vault, senderRef: &{FungibleToken.Receiver}?) pub fun addCollection(collection: @NonFungibleToken.Collection) pub fun checkCollection(key: String): Bool pub fun depositNFT(key: String, token:@NonFungibleToken.NFT, senderRef: &{NonFungibleToken.CollectionPublic}?) }
Private access
// Functions that only the owner can call pub resource interface DomainPrivate { // ... pub fun withdrawVault(key: String, amount: UFix64): @FungibleToken.Vault pub fun withdrawNFT(key: String, itemId: UInt64): @NonFungibleToken.NFT // Switch inbox pub fun setReceivable(_ flag: Bool) }
Here we can see that the interfaces of DomainPublic and DomainPrivate differentiate and are revealed through different paths of user account to achieve different permission controls.
Public can only get access to the methods via DomainPublic interface , while the borrowDomainPrivate function that users stored in the CollectionPrivate private path, can only be accessed by owner.

Transaction assets with Inbox fallback

Next, let's take a look at how to implement the fallback inbox function of transfer transactions through flexible cadence scripts. As usual, let's take a look at FT's transaction script

FT transaction with Inbox fallback

import FungibleToken from 0xFungibleToken import Domains from 0xFlowns import <Token> from <TokenAddress> transaction(amount: UFix64, recipient: Address) { // The Vault resource that holds the tokens that are being transfered let senderRef: &{FungibleToken.Receiver} let sentVault: @FungibleToken.Vault let sender: Address prepare(signer: AuthAccount) { // Get a reference to the signer's stored vault let vaultRef = signer.borrow<&<Token>.Vault>(from: <Token>.VaultStoragePath) ?? panic("Could not borrow reference to the owner's Vault!") self.senderRef = signer.getCapability(<Token>.ReceiverPublicPath) .borrow<&{FungibleToken.Receiver}>()! self.sender = vaultRef.owner!.address // Withdraw tokens from the signer's stored vault self.sentVault <- vaultRef.withdraw(amount: amount) } execute { // Get the recipient's public account object let recipientAccount = getAccount(recipient) // Get a reference to the recipient's Receiver let receiverRef = recipientAccount.getCapability(<Token>.ReceiverPublicPath) .borrow<&{FungibleToken.Receiver}>() // Use inbox as receiver when receiver has no resource if receiverRef == nil { // query Flowns domain Collection info let collectionCap = recipientAccount.getCapability<&{Domains.CollectionPublic}>(Domains.CollectionPublicPath) let collection = collectionCap.borrow()! var defaultDomain: &{Domains.DomainPublic}? = nil let ids = collection.getIDs() // panic if user has no domain if ids.length == 0 { panic("Recipient have no domain ") } // find default domain as Inbox defaultDomain = collection.borrowDomain(id: ids[0])! // check defualt domain for id in ids { let domain = collection.borrowDomain(id: id)! let isDefault = domain.getText(key: "isDefault") if isDefault == "true" { defaultDomain = domain } } // Deposit the withdrawn tokens in the recipient's domain inbox defaultDomain!.depositVault(from: <- self.sentVault, senderRef: self.senderRef) } else { // Deposit the withdrawn tokens in the recipient's receiver receiverRef!.deposit(from: <- self.sentVault) } } }
The workflow of deposit is simple. First, query whether the user has initialized the resources to receive assets. If not, query the information of Flowns domain that user has. If yes, deposit the assets into the user's default Flowns domain.

Withdraw FT and initialize resources

import Domains from 0xDomains import FungibleToken from 0xFungibleToken import Flowns from 0xFlowns import <Token> from <TokenAddress> transaction(name: String, root:String, key:String, amount: UFix64) { var domain: &{Domains.DomainPrivate} var vaultRef: &<Token>.Vault prepare(account: AuthAccount) { // calculate domain namehash with Cadence let prefix = "0x" let rootHahsh = Flowns.hash(node: "", lable: root) let nameHash = prefix.concat(Flowns.hash(node: rootHahsh, lable: name)) let collectionCap = account.getCapability<&{Domains.CollectionPublic}>(Domains.CollectionPublicPath) let collection = collectionCap.borrow()! var domain: &{Domains.DomainPrivate}? = nil // here the only owner can borrow the `CollectionPrivate` resource interface to withdraw let collectionPrivate = account.borrow<&{Domains.CollectionPrivate}>(from: Domains.CollectionStoragePath) ?? panic("Could not find your domain collection cap") let ids = collection.getIDs() // get domain id with namehash let id = Domains.getDomainId(nameHash) if id != nil && !Domains.isDeprecated(nameHash: nameHash, domainId: id!) { domain = collectionPrivate.borrowDomainPrivate(id!) } self.domain = domain! // set up user's receiver resource befor withdraw from domain let vaultRef = account.borrow<&<Token>.Vault>(from: <TokenStoragePath>) if vaultRef == nil { account.save(<- <Token>.createEmptyVault(), to: <TokenStoragePath>) // linking account with resource account.link<&<Token>.Vault{FungibleToken.Receiver}>( <TokenReceiverPath>, target: <TokenStoragePath> ) account.link<&<Token>.Vault{FungibleToken.Balance}>( <TokenBalancePath>, target: <TokenStoragePath> ) self.vaultRef = account.borrow<&<Token>.Vault>(from: <TokenStoragePath>) ?? panic("Could not borrow reference to the owner's Vault!") } else { self.vaultRef = vaultRef! } } execute { self.vaultRef.deposit(from: <- self.domain.withdrawVault(key: key, amount: amount)) } }
Here, in order to facilitate the integration of third parties, we use <Token> <TokenAddress> as a placeholder to facilitate the replacement of different asset types in the future

NFT transfer with inbox fallback

import NonFungibleToken from 0xNonFungibleToken import Domains from 0xDomains import <NFT> from <NFTAddress> // This transaction is for transferring and NFT from // one account to another transaction(recipient: Address, withdrawID: UInt64) { prepare(signer: AuthAccount) { // get the recipients public account object let recipient = getAccount(recipient) // borrow a reference to the signer's NFT collection let collectionRef = signer .borrow<&NonFungibleToken.Collection>(from: <NFT>.CollectionStoragePath) ?? panic("Could not borrow a reference to the owner's collection") let senderRef = signer .getCapability(<NFT>.CollectionPublicPath) .borrow<&{NonFungibleToken.CollectionPublic}>() // borrow a public reference to the receivers collection let recipientRef = recipient .getCapability(<NFT>.CollectionPublicPath) .borrow<&{<NFT>.CollectionPublic}>() if recipientRef == nil { let collectionCap = recipient.getCapability<&{Domains.CollectionPublic}>(Domains.CollectionPublicPath) let collection = collectionCap.borrow()! var defaultDomain: &{Domains.DomainPublic}? = nil let ids = collection.getIDs() if ids.length == 0 { panic("Recipient have no domain ") } // check defualt domain defaultDomain = collection.borrowDomain(id: ids[0])! // check defualt domain for id in ids { let domain = collection.borrowDomain(id: id)! let isDefault = domain.getText(key: "isDefault") if isDefault == "true" { defaultDomain = domain } } let typeKey = collectionRef.getType().identifier // withdraw the NFT from the owner's collection let nft <- collectionRef.withdraw(withdrawID: withdrawID) if defaultDomain!.checkCollection(key: typeKey) == false { let collection <- <NFT>.createEmptyCollection() defaultDomain!.addCollection(collection: <- collection) } defaultDomain!.depositNFT(key: typeKey, token: <- NFT, senderRef: senderRef ) } else { // withdraw the NFT from the owner's collection let nft <- collectionRef.withdraw(withdrawID: withdrawID) // Deposit the NFT in the recipient's collection recipientRef!.deposit(token: <-nft) } } }
The deposit process is similar to FT.

Withdraw NFT and init resource

import Domains from 0xDomains import NonFungibleToken from 0xNonFungibleToken import <NFT> from <NFTAddress> // key will be like 'A.f8d6e0586b0a20c7.Domains.Collection' of a NFT collection transaction(name: String, root:String, key: String, itemId: UInt64) { var domain: &{Domains.DomainPrivate} var collectionRef: &Domains.Collection prepare(account: AuthAccount) { var domain: &{Domains.DomainPrivate}? = nil // here the only owner can borrow the `CollectionPrivate` resource interface to withdraw let collectionPrivate = account.borrow<&{Domains.CollectionPrivate}>(from: Domains.CollectionStoragePath) ?? panic("Could not find your domain collection cap") // calculate domain namehash with Cadence let prefix = "0x" let rootHahsh = Flowns.hash(node: "", lable: root) let nameHash = prefix.concat(Flowns.hash(node: rootHahsh, lable: name)) // get domain id let id = Domains.getDomainId(nameHash) if id !=nil { domain = collectionPrivate.borrowDomainPrivate(id!) } self.domain = domain! // get receiver resource let collectionRef = account.borrow<&<NFT>.Collection>(from: <NFT>.CollectionStoragePath) // If receiver is nil, init collection resource before withdraw NFT if collectionRef == nil { account.save<@NonFungibleToken.Collection>(<- <NFT>.createEmptyCollection(), to: <NFT>.CollectionStoragePath) account.link<&<NFT>.Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, <NFT>.CollectionPublic}>(<NFT>.CollectionPublicPath, target: <NFT>.CollectionStoragePath) account.link<&<NFT>.Collection>(<NFT>.CollectionPrivatePath, target: <NFT>.CollectionStoragePath) // get receiver after init resource self.collectionRef = account.borrow<&<NFT>.Collection>(from: <NFT>.CollectionStoragePath)?? panic("Can not borrow collection") } else { self.collectionRef = collectionRef! } } execute { self.collectionRef.deposit(token: <- self.domain.withdrawNFT(key: key, itemId: itemId)) } }
All scripts here are assembled in a template, and are also compatible with flow FT and NFT standards.
In the scripts to withdraw FT and NFT, we use CollectionPrivate path, the functions of which are accessible only to the owner of Collection. And with that, the DomainPrivate path can be obtained. Therefore, in regard of permission and storage, the function of withdrawing FT and NFT with pub fun modifier is safe.
Then we can query the domain information with thisapi
{ "id": "23447", "owner": "0x7e5d2312899dcf9f", "name": "switchtest1.meow", "nameHash": "0xb7d21b40e4014cfbc7687935474ef78c6569957bbc6ef8ef8035134b630a8291", "expiredAt": "4807525926.00000000", "addresses": {}, "texts": {}, "parentName": "meow", "subdomainCount": "0", "subdomains": {}, "createdAt": "1653925926.00000000", "vaultBalances": {"A.7e60df042a9c0868.FlowToken.Vault": "1.00000000"}, "collections": {"A.0afe396ebc8eee65.FLOAT.Collection": [11238]}, "receivable": true, "deprecated": false, "isExpired": false, "mediaUrl": "<https://testnet.flowns.org/api/data/image/switchtest1.meow>", "isDefault": false }
The vaultBalances and collections is the FTs and NFTs that the domain receieved. Also we can use Cadence script to query the data.
// query domain detail pub fun main(nameHash: String): Domains.DomainDetail? { let address = Domains.getRecords(nameHash) ?? panic("Domain not exist") let account = getAccount(address) let collectionCap = account.getCapability<&{Domains.CollectionPublic}>(Domains.CollectionPublicPath) let collection = collectionCap.borrow()! var detail: Domains.DomainDetail? = nil let id = Domains.getDomainId(nameHash) if id != nil && !Domains.isDeprecated(nameHash: nameHash, domainId: id!) { let domain = collection.borrowDomain(id: id!) detail = domain.getDetail() } return detail }
With all these, technical details of composable FT and NFT are summarized and elaborated, hope it's helpful for you to understand cadence's resource model.

Summary

Inbox is just a small usecase by Flowns using cadence resource model. In addition to being an inbox, domain name NFT resources can be applied by more asset types and to usage scenarios. At present, standard NFT remains the only mainstream utility in the flow ecosystem, but there are also similar composable NFT projects exploring related applications.
On July 6, the ecosystem will see the beginning of permissionless deployment, thereafter, more innovation and possibilities will burst in Flow, Cadence can contribute greater based on NFT nesting and composability. Flowns will continue to explore and practice NFT composability, and expand domain name NFT to more application scenarios.
In addition, Flowns also supports user-defined data on-chain. Through the maintenance of the texts field of Domain NFT, domain name owner can unlock the ability to customize the content of the domain name (profile on-chain). In Flow network, domain name owners bear the cost of domain name and data storage, but given that the overall network storage cost is negligibly low, and it's a one-off cost that allows us to write data to the domain name, letting domain name become part of the user identity data on-chain.
Flowns' in-depth integration with lilico is just the beginning. With the business scenario as catalyst, the potential of Flowns as an assets and data protocol will be gradually brought into play.
Cadence's flexibility may also fit in many application layer innovations in the future, helping cadence developers smoothly deal with complex business cases, improve efficiency and performance without compromising asset decentralization.
Last but not least, many thanks to Lilico team! With your support and help, the composable ability of Flowns domain is now seen by everyone. Going forward, entering into application layer, Flowns and Lilico will deepen the collaboration in the infrastructure sector, together venture for more scenarios, and provide more diverse services for Lilico and Flowns users. Thanks a lot.