# BotSwarm - a typescript library for scheduling onchain transactions Source: https://github.com/nounish/botswarm ## Summary BotSwarm is a typesafe TypeScript library for scheduling onchain transactions, with tools for building Farcaster bots that react to smart contract events. Its Ethereum adapter, built on Viem, lets users watch contract events, read and write contracts, and schedule contract calls to execute after a given block number. Failed tasks are retried a few blocks later once, then removed from the queue if they fail again, and tasks are cached to .botswarm/cache.json. The Farcaster adapter wraps farcaster-js to support casting, reacting, and profile updates. ## Article ██████╗ ██████╗ ████████╗███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗ ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║ ██████╔╝██║ ██║ ██║ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║ ██╔══██╗██║ ██║ ██║ ╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║ ██████╔╝╚██████╔╝ ██║ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ Getting Started Installation Configuration Usage Ethereum Reacting to onchain events Read and writing to contracts Scheduling tasks Hooks Usage with Viem Farcaster Casting Reacting Update Profile Logging Full BotSwarm API Getting Started BotSwarm is a typesafe library for scheduling onchain transactions. It also includes tools for creating Farcaster bots that can react to events emitted by smart contracts. To get started you can Clone our implementation of BotSwarm Check out some examples Follow along with the documentation below Installation Create a new NPM project and run npm i @federationwtf/botswarm Configuration To initialize BotSwarm, simply call the BotSwarm function with an optional config. import BotSwarm from "@federationwtf/botswarm"; const bot = BotSwarm({ /*** Optional (defaults) ***/ log: true, // Log status updates to the terminal }); Usage BotSwarm provides two adapters you can use, Ethereum and Farcaster each with their own platform specific actions const { Ethereum, Farcaster } = BotSwarm(); Check out the full api for all BotSwarm features Ethereum The Ethereum adapter allows you to interact with contracts on any EVM compatible chain. When passing in the ABI you must declare it as const or it won't be typesafe. BotSwarm uses Viem under the hood and the deployment networks are extended from it. const ethereum = Ethereum({ /*** Required ***/ contracts: { MyContract: { // The contract name abi: [...] as const, // The contract abi deployments: { mainnet: "0xA...A" // The network and address of the deployment } }, } privateKey: process.env.ETHEREUM_PRIVATE_KEY, /*** Optional (defaults) ***/ cacheTasks: true, // Cache tasks to .botswarm/cache.json rps: { // Override public rpcs for clients and wallets mainnet: "https://rpc.flashbots.net", }, gasLimitBuffer: 30000, // Increases the gas limit of a transaction by the buffer amount in gas units blockExecutionBuffer: 0 // Delays execution of all tasks by a certain number of blocks }); We provide some premade contracts from Federation and Nouns import BotSwarm from "@federationwtf/botswarm"; import { // Federation FederationNounsPool, // Nouns NounsDAOLogicV3, NounsAuctionHouse, NounsDAOExecutor, NounsDescriptor, NounsSeeder, NounsToken } from "@federationwtf/botswarm/contracts"; const { Ethereum } = BotSwarm(); const ethereum = Ethereum({ contracts: { FederationNounsPool, NounsDAOLogicV3, NounsAuctionHouse, NounsDAOExecutor, NounsDescriptor, NounsSeeder, NounsToken } privateKey: process.env.ETHEREUM_PRIVATE_KEY, }); Check out the full api for all Ethereum features Reacting to onchain events To react to onchain events you can use onBlock or watch. The onBlock function takes in a chain and a callback which will be called on every new block. The watch function takes in the contract name, chain, event name, and a callback. These values are typesafe and are derived from the contracts in the Ethereum configuration. Once a BidPlaced event is picked up BotSwarm will run the callback. The event object returned by the watch callback is a Viem Log. const { onBlock, watch } = Ethereum({ contracts: { FederationNounsPool } }); onBlock("mainnet", async (block) => { console.log(`Current block: ${block}`); }) watch({ contract: "FederationNounsPool", chain: "mainnet", event: "BidPlaced", }, async (event) => { console.log("A new bid was placed!"); }) Read and writing to contracts The Ethereum adapter also returns a read and write function for abitrary contract calls. These are typesafe wrappers around Viem's readContract and writeContract functions. const { read, write } = Ethereum({ contracts: { FederationNounsPool } }); const { castWindow } = await read({ contract: "FederationNounsPool", chain: "mainnet", functionName: "getConfig", }); const hash = await write({ contract: "FederationNounsPool", chain: "mainnet", functionName: "castVote", args: [325] }); The write function also includes gas related options that can be overriden for more control over the transaction. const hash = await write({ contract: "FederationNounsPool", chain: "mainnet", functionName: "castVote", args: [325], maxPriorityFeePerGas: 10, maxFeePerGas: 30, gasLimit: 300000 }); Scheduling tasks Tasks are specified contract calls to be executed after a given block. To add a task call addTask which takes in a block number contract call details which mimic the parameters of the write function used above. BotSwarm will watch the specified chain and call the write function when the current block is >= the block passed into addTask. Below is an example of our implementation of this to cast a FederationNounsPool vote result to NounsDAO before the proposal ends. If task execution fails then BotSwarm will make a second attempt and reschedule it a few blocks after. If the execution fails a second time then the task will be removed from the queue. All tasks are cached to .botswarm/cache.json when added or removed. BotSwarm will load all cached tasks when restarted. import BotSwarm from "@federationwtf/botswarm"; import { FederationNounsPool, NounsDAOLogicV3 } from "@federationwtf/botswarm/contracts"; const { Ethereum } = BotSwarm(); const { addTask, read, watch } = Ethereum({ contracts: { FederationNounsPool, NounsDAOLogicV3 }, privateKey: process.env.ETHEREUM_PRIVATE_KEY }); watch( { contract: "FederationNounsPool", chain: "mainnet", event: "BidPlaced" }, async (event) => { const { castWindow } = await read({ contract: "FederationNounsPool", chain: "mainnet", functionName: "getConfig", }); const { endBlock } = await read({ contract: "NounsDAOLogicV3", chain: "mainnet", functionName: "proposals", args: [event.args.propId], }); addTask({ block: endBlock - castWindow, contract: "FederationNounsPool", chain: "mainnet", functionName: "castVote", args: [event.args.propId], }); } ); Some usecases like MEV extraction might require that tasks execute as fast as possible. For situations like this you can specify the priorityFee (gwei) and maxBaseFeeForPriority (gwei) properties. If maxBaseFeeForPriority is less than the base fee at time of execution then BotSwarm will drop the priorityFee from the transaction. This is beneficial for scenarios where you still want the reliability of the transaction going through but also the ability to drop the priority fee if the base fee makes it unprofitable. addTask({ block: endBlock - castWindow, contract: "FederationNounsPool", chain: "mainnet", functionName: "castVote", args: [event.args.propId], priorityFee: 10, maxBaseFeeForPriority: 25, }); Execution Hooks Sometimes, there may be function arguments that need to be dynamically generated at time of exection. This could be because certain contract state needs to be retrieved at a block that didn't exist at the time of adding the task to the BotSwarm queue, or some other reason. To address this problem, BotSwarm provides a hooks property in the Ethereum adapter config which takes a key and a function (synchronous or asyncronous) that modifies and returns a task. These functions "hook" into the execution lifecycle of a task and are run prior to task exection. import BotSwarm from "@federationwtf/botswarm"; import { NounsDAOLogicV3 } from "@federationwtf/botswarm/contracts"; const { Ethereum } = BotSwarm(); const { addTask, read, watch } = Ethereum({ contracts: { FederationNounsPool, NounsDAOLogicV3 }, hooks: { getVoteSupport: async (task, block) => { const support = block % 2 === 0 ? 0 : 1; task.args.push(support); return task; } }, privateKey: process.env.ETHEREUM_PRIVATE_KEY }); watch( { contract: "NounsDAOLogicV3", chain: "mainnet", event: "VoteCast" }, async (event) => { const { endBlock } = await read({ contract: "NounsDAOLogicV3", chain: "mainnet", functionName: "proposals", args: [event.args.proposalId], }); addTask({ block: endBlock - 100, hooks: ["getVoteSupport"], contract: "NounsDAOLogicV3", chain: "mainnet", functionName: "castVote", args: [event.args.proposalId], }); } ); In this example the getVoteSupport function gets called right before the castVote task executes adding the support argument based on whether or not the current block is even or odd. The function args in the transaction will be [event.args.proposalId, support] when broadcasted to the network. Usage with Viem The BotSwarm Ethereum adapter uses Viem under the hood but it can be used directly by referencing the contracts object. import BotSwarm from "@federationwtf/botswarm"; import NounsPoolABI from "./contracts/NounsPool"; import { getContract } from "viem"; const { Ethereum } = BotSwarm() const { contracts, clients, wallets } = Ethereum({ NounsPool: { abi: NounsPoolABI, deployments: { mainnet: "0xBE5E6De0d0Ac82b087bAaA1d53F145a52EfE1642", }, }, }); const NounsPool = getContract({ address: contracts.NounsPool.deployments.mainnet.address, abi: NounsPoolABI, publicClient: clients.mainnet, walletClient: wallets.mainnet }) Farcaster The Farcaster adapter is a native wrapper around farcaster-js making it incredibly easy to create Farcaster bots. const farcaster = Farcaster({ fid: 16074, // @federation signerPrivateKey: process.env.FARCASTER_PRIVATE_KEY, rpc: "hub.rpc.url:2283", network: "mainnet" // Optional - "mainnet" | "testnet" | "devnet" defaults to "mainnet" }); Note: The signerPrivateKey is not your Farcaster nmemonic phrase. It is the private key of a generated signer for your account. To generate a signer run this script and follow the instructions. node node_modules/@federationwtf/botswarm/src/scripts/generateSigner.js Learn more about signers Check out the full api for all Farcaster features Casting Automating casts to the Farcaster network is as easy as calling cast along with the text. const { cast, reply, removeCast} = Farcaster({ ... }); const post = await cast("Wow, BotSwarm is pretty cool!"); if (post) { const postReply = reply("Yeah, automating my Farcaster posts is super simple!", post); } const postInChannel = await cast("This casts to a channel", { channel: "channel" }); We provide some built in popular channels from Warpcast. import { Nouns } from "@federationwtf/botswarm/channels"; const { cast } = Farcaster({ ... }); const postInChannel = await cast( "This casts to a channel", { channel: Nouns } ); Reacting If a post was successful, you can react to it by providing the returned post object along with the reaction type. const { cast, react } = Farcaster({ ... }); const post = await cast("This is a post"); if (post) { react(post, "like"); react(post, "recast"); } Update Profile To update your Farcaster profile you can call the updateProfile function and pass in any parameter you would like to change. const { updateProfile } = Farcaster({ ... }); updateProfile({ pfp: "https://link.to/image"; // Optional displayName: "Display Name"; // Optional bio: "A bio for your Farcaster profile"; // Optional url: "https://some.url/"; // Optional username: "username"; // Optional }) Logging If you would like to log custom data to the terminal you can import success, warn, error, and/or active which each take in a string. import BotSwarm from "@federationwtf/botswarm"; const { log } = BotSwarm(); log.success("This is a success!") // Will prepend with a green checkmark log.warn("This is a warning.") // Will prepend with a warning symbol log.error("This is probably bad.") // Will prepend with a red x log.active("Doing something") // Will change the spinner to blue Full BotSwarm API Below is a complete example of all of the components you can use to add advanced functionality to your bot. import BotSwarm from "@federationwtf/botswarm"; const { Ethereum, // Ethereum adapter Farcaster, // Farcaster adapter log: { success, // Will prepend with a green checkmark warn, // Will prepend with a warning symbol error, // Will prepend with a red x active, // Will change the spinner to blue colors // Internal colors used by the logger }, cache: { save, // Cache data to .botswarm/cache.json under a given key load, // Load cached data for a given key clear // Clear all the cache for a key or entirely } } = BotSwarm({ ... }); const { clients, // Viem public clients for each chain wallets, // Viem wallet clients for each chain contracts: config.contracts, // User defined contracts tasks, // Tasks that are currently executing rescheduled, // Tasks that have been rescheduled addTask, // Add a task getTask, // Get a task with an id removeTask, // Remove a task rescheduleTask, // Reschedule a task for a later block cacheTasks, // Cache tasks to .botswarm/cache.json execute, // Internal function used to execute tasks executing, // Tasks that are currently executing write, // Write to a contract onBlock, // Watch a block on a given chain watch, // Watch a contract event read, // Read a contract } = Ethereum({ ... }); const { client, // The Farcaster client signer, // The Farcaster signer cast, // Cast to Farcaster removeCast, // Remove a cast reply, // Reply to a cast react, // React to a cast removeReaction, // Remove a reaction from a cast updateProfile, // Update your Farcaster profile } = Farcaster({ ... })