Skip to content

nounish/botswarm

Repository files navigation

██████╗  ██████╗ ████████╗███████╗██╗    ██╗ █████╗ ██████╗ ███╗   ███╗
██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║    ██║██╔══██╗██╔══██╗████╗ ████║
██████╔╝██║   ██║   ██║   ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║
██╔══██╗██║   ██║   ██║   ╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║
██████╔╝╚██████╔╝   ██║   ███████║╚███╔███╔╝██║  ██║██║  ██║██║ ╚═╝ ██║
╚═════╝  ╚═════╝    ╚═╝   ╚══════╝ ╚══╝╚══╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝

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

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({ ... })

Releases

No releases published

Packages

No packages published