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 ofAssetBalance
instances.authDescriptor
: an array ofAuthDescriptor
instances.session
: theBlockchainSession
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.
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) {
...
}