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

Make the auction example end-to-end #6477

Merged
merged 18 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions doc/docusaurus/docs/auction-smart-contract/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "Example: An Auction Smart Contract",
"position": 30,
"link": {
"type": "generated-index",
"description": "In this example we first present the Plutus Tx code for writing the on-chain validator script of a smart contract that controls the auction of an asset, which can be executed on the Cardano blockchain. We will then walk you through the steps to run it end-to-end on Cardano's Preview testnet."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "End to end",
"position": 10,
"link": {
"type": "generated-index",
"description": "We will now demonstrate the process of running the auction example end-to-end on Cardano's Preview testnet."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
sidebar_position: 25
---

# Closing the Auction

Once the auction's end time has elapsed, a transaction can be submitted to finalize the auction, distributing the token and the highest bid accordingly.
This transaction needs to do the following:

- Spend the UTxO that contains the token being auctioned.
- If no bids were placed (which can be determined by examining the datum attached to the UTxO), the token should be returned to the seller's address.
- If at least one bid was placed, the token should be transferred to the highest bidder's address, and the highest bid amount should be sent to the seller's address.
- Set a validity interval that starts no earlier than the auction's end time.

The off-chain code for building and submitting this transaction will be very similar to the code for the bidding transactions, so the details are left as an exercise.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
sidebar_position: 5
---

# Generating Keys and Addresses

To start, clone the plutus-tx-template repository into the `on-chain` directory.
Make sure to have [NodeJS](https://nodejs.org/en) and [yarn](https://yarnpkg.com/) (or [npm](https://github.com/npm/cli), which comes bundled with NodeJS) installed. Then, create a separate `off-chain` directory, set up `package.json`, and add the required dependencies:

```
git clone [email protected]:IntersectMBO/plutus-tx-template.git on-chain
mkdir off-chain && cd $_
yarn init -y
zliu41 marked this conversation as resolved.
Show resolved Hide resolved
yarn add @meshsdk/core
yarn add cbor
```

We recommend using the Nix shell that comes with `plutus-tx-template` to run this example.
The Nix shell provides the correct versions of all dependencies, including GHC, Cabal, Node.js, and various C libraries.
To enter the nix shell, run

```
nix develop on-chain/
```

The first run of `nix develop` may take some time so please be patient.

We'll use [mesh](https://meshjs.dev/), a JavaScript framework, for writing off-chain code.
We'll use [Blockfrost](https://blockfrost.io/) as the blockchain provider, to avoid the need of running a local node.
zliu41 marked this conversation as resolved.
Show resolved Hide resolved
If you don't have a Blockfrost account, you can sign up for one, and create a project for the Preview network.

The first step is to generate keys and addresses for the seller and the bidders.
Add a new file named `off-chain/generate-keys.mjs`, with the following content:

<LiteralInclude file="generate-keys.mjs" language="javascript" title="generate-keys.mjs" />

Then, generate keys and addresses for one seller and two bidders by running:

```
node generate-keys.mjs seller
node generate-keys.mjs bidder1
node generate-keys.mjs bidder2
```

This will create three files for each participant (seller, bidder1, and bidder2): a `.skey` file that contains a secret key, a `.addr` file that contains the corresponding wallet address, and a `.pkh` file that contains the corresponding public key hash.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
sidebar_position: 10
---

# Getting Funds from the Faucet

Next, we'll need to fund the wallet of each participant (seller, bidder1 and bidder2), in order to cover transaction fees and place bids.
We can get funds from Cardano's [testnet faucet](https://docs.cardano.org/cardano-testnets/tools/faucet/).

To request funds, enter the seller's address into the address field and click "request funds."
This will deposit 10,000 (test) ADA into the seller's wallet.
Make sure you select the correct network (Preview).
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏼


Since the faucet limits how frequently you can request funds, and 10,000 ADA is more than sufficient for this example, we'll share the 10,000 ADA among the seller, bidder1, and bidder2.
To do so, create a file named `off-chain/send-lovelace.mjs` with the following content:

<LiteralInclude file="send-lovelace.mjs" language="javascript" title="send-lovelace.mjs" />

Substitute your Blockfrost project ID for `Replace with Blockfrost Project ID`.

This Javascript module builds and submits a transaction that sends 1 billion Lovelace (equivalent to 1000 Ada) from the seller's wallet to the specified recipient.
Run the following commands:

```
node send-lovelace.mjs bidder1
node send-lovelace.mjs bidder2
Comment on lines +25 to +26
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
node send-lovelace.mjs bidder1
node send-lovelace.mjs bidder2
pushd off-chain
node send-lovelace.mjs bidder1
node send-lovelace.mjs bidder2
popd

Copy link
Contributor

Choose a reason for hiding this comment

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

previous commands assume that current dir is one level above the off-chain dir, but this command fails when executed not from the off-chain dir.

Copy link
Member Author

Choose a reason for hiding this comment

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

previous commands assume that current dir is one level above the off-chain dir

Are you referring to "To do so, create a file named off-chain/send-lovelace.mjs"? I can change it to "To do so, create a file named send-lovelace.mjs in off-chain". The previous task was to generate keys, and there's no need to leave the off-chain dir.

```

After the transactions are confirmed and included in a block (usually within a minute), bidder1's and bidder2's wallets should each have 1000 Ada, and the seller's wallet should have approximately 8000 Ada (minus transaction fees), which you can verify on [Cardanoscan](https://preview.cardanoscan.io/).
91 changes: 91 additions & 0 deletions doc/docusaurus/docs/auction-smart-contract/end-to-end/mint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
sidebar_position: 15
---

# Minting the Token to be Auctioned

Before we can start the auction, we need to mint a token to be auctioned.
To do so, we first must determine the token's currency symbol and name.
To mint or burn tokens with a specific currency symbol (`currencySymbol`), a Plutus script whose hash matches `currencySymbol` must be provided, and is used to validate the minting or burning action.
Therefore, we'll first write the on-chain script, then compute its hash and use it as the currency symbol.

## On-chain Minting Policy Script

The full minting policy code can be found at [AuctionMintingPolicy.hs](https://github.com/IntersectMBO/plutus-tx-template/blob/main/src/AuctionMintingPolicy.hs).
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that minting policy uses Plutus V2. Do we have a task to update it to the Plutus V3?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think V2 is fine for this example, since there's no advantage (in fact there's disadvantage) to use Plutus V3. But if anyone wants to update the example to V3, you are welcome to.

The main logic is in the following function:

<LiteralInclude file="AuctionMintingPolicy.hs" language="haskell" title="AuctionMintingPolicy.hs" start="-- BLOCK1" end="-- BLOCK2" />

This script will pass if the following two conditions are met:

1. The transaction is signed by a specific public key.
2. The transaction mints exactly one token, whose currency symbol matches the script's hash (i.e., `ownCurrencySymbol ctx`).
The token name can be anything.

> :pushpin: **NOTE**
>
> A token minted in this way is _not_ considered a non-fungible token (NFT) because, while only one token can be minted in a single transaction, multiple transactions can mint additional tokens with the same currency symbol and token name.
> To create a truly unique token, you would need a more complex minting policy, but for simplicity, that is not covered here.

## Compile and Generate Blueprint for the Minting Policy

Next, we need to compile the minting policy script and create its blueprint.
To do so, we first need to supply a public key hash, which the minting policy will use for checking condition 1 above.
Assuming the seller is the one minting the token, this should be the seller's public key hash.
Open `GenMintingPolicyBlueprint.hs` in the `on-chain` directory, and replace `error "Replace with seller pkh"` with the content of `off-chain/seller.pkh`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to put auction validator script and minting policy script in two different blueprint files?
I can see how two different plutus scripts (validator, policy) together constitute a one logical "contract".

Copy link
Member Author

Choose a reason for hiding this comment

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

To generate blueprint for the auction validator, we need to know the hash of the minting validator, and currently the best way to get the hash is from the blueprint.


The minting policy code comes with `plutus-tx-template`, so you can find it in the `on-chain` repository.
To compile it and generate the blueprint, navigate to the `on-chain` directory and run

```
cabal run gen-minting-policy-blueprint -- ../off-chain/plutus-auction-minting-policy.json
```

You may need to run `cabal update` before executing this command for the first time.

This should produce a blueprint file named `off-chain/plutus-auction-minting-policy.json`.

## Compile and Generate Blueprint for the Auction Validator

One final step before minting the token: since we want to lock the minted token at the script address corresponding to the auction validator,
we must supply the parameters (i.e., `AuctionParams`) to the auction validator, compile the auction validator, and calculate its script address.

Open `GenAuctionValidatorBlueprint.hs` in the `on-chain` directory, and replace all placeholders:
- Replace `error "Replace with sellerh pkh"` with the content of `off-chain/seller.pkh`.
- Replace `error "Replace with currency symbol"` with the minting policy hash, which you can find in the `hash` field in `off-chain/plutus-auction-minting-policy.json`.
- Replace `error "Replace with the auction's end time"` with a POSIX timestamp for a time in the near future (say 24 hours from now).
Note that the POSIX timestamp in Plutus is the number of _milliseconds_, rather than seconds, elapsed since January 1, 1970.
In other words, add three zeros to the usual POSIX timestamp.
For instance, the POSIX timestamp of September 1, 2024, 21:44:51 UTC, is 1725227091000.

Then, navigate to the `on-chain` directory and run

```
cabal run gen-auction-validator-blueprint -- ../off-chain/plutus-auction-validator.json
```

This will generate a blueprint file named `off-chain/plutus-auction-validator.json`, which the off-chain code can read and calculate the auction validator's script address.


## Off-chain Code for Minting

We are now ready to write and execute the off-chain code for minting.
Create a file named `off-chain/mint-token-for-auction.mjs` with the following content:

<LiteralInclude file="mint-token-for-auction.mjs" language="javascript" title="mint-token-for-auction.mjs" />

Substitute your Blockfrost project ID for `Replace with Blockfrost Project ID`.

This Javascript module uses the mesh library to build a transaction that mints a token (`tx.mintAsset`).
The token will have the currency symbol of the minting policy's hash, and a token name of `TokenToBeAuctioned`.
It will be sent to `auctionValidatorAddress`, with a datum corresponding to `Nothing`.
The transaction is signed by the seller (`seller.skey`), and then submitted to the Preview testnet.

Run the coding using:

```
node mint-token-for-auction.mjs
```

and you should see a message "Minted a token at address ..." printed in the console.
Within a minute, you should be able to find the transaction using the transaction hash on [Cardanoscan](https://preview.cardanoscan.io/) and review its details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
sidebar_position: 20
---

# Placing Bids

Now we can start bidding.
Let's place a bid of 100 Ada from bidder1, followed by a bid of 200 Ada from bidder2.
Each transaction that places a bid must do the following:

- Spend the UTxO that contains the token being auctioned.
For bidder1, the transaction that produced the UTxO is the one that minted the token.
For bidder2, the transaction that produced the UTxO is bidder1's transaction.
The address of this UTxO is always the auction validator's script address, so each bidding transaction must include the auction validator and a redeemer[^1].
- Place a bid (via the redeemer) with an amount at least as high as the auction's minimum bid, and higher than the previous highest bid (if any).
The existence and the details of a previous highest bid can be determined by inspecting the datum attached to the aforementioned UTxO.
This is enforced by the auction validator's `sufficientBid` condition.
- Lock the token being auctioned, together with the bid amount, in a new UTxO at the auction validator's script address.
The new UTxO should also include a datum containing the details of this bid.
This is enforced by the auction validator's `correctOutput` condition.
- Refund the previous highest bid (if any) to its bidder's wallet address.
This is enforced by the auction validator's `refundsPreviousHighestBid` condition.
- Set a validity interval that ends no later than the auction's end time.
This is enforced by the auction validator's `validBidTime` condition.

To submit these bidding transactions, create a file named `off-chain/bid.mjs` for the off-chain code, with the following content:

<LiteralInclude file="bid.mjs" language="javascript" title="bid.mjs" />

This Javascript module builds and submits a transaction that does exactly the above.

The following substitutions are needed:

- Substitute your Blockfrost project ID for `Replace with Blockfrost Project ID`.
- Substitute a slot number no later than the auction's end time for `Replace with transaction expiration time`.
For instance, if you set the auction's end time to be approximately 24 hours from now, you can use a slot number corresponding to approximately 12 hours from now.
To determine the slot nmber, go to [Cardanoscan](https://preview.cardanoscan.io/), click on a recent transaction, take its Absolute Slot, and add 12 hours (43200) to it.

Place the first bid by running

```
node bid.mjs <minting-transaction-hash> bidder1 100000000
```

Replace `<minting-transaction-hash>` with the hash of the transaction we previously submitted for minting the token.
This hash is used by the off-chain code to locate the UTxO that contains the token.

After the first bidding transaction is confirmed, we can submit the second bid from bidder2, with a similar command:

```
node bid.mjs <bidder1-transaction-hash> bidder2 200000000
```

Replace `<bidder1-transaction-hash>` with the hash of the previous transaction.

---

[^1]: Instead of including the script in the transaction, we can use a reference script, but to keep things simple, we won't discuss that here.
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
---
sidebar_position: 25
sidebar_position: 15
---

# Life cycle of the auction smart contract

With the Plutus script written, Alice is now ready to start the auction smart contract.
At the outset, Alice creates a script UTXO whose address is the hash of the Plutus script, whose value is the token to be auctioned, and whose datum is `Nothing`.
Recall that the datum represents the highest bid, and there's no bid yet.
With the Plutus script written, Alice is now ready to start the auction smart contract.
At the outset, Alice creates a script UTXO whose address is the hash of the Plutus script, whose value is the token to be auctioned, and whose datum is `Nothing`.
Recall that the datum represents the highest bid, and there's no bid yet.
This script UTXO also contains the script itself, so that nodes validating transactions that try to spend this script UTXO have access to the script.

## Initial UTXO

Alice needs to create the initial UTXO transaction with the desired UTXO as an output.
The token being auctioned can either be minted by this transaction, or if it already exists in another UTXO on the ledger, the transaction should consume that UTXO as an input.
Alice needs to create the initial UTXO transaction with the desired UTXO as an output.
The token being auctioned can either be minted by this transaction, or if it already exists in another UTXO on the ledger, the transaction should consume that UTXO as an input.
We will not go into the details here of how minting tokens works.

## The first bid

Suppose Bob, the first bidder, wants to bid 100 Ada for Alice's NFT.
Suppose Bob, the first bidder, wants to bid 100 Ada for Alice's NFT.
In order to do this, Bob creates a transaction that has at least two inputs and at least one output.

The required inputs are (1) the script UTXO Alice created; (2) Bob's bid of 100 Ada.
The 100 Ada can come in one or multiple UTXOs.
The required inputs are (1) the script UTXO Alice created; (2) Bob's bid of 100 Ada.
The 100 Ada can come in one or multiple UTXOs.
Note that the input UTXOs must have a total value of more than 100 Ada, because in addition to the bid amount, they also need to cover the transaction fee.

The required output is a script UTXO with the same address as the initial UTXO (since the Plutus script itself remains the same), which is known as a *continuing output*.
The required output is a script UTXO with the same address as the initial UTXO (since the Plutus script itself remains the same), which is known as a *continuing output*.
This continuing output UTXO should contain:

- a datum that contains Bob's wallet address and Bob's bid amount (100 Ada).
Expand All @@ -34,41 +34,40 @@ This continuing output UTXO should contain:
If the input UTXOs contain more Ada than 100 plus the transaction fee, then there should be additional output UTXOs that return the extra Ada.
Again, verifying that the input value of a transaction minus the transaction fee equals the output value (unless the transaction is burning tokens) is the responsibility of the ledger, not the Plutus script.

In order for Bob's transaction to be able to spend the initial script UTXO Alice created, Bob's transaction must also contain a redeemer.
As shown in the code above, there are two kinds of redeemers in our example: `NewBid Bid` and `Payout`.
In order for Bob's transaction to be able to spend the initial script UTXO Alice created, Bob's transaction must also contain a redeemer.
As shown in the code above, there are two kinds of redeemers in our example: `NewBid Bid` and `Payout`.
The redeemer in Bob's transaction is a `NewBid Bid` where the `Bid` contains Bob's wallet address and bid amount.

![First bid diagram](../../static/img/first-bid-simple-auction-v3.png)

Once Bob's transaction is submitted, the node validating this transaction will run the Plutus script, which checks a number of conditions like whether the bid happens before the deadline, and whether the bid is high enough.
If the checks pass and everything else about the transaction is valid, the transaction will go through and be included in a block.
Once Bob's transaction is submitted, the node validating this transaction will run the Plutus script, which checks a number of conditions like whether the bid happens before the deadline, and whether the bid is high enough.
If the checks pass and everything else about the transaction is valid, the transaction will go through and be included in a block.
At this point, the initial UTXO created by Alice no longer exists on the ledger, since it has been spent by Bob's transaction.

## The second bid

Next, suppose a second bidder, Charlie, wants to outbid Bob.
Next, suppose a second bidder, Charlie, wants to outbid Bob.
Charlie wants to bid 200 Ada.

Charlie will create another transaction.
This transaction should have an additional output compared to Bob's transaction: a UTXO that returns Bob's bid of 100 Ada.
Charlie will create another transaction.
This transaction should have an additional output compared to Bob's transaction: a UTXO that returns Bob's bid of 100 Ada.
Recall that this is one of the conditions checked by the Plutus script; the transaction is rejected if the refund output is missing.

![Second bid diagram](../../static/img/second-bid-simple-auction-v3.png)

Charlie's transaction needs to spend the script UTXO produced by Bob's transaction, so it also needs a redeemer.
Charlie's transaction needs to spend the script UTXO produced by Bob's transaction, so it also needs a redeemer.
The redeemer is a `NewBid Bid` where `Bid` contains Charlie's wallet address and bid amount.
Charlie's transaction cannot spend the initial UTXO produced by Alice, since it has already been spent by Bob's transaction.

## Closing the auction

Let's assume that there won't be another bid.
Let's assume that there won't be another bid.
Once the deadline has passed, the auction can be closed.

In order to do that, somebody has to create another transaction.
That could be Alice, who wants to collect the bid, or it could be Charlie, who wants to collect the NFT.
In order to do that, somebody has to create another transaction.
That could be Alice, who wants to collect the bid, or it could be Charlie, who wants to collect the NFT.
It can be anybody, but Alice and Charlie have an incentive to create it.

This transaction has one required input: the script UTXO produced by Charlie's transaction, and two required outputs: (1) the payment of the auctioned token to Charlie; (2) the payment of 200 Ada to Alice.

![Closing transaction diagram](../../static/img/closing-tx-simple-auction-v3.png)

Loading
Loading