diff --git a/.github/workflows/tests-rs-02.yml b/.github/workflows/tests-rs-02.yml index 552a96a7..95e69cb0 100644 --- a/.github/workflows/tests-rs-02.yml +++ b/.github/workflows/tests-rs-02.yml @@ -10,5 +10,5 @@ jobs: - uses: actions/checkout@v4 - name: Install and test modules run: | - cd ./contract-rs/02-owner-claims-money + cd ./contract-rs/02-winner-gets-nft cargo test diff --git a/.github/workflows/tests-rs-03.yml b/.github/workflows/tests-rs-03.yml index 8edc0f88..a826326c 100644 --- a/.github/workflows/tests-rs-03.yml +++ b/.github/workflows/tests-rs-03.yml @@ -10,5 +10,5 @@ jobs: - uses: actions/checkout@v4 - name: Install and test modules run: | - cd ./contract-rs/03-owner-claims-winner-gets-nft + cd ./contract-rs/03-bid-with-fts cargo test diff --git a/.github/workflows/tests-rs-04.yml b/.github/workflows/tests-rs-04.yml deleted file mode 100644 index fd26812a..00000000 --- a/.github/workflows/tests-rs-04.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: 04 - Tests Contract RS -on: push -jobs: - workflows: - strategy: - matrix: - platform: [ubuntu-latest, macos-latest] - runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v4 - - name: Install and test modules - run: | - cd ./contract-rs/04-ft-owner-claims-winner-gets-nft - cargo test diff --git a/.github/workflows/tests-ts-02.yml b/.github/workflows/tests-ts-02.yml index b6f13c59..1576d3a5 100644 --- a/.github/workflows/tests-ts-02.yml +++ b/.github/workflows/tests-ts-02.yml @@ -14,6 +14,6 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install and test modules run: | - cd ./contract-ts/02-owner-claims-money + cd ./contract-ts/02-winner-gets-nft yarn yarn test diff --git a/.github/workflows/tests-ts-03.yml b/.github/workflows/tests-ts-03.yml index 5788e4ec..95213795 100644 --- a/.github/workflows/tests-ts-03.yml +++ b/.github/workflows/tests-ts-03.yml @@ -14,6 +14,6 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install and test modules run: | - cd ./contract-ts/03-owner-claims-winner-gets-nft + cd ./contract-ts/03-bid-with-fts yarn yarn test diff --git a/.github/workflows/tests-ts-04.yml b/.github/workflows/tests-ts-04.yml deleted file mode 100644 index ba288beb..00000000 --- a/.github/workflows/tests-ts-04.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: 04 - Tests Contract TS -on: push -jobs: - workflows: - strategy: - matrix: - platform: [ubuntu-latest, macos-latest] - node-version: [18, 20] - runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - name: Install and test modules - run: | - cd ./contract-ts/04-ft-owner-claims-winner-gets-nft - yarn - yarn test diff --git a/contract-rs/01-basic-auction/src/lib.rs b/contract-rs/01-basic-auction/src/lib.rs index 53f9b889..a9aac4dd 100644 --- a/contract-rs/01-basic-auction/src/lib.rs +++ b/contract-rs/01-basic-auction/src/lib.rs @@ -14,19 +14,23 @@ pub struct Bid { pub struct Contract { highest_bid: Bid, auction_end_time: U64, + auctioneer: AccountId, + claimed: bool, } #[near] impl Contract { #[init] - #[private] // Only callable by the contract's account - pub fn init(end_time: U64) -> Self { + #[private] // only callable by the contract's account + pub fn init(end_time: U64, auctioneer: AccountId) -> Self { Self { highest_bid: Bid { bidder: env::current_account_id(), bid: NearToken::from_yoctonear(1), }, auction_end_time: end_time, + claimed: false, + auctioneer, } } @@ -58,6 +62,19 @@ impl Contract { Promise::new(last_bidder).transfer(last_bid) } + pub fn claim(&mut self) -> Promise { + require!( + env::block_timestamp() > self.auction_end_time.into(), + "Auction has not ended yet" + ); + + require!(!self.claimed, "Auction has already been claimed"); + self.claimed = true; + + // Transfer tokens to the auctioneer + Promise::new(self.auctioneer.clone()).transfer(self.highest_bid.bid) + } + pub fn get_highest_bid(&self) -> Bid { self.highest_bid.clone() } @@ -65,25 +82,37 @@ impl Contract { pub fn get_auction_end_time(&self) -> U64 { self.auction_end_time } + + pub fn get_auctioneer(&self) -> AccountId { + self.auctioneer.clone() + } + + pub fn get_claimed(&self) -> bool { + self.claimed + } } -/* - * The rest of this file holds the inline tests for the code above - * Learn more about Rust tests: https://doc.rust-lang.org/book/ch11-01-writing-tests.html - */ #[cfg(test)] mod tests { use super::*; #[test] fn init_contract() { - let contract = Contract::init(U64::from(1000)); + let end_time: U64 = U64::from(1000); + let alice: AccountId = "alice.near".parse().unwrap(); + let contract = Contract::init(end_time.clone(), alice.clone()); let default_bid = contract.get_highest_bid(); assert_eq!(default_bid.bidder, env::current_account_id()); assert_eq!(default_bid.bid, NearToken::from_yoctonear(1)); - let end_time = contract.get_auction_end_time(); - assert_eq!(end_time, U64::from(1000)); + let auction_end_time = contract.get_auction_end_time(); + assert_eq!(auction_end_time, end_time); + + let auctioneer = contract.get_auctioneer(); + assert_eq!(auctioneer, alice); + + let claimed = contract.get_claimed(); + assert_eq!(claimed, false); } } diff --git a/contract-rs/01-basic-auction/tests/test_basics.rs b/contract-rs/01-basic-auction/tests/test_basics.rs index d86ea1d5..a8c0fecf 100644 --- a/contract-rs/01-basic-auction/tests/test_basics.rs +++ b/contract-rs/01-basic-auction/tests/test_basics.rs @@ -1,9 +1,7 @@ use chrono::Utc; -use near_workspaces::types::{NearToken, AccountId}; -use serde_json::json; use near_sdk::near; - -const TEN_NEAR: NearToken = NearToken::from_near(10); +use near_workspaces::types::{AccountId, Gas, NearToken}; +use serde_json::json; #[near(serializers = [json])] #[derive(Clone)] @@ -12,6 +10,8 @@ pub struct Bid { pub bid: NearToken, } +const TEN_NEAR: NearToken = NearToken::from_near(10); + #[tokio::test] async fn test_contract_is_operational() -> Result<(), Box> { let sandbox = near_workspaces::sandbox().await?; @@ -21,6 +21,7 @@ async fn test_contract_is_operational() -> Result<(), Box // Create accounts let alice = create_subaccount(&root, "alice").await?; let bob = create_subaccount(&root, "bob").await?; + let auctioneer = create_subaccount(&root, "auctioneer").await?; let contract_account = create_subaccount(&root, "contract").await?; // Deploy and initialize contract @@ -32,7 +33,7 @@ async fn test_contract_is_operational() -> Result<(), Box let init = contract .call("init") - .args_json(json!({"end_time": a_minute_from_now.to_string()})) + .args_json(json!({"end_time": a_minute_from_now.to_string(),"auctioneer":auctioneer.id()})) .transact() .await?; @@ -68,7 +69,7 @@ async fn test_contract_is_operational() -> Result<(), Box assert_eq!(highest_bid.bid, NearToken::from_near(2)); assert_eq!(highest_bid.bidder, *bob.id()); - // Check that alice was returned her bid + // Check that Alice was returned her bid let new_alice_balance = alice.view_account().await?.balance; assert!(new_alice_balance == alice_balance.saturating_add(NearToken::from_near(1))); @@ -81,10 +82,45 @@ async fn test_contract_is_operational() -> Result<(), Box assert!(alice_bid.is_failure()); + // Auctioneer claims auction but did not finish + let auctioneer_claim = auctioneer + .call(contract_account.id(), "claim") + .args_json(json!({})) + .gas(Gas::from_tgas(300)) + .transact() + .await?; + + assert!(auctioneer_claim.is_failure()); + // Fast forward 200 blocks let blocks_to_advance = 200; sandbox.fast_forward(blocks_to_advance).await?; + // Auctioneer claims the auction + let auctioneer_claim = auctioneer + .call(contract_account.id(), "claim") + .args_json(json!({})) + .gas(Gas::from_tgas(300)) + .transact() + .await?; + + assert!(auctioneer_claim.is_success()); + + // Checks the auctioneer has the correct balance + let auctioneer_balance = auctioneer.view_account().await?.balance; + assert!(auctioneer_balance <= NearToken::from_near(12)); + assert!(auctioneer_balance > NearToken::from_millinear(11990)); + + // Auctioneer tries to claim the auction again + let auctioneer_claim = auctioneer + .call(contract_account.id(), "claim") + .args_json(json!({})) + .gas(Gas::from_tgas(300)) + .transact() + .await?; + + assert!(auctioneer_claim.is_failure()); + // Alice tries to make a bid when the auction is over let alice_bid = alice .call(contract.id(), "bid") diff --git a/contract-rs/02-owner-claims-money/src/lib.rs b/contract-rs/02-owner-claims-money/src/lib.rs deleted file mode 100644 index eb0b47f3..00000000 --- a/contract-rs/02-owner-claims-money/src/lib.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Find all our documentation at https://docs.near.org -use near_sdk::json_types::U64; -use near_sdk::{env, near, require, AccountId, NearToken, PanicOnDefault, Promise}; - -#[near(serializers = [json, borsh])] -#[derive(Clone)] -pub struct Bid { - pub bidder: AccountId, - pub bid: NearToken, -} - -#[near(contract_state, serializers = [json, borsh])] -#[derive(PanicOnDefault)] -pub struct Contract { - highest_bid: Bid, - auction_end_time: U64, - auctioneer: AccountId, - claimed: bool, -} - -#[near] -impl Contract { - #[init] - #[private] // only callable by the contract's account - pub fn init(end_time: U64, auctioneer: AccountId) -> Self { - Self { - highest_bid: Bid { - bidder: env::current_account_id(), - bid: NearToken::from_yoctonear(1), - }, - auction_end_time: end_time, - claimed: false, - auctioneer, - } - } - - #[payable] - pub fn bid(&mut self) -> Promise { - // Assert the auction is still ongoing - require!( - env::block_timestamp() < self.auction_end_time.into(), - "Auction has ended" - ); - - // Current bid - let bid = env::attached_deposit(); - let bidder = env::predecessor_account_id(); - - // Last bid - let Bid { - bidder: last_bidder, - bid: last_bid, - } = self.highest_bid.clone(); - - // Check if the deposit is higher than the current bid - require!(bid > last_bid, "You must place a higher bid"); - - // Update the highest bid - self.highest_bid = Bid { bidder, bid }; - - // Transfer tokens back to the last bidder - Promise::new(last_bidder).transfer(last_bid) - } - - pub fn claim(&mut self) -> Promise { - require!( - env::block_timestamp() > self.auction_end_time.into(), - "Auction has not ended yet" - ); - - require!(!self.claimed, "Auction has already been claimed"); - self.claimed = true; - - // Transfer tokens to the auctioneer - Promise::new(self.auctioneer.clone()).transfer(self.highest_bid.bid) - } - - pub fn get_highest_bid(&self) -> Bid { - self.highest_bid.clone() - } - - pub fn get_auction_end_time(&self) -> U64 { - self.auction_end_time - } - - pub fn get_auction_info(&self) -> &Contract { - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn init_contract() { - let end_time: U64 = U64::from(1000); - let alice: AccountId = "alice.near".parse().unwrap(); - let contract = Contract::init(end_time.clone(), alice.clone()); - - let default_bid = contract.get_highest_bid(); - assert_eq!(default_bid.bidder, env::current_account_id()); - assert_eq!(default_bid.bid, NearToken::from_yoctonear(1)); - - let auction_info = contract.get_auction_info(); - assert_eq!(auction_info.auction_end_time, end_time); - assert_eq!(auction_info.auctioneer, alice); - assert_eq!(auction_info.claimed, false); - } -} diff --git a/contract-rs/02-owner-claims-money/tests/test_basics.rs b/contract-rs/02-owner-claims-money/tests/test_basics.rs deleted file mode 100644 index a503b20f..00000000 --- a/contract-rs/02-owner-claims-money/tests/test_basics.rs +++ /dev/null @@ -1,148 +0,0 @@ -use chrono::Utc; -use serde_json::json; -use near_workspaces::types::{NearToken, AccountId, Gas}; -use near_sdk::near; - -#[near(serializers = [json])] -#[derive(Clone)] -pub struct Bid { - pub bidder: AccountId, - pub bid: NearToken, -} - -const TEN_NEAR: NearToken = NearToken::from_near(10); - -#[tokio::test] -async fn test_contract_is_operational() -> Result<(), Box> { - let sandbox = near_workspaces::sandbox().await?; - - let root = sandbox.root_account()?; - - // Create accounts - let alice = create_subaccount(&root, "alice").await?; - let bob = create_subaccount(&root, "bob").await?; - let auctioneer = create_subaccount(&root, "auctioneer").await?; - let contract_account = create_subaccount(&root, "contract").await?; - - // Deploy and initialize contract - let contract_wasm = near_workspaces::compile_project("./").await?; - let contract = contract_account.deploy(&contract_wasm).await?.unwrap(); - - let now = Utc::now().timestamp(); - let a_minute_from_now = (now + 60) * 1000000000; - - let init = contract - .call("init") - .args_json(json!({"end_time": a_minute_from_now.to_string(),"auctioneer":auctioneer.id()})) - .transact() - .await?; - - assert!(init.is_success()); - - // Alice makes first bid - let alice_bid = alice - .call(contract.id(), "bid") - .deposit(NearToken::from_near(1)) - .transact() - .await?; - - assert!(alice_bid.is_success()); - - let highest_bid_json = contract.view("get_highest_bid").await?; - let highest_bid: Bid = highest_bid_json.json::()?; - assert_eq!(highest_bid.bid, NearToken::from_near(1)); - assert_eq!(highest_bid.bidder, *alice.id()); - - let alice_balance = alice.view_account().await?.balance; - - // Bob makes a higher bid - let bob_bid = bob - .call(contract.id(), "bid") - .deposit(NearToken::from_near(2)) - .transact() - .await?; - - assert!(bob_bid.is_success()); - - let highest_bid_json = contract.view("get_highest_bid").await?; - let highest_bid: Bid = highest_bid_json.json::()?; - assert_eq!(highest_bid.bid, NearToken::from_near(2)); - assert_eq!(highest_bid.bidder, *bob.id()); - - // Check that Alice was returned her bid - let new_alice_balance = alice.view_account().await?.balance; - assert!(new_alice_balance == alice_balance.saturating_add(NearToken::from_near(1))); - - // Alice tries to make a bid with less NEAR than the previous - let alice_bid = alice - .call(contract.id(), "bid") - .deposit(NearToken::from_near(1)) - .transact() - .await?; - - assert!(alice_bid.is_failure()); - - // Auctioneer claims auction but did not finish - let auctioneer_claim = auctioneer - .call(contract_account.id(), "claim") - .args_json(json!({})) - .gas(Gas::from_tgas(300)) - .transact() - .await?; - - assert!(auctioneer_claim.is_failure()); - - // Fast forward 200 blocks - let blocks_to_advance = 200; - sandbox.fast_forward(blocks_to_advance).await?; - - // Auctioneer claims the auction - let auctioneer_claim = auctioneer - .call(contract_account.id(), "claim") - .args_json(json!({})) - .gas(Gas::from_tgas(300)) - .transact() - .await?; - - assert!(auctioneer_claim.is_success()); - - // Checks the auctioneer has the correct balance - let auctioneer_balance = auctioneer.view_account().await?.balance; - assert!(auctioneer_balance <= NearToken::from_near(12)); - assert!(auctioneer_balance > NearToken::from_millinear(11990)); - - // Auctioneer tries to claim the auction again - let auctioneer_claim = auctioneer - .call(contract_account.id(), "claim") - .args_json(json!({})) - .gas(Gas::from_tgas(300)) - .transact() - .await?; - - assert!(auctioneer_claim.is_failure()); - - // Alice tries to make a bid when the auction is over - let alice_bid = alice - .call(contract.id(), "bid") - .deposit(NearToken::from_near(3)) - .transact() - .await?; - - assert!(alice_bid.is_failure()); - - Ok(()) -} - -async fn create_subaccount( - root: &near_workspaces::Account, - name: &str, -) -> Result> { - let subaccount = root - .create_subaccount(name) - .initial_balance(TEN_NEAR) - .transact() - .await? - .unwrap(); - - Ok(subaccount) -} diff --git a/contract-rs/02-owner-claims-money/Cargo.toml b/contract-rs/02-winner-gets-nft/Cargo.toml similarity index 96% rename from contract-rs/02-owner-claims-money/Cargo.toml rename to contract-rs/02-winner-gets-nft/Cargo.toml index dcd61c60..999b8076 100644 --- a/contract-rs/02-owner-claims-money/Cargo.toml +++ b/contract-rs/02-winner-gets-nft/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -near-sdk = "5.5.0" +near-sdk = "5.5.0" [dev-dependencies] near-sdk = { version = "5.5.0", features = ["unit-testing"] } diff --git a/contract-rs/02-owner-claims-money/README.md b/contract-rs/02-winner-gets-nft/README.md similarity index 100% rename from contract-rs/02-owner-claims-money/README.md rename to contract-rs/02-winner-gets-nft/README.md diff --git a/contract-rs/02-owner-claims-money/rust-toolchain.toml b/contract-rs/02-winner-gets-nft/rust-toolchain.toml similarity index 100% rename from contract-rs/02-owner-claims-money/rust-toolchain.toml rename to contract-rs/02-winner-gets-nft/rust-toolchain.toml diff --git a/contract-rs/03-owner-claims-winner-gets-nft/src/ext.rs b/contract-rs/02-winner-gets-nft/src/ext.rs similarity index 100% rename from contract-rs/03-owner-claims-winner-gets-nft/src/ext.rs rename to contract-rs/02-winner-gets-nft/src/ext.rs diff --git a/contract-rs/03-owner-claims-winner-gets-nft/src/lib.rs b/contract-rs/02-winner-gets-nft/src/lib.rs similarity index 100% rename from contract-rs/03-owner-claims-winner-gets-nft/src/lib.rs rename to contract-rs/02-winner-gets-nft/src/lib.rs diff --git a/contract-rs/03-owner-claims-winner-gets-nft/tests/non_fungible_token.wasm b/contract-rs/02-winner-gets-nft/tests/non_fungible_token.wasm similarity index 100% rename from contract-rs/03-owner-claims-winner-gets-nft/tests/non_fungible_token.wasm rename to contract-rs/02-winner-gets-nft/tests/non_fungible_token.wasm diff --git a/contract-rs/03-owner-claims-winner-gets-nft/tests/test_basics.rs b/contract-rs/02-winner-gets-nft/tests/test_basics.rs similarity index 100% rename from contract-rs/03-owner-claims-winner-gets-nft/tests/test_basics.rs rename to contract-rs/02-winner-gets-nft/tests/test_basics.rs diff --git a/contract-rs/03-owner-claims-winner-gets-nft/Cargo.toml b/contract-rs/03-bid-with-fts/Cargo.toml similarity index 96% rename from contract-rs/03-owner-claims-winner-gets-nft/Cargo.toml rename to contract-rs/03-bid-with-fts/Cargo.toml index b3338c0d..211cf7a6 100644 --- a/contract-rs/03-owner-claims-winner-gets-nft/Cargo.toml +++ b/contract-rs/03-bid-with-fts/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -near-sdk = "5.5.0" +near-sdk = "5.5.0" [dev-dependencies] near-sdk = { version = "5.5.0", features = ["unit-testing"] } diff --git a/contract-rs/03-owner-claims-winner-gets-nft/README.md b/contract-rs/03-bid-with-fts/README.md similarity index 100% rename from contract-rs/03-owner-claims-winner-gets-nft/README.md rename to contract-rs/03-bid-with-fts/README.md diff --git a/contract-rs/03-owner-claims-winner-gets-nft/rust-toolchain.toml b/contract-rs/03-bid-with-fts/rust-toolchain.toml similarity index 100% rename from contract-rs/03-owner-claims-winner-gets-nft/rust-toolchain.toml rename to contract-rs/03-bid-with-fts/rust-toolchain.toml diff --git a/contract-rs/04-ft-owner-claims-winner-gets-nft/src/ext.rs b/contract-rs/03-bid-with-fts/src/ext.rs similarity index 100% rename from contract-rs/04-ft-owner-claims-winner-gets-nft/src/ext.rs rename to contract-rs/03-bid-with-fts/src/ext.rs diff --git a/contract-rs/04-ft-owner-claims-winner-gets-nft/src/lib.rs b/contract-rs/03-bid-with-fts/src/lib.rs similarity index 100% rename from contract-rs/04-ft-owner-claims-winner-gets-nft/src/lib.rs rename to contract-rs/03-bid-with-fts/src/lib.rs diff --git a/contract-rs/04-ft-owner-claims-winner-gets-nft/tests/fungible_token.wasm b/contract-rs/03-bid-with-fts/tests/fungible_token.wasm similarity index 100% rename from contract-rs/04-ft-owner-claims-winner-gets-nft/tests/fungible_token.wasm rename to contract-rs/03-bid-with-fts/tests/fungible_token.wasm diff --git a/contract-rs/04-ft-owner-claims-winner-gets-nft/tests/non_fungible_token.wasm b/contract-rs/03-bid-with-fts/tests/non_fungible_token.wasm similarity index 100% rename from contract-rs/04-ft-owner-claims-winner-gets-nft/tests/non_fungible_token.wasm rename to contract-rs/03-bid-with-fts/tests/non_fungible_token.wasm diff --git a/contract-rs/04-ft-owner-claims-winner-gets-nft/tests/test_basics.rs b/contract-rs/03-bid-with-fts/tests/test_basics.rs similarity index 100% rename from contract-rs/04-ft-owner-claims-winner-gets-nft/tests/test_basics.rs rename to contract-rs/03-bid-with-fts/tests/test_basics.rs diff --git a/contract-rs/04-ft-owner-claims-winner-gets-nft/Cargo.toml b/contract-rs/04-ft-owner-claims-winner-gets-nft/Cargo.toml deleted file mode 100644 index b8dd9566..00000000 --- a/contract-rs/04-ft-owner-claims-winner-gets-nft/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "auction-contract" -description = "Auction Example Part 4" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -near-sdk = "5.5.0" - -[dev-dependencies] -near-sdk = { version = "5.5.0", features = ["unit-testing"] } -near-workspaces = { version = "0.14.0", features = ["unstable"] } -tokio = { version = "1.12.0", features = ["full"] } -serde_json = "1" -chrono = "0.4.38" - -[profile.release] -codegen-units = 1 -opt-level = "z" -lto = true -debug = false -panic = "abort" -overflow-checks = true \ No newline at end of file diff --git a/contract-rs/04-ft-owner-claims-winner-gets-nft/README.md b/contract-rs/04-ft-owner-claims-winner-gets-nft/README.md deleted file mode 100644 index ebc0f142..00000000 --- a/contract-rs/04-ft-owner-claims-winner-gets-nft/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# contract-rs - -cargo-near-new-project-description - -## How to Build Locally? - -Install [`cargo-near`](https://github.com/near/cargo-near) and run: - -```bash -cargo near build -``` - -## How to Test Locally? - -```bash -cargo test -``` - -## How to Deploy? - -To deploy manually, install [`cargo-near`](https://github.com/near/cargo-near) and run: - -```bash -# Create a new account -cargo near create-dev-account - -# Deploy the contract on it -cargo near deploy -``` -## Useful Links - -- [cargo-near](https://github.com/near/cargo-near) - NEAR smart contract development toolkit for Rust -- [near CLI](https://near.cli.rs) - Iteract with NEAR blockchain from command line -- [NEAR Rust SDK Documentation](https://docs.near.org/sdk/rust/introduction) -- [NEAR Documentation](https://docs.near.org) -- [NEAR StackOverflow](https://stackoverflow.com/questions/tagged/nearprotocol) -- [NEAR Discord](https://near.chat) -- [NEAR Telegram Developers Community Group](https://t.me/neardev) -- NEAR DevHub: [Telegram](https://t.me/neardevhub), [Twitter](https://twitter.com/neardevhub) diff --git a/contract-rs/04-ft-owner-claims-winner-gets-nft/rust-toolchain.toml b/contract-rs/04-ft-owner-claims-winner-gets-nft/rust-toolchain.toml deleted file mode 100644 index a82ade34..00000000 --- a/contract-rs/04-ft-owner-claims-winner-gets-nft/rust-toolchain.toml +++ /dev/null @@ -1,4 +0,0 @@ -[toolchain] -channel = "stable" -components = ["rustfmt"] -targets = ["wasm32-unknown-unknown"] diff --git a/contract-ts/01-basic-auction/package.json b/contract-ts/01-basic-auction/package.json index 45eb68fc..112b7683 100644 --- a/contract-ts/01-basic-auction/package.json +++ b/contract-ts/01-basic-auction/package.json @@ -1,11 +1,11 @@ { - "name": "auction", + "name": "auction-contract", "version": "1.0.0", "license": "(MIT AND Apache-2.0)", "type": "module", "scripts": { - "build": "near-sdk-js build src/contract.ts build/auction.wasm", - "test": "$npm_execpath run build && ava -- ./build/auction.wasm" + "build": "near-sdk-js build src/contract.ts build/auction-contract.wasm", + "test": "$npm_execpath run build && ava -- ./build/auction-contract.wasm" }, "dependencies": { "near-sdk-js": "2.0.0" diff --git a/contract-ts/01-basic-auction/sandbox-test/main.ava.js b/contract-ts/01-basic-auction/sandbox-test/main.ava.js index b537511e..f6012a0c 100644 --- a/contract-ts/01-basic-auction/sandbox-test/main.ava.js +++ b/contract-ts/01-basic-auction/sandbox-test/main.ava.js @@ -16,6 +16,7 @@ test.beforeEach(async (t) => { const alice = await root.createSubAccount("alice", { initialBalance: NEAR.parse("10 N").toString() }); const bob = await root.createSubAccount("bob", { initialBalance: NEAR.parse("10 N").toString() }); + const auctioneer = await root.createSubAccount("auctioneer", { initialBalance: NEAR.parse("10 N").toString() }); const contract = await root.createSubAccount("contract", { initialBalance: NEAR.parse("10 N").toString() }); // Deploy contract (input from package.json) @@ -24,11 +25,12 @@ test.beforeEach(async (t) => { // Initialize contract, finishes in 1 minute await contract.call(contract, "init", { end_time: String((Date.now() + 60000) * 10 ** 6), + auctioneer: auctioneer.accountId, }); // Save state for test runs, it is unique for each test t.context.worker = worker; - t.context.accounts = { alice, bob, contract }; + t.context.accounts = { alice, bob, contract, auctioneer }; }); test.afterEach.always(async (t) => { @@ -39,14 +41,13 @@ test.afterEach.always(async (t) => { }); test("Test full contract", async (t) => { - const { alice, bob, contract } = t.context.accounts; + const { alice, bob, auctioneer, contract } = t.context.accounts; // Alice makes first bid await alice.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("1 N").toString() }); let highest_bid = await contract.view("get_highest_bid", {}); t.is(highest_bid.bidder, alice.accountId); t.is(highest_bid.bid, NEAR.parse("1 N").toString()); - const aliceBalance = await alice.balance(); // Bob makes a higher bid @@ -59,12 +60,29 @@ test("Test full contract", async (t) => { const aliceNewBalance = await alice.balance(); t.deepEqual(aliceNewBalance.available, aliceBalance.available.add(NEAR.parse("1 N"))); - // Alice tries to make a bid with less NEAR than the previous - await t.throwsAsync(alice.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("1 N").toString() })) + // Alice tires to make a bid with less NEAR than the previous + await t.throwsAsync(alice.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("1 N").toString() })); + + // Auctioneer claims auction but did not finish + await t.throwsAsync(auctioneer.call(contract, "claim", {}, { gas: "300000000000000" })); // Fast forward 200 blocks await t.context.worker.provider.fastForward(200) + const auctioneerBalance = await auctioneer.balance(); + const available = parseFloat(auctioneerBalance.available.toHuman()); + + // Auctioneer claims the auction + await auctioneer.call(contract, "claim", {}, { gas: "300000000000000" }); + + // Checks that the auctioneer has the correct balance + const contractNewBalance = await auctioneer.balance(); + const new_available = parseFloat(contractNewBalance.available.toHuman()); + t.is(new_available.toFixed(1), (available + 2).toFixed(1)); + + // Auctioneer tries to claim the auction again + await t.throwsAsync(auctioneer.call(contract, "claim", {}, { gas: "300000000000000" })) + // Alice tries to make a bid when the auction is over - await t.throwsAsync(alice.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("3 N").toString() })) + await t.throwsAsync(alice.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("1 N").toString() })); }); \ No newline at end of file diff --git a/contract-ts/01-basic-auction/src/contract.ts b/contract-ts/01-basic-auction/src/contract.ts index 484fa957..f6609b61 100644 --- a/contract-ts/01-basic-auction/src/contract.ts +++ b/contract-ts/01-basic-auction/src/contract.ts @@ -10,11 +10,14 @@ class Bid { class AuctionContract { highest_bid: Bid = { bidder: '', bid: BigInt(0) }; auction_end_time: bigint = BigInt(0); + auctioneer: AccountId = ""; + claimed: boolean = false; @initialize({ privateFunction: true }) - init({ end_time}: { end_time: bigint}) { + init({ end_time, auctioneer}: { end_time: bigint, auctioneer: AccountId}) { this.auction_end_time = end_time; this.highest_bid = { bidder: near.currentAccountId(), bid: BigInt(1) }; + this.auctioneer = auctioneer; } @call({ payableFunction: true }) @@ -33,12 +36,20 @@ class AuctionContract { assert(bid > lastBid, "You must place a higher bid"); // Update the highest bid - this.highest_bid = { bidder, bid }; + this.highest_bid = { bidder, bid }; // Save the new bid // Transfer tokens back to the last bidder return NearPromise.new(lastBidder).transfer(lastBid); } + @call({}) + claim() { + assert(this.auction_end_time <= near.blockTimestamp(), "Auction has not ended yet"); + assert(!this.claimed, "Auction has been claimed"); + this.claimed = true; + return NearPromise.new(this.auctioneer).transfer(this.highest_bid.bid) + } + @view({}) get_highest_bid(): Bid { return this.highest_bid; @@ -48,4 +59,14 @@ class AuctionContract { get_auction_end_time(): BigInt { return this.auction_end_time; } + + @view({}) + get_auctioneer(): AccountId { + return this.auctioneer; + } + + @view({}) + get_claimed(): boolean { + return this.claimed; + } } \ No newline at end of file diff --git a/contract-ts/02-owner-claims-money/package.json b/contract-ts/02-owner-claims-money/package.json deleted file mode 100644 index 7569b624..00000000 --- a/contract-ts/02-owner-claims-money/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "auction", - "version": "1.0.0", - "license": "(MIT AND Apache-2.0)", - "type": "module", - "scripts": { - "build": "near-sdk-js build src/contract.ts build/auction.wasm", - "test": "$npm_execpath run build && ava -- ./build/auction.wasm" - }, - "dependencies": { - "near-sdk-js": "1.0.0" - }, - "devDependencies": { - "ava": "^6.1.3", - "near-workspaces": "^3.5.0", - "typescript": "^5.4.5" - }, - "ava": { - "timeout": "50000", - "files": ["sandbox-test/*.ava.js"] - } -} diff --git a/contract-ts/02-owner-claims-money/sandbox-test/main.ava.js b/contract-ts/02-owner-claims-money/sandbox-test/main.ava.js deleted file mode 100644 index a3e455a1..00000000 --- a/contract-ts/02-owner-claims-money/sandbox-test/main.ava.js +++ /dev/null @@ -1,88 +0,0 @@ -import anyTest from 'ava'; -import { NEAR, Worker } from 'near-workspaces'; -import { setDefaultResultOrder } from 'dns'; setDefaultResultOrder('ipv4first'); // temp fix for node >v17 - -/** - * @typedef {import('near-workspaces').NearAccount} NearAccount - * @type {import('ava').TestFn<{worker: Worker, accounts: Record}>} - */ -const test = anyTest; -test.beforeEach(async (t) => { - // Init the worker and start a Sandbox server - const worker = t.context.worker = await Worker.init(); - - // Create accounts - const root = worker.rootAccount; - - const alice = await root.createSubAccount("alice", { initialBalance: NEAR.parse("10 N").toString() }); - const bob = await root.createSubAccount("bob", { initialBalance: NEAR.parse("10 N").toString() }); - const auctioneer = await root.createSubAccount("auctioneer", { initialBalance: NEAR.parse("10 N").toString() }); - const contract = await root.createSubAccount("contract", { initialBalance: NEAR.parse("10 N").toString() }); - - // Deploy contract (input from package.json) - await contract.deploy(process.argv[2]); - - // Initialize contract, finishes in 1 minute - await contract.call(contract, "init", { - end_time: String((Date.now() + 60000) * 10 ** 6), - auctioneer: auctioneer.accountId, - }); - - // Save state for test runs, it is unique for each test - t.context.worker = worker; - t.context.accounts = { alice, bob, contract, auctioneer }; -}); - -test.afterEach.always(async (t) => { - // Stop Sandbox server - await t.context.worker.tearDown().catch((error) => { - console.log('Failed to stop the Sandbox:', error); - }); -}); - -test("Test full contract", async (t) => { - const { alice, bob, auctioneer, contract } = t.context.accounts; - - // Alice makes first bid - await alice.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("1 N").toString() }); - let highest_bid = await contract.view("get_highest_bid", {}); - t.is(highest_bid.bidder, alice.accountId); - t.is(highest_bid.bid, NEAR.parse("1 N").toString()); - const aliceBalance = await alice.balance(); - - // Bob makes a higher bid - await bob.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("2 N").toString() }); - highest_bid = await contract.view("get_highest_bid", {}); - t.is(highest_bid.bidder, bob.accountId); - t.is(highest_bid.bid, NEAR.parse("2 N").toString()); - - // Check that alice was returned her bid - const aliceNewBalance = await alice.balance(); - t.deepEqual(aliceNewBalance.available, aliceBalance.available.add(NEAR.parse("1 N"))); - - // Alice tires to make a bid with less NEAR than the previous - await t.throwsAsync(alice.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("1 N").toString() })); - - // Auctioneer claims auction but did not finish - await t.throwsAsync(auctioneer.call(contract, "claim", {}, { gas: "300000000000000" })); - - // Fast forward 200 blocks - await t.context.worker.provider.fastForward(200) - - const auctioneerBalance = await auctioneer.balance(); - const available = parseFloat(auctioneerBalance.available.toHuman()); - - // Auctioneer claims the auction - await auctioneer.call(contract, "claim", {}, { gas: "300000000000000" }); - - // Checks that the auctioneer has the correct balance - const contractNewBalance = await auctioneer.balance(); - const new_available = parseFloat(contractNewBalance.available.toHuman()); - t.is(new_available.toFixed(2), (available + 2).toFixed(2)); - - // Auctioneer tries to claim the auction again - await t.throwsAsync(auctioneer.call(contract, "claim", {}, { gas: "300000000000000" })) - - // Alice tries to make a bid when the auction is over - await t.throwsAsync(alice.call(contract, "bid", {}, { attachedDeposit: NEAR.parse("1 N").toString() })); -}); \ No newline at end of file diff --git a/contract-ts/02-owner-claims-money/src/contract.ts b/contract-ts/02-owner-claims-money/src/contract.ts deleted file mode 100644 index 17fb7314..00000000 --- a/contract-ts/02-owner-claims-money/src/contract.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Find all our documentation at https://docs.near.org -import { NearBindgen, near, call, view, AccountId, NearPromise, initialize, assert } from "near-sdk-js"; - -class Bid { - bidder: AccountId; - bid: bigint; -} - -@NearBindgen({ requireInit: true }) -class AuctionContract { - highest_bid: Bid = { bidder: '', bid: BigInt(0) }; - auction_end_time: bigint = BigInt(0); - auctioneer: string = ""; - claimed: boolean = false; - - @initialize({ privateFunction: true }) - init({ end_time, auctioneer}: { end_time: bigint, auctioneer: string}) { - this.auction_end_time = end_time; - this.highest_bid = { bidder: near.currentAccountId(), bid: BigInt(1) }; - this.auctioneer = auctioneer; - } - - @call({ payableFunction: true }) - bid(): NearPromise { - // Assert the auction is still ongoing - assert(this.auction_end_time > near.blockTimestamp(), "Auction has ended"); - - // Current bid - const bid = near.attachedDeposit(); - const bidder = near.predecessorAccountId(); - - // Last bid - const { bidder: lastBidder, bid: lastBid } = this.highest_bid; - - // Check if the deposit is higher than the current bid - assert(bid > lastBid, "You must place a higher bid"); - - // Update the highest bid - this.highest_bid = { bidder, bid }; // Save the new bid - - // Transfer tokens back to the last bidder - return NearPromise.new(lastBidder).transfer(lastBid); - } - - @view({}) - get_highest_bid(): Bid { - return this.highest_bid; - } - - @view({}) - get_auction_end_time(): BigInt { - return this.auction_end_time; - } - - @call({}) - claim() { - assert(this.auction_end_time <= near.blockTimestamp(), "Auction has not ended yet"); - assert(!this.claimed, "Auction has been claimed"); - this.claimed = true; - return NearPromise.new(this.auctioneer).transfer(this.highest_bid.bid) - } -} \ No newline at end of file diff --git a/contract-ts/02-owner-claims-money/README.md b/contract-ts/02-winner-gets-nft/README.md similarity index 100% rename from contract-ts/02-owner-claims-money/README.md rename to contract-ts/02-winner-gets-nft/README.md diff --git a/contract-ts/03-owner-claims-winner-gets-nft/package.json b/contract-ts/02-winner-gets-nft/package.json similarity index 59% rename from contract-ts/03-owner-claims-winner-gets-nft/package.json rename to contract-ts/02-winner-gets-nft/package.json index 7569b624..112b7683 100644 --- a/contract-ts/03-owner-claims-winner-gets-nft/package.json +++ b/contract-ts/02-winner-gets-nft/package.json @@ -1,14 +1,14 @@ { - "name": "auction", + "name": "auction-contract", "version": "1.0.0", "license": "(MIT AND Apache-2.0)", "type": "module", "scripts": { - "build": "near-sdk-js build src/contract.ts build/auction.wasm", - "test": "$npm_execpath run build && ava -- ./build/auction.wasm" + "build": "near-sdk-js build src/contract.ts build/auction-contract.wasm", + "test": "$npm_execpath run build && ava -- ./build/auction-contract.wasm" }, "dependencies": { - "near-sdk-js": "1.0.0" + "near-sdk-js": "2.0.0" }, "devDependencies": { "ava": "^6.1.3", diff --git a/contract-ts/03-owner-claims-winner-gets-nft/sandbox-test/main.ava.js b/contract-ts/02-winner-gets-nft/sandbox-test/main.ava.js similarity index 98% rename from contract-ts/03-owner-claims-winner-gets-nft/sandbox-test/main.ava.js rename to contract-ts/02-winner-gets-nft/sandbox-test/main.ava.js index 6ae75ae0..58f2ed36 100644 --- a/contract-ts/03-owner-claims-winner-gets-nft/sandbox-test/main.ava.js +++ b/contract-ts/02-winner-gets-nft/sandbox-test/main.ava.js @@ -99,7 +99,7 @@ test("Test full contract", async (t) => { // Checks that the auctioneer has the correct balance const contractNewBalance = await auctioneer.balance(); const new_available = parseFloat(contractNewBalance.available.toHuman()); - t.is(new_available.toFixed(2), (available + 2).toFixed(2)); + t.is(new_available.toFixed(1), (available + 2).toFixed(1)); // Check highest bidder received the NFT const response = await nft_contract.call(nft_contract, "nft_token",{"token_id": "1"},{ gas: "300000000000000" }); diff --git a/contract-ts/03-owner-claims-winner-gets-nft/sandbox-test/non_fungible_token.wasm b/contract-ts/02-winner-gets-nft/sandbox-test/non_fungible_token.wasm similarity index 100% rename from contract-ts/03-owner-claims-winner-gets-nft/sandbox-test/non_fungible_token.wasm rename to contract-ts/02-winner-gets-nft/sandbox-test/non_fungible_token.wasm diff --git a/contract-ts/03-owner-claims-winner-gets-nft/src/contract.ts b/contract-ts/02-winner-gets-nft/src/contract.ts similarity index 91% rename from contract-ts/03-owner-claims-winner-gets-nft/src/contract.ts rename to contract-ts/02-winner-gets-nft/src/contract.ts index ceb0c794..a89d9137 100644 --- a/contract-ts/03-owner-claims-winner-gets-nft/src/contract.ts +++ b/contract-ts/02-winner-gets-nft/src/contract.ts @@ -13,13 +13,13 @@ const NO_DEPOSIT = BigInt(0); class AuctionContract { highest_bid: Bid = { bidder: '', bid: BigInt(0) }; auction_end_time: bigint = BigInt(0); - auctioneer: string = ""; + auctioneer: AccountId = ""; claimed: boolean = false; nft_contract: AccountId = ""; token_id: string = ""; @initialize({ privateFunction: true }) - init({ end_time, auctioneer, nft_contract, token_id }: { end_time: bigint, auctioneer: string, nft_contract: AccountId, token_id: string }) { + init({ end_time, auctioneer, nft_contract, token_id }: { end_time: bigint, auctioneer: AccountId, nft_contract: AccountId, token_id: string }) { this.auction_end_time = end_time; this.highest_bid = { bidder: near.currentAccountId(), bid: BigInt(1) }; this.auctioneer = auctioneer; @@ -49,16 +49,6 @@ class AuctionContract { return NearPromise.new(lastBidder).transfer(lastBid); } - @view({}) - get_highest_bid(): Bid { - return this.highest_bid; - } - - @view({}) - get_auction_end_time(): BigInt { - return this.auction_end_time; - } - @call({}) claim() { assert(this.auction_end_time <= near.blockTimestamp(), "Auction has not ended yet"); @@ -69,4 +59,19 @@ class AuctionContract { .functionCall("nft_transfer", JSON.stringify({ receiver_id: this.highest_bid.bidder, token_id: this.token_id }), BigInt(1), TWENTY_TGAS) .then(NearPromise.new(this.auctioneer).transfer(this.highest_bid.bid)) } + + @view({}) + get_highest_bid(): Bid { + return this.highest_bid; + } + + @view({}) + get_auction_end_time(): BigInt { + return this.auction_end_time; + } + + @view({}) + get_auction_info(): AuctionContract { + return this; + } } diff --git a/contract-ts/02-owner-claims-money/tsconfig.json b/contract-ts/02-winner-gets-nft/tsconfig.json similarity index 100% rename from contract-ts/02-owner-claims-money/tsconfig.json rename to contract-ts/02-winner-gets-nft/tsconfig.json diff --git a/contract-ts/03-owner-claims-winner-gets-nft/README.md b/contract-ts/03-bid-with-fts/README.md similarity index 100% rename from contract-ts/03-owner-claims-winner-gets-nft/README.md rename to contract-ts/03-bid-with-fts/README.md diff --git a/contract-ts/04-ft-owner-claims-winner-gets-nft/package.json b/contract-ts/03-bid-with-fts/package.json similarity index 59% rename from contract-ts/04-ft-owner-claims-winner-gets-nft/package.json rename to contract-ts/03-bid-with-fts/package.json index 7569b624..112b7683 100644 --- a/contract-ts/04-ft-owner-claims-winner-gets-nft/package.json +++ b/contract-ts/03-bid-with-fts/package.json @@ -1,14 +1,14 @@ { - "name": "auction", + "name": "auction-contract", "version": "1.0.0", "license": "(MIT AND Apache-2.0)", "type": "module", "scripts": { - "build": "near-sdk-js build src/contract.ts build/auction.wasm", - "test": "$npm_execpath run build && ava -- ./build/auction.wasm" + "build": "near-sdk-js build src/contract.ts build/auction-contract.wasm", + "test": "$npm_execpath run build && ava -- ./build/auction-contract.wasm" }, "dependencies": { - "near-sdk-js": "1.0.0" + "near-sdk-js": "2.0.0" }, "devDependencies": { "ava": "^6.1.3", diff --git a/contract-ts/04-ft-owner-claims-winner-gets-nft/sandbox-test/fungible_token.wasm b/contract-ts/03-bid-with-fts/sandbox-test/fungible_token.wasm similarity index 100% rename from contract-ts/04-ft-owner-claims-winner-gets-nft/sandbox-test/fungible_token.wasm rename to contract-ts/03-bid-with-fts/sandbox-test/fungible_token.wasm diff --git a/contract-ts/04-ft-owner-claims-winner-gets-nft/sandbox-test/main.ava.js b/contract-ts/03-bid-with-fts/sandbox-test/main.ava.js similarity index 100% rename from contract-ts/04-ft-owner-claims-winner-gets-nft/sandbox-test/main.ava.js rename to contract-ts/03-bid-with-fts/sandbox-test/main.ava.js diff --git a/contract-ts/04-ft-owner-claims-winner-gets-nft/sandbox-test/non_fungible_token.wasm b/contract-ts/03-bid-with-fts/sandbox-test/non_fungible_token.wasm similarity index 100% rename from contract-ts/04-ft-owner-claims-winner-gets-nft/sandbox-test/non_fungible_token.wasm rename to contract-ts/03-bid-with-fts/sandbox-test/non_fungible_token.wasm diff --git a/contract-ts/04-ft-owner-claims-winner-gets-nft/src/contract.ts b/contract-ts/03-bid-with-fts/src/contract.ts similarity index 90% rename from contract-ts/04-ft-owner-claims-winner-gets-nft/src/contract.ts rename to contract-ts/03-bid-with-fts/src/contract.ts index b3accfe1..889ee4ee 100644 --- a/contract-ts/04-ft-owner-claims-winner-gets-nft/src/contract.ts +++ b/contract-ts/03-bid-with-fts/src/contract.ts @@ -13,14 +13,14 @@ const NO_DEPOSIT = BigInt(0); class AuctionContract { highest_bid: Bid = { bidder: '', bid: BigInt(1) }; auction_end_time: bigint = BigInt(0); - auctioneer: string = ""; + auctioneer: AccountId = ""; claimed: boolean = false; ft_contract: AccountId = ""; nft_contract: AccountId = ""; token_id: string = ""; @initialize({ privateFunction: true }) - init({ end_time, auctioneer, ft_contract, nft_contract, token_id, starting_price }: { end_time: bigint, auctioneer: string, ft_contract: AccountId, nft_contract: AccountId, token_id: string, starting_price: bigint }) { + init({ end_time, auctioneer, ft_contract, nft_contract, token_id, starting_price }: { end_time: bigint, auctioneer: AccountId, ft_contract: AccountId, nft_contract: AccountId, token_id: string, starting_price: bigint }) { this.auction_end_time = end_time; this.highest_bid = { bidder: near.currentAccountId(), bid: starting_price }; this.auctioneer = auctioneer; @@ -29,30 +29,6 @@ class AuctionContract { this.token_id = token_id; } - @view({}) - get_highest_bid(): Bid { - return this.highest_bid; - } - - @view({}) - get_auction_end_time(): BigInt { - return this.auction_end_time; - } - - @call({}) - claim() { - assert(this.auction_end_time <= near.blockTimestamp(), "Auction has not ended yet"); - assert(!this.claimed, "Auction has been claimed"); - - this.claimed = true; - - return NearPromise.new(this.nft_contract) - .functionCall("nft_transfer", JSON.stringify({ receiver_id: this.highest_bid.bidder, token_id: this.token_id }), BigInt(1), THIRTY_TGAS) - .then(NearPromise.new(this.ft_contract) - .functionCall("ft_transfer", JSON.stringify({ receiver_id: this.auctioneer, amount: this.highest_bid.bid }), BigInt(1), THIRTY_TGAS)) - .asReturn() - } - @call({}) ft_on_transfer({ sender_id, amount, msg }: { sender_id: AccountId, amount: bigint, msg: String }) { @@ -75,8 +51,37 @@ class AuctionContract { .asReturn() } + @call({}) + claim() { + assert(this.auction_end_time <= near.blockTimestamp(), "Auction has not ended yet"); + assert(!this.claimed, "Auction has been claimed"); + + this.claimed = true; + + return NearPromise.new(this.nft_contract) + .functionCall("nft_transfer", JSON.stringify({ receiver_id: this.highest_bid.bidder, token_id: this.token_id }), BigInt(1), THIRTY_TGAS) + .then(NearPromise.new(this.ft_contract) + .functionCall("ft_transfer", JSON.stringify({ receiver_id: this.auctioneer, amount: this.highest_bid.bid }), BigInt(1), THIRTY_TGAS)) + .asReturn() + } + @call({ privateFunction: true }) ft_transfer_callback({ }): BigInt { return BigInt(0); } + + @view({}) + get_highest_bid(): Bid { + return this.highest_bid; + } + + @view({}) + get_auction_end_time(): BigInt { + return this.auction_end_time; + } + + @view({}) + get_auction_info(): AuctionContract { + return this; + } } \ No newline at end of file diff --git a/contract-ts/03-owner-claims-winner-gets-nft/tsconfig.json b/contract-ts/03-bid-with-fts/tsconfig.json similarity index 100% rename from contract-ts/03-owner-claims-winner-gets-nft/tsconfig.json rename to contract-ts/03-bid-with-fts/tsconfig.json diff --git a/contract-ts/04-ft-owner-claims-winner-gets-nft/README.md b/contract-ts/04-ft-owner-claims-winner-gets-nft/README.md deleted file mode 100644 index 43242c2a..00000000 --- a/contract-ts/04-ft-owner-claims-winner-gets-nft/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Hello NEAR Contract - -The smart contract exposes two methods to enable storing and retrieving a greeting in the NEAR network. - -```ts -@NearBindgen({}) -class HelloNear { - greeting: string = "Hello"; - - @view // This method is read-only and can be called for free - get_greeting(): string { - return this.greeting; - } - - @call // This method changes the state, for which it cost gas - set_greeting({ greeting }: { greeting: string }): void { - // Record a log permanently to the blockchain! - near.log(`Saving greeting ${greeting}`); - this.greeting = greeting; - } -} -``` - -
- -# Quickstart - -1. Make sure you have installed [node.js](https://nodejs.org/en/download/package-manager/) >= 16. -2. Install the [`NEAR CLI`](https://github.com/near/near-cli#setup) - -
- -## 1. Build and Test the Contract -You can automatically compile and test the contract by running: - -```bash -npm run build -``` - -
- -## 2. Create an Account and Deploy the Contract -You can create a new account and deploy the contract by running: - -```bash -near create-account --useFaucet -near deploy build/release/hello_near.wasm -``` - -
- - -## 3. Retrieve the Greeting - -`get_greeting` is a read-only method (aka `view` method). - -`View` methods can be called for **free** by anyone, even people **without a NEAR account**! - -```bash -# Use near-cli to get the greeting -near view get_greeting -``` - -
- -## 4. Store a New Greeting -`set_greeting` changes the contract's state, for which it is a `call` method. - -`Call` methods can only be invoked using a NEAR account, since the account needs to pay GAS for the transaction. - -```bash -# Use near-cli to set a new greeting -near call set_greeting '{"greeting":"howdy"}' --accountId -``` - -**Tip:** If you would like to call `set_greeting` using another account, first login into NEAR using: - -```bash -# Use near-cli to login your NEAR account -near login -``` - -and then use the logged account to sign the transaction: `--accountId `. \ No newline at end of file diff --git a/contract-ts/04-ft-owner-claims-winner-gets-nft/tsconfig.json b/contract-ts/04-ft-owner-claims-winner-gets-nft/tsconfig.json deleted file mode 100644 index c3d38e60..00000000 --- a/contract-ts/04-ft-owner-claims-winner-gets-nft/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "experimentalDecorators": true, - "target": "ES5", - "noEmit": true, - "noImplicitAny": false, - }, - "files": [ - "src/contract.ts" - ], - "exclude": [ - "node_modules" - ], -} \ No newline at end of file diff --git a/frontend/src/config.js b/frontend/src/config.js deleted file mode 100644 index 198b9451..00000000 --- a/frontend/src/config.js +++ /dev/null @@ -1 +0,0 @@ -export const AUCTION_CONTRACT = "auction-example.testnet"; // Replace with your contract name \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontends/01-frontend/.eslintrc.json similarity index 100% rename from frontend/.eslintrc.json rename to frontends/01-frontend/.eslintrc.json diff --git a/frontend/README.md b/frontends/01-frontend/README.md similarity index 100% rename from frontend/README.md rename to frontends/01-frontend/README.md diff --git a/frontend/jsconfig.json b/frontends/01-frontend/jsconfig.json similarity index 100% rename from frontend/jsconfig.json rename to frontends/01-frontend/jsconfig.json diff --git a/frontend/next.config.js b/frontends/01-frontend/next.config.js similarity index 100% rename from frontend/next.config.js rename to frontends/01-frontend/next.config.js diff --git a/frontend/package.json b/frontends/01-frontend/package.json similarity index 95% rename from frontend/package.json rename to frontends/01-frontend/package.json index c1412c0c..394b15bf 100644 --- a/frontend/package.json +++ b/frontends/01-frontend/package.json @@ -1,5 +1,5 @@ { - "name": "hello-near", + "name": "auction-frontend", "version": "1.0.0", "private": true, "engines": { diff --git a/frontend/public/favicon.ico b/frontends/01-frontend/public/favicon.ico similarity index 100% rename from frontend/public/favicon.ico rename to frontends/01-frontend/public/favicon.ico diff --git a/frontend/public/near-logo.svg b/frontends/01-frontend/public/near-logo.svg similarity index 100% rename from frontend/public/near-logo.svg rename to frontends/01-frontend/public/near-logo.svg diff --git a/frontend/public/near.svg b/frontends/01-frontend/public/near.svg similarity index 100% rename from frontend/public/near.svg rename to frontends/01-frontend/public/near.svg diff --git a/frontend/public/next.svg b/frontends/01-frontend/public/next.svg similarity index 100% rename from frontend/public/next.svg rename to frontends/01-frontend/public/next.svg diff --git a/frontend/public/vercel.svg b/frontends/01-frontend/public/vercel.svg similarity index 100% rename from frontend/public/vercel.svg rename to frontends/01-frontend/public/vercel.svg diff --git a/frontends/01-frontend/src/components/Bid.jsx b/frontends/01-frontend/src/components/Bid.jsx new file mode 100644 index 00000000..20493b8c --- /dev/null +++ b/frontends/01-frontend/src/components/Bid.jsx @@ -0,0 +1,60 @@ +import { useContext, useEffect, useState } from 'react'; +import { NearContext } from '@/context'; +import styles from './Bid.module.css'; +import { toast } from 'react-toastify'; + +const Bid = ({pastBids, lastBid, action}) => { + const [amount, setAmount] = useState(lastBid + 1); + const { signedAccountId } = useContext(NearContext); + const nearMultiplier = Math.pow(10, 24) + + const handleBid = async () => { + if (signedAccountId) { + await action(amount); + toast("you have made a successful bid"); + } else { + toast("Please sign in to make a bid"); + } + } + + useEffect(() => { + setAmount(lastBid + 1); + } + , [lastBid]); + + return ( +
+

History

+ {typeof pastBids === 'string' ? ( +

{pastBids}

+ ) : pastBids === null ? ( +

Loading...

+ ) : pastBids.length === 0 ? ( +

No bids have been placed yet

+ ) : ( +
    + {pastBids?.map((bid, index) => ( +
  • + {bid[1] / nearMultiplier} $NEAR + {bid[0]} +
  • + ))} +
+ )} +
+ setAmount(e.target.value)} + className={styles.inputField} + /> + +
+
+ ); +} + +export default Bid; \ No newline at end of file diff --git a/frontend/src/components/Bid.module.css b/frontends/01-frontend/src/components/Bid.module.css similarity index 100% rename from frontend/src/components/Bid.module.css rename to frontends/01-frontend/src/components/Bid.module.css diff --git a/frontends/01-frontend/src/components/LastBid.jsx b/frontends/01-frontend/src/components/LastBid.jsx new file mode 100644 index 00000000..eaa2b8ac --- /dev/null +++ b/frontends/01-frontend/src/components/LastBid.jsx @@ -0,0 +1,15 @@ +import styles from './LastBid.module.css'; + +const LastBid = ({lastBid, highestBidder, lastUpdate}) => { + return ( +
+
+ The last bid was {lastBid} $NEAR +
+ Made by {highestBidder} + Refresh page in {lastUpdate} +
+ ) +} + +export default LastBid \ No newline at end of file diff --git a/frontend/src/components/LastBid.module.css b/frontends/01-frontend/src/components/LastBid.module.css similarity index 100% rename from frontend/src/components/LastBid.module.css rename to frontends/01-frontend/src/components/LastBid.module.css diff --git a/frontend/src/components/Navigation.jsx b/frontends/01-frontend/src/components/Navigation.jsx similarity index 100% rename from frontend/src/components/Navigation.jsx rename to frontends/01-frontend/src/components/Navigation.jsx diff --git a/frontend/src/components/Skeletons/Skeleton.module.css b/frontends/01-frontend/src/components/Skeletons/Skeleton.module.css similarity index 100% rename from frontend/src/components/Skeletons/Skeleton.module.css rename to frontends/01-frontend/src/components/Skeletons/Skeleton.module.css diff --git a/frontend/src/components/Skeletons/SkeletonBid.jsx b/frontends/01-frontend/src/components/Skeletons/SkeletonBid.jsx similarity index 100% rename from frontend/src/components/Skeletons/SkeletonBid.jsx rename to frontends/01-frontend/src/components/Skeletons/SkeletonBid.jsx diff --git a/frontend/src/components/Skeletons/SkeletonTimer.jsx b/frontends/01-frontend/src/components/Skeletons/SkeletonTimer.jsx similarity index 100% rename from frontend/src/components/Skeletons/SkeletonTimer.jsx rename to frontends/01-frontend/src/components/Skeletons/SkeletonTimer.jsx diff --git a/frontend/src/components/Timer.jsx b/frontends/01-frontend/src/components/Timer.jsx similarity index 74% rename from frontend/src/components/Timer.jsx rename to frontends/01-frontend/src/components/Timer.jsx index 922fb286..4e69066b 100644 --- a/frontend/src/components/Timer.jsx +++ b/frontends/01-frontend/src/components/Timer.jsx @@ -44,20 +44,23 @@ const Timer = ({ endTime, claimed, action }) => {

Auction has been claimed!

} {showCounter && ( -
-
- {String(days).padStart(2, '0')} Days -
-
- {String(hours).padStart(2, '0')} Hours -
-
- {String(minutes).padStart(2, '0')} Minutes -
-
- {String(seconds).padStart(2, '0')} Seconds -
-
+
+

Time Remaining:

+
+
+ {String(days).padStart(2, '0')} Days +
+
+ {String(hours).padStart(2, '0')} Hours +
+
+ {String(minutes).padStart(2, '0')} Minutes +
+
+ {String(seconds).padStart(2, '0')} Seconds +
+
+
)} {showActionButton &&
diff --git a/frontend/src/components/Timer.module.css b/frontends/01-frontend/src/components/Timer.module.css similarity index 100% rename from frontend/src/components/Timer.module.css rename to frontends/01-frontend/src/components/Timer.module.css diff --git a/frontends/01-frontend/src/config.js b/frontends/01-frontend/src/config.js new file mode 100644 index 00000000..3885a2e0 --- /dev/null +++ b/frontends/01-frontend/src/config.js @@ -0,0 +1,2 @@ +export const AUCTION_CONTRACT = "basic-auction-example.testnet"; // Replace with your contract name +export const NetworkId = "testnet"; \ No newline at end of file diff --git a/frontend/src/context.js b/frontends/01-frontend/src/context.js similarity index 100% rename from frontend/src/context.js rename to frontends/01-frontend/src/context.js diff --git a/frontend/src/pages/_app.js b/frontends/01-frontend/src/pages/_app.js similarity index 82% rename from frontend/src/pages/_app.js rename to frontends/01-frontend/src/pages/_app.js index c7ea05a3..9d66ea65 100644 --- a/frontend/src/pages/_app.js +++ b/frontends/01-frontend/src/pages/_app.js @@ -5,12 +5,12 @@ import { NearContext } from '@/context'; import { Navigation } from '@/components/Navigation'; import { Wallet } from '@/wallets/near'; -import { NetworkId, AuctionContract } from '@/config'; +import { NetworkId } from '@/config'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -const wallet = new Wallet({ createAccessKeyFor: AuctionContract, networkId: NetworkId }); +const wallet = new Wallet({ networkId: NetworkId }); export default function MyApp({ Component, pageProps }) { const [signedAccountId, setSignedAccountId] = useState(''); diff --git a/frontends/01-frontend/src/pages/api/getBidHistory.js b/frontends/01-frontend/src/pages/api/getBidHistory.js new file mode 100644 index 00000000..61171667 --- /dev/null +++ b/frontends/01-frontend/src/pages/api/getBidHistory.js @@ -0,0 +1,42 @@ +export default async function handler(req, res) { + try { + if (!process.env.API_KEY) { + return res.status(500).json({ error: "API key not provided" }); + } + // Get all bid transactions + const { contractId } = req.query; + const bidsRes = await fetch(`https://api-testnet.nearblocks.io/v1/account/${contractId}/txns?method=bid&page=1&per_page=25&order=desc`, { + headers: { + 'Accept': '*/*', + 'Authorization': `Bearer ${process.env.API_KEY}` + } + }); + + const bidsJson = await bidsRes.json(); + + const txns = bidsJson.txns; + let pastBids = []; + + // Loop through all bids and add valid bids to the pastBids array until 5 are found + for (let i = 0; i < txns.length; i++) { + const txn = txns[i]; + + if (txn.receipt_outcome.status) { + let amount = txn.actions[0].deposit; + let account = txn.predecessor_account_id + + if (pastBids.length < 5) { + pastBids.push([account, amount]); + } else { + break; + } + } + } + + // Respond with the past bids + return res.status(200).json({ pastBids }); + } catch (error) { + return res.status(500).json({ error: "Failed to fetch past bids" }); + } + } + \ No newline at end of file diff --git a/frontends/01-frontend/src/pages/index.js b/frontends/01-frontend/src/pages/index.js new file mode 100644 index 00000000..da0c9718 --- /dev/null +++ b/frontends/01-frontend/src/pages/index.js @@ -0,0 +1,106 @@ +import styles from '@/styles/app.module.css'; +import Timer from '@/components/Timer'; +import Bid from '@/components/Bid'; +import { useContext, useEffect, useState } from 'react'; +import SkeletonTimer from '@/components/Skeletons/SkeletonTimer'; +import SkeletonBid from '@/components/Skeletons/SkeletonBid'; +import { NearContext } from '@/context'; +import { AUCTION_CONTRACT } from '@/config'; +import LastBid from '@/components/LastBid'; + +export default function Home() { + const [highestBid, setHighestBid] = useState(null) + const [highestBidder, setHighestBidder] = useState(null) + const [claimed, setClaimed] = useState(false) + const [auctionEndTime, setAuctionEndTime] = useState(null) + const [secondsRemaining, setSecondsRemaining] = useState(20) + const [pastBids, setPastBids] = useState(null) + const nearMultiplier = Math.pow(10, 24) + + const { wallet } = useContext(NearContext); + + useEffect(() => { + const getInfo = async () => { + const highestBidData = await wallet.viewMethod({ + contractId: AUCTION_CONTRACT, + method: "get_highest_bid", + }); + setHighestBid(highestBidData.bid / nearMultiplier) + setHighestBidder(highestBidData.bidder) + + const claimedData = await wallet.viewMethod({ + contractId: AUCTION_CONTRACT, + method: "get_claimed", + }); + setClaimed(claimedData) + + const auctionEndTimeData = await wallet.viewMethod({ + contractId: AUCTION_CONTRACT, + method: "get_auction_end_time", + }); + setAuctionEndTime(auctionEndTimeData) + } + getInfo(); + + fetchPastBids(); + + const intervalId = setInterval(() => { + getInfo(); + setSecondsRemaining(20); + }, 20000); + + const countdownIntervalId = setInterval(() => { + setSecondsRemaining(prev => (prev === 1 ? 20 : prev - 1)); + }, 1000); + + + return () => { + clearInterval(intervalId); + clearInterval(countdownIntervalId); + }; + }, []); + + const bid = async (amount) => { + let real_amount = amount * nearMultiplier + let response = await wallet.callMethod({ + contractId: AUCTION_CONTRACT, + method: "bid", + deposit: real_amount, + args: {}, + gas:"300000000000000" + }) + return response + } + + const claim = async () => { + let response = await wallet.callMethod({ + contractId: AUCTION_CONTRACT, + method: "claim", + gas:"300000000000000" + }) + return response + } + + const fetchPastBids = async () => { + const response = await fetch(`/api/getBidHistory?contractId=${AUCTION_CONTRACT}`); + const data = await response.json(); + if (data.error) { + setPastBids(data.error); + } else { + setPastBids(data.pastBids); + } + } + + return ( +
+
+ {!highestBid ? : } +
+
+ {!auctionEndTime ? : } + {!highestBidder ? : } +
+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/styles/app.module.css b/frontends/01-frontend/src/styles/app.module.css similarity index 100% rename from frontend/src/styles/app.module.css rename to frontends/01-frontend/src/styles/app.module.css diff --git a/frontend/src/styles/globals.css b/frontends/01-frontend/src/styles/globals.css similarity index 100% rename from frontend/src/styles/globals.css rename to frontends/01-frontend/src/styles/globals.css diff --git a/frontend/src/wallets/near.js b/frontends/01-frontend/src/wallets/near.js similarity index 100% rename from frontend/src/wallets/near.js rename to frontends/01-frontend/src/wallets/near.js diff --git a/frontends/03-frontend/.eslintrc.json b/frontends/03-frontend/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/frontends/03-frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/frontends/03-frontend/README.md b/frontends/03-frontend/README.md new file mode 100644 index 00000000..9d93bae0 --- /dev/null +++ b/frontends/03-frontend/README.md @@ -0,0 +1,45 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More about NEAR + +To learn more about NEAR, take a look at the following resources: + +- [NEAR Documentation](https://docs.near.org) - learn about NEAR. +- [Frontend Docs](https://docs.near.org/build/web3-apps/quickstart) - learn about this example. + +You can check out [the NEAR repository](https://github.com/near) - your feedback and contributions are welcome! + +## Learn More about Next.js + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/frontends/03-frontend/jsconfig.json b/frontends/03-frontend/jsconfig.json new file mode 100644 index 00000000..b8d6842d --- /dev/null +++ b/frontends/03-frontend/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/frontends/03-frontend/next.config.js b/frontends/03-frontend/next.config.js new file mode 100644 index 00000000..91ef62f0 --- /dev/null +++ b/frontends/03-frontend/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +}; + +module.exports = nextConfig; diff --git a/frontends/03-frontend/package.json b/frontends/03-frontend/package.json new file mode 100644 index 00000000..394b15bf --- /dev/null +++ b/frontends/03-frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "auction-frontend", + "version": "1.0.0", + "private": true, + "engines": { + "node": ">=18" + }, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@near-wallet-selector/core": "^8.9.11", + "@near-wallet-selector/here-wallet": "^8.9.11", + "@near-wallet-selector/modal-ui": "^8.9.11", + "@near-wallet-selector/my-near-wallet": "^8.9.11", + "bootstrap": "^5", + "bootstrap-icons": "^1.11.3", + "near-api-js": "^4.0.3", + "next": "14.2.3", + "react": "^18", + "react-dom": "^18", + "react-toastify": "^10.0.5" + }, + "devDependencies": { + "eslint": "^8", + "eslint-config-next": "14.2.3" + } +} diff --git a/frontends/03-frontend/public/favicon.ico b/frontends/03-frontend/public/favicon.ico new file mode 100644 index 00000000..e61f7869 Binary files /dev/null and b/frontends/03-frontend/public/favicon.ico differ diff --git a/frontends/03-frontend/public/near-logo.svg b/frontends/03-frontend/public/near-logo.svg new file mode 100644 index 00000000..50443c0e --- /dev/null +++ b/frontends/03-frontend/public/near-logo.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/frontends/03-frontend/public/near.svg b/frontends/03-frontend/public/near.svg new file mode 100644 index 00000000..acec6fc1 --- /dev/null +++ b/frontends/03-frontend/public/near.svg @@ -0,0 +1 @@ + diff --git a/frontends/03-frontend/public/next.svg b/frontends/03-frontend/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/frontends/03-frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontends/03-frontend/public/vercel.svg b/frontends/03-frontend/public/vercel.svg new file mode 100644 index 00000000..d2f84222 --- /dev/null +++ b/frontends/03-frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/AuctionItem.jsx b/frontends/03-frontend/src/components/AuctionItem.jsx similarity index 100% rename from frontend/src/components/AuctionItem.jsx rename to frontends/03-frontend/src/components/AuctionItem.jsx diff --git a/frontend/src/components/AuctionItem.module.css b/frontends/03-frontend/src/components/AuctionItem.module.css similarity index 100% rename from frontend/src/components/AuctionItem.module.css rename to frontends/03-frontend/src/components/AuctionItem.module.css diff --git a/frontend/src/components/Bid.jsx b/frontends/03-frontend/src/components/Bid.jsx similarity index 100% rename from frontend/src/components/Bid.jsx rename to frontends/03-frontend/src/components/Bid.jsx diff --git a/frontends/03-frontend/src/components/Bid.module.css b/frontends/03-frontend/src/components/Bid.module.css new file mode 100644 index 00000000..83345dcf --- /dev/null +++ b/frontends/03-frontend/src/components/Bid.module.css @@ -0,0 +1,54 @@ +.historyContainer { + /* border: 1px solid #ddd; */ + border-radius: 8px; + padding: 1rem; + width: 400px; + text-align: center; +} + +.bidItem { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid #eee; +} + +.bidItem:last-child { + border-bottom: none; +} + +.container { + display: flex; + align-items: center; + gap: 10px; /* Adds spacing between the input and button */ +} + +.inputField { + padding: 10px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 5px; + width: 100%; +} + +.bidButton { + background-color: #1679AB; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + font-size: 16px; + display: flex; + align-items: center; /* Center the icon and text vertically */ + justify-content: center; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.bidButton:hover { + background-color: #102C57; +} + +.iconFT { + margin-right: 10px; +} \ No newline at end of file diff --git a/frontend/src/components/LastBid.jsx b/frontends/03-frontend/src/components/LastBid.jsx similarity index 100% rename from frontend/src/components/LastBid.jsx rename to frontends/03-frontend/src/components/LastBid.jsx diff --git a/frontends/03-frontend/src/components/LastBid.module.css b/frontends/03-frontend/src/components/LastBid.module.css new file mode 100644 index 00000000..9811e6e7 --- /dev/null +++ b/frontends/03-frontend/src/components/LastBid.module.css @@ -0,0 +1,29 @@ +.iconFT { + margin-right: 10px; +} + +.description { + padding: 1rem; + text-align: center; +} + +.detail { + text-align: center; + display: flex; + align-items: center; + justify-content: center; +} + +.priceSection { + padding: 1rem; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.currentPrice { + font-size: 1.5rem; + color: #333; +} diff --git a/frontends/03-frontend/src/components/Navigation.jsx b/frontends/03-frontend/src/components/Navigation.jsx new file mode 100644 index 00000000..6849b8b6 --- /dev/null +++ b/frontends/03-frontend/src/components/Navigation.jsx @@ -0,0 +1,37 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { useEffect, useState, useContext } from 'react'; + +import { NearContext } from '@/context'; +import NearLogo from '/public/near-logo.svg'; + +export const Navigation = () => { + const { signedAccountId, wallet } = useContext(NearContext); + const [action, setAction] = useState(() => { }); + const [label, setLabel] = useState('Loading...'); + + useEffect(() => { + if (!wallet) return; + + if (signedAccountId) { + setAction(() => wallet.signOut); + setLabel(`Logout ${signedAccountId}`); + } else { + setAction(() => wallet.signIn); + setLabel('Login'); + } + }, [signedAccountId, wallet]); + + return ( + + ); +}; \ No newline at end of file diff --git a/frontends/03-frontend/src/components/Skeletons/Skeleton.module.css b/frontends/03-frontend/src/components/Skeletons/Skeleton.module.css new file mode 100644 index 00000000..3bead234 --- /dev/null +++ b/frontends/03-frontend/src/components/Skeletons/Skeleton.module.css @@ -0,0 +1,147 @@ +.container { + border: 1px solid #ddd; + border-radius: 8px; + overflow: hidden; + display: flex; + flex-direction: column; + width: 300px; + margin-bottom: 2rem; +} + +.imageSection {.container, .historyContainer { + border: 1px solid #ddd; + border-radius: 8px; + overflow: hidden; + display: flex; + flex-direction: column; + width: 300px; + margin-bottom: 2rem; + background-color: #f2f2f2; +} + +.imageSection { + position: relative; + padding: 1rem; +} + +.skeletonImage { + width: 100%; + height: 200px; + background-color: #e0e0e0; + border-radius: 8px; +} + +.description { + padding: 1rem; + text-align: center; +} + +.skeletonText, .skeletonStats { + height: 20px; + background-color: #e0e0e0; + margin: 10px 0; + border-radius: 4px; +} + +.skeletonStats { + height: 30px; + width: 50px; + margin: 0 auto; +} + +.priceSection { + background-color: #f7f7f7; + padding: 1rem; + text-align: center; +} + +.skeletonPrice { + height: 30px; + background-color: #e0e0e0; + margin: 10px 0; + border-radius: 4px; +} + +.skeletonButton { + height: 40px; + background-color: #e0e0e0; + margin: 10px 0; + border-radius: 4px; +} + +.timer { + display: flex; + gap: 1rem; + margin-bottom: 2rem; +} + +.timer div { + text-align: center; +} + +.timer span { + display: block; + font-size: 1.5rem; + font-weight: bold; +} + + +.skeletonTime { + height: 40px; + width: 40px; + background-color: #e0e0e0; + border-radius: 4px; +} + +.bidItem { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid #eee; +} + +.bidItem:last-child { + border-bottom: none; +} + position: relative; +} + +.imageSection img { + width: 100%; + height: auto; +} + +.description { + padding: 1rem; + text-align: center; +} + +.stats { + margin-top: 1rem; +} + +.stats span { + font-size: 1.2rem; + color: #555; +} + +.priceSection { + background-color: #f7f7f7; + padding: 1rem; + text-align: center; +} + +.currentPrice { + font-size: 1.5rem; + color: #333; +} + +.bidButton { + background-color: #ff9800; + color: white; + border: none; + padding: 0.5rem 1rem; + margin-top: 1rem; + cursor: pointer; + border-radius: 4px; +} diff --git a/frontend/src/components/Skeletons/SkeletonAuctionItem.jsx b/frontends/03-frontend/src/components/Skeletons/SkeletonAuctionItem.jsx similarity index 100% rename from frontend/src/components/Skeletons/SkeletonAuctionItem.jsx rename to frontends/03-frontend/src/components/Skeletons/SkeletonAuctionItem.jsx diff --git a/frontends/03-frontend/src/components/Skeletons/SkeletonBid.jsx b/frontends/03-frontend/src/components/Skeletons/SkeletonBid.jsx new file mode 100644 index 00000000..d86194ba --- /dev/null +++ b/frontends/03-frontend/src/components/Skeletons/SkeletonBid.jsx @@ -0,0 +1,19 @@ +import styles from './Skeleton.module.css'; + +const SkeletonBid = () => { + return ( +
+

History

+
    + {Array.from({ length: 5 }).map((_, index) => ( +
  • +
    +
    +
  • + ))} +
+
+ ); +} + +export default SkeletonBid; diff --git a/frontends/03-frontend/src/components/Skeletons/SkeletonTimer.jsx b/frontends/03-frontend/src/components/Skeletons/SkeletonTimer.jsx new file mode 100644 index 00000000..9f3391c8 --- /dev/null +++ b/frontends/03-frontend/src/components/Skeletons/SkeletonTimer.jsx @@ -0,0 +1,24 @@ +import styles from './Skeleton.module.css'; + +const SkeletonTimer = () => { + return ( +
+
+ 99 Days +
+
+ 99 Hours +
+
+ 99 Minutes +
+
+ 99 Seconds +
+
+ + + ); +} + +export default SkeletonTimer; \ No newline at end of file diff --git a/frontends/03-frontend/src/components/Timer.jsx b/frontends/03-frontend/src/components/Timer.jsx new file mode 100644 index 00000000..4e69066b --- /dev/null +++ b/frontends/03-frontend/src/components/Timer.jsx @@ -0,0 +1,77 @@ +import { useEffect, useState } from 'react'; +import styles from './Timer.module.css'; +import { toast } from 'react-toastify'; + +const Timer = ({ endTime, claimed, action }) => { + const claim = async() =>{ + await action(); + toast("Congratulations!!") + }; + + const [time, setTime] = useState((Number(endTime) / 10 ** 6) - Date.now()); + useEffect(() => { + const timer = setInterval(() => { + setTime((prevTime) => { + const newTime = prevTime - 1000; + if (newTime <= 0) { + clearInterval(timer); + return 0; + } + return newTime; + }); + }, 1000); + + return () => clearInterval(timer); + }, []); + + const formatTime = (time) => { + const allSeconds = Math.floor(time / 1000); + const days = Math.floor(allSeconds / (3600 * 24)); + const hours = Math.floor((allSeconds % (3600 * 24)) / 3600); + const minutes = Math.floor((allSeconds % 3600) / 60); + const seconds = allSeconds % 60; + + return { allSeconds, days, hours, minutes, seconds }; + }; + + const { allSeconds, days, hours, minutes, seconds } = formatTime(time); + + const showCounter = !claimed && allSeconds > 0 + const showActionButton = !claimed && allSeconds <=0 + return ( + <> + {claimed &&
+

Auction has been claimed!

+
} + {showCounter && ( +
+

Time Remaining:

+
+
+ {String(days).padStart(2, '0')} Days +
+
+ {String(hours).padStart(2, '0')} Hours +
+
+ {String(minutes).padStart(2, '0')} Minutes +
+
+ {String(seconds).padStart(2, '0')} Seconds +
+
+
+ )} + {showActionButton && +
+ +
+ } + + + ); +}; + +export default Timer; \ No newline at end of file diff --git a/frontends/03-frontend/src/components/Timer.module.css b/frontends/03-frontend/src/components/Timer.module.css new file mode 100644 index 00000000..3e54b668 --- /dev/null +++ b/frontends/03-frontend/src/components/Timer.module.css @@ -0,0 +1,33 @@ +.timer { + display: flex; + gap: 1rem; + margin-bottom: 2rem; +} + +.timer div { + text-align: center; +} + +.timer span { + display: block; + font-size: 1.5rem; + font-weight: bold; +} + +.button { + background-color: #1679AB; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + font-size: 16px; + display: flex; + align-items: center; /* Center the icon and text vertically */ + justify-content: center; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.button:hover { + background-color: #102C57; +} \ No newline at end of file diff --git a/frontends/03-frontend/src/config.js b/frontends/03-frontend/src/config.js new file mode 100644 index 00000000..a6a384c2 --- /dev/null +++ b/frontends/03-frontend/src/config.js @@ -0,0 +1,2 @@ +export const AUCTION_CONTRACT = "auction-example.testnet"; // Replace with your contract name +export const NetworkId = "testnet"; \ No newline at end of file diff --git a/frontends/03-frontend/src/context.js b/frontends/03-frontend/src/context.js new file mode 100644 index 00000000..74223096 --- /dev/null +++ b/frontends/03-frontend/src/context.js @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +/** + * @typedef NearContext + * @property {import('./wallets/near').Wallet} wallet Current wallet + * @property {string} signedAccountId The AccountId of the signed user + */ + +/** @type {import ('react').Context} */ +export const NearContext = createContext({ + wallet: undefined, + signedAccountId: '' +}); \ No newline at end of file diff --git a/frontends/03-frontend/src/pages/_app.js b/frontends/03-frontend/src/pages/_app.js new file mode 100644 index 00000000..9d66ea65 --- /dev/null +++ b/frontends/03-frontend/src/pages/_app.js @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +import '@/styles/globals.css'; +import { NearContext } from '@/context'; +import { Navigation } from '@/components/Navigation'; + +import { Wallet } from '@/wallets/near'; +import { NetworkId } from '@/config'; + +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +const wallet = new Wallet({ networkId: NetworkId }); + +export default function MyApp({ Component, pageProps }) { + const [signedAccountId, setSignedAccountId] = useState(''); + + useEffect(() => { wallet.startUp(setSignedAccountId) }, []); + + return ( + + + + + + ); +} diff --git a/frontend/src/pages/api/getBidHistory.js b/frontends/03-frontend/src/pages/api/getBidHistory.js similarity index 94% rename from frontend/src/pages/api/getBidHistory.js rename to frontends/03-frontend/src/pages/api/getBidHistory.js index 1dd2f4a7..a7298019 100644 --- a/frontend/src/pages/api/getBidHistory.js +++ b/frontends/03-frontend/src/pages/api/getBidHistory.js @@ -8,7 +8,7 @@ export default async function handler(req, res) { const bidsRes = await fetch(`https://api-testnet.nearblocks.io/v1/account/${contractId}/txns?from=${ftId}&method=ft_on_transfer&page=1&per_page=25&order=desc`, { headers: { 'Accept': '*/*', - 'Authorization': `Bearer ${process.env.API_KEY}` // Use your API key here + 'Authorization': `Bearer ${process.env.API_KEY}` } }); diff --git a/frontend/src/pages/index.js b/frontends/03-frontend/src/pages/index.js similarity index 100% rename from frontend/src/pages/index.js rename to frontends/03-frontend/src/pages/index.js diff --git a/frontends/03-frontend/src/styles/app.module.css b/frontends/03-frontend/src/styles/app.module.css new file mode 100644 index 00000000..07b13bc3 --- /dev/null +++ b/frontends/03-frontend/src/styles/app.module.css @@ -0,0 +1,93 @@ +.main { + /* max-width: 1200px; */ + width: 100%; + height: 100%; + min-height: 100vh; + margin: auto 0; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 1rem; + gap: 1rem; +} + +.description { + display: inherit; + justify-content: inherit; + align-items: inherit; + font-size: 0.85rem; + max-width: var(--max-width); + width: 100%; + z-index: 2; + font-family: var(--font-mono); +} + +.code { + font-weight: 700; + font-family: var(--font-mono); +} + +.card { + padding: 1rem 1.2rem; + border-radius: var(--border-radius); + background: rgba(var(--card-rgb), 0); + border: 1px solid rgba(var(--card-border-rgb), 0); + transition: background 200ms, border 200ms; +} + +.card span { + display: inline-block; + transition: transform 200ms; +} + +.card h2 { + font-weight: 600; + margin-bottom: 0.7rem; +} + +.card p { + margin: 0; + opacity: 0.6; + font-size: 0.9rem; + line-height: 1.5; + max-width: 30ch; +} + +.leftPanel{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 4rem 0; + width: 100%; +} + +.rightPanel{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 4rem 0; + width: 100%; +} + +/* Mobile */ +@media (max-width: 700px) { + .main { + display: block; + } + + .leftPanel{ + padding: 0; + } + + .rightPanel{ + padding: 0; + } +} + +/* Tablet and Smaller Desktop */ +@media (min-width: 701px) and (max-width: 1120px) { + +} diff --git a/frontends/03-frontend/src/styles/globals.css b/frontends/03-frontend/src/styles/globals.css new file mode 100644 index 00000000..be775194 --- /dev/null +++ b/frontends/03-frontend/src/styles/globals.css @@ -0,0 +1,95 @@ +@import 'bootstrap'; +@import 'bootstrap-icons'; + +:root { + --max-width: 1100px; + --border-radius: 12px; + + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; + + --primary-glow: conic-gradient(from 180deg at 50% 50%, + #16abff33 0deg, + #0885ff33 55deg, + #54d6ff33 120deg, + #0071ff33 160deg, + transparent 360deg); + --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), + rgba(255, 255, 255, 0)); + + --tile-start-rgb: 239, 245, 249; + --tile-end-rgb: 228, 232, 233; + --tile-border: conic-gradient(#00000080, + #00000040, + #00000030, + #00000020, + #00000010, + #00000010, + #00000080); + + --callout-rgb: 238, 240, 241; + --callout-border-rgb: 172, 175, 176; + --card-rgb: 180, 185, 188; + --card-border-rgb: 131, 134, 135; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + + --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); + --secondary-glow: linear-gradient(to bottom right, + rgba(1, 65, 255, 0), + rgba(1, 65, 255, 0), + rgba(1, 65, 255, 0.3)); + + --tile-start-rgb: 2, 13, 46; + --tile-end-rgb: 2, 5, 19; + --tile-border: conic-gradient(#ffffff80, + #ffffff40, + #ffffff30, + #ffffff20, + #ffffff10, + #ffffff10, + #ffffff80); + + --callout-rgb: 20, 20, 20; + --callout-border-rgb: 108, 108, 108; + --card-rgb: 100, 100, 100; + --card-border-rgb: 200, 200, 200; + } +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + + + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient(to bottom, + transparent, + rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} + +body { + height-min: 100vh; +} \ No newline at end of file diff --git a/frontends/03-frontend/src/wallets/near.js b/frontends/03-frontend/src/wallets/near.js new file mode 100644 index 00000000..5c700af5 --- /dev/null +++ b/frontends/03-frontend/src/wallets/near.js @@ -0,0 +1,142 @@ +// near api js +import { providers } from 'near-api-js'; + +// wallet selector +import { distinctUntilChanged, map } from 'rxjs'; +import '@near-wallet-selector/modal-ui/styles.css'; +import { setupModal } from '@near-wallet-selector/modal-ui'; +import { setupWalletSelector } from '@near-wallet-selector/core'; +import { setupHereWallet } from '@near-wallet-selector/here-wallet'; +import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; + +const THIRTY_TGAS = '30000000000000'; +const NO_DEPOSIT = '0'; + +export class Wallet { + /** + * @constructor + * @param {Object} options - the options for the wallet + * @param {string} options.networkId - the network id to connect to + * @param {string} options.createAccessKeyFor - the contract to create an access key for + * @example + * const wallet = new Wallet({ networkId: 'testnet', createAccessKeyFor: 'contractId' }); + * wallet.startUp((signedAccountId) => console.log(signedAccountId)); + */ + constructor({ networkId = 'testnet', createAccessKeyFor = undefined }) { + this.createAccessKeyFor = createAccessKeyFor; + this.networkId = networkId; + } + + /** + * To be called when the website loads + * @param {Function} accountChangeHook - a function that is called when the user signs in or out# + * @returns {Promise} - the accountId of the signed-in user + */ + startUp = async (accountChangeHook) => { + this.selector = setupWalletSelector({ + network: this.networkId, + modules: [setupMyNearWallet(), setupHereWallet()] + }); + + const walletSelector = await this.selector; + const isSignedIn = walletSelector.isSignedIn(); + const accountId = isSignedIn ? walletSelector.store.getState().accounts[0].accountId : ''; + + walletSelector.store.observable + .pipe( + map(state => state.accounts), + distinctUntilChanged() + ) + .subscribe(accounts => { + const signedAccount = accounts.find((account) => account.active)?.accountId; + accountChangeHook(signedAccount); + }); + + return accountId; + }; + + /** + * Displays a modal to login the user + */ + signIn = async () => { + const modal = setupModal(await this.selector, { contractId: this.createAccessKeyFor }); + modal.show(); + }; + + /** + * Logout the user + */ + signOut = async () => { + const selectedWallet = await (await this.selector).wallet(); + selectedWallet.signOut(); + }; + + /** + * Makes a read-only call to a contract + * @param {Object} options - the options for the call + * @param {string} options.contractId - the contract's account id + * @param {string} options.method - the method to call + * @param {Object} options.args - the arguments to pass to the method + * @returns {Promise} - the result of the method call + */ + viewMethod = async ({ contractId, method, args = {} }) => { + const url = `https://rpc.${this.networkId}.near.org`; + const provider = new providers.JsonRpcProvider({ url }); + + let res = await provider.query({ + request_type: 'call_function', + account_id: contractId, + method_name: method, + args_base64: Buffer.from(JSON.stringify(args)).toString('base64'), + finality: 'optimistic', + }); + return JSON.parse(Buffer.from(res.result).toString()); + }; + + + /** + * Makes a call to a contract + * @param {Object} options - the options for the call + * @param {string} options.contractId - the contract's account id + * @param {string} options.method - the method to call + * @param {Object} options.args - the arguments to pass to the method + * @param {string} options.gas - the amount of gas to use + * @param {string} options.deposit - the amount of yoctoNEAR to deposit + * @returns {Promise} - the resulting transaction + */ + callMethod = async ({ contractId, method, args = {}, gas = THIRTY_TGAS, deposit = NO_DEPOSIT }) => { + // Sign a transaction with the "FunctionCall" action + const selectedWallet = await (await this.selector).wallet(); + const outcome = await selectedWallet.signAndSendTransaction({ + receiverId: contractId, + actions: [ + { + type: 'FunctionCall', + params: { + methodName: method, + args, + gas, + deposit, + }, + }, + ], + }); + + return providers.getTransactionLastResult(outcome); + }; + + /** + * Retrieves transaction result from the network + * @param {string} txhash - the transaction hash + * @returns {Promise} - the result of the transaction + */ + getTransactionResult = async (txhash) => { + const walletSelector = await this.selector; + const { network } = walletSelector.options; + const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); + + // Retrieve transaction result from the network + const transaction = await provider.txStatus(txhash, 'unnused'); + return providers.getTransactionLastResult(transaction); + }; +} \ No newline at end of file