diff --git a/doc/docusaurus/docs/auction-smart-contract/_category_.json b/doc/docusaurus/docs/auction-smart-contract/_category_.json new file mode 100644 index 00000000000..311002d7b81 --- /dev/null +++ b/doc/docusaurus/docs/auction-smart-contract/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Example: An Auction Smart Contract", + "position": 15, + "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." + } + } diff --git a/doc/docusaurus/docs/auction-smart-contract/end-to-end/_category_.json b/doc/docusaurus/docs/auction-smart-contract/end-to-end/_category_.json new file mode 100644 index 00000000000..14cb6e5d0df --- /dev/null +++ b/doc/docusaurus/docs/auction-smart-contract/end-to-end/_category_.json @@ -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." + } +} diff --git a/doc/docusaurus/docs/auction-smart-contract/end-to-end/closing-the-auction.md b/doc/docusaurus/docs/auction-smart-contract/end-to-end/closing-the-auction.md new file mode 100644 index 00000000000..e79484062f8 --- /dev/null +++ b/doc/docusaurus/docs/auction-smart-contract/end-to-end/closing-the-auction.md @@ -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. diff --git a/doc/docusaurus/docs/auction-smart-contract/end-to-end/generating-keys.md b/doc/docusaurus/docs/auction-smart-contract/end-to-end/generating-keys.md new file mode 100644 index 00000000000..3d38257596d --- /dev/null +++ b/doc/docusaurus/docs/auction-smart-contract/end-to-end/generating-keys.md @@ -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 git@github.com:IntersectMBO/plutus-tx-template.git on-chain +mkdir off-chain && cd $_ +yarn init -y +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. +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: + + + +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. diff --git a/doc/docusaurus/docs/auction-smart-contract/end-to-end/getting-funds.md b/doc/docusaurus/docs/auction-smart-contract/end-to-end/getting-funds.md new file mode 100644 index 00000000000..e6e3fb49239 --- /dev/null +++ b/doc/docusaurus/docs/auction-smart-contract/end-to-end/getting-funds.md @@ -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). + +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: + + + +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 +``` + +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/). diff --git a/doc/docusaurus/docs/auction-smart-contract/end-to-end/mint.md b/doc/docusaurus/docs/auction-smart-contract/end-to-end/mint.md new file mode 100644 index 00000000000..ffc1bb204e9 --- /dev/null +++ b/doc/docusaurus/docs/auction-smart-contract/end-to-end/mint.md @@ -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). +The main logic is in the following function: + + + +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`. + +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: + + + +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. diff --git a/doc/docusaurus/docs/auction-smart-contract/end-to-end/placing-bids.md b/doc/docusaurus/docs/auction-smart-contract/end-to-end/placing-bids.md new file mode 100644 index 00000000000..547134e9dce --- /dev/null +++ b/doc/docusaurus/docs/auction-smart-contract/end-to-end/placing-bids.md @@ -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: + + + +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 bidder1 100000000 +``` + +Replace `` 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 bidder2 200000000 +``` + +Replace `` 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. diff --git a/doc/docusaurus/docs/simple-example/life-cycle.md b/doc/docusaurus/docs/auction-smart-contract/life-cycle.md similarity index 85% rename from doc/docusaurus/docs/simple-example/life-cycle.md rename to doc/docusaurus/docs/auction-smart-contract/life-cycle.md index b398bc65136..d90f21ff887 100644 --- a/doc/docusaurus/docs/simple-example/life-cycle.md +++ b/doc/docusaurus/docs/auction-smart-contract/life-cycle.md @@ -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). @@ -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) - diff --git a/doc/docusaurus/docs/auction-smart-contract/on-chain-code.md b/doc/docusaurus/docs/auction-smart-contract/on-chain-code.md new file mode 100644 index 00000000000..469742f265e --- /dev/null +++ b/doc/docusaurus/docs/auction-smart-contract/on-chain-code.md @@ -0,0 +1,157 @@ +--- +sidebar_position: 5 +--- + +# On-chain Code: The Auction Validator + +:::caution +The code in this example is not a production-ready implementation, as it is not optimized for security or efficiency. +It is provided purely as an example for illustration and ecudational purposes. +Refer to resources like **[Cardano Plutus Script Vulnerability Guide](https://library.mlabs.city/common-plutus-security-vulnerabilities)** for best practices on developing secure smart contracts. +::: + +# Auction Properties + +In this example, a seller wants to auction some asset she owns, represented as a non-fungible token (NFT) on Cardano. +She would like to create and deploy an auction smart contract with the following properties: + +- there is a minimum bid amount +- each bid must be higher than the previous highest bid (if any) +- once a new bid is made, the previous highest bid (if exists) is immediately refunded +- there is a deadline for placing bids; once the deadline has passed, new bids are no longer accepted, the asset can be transferred to the highest bidder (or to the seller if there are no bids), and the highest bid (if exists) can be transferred to the seller. + +# Plutus Tx Code + +Plutus Tx is a subset of Haskell, used to write on-chain code, also known as validators or scripts. +A Plutus Tx program is compiled into Plutus Core, which is interpreted on-chain. +The full Plutus Tx code for the auction smart contract can be found at [AuctionValidator.hs](https://github.com/IntersectMBO/plutus-tx-template/blob/main/src/AuctionValidator.hs). + + + +## Data types + +First, let's define the following data types and instances for the validator: + + + +The purpose of `makeLift` and `makeIsDataSchemaIndexed` will be explained later. + +Writing a Plutus Tx validator script for a smart contract often involves the following data types: + +### 1. Contract parameters + +These are fixed properties of the contract. You can put here values that will never change during the contract's life cycle. +In our example, it is the `AuctionParams` type, containing properties like seller and minimum bid. + +### 2. Datum + +This is part of a script UTXO. +It's commonly used to hold the state of the contract and values that can change throughout the contract's life cycle. +Our example requires only one piece of state: the current highest bid. +We use the `AuctionDatum` type to represent this. + +### 3. Redeemer + +This is an input to the Plutus script provided by the transaction that is trying to spend a script UTXO. +If a smart contract is regarded as a state machine, the redeemer would be the input that ticks the state machine. +In our example, it is the `AuctionRedeemer` type: one may either submit a new bid, or request to close the auction and pay out the winner and the seller, both of which lead to a new state of the auction. + +### 4. Script context + +This type contains the information of the transaction that the validator can inspect. +In our example, our validator verifies several conditions of the transaction; e.g., if it is a new bid, then it must be submitted before the auction's end time; the previous highest bid must be refunded to the previous bidder, etc. + +Different [ledger language versions](../working-with-scripts/ledger-language-version.md) use different script context types. +In this example we are writing a Plutus V2 scripts, so we import the `ScriptContext` data type from `PlutusLedgerApi.V2.Contexts`. +It can be easiely adapted for Plutus V1 or V3. + +> :pushpin: **NOTE** +> +> When writing a Plutus validator using Plutus Tx, it is advisable to turn off Haskell's `Prelude`. +> Usage of most functions and methods in `Prelude` should be replaced by their counterparts in the `plutus-tx` library, e.g., instead of the `==` from `base`, use `PlutusTx.Eq.==`. + +## Main Validator Function + +Now we are ready to introduce our main validator function. +The beginning of the function looks like the following: + + + +Depending on whether this transaction is attempting to submit a new bid or to request payout, the validator validates the corresponding set of conditions. + +### Sufficient Bid Condition + +The `sufficientBid` condition verifies that the bid amount is sufficient: + + + +### Valid Bid Time Condition + +The `validBidTime` condition verifies that the bid is submitted before the auction's deadline: + + + +Here, `to x` is the time interval ending at `x`, i.e., `(-∞, x]`. +`txInfoValidRange` is a transaction property. +It is the time interval in which the transaction is allowed to go through phase-1 validation. +`contains` takes two time intervals, and checks that the first interval completely includes the second. +Since the transaction may be validated at any point in the `txInfoValidRange` interval, we need to check that the entire interval lies within `(-∞, apEndTime params]`. + +The reason a script receives the `txInfoValidRange` interval instead of the exact time the script is run is due to [determinism](https://iohk.io/en/blog/posts/2021/09/06/no-surprises-transaction-validation-on-cardano/). +Using the exact time would be like calling a `getCurrentTime` function and branching based on the current time. +On the other hand, by using the `txInfoValidRange` interval, the same interval is always used by the same transaction. +If the current time when the transaction is validated is outside of the interval, the transaction is rejected immediately without running the script. + +Also note the tilde (`~`) in `~validBidTime = ...`. +When writing Plutus Tx it is [advisable](../using-plutus-tx/compiling-plutus-tx.md) to turn on the `Strict` extension, which generally improves script performance. +Doing so makes all bindings strict, which means, in this particular case, without the `~`, `validBidTime` would be evaluated even if the redeemer matches the `Payout` case, which doesn't need this condition. +Doing so results in unnecessary work or even unexpected evaluation failures. +The `~` makes `validBidTime` non-strict, i.e., only evaluated when used. + +On the other hand, it is unnecessary to add `~` to `sufficientBid`, since it has a function type, and a function cannot be evaluated further without receiving enough arguments. + +### Refunds Previous Highest Bid Condition + +The `refundsPreviousHighestBid` condition checks that the transaction pays the previous highest bid to the previous bidder: + + + +It uses `PlutusTx.find` to find the transaction output (a UTXO) that pays to the previous bidder the amount equivalent to the previous highest bid, and verifies that there is at least one such output. + +### Correct Output Condition + +The `correctOutput` condition verifies that the transaction produces a *continuing output* (see below for definition) containing the correct datum and value. +It has two subconditions: + +- `correctOutputDatum`: the datum should contain the new highest bid +- `correctOutputValue`: the value should contain (1) the token being auctioned, and (2) the bid amount. + + + +A "continuing output" is a transaction output that pays to the same script address from which we are currently spending. +Exactly one continuing output must be present in this example so that the next bidder can place a new bid. +The new bid, in turn, will need to spend the continuing output and get validated by the same script. + +If the transaction is requesting a payout, the validator will then verify the other three conditions: `validPayoutTime`, `sellerGetsHighestBid` and `highestBidderGetsAsset`. +These conditions are similar to the ones already explained, so their details are omitted. + +### Compiling the validator + +Finally, we need to compile the validator written in Plutus Tx into Plutus Core, using the Plutus Tx compiler: + + + +The type of a compiled Plutus V2 spending validator should be `CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit)`, as explained in [Plutus Ledger Language Version](../working-with-scripts/ledger-language-version.md). +The call to `PlutusTx.unsafeFromBuiltinData` is the reason we need the `PlutusTx.unstableMakeIsData` shown before, which derives `UnsafeFromData` instances. +And instead of returning a `Bool`, it simply returns `()`, and the validation succeeds if the script evaluates without error. + +Note that `AuctionParams` is _not_ an argument of the compiled validator. +`AuctionParams` contains contract properties that don't change, so it is simply built into the validator by partial application. +The partial application is done via `PlutusTx.unsafeApplyCode`. + +> :pushpin: **NOTE** +> +> It is worth noting that we must call `PlutusTx.compile` on the entire `auctionUntypedValidator`, rather than applying it to `params` before compiling, as in `$$(PlutusTx.compile [||auctionUntypedValidator params||])`. +> The latter won't work, because everything being compiled (inside `[||...||]`) must be known at compile time, but we won't be able to access `params` until runtime. +> Instead, once we have the `params` at runtime, we use `liftCode` to lift it into a Plutus Core term before calling `unsafeApplyCode`. +> This is the reason why we need the `Lift` instance for `AuctionParams`, derived via `PlutusTx.makeLift`. diff --git a/doc/docusaurus/docs/essential-concepts/plutus-core-and-plutus-tx.md b/doc/docusaurus/docs/essential-concepts/plutus-core-and-plutus-tx.md index 6badd194537..b4eb9a8572c 100644 --- a/doc/docusaurus/docs/essential-concepts/plutus-core-and-plutus-tx.md +++ b/doc/docusaurus/docs/essential-concepts/plutus-core-and-plutus-tx.md @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 5 --- # Plutus Core and Plutus Tx @@ -42,4 +42,4 @@ See [Overview of Languages Compiling to UPLC](../delve-deeper/languages.md) for The formal details of Plutus Core are in its [specification](https://github.com/IntersectMBO/plutus#specifications-and-design). -PIR is discussed in [_Unraveling recursion: compiling an IR with recursion to System F_](https://iohk.io/en/research/library/papers/unraveling-recursion-compiling-an-ir-with-recursion-to-system-f/). \ No newline at end of file +PIR is discussed in [_Unraveling recursion: compiling an IR with recursion to System F_](https://iohk.io/en/research/library/papers/unraveling-recursion-compiling-an-ir-with-recursion-to-system-f/). diff --git a/doc/docusaurus/docs/essential-concepts/versions.md b/doc/docusaurus/docs/essential-concepts/versions.md index 43fd4f3f561..65f2fc7d679 100644 --- a/doc/docusaurus/docs/essential-concepts/versions.md +++ b/doc/docusaurus/docs/essential-concepts/versions.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 10 --- # Different Notions of Version diff --git a/doc/docusaurus/docs/glossary.md b/doc/docusaurus/docs/glossary.md index 65ca3800e4b..74fb82ed3c5 100644 --- a/doc/docusaurus/docs/glossary.md +++ b/doc/docusaurus/docs/glossary.md @@ -1,5 +1,5 @@ --- -sidebar_position: 15 +sidebar_position: 25 --- # Glossary diff --git a/doc/docusaurus/docs/simple-example/_category_.json b/doc/docusaurus/docs/simple-example/_category_.json deleted file mode 100644 index 055fc46b7db..00000000000 --- a/doc/docusaurus/docs/simple-example/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Simple example", - "position": 30, - "link": { - "type": "generated-index", - "description": "This section walks you through a straightforward auction smart contract through a practical example, detailing the EUTXO model, how Plutus Tx integrates data types, validator functions and script execution." - } - } diff --git a/doc/docusaurus/docs/simple-example/alternatives-to-plutus-tx.md b/doc/docusaurus/docs/simple-example/alternatives-to-plutus-tx.md deleted file mode 100644 index 41fcec45308..00000000000 --- a/doc/docusaurus/docs/simple-example/alternatives-to-plutus-tx.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -sidebar_position: 35 ---- - -# Alternatives to Plutus Tx - -There are languages other than Plutus Tx that can be compiled into Plutus Core. -We list some of them here for reference. -However, we are not endorsing them; we are not representing their qualities nor their state of development regarding their production-readiness. - -- [Aiken](https://github.com/txpipe/aiken/) -- [Hebi](https://github.com/OpShin/hebi) -- [Helios](https://github.com/hyperion-bt/helios) -- [OpShin](https://github.com/OpShin/opshin) -- [plu-ts](https://github.com/HarmonicLabs/plu-ts) -- [Plutarch](https://github.com/Plutonomicon/plutarch-core) -- [Pluto](https://github.com/Plutonomicon/pluto) - diff --git a/doc/docusaurus/docs/simple-example/auction-properties.md b/doc/docusaurus/docs/simple-example/auction-properties.md deleted file mode 100644 index 7ba9bee0d27..00000000000 --- a/doc/docusaurus/docs/simple-example/auction-properties.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -sidebar_position: 15 ---- - -# Auction properties - -In this example, Alice wants to auction some asset she owns, represented as a non-fungible token (NFT) on Cardano. -She would like to create and deploy an auction smart contract with the following properties: - -- there is a minimum bid amount -- each bid must be higher than the previous highest bid (if any) -- once a new bid is made, the previous highest bid (if it exists) is immediately refunded -- there is a deadline for placing bids; once the deadline has passed, new bids are no longer accepted, the asset can be transferred to the highest bidder (or to the seller if there are no bids), and the highest bid (if one exists) can be transferred to the seller. - -Next, let's go through and discuss the Plutus Tx code we're using, in the next section, for this specific example of an auction smart contract. diff --git a/doc/docusaurus/docs/simple-example/eutxo-model.md b/doc/docusaurus/docs/simple-example/eutxo-model.md deleted file mode 100644 index c9660fec4bb..00000000000 --- a/doc/docusaurus/docs/simple-example/eutxo-model.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -sidebar_position: 10 ---- - -# The EUTXO model, datum, redeemer and script context - -On the Cardano blockchain, a transaction contains an arbitrary number of inputs and an arbitrary number of outputs. -The effect of a transaction is to consume inputs and produce new outputs. - - - -UTXO (unspent transaction output) is the ledger model used by some blockchains, including bitcoin. -A UTXO is produced by a transaction, is immutable, and can only be spent once by another transaction. -In the original UTXO model, a UTXO contains a wallet address and a value (e.g., some amount of one or more currencies/tokens). -Inside a transaction, a UTXO is uniquely identified by the wallet address. -It can be spent by a transaction if the transaction is signed by the private key of the wallet address. - - - -The Extended UTXO model (EUTXO) extends the original model with a new kind of UTXO: script UTXO. -A script UTXO contains a value, a script (usually a Plutus script), a piece of data called *datum*, and is identified by the hash of the script. -For a transaction to spend it, the transaction must provide a piece of input data to the script, referred to as the *redeemer*. -The script is then run, and it must succeed in order for the transaction to be allowed to spend the UTXO. -In addition to the redeemer, the script also has access to the datum contained in the UTXO, as well as the details of the transaction trying to spend it. -This is referred to as *script context*. - - - -Note that the only thing a Plutus script does is to determine whether a transaction can spend the script UTXO that contains the script. -It is *not* responsible for such things as deciding whether it can spend a different UTXO, checking that the input value in a transaction equals the output value, or updating the state of the smart contract. -Consider it a pure function that returns `Bool`. -Checking transaction validity is done by the ledger rules, and updating the state of a smart contract is done by constructing the transaction to produce a new script UTXO with an updated datum. - - - -The immutability of UTXOs leads to the extremely useful property of completely predictable transaction fees. -The Plutus script in a transaction can be run off-chain to determine the fee before submitting the transaction onto the blockchain. -When the transaction is submitted, if some UTXOs it tries to spend have already been spent, the transaction is immediately rejected without penalty. -If all input UTXOs still exist, and the Plutus script is invoked, the on-chain behavior would be exactly identical to the off-chain behavior. -This could not be achieved if transaction inputs were mutable, such as is the case in Ethereum's account-based model. - -See also: - -- [Working with scripts](../category/working-with-scripts) for further reading about scripts -- [Understanding the Extended UTXO model](https://docs.cardano.org/learn/eutxo-explainer) - diff --git a/doc/docusaurus/docs/simple-example/further-reading.md b/doc/docusaurus/docs/simple-example/further-reading.md deleted file mode 100644 index 6f1a5467ff1..00000000000 --- a/doc/docusaurus/docs/simple-example/further-reading.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -sidebar_position: 45 ---- - -# Further reading - -## The EUTXO model - -- [The Extended UTXO Model](https://iohk.io/en/research/library/papers/the-extended-utxo-model/) (Paper) -- [The EUTXO Handbook](https://www.essentialcardano.io/article/the-eutxo-handbook) -- Blog Post: Cardano's Extended UTXO accounting model—built to support multi-assets and smart contracts ([part 1](https://iohk.io/en/blog/posts/2021/03/11/cardanos-extended-utxo-accounting-model/), [part 2](https://iohk.io/en/blog/posts/2021/03/12/cardanos-extended-utxo-accounting-model-part-2/)) diff --git a/doc/docusaurus/docs/simple-example/libraries.md b/doc/docusaurus/docs/simple-example/libraries.md deleted file mode 100644 index ffc7dc22067..00000000000 --- a/doc/docusaurus/docs/simple-example/libraries.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -sidebar_position: 30 ---- - -# Libraries for writing Plutus Tx scripts - -This auction example shows a relatively low-level way of writing scripts using Plutus Tx. -In practice, you may consider using a higher-level library that abstracts away some of the details. -For example, [plutus-apps](https://github.com/IntersectMBO/plutus-apps) provides a constraint library for writing Plutus Tx. -Using these libraries, writing a validator in Plutus Tx becomes a matter of defining state transactions and the corresponding constraints, e.g., the condition `refundsPreviousHighestBid` can simply be written as `Constraints.mustPayToPubKey bidder (lovelaceValue amt)`. - diff --git a/doc/docusaurus/docs/simple-example/off-chain-code.md b/doc/docusaurus/docs/simple-example/off-chain-code.md deleted file mode 100644 index a1319a96779..00000000000 --- a/doc/docusaurus/docs/simple-example/off-chain-code.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -sidebar_position: 40 ---- - -# Off-chain code - -Since the main purpose of this example is to introduce Plutus Tx and Plutus Core, we walked through only the on-chain code, which is responsible for validating transactions (in the sense of determining whether a transaction is allowed to spend a UTXO). - -In addition to the on-chain code, one typically needs the accompanying off-chain code and services to perform tasks like building transactions, submitting transactions, deploying smart contracts, querying for available UTXOs on the chain, etc. - - - -A full suite of solutions is [in development](https://plutus-apps.readthedocs.io/en/latest/plutus/explanations/plutus-tools-component-descriptions.html). -See the [plutus-apps](https://github.com/IntersectMBO/plutus-apps) repo and its accompanying [Plutus tools SDK user guide](https://plutus-apps.readthedocs.io/en/latest/) for more details. - -Some other alternatives include [cardano-transaction-lib](https://github.com/Plutonomicon/cardano-transaction-lib) and [mesh](https://meshjs.dev/). -All these are based on the [Cardano API](https://github.com/IntersectMBO/cardano-api), a low-level API that provides the capability to do the off-chain work with a local running node. diff --git a/doc/docusaurus/docs/simple-example/plutus-tx-code.md b/doc/docusaurus/docs/simple-example/plutus-tx-code.md deleted file mode 100644 index 30a970ee07b..00000000000 --- a/doc/docusaurus/docs/simple-example/plutus-tx-code.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -sidebar_position: 20 ---- - -# Plutus Tx code - -Recall that Plutus Tx is a subset of Haskell. -It is the source language one uses to write Plutus validators. -A Plutus Tx program is compiled into Plutus Core, which is interpreted on-chain. -The full Plutus Tx code for the auction smart contract can be found at [AuctionValidator.hs](https://github.com/IntersectMBO/plutus-tx-template/blob/main/app/AuctionValidator.hs). - - - -## Data types - -First, let's define the following data types and instances for the validator: - - - -The purpose of `makeLift` and `unstableMakeIsData` will be explained later. - -Typically, writing a Plutus Tx validator script for a smart contract involves four data types: - -### 1. Contract parameters - -These are fixed properties of the contract. -In our example, it is the `AuctionParams` type, containing properties like seller and minimum bid. - -### 2. Datum - -This is part of a script UTXO. -It should be thought of as the state of the contract. -Our example requires only one piece of state: the current highest bid. -We use the `AuctionDatum` type to represent this. - -### 3. Redeemer - -This is an input to the Plutus script provided by the transaction that is trying to spend a script UTXO. -If a smart contract is regarded as a state machine, the redeemer would be the input that ticks the state machine. -In our example, it is the `AuctionRedeemer` type: one may either submit a new bid, or request to close the auction and pay out the winner and the seller, both of which lead to a new state of the auction. - -### 4. Script context - -This type contains the information of the transaction that the validator can inspect. -In our example, our validator verifies several conditions of the transaction; e.g., if it is a new bid, then it must be submitted before the auction's end time; the previous highest bid must be refunded to the previous bidder, etc. - -The script context type is fixed for each Plutus language version. -For Plutus V2, for example, it is `PlutusLedgerApi.V2.Contexts.ScriptContext`. - -> :pushpin: **NOTE** -> -> When writing a Plutus validator using Plutus Tx, it is advisable to turn off Haskell's `Prelude`. -> Usage of most functions and methods in `Prelude` should be replaced by their counterparts in the `plutus-tx` library, e.g., `PlutusTx.Eq.==`. - -## Main validator function - -Now we are ready to introduce our main validator function. -The beginning of the function looks like the following: - - - -Depending on whether this transaction is attempting to submit a new bid or to request payout, the validator validates the corresponding set of conditions. - -### Sufficient bid condition - -The `sufficientBid` condition verifies that the bid amount is sufficient: - - - -### Valid bid time condition - -The `validBidTime` condition verifies that the bid is submitted before the auction's deadline: - - - -Here, `to x` is the time interval ending at `x`, i.e., `(-∞, x]`. -`txInfoValidRange` is a transaction property. -It is the time interval in which the transaction is allowed to go through phase-1 validation. -`contains` takes two time intervals, and checks that the first interval completely includes the second. -Since the transaction may be validated at any point in the `txInfoValidRange` interval, we need to check that the entire interval lies within `(-∞, apEndTime params]`. - -The reason we need the `txInfoValidRange` interval instead of using the exact time the transaction is validated is due to [determinism](https://iohk.io/en/blog/posts/2021/09/06/no-surprises-transaction-validation-on-cardano/). -Using the exact time would be like calling a `getCurrentTime` function and branching based on the current time. -On the other hand, by using the `txInfoValidRange` interval, the same interval is always used by the same transaction. - -### Refunds previous highest bid condition - -The `refundsPreviousHighestBid` condition checks that the transaction pays the previous highest bid to the previous bidder: - - - -It uses `PlutusTx.find` to find the transaction output (a UTXO) that pays to the previous bidder the amount equivalent to the previous highest bid, and verifies that there is at least one such output. - -`lovelaceValue amt` constructs a `Value` with `amt` Lovelaces (the subunit of the Ada currency). -`Value` is a multi-asset type that represents a collection of assets, including Ada. -An asset is identified by a (symbol, token) pair, where the symbol represents the policy that controls the minting and burning of tokens, and the token represents a particular kind of token manipulated by the policy. -`(adaSymbol, adaToken)` is the special identifier for Ada/Lovelace. - -### Correct new datum condition - -The `correctNewDatum` condition verifies that the transaction produces a *continuing output* containing the correct datum (the new highest bid): - - - -A "continuing output" is a transaction output that pays to the same script address from which we are currently spending. -Exactly one continuing output must be present in this example so that the next bidder can place a new bid. -The new bid, in turn, will need to spend the continuing output and get validated by the same validator script. - -If the transaction is requesting a payout, the validator will then verify the other three conditions: `validPayoutTime`,`sellerGetsHighestBid` and `highestBidderGetsAsset`. -These conditions are similar to the ones already explained, so their details are omitted. - -### Compiling the validator - -Finally, we need to compile the validator written in Plutus Tx into Plutus Core, using the Plutus Tx compiler: - - - -The type of the compiled validator is `CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> ())`, where type `BuiltinData -> BuiltinData -> BuiltinData -> ()` is also known as the *untyped validator*. -An untyped validator takes three `BuiltinData` arguments, representing the serialized datum, redeemer, and script context. -The call to `PlutusTx.unsafeFromBuiltinData` is the reason we need the `PlutusTx.unstableMakeIsData` shown before, which derives `UnsafeFromData` instances. -And instead of returning a `Bool`, it simply returns `()`, and the validation succeeds if the script evaluates without error. - -Note that `AuctionParams` is an argument of neither the untyped validator nor the final UPLC program. -`AuctionParams` contains contract properties that don't change, so it is simply built into the validator. - -Since the Plutus Tx compiler compiles `a` into `CompiledCode a`, we first use `auctionUntypedValidator` to obtain an untyped validator. -It takes `AuctionParams`, and returns an untyped validator. -We then define the `auctionValidatorScript` function, which takes `AuctionParams` and returns the compiled Plutus Core program. - -To create the Plutus validator script for a particular auction, we call `auctionValidatorScript` with the appropriate `AuctionParams`. -We will then be able to launch the auction on-chain by submitting a transaction that outputs a script UTXO with `Nothing` as the datum. - -> :pushpin: **NOTE** -> -> It is worth noting that we must call `PlutusTx.compile` on the entire `auctionUntypedValidator`, rather than applying it to `params` before compiling, as in `$$(PlutusTx.compile [||auctionUntypedValidator params||])`. -> The latter won't work, because everything being compiled (inside `[||...||]`) must be known at compile time, but `params` is not: it can differ at runtime depending on what kind of auction we want to run. -> Instead, we compile the entire `auctionUntypedValidator` into Plutus Core, then use `liftCode` to lift `params` into a Plutus Core term, and apply the compiled `auctionUntypedValidator` to it at the Plutus Core level. -> To do so, we need the `Lift` instance for `AuctionParams`, derived via `PlutusTx.makeLift`. - diff --git a/doc/docusaurus/docs/simple-example/simple-example.md b/doc/docusaurus/docs/simple-example/simple-example.md deleted file mode 100644 index f181751a844..00000000000 --- a/doc/docusaurus/docs/simple-example/simple-example.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Overview - -:::caution -This conceptual guide to an auction smart contract in Plutus introduces fundamentals for educational use. -However, it is not optimized for security or efficiency and should not be deployed in production environments. -This example simplifies some security aspects, leading to potential vulnerabilities. -For detailed insights on developing secure smart contracts, please refer to the **[Cardano Plutus Script Vulnerability Guide](https://library.mlabs.city/common-plutus-security-vulnerabilities)** by MLabs. -::: - -## About this example - -This example presents Plutus Tx code for a smart contract that controls the auction of an asset, which can be executed on the Cardano blockchain. -In a sense, the smart contract is acting as the auctioneer in that it enforces certain rules and requirements in order for the auction to occur successfully. - - - -Plutus Tx is a high-level language for writing the validation logic of the contract, the logic that determines whether a transaction is allowed to spend a UTXO. -Plutus Tx is not a new language, but rather a subset of Haskell, and it is compiled into Plutus Core, a low-level language based on higher-order polymorphic lambda calculus. -Plutus Core is the code that runs on-chain, i.e., by every node validating the transaction, using an interpreter known as the CEK machine. -A Plutus Core program included in a Cardano transaction is often referred to as Plutus script or Plutus validator. - - - -To develop and deploy a smart contract, you would also need off-chain code for building transactions, submitting transactions, deploying smart contracts, querying for available UTXOs on the chain and so on. -You may also want a front-end interface for your smart contract for better user experiences. -In this example, we are not covering these aspects. - - - - -Before we get to the Plutus Tx code, let's briefly go over some basic concepts, including UTXO, EUTXO, datum, redeemer, and script context. - diff --git a/doc/docusaurus/docs/using-plutus-tx/advanced-plutus-tx-concepts/optimizing-scripts-with-asData.md b/doc/docusaurus/docs/using-plutus-tx/advanced-plutus-tx-concepts/optimizing-scripts-with-asData.md index e4898dacb1c..72082e30a20 100644 --- a/doc/docusaurus/docs/using-plutus-tx/advanced-plutus-tx-concepts/optimizing-scripts-with-asData.md +++ b/doc/docusaurus/docs/using-plutus-tx/advanced-plutus-tx-concepts/optimizing-scripts-with-asData.md @@ -4,40 +4,40 @@ sidebar_position: 20 # Optimizing scripts with `asData` -The Plutus libraries contain a `PlutusTx.asData` module that contains Template Haskell (TH) code for encoding algebraic data types (ADTs) as `Data` objects in Plutus Core, as opposed to sums-of-products terms. -In general, `asData` pushes the burden of a computation nearer to where a value is used, in a crude sense making the evaluation less strict and more lazy. +The Plutus libraries contain a `PlutusTx.asData` module that contains Template Haskell (TH) code for encoding algebraic data types (ADTs) as `Data` objects in Plutus Core, as opposed to sums-of-products terms. +In general, `asData` pushes the burden of a computation nearer to where a value is used, in a crude sense making the evaluation less strict and more lazy. This is intended for expert Plutus developers. ## Purpose -Values stored in datums or redeemers need to be encoded into `Data` objects. -When writing and optimizing a Plutus script, one of the challenges is finding the right approach to handling `Data` objects and how expensive that method will be. -To make an informed decision, you may need to benchmark and profile your smart contract code to measure its actual resource consumption. +Values stored in datums or redeemers need to be encoded into `Data` objects. +When writing and optimizing a Plutus script, one of the challenges is finding the right approach to handling `Data` objects and how expensive that method will be. +To make an informed decision, you may need to benchmark and profile your smart contract code to measure its actual resource consumption. The primary purpose of `asData` is to give you more options for how you want to handle `Data`. ## Choice of two approaches -When handling `Data` objects, you have a choice of two pathways. -It is up to you to determine which pathway to use depending on your particular use case. +When handling `Data` objects, you have a choice of two pathways. +It is up to you to determine which pathway to use depending on your particular use case. There are trade offs in performance and where errors occur. ### Approach one: proactively do all of the parsing -The first approach is to parse the object immediately (using `fromBuiltinData`) into a native Plutus Core datatype, which will also identify any problems with the structuring of the object. +The first approach is to parse the object immediately (using `fromBuiltinData`) into a native Plutus Core datatype, which will also identify any problems with the structuring of the object. However, this performs all the work up front. This is the normal style that has been promoted in the past. ### Approach two: only do the parsing if and when necessary -In the second approach, the script doesn't do any parsing work immediately, and instead does it later, when it needs to. -It might be that this saves you a lot of work, because you may never need to parse the entire object. +In the second approach, the script doesn't do any parsing work immediately, and instead does it later, when it needs to. +It might be that this saves you a lot of work, because you may never need to parse the entire object. Instead, the script will just carry the item around as a `Data` object. -Using this method, every time the script uses the object, it will look at it to find out if it has the right shape. +Using this method, every time the script uses the object, it will look at it to find out if it has the right shape. If it does have the right shape, it will deconstruct the `Data` object and do its processing; if -not, it will throw an error. -This work may be repeated depending on how your script is written. +not, it will throw an error. +This work may be repeated depending on how your script is written. In some cases, you might do less work, in some cases you might do more work, depending on your specific use case. The Plutus Tx library provides some helper functions to make this second style easier to do, in the form of the `asData` function. @@ -46,13 +46,13 @@ The Plutus Tx library provides some helper functions to make this second style e The `asData` function takes the definition of a data type and replaces it with an equivalent definition whose representation uses `Data` directly. -For example, if we wanted to use it on the types from the [auction example](simple-example/simple-example.md), we would put the datatype declarations inside a Template Haskell quote and call `asData` on it. +For example, if we wanted to use it on the types from the [auction example](../../auction-smart-contract/on-chain-code.md), we would put the datatype declarations inside a Template Haskell quote and call `asData` on it. This is normal Template Haskell that just generates new Haskell source, so you can see the code that it generates with `{-# OPTIONS_GHC-ddump-splices #-}` but it will look something like this: -``` +``` PlutusTx.asData [d| data Bid' = Bid' {bBidder' :: PubKeyHash, bAmount' :: Lovelace} @@ -84,22 +84,22 @@ That is: - It creates a newtype wrapper around `BuiltinData` - It creates pattern synonyms corresponding to each of the constructors you wrote -This lets you write code "as if" you were using the original declaration that you wrote, while in fact the pattern synonyms are handling conversion to/from `Data` for you. -But any values of this type actually are represented with `Data`. +This lets you write code "as if" you were using the original declaration that you wrote, while in fact the pattern synonyms are handling conversion to/from `Data` for you. +But any values of this type actually are represented with `Data`. That means that when we newtype-derive the instances for converting to and from `Data` we get the instances for `BuiltinData` - which are free! ### Nested fields -The most important caveat to using `asData` is that `Data` objects encoding datatypes must also encode the *fields* of the datatype as `Data`. +The most important caveat to using `asData` is that `Data` objects encoding datatypes must also encode the *fields* of the datatype as `Data`. However, `asData` tries to make the generated code a drop-in replacement for the original code, which means that when using the pattern synonyms they try to give you the fields as they were originally defined, which means *not* encoded as `Data`. -For example, in the `Bid` case above the `bAmount` field is originally defined to have type `Lovelace` which is a newtype around a Plutus Core builtin integer. -However, since we are using `asData`, we need to encode the field into `Data` in order to store it. +For example, in the `Bid` case above the `bAmount` field is originally defined to have type `Lovelace` which is a newtype around a Plutus Core builtin integer. +However, since we are using `asData`, we need to encode the field into `Data` in order to store it. That means that when you construct a `Bid` object you must take the `Integer` that you start with and convert it to `Data`, and when you pattern match on a `Bid` object you do the reverse conversion. -These conversions are potentially expensive! -If the `bAmount` field was a complex data structure, then every time we constructed or deconstructed a `Bid` object we would need to convert that datastructure to or from `Data`. +These conversions are potentially expensive! +If the `bAmount` field was a complex data structure, then every time we constructed or deconstructed a `Bid` object we would need to convert that datastructure to or from `Data`. Whether or not this is a problem depends on the precise situation, but in general: - If the field is a builtin integer or bytestring or a wrapper around those, it is probably cheap @@ -116,6 +116,6 @@ There are a number of tradeoffs to consider: 2. If it is important to check that the entire structure is well-formed, then it is better to parse it up-front, since the conversion will check the entire structure for well-formedness immediately, rather than checking only the parts that are used when they are used. 3. If you do not want to use `asData` for the types of the fields, then it may be better to not use it at all in order to avoid conversion penalties at the use sites. -Which approach is better is an empirical question and may vary in different cases. -A single script may wish to use different approaches in different places. +Which approach is better is an empirical question and may vary in different cases. +A single script may wish to use different approaches in different places. For example, your datum might contain a large state object which is usually only inspected in part (a good candidate for `asData`), whereas your redeemer might be a small object which is inspected frequently to determine what to do (a good candidate for a native Plutus Tx datatype). diff --git a/doc/docusaurus/static/code/AuctionMintingPolicy.hs b/doc/docusaurus/static/code/AuctionMintingPolicy.hs new file mode 100644 index 00000000000..388f63ede34 --- /dev/null +++ b/doc/docusaurus/static/code/AuctionMintingPolicy.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE Strict #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -fno-full-laziness #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-spec-constr #-} +{-# OPTIONS_GHC -fno-specialise #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fno-unbox-small-strict-fields #-} +{-# OPTIONS_GHC -fno-unbox-strict-fields #-} +{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.0.0 #-} + +module AuctionMintingPolicy where + +import PlutusCore.Version (plcVersion100) +import PlutusLedgerApi.V1.Value (flattenValue) +import PlutusLedgerApi.V2 (PubKeyHash, ScriptContext (..), TxInfo (..)) +import PlutusLedgerApi.V2.Contexts (ownCurrencySymbol, txSignedBy) +import PlutusTx +import PlutusTx.Prelude qualified as PlutusTx + +-- BLOCK1 +type AuctionMintingParams = PubKeyHash +type AuctionMintingRedeemer = () + +{-# INLINEABLE auctionTypedMintingPolicy #-} +auctionTypedMintingPolicy :: + AuctionMintingParams -> + AuctionMintingRedeemer -> + ScriptContext -> + Bool +auctionTypedMintingPolicy pkh _redeemer ctx = + txSignedBy txInfo pkh PlutusTx.&& mintedExactlyOneToken + where + txInfo = scriptContextTxInfo ctx + mintedExactlyOneToken = case flattenValue (txInfoMint txInfo) of + [(currencySymbol, _tokenName, quantity)] -> + currencySymbol PlutusTx.== ownCurrencySymbol ctx PlutusTx.&& quantity PlutusTx.== 1 + _ -> False +-- BLOCK2 + +auctionUntypedMintingPolicy :: + AuctionMintingParams -> + BuiltinData -> + BuiltinData -> + PlutusTx.BuiltinUnit +auctionUntypedMintingPolicy pkh redeemer ctx = + PlutusTx.check + ( auctionTypedMintingPolicy + pkh + (PlutusTx.unsafeFromBuiltinData redeemer) + (PlutusTx.unsafeFromBuiltinData ctx) + ) + +auctionMintingPolicyScript :: + AuctionMintingParams -> + CompiledCode (BuiltinData -> BuiltinData -> PlutusTx.BuiltinUnit) +auctionMintingPolicyScript pkh = + $$(PlutusTx.compile [||auctionUntypedMintingPolicy||]) + `PlutusTx.unsafeApplyCode` PlutusTx.liftCode plcVersion100 pkh diff --git a/doc/docusaurus/static/code/AuctionValidator.hs b/doc/docusaurus/static/code/AuctionValidator.hs index f719b04351f..df980bbab05 100644 --- a/doc/docusaurus/static/code/AuctionValidator.hs +++ b/doc/docusaurus/static/code/AuctionValidator.hs @@ -1,5 +1,8 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE MultiParamTypeClasses #-} @@ -8,84 +11,110 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE Strict #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} - +{-# OPTIONS_GHC -fno-full-laziness #-} {-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} {-# OPTIONS_GHC -fno-omit-interface-pragmas #-} -{-# OPTIONS_GHC -fno-full-laziness #-} {-# OPTIONS_GHC -fno-spec-constr #-} {-# OPTIONS_GHC -fno-specialise #-} {-# OPTIONS_GHC -fno-strictness #-} -{-# OPTIONS_GHC -fno-unbox-strict-fields #-} {-# OPTIONS_GHC -fno-unbox-small-strict-fields #-} +{-# OPTIONS_GHC -fno-unbox-strict-fields #-} {-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.0.0 #-} module AuctionValidator where +import GHC.Generics (Generic) + import PlutusCore.Version (plcVersion100) -import PlutusLedgerApi.V1 (Lovelace, POSIXTime, PubKeyHash, Value) -import PlutusLedgerApi.V1.Address (pubKeyHashAddress) +import PlutusLedgerApi.V1 (Lovelace, POSIXTime, PubKeyHash) +import PlutusLedgerApi.V1.Address (toPubKeyHash) import PlutusLedgerApi.V1.Interval (contains) -import PlutusLedgerApi.V1.Value (lovelaceValue) -import PlutusLedgerApi.V2 (Datum (..), OutputDatum (..), ScriptContext (..), TxInfo (..), - TxOut (..), from, to) +import PlutusLedgerApi.V1.Value (lovelaceValueOf, valueOf) +import PlutusLedgerApi.V2 (CurrencySymbol, Datum (..), OutputDatum (..), ScriptContext (..), + TokenName, TxInfo (..), TxOut (..), from, to) import PlutusLedgerApi.V2.Contexts (getContinuingOutputs) import PlutusTx import PlutusTx.AsData qualified as PlutusTx +import PlutusTx.Blueprint import PlutusTx.Prelude qualified as PlutusTx import PlutusTx.Show qualified as PlutusTx -- BLOCK1 +-- AuctionValidator.hs data AuctionParams = AuctionParams - { apSeller :: PubKeyHash, - -- ^ Seller's wallet address. The highest bid (if exists) will be sent to the seller. - -- If there is no bid, the asset auctioned will be sent to the seller. - apAsset :: Value, - -- ^ The asset being auctioned. It can be a single token, multiple tokens of the same - -- kind, or tokens of different kinds, and the token(s) can be fungible or non-fungible. - -- These can all be encoded as a `Value`. - apMinBid :: Lovelace, - -- ^ The minimum bid in Lovelace. - apEndTime :: POSIXTime - -- ^ The deadline for placing a bid. This is the earliest time the auction can be closed. + { apSeller :: PubKeyHash + -- ^ Seller's public key hash. The highest bid (if exists) will be sent to the seller. + -- If there is no bid, the asset auctioned will be sent to the seller. + , apCurrencySymbol :: CurrencySymbol + -- ^ The currency symbol of the token being auctioned. + , apTokenName :: TokenName + -- ^ The name of the token being auctioned. + -- These can all be encoded as a `Value`. + , apMinBid :: Lovelace + -- ^ The minimum bid in Lovelace. + , apEndTime :: POSIXTime + -- ^ The deadline for placing a bid. This is the earliest time the auction can be closed. } + deriving stock (Generic) + deriving anyclass (HasBlueprintDefinition) PlutusTx.makeLift ''AuctionParams +PlutusTx.makeIsDataSchemaIndexed ''AuctionParams [('AuctionParams, 0)] data Bid = Bid - { bBidder :: PubKeyHash, - -- ^ Bidder's wallet address. - bAmount :: Lovelace - -- ^ Bid amount in Lovelace. + { bAddr :: PlutusTx.BuiltinByteString + -- ^ Bidder's wallet address + , bPkh :: PubKeyHash + -- ^ Bidder's public key hash. + , bAmount :: Lovelace + -- ^ Bid amount in Lovelace. } + deriving stock (Generic) + deriving anyclass (HasBlueprintDefinition) PlutusTx.deriveShow ''Bid -PlutusTx.unstableMakeIsData ''Bid +PlutusTx.makeIsDataSchemaIndexed ''Bid [('Bid, 0)] instance PlutusTx.Eq Bid where {-# INLINEABLE (==) #-} bid == bid' = - bBidder bid PlutusTx.== bBidder bid' - PlutusTx.&& bAmount bid PlutusTx.== bAmount bid' + bPkh bid + PlutusTx.== bPkh bid' + PlutusTx.&& bAmount bid + PlutusTx.== bAmount bid' --- | Datum represents the state of a smart contract. In this case --- it contains the highest bid so far (if exists). -newtype AuctionDatum = AuctionDatum { adHighestBid :: Maybe Bid } - -PlutusTx.unstableMakeIsData ''AuctionDatum +{- | Datum represents the state of a smart contract. In this case +it contains the highest bid so far (if exists). +-} +newtype AuctionDatum = AuctionDatum {adHighestBid :: Maybe Bid} + deriving stock (Generic) + deriving newtype + ( HasBlueprintDefinition + , PlutusTx.ToData + , PlutusTx.FromData + , PlutusTx.UnsafeFromData + ) --- | Redeemer is the input that changes the state of a smart contract. --- In this case it is either a new bid, or a request to close the auction --- and pay out the seller and the highest bidder. +{- | Redeemer is the input that changes the state of a smart contract. +In this case it is either a new bid, or a request to close the auction +and pay out the seller and the highest bidder. +-} data AuctionRedeemer = NewBid Bid | Payout + deriving stock (Generic) + deriving anyclass (HasBlueprintDefinition) -PlutusTx.unstableMakeIsData ''AuctionRedeemer --- BLOCK2 - +PlutusTx.makeIsDataSchemaIndexed ''AuctionRedeemer [('NewBid, 0), ('Payout, 1)] +-- BLOCK2 +-- AuctionValidator.hs {-# INLINEABLE auctionTypedValidator #-} --- | Given the auction parameters, determines whether the transaction is allowed to --- spend the UTXO. + +{- | Given the auction parameters, determines whether the transaction is allowed to +spend the UTXO. +-} auctionTypedValidator :: AuctionParams -> AuctionDatum -> @@ -100,98 +129,118 @@ auctionTypedValidator params (AuctionDatum highestBid) redeemer ctx@(ScriptConte NewBid bid -> [ -- The new bid must be higher than the highest bid. -- If this is the first bid, it must be at least as high as the minimum bid. - sufficientBid bid, - -- The bid is not too late. - validBidTime, - -- The previous highest bid should be refunded. - refundsPreviousHighestBid, - -- A correct new datum is produced, containing the new highest bid. - correctNewDatum bid + sufficientBid bid + , -- The bid is not too late. + validBidTime + , -- The previous highest bid should be refunded. + refundsPreviousHighestBid + , -- A correct new datum is produced, containing the new highest bid. + correctOutput bid ] Payout -> [ -- The payout is not too early. - validPayoutTime, - -- The seller gets the highest bid. - sellerGetsHighestBid, - -- The highest bidder gets the asset. + validPayoutTime + , -- The seller gets the highest bid. + sellerGetsHighestBid + , -- The highest bidder gets the asset. highestBidderGetsAsset ] -- BLOCK3 +-- AuctionValidator.hs sufficientBid :: Bid -> Bool - sufficientBid (Bid _ amt) = case highestBid of - Just (Bid _ amt') -> amt PlutusTx.> amt' - Nothing -> amt PlutusTx.>= apMinBid params + sufficientBid (Bid _ _ amt) = case highestBid of + Just (Bid _ _ amt') -> amt PlutusTx.> amt' + Nothing -> amt PlutusTx.>= apMinBid params -- BLOCK4 +-- AuctionValidator.hs validBidTime :: Bool - validBidTime = to (apEndTime params) `contains` txInfoValidRange txInfo + ~validBidTime = to (apEndTime params) `contains` txInfoValidRange txInfo -- BLOCK5 +-- AuctionValidator.hs refundsPreviousHighestBid :: Bool - refundsPreviousHighestBid = case highestBid of + ~refundsPreviousHighestBid = case highestBid of Nothing -> True - Just (Bid bidder amt) -> + Just (Bid _ bidderPkh amt) -> case PlutusTx.find - (\o -> txOutAddress o PlutusTx.== pubKeyHashAddress bidder - PlutusTx.&& txOutValue o PlutusTx.== lovelaceValue amt) + ( \o -> + (toPubKeyHash (txOutAddress o) PlutusTx.== Just bidderPkh) + PlutusTx.&& (lovelaceValueOf (txOutValue o) PlutusTx.== amt) + ) (txInfoOutputs txInfo) of Just _ -> True - Nothing -> PlutusTx.traceError ("Not found: refund output") + Nothing -> PlutusTx.traceError "Not found: refund output" -- BLOCK6 - correctNewDatum :: Bid -> Bool - correctNewDatum bid = case getContinuingOutputs ctx of - [o] -> case txOutDatum o of - OutputDatum (Datum newDatum) -> case PlutusTx.fromBuiltinData newDatum of - Just bid' -> - PlutusTx.traceIfFalse - ( "Invalid output datum: expected " - PlutusTx.<> PlutusTx.show bid - PlutusTx.<> ", but got " - PlutusTx.<> PlutusTx.show bid' - ) - (bid PlutusTx.== bid') - Nothing -> - PlutusTx.traceError - ( "Failed to decode output datum: " - PlutusTx.<> PlutusTx.show newDatum - ) - OutputDatumHash _ -> - PlutusTx.traceError "Expected OutputDatum, got OutputDatumHash" - NoOutputDatum -> - PlutusTx.traceError "Expected OutputDatum, got NoOutputDatum" +-- AuctionValidator.hs + currencySymbol :: CurrencySymbol + currencySymbol = apCurrencySymbol params + + tokenName :: TokenName + tokenName = apTokenName params + + correctOutput :: Bid -> Bool + correctOutput bid = case getContinuingOutputs ctx of + [o] -> + let correctOutputDatum = case txOutDatum o of + OutputDatum (Datum newDatum) -> case PlutusTx.fromBuiltinData newDatum of + Just (AuctionDatum (Just bid')) -> + PlutusTx.traceIfFalse + "Invalid output datum: contains a different Bid than expected" + (bid PlutusTx.== bid') + Just (AuctionDatum Nothing) -> + PlutusTx.traceError "Invalid output datum: expected Just Bid, got Nothing" + Nothing -> + PlutusTx.traceError "Failed to decode output datum" + OutputDatumHash _ -> + PlutusTx.traceError "Expected OutputDatum, got OutputDatumHash" + NoOutputDatum -> + PlutusTx.traceError "Expected OutputDatum, got NoOutputDatum" + + outValue = txOutValue o + + correctOutputValue = + (lovelaceValueOf outValue PlutusTx.== bAmount bid) + PlutusTx.&& (valueOf outValue currencySymbol tokenName PlutusTx.== 1) + in correctOutputDatum PlutusTx.&& correctOutputValue os -> PlutusTx.traceError ( "Expected exactly one continuing output, got " PlutusTx.<> PlutusTx.show (PlutusTx.length os) ) -- BLOCK7 +-- AuctionValidator.hs validPayoutTime :: Bool - validPayoutTime = from (apEndTime params) `contains` txInfoValidRange txInfo + ~validPayoutTime = from (apEndTime params) `contains` txInfoValidRange txInfo sellerGetsHighestBid :: Bool - sellerGetsHighestBid = case highestBid of + ~sellerGetsHighestBid = case highestBid of Nothing -> True - Just (Bid _ amt) -> + Just bid -> case PlutusTx.find ( \o -> - txOutAddress o PlutusTx.== pubKeyHashAddress (apSeller params) - PlutusTx.&& txOutValue o PlutusTx.== lovelaceValue amt + (toPubKeyHash (txOutAddress o) PlutusTx.== Just (apSeller params)) + PlutusTx.&& (lovelaceValueOf (txOutValue o) PlutusTx.== bAmount bid) ) (txInfoOutputs txInfo) of Just _ -> True - Nothing -> PlutusTx.traceError ("Not found: Output paid to seller") + Nothing -> PlutusTx.traceError "Not found: Output paid to seller" highestBidderGetsAsset :: Bool - highestBidderGetsAsset = case highestBid of - Nothing -> True - Just (Bid bidder _) -> - case PlutusTx.find - ( \o -> - txOutAddress o PlutusTx.== pubKeyHashAddress bidder - PlutusTx.&& txOutValue o PlutusTx.== apAsset params - ) - (txInfoOutputs txInfo) of - Just _ -> True - Nothing -> PlutusTx.traceError ("Not found: Output paid to highest bidder") + ~highestBidderGetsAsset = + let highestBidder = case highestBid of + -- If there are no bids, the asset should go back to the seller + Nothing -> apSeller params + Just bid -> bPkh bid + in case PlutusTx.find + ( \o -> + (toPubKeyHash (txOutAddress o) PlutusTx.== Just highestBidder) + PlutusTx.&& (valueOf (txOutValue o) currencySymbol tokenName PlutusTx.== 1) + ) + (txInfoOutputs txInfo) of + Just _ -> True + Nothing -> PlutusTx.traceError "Not found: Output paid to highest bidder" + -- BLOCK8 +-- AuctionValidator.hs {-# INLINEABLE auctionUntypedValidator #-} auctionUntypedValidator :: AuctionParams -> @@ -214,25 +263,31 @@ auctionValidatorScript :: auctionValidatorScript params = $$(PlutusTx.compile [||auctionUntypedValidator||]) `PlutusTx.unsafeApplyCode` PlutusTx.liftCode plcVersion100 params + -- BLOCK9 -PlutusTx.asData [d| - data Bid' = Bid' - { bBidder' :: PubKeyHash, - -- ^ Bidder's wallet address. - bAmount' :: Lovelace - -- ^ Bid amount in Lovelace. - } - -- We can derive instances with the newtype strategy, and they - -- will be based on the instances for 'Data' - deriving newtype (Eq, Ord, PlutusTx.ToData, FromData, UnsafeFromData) - - -- don't do this for the datum, since it's just a newtype so - -- simply delegates to the underlying type - - -- | Redeemer is the input that changes the state of a smart contract. - -- In this case it is either a new bid, or a request to close the auction - -- and pay out the seller and the highest bidder. - data AuctionRedeemer' = NewBid' Bid | Payout' - deriving newtype (Eq, Ord, PlutusTx.ToData, FromData, UnsafeFromData) - |] +-- AuctionValidator.hs +PlutusTx.asData + [d| + data Bid' = Bid' + { bPkh' :: PubKeyHash + , -- \^ Bidder's wallet address. + bAmount' :: Lovelace + } + -- \^ Bid amount in Lovelace. + + -- We can derive instances with the newtype strategy, and they + -- will be based on the instances for 'Data' + deriving newtype (Eq, Ord, PlutusTx.ToData, FromData, UnsafeFromData) + + -- don't do this for the datum, since it's just a newtype so + -- simply delegates to the underlying type + + -- \| Redeemer is the input that changes the state of a smart contract. + -- In this case it is either a new bid, or a request to close the auction + -- and pay out the seller and the highest bidder. + data AuctionRedeemer' = NewBid' Bid | Payout' + deriving newtype (Eq, Ord, PlutusTx.ToData, FromData, UnsafeFromData) + |] + -- BLOCK10 +-- AuctionValidator.hs diff --git a/doc/docusaurus/static/code/bid.mjs b/doc/docusaurus/static/code/bid.mjs new file mode 100644 index 00000000000..5fd4b6cdeb8 --- /dev/null +++ b/doc/docusaurus/static/code/bid.mjs @@ -0,0 +1,137 @@ +import cbor from 'cbor' +import { + BlockfrostProvider, + MeshWallet, + Transaction, + deserializeDatum, + hexToString, + serializePlutusScript, + resolveScriptHash, + stringToHex +} from '@meshsdk/core' + +import fs from 'node:fs' + +const blockfrostKey = 'Replace with Blockfrost Project ID' +const blockchainProvider = new BlockfrostProvider(blockfrostKey) + +const previousTxHash = process.argv[2] +const bidder = process.argv[3] +const bidAmt = process.argv[4] + +const wallet = new MeshWallet({ + networkId: 0, + fetcher: blockchainProvider, + submitter: blockchainProvider, + key: { + type: 'root', + bech32: fs.readFileSync(`${bidder}.skey`).toString().trim() + } +}) + +const bidderAddr = fs.readFileSync(`${bidder}.addr`).toString() +const bidderPkh = fs.readFileSync(`${bidder}.pkh`).toString() + +const auctionValidatorBlueprint = JSON.parse( + fs.readFileSync('./plutus-auction-validator.json') +) + +const auctionValidator = { + code: cbor + .encode( + Buffer.from(auctionValidatorBlueprint.validators[0].compiledCode, 'hex') + ) + .toString('hex'), + version: 'V2' +} + +const auctionValidatorAddress = serializePlutusScript(auctionValidator).address + +const mintingPolicyBlueprint = JSON.parse( + fs.readFileSync('./plutus-auction-minting-policy.json') +) + +const mintingPolicy = { + code: cbor + .encode( + Buffer.from(mintingPolicyBlueprint.validators[0].compiledCode, 'hex') + ) + .toString('hex'), + version: 'V2' +} + +const mintingPolicyHash = resolveScriptHash( + mintingPolicy.code, + mintingPolicy.version +) + +const utxos = await blockchainProvider.fetchAddressUTxOs( + auctionValidatorAddress +) + +const utxoIn = utxos.find(utxo => { + return utxo.input.txHash == previousTxHash +}) + +if (!utxoIn) throw new Error(`utxo not found for ${previousTxHash}`) + +const datumIn = deserializeDatum(utxoIn.output.plutusData) + +var highestBidderAddress +var highestBidAmount + +if (datumIn.fields.length > 0) { + highestBidderAddress = hexToString(datumIn.fields[0].fields[0].bytes) + highestBidAmount = datumIn.fields[0].fields[2].int +} + +const bid = { + alternative: 0, + fields: [bidderAddr, bidderPkh, parseInt(bidAmt)] +} + +const redeemer = { + data: { + alternative: 0, + fields: [bid] + } +} + +const datumOut = { + alternative: 0, + fields: [bid] +} + +const tx = new Transaction({ initiator: wallet }) + .redeemValue({ + value: utxoIn, + script: auctionValidator, + redeemer: redeemer + }) + .sendAssets( + { + address: auctionValidatorAddress, + datum: { value: datumOut, inline: true } + }, + [ + { + unit: 'lovelace', + quantity: bidAmt + }, + { + unit: mintingPolicyHash + stringToHex('TokenToBeAuctioned'), + quantity: '1' + } + ] + ) + .setTimeToExpire('Replace with transaction expiration time') + +if (highestBidderAddress) { + tx.sendLovelace(highestBidderAddress, highestBidAmount.toString()) +} + +const unsignedTx = await tx.build() +const signedTx = await wallet.signTx(unsignedTx) +const txHash = await wallet.submitTx(signedTx) + +console.log(`Bid successful. Tx hash: ${txHash}`) diff --git a/doc/docusaurus/static/code/generate-keys.mjs b/doc/docusaurus/static/code/generate-keys.mjs new file mode 100644 index 00000000000..12da31023e7 --- /dev/null +++ b/doc/docusaurus/static/code/generate-keys.mjs @@ -0,0 +1,27 @@ +import { MeshWallet, deserializeAddress } from '@meshsdk/core' +import fs from 'node:fs' + +// generate a new secret key +const skey = MeshWallet.brew(true) + +// create a Mesh wallet with the secret key +const wallet = new MeshWallet({ + networkId: 0, + key: { + type: 'root', + bech32: skey + } +}) + +// obtain the address associated with the secret key +const address = wallet.getUnusedAddresses()[0] + +// derive PubKeyHash from the address +const pubKeyHash = deserializeAddress(address).pubKeyHash + +const filename = process.argv[2] + +// write the secret key, the address and the PubKeyHash to files +fs.writeFileSync(`${filename}.skey`, skey) +fs.writeFileSync(`${filename}.addr`, address) +fs.writeFileSync(`${filename}.pkh`, pubKeyHash) diff --git a/doc/docusaurus/static/code/mint-token-for-auction.mjs b/doc/docusaurus/static/code/mint-token-for-auction.mjs new file mode 100644 index 00000000000..63eddd341ba --- /dev/null +++ b/doc/docusaurus/static/code/mint-token-for-auction.mjs @@ -0,0 +1,86 @@ +import cbor from 'cbor' +import { + BlockfrostProvider, + MeshWallet, + Transaction, + serializePlutusScript, +} from '@meshsdk/core' + +import fs from 'node:fs' + +const blockfrostKey = 'Replace with Blockfrost Project ID' +const blockchainProvider = new BlockfrostProvider(blockfrostKey) + +const wallet = new MeshWallet({ + networkId: 0, + fetcher: blockchainProvider, + submitter: blockchainProvider, + key: { + type: 'root', + bech32: fs.readFileSync('seller.skey').toString().trim() + } +}) + +const auctionValidatorBlueprint = JSON.parse( + fs.readFileSync('./plutus-auction-validator.json') +) + +const auctionValidator = { + code: cbor + .encode( + Buffer.from(auctionValidatorBlueprint.validators[0].compiledCode, 'hex') + ) + .toString('hex'), + version: 'V2' +} + +const auctionValidatorAddress = serializePlutusScript(auctionValidator).address + +const mintingPolicyBlueprint = JSON.parse( + fs.readFileSync('./plutus-auction-minting-policy.json') +) + +const mintingPolicy = { + code: cbor + .encode( + Buffer.from(mintingPolicyBlueprint.validators[0].compiledCode, 'hex') + ) + .toString('hex'), + version: 'V2' +} + +// The `AuctionDatum` to be stored in the output. +const datumOut = { + alternative: 1, // Corresponds to `Nothing` + fields: [] +} + +// The token we are minting +const token = { + assetName: 'TokenToBeAuctioned', + assetQuantity: '1', + recipient: { + address: auctionValidatorAddress, + datum: { value: datumOut, inline: true } + } +} + +const walletAddress = wallet.getUsedAddresses()[0] + +// The redeemer for the minting policy, corresponding to `()`. +const redeemer = { + data: { + alternative: 0, + fields: [] + } +} + +const tx = new Transaction({ initiator: wallet }) +tx.mintAsset(mintingPolicy, token, redeemer) +const unsignedTx = await tx.setRequiredSigners([walletAddress]).build() +const signedTx = wallet.signTx(unsignedTx) +const txHash = await wallet.submitTx(signedTx) + +console.log( + `Minted a token at address ${auctionValidatorAddress}. Tx hash: ${txHash}` +) diff --git a/doc/docusaurus/static/code/send-lovelace.mjs b/doc/docusaurus/static/code/send-lovelace.mjs new file mode 100644 index 00000000000..2a4edcb2f78 --- /dev/null +++ b/doc/docusaurus/static/code/send-lovelace.mjs @@ -0,0 +1,29 @@ +import { BlockfrostProvider, MeshWallet, Transaction } from '@meshsdk/core' + +import fs from 'node:fs' + +const blockfrostKey = 'Replace with Blockfrost Project ID' +const blockchainProvider = new BlockfrostProvider(blockfrostKey) + +const recipient = fs.readFileSync(`${process.argv[2]}.addr`).toString() + +const wallet = new MeshWallet({ + networkId: 0, + fetcher: blockchainProvider, + submitter: blockchainProvider, + key: { + type: 'root', + bech32: fs.readFileSync('seller.skey').toString().trim() + } +}) + +// Send 1000 Ada +const unsignedTx = await new Transaction({ initiator: wallet }) + .sendLovelace(recipient, '1000000000') + .build() + +const signedTx = await wallet.signTx(unsignedTx) + +const txHash = await wallet.submitTx(signedTx) + +console.log(`1000 Ada sent. Recipient: ${recipient}, Tx hash: ${txHash}`)