From da5bbb373196a106fba8d29cf7e982f68b6f9af5 Mon Sep 17 00:00:00 2001 From: fubuloubu <3859395+fubuloubu@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:48:59 -0400 Subject: [PATCH] refactor(demo): create a local experiment, and a demo Silverback app --- bots/example.py | 49 +++++++++++++++++++++++ scripts/demo.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++ scripts/example.py | 55 -------------------------- 3 files changed, 145 insertions(+), 55 deletions(-) create mode 100644 bots/example.py create mode 100644 scripts/demo.py delete mode 100644 scripts/example.py diff --git a/bots/example.py b/bots/example.py new file mode 100644 index 0000000..4944e7c --- /dev/null +++ b/bots/example.py @@ -0,0 +1,49 @@ +import os +from datetime import timedelta + +from ape_ethereum import multicall +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"]) + +global db +# 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(_): + global db + + for stream in sm.active_streams(): + db.append(stream) + + db = sorted(db) + + +@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.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) diff --git a/scripts/demo.py b/scripts/demo.py new file mode 100644 index 0000000..e058d3c --- /dev/null +++ b/scripts/demo.py @@ -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.") diff --git a/scripts/example.py b/scripts/example.py deleted file mode 100644 index ad63941..0000000 --- a/scripts/example.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -A simple example showing how to use ApePay in a script. -""" - -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( - "--apepay", - "sm", - default="0xb5ed1ef2a90527b402cd7e7d415027cb94e1db4e", - callback=lambda c, p, v: StreamManager(address=v), -) -@click.option("--token", default="0xbc083d97825da7f7182f37fcec51818e196af1ff") -def cli(cli_ctx, network, sm, token): - if network.name != "sepolia-fork": - cli_ctx.abort("Currently, this script only works on sepolia-fork.") - - payer = cli_ctx.account_manager.test_accounts[0] - - # Make sure account can pay. - token = cli_ctx.chain_manager.contracts.instance_at(token) - - # Make sure your payer has 10k tokens. - balance = token.balanceOf(payer) - desired_balance = 10_000 * 10 ** token.decimals() - if balance < desired_balance: - difference = desired_balance - balance - token.DEBUG_mint(payer, difference, sender=payer) - - # Approve the amount it costs for the deployment. - # In this demo, we know it will add up to 26 tokens. - token.approve(sm.contract, 2**256 - 1, sender=payer) - decimals = token.decimals() - - # 26 tokens per day - seconds = timedelta(days=1).total_seconds() - tokens = 26 * 10**decimals - - # Create the stream. - stream = sm.create( - token, - int(tokens / seconds), - reason="1", # The ID of the deployment as a string - sender=payer, - ) - - click.echo(f"Stream '{stream.stream_id}' created successfully by '{stream.creator}'.")