Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(py-sdk)!: clean up validators and bots #110

Merged
merged 12 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions bots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Silverback Example App

These apps are intended to be examples of how to work with your own deployment of the ApePay
protocol interacting your own infrastructure for a SaaS product, using Silverback for real-time
events. You can host these examples on the Silverback Cloud (https://silverback.apeworx.io) or
self-host.

## Stream Handling

ApePay has some different events that should trigger updates to your off-chain tracking of the
current status of a Stream, which should also affect the delivery of your product or service.

The lifecycle of a Stream starts with being created, after which funds can be added at any time to
extend the stream, and finally the Stream can be cancelled at any time (after an initial required
delay).

At each point in the Stream lifecycle, an update to an off-chain record should be created to ensure
consistency within your product and how it is managing aspects of your microservice architecture,
and your Silverback app can directly trigger changes in those other services or perform those
changes themselves.

One important thing to note is that while almost every part of the lifecycle has some sort of on-
chain action that occurs when a user performs an action, a Stream can also "run out of time" which
means you should notify your users as the deadline becomes near to prevent service interruptions,
and also necessitates performing "garbage collection" after the Streams run out completely in order
to remove the associated resources that are no longer being paid for (similar to when a Stream is
cancelled manually).

Lastly, it is important that you understand your own regulatory and reporting requirements and
implement those using a combination of specialized ApePay "validator" contracts as well as trigger-
ing manual cancellation (or review) of the services for breach of terms within your app.
These tasks require a key with access to the Owner role and may be best implemented as a separate
bot to ensure access control restrictions and not interfere with other running bots.

## Revenue Collection

One interesting perk of ApePay is that claiming a Stream (aka "earning revenue") is entirely left
to you as an action you can do at whatever frequency you desire (based on revenue cycles, gas
costs, etc.), and can integrate into other various parts of your revenue management systems, tax
tracking, or whatever else is needed to be accounted for (according to your business needs or
jurisdiction).

It is recommended that you implement this as a separate bot, as optimizing revenue operations can
be a great way to improve overall cost optimization, and also may require advanced access control
rights that your other microservices don't require. We provide an example here of what that might
look like within the example, however please note that a key that has the ability to make
transactions is required for production use.
47 changes: 47 additions & 0 deletions bots/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
from datetime import timedelta

from ape_ethereum import multicall
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not technically used yet, I had this in some of my other code for making claims I didn't publish yet

from silverback import SilverbackApp

from apepay import StreamManager

app = SilverbackApp()
# NOTE: You should use one bot per-supported network
# NOTE: This bot assumes you use a new bot per deployment
sm = StreamManager(os.environ["APEPAY_CONTRACT_ADDRESS"])

# NOTE: You would probably want to index this by network and deployment,
# if you were operating on multiple networks or deployments
db = []
# TODO: Migrate to `app.state.db` when feature becomes available


@app.on_startup()
async def load_db(_):
for stream in sm.active_streams():
db.append(stream)


@sm.on_stream_created(app)
async def grant_product(stream):
db.append(stream)
print(f"provisioning product for {stream.creator}")
return stream.time_left


@sm.on_stream_funded(app)
async def update_product_funding(stream):
# NOTE: properties of stream have changed, you may not need to handle this
# but typically you would want to update `stream.time_left` in record
db.remove(
next(s for s in db if s.stream_id == stream.stream_id and s.creator == stream.creator)
)
db.append(stream)
return stream.time_left


@sm.on_stream_cancelled(app)
async def revoke_product(stream):
print(f"unprovisioning product for {stream.creator}")
db.remove(stream)
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ bot = [
"silverback>=0.5,<1",
]
lint = [
"flake8",
"black",
"isort",
"mypy",
# NOTE: Be able to lint our silverback add-ons
"apepay[bot]"
]
test = [
"ape-foundry",
Expand Down
96 changes: 96 additions & 0 deletions scripts/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
A demo showing some accounts randomly creating, modifying, and cancelling streams
"""

import random
from datetime import timedelta

import click
from ape.cli import ConnectedProviderCommand, ape_cli_context

from apepay import StreamManager


@click.command(cls=ConnectedProviderCommand)
@ape_cli_context()
@click.option("-l", "--min-stream-life", default=0)
@click.option("-n", "--num-accounts", default=10)
@click.option("-b", "--num-blocks", default=1000)
@click.option("-m", "--max-streams", default=10)
@click.option("-c", "--create-stream", type=float, default=0.1)
@click.option("-f", "--fund-stream", type=float, default=0.7)
@click.option("-k", "--cancel-stream", type=float, default=0.2)
def cli(
cli_ctx,
min_stream_life,
num_accounts,
num_blocks,
max_streams,
create_stream,
fund_stream,
cancel_stream,
):
# Initialize experiment
deployer = cli_ctx.account_manager.test_accounts[-1]
token = cli_ctx.local_project.TestToken.deploy(sender=deployer)
sm = StreamManager(
cli_ctx.local_project.StreamManager.deploy(
deployer, min_stream_life, [], [token], sender=deployer
)
)

# Wait for user to start the example SB app...
click.secho(
f"Please run `APEPAY_CONTRACT_ADDRESS={sm.address} silverback run bots.example:app`",
fg="bright_magenta",
)
if not click.confirm("Start experiment?"):
return

# Make sure all accounts have some tokens
accounts = cli_ctx.account_manager.test_accounts[:num_accounts]
decimals = token.decimals()
for account in accounts:
token.DEBUG_mint(account, 10_000 * 10**decimals, sender=account)

# 26 tokens per day
starting_life = timedelta(minutes=5).total_seconds()
starting_tokens = 26 * 10**decimals
funding_amount = 2 * 10**decimals
streams = {a.address: [] for a in accounts}

while cli_ctx.chain_manager.blocks.head.number < num_blocks:
payer = random.choice(accounts)

# Do a little garbage collection
for stream in streams[payer.address]:
click.echo(f"{payer}:{stream.stream_id} - {stream.time_left}")
if not stream.is_active:
click.echo(f"Stream '{payer}:{stream.stream_id}' is expired, removing...")
streams[payer.address].remove(stream)

if len(streams[payer.address]) > 0:
stream = random.choice(streams[payer.address])

if token.balanceOf(payer) >= 10 ** (decimals + 1) and random.random() < fund_stream:
click.echo(
f"Stream '{payer}:{stream.stream_id}' is being funded "
f"w/ {funding_amount / 10**decimals:.2f} tokens..."
)
token.approve(sm.address, funding_amount, sender=payer)
stream.add_funds(funding_amount, sender=payer)

elif random.random() < cancel_stream:
click.echo(f"Stream '{payer}:{stream.stream_id}' is being cancelled...")
stream.cancel(sender=payer)
streams[payer.address].remove(stream)

elif token.balanceOf(payer) < starting_tokens:
continue

elif len(streams[payer.address]) < max_streams and random.random() < create_stream:
click.echo(f"'{payer}' is creating a new stream...")
token.approve(sm.address, starting_tokens, sender=payer)
stream = sm.create(token, int(starting_tokens / starting_life), sender=payer)
streams[payer.address].append(stream)
click.echo(f"Stream '{payer}:{stream.stream_id}' was created successfully.")
55 changes: 0 additions & 55 deletions scripts/example.py

This file was deleted.

Loading
Loading