Skip to content

Commit

Permalink
Merge pull request #9 from casper-network/part3
Browse files Browse the repository at this point in the history
part 3 tutorial
  • Loading branch information
melpadden authored Jun 17, 2024
2 parents 09f9dd5 + 89cf0ca commit ba054d4
Show file tree
Hide file tree
Showing 12 changed files with 552 additions and 0 deletions.
8 changes: 8 additions & 0 deletions nft_zero_to_hero/part3/auctions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.idea
.vscode
/target
Cargo.lock
.backend*
.builder*
/wasm
.env
5 changes: 5 additions & 0 deletions nft_zero_to_hero/part3/auctions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Changelog for `auctions`.

## [0.1.0] - 2024-06-06
### Added
- `flipper` module.
31 changes: 31 additions & 0 deletions nft_zero_to_hero/part3/auctions/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "auctions"
version = "0.1.0"
edition = "2021"

[dependencies]
odra = { version = "1.0.0-rc.1", features = [], default-features = false }
odra-modules = "1.0.0-rc.1"

[dev-dependencies]
odra-test = { version = "1.0.0-rc.1", features = [], default-features = false }

[build-dependencies]
odra-build = { version = "1.0.0-rc.1", features = [], default-features = false }

[[bin]]
name = "auctions_build_contract"
path = "bin/build_contract.rs"
test = false

[[bin]]
name = "auctions_build_schema"
path = "bin/build_schema.rs"
test = false

[profile.release]
codegen-units = 1
lto = true

[profile.dev.package."*"]
opt-level = 3
2 changes: 2 additions & 0 deletions nft_zero_to_hero/part3/auctions/Odra.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[[contracts]]
fqn = "mycontract::Mycontract"
31 changes: 31 additions & 0 deletions nft_zero_to_hero/part3/auctions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# auctions

## Usage
It's recommended to install
[cargo-odra](https://github.com/odradev/cargo-odra) first.

### Build

```
$ cargo odra build
```
To build a wasm file, you need to pass the -b parameter.
The result files will be placed in `${project-root}/wasm` directory.

```
$ cargo odra build -b casper
```

### Test
To run test on your local machine, you can basically execute the command:

```
$ cargo odra test
```

To test actual wasm files against a backend,
you need to specify the backend passing -b argument to `cargo-odra`.

```
$ cargo odra test -b casper
```
5 changes: 5 additions & 0 deletions nft_zero_to_hero/part3/auctions/bin/build_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![doc = "Binary for building wasm files from odra contracts."]
#![no_std]
#![no_main]
#![allow(unused_imports, clippy::single_component_path_imports)]
use auctions;
69 changes: 69 additions & 0 deletions nft_zero_to_hero/part3/auctions/bin/build_schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#![doc = "Binary for building schema definitions from odra contracts."]
#[allow(unused_imports)]
use auctions;

#[cfg(not(target_arch = "wasm32"))]
extern "Rust" {
fn module_schema() -> odra::contract_def::ContractBlueprint;
fn casper_contract_schema() -> odra::schema::casper_contract_schema::ContractSchema;
}

#[cfg(not(target_arch = "wasm32"))]
fn main() {
let module = std::env::var("ODRA_MODULE").expect("ODRA_MODULE environment variable is not set");
let module = to_snake_case(&module);

let contract_schema = unsafe { crate::casper_contract_schema() };
let module_schema = unsafe { crate::module_schema() };

write_schema_file(
"resources/casper_contract_schemas",
&module,
contract_schema
.as_json()
.expect("Failed to convert schema to JSON")
);

write_schema_file(
"resources/legacy",
&module,
module_schema
.as_json()
.expect("Failed to convert schema to JSON")
);
}

fn write_schema_file(path: &str, module: &str, json: String) {
if !std::path::Path::new(path).exists() {
std::fs::create_dir_all(path).expect("Failed to create resources directory");
}
let filename = format!("{}/{}_schema.json", path, module);
let mut schema_file = std::fs::File::create(filename).expect("Failed to create schema file");

std::io::Write::write_all(&mut schema_file, &json.into_bytes())
.expect("Failed to write to schema file");
}

fn to_snake_case(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
let mut is_first = true;

while let Some(c) = chars.next() {
if c.is_uppercase() {
if !is_first {
if let Some(next) = chars.peek() {
if next.is_lowercase() {
result.push('_');
}
}
}
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
is_first = false;
}

result
}
6 changes: 6 additions & 0 deletions nft_zero_to_hero/part3/auctions/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! Odra's contracts build script.
/// Uses the ENV variable `ODRA_MODULE` to set the `odra_module` cfg flag.
pub fn main() {
odra_build::build();
}
1 change: 1 addition & 0 deletions nft_zero_to_hero/part3/auctions/rust-toolchain
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nightly-2024-02-09
189 changes: 189 additions & 0 deletions nft_zero_to_hero/part3/auctions/src/auctions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
use odra::{
args::Maybe,
casper_types::{U256, U512},
module::Module,
Address, ContractRef, Mapping, SubModule, Var,
};
use odra_modules::cep78::token::Cep78ContractRef;
use odra_modules::{access::Ownable, security::Pauseable};

#[odra::module]
/// This contract facilitates NFT auctions, allowing users to create and participate in auctions for CEP-78 NFTs.
pub struct Auctions {
/// Ownable submodule for managing contract ownership and permissions.
ownable: SubModule<Ownable>,
/// Pauseable submodule for pausing/unpausing contract functionality.
pausable: SubModule<Pauseable>,
/// Storage for active auctions, indexed by a unique auction ID.
auctions: Mapping<U256, Auction>,
/// Counter to track the total number of auctions created.
auction_counter: Var<U256>,
/// Minimum allowed duration for an auction, set by the contract owner.
min_auction_duration: Var<u64>,
}

#[odra::odra_error]
/// Errors that may occur during the contract execution.
pub enum Error {
/// Invalid auction duration, shorter than the minimum allowed.
InvalidAuctionDuration = 1,
/// Invalid bid amount, lower than the starting price or the current highest bid.
InvalidBid = 2,
/// Attempted to end an auction that has already ended.
AuctionHasEnded = 3,
/// Attempted to end an auction that is still in progress.
AuctionStillInProgress = 4,
}

#[odra::odra_type]
/// Represents an active auction for an NFT.
pub struct Auction {
/// Address of the seller who initiated the auction.
seller: Address,
/// Address of the CEP-78 NFT contract.
nft_contract: Address,
/// ID of the NFT being auctioned.
nft_id: u64,
/// Starting price of the auction in CSPR.
starting_price: U512,
/// Timestamp of when the auction ends.
ends_at: u64,
/// Optional address of the highest bidder (None if no bids yet).
highest_bidder: Option<Address>,
/// Amount of the highest bid in CSPR.
highest_bid: U512,
}

#[odra::module]
impl Auctions {
/// Initializes the contract, setting the owner (optional) and minimum auction duration.
pub fn init(&mut self, admin: Option<Address>, min_auction_duration: u64) {
self.ownable.init();
if let Some(a) = admin {
self.ownable.transfer_ownership(&a); // Transfer ownership to the provided admin
}
self.auction_counter.set(U256::one()); // Start auction counter from 1
self.min_auction_duration.set(min_auction_duration);
}

/**********
* TRANSACTIONS
**********/

/// Creates a new auction for a CEP-78 NFT.
pub fn create_auction(
&mut self,
nft_contract: Address,
nft_id: u64,
starting_price: U512,
duration: u64,
) {
self.pausable.require_not_paused(); // Ensure contract is not paused

if duration < self.min_auction_duration.get_or_default() {
self.env().revert(Error::InvalidAuctionDuration) // Revert if duration is too short
}

let seller = self.env().caller();

// Transfer the NFT to the auction contract
Cep78ContractRef::new(self.env(), nft_contract).transfer(
Maybe::Some(nft_id),
Maybe::None,
seller,
self.env().self_address(),
);

// Create and store the auction details
let auction = Auction {
nft_contract,
nft_id,
seller,
starting_price,
highest_bid: U512::zero(),
highest_bidder: None,
ends_at: self.env().get_block_time() + duration,
};
self.auctions
.set(&self.auction_counter.get_or_default(), auction);
self.auction_counter.add(U256::one()); // Increment auction counter
}

/// Places a bid on an active auction.
#[odra(payable)] // Indicates this function accepts CSPR payments
pub fn bid(&mut self, auction_id: U256) {
self.pausable.require_not_paused();

let bidder = self.env().caller();
let amount = self.env().attached_value(); // Get the attached CSPR amount
let mut auction = self.auctions.get(&auction_id).unwrap();

// Validate bid amount
if amount < auction.starting_price || amount < auction.highest_bid {
self.env().revert(Error::InvalidBid);
}

// Check if auction is still ongoing
if self.env().get_block_time() > auction.ends_at {
self.env().revert(Error::AuctionHasEnded);
}

// Refund the previous highest bidder (if any)
if let Some(highest_bidder) = auction.highest_bidder {
self.env()
.transfer_tokens(&highest_bidder, &auction.highest_bid);
}

// Update the auction with the new highest bid and bidder
auction.highest_bid = amount;
auction.highest_bidder = Some(bidder);
self.auctions.set(&auction_id, auction);
}

/// Ends an auction and distributes the NFT and funds accordingly.
pub fn end_auction(&mut self, auction_id: U256) {
self.pausable.require_not_paused();
let auction = self.auctions.get(&auction_id).unwrap();

// Check if auction has ended
if self.env().get_block_time() < auction.ends_at {
self.env().revert(Error::AuctionStillInProgress);
}

// Transfer the NFT and funds
if let Some(winner) = auction.highest_bidder {
Cep78ContractRef::new(self.env(), auction.nft_contract).transfer(
Maybe::Some(auction.nft_id),
Maybe::None,
self.env().self_address(),
winner,
);
self.env()
.transfer_tokens(&auction.seller, &auction.highest_bid);
} else {
// No bids were placed, return the NFT to the seller
Cep78ContractRef::new(self.env(), auction.nft_contract).transfer(
Maybe::Some(auction.nft_id),
Maybe::None,
self.env().self_address(),
auction.seller,
);
}
}

/**********
* ADMIN
**********/

/// Pauses the contract, preventing further interactions.
pub fn pause(&mut self) {
self.ownable.assert_owner(&self.env().caller());
self.pausable.pause();
}

/// Unpauses the contract, resuming normal operation.
pub fn unpause(&mut self) {
self.ownable.assert_owner(&self.env().caller());
self.pausable.unpause();
}
}
5 changes: 5 additions & 0 deletions nft_zero_to_hero/part3/auctions/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]
extern crate alloc;

pub mod auctions;
Loading

0 comments on commit ba054d4

Please sign in to comment.