diff --git a/.gitignore b/.gitignore index 05d339b4..a1389fdb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules dist .DS_Store +settings.json diff --git a/docs/flow-token.md b/docs/flow-token.md index 443da0a6..0be14c67 100644 --- a/docs/flow-token.md +++ b/docs/flow-token.md @@ -4,6 +4,8 @@ sidebar_title: FLOW Token description: How to mint FLOW Token --- +## FLOW + Some actions on the network will require an account to have a certain amount of FLOW (tokens) - transaction and storage fees, account creation, etc. Framework provides a method to query FLOW balances with `getFlowBalance` and mint new tokens via `mintFlow`. @@ -81,3 +83,84 @@ const main = async () => { main(); ``` + +## FUSD + +You can also mint FUSD similarly to how you mint FLOW Token + +## `getFUSDBalance(address)` + +Returns current FUSD token balance of the specified account. + +#### Arguments + +| Name | Type | Description | +| --------- | --------------------------------------------------- | ------------------------------- | +| `address` | [Address](https://docs.onflow.org/fcl/reference/api/#address) | address of the account to check | + +#### Returns + +| Type | Description | +| -------- | ------------------------------------------------------ | +| `string` | UFix64 amount of FUSD tokens stored in account storage | + +#### Usage + +```javascript +import { init, emulator, getFUSDBalance } from "flow-js-testing"; + +const main = async () => { + const basePath = path.resolve(__dirname, "../cadence"); + const port = 8080; + + await init(basePath, { port }); + await emulator.start(port); + + const Alice = await getAccountAddress("Alice"); + + const [result, error] = await getFUSDBalance(Alice); + console.log(result, error); + + await emulator.stop(); +}; + +main(); +``` + +## `mintFUSD(recipient, amount)` + +Sends transaction to mint the specified amount of FUSD and send it to recipient. + +> ⚠️ **Required:** Framework shall be initialized with `init` method for this method to work. + +#### Arguments + +| Name | Type | Description | +| ----------- | --------------------------------------------------- | ---------------------------------------------------------- | +| `recipient` | [Address](https://docs.onflow.org/fcl/reference/api/#address) | address of the account to check | +| `amount` | string | UFix64 amount of FUSD tokens to mint and send to recipient | + +#### Usage + +```javascript +import { init, emulator, mintFUSD } from "flow-js-testing"; + +const main = async () => { + const basePath = path.resolve(__dirname, "../cadence"); + const port = 8080; + + await init(basePath, { port }); + await emulator.start(port); + + const Alice = await getAccountAddress("Alice"); + const amount = "42.0"; + const [mintResult, error] = await mintFUSD(Alice); + console.log( mintResult, error ); + + await emulator.stop(); +}; + +main(); +``` + +Note: in order to transfer flow from one account to the other, you must either run `getFUSDBalance` or `mintFUSD` for both accounts. These functions create and store the FUSD vault. The storage path is `/storage/fusdVault` and the receiver is in `/public/fusdReceiver` \ No newline at end of file diff --git a/src/file.js b/src/file.js index 5b42225f..08d8d393 100644 --- a/src/file.js +++ b/src/file.js @@ -36,6 +36,7 @@ export const defaultsByName = { FungibleToken: "0xee82856bf20e2aa6", FlowFees: "0xe5a8b7f23e8b548f", FlowStorageFees: "0xf8d6e0586b0a20c7", + FUSD: "0xf8d6e0586b0a20c7", }; /** diff --git a/src/flow-token.js b/src/flow-token.js index 929c3da4..f0057353 100644 --- a/src/flow-token.js +++ b/src/flow-token.js @@ -16,10 +16,7 @@ * limitations under the License. */ -import { defaultsByName } from "./file"; -import { replaceImportAddresses } from "./imports"; -import { executeScript, sendTransaction } from "./interaction"; -import { makeGetBalance, makeMintTransaction } from "./templates"; +import { getTokenBalance, mintToken } from "./token"; /** * Returns current FlowToken balance of account specified by address @@ -27,11 +24,7 @@ import { makeGetBalance, makeMintTransaction } from "./templates"; * @returns {Promise<*>} */ export const getFlowBalance = async (address) => { - const raw = await makeGetBalance("FlowToken"); - const code = replaceImportAddresses(raw, defaultsByName); - const args = [address]; - - return executeScript({ code, args }); + return await getTokenBalance("FlowToken", address); }; /** @@ -42,8 +35,5 @@ export const getFlowBalance = async (address) => { * @returns {Promise<*>} */ export const mintFlow = async (recipient, amount) => { - const raw = await makeMintTransaction("FlowToken"); - const code = replaceImportAddresses(raw, defaultsByName); - const args = [recipient, amount]; - return sendTransaction({ code, args }); + return await mintToken("FlowToken", recipient, amount); }; diff --git a/src/fusd-token.js b/src/fusd-token.js new file mode 100644 index 00000000..7e5bb8a1 --- /dev/null +++ b/src/fusd-token.js @@ -0,0 +1,41 @@ +/* + * Flow JS Testing + * + * Copyright 2020-2021 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getTokenBalance, mintNoLimitToken, setupVault } from "./token"; + +/** + * Returns current FUSD balance of account specified by address + * @param {string} address - address of account to check + * @returns {Promise<*>} + */ +export const getFUSDBalance = async (address) => { + await setupVault("FUSD", address); + return await getTokenBalance("FUSD", address); +}; + +/** + * Sends transaction to mint specified amount of FUSD and send it to recipient. + * Returns result of the transaction. + * @param {string} recipient - address of recipient account + * @param {string} amount - amount to mint and send + * @returns {Promise<*>} + */ +export const mintFUSD = async (recipient, amount) => { + await setupVault("FUSD", recipient); + return await mintNoLimitToken("FUSD", recipient, amount); +}; diff --git a/src/generated/transactions/index.js b/src/generated/transactions/index.js index d48170dc..0b6640e4 100644 --- a/src/generated/transactions/index.js +++ b/src/generated/transactions/index.js @@ -6,6 +6,7 @@ import { registerContractTemplate, registerContract } from "./registerContract"; import { scratchTemplate, scratch } from "./scratch"; import { setBlockOffsetTemplate, setBlockOffset } from "./setBlockOffset"; import { updateContractTemplate, updateContract } from "./updateContract"; +import { setupVaultTemplate } from "./setupVault"; export default { createAccountTemplate, @@ -22,6 +23,7 @@ export default { scratch, setBlockOffsetTemplate, setBlockOffset, + setupVaultTemplate, updateContractTemplate, updateContract, }; diff --git a/src/generated/transactions/setupVault.js b/src/generated/transactions/setupVault.js new file mode 100644 index 00000000..798a4074 --- /dev/null +++ b/src/generated/transactions/setupVault.js @@ -0,0 +1,53 @@ +// my guess is that this template should be generated. Not sure how to generate +// the transaction file I want, so just creating it here + +import { + getEnvironment, + replaceImportAddresses, + reportMissingImports, +} from 'flow-cadut' + +export const CODE = ` +import FungibleToken from 0xFungibleToken +import ExampleToken from 0xTOKENADDRESS + +transaction { + prepare(acct: AuthAccount) { + // if the vault already exists, just exit + let existingVault = acct.borrow<&ExampleToken.Vault>(from: /storage/exampleTokenVault) + if existingVault != nil { + return + } + // Create a new empty Vault object + let vaultA <- ExampleToken.createEmptyVault() + + // Store the vault in the account storage + acct.save<@ExampleToken.Vault>(<-vaultA, to: /storage/exampleTokenVault) + + // Create a public Receiver capability to the Vault + let ReceiverRef = acct.link<&ExampleToken.Vault{FungibleToken.Receiver}>( + /public/exampleTokenReceiver, target: /storage/exampleTokenVault) + + // Create a balance capability + let BalanceRef = acct.link<&ExampleToken.Vault{FungibleToken.Balance}>( + /public/exampleTokenBalance, target: /storage/exampleTokenVault) + } +} +`; + +/** +* Method to generate cadence code for mintTokens transaction +* @param {Object.} addressMap - contract name as a key and address where it's deployed as value +*/ +export const setupVaultTemplate = async (addressMap = {}) => { + const envMap = await getEnvironment(); + const fullMap = { + ...envMap, + ...addressMap, + }; + + // If there are any missing imports in fullMap it will be reported via console + reportMissingImports(CODE, fullMap, `setupVault =>`) + + return replaceImportAddresses(CODE, fullMap); +}; diff --git a/src/index.js b/src/index.js index 406fe6e9..4ff130fb 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,7 @@ export { set, getConfigValue } from "./config"; export { getTemplate, getScriptCode, getContractCode, getTransactionCode } from "./file"; export { sendTransaction, executeScript } from "./interaction"; export { getFlowBalance, mintFlow } from "./flow-token"; +export { getFUSDBalance, mintFUSD } from "./fusd-token"; export { deployContract, deployContractByName } from "./deploy-code"; export { getAccountAddress } from "./account"; export { getServiceAddress, getBlockOffset, setBlockOffset } from "./manager"; diff --git a/src/templates.js b/src/templates.js index 83cfde0f..76dd0c82 100644 --- a/src/templates.js +++ b/src/templates.js @@ -21,16 +21,19 @@ import { defaultsByName } from "./file"; const FlowTokenMap = { ExampleToken: defaultsByName.FlowToken }; -const lowerFirst = (name) => { - return name[0].toLowerCase() + name.slice(1); -}; +// sets the starting contiguous uppercase characters to lowercase +// e.g. FlowToken -> flowToken, FUSD -> fusd +const lowerUpperStart = (name) => { + const pattern = /^([A-Z]+)/ + return name.replace(pattern, (_, uppers) => uppers.toLowerCase()) +} export const makeMintTransaction = async (name) => { const code = await registry.transactions.mintTokensTemplate(FlowTokenMap); const pattern = /(ExampleToken)/gi; return code.replace(pattern, (match) => { - return match === "ExampleToken" ? name : lowerFirst(name); + return match === "ExampleToken" ? name : lowerUpperStart(name); }); }; @@ -39,6 +42,15 @@ export const makeGetBalance = async (name) => { const pattern = /(ExampleToken)/gi; return code.replace(pattern, (match) => { - return match === "ExampleToken" ? name : lowerFirst(name); + return match === "ExampleToken" ? name : lowerUpperStart(name); }); }; + +export const makeSetupVaultTransaction = async (name) => { + const code = await registry.transactions.setupVaultTemplate(FlowTokenMap); + const pattern = /(ExampleToken)/gi; + + return code.replace(pattern, (match) => { + return match === "ExampleToken" ? name : lowerUpperStart(name); + }); +} diff --git a/src/token.js b/src/token.js new file mode 100644 index 00000000..07491381 --- /dev/null +++ b/src/token.js @@ -0,0 +1,76 @@ +import { defaultsByName } from "./file"; +import { replaceImportAddresses } from "./imports"; +import { executeScript, sendTransaction } from "./interaction"; +import { makeGetBalance, makeMintTransaction, makeSetupVaultTransaction } from "./templates"; + +/** + * Returns current FlowToken balance of account specified by address + * @param {string} tokenName - name of the token to check + * @param {string} address - address of account to check + * @returns {Promise<*>} + */ +export const getTokenBalance = async (tokenName, address, addressMap = {}) => { + const raw = await makeGetBalance(tokenName); + const code = replaceImportAddresses(raw, { + ...defaultsByName, + ...addressMap + }); + const args = [address]; + + return executeScript({ code, args }); +}; + +/** + * Sends transaction to mint specified amount of FlowToken and send it to recipient. + * Returns result of the transaction. + * @param {string} tokenName - name of the token to mint + * @param {string} recipient - address of recipient account + * @param {string} amount - amount to mint and send + * @returns {Promise<*>} + */ +export const mintToken = async (tokenName, recipient, amount, addressMap = {}) => { + const raw = await makeMintTransaction(tokenName); + const code = replaceImportAddresses(raw, { + ...defaultsByName, + ...addressMap + }); + const args = [recipient, amount]; + return sendTransaction({ code, args }); +}; + +/** + * Sends transaction to mint specified amount of custom token and send it to recipient. + * Returns result of the transaction. + * @param {string} tokenName - name of the token to mint + * @param {string} recipient - address of recipient account + * @param {string} amount - amount to mint and send + * @returns {Promise<*>} + */ + export const mintNoLimitToken = async (tokenName, recipient, amount, addressMap = {}) => { + const raw = await makeMintTransaction(tokenName); + const code = replaceImportAddresses(raw, { + ...defaultsByName, + ...addressMap + }).replace('self.tokenAdmin.createNewMinter(allowedAmount: amount)', + 'self.tokenAdmin.createNewMinter()'); + + const args = [recipient, amount]; + return sendTransaction({ code, args }); +}; + +/** + * Creates a token vault and saves it to the account's storage with canonical naming + * @param {string} tokenName - name of the token vault to set up (omit 'Vault') + * @param {string} account address of the account + * @param {object} addressMap additional contract address mappings + * @returns {Promise<*>} + */ +export const setupVault = async (tokenName, account, addressMap = {}) => { + const raw = await makeSetupVaultTransaction(tokenName); + const code = replaceImportAddresses(raw, { + ...defaultsByName, + ...addressMap + }); + const signers = [account]; + return sendTransaction({ code, signers }); +} diff --git a/test/fusd.test.js b/test/fusd.test.js new file mode 100644 index 00000000..c93dd57e --- /dev/null +++ b/test/fusd.test.js @@ -0,0 +1,35 @@ +import path from "path"; +import { + init, + emulator, + getAccountAddress, + mintFUSD, + getFUSDBalance +} from "../src" + +describe("fusd-tests", () => { + beforeEach(async () => { + const basePath = path.resolve(__dirname, "../cadence"); + // You can specify different port to parallelize execution of describe blocks + const port = 8080; + // Setting logging flag to true will pipe emulator output to console + const logging = true; + + await init(basePath, { port }); + return emulator.start(port, logging); + }); + + afterEach(async () => { + return emulator.stop(); + }); + + test("mintFUSD", async () => { + const acct = await getAccountAddress("acct"); + + const response = await mintFUSD(acct, "33.0"); + const balance = await getFUSDBalance(acct); + expect(response[1]).toBeNull(); + expect(balance[1]).toBeNull(); + expect(parseFloat(balance)).toBe(33.0); + }); +}); \ No newline at end of file