Skip to content

Commit

Permalink
Merge branch 'master' into st/chore/fix-ci-markdown-lint-check
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf committed Nov 28, 2024
2 parents 5c88283 + ef94263 commit 0d4ade1
Show file tree
Hide file tree
Showing 39 changed files with 1,498 additions and 1,110 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-mugs-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/recipes": patch
---

docs: proxy contract cookbook
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ apps/create-fuels-counter-guide
apps/docs/src/typegend
apps/docs/src/**/*.test.ts

packages/fuels/src/cli/commands/deploy/proxy
packages/recipes/src
packages/fuels/test/fixtures/project
packages/account/src/providers/__generated__
packages/account/src/providers/assets
Expand Down
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ apps/docs/src/api
apps/docs/src/typegend
apps/docs/src/**/*.test.ts

packages/fuels/src/cli/commands/deploy/proxy
packages/recipes/src
packages/fuels/test/fixtures/project
packages/account/src/providers/assets

Expand Down
3 changes: 2 additions & 1 deletion apps/docs-api/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"../../packages/errors",
"../../packages/hasher",
"../../packages/math",
"../../packages/transactions"
"../../packages/transactions",
"../../packages/recipes"
],
"out": "src/api",
"readme": "./index.md",
Expand Down
46 changes: 44 additions & 2 deletions apps/docs/src/guide/contracts/proxy-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,51 @@

Automatic deployment of proxy contracts can be enabled in `Forc.toml`.

Once that is in place, [fuels deploy](https://docs.fuel.network/docs/fuels-ts/fuels-cli/commands/#fuels-deploy) will take care of it.
We recommend that you use [fuels deploy](https://docs.fuel.network/docs/fuels-ts/fuels-cli/commands/#fuels-deploy) to deploy and upgrade your contract using a proxy as it will take care of everything for you. However, if you want to deploy a proxy contract manually, you can follow the guide below.

## Docs
## Manually Deploying and Upgrading by Proxy

As mentioned above, we recommend using [fuels deploy](https://docs.fuel.network/docs/fuels-ts/fuels-cli/commands/#fuels-deploy) to deploy and upgrade your contract as everything is handled under the hood. But the below guide will detail this process should you want to implement it yourself.

We recommend using the [SRC14 compliant owned proxy contract](https://github.com/FuelLabs/sway-standard-implementations/tree/174f5ed9c79c23a6aaf5db906fe27ecdb29c22eb/src14/owned_proxy/contract/out/release) as the underlying proxy as that is the one we will use in this guide and the one used by [fuels deploy](https://docs.fuel.network/docs/fuels-ts/fuels-cli/commands/#fuels-deploy). A TypeScript implementation of this proxy is exported from the `fuels` package as `Src14OwnedProxy` and `Src14OwnedProxyFactory`.

The overall process is as follows:

1. Deploy your contract
1. Deploy the proxy contract
1. Set the target of the proxy contract to your deployed contract
1. Make calls to the contract via the proxy contract ID
1. Upgrade the contract by deploying a new version of the contract and updating the target of the proxy contract

> **Note**: When new storage slots are added to the contract, they must be initialized in the proxy contract before they can be read from. This can be done by first writing to the new storage slot in the proxy contract. Failure to do so will result in the transaction being reverted.
For example, lets imagine we want to deploy the following counter contract:

<<< @/../../docs/sway/counter/src/main.sw#proxy-1{rs:line-numbers}

Let's deploy and interact with it by proxy. First let's setup the environment and deploy the counter contract:

<<< @./snippets/proxy-contracts.ts#proxy-2{ts:line-numbers}

Now let's deploy the [SRC14 compliant proxy contract](https://github.com/FuelLabs/sway-standard-implementations/tree/174f5ed9c79c23a6aaf5db906fe27ecdb29c22eb/src14/owned_proxy/contract/out/release) and initialize it by setting its target to the counter target ID.

<<< @./snippets/proxy-contracts.ts#proxy-3{ts:line-numbers}

Finally, we can call our counter contract using the contract ID of the proxy.

<<< @./snippets/proxy-contracts.ts#proxy-4{ts:line-numbers}

Now let's make some changes to our initial counter contract by adding an additional storage slot to track the number of increments and a new get method that retrieves its value:

<<< @/../../docs/sway/counter-v2/src/main.sw#proxy-5{rs:line-numbers}

We can deploy it and update the target of the proxy like so:

<<< @./snippets/proxy-contracts.ts#proxy-6{ts:line-numbers}

Then, we can instantiate our upgraded contract via the same proxy contract ID:

<<< @./snippets/proxy-contracts.ts#proxy-7{ts:line-numbers}

For more info, please check these docs:

Expand Down
106 changes: 106 additions & 0 deletions apps/docs/src/guide/contracts/snippets/proxy-contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// #region proxy-2
import {
Provider,
Wallet,
Src14OwnedProxy,
Src14OwnedProxyFactory,
} from 'fuels';

import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env';
import {
Counter,
CounterFactory,
CounterV2,
CounterV2Factory,
} from '../../../typegend';

const provider = await Provider.create(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const counterContractFactory = new CounterFactory(wallet);
const deploy = await counterContractFactory.deploy();
const { contract: counterContract } = await deploy.waitForResult();
// #endregion proxy-2

// #region proxy-3
/**
* It is important to pass all storage slots to the proxy in order to
* initialize the storage slots.
*/
const storageSlots = counterContractFactory.storageSlots.concat(
Src14OwnedProxy.storageSlots
);
/**
* These configurables are specific to our recommended SRC14 compliant
* contract. They must be passed on deployment and then `initialize_proxy`
* must be called to setup the proxy contract.
*/
const configurableConstants = {
INITIAL_TARGET: { bits: counterContract.id.toB256() },
INITIAL_OWNER: {
Initialized: { Address: { bits: wallet.address.toB256() } },
},
};

const proxyContractFactory = new Src14OwnedProxyFactory(wallet);
const proxyDeploy = await proxyContractFactory.deploy({
storageSlots,
configurableConstants,
});

const { contract: proxyContract } = await proxyDeploy.waitForResult();
const { waitForResult } = await proxyContract.functions
.initialize_proxy()
.call();

await waitForResult();
// #endregion proxy-3

// #region proxy-4
/**
* Make sure to use only the contract ID of the proxy when instantiating
* the contract as this will remain static even with future upgrades.
*/
const proxiedContract = new Counter(proxyContract.id, wallet);

const incrementCall = await proxiedContract.functions.increment_count(1).call();
await incrementCall.waitForResult();

const { value: count } = await proxiedContract.functions.get_count().get();
// #endregion proxy-4

console.log('count:', count.toNumber() === 1);

// #region proxy-6
const deployV2 = await CounterV2Factory.deploy(wallet);
const { contract: contractV2 } = await deployV2.waitForResult();

const updateTargetCall = await proxyContract.functions
.set_proxy_target({ bits: contractV2.id.toB256() })
.call();

await updateTargetCall.waitForResult();
// #endregion proxy-6

// #region proxy-7
/**
* Again, we are instantiating the contract with the same proxy ID
* but using a new contract instance.
*/
const upgradedContract = new CounterV2(proxyContract.id, wallet);

const incrementCall2 = await upgradedContract.functions
.increment_count(1)
.call();

await incrementCall2.waitForResult();

const { value: increments } = await upgradedContract.functions
.get_increments()
.get();

const { value: count2 } = await upgradedContract.functions.get_count().get();
// #endregion proxy-7

console.log('secondCount', count2.toNumber() === 2);
console.log('increments', increments);
1 change: 1 addition & 0 deletions apps/docs/sway/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"call-test-script",
"configurable-pin",
"counter",
"counter-v2",
"echo-asset-id",
"echo-bytes",
"echo-configurables",
Expand Down
7 changes: 7 additions & 0 deletions apps/docs/sway/counter-v2/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "counter-v2"

[dependencies]
52 changes: 52 additions & 0 deletions apps/docs/sway/counter-v2/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// #region proxy-5
contract;

abi Counter {
#[storage(read)]
fn get_count() -> u64;

#[storage(read)]
fn get_increments() -> u64;

#[storage(write, read)]
fn increment_count(amount: u64) -> u64;

#[storage(write, read)]
fn decrement_count(amount: u64) -> u64;
}

storage {
counter: u64 = 0,
increments: u64 = 0,
}

impl Counter for Contract {
#[storage(read)]
fn get_count() -> u64 {
storage.counter.try_read().unwrap_or(0)
}

#[storage(read)]
fn get_increments() -> u64 {
storage.increments.try_read().unwrap_or(0)
}

#[storage(write, read)]
fn increment_count(amount: u64) -> u64 {
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current + amount);

let current_iteration: u64 = storage.increments.try_read().unwrap_or(0);
storage.increments.write(current_iteration + 1);

storage.counter.read()
}

#[storage(write, read)]
fn decrement_count(amount: u64) -> u64 {
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current - amount);
storage.counter.read()
}
}
// #endregion proxy-5
8 changes: 5 additions & 3 deletions apps/docs/sway/counter/src/main.sw
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// #region proxy-1
contract;

abi Counter {
Expand All @@ -18,20 +19,21 @@ storage {
impl Counter for Contract {
#[storage(read)]
fn get_count() -> u64 {
storage.counter.read()
storage.counter.try_read().unwrap_or(0)
}

#[storage(write, read)]
fn increment_count(amount: u64) -> u64 {
let current = storage.counter.read();
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current + amount);
storage.counter.read()
}

#[storage(write, read)]
fn decrement_count(amount: u64) -> u64 {
let current = storage.counter.read();
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current - amount);
storage.counter.read()
}
}
// #endregion proxy-1
46 changes: 46 additions & 0 deletions packages/fuel-gauge/src/recipes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { hexlify, randomBytes, Src14OwnedProxy, Src14OwnedProxyFactory } from 'fuels';
import { launchTestNode } from 'fuels/test-utils';

/**
* @group node
* @group browser
*/
describe('recipes', () => {
it('deploy and interact with Src14OwnedProxy', async () => {
using launched = await launchTestNode();

const {
wallets: [wallet],
} = launched;

const targetAddress = hexlify(randomBytes(32));
const configurableConstants = {
INITIAL_TARGET: { bits: targetAddress },
INITIAL_OWNER: { Initialized: { Address: { bits: wallet.address.toB256() } } },
};

const proxyFactory = new Src14OwnedProxyFactory(wallet);
const { waitForResult: waitForProxyDeploy } = await proxyFactory.deploy({
configurableConstants,
});
const { contract: proxyContract } = await waitForProxyDeploy();
const { waitForResult: waitForProxyInit } = await proxyContract.functions
.initialize_proxy()
.call();
await waitForProxyInit();
const proxyAddress = proxyContract.id.toB256();

const { waitForResult: waitForFirstTarget } = await proxyContract.functions
.proxy_target()
.call();
const firstTarget = await waitForFirstTarget();
expect(firstTarget.value.bits).toEqual(targetAddress);

const anotherProxy = new Src14OwnedProxy(proxyAddress, wallet);
const { waitForResult: waitForAnotherTarget } = await anotherProxy.functions
.proxy_target()
.call();
const anotherTarget = await waitForAnotherTarget();
expect(anotherTarget.value.bits).toEqual(targetAddress);
});
});
4 changes: 2 additions & 2 deletions packages/fuels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@
"dist"
],
"scripts": {
"build": "run-s build:proxy build:package build:browser build:minified",
"build:proxy": "tsx scripts/build-proxy-contract.ts",
"build": "run-s build:package build:browser build:minified",
"build:package": "tsup",
"build:browser": "pnpm vite build",
"build:minified": "pnpm uglifyjs --compress --mangle --output dist/browser.min.mjs -- dist/browser.mjs",
Expand All @@ -79,6 +78,7 @@
"@fuel-ts/transactions": "workspace:*",
"@fuel-ts/utils": "workspace:*",
"@fuel-ts/versions": "workspace:*",
"@fuel-ts/recipes": "workspace:*",
"bundle-require": "^5.0.0",
"chalk": "4",
"chokidar": "^3.6.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/fuels/src/cli/commands/deploy/deployContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { WalletUnlocked } from '@fuel-ts/account';
import { ContractFactory } from '@fuel-ts/contract';
import type { DeployContractOptions } from '@fuel-ts/contract';
import { Contract } from '@fuel-ts/program';
import { Src14OwnedProxy, Src14OwnedProxyFactory } from '@fuel-ts/recipes';
import { existsSync, readFileSync } from 'fs';

import {
Expand All @@ -20,7 +21,6 @@ import { debug, log } from '../../utils/logger';

import { createWallet } from './createWallet';
import { getDeployConfig } from './getDeployConfig';
import { Src14OwnedProxy, Src14OwnedProxyFactory } from './proxy/types';

/**
* Deploys one contract.
Expand Down
2 changes: 0 additions & 2 deletions packages/fuels/src/cli/commands/deploy/proxy/.gitignore

This file was deleted.

Loading

0 comments on commit 0d4ade1

Please sign in to comment.