Documentation Index
Fetch the complete documentation index at: https://docs.goldsky.com/llms.txt
Use this file to discover all available pages before exploring further.
We covered how to set your own Environment Variables in the manifest, but you’ll likely need to work 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.
Compose supports two manifest environments: local (used when you run goldsky compose start or goldsky compose dev) and cloud (used when your app
runs on Compose’s infrastructure after goldsky compose deploy). The active environment is picked automatically based on whether the runtime is executing
locally or in the cloud — there is no --env flag on deploy.
Chains and RPCs
There are 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 to 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 RPC 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.
import { TaskContext, Chain } from "compose";
export async function main({ evm }: TaskContext) {
// 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 });
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" as `0x${string}`;
const resultId = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
const payouts = [1000n, 2000n, 3000n];
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 our contract to mainnet
tasks:
- name: "bitcoin_oracle"
path: "./src/tasks/bitcoin_oracle.ts"
triggers:
- type: "cron"
expression: "* * * * *"
Task code
import { TaskContext } from "compose";
export async function main({ evm, env }: TaskContext) {
// 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: [env.RPC_URL] }, // this is the url used by your local Anvil (etc) node
public: { http: [env.RPC_URL] },
};
}
const resultId = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
const payouts = [1000n, 2000n, 3000n];
const { hash } = await wallet.writeContract(
chain,
env.CONTRACT_ADDRESS as `0x${string}`, // 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 your app with the --fork-chains option like so: goldsky compose start --fork-chains. 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 its state.
import { TaskContext } from "compose";
export async function main({ evm }: TaskContext) {
// we'll auto-fund all smart wallets in forking mode so you can simulate gas sponsoring in prod
const wallet = await evm.wallet({ name: "my-smart-wallet" });
const resultId = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
const payouts = [1000n, 2000n, 3000n];
const { hash } = await wallet.writeContract(
// when you run compose with the --fork-chains flag we'll make a fork of base at the time of running the app and we'll clone all your existing contract state
// when you deploy this to cloud, it'll run against the actual base mainnet, thus you don't need any special dev code when testing locally with forking
evm.chains.base,
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" as `0x${string}`,
"reportPayouts(bytes32,uint256[])",
[resultId, payouts],
{
confirmations: 3,
}
);
}
BYO RPCs for mainnets and testnets
By default, Compose uses our internal edge RPCs and our gas-sponsored smart wallets, so you don’t need to 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:
# set these with "goldsky compose secret set <NAME> --value <VALUE>" prior to deploying, see secrets docs for more info
- PROD_FUNDING_WALLET
- ALCHEMY_TOKEN
env:
local:
ALCHEMY_BASEURL: "https://base-testnet.g.alchemy.com/v2/"
cloud:
ALCHEMY_BASEURL: "https://base-mainnet.g.alchemy.com/v2/"
tasks:
- name: "bitcoin_oracle"
path: "./src/tasks/bitcoin_oracle.ts"
triggers:
- type: "cron"
expression: "* * * * *"
Task code
import { TaskContext } from "compose";
export async function main({ evm, env }: TaskContext) {
// 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: [`${env.ALCHEMY_BASEURL}${env.ALCHEMY_TOKEN}`] },
public: { http: [`${env.ALCHEMY_BASEURL}${env.ALCHEMY_TOKEN}`] },
},
};
const resultId = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
const payouts = [1000n, 2000n, 3000n];
const { hash } = await wallet.writeContract(
chain,
"0x1234567890abcdef1234567890abcdef12345678" as `0x${string}`,
"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
import { TaskContext } from "compose";
export async function main({ env, fetch }: TaskContext) {
const apiEndpoint = `${env.API_BASE}/some/endpoint`;
const resp = await fetch(apiEndpoint);
}