Skip to main content

Javascript library

In this section, we explain how to use the client side library (ft3-lib node package).

Initialize blockchain object

You need to initialize a Blockchain object that's used to interact with the blockchain before you try to access a blockchain:

// /client/index.js
import { Postchain } from "ft3-lib";
import { blockchainRID, blockchainUrl } from "./configs/constants"; // these configs are set in previous section

const chainId = Buffer.from(blockchainRID, "hex");
const blockchain = await new Postchain(blockchainUrl).blockchain(chainId);

You can access details of the initialized chain by info property which has name, website and description properties:

console.log(`-------------- Blockchain Info --------------`);
console.log(`name : ${blockchain.info.name} `);
console.log(`website : ${blockchain.info.website} `);
console.log(`description : ${blockchain.info.description} `);

These fields have the values which we previously set in the config.template.xml file.


The user class

The User class represents a logged-in user, and it keeps the user's key pair and authentication descriptor.

Any method that requires transaction signing needs an object of this class.

import { SingleSignatureAuthDescriptor, User, FlagsType } from 'ft3-lib';

...

const authDescriptor = new SingleSignatureAuthDescriptor(
keyPair.pubKey,
[FlagsType.Account, FlagsType.Transfer]
);
const user = new User(keyPair, authDescriptor);

Many functions provided by Blockchain class require User object, for example:

const authDescriptor = ...;
const user = ....;

// Gets all accounts where this authDescriptor has control over
const accounts = await blockchain.getAccountsByAuthDescriptorId(
authDescriptor.id,
user
);

In most cases, the same User instance gets used throughout an app (the "current user"). To avoid passing both Blockchain and User objects around in an app, you need the BlockchainSession class.

It has many of the same functions as the Blockchain class, but with a difference that functions provided by the BlockchainSession don't require a User parameter:

const authDescriptor = ...;
const user = ....;

const session = blockchain.newSession(user);
const accounts = await session.getAccountsByAuthDescriptorId(authDescriptor.id);

AuthDescriptor rules

Both AuthDescriptor constructors accept an optional third parameter of type Rules, which define constraints for the descriptor's "valid period."

Supported constrains are:

Rules.operationCount

Number of operations this authDescriptor can perform:

import { SingleSignatureAuthDescriptor, FlagsType, Rules } from "ft3-lib";

const authDescriptor = new SingleSignatureAuthDescriptor(
keyPair.pubKey,
[FlagsType.Account, FlagsType.Transfer],
Rules.operationCount.lessOrEqual(2) // This authDescriptor is only valid for 2 operations
);

Rules.blockTime

Time period during which the authDescriptor has effect:

import { SingleSignatureAuthDescriptor, FlagsType, Rules } from "ft3-lib";

const authDescriptor = new SingleSignatureAuthDescriptor(
keyPair.pubKey,
[FlagsType.Account, FlagsType.Transfer],
Rules.blockTime.greaterThan(Date.now() + 12 * 60 * 60 * 1000) // This authDescriptor will start working 12 hours from now
);

Rules.blockHeight

Block height limitation of the authDescriptor:

import { SingleSignatureAuthDescriptor, FlagsType, Rules } from "ft3-lib";

const authDescriptor = new SingleSignatureAuthDescriptor(
keyPair.pubKey,
[FlagsType.Account, FlagsType.Transfer],
Rules.blockHeight.equal(0) // This authDescriptor is only valid when the chain was just created (0 block is in the chain)
);

Supported operators are:

  • lessThan
  • lessThanOrEqual
  • equal
  • greaterThan
  • greaterOrEqual

Is it also possible to build composite rules:

import { SingleSignatureAuthDescriptor, FlagsType, Rules } from "ft3-lib";

// This authDescriptor will start working 12 hours from now and is only valid for 24 hours
const startDate = Date.now() + 12 * 60 * 60 * 1000;
const endDate = Date.now() + 36 * 60 * 60 * 1000;
const authDescriptor = new SingleSignatureAuthDescriptor(
keyPair.pubKey,
[FlagsType.Account, FlagsType.Transfer],
Rules.blockTime.greaterThan(startDate).and.blockTime.lessThanOrEqual(endDate)
);

The Account class

An Account object contains:

  • assets: an array of AssetBalance instances.
  • authDescriptor: an array of AuthDescriptor instances.
  • session: the BlockchainSession that returned it.

Account registration

const ownerKeyPair = ...;
const authDescriptor = new SingleSignatureAuthDescriptor(
ownerKeyPair.pubKey,
[FlagsType.Account, FlagsType.Transfer]
);

const account = await blockchain.registerAccount(authDescriptor, user);

Usually, the current user creates the account. In those cases, we can simply pass user.authDescriptor into the operation:

const account = await blockchain.registerAccount(user.authDescriptor, user);

Searching accounts

You can search accounts by account ID:

const account = await session.getAccountById(accountId);

by authentication descriptor ID:

const accounts = await session.getAccountsByAuthDescriptorId(authDescriptorId);

or by participant ID:

const accounts = await session.getAccountsByParticipantId(user.keyPair.pubKey);

For SingleSig and MultiSig account descriptors, participant ID is pubKey. Therefore this function allows searching for accounts by pubKey.

info

The difference between getAccountsByParticipantId and getAccountsByAuthDescriptorId is:

  • getAccountsByParticipantId returns all accounts where the user is a participant, no matter which access rights the user has or which type of authentication gets used to control the accounts,
  • while getAccountsByAuthDescriptorId returns only account where the user has access with a specific type of authentication and authorization. :::

Adding authentication descriptor

const newAuthDescriptor = new SingleSignatureAuthDescriptor(pubKey, [
FlagsType.Account,
FlagsType.Transfer,
]);
const account = await session.getAccountById(accountId);
await account.addAuthDescriptor(newAuthDescriptor);

Assets management

AssetBalance

Each account, when queried, comes with an account.assets array of AssetBalance.

An AssetBalance contains information about an Asset the account owns (assetBalance.asset) and the amount owned (assetBalance.amount).

You can also get the asset balance of an account by calling AssetBalance.getByAccountId:

const balances = await AssetBalance.getByAccountId(accountId, blockchain);

or if you take an interest in only one specific asset:

const balance = await AssetBalance.getByAccountAndAssetId(
accountId,
assetId,
blockchain
);

Asset

Each asset has a name and a chainId, which is the id of the chain where the asset come from.

The unique identifier of asset (asset.id) gets generated from the hash of name and chainId, so asset name is unique within a chain, but different chains can have asset with the same name.

For recognition, an asset must have registration on a chain as follows:

await Asset.register(assetName, blockchainId, blockchain);

You can query registered assets by their name as follows:

const assets = await blockchain.getAssetsByName(assetName);

or by id:

const asset = await blockchain.getAssetById(assetId);

You can also get all assets of a chain by calling getAllAssets:

const assets = await blockchain.getAllAssets();

Transferring assets

const account = await session.getAccountById(accountId);
await account.transfer(recipientId, assetId, amount);

Here we see that the Account class retains the same characteristic as BlockchainSession: we don't need to provide a User object to sign the transaction. :::

Transfer history

const history = await account.getPaymentHistory();

getPaymentHistory return an array of PaymentHistoryEntryShort:

class PaymentHistoryEntryShort {
readonly isInput: boolean; // true if account is the sender, false in case of receiver
readonly delta: number; // amount transferred: negative for sender (e.g. -10), positive for receiver (e.g. 12)
readonly asset: string;
readonly assetId: string;
readonly entryIndex: number;
readonly timestamp: Date;
readonly transactionId: string;
readonly transactionData: Buffer;
readonly blockHeight: number;
}

Calling operations

Single operation

FT3 and other blockchain operations can also be directly called using the Blockchain and BlockchainSession classes.

For instance, you can perform the same "adding auth descriptor" operation by using the following:

import { op } from 'ft3-lib';

const account = ...
const user = ...
const newAuthDescriptor = ...

await blockchain.call(
op(
'ft3.add_auth_descriptor',
accountId,
user.authDescriptor.id,
newAuthDescriptor
),
user
)

Multiple operations

You can use transaction builder to call multiple operations in a single transaction:

await blockchain
.transactionBuilder()
.add(op("foo", param1, param2))
.add(op("bar", param))
.buildAndSign(user)
.post();

The previous statement creates a single transaction with foo and bar operations, adds signers from the user's auth descriptor, and signs it with the user's private key.

If you need more control over signers and signing, then you can use the build and sign functions:

await blockchain
.transactionBuilder()
.add(op("foo", param1, param2))
.add(op("bar", param))
.build(signersPublicKeys)
.sign(keyPair1)
.sign(keyPair2)
.post();

Instead of immediately sending a transaction after building it, it's also possible to get a raw transaction:

const rawTransaction = blockchain
.transactionBuilder()
.add(op("foo", param1, param2))
.buildAndSign(user)
.raw();

which you can send to a blockchain node later:

await blockchain.postRaw(rawTransaction);

The nop operation

To prevent a replay attack, postchain rejects a transaction if it has the same content as one of the transactions already stored on the blockchain. For example, if you directly call ft3.transfer operation two times, the second call fails.

const inputs = ...
const outputs = ...
const user = ...

// first will succeed
await blockchain.call(op('ft3.transfer', inputs, outputs), user);

// second will fail
await blockchain.call(op('ft3.transfer', inputs, outputs), user);

To avoid a transaction failing, you can add a nop operation to a second transaction to make it different from the first transaction.

import { op, nop } from "ft3-lib";

await blockchain
.transactionBuilder()
.add(op("ft3.transfer", inputs, outputs))
.add(nop())
.buildAndSign(user)
.post();

nop() function returns the nop operation with a random number as an argument

GtvSerializable interface

In typescript, op function is as follows:

function op(name: string, ...args: GtvSerializable[]): Operation {
return new Operation(name, ...args);
}

It expects arguments to implement GtvSerializable interface, that's, to have implemented toGTV() function.

Array, Buffer, String, and Number are already extended with the toGTV function.

If you need to pass user-defined object to an operation, you need to implement GtvSerializable interface as follows:

class Player {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

toGTV() {
return [this.firstName, this.lastName],
}
}

await blockchain.call(
op('some_op', new Player('John', 'Doe')),
user
)

To be able to handle the Player object on the blockchain side, you need to define some_op as either:

operation some_op(player: list<gtv>) {
...
}

or

struct player {
first_name: text;
last_name: text;
}

operation some_op(player) {
...
}