Skip to main content

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.

There are several types of wallets and wallet behaviors that compose supports.

Smart wallets

If you just need to interact with smart contracts, then the easiest thing to do is just build one of our smart wallets. You’ll be able to see all of your wallets in the dashboard at https://app.goldsky.com/{projectId}/dashboard/compose/{appName}.

Create a smart wallet

import { TaskContext } from "compose";

export async function main({ evm, env }: TaskContext) {
  // this is idempotent so the wallet is only created the first time this is called and is "retrieved" after that.
  // passing no args will create a "default" wallet for your app
  const wallet = await evm.wallet();
  console.log(wallet.address);

  // you can create multiple named wallets too.  This will generate multiple saved smart wallets that can be referenced by name in different 
  // tasks and task runs
  const walletOne = await evm.wallet({ name: "wallet-one" });
  const walletTwo = await evm.wallet({ name: "wallet-two" });

  // private key wallet
  const privateKeyWallet = await evm.wallet({ privateKey: env.MY_KEY });

  // now you can use these wallet to make transactions (see below for details)
}

Using wallets

Once you have a wallet created you can use it to write to smart contracts, see the full smart contract docs for more details
import { TaskContext } from "compose";

export async function main({ evm, env, fetch }: TaskContext) {
  const wallet = await evm.wallet();

  const response = await fetch<{ bitcoin: { usd: number } }>(
    "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
  );

  // Convert timestamp and price to bytes32 format
  const timestamp = Date.now();
  const bitcoinPrice = response.bitcoin.usd;
  const timestampAsBytes32 = `0x${timestamp.toString(16).padStart(64, "0")}`;
  const priceAsBytes32 = `0x${Math.round(bitcoinPrice * 100).toString(16).padStart(64, "0")}`;

  const bitcoinOracleContract = new evm.contracts.BitcoinOracleContract(
    env.ORACLE_ADDRESS,
    evm.chains.base,
    wallet
  );
  const { hash } = await bitcoinOracleContract.setPrice(
    timestampAsBytes32,
    priceAsBytes32
  );
}
You can also make or simulate transactions with methods on the Wallet class:
import { TaskContext } from "compose";

export async function main({ evm, env }: TaskContext) {
  const wallet = await evm.wallet();

  const resultId = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
  const payouts = [1000n, 2000n, 3000n];

  const { hash, receipt, userOpHash } = await wallet.writeContract(
    evm.chains.polygon,
    env.CONTRACT_ADDRESS as `0x${string}`,
    "reportPayouts(bytes32,uint256[])",
    [resultId, payouts],
    {
      confirmations: 3, // this will not resolve the promise until the transaction has been seen in 3 blocks
      onReorg: {
        // this will replay the transaction with new nonce and new gas if it's reorged later on after the three confirmations have passed
        // see "Reorg Handling" for more info
        action: {
          type: "replay",
        },
        depth: 200,
      },
    }
  );

  // userOpHash is set for gas-sponsored transactions (ERC-4337 UserOperation hash)
  // useful for debugging on bundler explorers
  if (userOpHash) {
    console.log(`UserOp hash: ${userOpHash}`);
  }
}

sendTransaction

For lower-level control, use sendTransaction to send a transaction with pre-encoded calldata. This is useful when you need to encode the transaction data yourself, pass specific gas parameters, or interact with contracts in ways that writeContract doesn’t cover.
import { TaskContext } from "compose";

export async function main({ evm, env }: TaskContext) {
  const wallet = await evm.wallet();

  // encode your calldata however you like
  const data = "0x..." as `0x${string}`;

  const { hash, receipt } = await wallet.sendTransaction(
    {
      to: env.CONTRACT_ADDRESS as `0x${string}`,
      data,
      chain: evm.chains.ethereum,
    },
    { confirmations: 3 }
  );

  console.log(`Transaction confirmed: ${hash}, status: ${receipt.status}`);
}
You can also pass explicit gas parameters for full control over fees and gas limits:
const { hash, receipt } = await wallet.sendTransaction(
  {
    to: env.CONTRACT_ADDRESS as `0x${string}`,
    data,
    chain: evm.chains.ethereum,
    value: 0n,                        // ETH value to send with the transaction
    gas: 500000n,                     // gas limit
    maxFeePerGas: 30000000000n,       // EIP-1559 max fee per gas
    maxPriorityFeePerGas: 1000000000n, // EIP-1559 priority fee
    nonce: 42,                        // explicit nonce (EOA wallets only)
  },
  {
    confirmations: 5,
    onReorg: {
      depth: 200,
      action: { type: "replay" },
    },
  }
);
The nonce parameter is only supported with EOA (private key) wallets. Smart wallets manage nonces internally.

getBalance

Check the native token balance of any wallet:
import { TaskContext } from "compose";

export async function main({ evm }: TaskContext) {
  const wallet = await evm.wallet();

  // Returns balance in wei as a string
  const balance = await wallet.getBalance(evm.chains.ethereum);
  console.log(`Balance: ${balance} wei`);
}

Wallet properties

Every wallet exposes name and address as read-only properties:
const wallet = await evm.wallet({ name: "my-wallet" });
console.log(wallet.name);    // "my-wallet"
console.log(wallet.address); // "0x..."

EOA wallets

You can also use EOAs that you already own, this allows you to self-fund gas, send and receive tokens in your tasks, and interact with smart contracts in which an EOA you already own has privileges on particular contract methods. Currently we support storing your EOA private key in Compose’s secret management system, but in the future you’ll be able to use private keys that are secured within TEEs. EOA wallets never pass their private keys outside of the task process and they sign requests passed in unsigned from the host process. When tasks run in TEEs they’ll be able to use private keys very securely within the TEE, never exposing it to any part of the stack outside of the TEE. This will empower Compose to run the most security sensitive use cases. First, you’ll need to store the private key in Goldsky’s secret management system and reference it in your compose.yaml file. You can see details on how to do that in the Secrets docs. Once you have your private key secret stored, you can use it to create a wallet:
import { TaskContext } from "compose";

export async function main({ evm, env }: TaskContext) {
  const privateKeyWallet = await evm.wallet({ privateKey: env.MY_PRIVATE_KEY_SECRET });

  // now you can use the wallet the same as any other wallet
}

Gas sponsoring

By default, smart wallets (wallets created without a private key) use gas sponsoring so you don’t have to think about managing gas funding. Smart wallets use EIP-7702 delegation for account abstraction, with gas costs handled through ERC-4337 UserOperations. You pay the gas bill as part of your normal monthly Goldsky bill, avoiding the complex budgetary and tax issues of purchasing gas tokens. When you don’t use gas sponsoring, you’ll need to get your wallet address from the compose dashboard at https://app.goldsky.com/{projectId}/dashboard/compose/{appName} and then transfer gas tokens to that wallet through a wallet or an exchange.

Gas sponsoring with EOA (private key) wallets

You can also opt into gas sponsoring for EOA wallets by passing sponsorGas: true when creating the wallet. Compose delegates the EOA via EIP-7702 on first use and routes transactions through ERC-4337 UserOperations, just like smart wallets — you keep full control of the key and signing, but you don’t have to fund the wallet with native gas tokens.
import { TaskContext } from "compose";

export async function main({ evm, env }: TaskContext) {
  const wallet = await evm.wallet({
    privateKey: env.MY_PRIVATE_KEY,
    sponsorGas: true,
  });

  // transactions now go through the sponsored UserOp flow
  // you pay for gas on your monthly Goldsky bill instead of from this EOA
}
Sponsored EOA transactions only run in deployed apps. Running with sponsorGas: true locally will throw a clear error that tells you to either remove sponsorGas, fund the wallet manually, or re-run with --fork-chains to exercise the sponsored flow against a forked chain. In --fork-chains mode the transaction is executed against the local fork (where gas is free), and you’ll see a warning reminding you the sponsored flow only takes effect once deployed to cloud.

Gas usage in the dashboard

Every sponsored or self-paid transaction emits a run event with evm.gas_used and evm.total_cost_wei attributes (both decimal strings in wei) so you can audit per-transaction gas spend from the Compose dashboard at https://app.goldsky.com/{projectId}/dashboard/compose/{appName}. On OP Stack L2s (Lisk, Base, Optimism, and friends) evm.total_cost_wei includes the L1 data fee, which typically dominates the total cost — reading gasUsed × effectiveGasPrice from the receipt alone will under-report by roughly two orders of magnitude.

Gas pricing

For non-sponsored transactions, Compose uses automatic gas estimation with sensible defaults. If you need precise control over gas parameters, use sendTransaction instead of writeContract and specify maxFeePerGas, maxPriorityFeePerGas, and gas explicitly.
writeContract automatically simulates the transaction before submitting it, catching revert errors early. sendTransaction does not simulate — it submits directly.

Override default gas sponsoring behavior

import { TaskContext } from "compose";

export async function main({ evm, env }: TaskContext) {
  // disable gas sponsoring on a smart wallet (default is true for smart wallets)
  const smartWallet = await evm.wallet({ name: "self-funded", sponsorGas: false });

  // enable gas sponsoring on an EOA wallet (default is false for EOA wallets)
  const sponsoredEoa = await evm.wallet({
    privateKey: env.MY_PRIVATE_KEY,
    sponsorGas: true,
  });
}

Full wallet interface

export interface WalletConfig {
  name?: string; // defaults to "default"
  privateKey?: string;
  sponsorGas?: boolean; // defaults to true if no privateKey and false if privateKey
}

export interface IWallet {
  readonly name: string;
  readonly address: `0x${string}`;
  writeContract(
    chain: Chain,
    contractAddress: `0x${string}`,
    functionSig: string,
    args: unknown[],
    confirmation?: TransactionConfirmation,
    retryConfig?: ContextFunctionRetryConfig
  ): Promise<{
    hash: string;
    receipt: TransactionReceipt;
    userOpHash?: string; // set for gas-sponsored transactions (ERC-4337)
  }>;
  readContract<T = unknown>(
    chain: Chain,
    contractAddress: `0x${string}`,
    functionSig: string,
    args: unknown[],
    retryConfig?: ContextFunctionRetryConfig
  ): Promise<T>;
  sendTransaction(
    config: {
      to: `0x${string}`;
      data: `0x${string}`;
      chain: Chain;
      value?: bigint;
      maxFeePerGas?: bigint;
      maxPriorityFeePerGas?: bigint;
      gas?: bigint;
      nonce?: number;
    },
    confirmation?: TransactionConfirmation,
    retryConfig?: ContextFunctionRetryConfig
  ): Promise<{
    hash: string;
    receipt: TransactionReceipt;
    userOpHash?: string; // set for gas-sponsored transactions (ERC-4337)
  }>;
  simulate(
    chain: Chain,
    contractAddress: `0x${string}`,
    functionSig: string,
    args: unknown[],
    retryConfig?: ContextFunctionRetryConfig
  ): Promise<unknown>;
  getBalance(
    chain: Chain,
    retryConfig?: ContextFunctionRetryConfig
  ): Promise<string>; // native token balance in wei
}