client examples
Preparation
In the following examples, we will interact with the coin
contract. To have
better type support, we strongly recommend generating the type definition from
the contract using @kadena/pactjs-cli
.
To install @kadena/pactjs-cli
as a dev dependency for your project, run the
following command in the terminal:
npm install @kadena/pactjs-cli --save-dev
npm install @kadena/pactjs-cli --save-dev
You can generate type definitions from either a local file or directly from the chain.
Creating a type definition from a contract deployed on the chain
npx pactjs contract-generate --contract="coin" --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact
npx pactjs contract-generate --contract="coin" --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact
Creating a type definition from a pact file
npx pactjs contract-generate --file=./coin.pact --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact
npx pactjs contract-generate --file=./coin.pact --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact
if your contract has dependency to other modules you should either pass those
modules with --file
or if they are already deployed on the chain you can use
--api
to let the script fetch them from the chain. So for the coin example you
can alternatively use the following command if you have all of the files
locally.
npx pactjs contract-generate --file=./coin.pact --file=./fungible-v2.pact --file=./fungible-xchain-v1.pact
npx pactjs contract-generate --file=./coin.pact --file=./fungible-v2.pact --file=./fungible-xchain-v1.pact
Note: You can use --file
and --contract
several times, and even together.
Tip: Remember to persist the generated types by adding the command as a npm scripts.
Transfer KDA
In this example, we will use @kadena/client
to transfer 1
KDA from bob
to
alice
.
This example demonstrates how to use Pact.builder
, Pact.modules
,
signWithChainweaver
, and getClient
utilities.
import { createClient, ICommand, isSignedTransaction, Pact, signWithChainweaver,} from '@kadena/client'; interface IAccount { // In KDA, the account name is not the same as the public key. The reason is that the account could be multi-signature, and you can choose a user-friendly name for yourself. accountName: string; // We need this for signing the transaction. publicKey: string;} // The reason you need to call createClient for accessing helpers like submit, listen, and etc is// We don't want to force everybody to send transactions to kadena.io nodes. and let the developer decide// about the node they want to send the Tx// you can configure createClient with a hostUrlGenerator function that returns the base url// based on networkId, chainId. otherwise it uses kadena.io urls.// in a real application you can configure this once in a module and then export the helpers you needconst { submit, listen } = createClient(); async function transfer( sender: IAccount, receiver: IAccount, amount: string,): Promise<void> { // The first step for interacting with the blockchain is creating a request object. // We call this object a transaction object. `Pact.builder` helps you to create this object easily. const transaction = Pact.builder // There are two types of commands in pact: execution and continuation. Most of the typical use cases only use execution. .execution( // This function uses the type definition we generated in the previous step and returns the pact code as a string. // The following code means you want to call the transfer function of the coin module (contract). // You can open the coin.d.ts file and see all of the available functions. Pact.modules.coin.transfer( sender.accountName, receiver.accountName, // As we know JS rounds float numbers, which is not a desirable behavior when you are working with money. // So instead, we send the amount as a string in this format. // alternatively you can use PactNumber class from "@kadena/pactjs" that creates the same object { decimal: amount, }, ), ) // The sender needs to sign the command; otherwise, the blockchain node will refuse to do the transaction. // there is the concept of capabilities in Pact, we will explain it in the more in-depth part. // if you are using TypeScript this function comes with types of the available capabilities based on the execution part. .addSigner(sender.publicKey, (withCapability) => [ // The sender scopes their signature only to "coin.TRANSFER" and "coin.GAS" with the specific arguments. // The sender mentions they want to pay the gas fee by adding the "coin.GAS" capability. withCapability('coin.GAS'), // coin.TRANSFER capability has some arguments that lets users mention the sender, receiver and the maximum // amount they want to transfer withCapability( 'coin.TRANSFER', sender.accountName, receiver.accountName, { decimal: amount, }, ), ]) // Since Kadena has a multi-chain architecture, we need to set the chainId. // We also need to mention who is going to pay the gas fee. .setMeta({ chainId: '0', senderAccount: sender.accountName }) // We set the networkId to "testnet04"; this could also be "mainnet01" or something else if you use a private network or a fork. .setNetworkId('testnet04') // Finalize the command and add default values and hash to it. After this step, no one can change the command. .createTransaction(); // The transaction now has three properties: // - cmd: stringified version of the command // - hash: the hash of the cmd field // - sigs: an array that has the same length as signers in the command but all filled by undefined // Now you need to sign the transaction; you can do it in a way that suits you. // We exported some helpers like `signWithChainweaver` and `signWithWalletConnect`, but you can also use other wallets. // By the end, the signer function should fill the sigs array and return the signed transaction. const { sigs } = (await signWithChainweaver(transaction)) as ICommand; // signWithChainweaver already returns a signedTx and its completely safe to use it, but I rather extracted the sigs part and regenerated the signedTx again, its double security, if you are using a wallet that are not completely sure about it's implementation, its better to do tha same technique. const signedTx: ICommand = { ...transaction, sigs }; // As the signer function could be an external function, we double-check if the transaction is signed correctly. if (!isSignedTransaction(signedTx)) { throw new Error('TX_IS_NOT_SIGNED'); } // Now it's time to submit the transaction; this function returns the requestDescriptor {requestKey, networkId, chainId}. // by storing this object in a permanent storage you always can fetch the result of the transaction from the blockchain const requestDescriptor = await submit(signedTx); // We listen for the result of the request. const response = await listen(requestDescriptor); // Now we need to check the status. if (response.result.status === 'failure') { throw response.result.error; } // Congratulations! You have successfully submitted a transfer transaction. console.log(response.result);} // Calling the function with proper inputtransfer( { accountName: 'bob', publicKey: 'bob_public_key' }, { accountName: 'alice', publicKey: 'alice_public_key' }, '1',).catch(console.error);
import { createClient, ICommand, isSignedTransaction, Pact, signWithChainweaver,} from '@kadena/client'; interface IAccount { // In KDA, the account name is not the same as the public key. The reason is that the account could be multi-signature, and you can choose a user-friendly name for yourself. accountName: string; // We need this for signing the transaction. publicKey: string;} // The reason you need to call createClient for accessing helpers like submit, listen, and etc is// We don't want to force everybody to send transactions to kadena.io nodes. and let the developer decide// about the node they want to send the Tx// you can configure createClient with a hostUrlGenerator function that returns the base url// based on networkId, chainId. otherwise it uses kadena.io urls.// in a real application you can configure this once in a module and then export the helpers you needconst { submit, listen } = createClient(); async function transfer( sender: IAccount, receiver: IAccount, amount: string,): Promise<void> { // The first step for interacting with the blockchain is creating a request object. // We call this object a transaction object. `Pact.builder` helps you to create this object easily. const transaction = Pact.builder // There are two types of commands in pact: execution and continuation. Most of the typical use cases only use execution. .execution( // This function uses the type definition we generated in the previous step and returns the pact code as a string. // The following code means you want to call the transfer function of the coin module (contract). // You can open the coin.d.ts file and see all of the available functions. Pact.modules.coin.transfer( sender.accountName, receiver.accountName, // As we know JS rounds float numbers, which is not a desirable behavior when you are working with money. // So instead, we send the amount as a string in this format. // alternatively you can use PactNumber class from "@kadena/pactjs" that creates the same object { decimal: amount, }, ), ) // The sender needs to sign the command; otherwise, the blockchain node will refuse to do the transaction. // there is the concept of capabilities in Pact, we will explain it in the more in-depth part. // if you are using TypeScript this function comes with types of the available capabilities based on the execution part. .addSigner(sender.publicKey, (withCapability) => [ // The sender scopes their signature only to "coin.TRANSFER" and "coin.GAS" with the specific arguments. // The sender mentions they want to pay the gas fee by adding the "coin.GAS" capability. withCapability('coin.GAS'), // coin.TRANSFER capability has some arguments that lets users mention the sender, receiver and the maximum // amount they want to transfer withCapability( 'coin.TRANSFER', sender.accountName, receiver.accountName, { decimal: amount, }, ), ]) // Since Kadena has a multi-chain architecture, we need to set the chainId. // We also need to mention who is going to pay the gas fee. .setMeta({ chainId: '0', senderAccount: sender.accountName }) // We set the networkId to "testnet04"; this could also be "mainnet01" or something else if you use a private network or a fork. .setNetworkId('testnet04') // Finalize the command and add default values and hash to it. After this step, no one can change the command. .createTransaction(); // The transaction now has three properties: // - cmd: stringified version of the command // - hash: the hash of the cmd field // - sigs: an array that has the same length as signers in the command but all filled by undefined // Now you need to sign the transaction; you can do it in a way that suits you. // We exported some helpers like `signWithChainweaver` and `signWithWalletConnect`, but you can also use other wallets. // By the end, the signer function should fill the sigs array and return the signed transaction. const { sigs } = (await signWithChainweaver(transaction)) as ICommand; // signWithChainweaver already returns a signedTx and its completely safe to use it, but I rather extracted the sigs part and regenerated the signedTx again, its double security, if you are using a wallet that are not completely sure about it's implementation, its better to do tha same technique. const signedTx: ICommand = { ...transaction, sigs }; // As the signer function could be an external function, we double-check if the transaction is signed correctly. if (!isSignedTransaction(signedTx)) { throw new Error('TX_IS_NOT_SIGNED'); } // Now it's time to submit the transaction; this function returns the requestDescriptor {requestKey, networkId, chainId}. // by storing this object in a permanent storage you always can fetch the result of the transaction from the blockchain const requestDescriptor = await submit(signedTx); // We listen for the result of the request. const response = await listen(requestDescriptor); // Now we need to check the status. if (response.result.status === 'failure') { throw response.result.error; } // Congratulations! You have successfully submitted a transfer transaction. console.log(response.result);} // Calling the function with proper inputtransfer( { accountName: 'bob', publicKey: 'bob_public_key' }, { accountName: 'alice', publicKey: 'alice_public_key' }, '1',).catch(console.error);
More in-depth
What is a capability?
A capability is the security model of Pact - the Kadena smart contract language. It is used widely in Pact contracts for security reasons, but from the user's perspective, it allows users to scope their signatures. For example, you can say "I signed this contract only for paying gas," or "I want a transfer to happen but only to a specific account and with a specific maximum amount."
Why do we add coin.TRANSFER
via withCapability
once we already added a
similar thing via Pact.modules.coin.transfer
?
Pact is a modular language, which means other contracts can import the coin
contract and call the transfer function. To prevent unauthorized transfers, the
coin contract needs to guard against this and ensure that the user is aware of
the transfer happening. withCapability
is the place for that. So you tell Pact
that you expect a transfer to happen during the execution of this transaction.
why do we set senderAccount?
In Kadane, the account paying the gas fee might not be the same as others involved in the transaction. By choosing the senderAccount, you're telling the system which account should cover the gas cost. In a regular transaction, the owner of this account must also sign the transaction.
Do we always need to add coin.GAS
capability, Isn't this redundant while we
set the senderAccount in the metadata?
When working with capabilities, remember to clearly define the scope of your
signature. Once you include one capability, you need to list all of them. So, if
you add coin.TRANSFER
to your scope and you're also the senderAccount, you
must add coin.GAS
too. And even if you're not adding any other capabilities,
it's a good idea to include coin.GAS
. This helps ensure that you control what
happens during the transaction.
Extra information
In Kadena, there's a special account type called a "gas station" which can act as the senderAccount. This allows users to send transactions without directly paying for gas fee. Gas stations don't sign transactions themselves; instead, a smart contract takes care of this.