diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..80dc447 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,68 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# Sample workflow for building and deploying a Jekyll site to GitHub Pages +name: Deploy Jekyll site to Pages + +on: + push: + branches: ["docs"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Build job + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: docs + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' # Not needed with a .ruby-version file + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + cache-version: 0 # Increment this number if you need to re-download cached gems + working-directory: '${{ github.workspace }}/docs' + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + - name: Build with Jekyll + # Outputs to the './_site' directory by default + run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" + env: + JEKYLL_ENV: production + - name: Upload artifact + # Automatically uploads an artifact from the './_site' directory by default + uses: actions/upload-pages-artifact@v3 + with: + path: docs/_site/ + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..918de83 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,6 @@ +_site +.sass-cache +.jekyll-cache +.jekyll-metadata +vendor +Gemfile.lock diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..086a5c9 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,25 @@ +--- +permalink: /404.html +layout: default +--- + + + +
+

404

+ +

Page not found :(

+

The requested page could not be found.

+
diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..6e2d920 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,35 @@ +source "https://rubygems.org" +# Hello! This is where you manage which Jekyll version is used to run. +# When you want to use a different version, change it below, save the +# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: +# +# bundle exec jekyll serve +# +# This will help ensure the proper Jekyll version is running. +# Happy Jekylling! +#gem "jekyll", "~> 4.3.4" +# This is the default theme for new Jekyll sites. You may change this to anything you like. +# gem "minima", "~> 2.5" +gem "just-the-docs" +# If you want to use GitHub Pages, remove the "gem "jekyll"" above and +# uncomment the line below. To upgrade, run `bundle update github-pages`. +# gem "github-pages", group: :jekyll_plugins +gem "github-pages", "~> 232", group: :jekyll_plugins +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.12" +end + +# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem +# and associated library. +platforms :mingw, :x64_mingw, :mswin, :jruby do + gem "tzinfo", ">= 1", "< 3" + gem "tzinfo-data" +end + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1", :platforms => [:mingw, :x64_mingw, :mswin] + +# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem +# do not have a Java counterpart. +gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..c486523 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,61 @@ +# Welcome to Jekyll! +# +# This config file is meant for settings that affect your whole blog, values +# which you are expected to set up once and rarely edit after that. If you find +# yourself editing this file very often, consider using Jekyll's data files +# feature for the data you need to update frequently. +# +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'bundle exec jekyll serve'. If you change this file, please restart the server process. +# +# If you need help with YAML syntax, here are some quick references for you: +# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml +# https://learnxinyminutes.com/docs/yaml/ +# +# Site settings +# These are used to personalize your new site. If you look in the HTML files, +# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. +# You can create any custom variable you would like, and they will be accessible +# in the templates via {{ site.myvariable }}. + +title: Plutus-Bench Documentation +#email: your-email@example.com +description: >- # this means to ignore newlines until "baseurl:" + Test and benchmark Plutus Smart Contracts. This project enables you to construct + entire smart contract transactions against a mock environment, and measure the + correctness (i.e. unit test) and performance of your smart contracts. +baseurl: "" # the subpath of your site, e.g. /blog +url: "" # the base hostname & protocol for your site, e.g. http://example.com + +# Build settings +theme: just-the-docs +plugins: + - jekyll-feed + +# Exclude from processing. +# The following items will not be processed, by default. +# Any item listed under the `exclude:` key here will be automatically added to +# the internal "default list". +# +# Excluded items can be processed by explicitly listing the directories or +# their entries' file path in the `include:` list. +# +# exclude: +# - .sass-cache/ +# - .jekyll-cache/ +# - gemfiles/ +# - Gemfile +# - Gemfile.lock +# - node_modules/ +# - vendor/bundle/ +# - vendor/cache/ +# - vendor/gems/ +# - vendor/ruby/ + + +nav_external_links: + - title: MockFrost API + url: https://mockfrost.dev/redoc + hide_icon: false + opens_in_new_tab: false + diff --git a/docs/_posts/2025-01-21-welcome-to-jekyll.markdown b/docs/_posts/2025-01-21-welcome-to-jekyll.markdown new file mode 100644 index 0000000..132c53d --- /dev/null +++ b/docs/_posts/2025-01-21-welcome-to-jekyll.markdown @@ -0,0 +1,29 @@ +--- +layout: post +title: "Welcome to Jekyll!" +date: 2025-01-21 11:52:59 +0000 +categories: jekyll update +--- +You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. + +Jekyll requires blog post files to be named according to the following format: + +`YEAR-MONTH-DAY-title.MARKUP` + +Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works. + +Jekyll also offers powerful support for code snippets: + +{% highlight ruby %} +def print_hi(name) + puts "Hi, #{name}" +end +print_hi('Tom') +#=> prints 'Hi, Tom' to STDOUT. +{% endhighlight %} + +Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. + +[jekyll-docs]: https://jekyllrb.com/docs/home +[jekyll-gh]: https://github.com/jekyll/jekyll +[jekyll-talk]: https://talk.jekyllrb.com/ diff --git a/docs/assets/plutus-bench.png b/docs/assets/plutus-bench.png new file mode 100644 index 0000000..f6b76e3 Binary files /dev/null and b/docs/assets/plutus-bench.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..2128901 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,26 @@ +--- +layout: home +title: About +nav_order: 1 +--- + +
+A person with the Cardano logo as face, lifting heavy weights +

Plutus-Bench

+
+ + +Test and benchmark Plutus Smart Contracts. +This project enables you to construct entire smart contract transactions against a mock environment, and measure the correctness (i.e. unit test) and performance of your smart contracts. + +Plutus Bench creates a mock ledger with an arbitrary UTxO set specified by the user. +It then hosts a mock blockfrost API to interact with the ledger, supporting popular off-chain tooling +like [translucent](https://github.com/antibody-cardano/translucent) and [pycardano](https://pycardano.readthedocs.io/en/latest/). + +> Note: Plutus Bench is currently a Work In Progress, and is not yet ready for production use. + +### Why Plutus Bench? + +- **Compatability**: Plutus Bench is written in Python, and is compatible with popular off-chain tooling relying on BlockFrost APIs. Ogmios mocking is also planned. +- **Holistic Testing**: Plutus Bench allows you to test the entire lifecycle of a smart contract, from minting to consuming, in a single test. +- **Simple**: Plutus Bench is designed to be simple to use, and easy to integrate with your existing infrastructure. \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..e26c946 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,20 @@ +--- +title: Installation +permalink: /install/ +nav_order: 2 +--- + +
+

Installation

+
+ +Plutus-Bench is currently not available via any package managers. +Cloning the github Repo and manually installing with pip is the current recommended method: + +```bash +# clone the plutus-bench package +git clone https://github.com/OpShin/plutus-bench.git +cd plutus-bench +# install the package with pip +pip install . +``` diff --git a/docs/usage/_python_api.md b/docs/usage/_python_api.md new file mode 100644 index 0000000..40d19ed --- /dev/null +++ b/docs/usage/_python_api.md @@ -0,0 +1,125 @@ +--- +layout: page +title: Usage +permalink: /usage/ +--- + +
+

Usage

+
+Generally the workflow is as follows: + +- Create a new session with the `/session/create` endpoint. You receive a session ID. +- Initialize a BlockFrost client in your off-chain code, and point it to the mock server. The base url is `http://localhost:8000//api/v1`. Project id is not required. + +That's it! You can now interact with the mock ledger using the BlockFrost client. + +You may further manipulate the ledger using the `//ledger` endpoints. + + + +
+

CMD Mockfrost server

+
+ +A local mockfrost server can be started with the following command +```bash +uvicorn plutus_bench.mockfrost.server:app +``` +After running these commands, a mock blockfrost server will be running on `http://localhost:8000`. +Head to `http://localhost:8000/docs` to see the API documentation. + +
+

Python Mockfrost server

+
+A local mockfrost server can be started from within python: +```python +import uvicorn +from plutus_bench.mockfrost.server import app + +uvicorn.run(app, port=8000) +``` + +However, in the context of Unit testing the following is recommended Usage. + + +```python +from multiprocessing import Process +from time import sleep +import pytest +import uvicorn +from plutus_bench.mockfrost.server import app + +own_path = pathlib.Path(__file__) + +def run_server(): + uvicorn.run(app, port=8000) + +@pytest.fixture +def server(): + proc = Process(target=run_server, args=(), daemon=True) + proc.start() + sleep(1) # Wait for server to start + yield + proc.kill() # Cleanup after test + + +def test_code(server): + #test code here + ... +``` + +
+

Using the Mockfrost server with pycardano

+
+ +The mockfrost server can now be accessed via: +```python +from plutus_bench.mockfrost.client import MockFrostClient, MockFrostUser + +def test_code(server): + client = MockFrostClient(base_url="http://127.0.0.1:8000") + session = client.create_session() + context = session.chain_context() +``` +Session is a `plutus_bench.mockfrost.MockFrostSession` instance that provides a python API to manipulate the server: +```python + session.add_txout( + pycardano.TransactionOutput(address=..., amount=...,), + ) + +``` + + +The `context` created above is a `pycardano.BlockFrostChainContext` instance pointing towards your mockfrost server: +```python + # get list of all utxos + utxos = context.utxos(address=...) + # create and submit pycardano transactions + txbuilder = pycardano.TransactionBuilder(context=context) + ... + # build transaction + ... + tx = txbuilder.build_and_sign(...) + context.submit(tx) +``` + + + + +The `plutus_bench.mockfrost.MockFrostUser` class can be used to create mock users on the server: + +```python + # Create and fund a user + user = MockFrostUser(session) + fund(100_000_000) + + # Return list of utxos for user + utxos = user.utxos() + + # User class holds all required keys and address. + s, v, a = user.signing_key, user.verification_key, user.address +``` + + + diff --git a/docs/usage/index.md b/docs/usage/index.md new file mode 100644 index 0000000..8f0a76e --- /dev/null +++ b/docs/usage/index.md @@ -0,0 +1,17 @@ +--- +title: Usage +nav_order: 3 +permalink: /usage/ +--- + +# Usage + + +Generally the workflow is as follows: + +- Create a new session with the `/session/create` endpoint. You receive a session ID. +- Initialize a BlockFrost client in your off-chain code, and point it to the mock server. The base url is `http://localhost:8000//api/v1`. Project id is not required. + +That's it! You can now interact with the mock ledger using the BlockFrost client. + +You may further manipulate the ledger using the `//ledger` endpoints. \ No newline at end of file diff --git a/docs/usage/mock_python_client.md b/docs/usage/mock_python_client.md new file mode 100644 index 0000000..4911506 --- /dev/null +++ b/docs/usage/mock_python_client.md @@ -0,0 +1,128 @@ +--- +title: Python Client and API +parent: Usage +nav_order: 2 +--- + +# Python Mockfrost Client +{: .no_toc} + +## Table of contents +{: .no_toc .text-delta} + +1. TOC +{:toc} + +## Connect to the server {#client} +Given a [Mockfrost Server]({% link usage/mock_server.md %}) running on the default address `http://127.0.0.1:8000`, a Mockfrost client can be configured to connect to it as follows: +```python +from plutus_bench.mockfrost.client import MockFrostClient, MockFrostUser + +def test_code(server): + # Create client and point it towards local mockfrost address + client = MockFrostClient(base_url="http://127.0.0.1:8000") + + # The client can be used to create a plutus_bench.mockfrost.MockFrostSession + # This can be used to directly manipulate your mock chain + session = client.create_session() + + # We can create a pycardano.BlockFrostChainContext from the session. + # You should be able to interact with the local mockfrost session though the chain context + # the same way you interact with testnet or mainnet + context = session.chain_context() +``` + +## MockFrostSession +The `session` object [created above](#client) with a `client.create_session()` call is a `plutus_bench.mockfrost.MockFrostSession` instance. +This creates a unique blockchain state on the mockfrost server accessed via the session ID with the base url `http://localhost:8000//api/v0`. +The session ID is held as the attribute `session.session_id`. + +The MockFrostSession instance provides a python API to manipulate the ledger. +UTxOs can be added to the mock ledger in two ways: +```python + session.add_utxo( + pycardano.UTxO( + input = pycardano.TransactionInput(transaction_id=..., index=...), + output = pycardano.TransactionOutput(address=..., amount=...), + ) + ) + #Alternatively if the input does not matter: + session.add_txout( + pycardano.TransactionOutput(address=..., amount=...,), + ) +``` + +UTxO's can be removed from the ledger via the transaction input: +```python + session.del_txout( + pycardano.TransactionInput(transaction_id..., index=...), + ) +``` + +The slot number of the mock ledger may be directly set using `set_slot`: +```python + session.set_slot(71_071_542) +``` + +Additionally, for testing staking it is possible to register a mock pool to delegate to and manually distribute rewards: +```python + session.add_mock_pool(pycardano.pool_params.PoolId(...)) + session.distribute_rewards(100_000_000) # each correctly delegated account receives this reward +``` + + + + + + +## Pycardano ChainContext + +The `context` object created [above](#client) is a `pycardano.BlockFrostChainContext` instance pointing towards a mockfrost session on your server. +This should be used to interact with the local network as you would with testnet or mainnet: +```python + # get parameters and properties + print(f'Protocol: {context.protocol_param}') + print(f'Genesis: {context.genesis_param}') + print(f'Epoch: {context.epoch}') + print(f'Slot: {context.last_block_slot}') + print(f'Network: {context.network}') + + # Retrieve all utxos beloning to an address + utxos = context.utxos(address=...) + + # create and submit pycardano transactions + txbuilder = pycardano.TransactionBuilder(context=context) + ... + ... + tx = txbuilder.build_and_sign(...) + context.submit(tx) +``` + +## Mock Users + +The `plutus_bench.mockfrost.MockFrostUser` class can be used to create mock users on the server: + +```python + from plutus_bench.mockfrost import MockFrostUser + # Create and fund a user + user = MockFrostUser(session) + fund(100_000_000) + + # Return list of utxos for user + utxos = user.utxos() + + # User class holds all required keys and address. + s, v, a = user.signing_key, user.verification_key, user.address +``` + +## Mock Pool +For staking you need a mock pool with which to delegate to. This can be most easily achieved with the `plutus_bench.mockfrost.MockFrostPool`: +```python + from plutus_bench.mockfrost import MockFrostPool + # Create Pool + stake_pool = MockFrostPool(session) + + # distribute rewards to delegated accounts + session.distribute_rewards(100_000_000) + + diff --git a/docs/usage/mock_server.md b/docs/usage/mock_server.md new file mode 100644 index 0000000..6a7c3f9 --- /dev/null +++ b/docs/usage/mock_server.md @@ -0,0 +1,67 @@ +--- +title: Mockfrost Server +parent: Usage +nav_order: 1 +--- +# MockFrost Server + +{: .no_toc} +## Table of contents +{: .no_toc .text-delta} + +1. TOC +{:toc} + +## CMD + + + +A local mockfrost server can be started with the following command +```bash +uvicorn plutus_bench.mockfrost.server:app +``` +After running these commands, a mock blockfrost server will be running on `http://localhost:8000`. +Head to `http://localhost:8000/docs` to see the API documentation. + + + + +## Python + +A local mockfrost server can be started from within python: +```python +import uvicorn +from plutus_bench.mockfrost.server import app + +uvicorn.run(app, port=8000) +``` + +For integration into unit testing we would recommend the following style: + + +```python +from multiprocessing import Process +from time import sleep +import pytest +import uvicorn +from plutus_bench.mockfrost.server import app + +own_path = pathlib.Path(__file__) + +def run_server(): + uvicorn.run(app, port=8000) + +@pytest.fixture +def server(): + proc = Process(target=run_server, args=(), daemon=True) + proc.start() + sleep(1) # Wait for server to start + yield + proc.kill() # Cleanup after test + + +def test_contracts(server): + # test your contracts here + ... +``` +