Skip to content

Commit

Permalink
feat: support configurable contants for predicates (#998)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf authored May 15, 2023
1 parent a17bc14 commit 7c8439f
Show file tree
Hide file tree
Showing 14 changed files with 489 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-dots-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/predicate": minor
---

support configurable contants for predicates
1 change: 1 addition & 0 deletions apps/docs-snippets/contracts/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ members = [
"simple-token-abi",
"echo-configurables",
"transfer-to-address",
"whitelisted-address-predicate",
]
1 change: 1 addition & 0 deletions apps/docs-snippets/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum SnippetContractEnum {
TOKEN_DEPOSITOR = 'token-depositor',
TRANSFER_TO_ADDRESS = 'transfer-to-address',
ECHO_CONFIGURABLES = 'echo-configurables',
WHITELISTED_ADDRESS_PREDICATE = 'whitelisted-address-predicate',
}

const getSnippetContractPath = (contract: SnippetContractEnum) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["FuelLabs"]
entry = "main.sw"
license = "Apache-2.0"
name = "whitelisted-address-predicate"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// #region predicates-with-configurable-constants-1
predicate;

configurable {
WHITELISTED: b256 = 0xa703b26833939dabc41d3fcaefa00e62cee8e1ac46db37e0fa5d4c9fe30b4132
}

fn main(address: b256) -> bool {
WHITELISTED == address
}
// #endregion predicates-with-configurable-constants-1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { WalletUnlocked, Predicate, NativeAssetId, BN, getRandomB256 } from 'fuels';

import { SnippetContractEnum, getSnippetContractArtifacts } from '../../../contracts';
import { getTestWallet } from '../../utils';

describe(__filename, () => {
let wallet: WalletUnlocked;

const { abi, bin } = getSnippetContractArtifacts(
SnippetContractEnum.WHITELISTED_ADDRESS_PREDICATE
);

beforeAll(async () => {
wallet = await getTestWallet();
});

it('should successfully tranfer to setted whitelisted address', async () => {
// #region predicates-with-configurable-constants-2
const newWhitelistedAddress = getRandomB256();

const configurable = { WHITELISTED: newWhitelistedAddress };

// instantiate predicate with configurable constants
const predicate = new Predicate(bin, abi, wallet.provider, configurable);

// set predicate data to be the same as the configurable constant
predicate.setData(configurable.WHITELISTED);

// transfering funds to the predicate
const tx1 = await wallet.transfer(predicate.address, 500);

await tx1.waitForResult();

const destinationWallet = WalletUnlocked.generate();

const amountToTransfer = 100;

// transfering funds from the predicate to destination if predicate returns true
const tx2 = await predicate.transfer(destinationWallet.address, amountToTransfer);

await tx2.waitForResult();
// #endregion predicates-with-configurable-constants-2

const destinationBalance = await destinationWallet.getBalance(NativeAssetId);

expect(new BN(destinationBalance).toNumber()).toEqual(amountToTransfer);
});

it('should successfully tranfer to default whitelisted address', async () => {
// #region predicates-with-configurable-constants-3
const predicate = new Predicate(bin, abi, wallet.provider);

// set predicate data to be the same as the configurable constant
predicate.setData('0xa703b26833939dabc41d3fcaefa00e62cee8e1ac46db37e0fa5d4c9fe30b4132');

// transfering funds to the predicate
const tx1 = await wallet.transfer(predicate.address, 500);

await tx1.waitForResult();

const destinationWallet = WalletUnlocked.generate();

const amountToTransfer = 100;

// transfering funds from the predicate to destination if predicate returns true
const tx2 = await predicate.transfer(destinationWallet.address, amountToTransfer);

await tx2.waitForResult();
// #endregion predicates-with-configurable-constants-3

const destinationBalance = await destinationWallet.getBalance(NativeAssetId);

expect(new BN(destinationBalance).toNumber()).toEqual(amountToTransfer);
});
});
4 changes: 4 additions & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ export default defineConfig({
text: 'Send And Spend Funds From Predicates',
link: '/guide/predicates/send-and-spend-funds-from-predicates',
},
{
text: 'Predicates With Configurable Constants',
link: '/guide/predicates/predicates-with-configurable-constants',
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Predicates With Configurable Constants

Predicates, [much like Contracts](../contracts/configurable-constants.md), support configurable constants. This enables Predicates to suit specific use cases and enhance their functionality.

## Example: Asset Transfer Validation

Let's consider an example where a Predicate is used to validate an asset transfer. In this case, the transfer will only be executed if the recipient's address is on a pre-approved whitelist.

The following snippet illustrates how this could be implemented:

<<< @/../../docs-snippets/contracts/whitelisted-address-predicate/src/main.sw#predicates-with-configurable-constants-1{rust:line-numbers}

In this example, you'll notice the use of a configurable constant named `WHITELISTED`. This constant has a default value that represents the default approved address.

## Modifying The Whitelist

If there is a need to whitelist another address, the `WHITELISTED` constant can be easily updated. The following snippet demonstrates how to set a new value for the `WHITELISTED` constant and to make the Predicate execute the transfer:

<<< @/../../docs-snippets/src/guide/predicates/predicates-with-configurable.test.ts#predicates-with-configurable-constants-2{ts:line-numbers}

By ensuring that the updated `WHITELISTED` address matches the intended recipient's address, the Predicate will validate the transfer successfully.

## Default Whitelist Address

In scenarios where the default whitelisted address is already the intended recipient, there's no need to update the `WHITELISTED` constant. The Predicate will validate the transfer based on the default value. Here's how this scenario might look:

<<< @/../../docs-snippets/src/guide/predicates/predicates-with-configurable.test.ts#predicates-with-configurable-constants-3{ts:line-numbers}

This ability to configure constants within Predicates provides a flexible mechanism for customizing their behavior, thereby enhancing the robustness and versatility of our asset transfer process.

It's important to note that these customizations do not directly modify the original Predicate. The address of a Predicate is a hash of its bytecode. Any change to the bytecode, including altering a constant value, would generate a different bytecode, and thus a different hash. This leads to the creation of a new Predicate with a new address.

This doesn't mean that we're changing the behavior of the original Predicate. Instead, we're creating a new Predicate with a different configuration.

Therefore, while configurable constants do indeed enhance the flexibility and robustness of Predicates, it is achieved by creating new Predicates with different configurations, rather than altering the behavior of existing ones.
159 changes: 159 additions & 0 deletions packages/fuel-gauge/src/predicate-with-configurable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { generateTestWallet } from '@fuel-ts/wallet/test-utils';
import { readFileSync } from 'fs';
import type { Account, CoinQuantityLike } from 'fuels';
import { getRandomB256, WalletUnlocked, Predicate, BN, NativeAssetId, Provider } from 'fuels';
import { join } from 'path';

import abi from '../test-projects/predicate-with-configurable/out/debug/predicate-with-configurable-abi.json';

const bytecode = readFileSync(
join(
__dirname,
'../test-projects/predicate-with-configurable/out/debug/predicate-with-configurable.bin'
)
);

const defaultValues = {
FEE: 10,
ADDRESS: '0x38966262edb5997574be45f94c665aedb41a1663f5b0528e765f355086eebf96',
};

let wallet: WalletUnlocked;

const fundPredicate = async (predicate: Predicate<[number]>, amount: number) => {
const tx = await wallet.transfer(predicate.address, amount);

await tx.waitForResult();
};

const assertAccountBalance = async (account: Account, valueToAssert: number) => {
const balance = await account.getBalance(NativeAssetId);

expect(new BN(balance).toNumber()).toEqual(valueToAssert);
};

describe('Predicate With Configurable', () => {
beforeAll(async () => {
const provider = new Provider('http://127.0.0.1:4000/graphql');

const quantities: CoinQuantityLike[] = [
{
amount: 1_000_000,
assetId: NativeAssetId,
},
];

wallet = await generateTestWallet(provider, quantities);
});

it('should assert when input values are set to default configurable constants values', async () => {
const predicate = new Predicate(bytecode, abi, wallet.provider);

const amountToTransfer = 200;

// transfer funds to predicate
await fundPredicate(predicate, 500);

// create destination wallet
const destination = WalletUnlocked.generate();

await assertAccountBalance(destination, 0);

// set predicate input data to be the same as default configurable value
predicate.setData(defaultValues.FEE, defaultValues.ADDRESS);

const tx = await predicate.transfer(destination.address, amountToTransfer);

await tx.waitForResult();

await assertAccountBalance(destination, amountToTransfer);
});

it('should assert when input and configurable values are set equal (FEE)', async () => {
const configurableConstants = { FEE: 35 };

expect(configurableConstants.FEE).not.toEqual(defaultValues.FEE);

const predicate = new Predicate(bytecode, abi, wallet.provider, configurableConstants);

const amountToTransfer = 300;

const destination = WalletUnlocked.generate();

await assertAccountBalance(destination, 0);

// transfer funds to predicate
await fundPredicate(predicate, 500);

predicate.setData(configurableConstants.FEE, defaultValues.ADDRESS);

// executing predicate transfer
const tx = await predicate.transfer(destination.address, amountToTransfer);

await tx.waitForResult();

await assertAccountBalance(destination, amountToTransfer);
});

it('should assert when input and configurable values are set equal (ADDRESS)', async () => {
const configurableConstants = { ADDRESS: getRandomB256() };

expect(configurableConstants.ADDRESS).not.toEqual(defaultValues.ADDRESS);

const predicate = new Predicate(bytecode, abi, wallet.provider, configurableConstants);

const amountToTransfer = 300;

const destination = WalletUnlocked.generate();

await assertAccountBalance(destination, 0);

// transfer funds to predicate
await fundPredicate(predicate, 500);

predicate.setData(defaultValues.FEE, configurableConstants.ADDRESS);

// executing predicate transfer
const tx = await predicate.transfer(destination.address, amountToTransfer);

await tx.waitForResult();

await assertAccountBalance(destination, amountToTransfer);
});

it('should assert when input and configurable values are set equal (BOTH)', async () => {
const configurableConstants = {
FEE: 90,
ADDRESS: getRandomB256(),
};

expect(configurableConstants.FEE).not.toEqual(defaultValues.FEE);
expect(configurableConstants.ADDRESS).not.toEqual(defaultValues.ADDRESS);

const predicate = new Predicate(bytecode, abi, wallet.provider, configurableConstants);

const amountToTransfer = 300;

const destination = WalletUnlocked.generate();

await assertAccountBalance(destination, 0);

await fundPredicate(predicate, 500);

predicate.setData(configurableConstants.FEE, configurableConstants.ADDRESS);

const tx = await predicate.transfer(destination.address, amountToTransfer);

await tx.waitForResult();

await assertAccountBalance(destination, amountToTransfer);
});

it('should throws when no input data is given', async () => {
const predicate = new Predicate(bytecode, abi, wallet.provider);

const destination = WalletUnlocked.generate();

await expect(predicate.transfer(destination.address, 300)).rejects.toThrowError();
});
});
1 change: 1 addition & 0 deletions packages/fuel-gauge/test-projects/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ members = [
"predicate-struct",
"predicate-triple-sig",
"predicate-true",
"predicate-with-configurable",
"predicate-u32",
"revert-error",
"script-main-args",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
license = "Apache-2.0"
name = "predicate-with-configurable"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
predicate;

configurable {
FEE: u8 = 10,
ADDRESS: b256 = 0x38966262edb5997574be45f94c665aedb41a1663f5b0528e765f355086eebf96
}

fn main(fee: u8, address: b256) -> bool {
FEE == fee && address == ADDRESS
}
Loading

0 comments on commit 7c8439f

Please sign in to comment.