Skip to main content
Last updated

client

API Reference can be found here client.api.md

Package @kadena/client

@kadena/client allows JavaScript/TypeScript users to easily interact with the Kadena Blockchain.

Readme for @kadena/client v1.0.0 This is the README for @kadena/client v1.0.0 that introduces a new API
To read the README for the old API (< 0.6.1) read client_v0.6.1/packages/libs/client/README.md

Getting started

Transaction building

Interaction with the Kadena Blockchain works in various ways. In @kadena/client we expose a builder pattern and a functional pattern . They can both be used with or without the use of type-definitions, but it's recommended to use the type definitions that you can generate with the @kadena/pactjs-cli

Signing

There's also information on an integrated way of signing using Chainweaver . With @kadena/client you can also send a request to the blockchain . That's covered in this article. We'll also be exploring the concepts and rationale of @kadena/client.

Prerequisites

To use @kadena/client, Node.js v14 or higher is required. Let's install the bare minimum you need to get started:

sh
mkdir my-dapp-with-kadena-clientcd my-dapp-with-kadena-clientnpm init -ynpm install @kadena/clientnpm install --save-dev @kadena/pactjs-cli typescript ts-nodenpx tsc --init
sh
mkdir my-dapp-with-kadena-clientcd my-dapp-with-kadena-clientnpm init -ynpm install @kadena/clientnpm install --save-dev @kadena/pactjs-cli typescript ts-nodenpx tsc --init

Contract-based interaction using @kadena/client

We wanted @kadena/client to be independent so this is a tool that can be used with arbitrary contracts. That is also why you have to generate the interfaces used by @kadena/client. You can use smart contracts from the blockchain or your own local ones.

For the template based interaction we will provide a repository with templates that can be used.

Generate interfaces from the blockchain

Generate types directly from a contract on the blockchain:

sh
pactjs contract-generate --contract "coin" --api "https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact"
sh
pactjs contract-generate --contract "coin" --api "https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact"

The log shows what has happened. Inside the node_modules directory, a new package has been created: .kadena/pactjs-generated. This package is referenced by @kadena/client to give you type information.

NOTE: Make sure to add the new types to compilerOptions in tsconfig.json:

json
{  "compilerOptions": {    "types": [".kadena/pactjs-generated"]  }}
json
{  "compilerOptions": {    "types": [".kadena/pactjs-generated"]  }}

Generate interfaces locally

You can create your own smart contract or download it from the blockchain using pactjs.

Using the contract we'll now generate all the functions (defun) with their (typed) arguments and capabilities (defcap).

sh
pactjs contract-generate --file "./contracts/coin.module.pact"
sh
pactjs contract-generate --file "./contracts/coin.module.pact"

Downloading contracts from the blockchain

Let's download the contracts you want to create Typescript interfaces for:

sh
mkdir contractsnpx pactjs retrieve-contract --out "./contracts/coin.module.pact" --module "coin"
sh
mkdir contractsnpx pactjs retrieve-contract --out "./contracts/coin.module.pact" --module "coin"

There are several options to retrieve contracts from another network or chain.

Use --help to get information on retrieve-contract:

txt
> pactjs retrieve-contract --helpUsage: pactjs retrieve-contract [options]Retrieve contract from a chainweb-api in a /local call (see also: https://github.com/kadena-io/chainweb-node#configuring-running-and-monitoring-the-health-of-a-chainweb-node).Options:  -m, --module <module>    The module you want to retrieve (e.g. "coin")  -o, --out <file>         File to write the contract to  --api <url>              API to retrieve from (e.g. "https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact")  -n, --network <network>  Network to retrieve from (default "mainnet") (default: "mainnet")  -c, --chain <number>     Chain to retrieve from (default 1) (default: 1)  -h, --help               display help for command
txt
> pactjs retrieve-contract --helpUsage: pactjs retrieve-contract [options]Retrieve contract from a chainweb-api in a /local call (see also: https://github.com/kadena-io/chainweb-node#configuring-running-and-monitoring-the-health-of-a-chainweb-node).Options:  -m, --module <module>    The module you want to retrieve (e.g. "coin")  -o, --out <file>         File to write the contract to  --api <url>              API to retrieve from (e.g. "https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact")  -n, --network <network>  Network to retrieve from (default "mainnet") (default: "mainnet")  -c, --chain <number>     Chain to retrieve from (default 1) (default: 1)  -h, --help               display help for command

Building a simple transaction from the contract

Now that everything is bootstrapped, we can start building transactions.

Create a new file and name it transfer.ts (or .js):

ts
import { Pact } from '@kadena/client'; const unsignedTransaction = Pact.builder  .execution(    Pact.modules.coin.transfer('k:your-pubkey', 'k:receiver-pubkey', {      decimal: '231',    }),  )  .addSigner('your-pubkey', (withCapability) => [    // add necessary coin.GAS capability (this defines who pays the gas)    withCapability('coin.GAS'),    // add necessary coin.TRANSFER capability    withCapability('coin.TRANSFER', 'k:your-pubkey', 'k:receiver-pubkey', {      decimal: '231',    }),  ])  .setMeta({ chainId: '1', senderAccount: 'your-pubkey' })  .setNetworkId('mainnet01')  .createTransaction();
ts
import { Pact } from '@kadena/client'; const unsignedTransaction = Pact.builder  .execution(    Pact.modules.coin.transfer('k:your-pubkey', 'k:receiver-pubkey', {      decimal: '231',    }),  )  .addSigner('your-pubkey', (withCapability) => [    // add necessary coin.GAS capability (this defines who pays the gas)    withCapability('coin.GAS'),    // add necessary coin.TRANSFER capability    withCapability('coin.TRANSFER', 'k:your-pubkey', 'k:receiver-pubkey', {      decimal: '231',    }),  ])  .setMeta({ chainId: '1', senderAccount: 'your-pubkey' })  .setNetworkId('mainnet01')  .createTransaction();

Notes

  • Namespaced arguments (k:, w: etc) are account names, where non-namespaced arguments are assumed to be public keys.
  • The contract doesn't specify whether you need to pass an account name or public key. This is knowledge that can be obtained by inspecting the contract downloaded earlier or consulting the documentation for the contract.
  • The addSigner function accepts the public-key of the signer and let signer add the capabilities they want to sign for. Note that coin.GAS doesn't have any arguments, but coin.TRANSFER does.
  • The setMeta argument object has a senderAccount property. This is an account and could be gas station account in some scenarios.
  • To add an Unrestricted Signer (Unscoped Signature ), call addSigner without extra arguments

Signing

Signing can be done in various ways. Either manually, by signing the hash of the transaction or with a wallet. There's currently two options in @kadena/client to sign with a wallet:

  1. WalletConnect (preferred)
  2. Chainweaver

Manually signing the transaction

The unsignedTransaction can be pasted into the SigData of Chainweaver.

The createTransaction function will return the transaction. The hash will be calculated and the command will be serialized.

Integrated sign request to Chainweaver desktop

Using the transaction we can send a sign request to Chainweaver.

Note: This can only be done using the desktop version, not the web version, as it's exposing port 9467 .

ts
import { signWithChainweaver } from '@kadena/client'; // use the transaction, and sign it with Chainweaverconst signedTransaction = signWithChainweaver(unsignedTransaction)  .then(console.log)  .catch(console.error);
ts
import { signWithChainweaver } from '@kadena/client'; // use the transaction, and sign it with Chainweaverconst signedTransaction = signWithChainweaver(unsignedTransaction)  .then(console.log)  .catch(console.error);

To send the transaction to the blockchain, continue with Send a request to the blockchain

Signing with a WalletConnect compatible wallet

There's several steps to setup a WalletConnect connections and sign with WalletConnect.

  1. Setting up the connection using ClientContextProvider.tsx
  2. Use signWithWalletConnect to request a signature from the wallet (Transaction.tsx)[https://github.com/kadena-io/wallet-connect-example/blob/2efc34296f845aea75f37ab401a5c49081f75b47/src/components/Transaction.tsx#L104 ]

Using the commandBuilder

You may prefer to not generate JavaScript code for your contracts or use templates. In that case, you can use the commandBuilder function to build a command and submit the transaction yourself:

ts
import { Pact } from '@kadena/client'; const client = createClient(  'https://api.testnet.chainweb.com/chainweb/0.0/testnet04/chain/8/pact',); const unsignedTransaction = Pact.builder  .execution('(format "Hello {}!" [(read-msg "person")])')  // add signer(s) if its required  .addSigner('your-pubkey')  // set chain id and sender  .setMeta({    chainId: '8',    senderAccount: 'your-k-or-w-account-or-gas-station',  })  // set networkId  .setNetworkId('mainnet01')  // create transaction with hash  .createTransaction(); // Send it or local itclient.local(unsignedTransaction);client.submit(unsignedTransaction);
ts
import { Pact } from '@kadena/client'; const client = createClient(  'https://api.testnet.chainweb.com/chainweb/0.0/testnet04/chain/8/pact',); const unsignedTransaction = Pact.builder  .execution('(format "Hello {}!" [(read-msg "person")])')  // add signer(s) if its required  .addSigner('your-pubkey')  // set chain id and sender  .setMeta({    chainId: '8',    senderAccount: 'your-k-or-w-account-or-gas-station',  })  // set networkId  .setNetworkId('mainnet01')  // create transaction with hash  .createTransaction(); // Send it or local itclient.local(unsignedTransaction);client.submit(unsignedTransaction);

Using FP approach

This library uses a couple of utility functions in order to create pactCommand you can import those function from @kadena/client/fp if you need more flexibility on crating command like composing command or lazy loading.

Here are two examples to demonstrate this:

Send a request to the blockchain

The @kadena/client provides a createClient function with some utility functions. these helpers call the Pact API under the hood Pactjs API .

  • submit
  • pollStatus
  • getStatus
  • pollSpv
  • getSpv
  • local,
  • preflight
  • dirtyRead
  • signatureVerification

createClient accepts the host url or the host url generator function that handles url generating as pact is a multi chain network we need to change the url based on that.

ts
// we only want to send request to the chain 1 one the mainnetconst hostUrl = 'https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact';const client = createClient(hostUrl);// we need more flexibility to call different chains or even networks, then functions// extract networkId and chainId from the cmd part of the transaction and use the hostUrlGenerator to generate the urlconst hostUrlGenerator = ({ networkId, chainId }) =>  `https://api.chainweb.com/chainweb/0.0/${networkId}/chain/${chainId}/pact`;const { local, submit, getStatus, pollStatus, getSpv, pollSpv } =  createClient(hostUrlGenerator);
ts
// we only want to send request to the chain 1 one the mainnetconst hostUrl = 'https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact';const client = createClient(hostUrl);// we need more flexibility to call different chains or even networks, then functions// extract networkId and chainId from the cmd part of the transaction and use the hostUrlGenerator to generate the urlconst hostUrlGenerator = ({ networkId, chainId }) =>  `https://api.chainweb.com/chainweb/0.0/${networkId}/chain/${chainId}/pact`;const { local, submit, getStatus, pollStatus, getSpv, pollSpv } =  createClient(hostUrlGenerator);

Probably the simplest call you can make is describe-module, but as this is not on the coin contract, we have to trick Typescript a little:

ts
const res = await local({  payload: {    exec: {      code: Pact.modules.coin['get-balance']('albert'),    },  },});console.log(JSON.stringify(res, null, 2));
ts
const res = await local({  payload: {    exec: {      code: Pact.modules.coin['get-balance']('albert'),    },  },});console.log(JSON.stringify(res, null, 2));

A more elaborate example that includes signing, sending and polling can be found in example-contract/transfer.ts

Upgrading from @kadena/client 0.x to 1.0.0

The highlights of the difference between 0.x and 1.0.0 are:

  • the expression generation is separate from transaction building. This allows for multiple statements per transaction
  • the client is it's own separate entity
  • signing is applied on a vanilla JS Object

Here are two examples of old to new rewrites

Sending a transaction 'transfer'

Old implementation

ts
async function transaction(  sender: string,  senderPublicKey: string,  receiver: string,  amount: IPactDecimal,): Promise<void> {  const unsignedTransaction = Pact.modules.coin    .transfer(sender, receiver, amount)    .addCap('coin.GAS', senderPublicKey)    .addCap('coin.TRANSFER', senderPublicKey, sender, receiver, amount)    .setMeta({ senderAccount: sender }, 'testnet04');   const res = await signWithChainweaver(unsignedTransaction);   const sendRequests = res.map((tx) => {    console.log('sending transaction', tx.code);    return tx.send(testnetChain1ApiHost);  });   const sendResponses = await Promise.all(sendRequests);  sendResponses.map(async function (sendResponse: SendResponse): Promise<void> {    const requestKey = (await sendRequests[0]).requestKeys[0];    await pollMain(requestKey);    console.log(`Transaction '${requestKey}' finished`);  });} async function pollMain(...requestKeys: string[]): Promise<void> {  // ... some code to poll the status of the requestKeys}
ts
async function transaction(  sender: string,  senderPublicKey: string,  receiver: string,  amount: IPactDecimal,): Promise<void> {  const unsignedTransaction = Pact.modules.coin    .transfer(sender, receiver, amount)    .addCap('coin.GAS', senderPublicKey)    .addCap('coin.TRANSFER', senderPublicKey, sender, receiver, amount)    .setMeta({ senderAccount: sender }, 'testnet04');   const res = await signWithChainweaver(unsignedTransaction);   const sendRequests = res.map((tx) => {    console.log('sending transaction', tx.code);    return tx.send(testnetChain1ApiHost);  });   const sendResponses = await Promise.all(sendRequests);  sendResponses.map(async function (sendResponse: SendResponse): Promise<void> {    const requestKey = (await sendRequests[0]).requestKeys[0];    await pollMain(requestKey);    console.log(`Transaction '${requestKey}' finished`);  });} async function pollMain(...requestKeys: string[]): Promise<void> {  // ... some code to poll the status of the requestKeys}

New implementation

ts
const NETWORK_ID: string = 'testnet04'; async function transfer(  sender: string,  senderPublicKey: string,  receiver: string,  amount: IPactDecimal,): Promise<void> {  const transaction = Pact.builder    .execution(      // pact expression      Pact.modules.coin.transfer(sender, receiver, amount),    )    // add signers    .addSigner(senderPublicKey, (withCapability) => [      // add capabilities      withCapability('coin.GAS'),      withCapability('coin.TRANSFER', sender, receiver, amount),    ])    // set chainId and sender    .setMeta({ chainId: '0', senderAccount: sender })    .setNetworkId(NETWORK_ID)    // will create a IUnsignedTransaction { cmd, hash, sigs }    .createTransaction();   const signedTx = await signWithChainweaver(transaction);   // create generic client  const client = createClient(apiHostGenerator);   // check if all necessary signatures are added  if (isSignedTransaction(signedTx)) {    const transactionDescriptor = await client.submit(signedTx);    const response = await client.listen(transactionDescriptor, {});    if (response.result.status === 'failure') {      throw response.result.error;    } else {      console.log(response.result);    }  }} transfer(senderAccount, senderPublicKey, receiverAccount, {  decimal: '13.37',}).catch(console.error);
ts
const NETWORK_ID: string = 'testnet04'; async function transfer(  sender: string,  senderPublicKey: string,  receiver: string,  amount: IPactDecimal,): Promise<void> {  const transaction = Pact.builder    .execution(      // pact expression      Pact.modules.coin.transfer(sender, receiver, amount),    )    // add signers    .addSigner(senderPublicKey, (withCapability) => [      // add capabilities      withCapability('coin.GAS'),      withCapability('coin.TRANSFER', sender, receiver, amount),    ])    // set chainId and sender    .setMeta({ chainId: '0', senderAccount: sender })    .setNetworkId(NETWORK_ID)    // will create a IUnsignedTransaction { cmd, hash, sigs }    .createTransaction();   const signedTx = await signWithChainweaver(transaction);   // create generic client  const client = createClient(apiHostGenerator);   // check if all necessary signatures are added  if (isSignedTransaction(signedTx)) {    const transactionDescriptor = await client.submit(signedTx);    const response = await client.listen(transactionDescriptor, {});    if (response.result.status === 'failure') {      throw response.result.error;    } else {      console.log(response.result);    }  }} transfer(senderAccount, senderPublicKey, receiverAccount, {  decimal: '13.37',}).catch(console.error);

Read from the blockchain 'getBalance'

Old implementation

ts
async function getBalance(account: string): Promise<void> {  // generation of transaction and expression as one, and the client is part of the transaction  const res = await Pact.modules.coin['get-balance'](account).local(    'http://host.com/chain/0/pact',  );  console.log(res);} const myAccount: string =  'k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94'; getBalance(myAccount).catch(console.error);
ts
async function getBalance(account: string): Promise<void> {  // generation of transaction and expression as one, and the client is part of the transaction  const res = await Pact.modules.coin['get-balance'](account).local(    'http://host.com/chain/0/pact',  );  console.log(res);} const myAccount: string =  'k:554754f48b16df24b552f6832dda090642ed9658559fef9f3ee1bb4637ea7c94'; getBalance(myAccount).catch(console.error);

New implementation:

ts
async function getBalance(account: string): Promise<void> {  // `Pact.builder.execution` accepts a number of `Pact.modules.<module>.<fun>` calls  const transaction = Pact.builder    .execution(Pact.modules.coin['get-balance'](account))    .setMeta({ chainId: '1' })    .createTransaction();   // client creation is separate from the transaction builder  const staticClient = createClient('http://host.com/chain/0/pact');  const genericClient = createClient(    ({ networkId, chainId }) =>      `http://${networkId}.host.com/chain/${chainId}/pact`,  );   const res = await staticClient.local(transaction, {    preflight: false,    signatureVerification: false,  });   console.log(res);} getBalance(account).catch(console.error);
ts
async function getBalance(account: string): Promise<void> {  // `Pact.builder.execution` accepts a number of `Pact.modules.<module>.<fun>` calls  const transaction = Pact.builder    .execution(Pact.modules.coin['get-balance'](account))    .setMeta({ chainId: '1' })    .createTransaction();   // client creation is separate from the transaction builder  const staticClient = createClient('http://host.com/chain/0/pact');  const genericClient = createClient(    ({ networkId, chainId }) =>      `http://${networkId}.host.com/chain/${chainId}/pact`,  );   const res = await staticClient.local(transaction, {    preflight: false,    signatureVerification: false,  });   console.log(res);} getBalance(account).catch(console.error);

Further development

The @kadena/client is still in an early phase. Next steps will include to see what the community thinks of this approach. We'd love to hear your feedback and use cases, especially if the current @kadena/client and @kadena/pactjs-cli isn't sufficient.

Contact the team

We are available via Discord and Github issues: