From af6f07fe4c96360d433578f57bb8854a2c8bb2f7 Mon Sep 17 00:00:00 2001 From: Javad Khalilian Date: Wed, 2 Aug 2023 16:18:06 +0200 Subject: [PATCH 1/5] docs(client-examples): add transfer to readme --- packages/libs/client-examples/README.md | 160 ++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 11 deletions(-) diff --git a/packages/libs/client-examples/README.md b/packages/libs/client-examples/README.md index d26ac3833d..631feaba44 100644 --- a/packages/libs/client-examples/README.md +++ b/packages/libs/client-examples/README.md @@ -1,17 +1,155 @@ - +## Preparation -# @kadena/client-examples +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`. -Test project to verify pactjs-cli and pactjs-generator +To install `@kadena/pactjs-cli` as a dev dependency for your project, run the +following command in the terminal: - - - kadena.js logo - +```shell +npm install @kadena/pactjs-cli --save-dev +``` - +You can generate type definitions from either a local file or directly from the +chain. -## Client examples +### Creating a type definition from a contract deployed on the chain -This project demonstrates the use of the `@kadena/pact-cli` together with -`@kadena/client` for _smart contracts_. +```shell +npx pactjs contract-generate --contract="coin" --api http://104.248.41.186:8080/chainweb/0.0/development/chain/0/pact +``` + +### Creating a type definition from a pact file + +```shell +npx pactjs contract-generate --file=./coin.pact --api http://104.248.41.186:8080/chainweb/0.0/development/chain/0/pact +``` + +Note: Passing the API is optional here, but it's better to always do that so the +script can generate types for all of the dependencies as well. + +Note: You can use `--file` and `--contract` several times, and even together. + +## 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. + +```ts +import { + getClient, + isSignedCommand, + 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; +} + +const { submit, listen } = getClient(); + +async function transfer( + sender: IAccount, + receiver: IAccount, + amount: string, +): Promise { + // The first step for interacting with the blockchain is creating a request object. + // We call this object a transaction object. `Pact.builder` will help you to create this object easily. + const transaction = Pact.builder + // In Pact, we have two types of commands: 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. + // This 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. + { + decimal: amount, + }, + ), + ) + // The sender needs to sign the command; otherwise, the blockchain node will refuse to do the transaction. + .addSigner(sender.publicKey, (withCapability) => [ + // The sender also mentions they want to pay the transaction fee by adding the "coin.GAS" capability. + withCapability('coin.GAS'), + // The sender scopes their signature only to "coin.TRANSFER" with the specific arguments. + 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', sender: 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. + // For example, if you are using EckoWallet, it has a specific API for signing transactions. + // By the end, the signer function should fill the sigs array and return the signed transaction. + const signedTr = await signWithChainweaver(transaction); + + // As the signer function could be an external function, we double-check if the transaction is signed correctly. + if (isSignedCommand(signedTr)) { + // So it's time to submit the transaction; this function returns the requestKey. + const requestKey = await submit(signedTr); + // We listen for the result of the request. + const response = await listen(requestKey); + // Now we need to check the status. + if (response.result.status === 'failure') { + throw response.result.error; + } else { + // Congratulations! You have successfully submitted a transfer transaction. + console.log(response.result); + } + } +} + +// Calling the function with proper input +transfer( + { 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. From ba270a1e45e5d6ffebb3346e2f239b763ffbf226 Mon Sep 17 00:00:00 2001 From: Javad Khalilian Date: Fri, 18 Aug 2023 15:29:41 +0200 Subject: [PATCH 2/5] docs(client-examples): add header --- packages/libs/client-examples/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/libs/client-examples/README.md b/packages/libs/client-examples/README.md index 631feaba44..1d324a331c 100644 --- a/packages/libs/client-examples/README.md +++ b/packages/libs/client-examples/README.md @@ -1,3 +1,17 @@ + + +# @kadena/client-examples + +This project demonstrates the use of the `@kadena/pact-cli` together with +`@kadena/client` for _smart contracts_. + + + + kadena.js logo + + + + ## Preparation In the following examples, we will interact with the `coin` contract. To have From e88b0a4ef83fcc152e22d0e84fdb4060ca3df7e9 Mon Sep 17 00:00:00 2001 From: Javad Khalilian Date: Mon, 21 Aug 2023 17:19:29 +0200 Subject: [PATCH 3/5] docs(client-example): update readme --- packages/libs/client-examples/README.md | 72 +++++++++++++++++++------ 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/packages/libs/client-examples/README.md b/packages/libs/client-examples/README.md index 1d324a331c..53ec5b73fd 100644 --- a/packages/libs/client-examples/README.md +++ b/packages/libs/client-examples/README.md @@ -31,20 +31,30 @@ chain. ### Creating a type definition from a contract deployed on the chain ```shell -npx pactjs contract-generate --contract="coin" --api http://104.248.41.186:8080/chainweb/0.0/development/chain/0/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 ```shell -npx pactjs contract-generate --file=./coin.pact --api http://104.248.41.186:8080/chainweb/0.0/development/chain/0/pact +npx pactjs contract-generate --file=./coin.pact --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact ``` -Note: Passing the API is optional here, but it's better to always do that so the -script can generate types for all of the dependencies as well. +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 chian. So for the coin example you +can alternatively use the following command if you have all of the files +locally. + +```shell +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 @@ -93,6 +103,7 @@ async function transfer( ), ) // The sender needs to sign the command; otherwise, the blockchain node will refuse to do the transaction. + // 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 also mentions they want to pay the transaction fee by adding the "coin.GAS" capability. withCapability('coin.GAS'), @@ -108,7 +119,7 @@ async function transfer( ]) // 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', sender: sender.accountName }) + .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. @@ -126,19 +137,22 @@ async function transfer( const signedTr = await signWithChainweaver(transaction); // As the signer function could be an external function, we double-check if the transaction is signed correctly. - if (isSignedCommand(signedTr)) { - // So it's time to submit the transaction; this function returns the requestKey. - const requestKey = await submit(signedTr); - // We listen for the result of the request. - const response = await listen(requestKey); - // Now we need to check the status. - if (response.result.status === 'failure') { - throw response.result.error; - } else { - // Congratulations! You have successfully submitted a transfer transaction. - console.log(response.result); - } + if (!isSignedCommand(signedTr)) { + throw new Error('TX_IS_NOT_SIGNED'); } + + // Now it's time to submit the transaction; this function returns the requestDecelerator {requestKey, networkId, chainId}. + // by storing this object in a permanent storage you always can fetch the result of the transaction from the blockchain + const requestDecelerator = await submit(signedTr); + // We listen for the result of the request. + const response = await listen(requestDecelerator); + // 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 input @@ -167,3 +181,27 @@ 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 sender in the metadata?_ + +When working with capabilities, remember to clearly define the scope of your +signature. When you include capabilities, 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. From 217f4f926ecc187b9180bd38b8735367df2d68dd Mon Sep 17 00:00:00 2001 From: Javad Khalilian Date: Tue, 22 Aug 2023 11:01:01 +0200 Subject: [PATCH 4/5] docs: typo --- packages/libs/client-examples/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/libs/client-examples/README.md b/packages/libs/client-examples/README.md index 53ec5b73fd..1a50d80a26 100644 --- a/packages/libs/client-examples/README.md +++ b/packages/libs/client-examples/README.md @@ -2,7 +2,7 @@ # @kadena/client-examples -This project demonstrates the use of the `@kadena/pact-cli` together with +This project demonstrates the use of the `@kadena/pactjs-cli` together with `@kadena/client` for _smart contracts_. @@ -141,11 +141,11 @@ async function transfer( throw new Error('TX_IS_NOT_SIGNED'); } - // Now it's time to submit the transaction; this function returns the requestDecelerator {requestKey, networkId, chainId}. + // 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 requestDecelerator = await submit(signedTr); + const requestDescriptor = await submit(signedTr); // We listen for the result of the request. - const response = await listen(requestDecelerator); + const response = await listen(requestDescriptor); // Now we need to check the status. if (response.result.status === 'failure') { throw response.result.error; From e1f8a6e8a075e8371b4b99db282e93c84ce7b15c Mon Sep 17 00:00:00 2001 From: Javad Khalilian Date: Wed, 23 Aug 2023 13:55:21 +0200 Subject: [PATCH 5/5] docs(client-examples): update readme --- packages/libs/client-examples/README.md | 45 ++++--- .../example-contract/template-transaction.ts | 117 ++++++++++++++++-- 2 files changed, 136 insertions(+), 26 deletions(-) diff --git a/packages/libs/client-examples/README.md b/packages/libs/client-examples/README.md index 1a50d80a26..7ec49e0eda 100644 --- a/packages/libs/client-examples/README.md +++ b/packages/libs/client-examples/README.md @@ -65,8 +65,9 @@ This example demonstrates how to use `Pact.builder`, `Pact.modules`, ```ts import { - getClient, - isSignedCommand, + createClient, + ICommand, + isSignedTransaction, Pact, signWithChainweaver, } from '@kadena/client'; @@ -78,7 +79,13 @@ interface IAccount { publicKey: string; } -const { submit, listen } = getClient(); +// 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 need +const { submit, listen } = createClient(); async function transfer( sender: IAccount, @@ -86,28 +93,33 @@ async function transfer( amount: string, ): Promise { // The first step for interacting with the blockchain is creating a request object. - // We call this object a transaction object. `Pact.builder` will help you to create this object easily. + // We call this object a transaction object. `Pact.builder` helps you to create this object easily. const transaction = Pact.builder - // In Pact, we have two types of commands: execution and continuation. Most of the typical use cases only use execution. + // 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. - // This means you want to call the transfer function of the coin module (contract). + // 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. + // 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 also mentions they want to pay the transaction fee by adding the "coin.GAS" capability. + // 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'), - // The sender scopes their signature only to "coin.TRANSFER" with the specific arguments. + // 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, @@ -131,19 +143,20 @@ async function transfer( // - 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. - // For example, if you are using EckoWallet, it has a specific API for signing transactions. + // 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 signedTr = await signWithChainweaver(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 (!isSignedCommand(signedTr)) { + 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(signedTr); + 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. @@ -190,10 +203,10 @@ 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 sender in the metadata?_ +set the senderAccount in the metadata?_ When working with capabilities, remember to clearly define the scope of your -signature. When you include capabilities, you need to list all of them. So, if +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 diff --git a/packages/libs/client-examples/src/example-contract/template-transaction.ts b/packages/libs/client-examples/src/example-contract/template-transaction.ts index cdc452eb4c..53ae6bb79c 100644 --- a/packages/libs/client-examples/src/example-contract/template-transaction.ts +++ b/packages/libs/client-examples/src/example-contract/template-transaction.ts @@ -1,13 +1,110 @@ -// import { -// IUnsignedTransaction, -// Pact, -// signWithChainweaver, -// } from '@kadena/client'; +import { + createClient, + ICommand, + isSignedTransaction, + Pact, + signWithChainweaver, +} from '@kadena/client'; -// import { safeTransaction } from './tx-library'; +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; +} -// const unsignedTransaction = safeTransaction({ -// 'from-acct': 'k:02193', -// }) as IUnsignedTransaction; +// 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 need +const { submit, listen } = createClient(); -// signWithChainweaver(unsignedTransaction).then().catch(console.error); +async function transfer( + sender: IAccount, + receiver: IAccount, + amount: string, +): Promise { + // 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 input +transfer( + { accountName: 'bob', publicKey: 'bob_public_key' }, + { accountName: 'alice', publicKey: 'alice_public_key' }, + '1', +).catch(console.error);