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
.
- @kadena/client
- Getting started
- Transaction building
- Signing
- Prerequisites
- Contract-based interaction using @kadena/client
- Building a simple transaction from the contract
- Signing
- Verifier
- Using the commandBuilder
- Using FP approach
- Send a request to the blockchain
- Upgrading from @kadena/client 0.x to 1.0.0
- Further development
- Contact the team
Prerequisites
To use @kadena/client
, Node.js v14 or higher is required. Let's install the
bare minimum you need to get started:
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
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:
pactjs contract-generate --contract "coin" --api "https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact"
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.
Now you can use this by creating a transaction that calls a smart contract function .
NOTE: Make sure to add the new types
to compilerOptions
in
tsconfig.json
:
{ "compilerOptions": { "types": [".kadena/pactjs-generated"] }}
{ "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
).
pactjs contract-generate --file "./contracts/coin.module.pact"
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:
mkdir contractsnpx pactjs retrieve-contract --out "./contracts/coin.module.pact" --module "coin"
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
:
> 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
> 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
Take a look at https://github.com/kadena-community/kadena.js/blob/main/packages/libs/client-examples/src/example-contract/simple-transfer.ts for a complete example.
Now that everything is bootstrapped, we can start building transactions.
Create a new file and name it transfer.ts
(or .js
):
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();
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 thepublic-key
of the signer and let signer add the capabilities they want to sign for. Note thatcoin.GAS
doesn't have any arguments, butcoin.TRANSFER
does. - The
setMeta
argument object has asenderAccount
property. This is anaccount
and could begas 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:
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 .
import { signWithChainweaver } from '@kadena/client'; // use the transaction, and sign it with Chainweaverconst signedTransaction = signWithChainweaver(unsignedTransaction) .then(console.log) .catch(console.error);
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.
- Setting up the connection using
ClientContextProvider.tsx
- 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 ]
Verifier
Kadena supports verifier
as another way of gaining authority in Pact, as well
as the normal signature flow. In this way, a verifier
, proof
, and the
capability list
will be sent to the blockchain, and if the proof satisfies the
verifier function, the capabilities will be granted. It's useful, for example,
for ZK (Zero Knowledge Proof) or bridging between networks.
Add Verifier To The Transaction
import { Pact } from '@kadena/client'; const transaction = Pact.builder .execution( Pact.modules.coin.transfer('sender-account', 'receiver-account', { decimal: '231', }), ) .addVerifier({ name: 'ZK', proof: 'zk-proof-data' }, (withCapability) => [ // add necessary coin.GAS capability (this defines who pays the gas) withCapability('coin.GAS'), // add necessary coin.TRANSFER capability withCapability('coin.TRANSFER', 'sender-account', 'receiver-account', { decimal: '231', }), ]) .setMeta({ chainId: '1', senderAccount: 'sender-account' }) .setNetworkId('mainnet01') .createTransaction();
import { Pact } from '@kadena/client'; const transaction = Pact.builder .execution( Pact.modules.coin.transfer('sender-account', 'receiver-account', { decimal: '231', }), ) .addVerifier({ name: 'ZK', proof: 'zk-proof-data' }, (withCapability) => [ // add necessary coin.GAS capability (this defines who pays the gas) withCapability('coin.GAS'), // add necessary coin.TRANSFER capability withCapability('coin.TRANSFER', 'sender-account', 'receiver-account', { decimal: '231', }), ]) .setMeta({ chainId: '1', senderAccount: 'sender-account' }) .setNetworkId('mainnet01') .createTransaction();
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:
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);
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.
// 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);
// 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:
Also see example-contract/get-balance.ts .
const res = await local({ payload: { exec: { code: Pact.modules.coin['get-balance']('albert'), }, },});console.log(JSON.stringify(res, null, 2));
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
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}
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
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);
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
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);
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:
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);
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:
- Github Issues
- Discord in the #kadena-js channel