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/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 EOS 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 ouside 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
}
By default, smart wallets (wallets created without a private key) will use gas sponsoring, allowing you to not 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 need to deal with 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/dashboard/compose/{appName}
and then transfer gas tokens to that wallet through a wallet or an exchange.
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.
import { TaskContext } from "compose";
export async function main({ evm, env }: TaskContext) {
// disable gas sponsoring for a smart wallet (default is true for smart wallets)
const wallet = await evm.wallet({ name: "self-funded-wallet", sponsorGas: false });
}
Gas sponsoring is currently only supported on smart wallets (wallets created without a private key). Private key (EOA) wallets must be funded with native gas tokens directly.
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
}