Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/book/src/forc/plugins/forc_client/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,37 @@ The proxy contract includes both its own storage slots and preserves the storage
- Proxy contracts work with both regular and [chunked contracts](#large-contracts) (contracts over 100<!-- markdownlint-disable-line MD032 -->kB)
- The implementation uses the SRC-14 standard for maximum compatibility with the Fuel ecosystem

### Multi-Network Proxy Support

For projects that need to deploy across multiple networks (testnet, mainnet, devnet, local), you can configure network-specific proxy addresses using the `addresses` table instead of a single `address` field:

```TOML
[project]
name = "test_contract"
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
implicit-std = false

[proxy]
enabled = true

[proxy.addresses]
testnet = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
mainnet = "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"
devnet = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
local = "0x567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef123456"
```

When using network-specific proxy addresses:
- `forc-deploy` automatically determines the current network and uses the appropriate proxy address
- If no proxy address is configured for the current network, a new proxy contract will be deployed
- The proxy address for the current network is automatically updated in `Forc.toml` after deployment

**Note:** The `address` and `addresses` fields are mutually exclusive. Use `address` for single-network deployments or `addresses` for multi-network setups.

This approach eliminates the need to manually update proxy addresses when switching between networks, making multi-network deployment workflows much more robust.

## Large Contracts

For contracts over the maximum contract size limit (currently `100<!-- markdownlint-disable-line -->kB`) defined by the network, `forc-deploy` will split the contract into chunks and deploy the contract with multiple transactions using the Rust SDK's [loader contract](https://github.com/FuelLabs/fuels-rs/blob/master/docs/src/deploying/large_contracts.md) functionality. Chunks that have already been deployed will be reused on subsequent deployments.
Expand Down
40 changes: 40 additions & 0 deletions forc-pkg/src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,43 @@ pub struct Proxy {
/// Points to the proxy contract to be updated with the new contract id.
/// If there is a value for this field, forc will try to update the proxy contract's storage
/// field such that it points to current contract's deployed instance.
///
/// This field provides backward compatibility for single-network deployments.
/// For multi-network deployments, use the `addresses` field instead.
pub address: Option<String>,
/// Network-specific proxy addresses for multi-network deployments.
/// Keys should be network names (e.g., "testnet", "mainnet", "devnet", "local").
/// This field is mutually exclusive with the `address` field.
pub addresses: Option<BTreeMap<String, String>>,
}

impl Proxy {
/// Validate the proxy configuration.
///
/// Ensures that `address` and `addresses` fields are mutually exclusive.
pub fn validate(&self) -> anyhow::Result<()> {
if self.address.is_some() && self.addresses.is_some() {
bail!("Proxy configuration cannot have both `address` and `addresses` fields. Use `address` for single-network deployments or `addresses` for multi-network deployments.");
}
Ok(())
}

/// Returns the proxy address for a specific network.
///
/// If `addresses` is configured, looks up the network-specific address.
/// Otherwise, falls back to the single `address` field for backward compatibility.
pub fn address_for_network(&self, network: &str) -> Option<&String> {
if let Some(addresses) = &self.addresses {
addresses.get(network)
} else {
self.address.as_ref()
}
}

/// Returns true if the proxy has any addresses configured (either single or multi-network).
pub fn has_address(&self) -> bool {
self.address.is_some() || self.addresses.is_some()
}
}

impl DependencyDetails {
Expand Down Expand Up @@ -705,6 +741,7 @@ impl PackageManifest {
/// 2. The validity of the details provided. Makes sure that there are no mismatching detail
/// declarations (to prevent mixing details specific to certain types).
/// 3. The dependencies listed does not have an alias ("package" field) that is the same as package name.
/// 4. The proxy configuration is valid (mutually exclusive address/addresses fields).
pub fn validate(&self) -> Result<()> {
validate_project_name(&self.project.name)?;
if let Some(ref org) = self.project.organization {
Expand All @@ -725,6 +762,9 @@ impl PackageManifest {
))
}
}
if let Some(ref proxy) = self.proxy {
proxy.validate()?;
}
Ok(())
}

Expand Down
109 changes: 67 additions & 42 deletions forc-plugins/forc-client/src/op/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
constants::TX_SUBMIT_TIMEOUT_MS,
util::{
account::ForcClientAccount,
pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest},
pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest_for_network},
target::Target,
tx::{
check_and_create_wallet_at_default_path, prompt_forc_wallet_password, select_account,
Expand Down Expand Up @@ -45,6 +45,34 @@ use std::{
};
use sway_core::{asm_generation::ProgramABI, language::parsed::TreeType, BuildTarget};

/// Resolves the proxy address for the current network context.
///
/// Uses the network name from chain info to look up network-specific proxy addresses,
/// falling back to the single address field for backward compatibility.
fn resolve_proxy_address_for_network(
proxy: &forc_pkg::manifest::Proxy,
chain_info: &fuels::types::chain_info::ChainInfo,
) -> Option<String> {
if !proxy.enabled {
return None;
}

let target = Target::from_str(&chain_info.name).unwrap_or_default();
let network_name = match target {
Target::Testnet => "testnet",
Target::Mainnet => "mainnet",
Target::Devnet => "devnet",
Target::Local => "local",
};

// First try network-specific addresses
if let Some(address) = proxy.address_for_network(network_name) {
Some(address.clone())
} else {
None
}
}

/// Default maximum contract size allowed for a single contract. If the target
/// contract size is bigger than this amount, forc-deploy will automatically
/// starts dividing the contract and deploy them in chunks automatically.
Expand Down Expand Up @@ -553,45 +581,45 @@ pub async fn deploy_contracts(
deploy_pkg(command, pkg, salt, &provider, &account).await?
};

// Get chain info to resolve network-specific proxy addresses
let chain_info = provider.chain_info().await?;
let proxy_id = match &pkg.descriptor.manifest_file.proxy {
Some(forc_pkg::manifest::Proxy {
enabled: true,
address: Some(proxy_addr),
}) => {
// Make a call into the contract to update impl contract address to 'deployed_contract'.

// Create a contract instance for the proxy contract using default proxy contract abi and
// specified address.
let proxy_contract =
ContractId::from_str(proxy_addr).map_err(|e| anyhow::anyhow!(e))?;

update_proxy_contract_target(&account, proxy_contract, deployed_contract_id)
Some(proxy) if proxy.enabled => {
if let Some(proxy_addr) = resolve_proxy_address_for_network(proxy, &chain_info) {
// Make a call into the contract to update impl contract address to 'deployed_contract'.

// Create a contract instance for the proxy contract using default proxy contract abi and
// specified address.
let proxy_contract =
ContractId::from_str(&proxy_addr).map_err(|e| anyhow::anyhow!(e))?;

update_proxy_contract_target(&account, proxy_contract, deployed_contract_id)
.await?;
Some(proxy_contract)
} else {
// No proxy address configured for this network, deploy a new one
let pkg_name = &pkg.descriptor.name;
let pkg_storage_slots = &pkg.storage_slots;
// Deploy a new proxy contract.
let deployed_proxy_contract = deploy_new_proxy(
command,
pkg_name,
pkg_storage_slots,
&deployed_contract_id,
&provider,
&account,
)
.await?;
Some(proxy_contract)
}
Some(forc_pkg::manifest::Proxy {
enabled: true,
address: None,
}) => {
let pkg_name = &pkg.descriptor.name;
let pkg_storage_slots = &pkg.storage_slots;
// Deploy a new proxy contract.
let deployed_proxy_contract = deploy_new_proxy(
command,
pkg_name,
pkg_storage_slots,
&deployed_contract_id,
&provider,
&account,
)
.await?;

// Update manifest file such that the proxy address field points to the new proxy contract.
update_proxy_address_in_manifest(
&format!("0x{deployed_proxy_contract}"),
&pkg.descriptor.manifest_file,
)?;
Some(deployed_proxy_contract)
// Update manifest file such that the proxy address field points to the new proxy contract.
// This will now use network-aware updating logic
update_proxy_address_in_manifest_for_network(
&format!("0x{}", deployed_proxy_contract),
&pkg.descriptor.manifest_file,
&chain_info,
)?;
Some(deployed_proxy_contract)
}
}
// Proxy not enabled.
_ => None,
Expand Down Expand Up @@ -621,12 +649,9 @@ async fn confirm_transaction_details(
.map(|pkg| {
tx_count += 1;
let proxy_text = match &pkg.descriptor.manifest_file.proxy {
Some(forc_pkg::manifest::Proxy {
enabled: true,
address,
}) => {
Some(proxy) if proxy.enabled => {
tx_count += 1;
if address.is_some() {
if proxy.has_address() {
" + update proxy"
} else {
" + deploy proxy"
Expand Down
65 changes: 65 additions & 0 deletions forc-plugins/forc-client/src/util/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::{collections::HashMap, path::Path, sync::Arc};
use fuels::types::chain_info::ChainInfo;
use crate::util::target::Target;
use std::str::FromStr;

/// The name of the folder that forc generated proxy contract project will reside at.
pub const GENERATED_CONTRACT_FOLDER_NAME: &str = ".generated_contracts";
Expand Down Expand Up @@ -38,6 +41,68 @@ pub(crate) fn update_proxy_address_in_manifest(
Ok(())
}

/// Updates the given package manifest file with network-specific proxy address.
/// This function determines the network from the chain info and updates the appropriate
/// network entry in the proxy.addresses table.
pub(crate) fn update_proxy_address_in_manifest_for_network(
address: &str,
manifest: &PackageManifestFile,
chain_info: &ChainInfo,
) -> Result<()> {
let mut toml = String::new();
let mut file = File::open(manifest.path())?;
file.read_to_string(&mut toml)?;
let mut manifest_toml = toml.parse::<toml_edit::DocumentMut>()?;

if manifest.proxy().is_some() {
// Determine network name from chain info
let target = Target::from_str(&chain_info.name).unwrap_or_default();
let network_name = match target {
Target::Testnet => "testnet",
Target::Mainnet => "mainnet",
Target::Devnet => "devnet",
Target::Local => "local",
};

// Check if we're using the new addresses format or need to convert
if manifest.proxy().unwrap().addresses.is_some() {
// Update network-specific address in addresses table
if !manifest_toml["proxy"]["addresses"].is_table() {
manifest_toml["proxy"]["addresses"] = toml_edit::table();
}
manifest_toml["proxy"]["addresses"][network_name] = toml_edit::value(address);
} else if manifest.proxy().unwrap().address.is_some() {
// Migration from single address to network-specific addresses
// Move current address to network-specific and add new one

// Remove the old single address field
if manifest_toml["proxy"]["address"].is_value() {
manifest_toml["proxy"]["address"] = toml_edit::Item::None;
}

// Create addresses table and add the new address for this network
manifest_toml["proxy"]["addresses"] = toml_edit::table();
manifest_toml["proxy"]["addresses"][network_name] = toml_edit::value(address);

// Optionally preserve the old address under a generic network name
// This is commented out to avoid confusion, but could be enabled if desired
// manifest_toml["proxy"]["addresses"]["previous"] = toml_edit::value(current_address);
} else {
// No address configured yet, create addresses table with this network
manifest_toml["proxy"]["addresses"] = toml_edit::table();
manifest_toml["proxy"]["addresses"][network_name] = toml_edit::value(address);
}

let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(manifest.path())?;
file.write_all(manifest_toml.to_string().as_bytes())?;
}

Ok(())
}

/// Creates a proxy contract project at the given path, adds a forc.toml and source file.
pub(crate) fn create_proxy_contract(pkg_name: &str) -> Result<PathBuf> {
// Create the proxy contract folder.
Expand Down
Loading
Loading