Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,31 @@ Changelog for `odra`.
- `#[odra::external_contract]` keeps the annotated trait and implements it for the generated
`XxxContractRef` and `XxxHostRef`, so the trait can be used as a bound or implemented by a module.
A trait declared a second time by hand as a workaround must be removed.
- Livenet: `ODRA_CASPER_LIVENET_STATE_ROOT_HASH` pins every read to a past state root hash (transactions are
refused while it is set). In `odra-cli` the global `--state-root-hash <HEX>` flag does the same for one
invocation or REPL session, so any getter, `inspect` or `storage` command can look at the chain as it was (#572).
- `odra_cli::ContractLoaderExt`: `MyContract::load_from_file(&env, path)`, `load_from_file_named` and
`load_from_default_file(&env)` load an already deployed contract from the contracts file written by the
`deploy` command, instead of pasting its package hash into a script (#566).
- `ContractEnv::debug(message)` prints from a contract: always on OdraVM and for livenet getters, and on
the Casper test VM when the contract is built with the new `test-support` feature of `odra` (`cargo odra
test -b casper -- --nocapture`). Without the feature the wasm carries no trace of it (#616).
- `WalletProxy` example: a contract attaches CSPR to a cross-contract call with
`XxxContractRef::with_tokens(..)`, both from the value attached to its own call and from its balance;
tested on OdraVM and CasperVM (#529).
- Project templates ship `AGENTS.md` (and `CLAUDE.md` importing it): a short pointer that tells an AI
agent to install the Odra Claude Code plugin or read `llms.txt`, and lists the `cargo odra` commands.
- `odra_test::odra_env()` and `odra_test::casper_env()` are public, so a test can be pinned to one backend
regardless of `ODRA_BACKEND`. Modules that are not registered in `Odra.toml` (no wasm) can be tested
on OdraVM under `cargo odra test -b casper` this way.

### Changed
- `#[odra::module(name = "..")]` now also names the package: the named key the package hash is stored
under at install is `<name>_package_hash` instead of `<StructName>_package_hash` (`HasIdent::contract_name()`,
used by `InstallConfig` and `UpgradeConfig`). Modules without `name` are unaffected (#385).
- `HostEnv::advance_block_time` and `advance_with_auctions` take a `core::time::Duration`; `auction_delay()`
and `unbonding_delay()` return one. Block time itself (`block_time()`, `get_block_time()`) stays in
milliseconds. Wrap old values with `Duration::from_millis(..)` or write `Duration::from_secs(..)` (#589).
- Livenet: `CasperClient` caches global state and dictionary query responses for the state root hash
they were read at (a query at a fixed state root is deterministic). Resolving a contract's entity,
the events counter `HostEnv` reads after every call and repeated getters cost no RPC calls within
Expand Down
4 changes: 4 additions & 0 deletions core/src/contract_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ pub trait ContractContext {
/// * `event` - The event data to emit.
fn emit_native_event(&self, event: &Bytes);

/// Prints a debug message on the host: always on OdraVM and livenet, inside wasm only when the
/// contract is built with the `test-support` feature of `odra`.
fn debug(&self, message: &str);

/// Transfers tokens to the specified address.
///
/// # Arguments
Expand Down
11 changes: 10 additions & 1 deletion core/src/contract_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,17 @@ pub enum EntrypointAttribute {

/// A trait that should be implemented by each smart contract to allow the backend.
pub trait HasIdent {
/// Returns the contract's ident.
/// Returns the contract's ident - the name of the module struct.
fn ident() -> String;

/// Returns the contract's name: the `name` given in `#[odra::module(name = "..")]`, or the
/// ident when none was given. It names the contract in the schema and is the base of the
/// named key the package hash is stored under (`<name>_package_hash`).
///
/// Not called `name` on purpose: that is a common entry point name (token contracts).
fn contract_name() -> String {
Self::ident()
}
}

/// A trait that should be implemented by each smart contract to allow the backend
Expand Down
18 changes: 18 additions & 0 deletions core/src/contract_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,24 @@ impl ContractEnv {
backend.emit_event(&bytes.into())
}

/// Prints a debug message on the host running the contract.
///
/// Always printed on OdraVM and for getters on livenet (they run locally). Inside wasm the
/// call is a no-op unless the contract is built with the `test-support` feature of `odra`,
/// in which case the Casper VM used by `cargo odra test -b casper` prints it (run the tests
/// with `-- --nocapture` to see it). A contract built without the feature carries no trace of
/// the message - but the arguments are still evaluated, so keep `format!` out of hot paths.
/// Do not build production wasm with the feature: on a real network the message only lands in
/// the node's log and costs gas.
///
/// ```ignore
/// self.env().debug(format!("transfer of {amount} from {from:?}"));
/// ```
pub fn debug(&self, message: impl AsRef<str>) {
let backend = self.backend.borrow();
backend.debug(message.as_ref())
}

/// Emits an event with the specified data using the native mechanism.
pub fn emit_native_event<T: ToBytes + EventInstance>(&self, event: T) {
let backend = self.backend.borrow();
Expand Down
46 changes: 31 additions & 15 deletions core/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use casper_types::{
bytesrepr::{Bytes, FromBytes, ToBytes},
CLTyped, PublicKey, RuntimeArgs, U512
};
use core::time::Duration;

/// A host side reference to a contract.
pub trait HostRef {
Expand Down Expand Up @@ -176,7 +177,7 @@ impl InstallConfig {
/// Returns new InstallConfig
pub fn new<T: HasIdent>(is_upgradable: bool, allow_key_override: bool) -> Self {
InstallConfig {
package_named_key: T::ident(),
package_named_key: T::contract_name(),
is_upgradable,
allow_key_override
}
Expand All @@ -194,7 +195,7 @@ impl UpgradeConfig {
/// It is by default upgradable and allows key override.
pub fn new<T: HasIdent>() -> Self {
UpgradeConfig {
package_named_key: T::ident(),
package_named_key: T::contract_name(),
force_create_upgrade_group: false,
allow_key_override: true
}
Expand Down Expand Up @@ -501,29 +502,39 @@ impl HostEnv {
backend.set_caller(address)
}

/// Advances the block time by the specified time difference in milliseconds.
pub fn advance_block_time(&self, time_diff: u64) {
/// Advances the block time by `time_diff`.
///
/// Block time has millisecond resolution; anything finer is truncated.
///
/// ```
/// # use core::time::Duration;
/// # fn shift(env: &odra_core::host::HostEnv) {
/// env.advance_block_time(Duration::from_secs(60 * 60 * 24));
/// # }
/// ```
pub fn advance_block_time(&self, time_diff: Duration) {
let backend = self.backend.as_ref();
backend.advance_block_time(time_diff)
backend.advance_block_time(millis(time_diff))
}

/// Advances the block time by the specified time difference in milliseconds
/// and processes auctions.
pub fn advance_with_auctions(&self, time_diff: u64) {
/// Advances the block time by `time_diff` and processes auctions.
///
/// Block time has millisecond resolution; anything finer is truncated.
pub fn advance_with_auctions(&self, time_diff: Duration) {
let backend = self.backend.as_ref();
backend.advance_with_auctions(time_diff);
backend.advance_with_auctions(millis(time_diff));
}

/// Returns the era length in milliseconds.
pub fn auction_delay(&self) -> u64 {
/// Returns the era length.
pub fn auction_delay(&self) -> Duration {
let backend = self.backend.as_ref();
backend.auction_delay()
Duration::from_millis(backend.auction_delay())
}

/// Returns the delay between unstaking and the transfer of funds back to the delegator in milliseconds.
pub fn unbonding_delay(&self) -> u64 {
/// Returns the delay between unstaking and the transfer of funds back to the delegator.
pub fn unbonding_delay(&self) -> Duration {
let backend = self.backend.as_ref();
backend.unbonding_delay()
Duration::from_millis(backend.unbonding_delay())
}

/// Returns the amount of CSPR delegated to the specified validator by the specified delegator.
Expand Down Expand Up @@ -1042,6 +1053,11 @@ impl HostEnv {
}
}

/// Block time is kept in milliseconds by every backend.
fn millis(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}

#[cfg(test)]
mod test {
use core::fmt::Debug;
Expand Down
2 changes: 1 addition & 1 deletion examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ repository = { workspace = true }
version = { workspace = true }

[dependencies]
odra = { path = "../odra", features = [], default-features = false }
odra = { path = "../odra", features = ["test-support"], default-features = false }
odra-casper-livenet-env = { path = "../odra-casper/livenet-env", optional = true }
odra-modules = { path = "../modules", features = ["eip712"], default-features = false }
casper-eip-712 = { version = "1.2.0", optional = true }
Expand Down
3 changes: 3 additions & 0 deletions examples/Odra.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ fqn = "features::modules::ModulesContract"
[[contracts]]
fqn = "features::native_token::PublicWallet"

[[contracts]]
fqn = "features::native_token::WalletProxy"

[[contracts]]
fqn = "features::livenet::LivenetContract"

Expand Down
3 changes: 2 additions & 1 deletion examples/bin/validators_on_livenet.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Deploys an example Validators contract and tests its functionality.
use core::time::Duration;
use odra::casper_types::{PublicKey, U512};
use odra::host::{Deployer, HostEnv, HostRef};
use odra::prelude::*;
Expand Down Expand Up @@ -30,7 +31,7 @@ fn main() {
println!("Auction delay: {:?}", env.auction_delay());
println!("Unbonding delay: {:?}", env.unbonding_delay());

env.advance_with_auctions(1000);
env.advance_with_auctions(Duration::from_secs(1));
}

/// Deploys an ERC20 contract.
Expand Down
3 changes: 2 additions & 1 deletion examples/ourcoin/bin/our_token_livenet.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Deploys a new OurToken contract on the Casper livenet and mints some tokens for the tutorial
//! creator.
use core::time::Duration;
use std::str::FromStr;

use odra::casper_types::U256;
Expand Down Expand Up @@ -32,7 +33,7 @@ fn main() {
// we set the voting time to 10 minutes.
// OH NO! It is the Livenet, so we need to wait real time...
// Hopefully you are not in a hurry.
env.advance_block_time(11 * 60 * 1000);
env.advance_block_time(Duration::from_secs(11 * 60));

// Tally the votes.
token.tally();
Expand Down
5 changes: 3 additions & 2 deletions examples/ourcoin/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ impl OurToken {
#[cfg(test)]
mod tests {
use super::*;
use core::time::Duration;
use odra::host::Deployer;

#[test]
Expand All @@ -217,7 +218,7 @@ mod tests {
assert_eq!(token.balance_of(&env.get_account(0)), U256::zero());

// Wait for the vote to end.
env.advance_block_time(60 * 11 * 1000);
env.advance_block_time(Duration::from_secs(60 * 11));

// Finish the vote.
token.tally();
Expand All @@ -238,7 +239,7 @@ mod tests {
env.set_caller(env.get_account(0));
token.vote(false, U256::from(1000));

env.advance_block_time(60 * 11 * 1000);
env.advance_block_time(Duration::from_secs(60 * 11));

token.tally();

Expand Down
16 changes: 16 additions & 0 deletions examples/src/contracts/owned_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ impl OwnedToken {
#[cfg(test)]
pub mod tests {
use super::*;
use odra::contract_def::HasIdent;
use odra::host::InstallConfig;
use odra::{
host::{Deployer, HostRef},
VmError
Expand Down Expand Up @@ -217,4 +219,18 @@ pub mod tests {
CallerNotTheOwner.into()
);
}
#[test]
fn module_name_names_the_package() {
// `#[odra::module(name = "MyTokenContact")]` above.
assert_eq!(OwnedToken::ident(), "OwnedToken");
assert_eq!(OwnedToken::contract_name(), "MyTokenContact");
assert_eq!(
InstallConfig::upgradable::<OwnedToken>().package_named_key,
"MyTokenContact"
);
assert_eq!(
InstallConfig::upgradable::<OwnedTokenHostRef>().package_named_key,
"MyTokenContact"
);
}
}
11 changes: 7 additions & 4 deletions examples/src/contracts/tlw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,18 @@ pub struct Withdrawal {
mod test {
use super::*;
use crate::contracts::tlw::{Deposit, Withdrawal};
use core::time::Duration;
use odra::host::{Deployer, HostRef};

const ONE_DAY_IN_SECONDS: u64 = 60 * 60 * 24;
const ONE_DAY: Duration = Duration::from_secs(60 * 60 * 24);

fn setup() -> (TimeLockWalletHostRef, Address, Address) {
let test_env = odra_test::env();
(
TimeLockWallet::deploy(
&test_env,
TimeLockWalletInitArgs {
lock_duration: ONE_DAY_IN_SECONDS
lock_duration: ONE_DAY.as_millis() as u64
}
),
test_env.get_account(0),
Expand Down Expand Up @@ -197,7 +198,7 @@ mod test {
contract.with_tokens(deposit_amount).deposit();

// When the user makes two token withdrawals after the lock is expired.
test_env.advance_block_time(ONE_DAY_IN_SECONDS + 1);
test_env.advance_block_time(ONE_DAY + Duration::from_millis(1));
let balance_before_withdrawals = test_env.balance_of(&user);
let first_withdrawal_amount: U512 = 50.into();
let second_withdrawal_amount: U512 = 40.into();
Expand Down Expand Up @@ -254,7 +255,9 @@ mod test {
contract.with_tokens(deposit.into()).deposit();

// When the user withdraws more tokens than has in the deposit, an error occurs.
contract.env().advance_block_time(ONE_DAY_IN_SECONDS + 1);
contract
.env()
.advance_block_time(ONE_DAY + Duration::from_millis(1));
let withdrawal = deposit + 1;
assert_eq!(
contract.try_withdraw(&withdrawal.into()).unwrap_err(),
Expand Down
3 changes: 2 additions & 1 deletion examples/src/features/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ impl PartyContract {
#[cfg(test)]
mod tests {
use super::{NativePartyStarted, PartyContract, PartyStarted};
use core::time::Duration;
use odra::host::{Deployer, NoArgs};

#[test]
Expand All @@ -79,7 +80,7 @@ mod tests {
assert!(test_env.emitted_native(&party_contract, "NativePartyStarted"));
assert_eq!(test_env.events_count(&party_contract), 1);
assert_eq!(test_env.native_events_count(&party_contract), 1);
test_env.advance_block_time(42);
test_env.advance_block_time(Duration::from_millis(42));
test_env.set_caller(test_env.get_account(1));
party_contract.emit();

Expand Down
Loading
Loading