Developing Bots
In this guide, we are going to show you more details on how to build an bot with Silverback.
Prerequisites
You should have a python project with Silverback installed.
You can install Silverback via pip install silverback
Project structure
There are 3 suggested ways to structure your project. In the root directory of your project:
Create a
bot.py
file. This is the simplest way to define your bot project.Create a
bots/
folder. Then develop bots in this folder as separate scripts (Do not include a init.py file).Create a
bot/
folder with a__init__.py
file that will include the instantiation of yourSilverbackBot()
object.
The silverback
cli automatically searches for python scripts to run as bots in specific locations relative to the root of your project.
It will also be able to detect the scripts inside your bots/
directory and let you run those by name (in case you have multiple bots in your project).
If silverback
finds a module named bot
in the root directory of the project, then it will use that by default.
Note
It is suggested that you create the instance of your SilverbackBot()
object by naming the variable bot
, since silverback
will autodetect that variable name when loading your script file.
Another way you can structure your bot is to create a bot
folder and define a runner inside of that folder as __init__.py
.
If you have a more complicated project that requires multiple bots, naming each bot their own individual name is okay to do, and we encourage you to locate them under the bots/
folder relative to the root of your project.
This will work fairly seamlessly with the rest of the examples shown in this guide.
To run a bot, as long as your project directory follows the suggestions above by using a bot
module, you can run it easily with:
silverback run --network your:network:of:choice
If your bot’s module name is example.py
(for example), you can run it like this:
silverback run example --network your:network:of:choice
If the variable that you call the SilverbackBot()
object is something other than bot
, you can specific that by adding :{variable-name}
:
silverback run example:my_bot --network your:network:of:choice
We will automatically detect all scripts under the bots/
folder automatically, but if your bot resides in a location other than bots/
then you can use this to run it:
silverback run folder.example:bot --network your:network:of:choice
Note that with a bot/__init__.py
setup, silverback will also autodetect it, and you can run it with:
silverback run --network your:network:of:choice
Note
It is suggested that you develop your bots as scripts to keep your deployments simple.
If you have a deep understanding of containerization, and have specific needs, you can set your bots up however you’d like, and then create your own container definitions for deployments to publish to your reqistry of choice.
For the most streamlined experience, develop your bots as scripts, and avoid relying on local packages
(e.g. do not include an __init__.py
file inside your bots/
directory, and do not use local modules inside bots/
for reusable code).
If you follow these suggestions, your Silverback deployments will be easy to use and require almost no thought.
Creating a Bot
Creating a Silverback Bot is easy, to do so initialize the silverback.SilverbackBot
class:
from silverback import SilverbackBot
bot = SilverbackBot()
The SilverbackBot
class handles state and configuration.
Through this class, we can hook up event handlers to be executed each time we encounter a new block or each time a specific event is emitted.
Initializing the bot creates a network connection using the Ape configuration of your local project, making it easy to add a Silverback bot to your project in order to perform automation of necessary on-chain interactions required.
However, by default an bot has no configured event handlers, so it won’t be very useful.
This is where adding event handlers is useful via the bot.on_
method.
This method lets us specify which event will trigger the execution of our handler as well as which handler to execute.
New Block Events
To add a block handler, you will do the following:
from ape import chain
@bot.on_(chain.blocks)
def handle_new_block(block):
...
Inside of handle_new_block
you can define any logic that you want to handle each new block
detected by the silverback client.
You can return any serializable data structure from this function and that will be stored in the results database as a trackable metric for the execution of this handler.
Any errors you raise during this function will get captured by the client, and recorded as a failure to handle this block
.
New Event Logs
Similarly to blocks, you can handle events emitted by a contract by adding an event handler:
from ape import Contract
TOKEN = Contract(<your token address here>)
@bot.on_(TOKEN.Transfer)
def handle_token_transfer_events(transfer):
...
Inside of handle_token_transfer_events
you can define any logic that you want to handle each new transfer
event that gets emitted by TOKEN.Transfer
detected by the silverback client.
Again, you can return any serializable data structure from this function and that will be stored in the results database as a trackable metric for the execution of this handler.
Any errors you raise during this function will get captured by the client, and recorded as a failure to handle this transfer
event log.
Startup and Shutdown
Worker Events
If you have heavier resources you want to load during startup, or want to initialize things like database connections, you can add a worker startup function like so:
@bot.on_worker_startup()
def handle_on_worker_startup(state):
# Connect to DB, set initial state, etc
...
@bot.on_worker_shutdown()
def handle_on_worker_shutdown(state):
# cleanup resources, close connections cleanly, etc
...
This function comes a parameter state
that you can use for storing the results of your startup computation or resources that you have provisioned.
It’s import to note that this is useful for ensuring that your workers (of which there can be multiple) have the resources necessary to properly handle any updates you want to make in your handler functions, such as connecting to the Telegram API, an SQL or NoSQL database connection, or something else. This function will run on every worker process.
New in 0.2.0: These events moved from on_startup()
and on_shutdown()
for clarity.
Worker State
The state
variable is also useful as this can be made available to each handler method so other stateful quantities can be maintained for other uses. Each distributed worker has its own instance of state.
To access the state from a handler, you must annotate context
as a dependency like so:
from typing import Annotated
from taskiq import Context, TaskiqDepends
@bot.on_(chain.blocks)
def block_handler(block, context: Annotated[Context, TaskiqDepends()]):
# Access state via context.state
...
Bot Events
You can also add an bot startup and shutdown handler that will be executed once upon every bot startup. This may be useful for things like processing historical events since the bot was shutdown or other one-time actions to perform at startup.
@bot.on_startup()
def handle_on_startup(startup_state):
# Process missed events, etc
# process_history(start_block=startup_state.last_block_seen)
# ...or startup_state.last_block_processed
...
@bot.on_shutdown()
def handle_on_shutdown():
# Record final state, etc
...
Changed in 0.2.0: The behavior of the @bot.on_startup()
decorator and handler signature have changed. It is now executed only once upon bot startup and worker events have moved on @bot.on_worker_startup()
.
Bot State
Sometimes it is very useful to have access to values in a shared state across your workers.
For example you might have a value or complex reference type that you wish to update during one of your tasks, and read during another.
Silverback provides bot.state
to help with these use cases.
For example, you might want to pre-populate a large dataframe into state on startup, keeping that dataframe in sync with the chain through event logs, and then use that data to determine a signal under which you want trigger transactions to commit back to the chain. Such an bot might look like this:
@bot.on_startup()
def create_table(startup_state):
df = contract.MyEvent.query(..., start_block=startup_state.last_block_processed)
... # Do some further processing on df
bot.state.table = df
@bot.on_(contract.MyEvent)
def update_table(log):
bot.state.table = ... # Update using stuff from `log`
@bot.on_(chain.blocks)
def use_table(blk):
if bot.state.table[...].mean() > bot.state.table[...].sum():
# Trigger your bot to send a transaction from `bot.signer`
contract.myMethod(..., sender=bot.signer)
...
Warning
You can use bot.state
to store any python variable type, however note that the item is not networked nor threadsafe so it is not recommended to have multiple tasks write to the same value in state at the same time.
Note
Bot startup and bot runtime event triggers (e.g. block or event container) are handled distinctly and can be trusted not to execute at the same time.
Signing Transactions
If configured, your bot with have bot.signer
which is an Ape account that can sign arbitrary transactions you ask it to.
To learn more about signing transactions with Ape, see the documentation.
Warning
While not recommended, you can use keyfile accounts for automated signing. See this guide to learn more about how to do that.
Running your Bot
Once you have programmed your bot, it’s really useful to be able to run it locally and validate that it does what you expect it to do.
To run your bot locally, we have included a really useful cli command run
that takes care of connecting to the proper network, configuring signers (using your local Ape accounts), and starting up the bot client and in-memory task queue workers.
# Run your bot on the Ethereum Sepolia testnet, with your own signer:
$ silverback run my_bot --network :sepolia --account acct-name
Note
my_bot:bot
is not required for silverback run if you follow the suggested folder structure at the start of this page, you can just call it via my_bot
.
It’s important to note that signers are optional, if not configured in the bot then bot.signer
will be None
.
You can use this in your bot to enable a “test execution” mode, something like this:
# Compute some metric that might lead to creating a transaction
if bot.signer:
# Execute a transaction via `sender=bot.signer`
else:
# Log what the transaction *would* have done, had a signer been enabled
Warning
If you configure your bot to use a signer, and that signer signs anything given to it, remember that you can lose substational amounts of funds if you deploy this to a production network. Always test your bots throughly before deploying, and always use a dedicated key for production signing with your bot in a remote setting.
Note
It is highly suggested to use a dedicated cloud signer plugin, such as ape-aws
for signing transactions in a cloud environment.
Use segregated keys and limit your risk by controlling the amount of funds that key has access to at any given time.
Distributed Execution
Using only the silverback run ...
command in a default configuration executes everything in one process and the job queue is completely in-memory with a shared state.
In some high volume environments, you may want to deploy your Silverback bot in a distributed configuration using multiple processes to handle the messages at a higher rate.
The primary components are the client and workers. The client handles Silverback events (blocks and contract event logs) and creates jobs for the workers to process in an asynchronous manner.
For this to work, you must configure a TaskIQ broker capable of distributed processing. Additonally, it is highly suggested you should also configure a TaskIQ result backend in order to process and store the results of executing tasks.
Note
Without configuring a result backend, Silverback may not work as expected since your tasks will now suddenly return None
instead of the actual result.
For instance, with taskiq_redis
you could do something like this for the client:
export SILVERBACK_BROKER_CLASS="taskiq_redis:ListQueueBroker"
export SILVERBACK_BROKER_KWARGS='{"queue_name": "taskiq", "url": "redis://127.0.0.1:6379"}'
export SILVERBACK_RESULT_BACKEND_CLASS="taskiq_redis:RedisAsyncResultBackend"
export SILVERBACK_RESULT_BACKEND_URI="redis://127.0.0.1:6379"
silverback run --network :mainnet:alchemy
And then the worker process with 2 worker subprocesses:
export SILVERBACK_BROKER_CLASS="taskiq_redis:ListQueueBroker"
export SILVERBACK_BROKER_KWARGS='{"url": "redis://127.0.0.1:6379"}'
export SILVERBACK_RESULT_BACKEND_CLASS="taskiq_redis:RedisAsyncResultBackend"
export SILVERBACK_RESULT_BACKEND_URI="redis://127.0.0.1:6379"
silverback worker -w 2
The client will send tasks to the 2 worker subprocesses, and all task queue and results data will be go through Redis.
Testing your Bot
TODO: Add backtesting mode w/ silverback test
Deploying your Bot
Check out the Platform Deployment Userguide for more information on how to deploy your bot to the Silverback Platform.