Javascript library
In this section, we explain how to use the client side library
(ft3-lib
node package).
Initialize Blockchain object
The first thing that has to be done before a blockchain can be accessed is to initialize the Blockchain object used to interact with the 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);
Details of the initialized chain can be accessed from 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 is used to keep the user's key pair and authentication descriptor.
Any method that require transaction signing will need 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 of the cases the same User instance is used throughout an app (the "current user"). In order to avoid passing both Blockchain and User objects around in an app, the BlockchainSession class is introduced.
It has many of the same functions as Blockchain class, but with a difference that functions provided by the BlockchainSession don't require 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 3rd parameter of
type Rules
, which define constraint 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 heigh 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);
More commonly the current user will be creating an account for
themselves. In those cases we can simply pass user.authDescriptor
into
the operation:
const account = await blockchain.registerAccount(user.authDescriptor, user);
Searching accounts
Accounts can be searched 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 to search for accounts by pubKey.
The difference between getAccountsByParticipantId
and
getAccountsByAuthDescriptorId
is:
getAccountsByParticipantId
returns all accounts where user is participant, no matter which access rights user has or which type of authentication is used to control the accounts- while
getAccountsByAuthDescriptorId
returns only accounts where user has access with 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 contain information about an Asset the account own
(assetBalance.asset
), and the amount owned (assetBalance.amount
).
You can also get asset balance of an account by calling
AssetBalance.getByAccountId
:
const balances = await AssetBalance.getByAccountId(accountId, blockchain);
or if you are interested 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
) is 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.
An Asset must be registered on a chain to be recognized:
await Asset.register(assetName, blockchainId, blockchain);
Registered assets can be queried by name:
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 an 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 operations and other blockchain operations can also be directly called using the Blockchain and BlockchainSession classes.
For instance, the same "adding auth descriptor" operation above can be done using:
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
The transaction builder can be used if multiple operations have to be called in a single transaction:
await blockchain
.transactionBuilder()
.add(op("foo", param1, param2))
.add(op("bar", param))
.buildAndSign(user)
.post();
Previous statement creates a single transaction with both foo
and
bar
operations, adds signers from user's auth descriptor and signs it
with user's private key.
If more control is needed over signers and signing then build and sign functions could be used instead:
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 is also possible to get a raw transaction:
const rawTransaction = blockchain
.transactionBuilder()
.add(op("foo", param1, param2))
.buildAndSign(user)
.raw();
which can be sent to a blockchain node later:
await blockchain.postRaw(rawTransaction);
The nop operation
To prevent 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 we directly call ft3.transfer operation two times, the second call will fail.
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 transaction failing, nop operation can be added to a second transaction in order to make it differ 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 nop operation with a random number as argument.
GtvSerializable interface
In typescript, op
function is defined as:
function op(name: string, ...args: GtvSerializable[]): Operation {
return new Operation(name, ...args);
}
It expects arguments to implement GtvSerializable interface, i.e. to
have implemented toGTV()
function.
Array, Buffer, String and Number are already extended with toGTV function.
If user defined object wants to be passed to an operation, it has to implement GtvSerializable interface, e.g.
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 blockchain side, some_op
would have to be defined as either:
operation some_op(player: list<gtv>) {
...
}
or
struct player {
first_name: text;
last_name: text;
}
operation some_op(player) {
...
}