diff --git a/.changeset/large-kings-return.md b/.changeset/large-kings-return.md new file mode 100644 index 0000000000..645f309c8f --- /dev/null +++ b/.changeset/large-kings-return.md @@ -0,0 +1,4 @@ +--- +--- + +docs: add more thorough examples for predicates \ No newline at end of file diff --git a/.knip.json b/.knip.json index 4aa56873de..047d7bf5d4 100644 --- a/.knip.json +++ b/.knip.json @@ -7,6 +7,7 @@ "templates/**" ], "ignoreDependencies": [ + "@/sway-api/*", "@fuel-ts/*", "@internal/fuel-core", "@internal/forc", diff --git a/apps/create-fuels-counter-guide/src/pages/predicate.tsx b/apps/create-fuels-counter-guide/src/pages/predicate.tsx new file mode 100644 index 0000000000..dcd7176d19 --- /dev/null +++ b/apps/create-fuels-counter-guide/src/pages/predicate.tsx @@ -0,0 +1,183 @@ +import { Button } from "@/components/Button"; +import { FuelLogo } from "@/components/FuelLogo"; +import { Input } from "@/components/Input"; +import { Link } from "@/components/Link"; +import { useActiveWallet } from "@/hooks/useActiveWallet"; +/** @knipignore */ +import { TestPredicateAbi__factory } from "@/sway-api"; +import { BN, InputValue, Predicate } from "fuels"; +import { bn } from "fuels"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import useAsync from "react-use/lib/useAsync"; + +export default function PredicateExample() { + let baseAssetId: string; + + const { wallet, walletBalance, refreshWalletBalance } = useActiveWallet(); + + const [predicate, setPredicate] = useState>(); + + const [predicateBalance, setPredicateBalance] = useState(); + + const [pin, setPin] = useState(); + + useAsync(async () => { + if (wallet) { + baseAssetId = wallet.provider.getBaseAssetId(); + const predicate = TestPredicateAbi__factory.createInstance(wallet.provider); + setPredicate(predicate); + setPredicateBalance(await predicate.getBalance()); + } + }, [wallet]); + + const refreshBalances = async () => { + await refreshWalletBalance?.(); + setPredicateBalance(await predicate?.getBalance()); + }; + + const transferFundsToPredicate = async (amount: BN) => { + if (!predicate) { + return toast.error("Predicate not loaded"); + } + + if (!wallet) { + return toast.error("Wallet not loaded"); + } + + await wallet.transfer(predicate.address, amount, baseAssetId, { + gasLimit: 10_000, + }); + + await refreshBalances(); + + return toast.success("Funds transferred to predicate."); + }; + + const unlockPredicateAndTransferFundsBack = async (amount: BN) => { + try { + if (!wallet) { + return toast.error("Wallet not loaded"); + } + + const reInitializePredicate = TestPredicateAbi__factory.createInstance(wallet.provider, [bn(pin)]); + + if (!reInitializePredicate) { + return toast.error("Failed to initialize predicate"); + } + + const tx = await reInitializePredicate.transfer(wallet.address, amount, baseAssetId); + const { isStatusSuccess } = await tx.wait(); + + if (!isStatusSuccess) { + toast.error("Failed to unlock predicate"); + return; + } + + if (isStatusSuccess) { + toast.success("Predicate unlocked"); + } + + await refreshBalances(); + } catch (e) { + console.error(e); + toast.error( + "Failed to unlock predicate. You probably entered the wrong pin, or the predicate does not have enough balance. Try again." + ); + } + }; + + // #region change-pin-react-function + const changePin = async () => { + if (!wallet) { + return toast.error("Wallet not loaded"); + } + if (!predicate) { + return toast.error("Predicate not loaded"); + } + + if (walletBalance?.eq(0)) { + return toast.error( + "Your wallet does not have enough funds. Please click the 'Top-up Wallet' button in the top right corner, or use the local faucet." + ); + } + + if (!pin) { + return toast.error("Please enter a pin"); + } + + const configurable = { PIN: bn(pin) }; + // instantiate predicate with configurable constants + const reInitializePredicate = TestPredicateAbi__factory.createInstance(wallet.provider, [bn(configurable.PIN)], configurable); + + if (!reInitializePredicate) { + return toast.error("Failed to initialize predicate"); + } + + // transferring funds to the predicate + const tx = await wallet.transfer(reInitializePredicate.address, 1000, baseAssetId, { + gasLimit: 10_000, + }); + + const { isStatusSuccess } = await tx.wait(); + + if (!isStatusSuccess) { + toast.error("Failed to update pin in predicate"); + return; + } + + if (isStatusSuccess) { + toast.success("Predicate pin updated"); + } + + await refreshWalletBalance?.(); + }; + // #endregion change-pin-react-function + + return ( + <> +
+ +

Predicate

+
+ +
+
Wallet Balance:
+ {walletBalance?.toString()} +
+ +
+
Predicate Balance:
+ {predicateBalance?.toString()} +
+ + + + + + setPin(e.target.value)} + placeholder="Enter a new pin" + /> + + + + + Do note that when you 'unlock' a predicate, the predicate also pays for the gas of the transaction.
+ This is why you will notice that the balance of the predicate gets reduced by 1000 + a nominal gas fee. +
+ + + Learn more about Predicates + + + + Back to Home + + + ); +} diff --git a/apps/create-fuels-counter-guide/sway-programs/Forc.toml b/apps/create-fuels-counter-guide/sway-programs/Forc.toml index d7fbaaa8d2..419c878f5f 100644 --- a/apps/create-fuels-counter-guide/sway-programs/Forc.toml +++ b/apps/create-fuels-counter-guide/sway-programs/Forc.toml @@ -1,2 +1,2 @@ [workspace] -members = ["contract"] +members = ["contract", "predicate"] diff --git a/apps/create-fuels-counter-guide/sway-programs/predicate/.gitignore b/apps/create-fuels-counter-guide/sway-programs/predicate/.gitignore new file mode 100644 index 0000000000..77d3844f58 --- /dev/null +++ b/apps/create-fuels-counter-guide/sway-programs/predicate/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/apps/create-fuels-counter-guide/sway-programs/predicate/Forc.toml b/apps/create-fuels-counter-guide/sway-programs/predicate/Forc.toml new file mode 100644 index 0000000000..ab10742d0f --- /dev/null +++ b/apps/create-fuels-counter-guide/sway-programs/predicate/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "test-predicate" + +[dependencies] diff --git a/apps/create-fuels-counter-guide/sway-programs/predicate/src/main.sw b/apps/create-fuels-counter-guide/sway-programs/predicate/src/main.sw new file mode 100644 index 0000000000..c80cca363d --- /dev/null +++ b/apps/create-fuels-counter-guide/sway-programs/predicate/src/main.sw @@ -0,0 +1,9 @@ +predicate; + +configurable { + PIN: u64 = 1337, +} + +fn main(pin: u64) -> bool { + return PIN == pin; +} diff --git a/apps/docs-snippets/src/guide/predicates/interacting-with-predicates.test.ts b/apps/docs-snippets/src/guide/predicates/interacting-with-predicates.test.ts new file mode 100644 index 0000000000..f65d10f3d7 --- /dev/null +++ b/apps/docs-snippets/src/guide/predicates/interacting-with-predicates.test.ts @@ -0,0 +1,122 @@ +import { seedTestWallet } from '@fuel-ts/account/test-utils'; +import type { Provider, WalletUnlocked } from 'fuels'; +import { ScriptTransactionRequest, bn, Predicate, BN } from 'fuels'; + +import { + DocSnippetProjectsEnum, + getDocsSnippetsForcProject, +} from '../../../test/fixtures/forc-projects'; +import { getTestWallet } from '../../utils'; + +/** + * @group node + */ +describe(__filename, () => { + let wallet: WalletUnlocked; + let receiver: WalletUnlocked; + let baseAssetId: string; + let provider: Provider; + let predicate: Predicate<[string]>; + + const { abiContents: abi, binHexlified: bin } = getDocsSnippetsForcProject( + DocSnippetProjectsEnum.SIMPLE_PREDICATE + ); + + const inputAddress = '0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4'; + + beforeAll(async () => { + wallet = await getTestWallet(); + receiver = await getTestWallet(); + provider = wallet.provider; + + baseAssetId = wallet.provider.getBaseAssetId(); + + predicate = new Predicate<[string]>({ + bytecode: bin, + provider: wallet.provider, + abi, + inputData: [inputAddress], + }); + await seedTestWallet(predicate, [[100_000_000, baseAssetId]]); + }); + + it('should get predicate resources and add them to the predicate data', async () => { + // #region interacting-with-predicates-1 + + // Instantiate the transaction request + const transactionRequest = new ScriptTransactionRequest({ + gasLimit: 2000, + maxFee: bn(0), + }); + + const predicateCoins = await predicate.getResourcesToSpend([ + { amount: 2000, assetId: baseAssetId }, + ]); + + // Add the predicate input and resources + transactionRequest.addResources(predicateCoins); + // #endregion interacting-with-predicates-1 + + expect(transactionRequest.inputs.length).toBeGreaterThanOrEqual(1); + expect(transactionRequest.outputs.length).toEqual(1); + }); + + it('should successfully transfer funds to the predicate', async () => { + const transactionRequest = new ScriptTransactionRequest({ gasLimit: 2000, maxFee: bn(0) }); + transactionRequest.addCoinOutput(receiver.address, 100, baseAssetId); + + const txCost = await provider.getTransactionCost(transactionRequest, { + resourcesOwner: predicate, + }); + + transactionRequest.gasLimit = txCost.gasUsed; + transactionRequest.maxFee = txCost.maxFee; + + await predicate.fund(transactionRequest, txCost); + + // #region interacting-with-predicates-2 + + const result = await predicate.sendTransaction(transactionRequest); + + await result.waitForResult(); + // #endregion interacting-with-predicates-2 + + const { isStatusSuccess } = await result.waitForResult(); + + expect(isStatusSuccess).toBeTruthy(); + }); + + it('should successfully simulate a transaction with predicate', async () => { + // #region interacting-with-predicates-3 + const transactionRequest = new ScriptTransactionRequest({ gasLimit: 2000, maxFee: bn(0) }); + transactionRequest.addCoinOutput(receiver.address, 1000000, baseAssetId); + + const txCost = await provider.getTransactionCost(transactionRequest, { + resourcesOwner: predicate, + }); + + transactionRequest.gasLimit = txCost.gasUsed; + transactionRequest.maxFee = txCost.maxFee; + + await predicate.fund(transactionRequest, txCost); + + const result = await predicate.simulateTransaction(transactionRequest); + + // #endregion interacting-with-predicates-3 + + expect(result.receipts).toEqual([ + { + type: 1, + id: expect.any(String), + val: expect.any(BN), + pc: expect.any(BN), + is: expect.any(BN), + }, + { + type: 9, + gasUsed: expect.any(BN), + result: expect.any(BN), + }, + ]); + }); +}); diff --git a/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts b/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts index bd0d26c7b0..a3b618319f 100644 --- a/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts +++ b/apps/docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts @@ -179,8 +179,12 @@ describe(__filename, () => { } ); - const chainId = provider.getChainId(); + /* + You can retrieve the transaction ID before actually submitting it to the node + like this: + */ + const chainId = provider.getChainId(); const txId = transactionRequest.getTransactionId(chainId); const res = await predicate.sendTransaction(transactionRequest); diff --git a/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml b/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml index 1d453e4e9e..ded6b45dc7 100644 --- a/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml +++ b/apps/docs-snippets/test/fixtures/forc-projects/Forc.toml @@ -33,4 +33,5 @@ members = [ "script-signing", "input-output-types", "bytecode-input", + "configurable-pin", ] diff --git a/apps/docs-snippets/test/fixtures/forc-projects/configurable-pin/Forc.toml b/apps/docs-snippets/test/fixtures/forc-projects/configurable-pin/Forc.toml new file mode 100644 index 0000000000..9cee609fe4 --- /dev/null +++ b/apps/docs-snippets/test/fixtures/forc-projects/configurable-pin/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "configurable-pin" + +[dependencies] diff --git a/apps/docs-snippets/test/fixtures/forc-projects/configurable-pin/src/main.sw b/apps/docs-snippets/test/fixtures/forc-projects/configurable-pin/src/main.sw new file mode 100644 index 0000000000..3c275d2898 --- /dev/null +++ b/apps/docs-snippets/test/fixtures/forc-projects/configurable-pin/src/main.sw @@ -0,0 +1,12 @@ +// #region predicate-with-configurable-pin-1 +predicate; + +configurable { + PIN: u64 = 1337, +} + +fn main(pin: u64) -> bool { + return PIN == pin; +} + +// #endregion predicate-with-configurable-pin-1 diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index fd9713a5b7..6fa86a740d 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -83,6 +83,10 @@ export default defineConfig({ text: 'Deploying a dApp to Testnet', link: '/guide/creating-a-fuel-dapp/deploying-a-dapp-to-testnet', }, + { + text: 'Working with Predicates', + link: '/guide/creating-a-fuel-dapp/working-with-predicates', + }, ], }, { @@ -289,6 +293,10 @@ export default defineConfig({ text: 'Send And Spend Funds From Predicates', link: '/guide/predicates/send-and-spend-funds-from-predicates', }, + { + text: 'Methods', + link: '/guide/predicates/methods', + }, ], }, { diff --git a/apps/docs/spell-check-custom-words.txt b/apps/docs/spell-check-custom-words.txt index 85e2505c2e..61def69ded 100644 --- a/apps/docs/spell-check-custom-words.txt +++ b/apps/docs/spell-check-custom-words.txt @@ -316,6 +316,8 @@ Gwei onchain Vercel hardcoded +differentiators +updatable tsconfig deployer overriden diff --git a/apps/docs/src/guide/creating-a-fuel-dapp/index.md b/apps/docs/src/guide/creating-a-fuel-dapp/index.md index eae279d6ed..54377f9cd4 100644 --- a/apps/docs/src/guide/creating-a-fuel-dapp/index.md +++ b/apps/docs/src/guide/creating-a-fuel-dapp/index.md @@ -147,6 +147,12 @@ You should now be able to see the counter dApp running at `http://localhost:3000 ![Fullstack Fuel Dev Workflow](../../public/creating-a-fuel-dapp-create-fuels-split-view.png) +::: + +**Note** You may wish to learn more about how you could create a Fuel dApp that uses predicates, check out our [Working with Predicates](./working-with-predicates.md) guide. + +--- + ## Adding Decrement Functionality To add decrement functionality to our counter, we will have to do two things: 1. Add a `decrement_counter` function to our Sway contract, and 2. Modify the `./src/pages/index.tsx` file to add a button that calls this function. @@ -197,8 +203,11 @@ Whenever you want to add a new feature to your dApp and quickly prototype things - Now that you have a basic counter dApp running and have the `npm create fuels` workflow powering you, you can start building more complex dApps using the Fuel Stack. A good place to start for ideas and reference code is the [Sway Applications Repo](https://github.com/FuelLabs/sway-applications). +- As you may have noticed, there are different types of programs in your dApp, feel free to explore [Predicates](https://docs.fuel.network/docs/fuels-ts/predicates/) and [Scripts](https://docs.fuel.network/docs/fuels-ts/scripts/), which are both important differentiators in the Fuel Stack. + - If you want to deploy your dApp to the testnet, check out our [Deploying a dApp to Testnet](./deploying-a-dapp-to-testnet.md) guide. - If you have any questions or need help, feel free to reach out to us on the [Official Fuel Forum](https://forum.fuel.network/). - If you want to learn more about the Fuel Stack, check out the [Fuel Docs](https://docs.fuel.network/). + diff --git a/apps/docs/src/guide/creating-a-fuel-dapp/working-with-predicates.md b/apps/docs/src/guide/creating-a-fuel-dapp/working-with-predicates.md new file mode 100644 index 0000000000..b22d51ddeb --- /dev/null +++ b/apps/docs/src/guide/creating-a-fuel-dapp/working-with-predicates.md @@ -0,0 +1,53 @@ + +# Working with Predicates + +This guide builds on the [Creating a Fuel dApp](./index.md) guide. Once you've gotten the dApp there up and running, then you can continue here via clicking the Predicate Example link. We will modify the predicate we created in the previous guide. The final result will look like this: + +![End result of this guide](../../public/working-with-predicates-end-result.png) + +## Adding a Configurable pin + +The current predicate functionality we have is a simple one that checks if the user has a pin. We will modify this predicate to accept a configurable pin. This will allow the user to set their own pin. + +1. Modifying the Predicate Contract + +The first step is to modify the predicate contract to accept a configurable pin. We will use the [`configurable`](https://docs.fuel.network/guides/intro-to-predicates/configurables/#configurables) keyword to create an updatable constant to store the pin. We will also modify the main function to check this constant instead of a hardcoded pin. + +<<< @/../../docs-snippets/test/fixtures/forc-projects/configurable-pin/src/main.sw#predicate-with-configurable-pin-1{rust:line-numbers} + +2. Modifying the Frontend + +We will now add new button to the frontend that will update the `pin` in the predicate when clicked. To do this, we will modify the `./src/pages/predicate.tsx` file. + +We will add a function called `changePin`, which will use the current pin in state to update the pin in the predicate as well as transfer 1000 to the predicate. + +<<< @/../../create-fuels-counter-guide/src/pages/predicate.tsx#change-pin-react-function{ts:line-numbers} + +It would also be useful to change the placeholder text to say "Enter a new pin" instead of "Hint - the correct pin is 1337". + +```tsx + setPin(e.target.value)} + placeholder="Enter a new pin" +/> +``` + +Finally, we will add a button that calls the `changePin` function when clicked. + +```tsx + +``` + +Congratulations! That's all. You should now be able to see the modified predicate dApp running at `http://localhost:3000` with our newly added change pin functionality. + +You can find the complete source code of the dApp we built [here](https://github.com/FuelLabs/fuels-ts/tree/master/apps/create-fuels-counter-guide). + +## Next Steps + +- Now that you have a predicate dApp running and have the `npm create fuels` workflow powering you, you can start building more complex dApps using the Fuel Stack. A good place to start for ideas and reference code is the [Sway Applications Repo](https://github.com/FuelLabs/sway-applications). + +- If you have any questions or need help, feel free to reach out to us on the [Official Fuel Forum](https://forum.fuel.network/). + +- If you want to learn more about the Fuel Stack, check out the [Fuel Docs](https://docs.fuel.network/). diff --git a/apps/docs/src/guide/predicates/methods.md b/apps/docs/src/guide/predicates/methods.md new file mode 100644 index 0000000000..a9803ef565 --- /dev/null +++ b/apps/docs/src/guide/predicates/methods.md @@ -0,0 +1,55 @@ +# Interacting With Predicates + +The `Predicate` class extends the [`Account`](https://docs.fuel.network/docs/fuels-ts/account/) class, inheriting all its methods. Therefore, there are multiple ways to interact with predicates, but broadly speaking, we can think about three: + - `Checking Balances` + - `Transactions` + - `Transfers` + + +## Checking Balances + +### `getBalances` + +This will return the balances of all assets owned by the predicate. + +See also: [Checking Wallet Balances](https://docs.fuel.network/docs/fuels-ts/wallets/checking-balances/#getting-a-wallets-balance) + +### `getResourcesToSpend` + +This will return the resources owned by a predicate so that they can be added to a transaction request. + +This method is called under the hood when using [`transfer`](./methods.md#transfer) or [`createTransfer`](./methods.md#createtransfer). + +You may want to use this method when using a predicate in an existing transaction request. + +<<< @/../../docs-snippets/src/guide/predicates/interacting-with-predicates.test.ts#interacting-with-predicates-1{ts:line-numbers} + +## Transactions + +### `sendTransaction` + +This is used to send a transaction to the node. + +<<< @/../../docs-snippets/src/guide/predicates/interacting-with-predicates.test.ts#interacting-with-predicates-2{ts:line-numbers} + +### `simulateTransaction` + +You can use the `simulateTransaction` method to dry-run a predicate call without consuming resources. A typical use case of a dry-run call is to validate that sufficient funds are available to cover the transaction fees. + +<<< @/../../docs-snippets/src/guide/predicates/interacting-with-predicates.test.ts#interacting-with-predicates-3{ts:line-numbers} + +## Transfers + +### `createTransfer` + +The `createTransfer` method creates a transaction request with all the necessary transfer details. It automatically estimates the transaction costs via a dry-run call and funds the request with the required predicate resources. After this, one can submit the returned transaction request with greater certainty that it will succeed. + +However, please remember that you can still modify the transfer request details and use its properties before submitting it to the node. + +<<< @/../../docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts#send-and-spend-funds-from-predicates-8{ts:line-numbers} + +### `transfer` + +You can send funds to another address using the `transfer` method. + +<<< @/../../docs-snippets/src/guide/cookbook/transferring-assets.test.ts#transferring-assets-1{ts:line-numbers} diff --git a/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md b/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md index d6387de0bb..a20025005b 100644 --- a/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md +++ b/apps/docs/src/guide/predicates/send-and-spend-funds-from-predicates.md @@ -40,8 +40,6 @@ Once the predicate resolves with a return value `true` based on its predefined c In a similar approach, you can use the `createTransfer` method, which returns a [`ScriptTransactionRequest`](../../api/Account/ScriptTransactionRequest.md). Then, we can submit this transaction request by calling the `sendTransaction` method. -This can be useful if you need the transaction ID before actually submitting it to the node. - <<< @/../../docs-snippets/src/guide/predicates/send-and-spend-funds-from-predicates.test.ts#send-and-spend-funds-from-predicates-8{ts:line-numbers} ## Spending Entire Predicate Held Amount diff --git a/apps/docs/src/public/working-with-predicates-end-result.png b/apps/docs/src/public/working-with-predicates-end-result.png new file mode 100644 index 0000000000..fc0cad3891 Binary files /dev/null and b/apps/docs/src/public/working-with-predicates-end-result.png differ