NEAR Blockchain
Video/Text

Part 2: Building the Fungible Token Contract

Lesson 8 Chapter 2 Module 2

In Part 1, we built the supporting files (ft-models.ts, ft-types.ts, and ft-error-messages.ts) as well as started building out main.ts (sections 1, 2, and 3).  We're going to continue on with main.ts now...

Section 4 - The Fungible Token Standard Functions

Now, we're into the meat of the contract.  If you recall, the fungible token standard demands that we implement the functions defined in the spec.  To that end, ensure your main.ts file is open and add this next:

/***************************/
/* FUNGIBLE TOKEN STANDARD */
/************************* */

// CHANGE METHODS
// ————–

/**
* Increments the `allowance` for `escrow_account_id` by `amount` on the account of the caller of this contract
* (`predecessor_id`) who is the balance owner.
*
* @param escrow_account_id
* @param amount
*/
export function inc_allowance(escrow_account_id: AccountId, amount: Amount): void {
assert(amount > u128.Zero, ERR_INVALID_AMOUNT)
assert(env.isValidAccountID(escrow_account_id), ERR_INVALID_ACCOUNT_ID)
const owner_id = Context.predecessor
assert(escrow_account_id==owner_id, ERR_INCREMENT_ALLOWANCE_OWNER)

const balance = allowanceRegistry.get(keyFrom(owner_id, escrow_account_id))
if(u128.from(balance) > u128.Zero) {
allowanceRegistry.set(keyFrom(owner_id, escrow_account_id), u128.from(u128.add(u128.from(balance), u128.from(amount)).lo))
} else {
allowanceRegistry.delete(keyFrom(owner_id, escrow_account_id))
}

}

/**
* Decrements the `allowance` for `escrow_account_id` by `amount` on the account of the caller of this contract
* (`predecessor_id`) who is the balance owner.
*
* @param escrow_account_id
* @param amount
*/
export function dec_allowance(escrow_account_id: AccountId, amount: Amount): void {
assert(amount > u128.Zero, ERR_INVALID_AMOUNT)
assert(env.isValidAccountID(escrow_account_id), ERR_INVALID_ACCOUNT_ID)
const owner_id = Context.predecessor
assert(escrow_account_id==owner_id, ERR_DECREMENT_ALLOWANCE_OWNER)

const balance = allowanceRegistry.get(keyFrom(owner_id, escrow_account_id))
if(u128.from(balance) > u128.Zero) {
assert(u128.sub(u128.from(balance), u128.from(amount)) > u128.Zero, ERR_DECREMENT_LESS_THAN_ZERO)
allowanceRegistry.set(keyFrom(owner_id, escrow_account_id), u128.from(u128.sub(u128.from(balance), u128.from(amount)).lo))
} else {
allowanceRegistry.delete(keyFrom(owner_id, escrow_account_id))
}

}

/**
* Transfers the `amount` of tokens from `owner_id` to the `new_owner_id`.
* Requirements:
* – `amount` should be a positive integer.
* – `owner_id` should have balance on the account greater or equal than the transfer `amount`.
* – If this function is called by an escrow account (`owner_id != predecessor_id`),
* then the allowance of the caller of the function (`predecessor_id`) on
* the account of `owner_id` should be greater or equal than the transfer `amount`.
* @param owner_id
* @param new_owner_id
* @param amount
*/
export function transfer_from(owner_id: AccountId, new_owner_id: AccountId, amount: Amount): void {
assert(amount > u128.Zero, ERR_INVALID_AMOUNT)
assert(env.isValidAccountID(owner_id), ERR_INVALID_ACCOUNT_ID)
assert(env.isValidAccountID(new_owner_id), ERR_INVALID_ACCOUNT_ID)
assert(balanceRegistry.contains(owner_id), ERR_INVALID_ACCOUNT)
assert(balanceRegistry.getSome(owner_id) >= amount, ERR_INSUFFICIENT_BALANCE)

if(owner_id != Context.predecessor) {
const key = keyFrom(owner_id, Context.predecessor)
assert(allowanceRegistry.contains(key), ERR_INVALID_ESCROW_ACCOUNT)

const allowance = allowanceRegistry.getSome(key)
assert(allowance >= amount, ERR_INSUFFICIENT_ESCROW_BALANCE)

allowanceRegistry.set(key, u128.sub(allowance, amount))
}

const balanceOfOwner = balanceRegistry.getSome(owner_id)
const balanceOfNewOwner = balanceRegistry.get(new_owner_id, u128.Zero)!

balanceRegistry.set(owner_id, u128.sub(balanceOfOwner, amount))
balanceRegistry.set(new_owner_id, u128.add(balanceOfNewOwner, amount))
}

/**
* Transfer `amount` of tokens from the caller of the contract (`predecessor_id`) to
* `new_owner_id`.
* Note: This call behaves as if `transfer_from` with `owner_id` equal to the caller
* of the contract (`predecessor_id`).
* @param new_owner_id
* @param amount
*/
export function transfer(new_owner_id: AccountId, amount: Amount): void {
assert(env.isValidAccountID(new_owner_id), ERR_INVALID_ACCOUNT_ID)
const owner_id = Context.predecessor
transfer_from(owner_id, new_owner_id, amount)
}

// VIEW METHODS
// ————

/**
* Returns total supply of tokens.
*/
export function get_total_supply(): u128 {
return totalSupply.getSome(‘totalSupply’)
}

/**
* Returns balance of the `owner_id` account.
* @param owner_id
*/
export function get_balance(owner_id: AccountId): u128 {
assert(balanceRegistry.contains(owner_id), ERR_INVALID_ACCOUNT)
return balanceRegistry.getSome(owner_id)
}

/**
* Returns current allowance of `escrow_account_id` for the account of `owner_id`.
*
* NOTE: Other contracts should not rely on this information, because by the moment a contract
* receives this information, the allowance may already be changed by the owner.
* So this method should only be used on the front-end to see the current allowance.
*/
export function get_allowance(owner_id: AccountId, escrow_account_id: AccountId): u128 {
const key = keyFrom(owner_id, escrow_account_id)
assert(allowanceRegistry.contains(key), ERR_INVALID_ACCOUNT)
return allowanceRegistry.get(key, u128.Zero)!
}

Yeah - lot going on here.  Let's break it down - step-by-step.

First, you'll notice this section is divided into the Change methods and the View methods.  To refresh your memory - change methods are those that are going to change storage/state on the blockchain and the transaction will cost some gas.  View methods simply query the blockchain asking it to return what's currently there and do not cost anything.

On to the first function...

Change Methods

export function inc_allowance...

In plain English - this increments the allowance by the indicated amount of an account assigned to be an escrow account.  It allows that escrow account to transfer (spend) tokens up to the amount we've authorized it to by calling this function.  If it's the first time it's called for an escrow account, that account's allowance will be zero and this amount will be added to it to give it a new allowance amount.  Subsequent calls will see the allowance amount increment (increase) by the amount in the transaction.

  • we start out with some conditions checks with three assert statements:
    • assert(amount > u128.Zero, ERR_INVALID_AMOUNT) - checks to ensure that the amount we sent in is a positive amount (greater than zero) otherwise it aborts and sends back the invalid amount message
    • assert(env.isValidAccountID(escrow_account_id), ERR_INVALID_ACCOUNT) - checks to ensure the account we are designating as an escrow account is valid
    • next we assign Context.predecessor to the owner_id variable.  Remember that Context.predecessor is the account Id of the account that just called this contract function. As we're using the function to designate an escrow account and it's allowance we want to make sure we're not trying to set it to the same account that is calling the function.
    • assert(escrow_account_id==owner_id, ERR_INCREMENT_ALLOWANCE_OWNER) does the check to ensure the escrow account is not the same as the account calling the function (owner), otherwise it throws the error.
  • the next section does the following:
    • looks in the allowanceRegistry for the owner_id:escrow_account_id string and returns the value it finds associated with it - it gets stored in balance
    • if balance is greater than zero (meaning it found the key in the allowanceRegistry) - it sets a new association between the owner_id:escrow_account_id and the old balance plus the amount we want to increment it by in allowanceRegistry.  That becomes the new allowance limit for the escrow_account_id.
    • if balance is not greater than zero, it means it can't find the key.  Remember that there won't be a key-value pair of 0 because the assert check prohibits a zero amount to be passed in.  A command is sent to delete the key (allowanceRegistry.delete) if it does exist for whatever reason - because it shouldn't be there.

export function dec_allowance...

In plain English - identical to inc_allowance but decrements the allowance instead of incrementing it by the indicated amount of an account assigned to be an escrow account.  It allows that escrow account to transfer (spend) tokens up to the amount we've authorized it to by calling this function.  As you can't decrement an allowance that doesn't exist anytime this is called, it will see the allowance amount decrement (decrease) by the amount in the transaction assuming there was an allowance there to begin with.

  • we start out with some conditions checks with three assert statements:
    • assert(amount > u128.Zero, ERR_INVALID_AMOUNT) - checks to ensure that the amount we sent in is a positive amount (greater than zero) otherwise it aborts and sends back the invalid amount message
    • assert(env.isValidAccountID(escrow_account_id), ERR_INVALID_ACCOUNT) - checks to ensure the account we are designating as an escrow account is valid
    • next we assign Context.predecessor to the owner_id variable.  Remember that Context.predecessor is the account Id of the account that just called this contract function. As we're using the function to designate an escrow account and it's allowance we want to make sure we're not trying to set it to the same account that is calling the function.
    • assert(escrow_account_id==owner_id, ERR_INCREMENT_ALLOWANCE_OWNER) does the check to ensure the escrow account is not the same as the account calling the function (owner), otherwise it throws the error.
  • the next section does the following:
    • looks in the allowanceRegistry for the owner_id:escrow_account_id string and returns the value it finds associated with it - it gets stored in balance
    • if balance is greater than zero (meaning it found the key in the allowanceRegistry) - it does a second conditions check
      • checks to ensure the current balance minus the transaction amount is still positive (greater than zero).  If not, it aborts with a message saying the decrement amount is larger than the allowance available.  If it is positive - it carries on.
      • it sets a new association between the owner_id:escrow_account_id and the old balance minus the amount we want to decrement it by in allowanceRegistry.  That becomes the new allowance limit for the escrow_account_id.
    • if balance is not greater than zero, it means it can't find the key.  Remember that there won't be a key-value pair of 0 because the assert check prohibits a zero amount to be passed in.  A command is sent to delete the key (allowanceRegistry.delete) if it does exist for whatever reason - because it shouldn't be there.

export function transfer_from...

In plain English - transfers an amount of tokens from one account Id to another account Id.  We'll see how this is different from the transfer function in a bit, but transfer_from is typically called from a third-party who we've authorized to spend/transfer tokens on our behalf.

  • we start out with five conditions checks:
    • assert(amount > u128.Zero, ERR_INVALID_AMOUNT) - checks to ensure that the amount we sent in is a positive amount (greater than zero) otherwise it aborts and sends back the invalid amount message
    • two assert statements using env.isValidAccountID - checks to ensure both the account we are sending from and the account we are sending to are valid account Ids
    • assert(balanceRegistry.contains(owner_id), ERR_INVALID_ACCOUNT) checks to ensure the from (owner_id) exists in the balanceRegistry.
    • If the owner_id exists from previous assertion - retrieves the balance amount associated with the account and ensures it is greater than or equal to the amount we're looking to transfer - if not, it's an insufficient balance.
  • with the assert condition checks done and passing, the next section does the following:
    • If an Escrow Account - first - if the account Id calling the transfer_from function is not the from address (owner_id) (meaning this is a third-party, possibly an escrow account) - it first uses keyFrom to create the lookup key that the allowanceRegistry uses in format of x:y.  It then uses that to do two assertions:
      • an assert to confirm it is a valid escrow account Id in the registry; and
      • an assertion to confirm that escrow account trying to do the transfer has an allowable spending/transfer limit greater than or equal to amount being asked to transfer.
    • If those assertions pass - the escrow account's allowable spending/transfer limit is reduced by the amount of the transfer and set in the allowanceRegistry
    • If not an Escrow Account and next steps for Escrow accounts after allowance update above - (meaning from address (owner_id) is same as the address calling the transferFrom function (Context.predecessor) or we've moved on from the escrow allowance adjustments above - we first get the from address (owner_id) balance from the balanceRegistry and the to owner's balance from the balanceRegistry (new_owner_id).
    • Finally, we simply subtract the transfer balance from the owner_id's balance and add it to the new_owner_id's balance - setting both in the balanceRegistry.

export function transfer...

In plain English - transfers an amount of tokens from the owner account Id to another account Id.  While it looks very similar to transfer_from and in fact, simply calls transfer_from from inside it, it's used when a third party/escrow account is not involved in the transfer.  It's coming directly from the owner (owner_id = Context.predecessor).

  • we don't need any condition checks because they will all be performed in the transfer_from function
  • the transfer_from function needs three arguments - owner_id, new_owner_id, amount.  All we need to do is set the owner_id = Context.predecessor and pass all three to transfer_from.
  • The token transfer from that point enters the transfer_from function and is handled in the manner described above.

View Methods

View methods are pretty straight forward - they just return some information when invoked.

export function get_total_supply

In plain English - returns the total token supply that was set when the token was created.  In a pure implementation of the Fungible Token Standard - this amount should not change; however, if the standard has been extended with minting and burning functions - the totalSupply will fluctuate.

  • all this function does is look in the totalSupply PersistentMap storage collection that we created for the 'totalSupply' key and returns the u128 value representing the total supply of tokens available

export function get_balance

In plain English - returns the current balance of whatever account Id is passed into it.

  • first we do an assert to ensure the account Id is in the balanceRegistry otherwise identify that it is an invalid account Id
  • If there, we get and return the u128 value associated with the account Id representing the account's balance

export function get_allowance

In plain English - returns the current spending/transfer limit of the escrow account Id passed into it.  Remember that escrow accounts are third party accounts that an owner of a token has authorized to spend/transfer their tokens up to their authorized limit.

  • first we need to use the utility function keyFrom to create the key that we need to lookup in the allowanceRegistry (format x:y)
  • We use that key and do an assert to ensure it is in the allowanceRegistry - if not, we identify it as an invalid account (we could probably improve this by saying it is an invalid escrow account...)
  • If the assertion passes, we return the u128 value associated with the key if there is one.  If not, we return the default value which is u128.Zero.  Notice the ! at the end of this line - allowanceRegistry.get(key, u128.Zero)! - that is the non-null assertion operator and if you're trying to get a value and supplying a default - you might fail a type check - that ! prevents that.  Without it, you're going to get an error something like (Type u128 | null is not assignable to u128 or Type 'null' is not assignable to type u128)

Phew...that was quite the marathon, but that's the entire Fungible Token Standard for NEAR.  Next up, we're going to cover a few additional functions that we want to add to the implementation to provide some more functionality.

See you in the next lesson.

Pen
>