Skip to main content
We covered how to set your own Environment Variables in the manifest but likely you’ll need to be working with smart contracts locally and on testnets. Additionally you’ll likely need to interact with APIs, both your own and third party ones, with different credentials and different URLs throughout the development lifecycle.

Chains and RPCs

There’s four primary chain environments Compose is equipped to support: Fully local (Foundry, Truffle, Hardhat, etc), Locally forked networks (powered by TEVM), testnets and mainnets.

Local Chains (Foundry, Truffle, Hardhat)

If you’re developing your compose app locally alongside a smart contract running on a local chain, you can customize the RPC endpoints that Compose uses read from and write to your contract. You can use hard coded values right in the code, or env variables configured in your Manifest.

Use a local RCP node in code:

If you’re just starting out your contract with local development, and building your Compose app at the same time, you can use a Custom chain object right in code. For full reference in custom chain configuration, go here. For full reference on interacting with contracts, go here.
/// <reference types="../../.compose/types.d.ts" />

export async function main(
  { evm, env }: TaskContext,
  _args: any
) {
  // This is a local-only private key I've funded on my test chain (thus safe to hard code)
  // For private keys used on mainnets and testnets, always use Secrets
  const privateKey = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

  const wallet = await evm.wallet({ privateKey });

  export const localChain: Chain = {
    id: 0,
    name: "foundry-local",
    testnet: true,
    nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
    rpcUrls: {
      default: { http: ["http://127.0.0.1:8545"] }, // this is the url used by your local Anvil (etc) node
      public:  { http: ["http://127.0.0.1:8545"] },
    },
    blockExplorers: {
      default: { name: "na", url: "http://127.0.0.1:8545" }, // not used for local dev, so you can pass in any value here
    },
  };

  const localContractAddress = "0x1234567890abcdef1234567890abcdef12345678";

  const { hash } = await wallet.writeContract(
    localChain,
    localContractAddress,
    "reportPayouts(bytes32,uint256[])",
    [resultId, payouts],
    { 
      confirmations: 3,
    }
  );
}

Use a local RPC only in the dev environment

If you already have your contract and compose app deployed but are iterating on the contract and the Compose App locally, you can use env variables to override the RPC only when running locally. Manifest
name: "my_app"
env: ## since we're only specifying a local version of the RPC_URL env var, it'll be undefined when deployed to cloud
  local:
    ## This is a local-only private key I've funded on my test chain (thus safe to hard code)
    ## For private keys used on mainnets and testnets, always use Secrets
    PRIVATE_KEY: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    RPC_URL: "http://127.0.0.1:8545"
    CONTRACT_ADDRESS: "0x67206e6E82FA1b11fd8C545Ad3422dBb1444E53C" // we can update this as we iterate on the contract locally
  cloud:
    CONTRACT_ADDRESS: "0x1234567890abcdef1234567890abcdef12345678" // we can update this when we deploy or contract on mainnet
tasks:
  - name: "bitcoin_oracle"
    path: "./src/tasks/bitcoin_oracle.ts"
    triggers:
      - type: "cron"
        expression: "* * * * *"
Task code
/// <reference types="../../.compose/types.d.ts" />

export async function main(
  { evm, env }: TaskContext,
  _args: any
) {
  // private key will be undefined in cloud (since env var isn't set for cloud) and the wallet will default to our auto-funded smart wallets
  // however you can use a mainnet private key if you want by saving it as a secret, see wallet docs for more info
  const wallet = await evm.wallet({ privateKey: env.PRIVATE_KEY });

  const chain = {
    ...evm.chains.base,
  };

  // override the chain's RPC only if the env var is set
  if (env.RPC_URL) {
    chain.rpcUrls = {
      default: { http: ["http://127.0.0.1:8545"] }, // this is the url used by your local Anvil (etc) node
      public:  { http: ["http://127.0.0.1:8545"] },
    };
  }

  const { hash } = await wallet.writeContract(
    chain,
    env.CONTRACT_ADDRESS, // use the env var for the contract address which we'll update as we iterate
    "reportPayouts(bytes32,uint256[])",
    [resultId, payouts],
    { 
      confirmations: 3,
    }
  );
}

Forking for local Compose development

If you’re just iterating on your Compose app but your smart contract isn’t changing then a good option for local development can be to use TEVM for forking. This is done by starting you’re app with the —fork option like so goldsky compose start --fork. When you use forking, everything in your code will be exactly the same locally as in cloud, but internally we’ll fork all the chains you interact with and we’ll fund all your wallets on the local fork for gas. This allows you to freely iterate on your compose app while testing against a cloned contract and it’s state.

BYO RPCs for mainnets and testnets

By default, Compose will use our internal edge RPCs and our gas-sponsored smart wallets. Allowing you to not worry about wallets, rpcs or gas funding, while giving your app the most optimal performance and durability. However, there may be times when you want to use your own RPC nodes or wallets, which is also fully supported. Below is an example of using your own RPCs and private keys. See Wallets and Chains for more details. Manifest
name: "my_app"
secrets: 
  ## these will be set with "goldsky compose secret set <<secret-name>> <<private-key>> prior to deploying, see secrets docs for more info
  - PROD_FUNDING_WALLET 
  - ALCHEMY_TOKEN
tasks:
  - name: "bitcoin_oracle"
    path: "./src/tasks/bitcoin_oracle.ts"
    triggers:
      - type: "cron"
        expression: "* * * * *"
Task code
/// <reference types="../../.compose/types.d.ts" />

export async function main(
  { evm, env }: TaskContext,
  _args: any
) {
  // env.PROD_FUNDING_WALLET was populated via the secrets reference in the manifest
  const wallet = await evm.wallet({ privateKey: env.PROD_FUNDING_WALLET });

  const chain = {
    ...evm.chains.base,
    rpcUrls = {
      default: { http: ["https://base-mainnet.g.alchemy.com/v2/${env.ALCHEMY_TOKEN}"] }, 
      public:  { http: ["https://base-mainnet.g.alchemy.com/v2/${env.ALCHEMY_TOKEN}"] },
    },
  };

  const { hash } = await wallet.writeContract(
    chain,
    "0x1234567890abcdef1234567890abcdef12345678",
    "reportPayouts(bytes32,uint256[])",
    [resultId, payouts],
    { 
      confirmations: 3,
    }
  );
}

Other environment use cases

Another common need for different environments is interacting with your own, or third party, APIs. For example, you may need to test your compose app against a locally running version of your API for local development. This is pretty straight forward when configuring env vars in your Manifest. Manifest
name: "my_app"
env:
  local:
    API_BASE: "http://localhost:4001"
  cloud:
    API_BASE: "https://api.mydomain.com"
tasks:
  - name: "bitcoin_oracle"
    path: "./src/tasks/bitcoin_oracle.ts"
    triggers:
      - type: "cron"
        expression: "* * * * *"
Task code
/// <reference types="../../.compose/types.d.ts" />

export async function main(
  { env, fetch }: TaskContext,
  _args: any
) {
  const apiEndpoint = `${env.API_BASE}/some/endpoint`;

  const resp = await fetch(apiEndpoint);
}

env.COMPOSE_ENV

In addition to environment variables you configure in your manifest, compose also provides a COMPOSE_ENV variable for conditional logic. Maybe you have business logic that only needs to run in certain environments, COMPOSE_ENV lets you key off the environment your running in for environment specific blocks of code.
/// <reference types="../../.compose/types.d.ts" />

export async function main(
  { env }: TaskContext,
  _args: any
) {
  if (env.COMPOSE_ENV === "cloud") {
    // cloud only logic can go here
  }
}