JSPM

  • Created
  • Published
  • Downloads 644
  • Score
    100M100P100Q105117F
  • License BSD-2-Clause-FreeBSD

Client library for accessing a Postchain node through REST.

Package Exports

  • postchain-client

Readme

Postchain client

Example:

let crypto = require("crypto");
let secp256k1 = require("secp256k1");
let { encryption } = require("postchain-client")

// Create some dummy keys
let signerPrivKeyA = Buffer.alloc(32, "a");
let signerPubKeyA = secp256k1.publicKeyCreate(signerPrivKeyA);
let signerPrivKeyB = Buffer.alloc(32, "b");
let signerPubKeyB = secp256k1.publicKeyCreate(signerPrivKeyB);

// Create a simple signature provider.
// This is the standard way of signing a message using the gtxClient,
// and it can be used with the restClient too. Look further below for
// more details on how to use it.
let signatureProviderA = newSignatureProvider({privKey: signerPrivKeyA})

// The lower-level client that can be used for any
// postchain client messages. It only handles binary data.
let restClient = require("postchain-client").restClient;

// The higher-level client that is used for generic transactions, GTX.
// This utilizes the GTX format described below to pass function calls
// from the client to the postchain backend implementation.
let gtxClient = require("postchain-client").gtxClient;

// Each blockchain has a blockchainRID, that identifies the blockchain
// we want to work with. This blockchainRID must match the blockchain RID
// encoded into the first block of the blockchain. How the blockchainRID
// is constructed is up to the creator of the blockchain. In this example
// we use the linux command:
// echo "A blockchain example"| sha256sum
let blockchainRID = "7d565d92fd15bd1cdac2dc276cbcbc5581349d05a9e94ba919e1155ef4daf8f9";

// Create an instance of the rest client and configure it for a specific set of
// base urls and a blockchinRID. You may set an optional pool size for the connection pool,
// default pool size is 10. Applications that do hundreds of requests
// per second may consider setting this a bit higher, for example 100.
// It *may* speed things up a bit. You may also set an opitional
// poolinginterval in milliseconds, default is set to 500, a fail over
// config wich include attemps per endpoint and attempt interval, default is 3 and 500.
let rest = restClient.createRestClient([`http://localhost:7741`], blockchainRID, 5, 1000);

// Create an instance of the higher-level gtx client. It will
// use the rest client instance and it will allow calls to functions
// fun1 and fun2. The backend implementation in Postchain must
// provide those functions.
let gtx = gtxClient.createClient(rest, blockchainRID, ["fun1", "fun2"]);

// Start a new request. A request instance is created.
// The public keys are the keys that must sign the request
// before sending it to postchain. Can be empty.
let req = gtx.newTransaction([signatureProviderA.pubKey, signerPubKeyB]);

// call fun1 with three arguments: a string, an array and a Buffer
req.fun1("arg1", ["arg2", [1, 2]], Buffer.from("hello"));
// call the same function with only one argument
req.fun1("arg1");
// call fun2
req.fun2(1, 2);

// Signing can be done either through the sign() function ...
// The signatureProvider method sign() will be called, and
// it's expected to return the signature of the digest it's given
// as input (of type Buffer). It needs to return the output of
// secp256k1.ecdsaSign(...params).signature
await req.sign(signatureProviderA);
// You can also use this, if you don't need a signature provider
// The second parameter is optional, it will be generated if not given
//await req.sign(signerPrivKeyA, signerPubKeyA);

// ... or by the addSignature() function
let digestToSign = req.getDigestToSign();
// Sign the buffer externally.
let signatureFromB = askUserBToSign(digestToSign);
// and add the signature to the request
req.addSignature(signerPubKeyB, signatureFromB);

// Finally send the request and supply an error callback
req.send((error) => {
  if (error) {
    console.log(error);
  }
});

// Now we can query Postchain. The backend must have a method
// query method named "findStuff" (readOnlyConn, queryObject) that can
// understand the query object and typically perform a search using
// the database connection readOnlyConn. The backend query function
// can return any serializable result object you chose
gtx.query("findStuff", { text: "arg1" });

// This will make a request with a single operation
// and a single signature.
req = gtx.newTransaction(blockchainRID, [signatureProviderA.pubkey]);
req.fun1("arg1");
await req.sign(signatureProviderA);
req.send((error) => {
  if (!error) {
    done();
  }
});

function sha256(buffer) {
  return crypto.createHash("sha256").update(buffer).digest();
}

// This is to demonstrate that you can use external signing
// mechanisms. It could be a complex function, requiring you
// to sign from your phone, another device, or something else again
function askUserBToSign(buffer) {
  // The signed digest is a double sha-256
  var digest = sha256(sha256(buffer));
  return secp256k1.sign(digest, signerPrivKeyB).signature;
}

// The complex signature process can, however, even be implemented in
// a signatureProvider. Once you have a callback like the one above,
// it's a simple matter of making a signature provider:
let signatureProviderB {
  pubKey: signerPubKeyB,
  sign: askUserBToSign
}

A very simple backend for the above client might look like this:

module.exports.createSchema = async function (conn) {
  console.log("Creating schema in backend");
  await conn.query(
    "CREATE TABLE IF NOT EXISTS example " +
      "(id SERIAL PRIMARY KEY, stuff TEXT NOT NULL)"
  );
};

// Example backend implementation that doesn't do anything but log the function calls
module.exports.backendFunctions = {
  fun1: async function (
    conn,
    tx_iid,
    call_index,
    signers,
    stringArg,
    arrayArg,
    bufferArg
  ) {
    console.log("fun1 called in backend");
  },
  fun2: async function (conn, tx_iid, call_index, signers, intArg1, intArg2) {
    console.log("fun2 called in backend");
  },
};

module.exports.backendQueries = {
  findStuff: async function (readOnlyConn, queryObject) {
    console.log("Search for " + queryObject.text);
    if (queryObject.text === "giveMeHits") {
      return { hits: 4 };
    }
    return { hits: 0 };
  },
};

GTX architecture

Generic transactions were developed to make it easier to make user implementations of Postchain. The user doesn't have to invent a binary format for it's transactions. With GTX you specify a set of functions that you will call from the client, and the GTX client will serialize the function calls, sign them and send to Postchain.

User
 |
 | req.fun1('arg1', 'arg2');
 | req.fun2('arg1'); req.sign(privKeyA); req.send(err => {})
 v
GtxClient
 |
 | <Buffer with serialized message>
 v
RestClient
 |
 | 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

The first four arguments to backend.fun1 are

  • conn is a database connection that the backend function can use to query/update the database
  • tx_iid is the primary key of this postchain transaction.
  • call_index, 0 in this example. It's the index within the GTX of the current call
  • signers, all signers of this GTX. The signatures from these signers are already verified by the GTX framework when the backend function is called.