npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

postchain-client

v1.20.1

Published

Client library for accessing a Postchain node through REST.

Downloads

3,715

Readme

Postchain Client

Postchain Client is a set of predefined functions and utilities offering a convenient and simplified interface for interacting with a decentralized application (dapp) built using the Postchain blockchain framework, also known as Chromia.

Usage

The Postchain Client is compatible with both JavaScript and TypeScript. You can install the library from npm via https://www.npmjs.com/package/postchain-client.

Initializing the Client

Firstly, import the required libraries.

import crypto from "crypto-browserify";
import secp256k1 from "secp256k1";
import {
  encryption,
  createClient,
  newSignatureProvider,
} from "postchain-client";

Then, create some dummy keys.

const signerPrivKeyA = Buffer.alloc(32, "a");
const signerPubKeyA = secp256k1.publicKeyCreate(signerPrivKeyA);
const signerPrivKeyB = Buffer.alloc(32, "b");
const signerPubKeyB = secp256k1.publicKeyCreate(signerPrivKeyB);

Each blockchain has a Blockchain RID (blockchainRID) that identifies the specific blockchain we wish to interact with. This blockchainRID should match the Blockchain RID encoded into the first block of the blockchain. How the blockchainRID is structured depends on the blockchain's creator. In this example, we use the Linux command: echo "A blockchain example"| sha256sum.

const blockchainRid =
  "7d565d92fd15bd1cdac2dc276cbcbc5581349d05a9e94ba919e1155ef4daf8f9";

Create a Chromia client instance and configures it according to your needs.

Parameters

  • settings (Object): A set of network settings to customize the behavior of the Chromia client.
    • nodeUrlPool (Optional): An array of URLs representing the nodes the client will send requests to. Use this if you know the specific nodes that will handle the client requests. These nodes can either be local nodes or belong to the same cluster as the targeted blockchain.
    • directoryNodeUrlPool (Optional): An array of URLs representing nodes in the system cluster, where the directory chain is located. The client will automatically discover every node running the targeted application by querying the directory chain. This can be useful when the client needs to automatically adapt to updates to the nodes within the cluster where the blockchain is located."
    • blockchainRid (Optional): Resource Identifier (Rid) of the targeted blockchain. This is a unique identifier for the specific blockchain.
    • blockchainIid (Optional): Instance Identifier (Iid) of the targeted blockchain. The directory chain always has Iid 0.
    • statusPollInterval (Optional): Interval (in milliseconds) at which the client will poll the status after posting a transaction.
    • statusPollCount (Optional): Number of consecutive successful status polls before it should stop asking for the status. Defaults to 1.
    • failOverConfig (Optional): Configuration for failover behavior in case of node failures.
      • strategy (Optional): Failover strategy to use. Defaults to a strategy called Abort On Error.
      • attemptsPerEndpoint (Optional): Number of consecutive failed attempts allowed for each endpoint before considering it as unreachable. Defaults to 3.
      • attemptInterval (Optional): Interval (in milliseconds) between consecutive retry attempts during failover. Defaults to 5000 ms.
      • unreachableDuration (Optional): Duration (in milliseconds) that an endpoint should remain unreachable before reattempting. Defaults to 30000 ms.
    • useStickyNode(Optional): A boolean that will make sure that on succefull requests to a node, the client will continue using this node unless it starts failing.

Returns

A promise that resolves to the configured Chromia client instance.

Example:

  1. Client configured with a known node URL:
const chromiaClient = await createClient({
  nodeUrlPool: "http://localhost:7740",
  blockchainRid,
});
  1. Client configured for node discovery with an array of URLs representing nodes in the system cluster.
const chromiaClient = await createClient({
  directoryNodeUrlPool: ["url1", "url2", "url3", "etc."],
  blockchainRid,
});

Use sticky node

What is a "sticky node"?

A sticky node is a node that will will continue to be used for requests as long as the requests to it are successful, this means that if the client is making a request to get the block height of a dapp, and it successful, then following requests to get other data eg: dapp transactions will be using the same node, meaning that is "sticks" with the user once selected.

How does it work?

The client will need to be initialized with a directoryNodeUrlPool and have the property useStickyNode set to true in the settings parameter for it to be enabled. example:

const client = createClient({
  useStickyNode: true,
  directoryNodeUrlPool: ["http://localhost:7740"],
  ...restSettings,
});

This will then create internally create a nodeManager, that handles and keeps track of which nodes are available and which one is currently set to be the "sticky node".

As a user, when you first make a request with this feature enabled, you will not have any "sticky node" set. Instead whenever you make a request, the client will choose a random node out of the ones available. Should the request to the node be successful, then that node will be set as the "sticky node". This will happen regardless of what failoverStrategy has been set.

Any subsequent requests after the first successful one will continue to use "sticky node" as long as it continues to give successful requests. Should the node fail however, then it will be set at unavailable for the duration configured in the client settings (or the default time of 30000ms) and following requests will once again try to use a random available node and if successful set that one as the new "sticky node".

Setting how long a node should be unavailable

The duration that a node is configured as unavailable can be se in the failoverConfig of the client settings:

  const client = createClient({
  useStickyNode: true,
  directoryNodeUrlPool: ["http://localhost:7740"],
  failOverConfig: {
  startegy: "abortOnErrror",
  attemptsPerEndpoint: 4,
  attemptInterval: 3000,
  unreachableDuration: 50000,
  }
  ...restSettings
  })

Failover strategies

When initializing a client, you have the option to configure the failover strategy for the client. Additionally, you can modify certain parameters within the failover configuration, such as the number of attempts per endpoint and the interval between attempts.

The Postchain client offers three failover strategies:

Abort On Error

The request strategy will abort on client error and retry on server error. This means that if a client error occurs, such as an invalid query parameter, the request strategy will not retry the query. However, if a server error occurs, such as a timeout or internal server error, the request strategy will retry the query on another node.

Try Next On Error

The Try Next On Error request strategy is similar to Abort On Error, but will also retry on client error. This means that if a client error occurs, the request strategy will retry the query on another node, as well as retrying on server error.

Single Endpoint

The Single Endpoint request strategy will not retry on another node.

Query Majority

The Query Majority Request Strategy will query all nodes in parallel and wait until an EBFT majority of the nodes return the same response. This can help to ensure the integrity of the system by requiring a consensus among nodes before accepting a result.

Queries

Query Option 1

Use the query function to send a query to a dapp written in Rell. The function takes the query's name and an object of query arguments.

chromiaClient.query("get_foobar", {
  foo: 1,
  bar: 2,
});

Query Option 2

Alternatively, the query function can take an object with a name property and an args property.

chromiaClient.query({
  name: "get_foobar",
  args: {
    foo: 1,
    bar: 2,
  },
});

Typed Query

You can specify argument and return types for a given query in TypeScript.

type ArgumentsType = {
  foo: number;
  bar: number;
};

type ReturnType = {
  foobar: string;
};

const result = await chromiaClient.query<ReturnType, ArgumentsType>(
  "get_foobar",
  {
    foo: 1,
    bar: 2,
  }
);

Typed query 2

Alternatively, you can specify the types in a QueryObject to achieve type safety

type ReturnType = {
  foobar: string;
};

const myQuery: QueryObject<ReturnType> = {
  name: "get_fobar",
  args: { foo: "bar" },
};
const result = await chromiaClient.query(myQuery); // result has type ReturnType

Transactions

To send transactions, begin by creating a simple signature provider. The signature provider is used to sign transactions. More details on usage are provided further below.

const signatureProviderA = newSignatureProvider({ privKey: signerPrivKeyA });

Simple Transaction

The signAndSendUniqueTransaction function streamlines the process of sending a transaction in three steps. It adds a "nop" (no operation) with a random number that ensures the transaction is unique, signs it with a signature provider or private key, and sends it. The function generates a receipt that includes a status code, status, and tansactionRid. The status code indicates whether the server successfully processed the transaction. The status represents the current stage of the transaction on the blockchain, which can be one of the following: Waiting, Rejected, Confirmed, or Unknown.

const { status, statusCode, transactionRid } =
  await chromiaClient.signAndSendUniqueTransaction(
    {
      operations: [
        {
          name: "my_operation",
          args: ["arg1", "arg2"],
        },
      ],
      signers: [signatureProviderA.pubKey],
    },
    signatureProviderA
  );

It is also possible to pass a single operation.

const { status, statusCode, transactionRID } =
  await chromiaClient.signAndSendUniqueTransaction(
    {
      name: "my_operation",
      args: ["arg1", "arg2"],
    },
    signatureProviderA
  );

Signing a Transaction

Signs a transaction using the provided signing method. This can be a SignatureProvider or a key pair. A signature provider must contain a public key and a sign function that returns the signature of a digest transaction.

const signedTx = await chromiaClient.signTransaction(
  {
    operations: [
      {
        name: "my_operation",
        args: ["arg1"],
      },
    ],
    signers: [signatureProviderA.pubKey],
  },
  signatureProviderA
);

Sending an Unsigned Transaction

const receipt = await chromiaClient.sendTransaction({
  name: "my_operation",
  args: ["arg1", "arg2"],
});

Sending a Signed Transaction

chromiaClient.sendTransaction(signedTx);

Sending a Signed Transaction (with status polling enabled)

chromiaClient.sendTransaction(signedTx, true);

Advanced Transaction

Create a transaction object.

const tx = {
  operations: [
    {
      name: "my_operation_1",
      args: ["arg1", "arg2"],
    },
    {
      name: "my_operation_2",
      args: ["arg1", "arg2"],
    },
  ],
  signers: ["signer1", "signer2"],
};

You can modify the object to add operations or signers.

tx.operations.push({
  name: "my_operation_3",
  args: ["arg1", "arg2"],
});

tx.signers.push("signer3");

A nop can be added to make the transaction unique. It can be added manually to the transaction object or by using the addNop function.

const uniqueTx = chromiaClient.addNop(tx);

Sign and send the transaction.

const signedTx = await chromiaClient.signTransaction(
  uniqueTx,
  signatureProviderA
);

const receipt = await chromiaClient.sendTransaction(signedTx);

PromiEvent

When using functions that involve sending a transaction, you have the option to either wait for a promise or act on an event. The return value in this case is a "PromiEvent," which combines the functionalities of both a "Promise" and an "Event." This combination allows you to handle asynchronous operations. You can treat it as a Promise by utilizing the .then() and .catch() methods to handle the result or any potential errors. Moreover, it emits an event when a transaction is sent, providing you with the ability to listen for the event and execute custom logic based on your specific needs.

chromiaClient
  .sendTransaction({
    name: "my_operation",
    args: ["arg1", "arg2"],
  })
  .on("sent", (receipt: TransactionReceipt) => {
    console.log("The transaction is sent");
  });

External Signing Example

This example demonstrates that you can use external signing mechanisms. It could involve a complex function requiring you to sign from your phone, another device, or a different method.

function askUserBToSign(rawGtxBody) {
  const digest = getDigestToSignFromRawGtxBody(rawGtxBody);
  return Buffer.from(secp256k1.ecdsaSign(digest, signerPrivKeyB).signature);
}

This complex signature process can be implemented in a SignatureProvider. Once you have a callback like the one above, creating a signature provider is straightforward:

const signatureProviderB = {
  pubKey: signerPubKeyB,
  sign: askUserBToSign,
};

ICCF

Creates an ICCF (Inter-Chain Communication Framework) proof transaction. This function generates a proof that a specific transaction has occurred on the source blockchain. The function returns a transaction object with an operation called iccf_proof and the operation that should be accompanied by the proof should be added to this transaction object. The transaction can then be signed and posted to the target blockchain.

const managementBlockchainRid = "7d565d92fd15bd1cdac2dc276cbcbc5581349d05a9e94ba919e1155ef4daf8f9";

const chromiaClient = await createClient({
  nodeUrlPool: "<url-node-running-managementchain>",
  managementBlockchainRid,
});

const txToProveRid: Buffer = <txRid>;
const txToProveHash: Buffer = <txHash>;
const txToProveSigners: Pubkey[] = [<signer1>, <signer2>];
const sourceBlockchainRid: string = "<sourceBlockchainRid>";
const targetBlockchainRid: string = "<targetBlockchainRid>";

const { iccfTx, verifiedTx } = createIccfProofTx(chromiaClient, txToProveRID,txToProveHash,txToProveSigners, sourceBlockchainRid, targetBlockchainRid);

iccfTx is a transaction object with an operation called iccf_proof with argument containing the composed proof. To this transaction object you can now add the operation that will need the proof. Finally, the transaction object is ready to be signed and sent.

If necessary, it is possible to solely verify whether a specific transaction has been included in the anchoring blockchain:

isBlockAnchored(sourceClient, anchoringClient, txRid);

To create an anchoring client there is an utility function:

const anchoringClient = getAnchoringClient();

Architecture

In the Postchain client, Generic Transactions (GTX) are used to simplify user implementations of Postchain. Users do not need to invent a binary format for their transactions. The client will serialize the function calls, sign them, and send them to Postchain. Read more about GTX in the docs.

User
 |
 | chromiaClient.sendTransaction()
 |
 v
 |
 | <Buffer with serialized message>
 |
 v
 |
 | POST http://localhost:7741/tx {tx: 'hex-encoded message'}
 |
 v
RestApi
 |
 | <Buffer with serialized message>
 |
 v
Postchain
 |
 | backend.fun1(conn, tx_iid, 0, [pubKeyA], 'arg1', 'arg2');
 | backend.fun2(conn, tx_iid, 1, [pubKeyA], 'arg1');
 |
 v
Backend

Contributing to the Project

Run tests

Unit tests:

npm run test:unit

Integration tests:

  1. Make sure a postgres database is running. Read more here.

  2. Start blockchain

    cd resources/testDapp

    chr node start --wipe

  3. Run tests

    npm run test:integration

Release process

Guide regarding realease process is here