diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..eab63b9 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,41 @@ +name: Contracts CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + contracts: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: contracts + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + targets: wasm32-unknown-unknown + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run clippy + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + - name: Run tests + run: cargo test --workspace + + - name: Build WASM artifacts + run: cargo build --workspace --target wasm32-unknown-unknown --release + diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 6576b9a..8803414 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,13 +1,3 @@ -[package] -name = "geev-core" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -soroban-sdk = "25.0.0" - -[dev-dependencies] -soroban-sdk = { version = "25.0.0", features = ["testutils"] } \ No newline at end of file +[workspace] +members = ["geev-core"] +resolver = "2" diff --git a/contracts/geev-core/Cargo.toml b/contracts/geev-core/Cargo.toml index feb04fa..4639c9f 100644 --- a/contracts/geev-core/Cargo.toml +++ b/contracts/geev-core/Cargo.toml @@ -4,4 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +soroban-sdk = { version = "22.0.0", default-features = false } + +[dev-dependencies] soroban-sdk = { version = "22.0.0", features = ["testutils"] } diff --git a/contracts/geev-core/src/giveaway.rs b/contracts/geev-core/src/giveaway.rs index 85176e8..e9c05de 100644 --- a/contracts/geev-core/src/giveaway.rs +++ b/contracts/geev-core/src/giveaway.rs @@ -1,20 +1,8 @@ -use soroban_sdk::{contracttype, Address, Env, Symbol, token}; +use soroban_sdk::{contracttype, Address, Env}; #[derive(Clone, Copy, PartialEq, Eq)] #[contracttype] -pub enum GiveawayStatus { - Active, - Ended, -} - -#[derive(Clone)] -#[contracttype] pub struct Giveaway { - pub id: u64, - pub status: GiveawayStatus, - pub creator: Address, - pub token: Address, - pub amount: i128, pub end_time: u64, pub participant_count: u32, } @@ -24,52 +12,15 @@ pub struct Giveaway { pub enum DataKey { Giveaway(u64), Participant(u64, Address), - GiveawayCount, } -pub fn create_giveaway( - env: Env, - creator: Address, - token: Address, - amount: i128, - end_time: u64, -) -> u64 { - creator.require_auth(); - - // Initialize the Token Client using the provided token address - let token_client = token::Client::new(&env, &token); - - // Execute transfer from creator to current contract - token_client.transfer(&creator, &env.current_contract_address(), &amount); - - // Generate a new Giveaway ID - let count_key = DataKey::GiveawayCount; - let mut count: u64 = env.storage().instance().get(&count_key).unwrap_or(0); - count += 1; - env.storage().instance().set(&count_key, &count); - - // Create Giveaway struct +pub fn create_giveaway(env: &Env, giveaway_id: u64, end_time: u64) { + let key = DataKey::Giveaway(giveaway_id); let giveaway = Giveaway { - id: count, - status: GiveawayStatus::Active, - creator: creator.clone(), - token: token.clone(), - amount, end_time, participant_count: 0, }; - - // Save struct to Persistent Storage under key: Giveaway(id) - let giveaway_key = DataKey::Giveaway(count); - env.storage().persistent().set(&giveaway_key, &giveaway); - - // Emit GiveawayCreated event for the NestJS indexer - env.events().publish( - (Symbol::new(&env, "GiveawayCreated"), count, creator), - (amount, token), - ); - - count + env.storage().persistent().set(&key, &giveaway); } pub fn enter_giveaway(env: Env, user: Address, giveaway_id: u64) { @@ -88,13 +39,12 @@ pub fn enter_giveaway(env: Env, user: Address, giveaway_id: u64) { } let participant_key = DataKey::Participant(giveaway_id, user.clone()); - if env.storage().instance().has(&participant_key) { + if env.storage().persistent().has(&participant_key) { panic!("Double Entry"); } - env.storage().instance().set(&participant_key, &true); + env.storage().persistent().set(&participant_key, &true); giveaway.participant_count += 1; env.storage().persistent().set(&giveaway_key, &giveaway); } - diff --git a/contracts/geev-core/src/lib.rs b/contracts/geev-core/src/lib.rs index 79f518f..125b1b7 100644 --- a/contracts/geev-core/src/lib.rs +++ b/contracts/geev-core/src/lib.rs @@ -1,4 +1,5 @@ -mod giveaway; +#![no_std] -pub use giveaway::{enter_giveaway, DataKey, Giveaway}; +mod giveaway; +pub use giveaway::{create_giveaway, enter_giveaway, DataKey, Giveaway}; diff --git a/contracts/geev-core/test_snapshots/enter_once_before_end_increments_participant_count.1.json b/contracts/geev-core/test_snapshots/enter_once_before_end_increments_participant_count.1.json new file mode 100644 index 0000000..4189780 --- /dev/null +++ b/contracts/geev-core/test_snapshots/enter_once_before_end_increments_participant_count.1.json @@ -0,0 +1,236 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "", + "args": [] + } + }, + "sub_invocations": [] + } + ] + ] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 10, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Giveaway" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Giveaway" + }, + { + "u64": 1 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "end_time" + }, + "val": { + "u64": 20 + } + }, + { + "key": { + "symbol": "participant_count" + }, + "val": { + "u32": 1 + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Participant" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Participant" + }, + { + "u64": 1 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/geev-core/test_snapshots/reject_duplicate_entries.1.json b/contracts/geev-core/test_snapshots/reject_duplicate_entries.1.json new file mode 100644 index 0000000..fa5add3 --- /dev/null +++ b/contracts/geev-core/test_snapshots/reject_duplicate_entries.1.json @@ -0,0 +1,221 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 10, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Giveaway" + }, + { + "u64": 3 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Giveaway" + }, + { + "u64": 3 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "end_time" + }, + "val": { + "u64": 20 + } + }, + { + "key": { + "symbol": "participant_count" + }, + "val": { + "u32": 1 + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Participant" + }, + { + "u64": 3 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Participant" + }, + { + "u64": 3 + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/geev-core/test_snapshots/reject_entry_after_end_time.1.json b/contracts/geev-core/test_snapshots/reject_entry_after_end_time.1.json new file mode 100644 index 0000000..c37721b --- /dev/null +++ b/contracts/geev-core/test_snapshots/reject_entry_after_end_time.1.json @@ -0,0 +1,170 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 10, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Giveaway" + }, + { + "u64": 2 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Giveaway" + }, + { + "u64": 2 + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "end_time" + }, + "val": { + "u64": 5 + } + }, + { + "key": { + "symbol": "participant_count" + }, + "val": { + "u32": 0 + } + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/geev-core/tests/enter_giveaway.rs b/contracts/geev-core/tests/enter_giveaway.rs index 26d31ad..69b68f2 100644 --- a/contracts/geev-core/tests/enter_giveaway.rs +++ b/contracts/geev-core/tests/enter_giveaway.rs @@ -1,25 +1,30 @@ -use soroban_sdk::{testutils::{Address as AddressTest, Ledger}, Address, Env}; -use geev_core::{enter_giveaway, DataKey, Giveaway}; +use geev_core::{create_giveaway, enter_giveaway}; +use soroban_sdk::{ + contract, contractimpl, + testutils::{Address as AddressTest, Ledger}, + Address, Env, +}; + +#[contract] +pub struct TestContract; + +#[contractimpl] +impl TestContract { + pub fn dummy(_env: Env) {} +} #[test] fn enter_once_before_end_increments_participant_count() { let env = Env::default(); + env.mock_all_auths(); env.ledger().set_timestamp(10); let user =
::generate(&env); let giveaway_id: u64 = 1; - let contract_id = ::generate(&env); + let contract_id = env.register(TestContract, ()); env.as_contract(&contract_id, || { - let key = DataKey::Giveaway(giveaway_id); - - let giveaway = Giveaway { - end_time: 20, - participant_count: 0, - }; - - env.storage().instance().set(&key, &giveaway); - + create_giveaway(&env, giveaway_id, 20); enter_giveaway(env.clone(), user.clone(), giveaway_id); }); } @@ -28,22 +33,15 @@ fn enter_once_before_end_increments_participant_count() { #[should_panic] fn reject_entry_after_end_time() { let env = Env::default(); + env.mock_all_auths(); env.ledger().set_timestamp(10); let user = ::generate(&env); let giveaway_id: u64 = 2; - let contract_id = ::generate(&env); + let contract_id = env.register(TestContract, ()); env.as_contract(&contract_id, || { - let key = DataKey::Giveaway(giveaway_id); - - let giveaway = Giveaway { - end_time: 5, // already ended relative to timestamp 10 - participant_count: 0, - }; - - env.storage().instance().set(&key, &giveaway); - + create_giveaway(&env, giveaway_id, 5); enter_giveaway(env.clone(), user.clone(), giveaway_id); }); } @@ -52,22 +50,15 @@ fn reject_entry_after_end_time() { #[should_panic] fn reject_duplicate_entries() { let env = Env::default(); + env.mock_all_auths(); env.ledger().set_timestamp(10); let user = ::generate(&env); let giveaway_id: u64 = 3; - let contract_id = ::generate(&env); + let contract_id = env.register(TestContract, ()); env.as_contract(&contract_id, || { - let key = DataKey::Giveaway(giveaway_id); - - let giveaway = Giveaway { - end_time: 20, - participant_count: 0, - }; - - env.storage().instance().set(&key, &giveaway); - + create_giveaway(&env, giveaway_id, 20); enter_giveaway(env.clone(), user.clone(), giveaway_id); // second attempt should fail due to duplicate entry