-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(py-sdk)!: clean up validators and bots (#110)
* refactor(py-sdk): break out different classes into their own modules updates to some of the experience of using validators * chore: add flake8 * refactor(py-sdk): allow a better way to integrate w/ Silverback * fix: use new `.creation_metadata` API * refactor: allow using `StreamManager(address)` instead of `address=...` * refactor(demo): create a local experiment, and a demo Silverback app * docs(bot): add some documentation for the bot * tests: fix test for new StreamManager.__init__ * fix: remove `global` as bad practice * fix: remove stream before adding it * docs: fix name of platform * refactor(example): update comments and data structure used in bot
- Loading branch information
Showing
13 changed files
with
769 additions
and
675 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 Platform (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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import os | ||
from collections import defaultdict | ||
|
||
from ape.types import AddressType | ||
from silverback import SilverbackApp | ||
|
||
from apepay import Stream, StreamManager | ||
|
||
app = SilverbackApp() | ||
# NOTE: This bot assumes you use a new bot per ApePay deployment | ||
sm = StreamManager(os.environ["APEPAY_CONTRACT_ADDRESS"]) | ||
|
||
# NOTE: You would probably want to index your db by network and deployment address, | ||
# if you were operating on multiple networks and/or deployments (for easy lookup) | ||
db: defaultdict[AddressType, list[Stream]] = defaultdict(list) | ||
# TODO: Migrate to `app.state.db` when feature becomes available | ||
|
||
|
||
@app.on_startup() | ||
async def load_db(_): | ||
for stream in sm.active_streams(): | ||
while len(db[stream.creator]) < stream.stream_id: | ||
db[stream.creator].append(None) # Fill with empty values | ||
assert stream.stream_id == len(db[stream.creator]) | ||
db[stream.creator].append(stream) | ||
|
||
|
||
@sm.on_stream_created(app) | ||
async def grant_product(stream): | ||
assert stream.stream_id == len(db[stream.creator]) | ||
db[stream.creator].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 db for use in user Stream life notifications | ||
db[stream.creator].pop(stream.stream_id) | ||
db[stream.creator].insert(stream.stream_id, stream) | ||
return stream.time_left | ||
|
||
|
||
@sm.on_stream_cancelled(app) | ||
async def revoke_product(stream): | ||
print(f"unprovisioning product for {stream.creator}") | ||
db[stream.creator].pop(stream.stream_id) | ||
db[stream.creator].insert(stream.stream_id, None) | ||
return stream.time_left |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.