fcl-js
fcl-js copied to clipboard
End users need human readable text when asked to approve a wallet popup
This is a huge UX issue has been brought up a number of times in the Flow Discord, and it was suggested I raise the issue here so everyone can put their heads together. Here is the one conversation for example: https://discord.com/channels/613813861610684416/699275726704083064/858690789081350185
In short, when users are asked to sign a transaction, there needs to be some human readable text for what the user is approving.
Currently, the actual transaction code is provided to the user in the wallet modal. The actual code is great to have, and necessary. However, this is not enough, as very few mainstream users can read Cadence code (and this is all the info FCL currently offers to the wallet maker).
So without a solution, the mainstream user will face a cryptic dialog from wallet providers, which force the users to reject or approve something. This is a huge hurdle for mainstream users who really just want the TopShop user experience, and it could drive dapp makers to make custody wallet solutions instead.
The suggestion to allow dapp makers to pass a custom text field in FCL.send was quickly thrown out due to the concern a malicious app maker would say one thing and the transaction would do another ex. "Create Collection" could actually be draining your account of tokens.
The best solutions I've heard so far revolve around template transactions that have been verified by someone "trusted" (like the wallet provider). These transactions can go in an open source repo and/or in a smart contract as a hash or the full transaction code (the wallet providers can then verify code against the transaction hashes in the contract). This also has some cons, and custom transactions would still show up as unverified/untrusted to end users (which is bad for the dapp maker).
What recourse could wallets provide if I do sign and submit code that does something other than what I expected? (Reactive instead of proactive)
I feel like wallets should only be able to execute trusted/verified code, and never arbitrary code (on Mainnet) otherwise having 'verified' code is mostly pointless.
Verified templates feels like we're moving into 'app store' territory, where anything anyone wants to do needs to pass through an approval process, security screening, quality-control ...etc.
Maybe there's a DAO mechanic in here somewhere?
I'm thinking about managing a repository like this: https://github.com/portto/flow-transactions
Basically anyone can submit a PR with transaction templates like this
import FungibleToken from 0xFUNGIBLE_TOKEN_ADDRESS
import FlowToken from 0xFLOW_TOKEN_ADDRESS
transaction(amount: UFix64, to: Address) {
// The Vault resource that holds the tokens that are being transferred
let sentVault: @FungibleToken.Vault
prepare(signer: AuthAccount) {
// Get a reference to the signer's stored vault
let vaultRef = signer.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)
?? panic("Could not borrow reference to the owner's Vault!")
// Withdraw tokens from the signer's stored vault
self.sentVault <- vaultRef.withdraw(amount: amount)
}
execute {
// Get the recipient's public account object
let recipient = getAccount(to)
// Get a reference to the recipient's Receiver
let receiverRef = recipient.getCapability(/public/flowTokenReceiver)
.borrow<&{FungibleToken.Receiver}>()
?? panic("Could not borrow receiver reference to the recipient's Vault")
// Deposit the withdrawn tokens in the recipient's receiver
receiverRef.deposit(from: <-self.sentVault)
}
}
And provide the corresponding message to be shown for the transaction like
{
"arguments": [
"amount",
"to"
],
"messages": {
"en": "Send {amount} FLOW to {to}",
"zh_CN": "发送 {amount} FLOW 代币至 {to}",
"zh_TW": "發送 {amount} FLOW 代幣至 {to}"
}
}
We will then review the transactions and merge the PR if everything looks fine Once they're merged, the transaction templates will be processed to create actual transaction scripts for both mainnet and testnet with corresponding hash values. dapps should use the exact same transaction script as the processed file (we can also provide hex encoded version as well)
import FungibleToken from 0xf233dcee88fe0abe
import FlowToken from 0x1654653399040a61
transaction(amount: UFix64, to: Address) {
// The Vault resource that holds the tokens that are being transferred
let sentVault: @FungibleToken.Vault
prepare(signer: AuthAccount) {
// Get a reference to the signer's stored vault
let vaultRef = signer.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)
?? panic("Could not borrow reference to the owner's Vault!")
// Withdraw tokens from the signer's stored vault
self.sentVault <- vaultRef.withdraw(amount: amount)
}
execute {
// Get the recipient's public account object
let recipient = getAccount(to)
// Get a reference to the recipient's Receiver
let receiverRef = recipient.getCapability(/public/flowTokenReceiver)
.borrow<&{FungibleToken.Receiver}>()
?? panic("Could not borrow receiver reference to the recipient's Vault")
// Deposit the withdrawn tokens in the recipient's receiver
receiverRef.deposit(from: <-self.sentVault)
}
}
with corresponding hashes
sha3 dd522194d440fc01431d8bacfc4c346ed9962d871672401eb48858b918cdc5d1
sha256 8167aba60571d922eceffd524c40c69812376905622c6cf017a2a7ae750be585
When dapp initiates transaction, Blocto popup will hash the script and see if there's a match with
{
"dd522194d440fc01431d8bacfc4c346ed9962d871672401eb48858b918cdc5d1": {
"arguments": [
"amount",
"to"
],
"messages": {
"en": "Send {amount} FLOW to {to}",
"zh_CN": "发送 {amount} FLOW 代币至 {to}",
"zh_TW": "發送 {amount} FLOW 代幣至 {to}"
}
},
"15dfa12bbd0695f38c8d03b22654f6e49a2fd7a27fa0cd8497d352d760c6e4ce": {
"arguments": [
"amount",
"to"
],
"messages": {
"en": "Send {amount} tUSDT to {to}",
"zh_CN": "发送 {amount} tUSDT 代币至 {to}",
"zh_TW": "發送 {amount} tUSDT 代幣至 {to}"
}
},
"ce5a914732fb407e8bba8e6230ac4b6ad6b7f3dda368adf8fc85b32684420dfb": {
"arguments": [
"amount",
"target"
],
"messages": {
"en": "Teleport {amount} tUSDT to {target} on Ethereum",
"zh_CN": "将 {amount} tUSDT 跨链传送至以太坊上的 {target}",
"zh_TW": "將 {amount} tUSDT 跨鏈傳送至以太坊上的 {target}"
}
}
}
If it's a match, Blocto will display the message defined in the authorization popup
Also there's a managed list of contracts, to prevent people from creating a contract with the same name but different purposes, to steal users funds.
{
"0xFUNGIBLE_TOKEN_ADDRESS": {
"mainnet": "0xf233dcee88fe0abe",
"testnet": "0x9a0766d93b6608b7"
},
"0xFLOW_TOKEN_ADDRESS": {
"mainnet": "0x1654653399040a61",
"testnet": "0x7e60df042a9c0868"
},
"0xTELEPORTED_USDT_ADDRESS": {
"mainnet": "0xcfdd90d4a00f7b5b",
"testnet": "0xab26e0a07d770ec1"
},
"0xTELEPORT_ADMIN_ADDRESS": {
"mainnet": "0x55ad22f01ef568a1",
"testnet": "0xf086a545ce3c552d"
}
}
And this process is optional for dapps. It's just that if they do this, the users can trust the signature request a little bit more.
I really like this approach. I'd say let's move forward with this solution as a good initial step.
Also, I was trying to think of a way to also provide transaction re-use across contracts, but maybe we don't need to get that fancy out the gate. For example, TransferNFT.cdc transaction will likely look exactly the same for all apps:
import NonFungibleToken from 0xNON_FUNGIBLE_ADDRESS
import _GENERAL_NFT from 0xGENERAL_NFT_ADDRESS
// This transaction is for transferring and NFT from
// one account to another
transaction(recipient: Address, withdrawID: UInt64) {
prepare(acct: AuthAccount) {
// borrow a reference to the signer's NFT collection
let collectionRef = acct.borrow<&_GENERAL_NFT.Collection>(from: _GENERAL_NFT.CollectionStoragePath)
?? panic("Could not borrow a reference to the owner's collection")
// withdraw the NFT from the owner's collection
let nft <- collectionRef.withdraw(withdrawID: withdrawID)
}
execute {
// get the recipients public account object
let recipientAccount = getAccount(recipient)
// borrow a public reference to the receivers collection
let depositRef = recipientAccount.getCapability(_GENERAL_NFT.CollectionPublicPath)!.borrow<&{_GENERAL_NFT._GENERAL_NFTCollectionPublic}>()!
// Deposit the NFT in the recipient's collection
depositRef.deposit(token: <-nft)
}
}
In the above, your solution would already string replace the contract addresses in Lines 1 and 2 using the config.json file, so the only other string replace needed would be for the actual contract name _GENERAL_NFT. If you can think of a simple solution for this, it might help for standardization of similar transactions and for scaling assuming there could be thousands of separate ex. TransferNFT.cdc transactions being submitted for all the various NFT contracts on Flow.
It would be nice if cadence-docgen was expanded to handle transactions as well. So that the developer can add some words on what is going on in it.
I think the ultimate solution (to custom transactions etc) is to cover all mutations on the chain with transaction post conditions.
But showing that in a user-friendly manner which my mom can understand is totally different story.