Your First Fungible Asset
This tutorial introduces how you can compile, deploy, and mint your own fungible asset (FA), named FACoin. Make sure you have understood FA before moving on to the tutorial. If not, it is highly recommended to read it first.
Step 1: Pick an SDK
Install your preferred SDK from the below list:
Step 2: Install the CLI
Install the precompiled binary for the Aptos CLI.
Step 3: Run the example
Clone the aptos-ts-sdk
repo and build it:
git clone https://github.com/aptos-labs/aptos-ts-sdk.git
cd aptos-ts-sdk
pnpm install
pnpm build
Navigate to the TypeScript examples directory:
cd examples/typescript
Install the necessary dependencies:
pnpm install
Run the TypeScript your_fungible_asset
example:
pnpm run your_fungible_asset
The application will complete, printing:
===Publishing FAcoin package===
Transaction hash: 0x90fbe811171dde1ffad9157314ce0c4f6070fd5c1095a0e18a0b5634d10d7f48
metadata address: 0x77503715cb75fcc276b4e7236210fb0bcac7b510f6428233b65468b1cd3d708b
All the balances in this example refer to balance in primary fungible stores of each account.
Alice's initial FACoin balance: 0.
Bob's initial FACoin balance: 0.
Charlie's initial balance: 0.
Alice mints Charlie 100 coins.
Charlie's updated FACoin primary fungible store balance: 100.
Alice freezes Bob's account.
Alice as the admin forcefully transfers the newly minted coins of Charlie to Bob ignoring that Bob's account is frozen.
Bob's updated FACoin balance: 100.
Alice unfreezes Bob's account.
Alice burns 50 coins from Bob.
Bob's updated FACoin balance: 50.
Bob transfers 10 coins to Alice as the owner.
Alice's updated FACoin balance: 10.
Bob's updated FACoin balance: 40.
Step 4: FACoin in depth
Step 4.1: Building and publishing the FACoin package
Move contracts are effectively a set of Move modules known as a package. When deploying or upgrading a new package, the compiler must be invoked with --save-metadata
to publish the package. In the case of FACoin, the following output files are critical:
build/Examples/package-metadata.bcs
: Contains the metadata associated with the package.build/Examples/bytecode_modules/fa_coin.mv
: Contains the bytecode for thefa_coin.move
module.
These are read by the example and published to the Aptos blockchain:
In the TypeScript example, we use aptos move build-publish-payload
command to compile and build the module.
That command builds the build
folder that contains the package-metadata.bcs
and the bytecode for the moon_coin.mv
module. The command also builds a publication transaction payload and stores it in a JSON output file that we can later read from to get the metadataBytes
and byteCode
to publish the contract to chain with.
Compile the package:
export function compilePackage(
packageDir: string,
outputFile: string,
namedAddresses: Array<{ name: string; address: AccountAddress }>,
) {
const addressArg = namedAddresses
.map(({ name, address }) => `${name}=${address}`)
.join(" ");
// Assume-yes automatically overwrites the previous compiled version, only do this if you are sure you want to overwrite the previous version.
const compileCommand = `aptos move build-publish-payload --json-output-file ${outputFile} --package-dir ${packageDir} --named-addresses ${addressArg} --assume-yes`;
execSync(compileCommand);
}
compilePackage("move/facoin", "move/facoin/facoin.json", [
{ name: "FACoin", address: alice.accountAddress },
]);
Publish the package to chain:
import path from "path";
import fs from "fs";
export function getPackageBytesToPublish(filePath: string) {
// current working directory - the root folder of this repo
const cwd = process.cwd();
// target directory - current working directory + filePath (filePath json file is generated with the previous, compilePackage, CLI command)
const modulePath = path.join(cwd, filePath);
const jsonData = JSON.parse(fs.readFileSync(modulePath, "utf8"));
const metadataBytes = jsonData.args[0].value;
const byteCode = jsonData.args[1].value;
return { metadataBytes, byteCode };
}
const { metadataBytes, byteCode } = getPackageBytesToPublish(
"move/facoin/facoin.json",
);
// Publish FACoin package to chain
const transaction = await aptos.publishPackageTransaction({
account: alice.accountAddress,
metadataBytes,
moduleBytecode: byteCode,
});
const pendingTransaction = await aptos.signAndSubmitTransaction({
signer: alice,
transaction,
});
await aptos.waitForTransaction({ transactionHash: pendingTransaction.hash });
Step 4.2: Understanding the FACoin module
The FACoin module contains a function called init_module
in which it creates a named metadata object that defines a type of FA called “FACoin” with a bunch of properties. The init_module
function is called when the module is published. In this case, FACoin initializes the FACoin
metadata object, owned by the owner of the account. According to the module code, the owner will be the admin of “FACoin” so that they are entitled to manage “FACoin” under the fungible asset framework.
Managed Fungible Asset framework
Managed Fungible Asset
is a full-fledged FA management framework for FAs directly managed by users. It provides convenience wrappers around different refs
and both primary and secondary fungible stores. This example is a simplified version that only deals with primary stores.
Step 4.3: Understanding the management primitives of FACoin
The creator of FACoin have several managing primitives:
- Minting: Creating new coins.
- Burning: Deleting coins.
- Freezing/Unfreezing: Disabling/Enabling the owner of an account to withdraw from or deposit to their primary fungible store of FACoin.
- Withdraw: Withdrawing FACoin from the primary store of any account regardless of being frozen or not.
- Deposit: Deposit FACoin from the primary store of any account regardless of being frozen or not.
- Transfer: Withdraw from one account and deposit to another regardless of either being frozen or not.
The entity that creates FACoin gains the capabilities for minting, burning, freezing/unfreezing, and forceful transferring between any fungible stores no matter they are frozen or not.
So Withdraw
, Deposit
, and Transfer
in the management module have different semantics than those described in fungible asset framework that limited by frozen status.
Step 4.3.1: Initializing “FACoin” metadata object
After publishing the module to the Aptos blockchain, the entity that published that coin type should initialize a metadata object describing the information about this FA:
module deploy_addr::fa_coin {
fun init_module(admin: &signer) {
let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL);
primary_fungible_store::create_primary_store_enabled_fungible_asset(
constructor_ref,
option::none(),
utf8(b"FA Coin"), /* name */
utf8(ASSET_SYMBOL), /* symbol */
8, /* decimals */
utf8(b"https://example.com/favicon.ico"), /* icon */
utf8(b"https://example.com"), /* project */
);
// Create mint/burn/transfer refs to allow creator to manage the fungible asset.
let mint_ref = fungible_asset::generate_mint_ref(constructor_ref);
let burn_ref = fungible_asset::generate_burn_ref(constructor_ref);
let transfer_ref = fungible_asset::generate_transfer_ref(constructor_ref);
let metadata_object_signer = object::generate_signer(constructor_ref);
move_to(
&metadata_object_signer,
ManagedFungibleAsset { mint_ref, transfer_ref, burn_ref }
)
}
}
This ensures that this FA type has never been initialized before using a named object. Notice the first line create a named object with a static seed, if the metadata object has been created it will abort.
Then we call primary_fungible_store::create_primary_store_enabled_fungible_asset
to create a FA metadata resource inside the newly created object, most of the time you call this function to initialize
the metadata object. After this call, we generate all the Refs
that are necessary for the management api and store them in a customized wrapper resource.
FACoin does all the initialization automatically upon package publishing via init_module(&signer)
.
Different from coin module, FA doesn’t require users to register to use it because primary store will be automatically created if necessary.
Step 4.3.3: Managing a coin
Minting coins requires MintRef
that was produced during initialization. the function mint
(see below) takes in the creator and an amount, and returns a FungibleAsset
struct containing that amount of FA. If the FA tracks supply, it will be updated.
module deploy_addr::fa_coin {
/// Mint as the owner of metadata object.
public entry fun mint(admin: &signer, to: address, amount: u64) acquires ManagedFungibleAsset {
let asset = get_metadata();
let managed_fungible_asset = authorized_borrow_refs(admin, asset);
let to_wallet = primary_fungible_store::ensure_primary_store_exists(to, asset);
let fa = fungible_asset::mint(&managed_fungible_asset.mint_ref, amount);
fungible_asset::deposit_with_ref(&managed_fungible_asset.transfer_ref, to_wallet, fa);
}
}
FACoin
makes this easier by providing an entry function fa_coin::mint
that accesses the required MintRef
for the creator.
Similarly, the module provides burn
, set_frozen_flag
, transfer
, Withdraw
and Deposit
functions to manage FACoin following the same pattern with different refs.
Step 4.3.4: API of Transferring FAs
Aptos provides several APIs to support FA flows with same names in different modules:
fungible_asset::{transfer/withdraw/deposit}
: Move FA between different unfrozen fungible stores objects.fungible_asset::{transfer/withdraw/deposit}_with_ref
: Move FA between different fungible stores objects with the correspondingTransferRef
regardless of their frozen status.primary_fungible_store::{transfer/withdraw/deposit}
: Move FA between unfrozen primary stores of different accounts.
There are two separate withdraw and deposit events instead of a single transfer event.
Step 4.4: Understanding how to use Dispatchable Hooks
Aptos provides APIs to register withdraw and deposit hooks, allowing for custom logic being injected at each step during a transfer. In the hooks, we will add a global pause to all withdraws and deposits upon a transfer.
module deploy_addr::fa_coin {
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
/// Global state to pause the FA coin.
struct State has key {
paused: bool,
}
}
This is how you would register the hooks upon asset creation (init_module
).
module deploy_addr::fa_coin {
init_module(admin: &signer) {
// ...previous code
// Override the deposit and withdraw functions which mean overriding transfer.
// This ensures all transfer will call withdraw and deposit functions in this module
// and perform the necessary checks.
let deposit = function_info::new_function_info(
admin,
string::utf8(b"fa_coin"),
string::utf8(b"deposit"),
);
let withdraw = function_info::new_function_info(
admin,
string::utf8(b"fa_coin"),
string::utf8(b"withdraw"),
);
dispatchable_fungible_asset::register_dispatch_functions(
constructor_ref,
option::some(withdraw),
option::some(deposit),
option::none(),
);
}
}
After registration of the hooks, we can call the newly defined assert_not_paused
function in our hooks to verify that a transfer is allowed.
module deploy_addr::fa_coin {
// ...previous code
/// Deposit function override to ensure that the account is not denylisted and the FA coin is not paused.
public fun deposit<T: key>(
store: Object<T>,
fa: FungibleAsset,
transfer_ref: &TransferRef,
) acquires State {
assert_not_paused();
fungible_asset::deposit_with_ref(transfer_ref, store, fa);
}
/// Withdraw function override to ensure that the account is not denylisted and the FA coin is not paused.
public fun withdraw<T: key>(
store: Object<T>,
amount: u64,
transfer_ref: &TransferRef,
): FungibleAsset acquires State {
assert_not_paused();
fungible_asset::withdraw_with_ref(transfer_ref, store, amount)
}
/// Assert that the FA coin is not paused.
fun assert_not_paused() acquires State {
let state = borrow_global<State>(object::create_object_address(&@FACoin, ASSET_SYMBOL));
assert!(!state.paused, EPAUSED);
}
}
Note that this will not prevent transfers of stores or impose the pause during minting and burning. When using this feature, developers should be careful to follow the guidelines to prevent circular dependencies between modules. Please read the developer guidelines in the Fungible Asset Standard section.