Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: impl details of #[callback_unwrap] #1319

Closed
wants to merge 10 commits into from
92 changes: 38 additions & 54 deletions examples/adder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,45 @@
use near_sdk::near;
use near_sdk::{env, near, Promise};

#[derive(Debug, PartialEq, Eq)]
#[near(serializers=[borsh, json])]
pub struct Pair(u32, u32);

#[near(serializers=[borsh, json])]
pub struct DoublePair {
first: Pair,
second: Pair,
}

#[derive(Default)]
#[near(serializers=[borsh, json], contract_state)]
pub struct Adder {}

#[near]
impl Adder {
/// Call functions a, b, and c, d, asynchronously and handle results with `add_callback_vec`.
pub fn call_all() -> Promise {
Self::ext(env::current_account_id())
.a()
.and(Self::ext(env::current_account_id()).b())
.and(Self::ext(env::current_account_id()).c())
.and(Self::ext(env::current_account_id()).d())
.then(Self::ext(env::current_account_id()).add_callback_vec())
}

/// Adds two pairs point-wise.
pub fn add(&self, a: Pair, b: Pair) -> Pair {
sum_pair(&a, &b)
pub fn a(&self) -> Pair {
Pair(1, 1)
}

pub fn b(&self) -> Pair {
Pair(2, 3)
}

pub fn c(&self) -> Pair {
Pair(5, 8)
}

#[result_serializer(borsh)]
pub fn add_borsh(&self, #[serializer(borsh)] a: Pair, #[serializer(borsh)] b: Pair) -> Pair {
sum_pair(&a, &b)
pub fn d(&self) -> Pair {
Pair(13, 21)
}

pub fn add_callback(
&self,
#[callback_unwrap] a: DoublePair,
#[callback_unwrap] b: DoublePair,
#[callback_vec] others: Vec<DoublePair>,
) -> DoublePair {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[callback_vec] isn't really compatible with #[callback_unwrap] to be used in this manner, due to a feature??/bug??, because it revisits the same promise indices, already visited by callback_unwrap,
so others vector in this example contained a and b as first elements

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created #1320 to describe with an example + assertion of current behaviour, because it looks like it's not obvious to get it with cargo expand example

Some(b).iter().chain(others.iter()).fold(a, |acc, el| DoublePair {
first: sum_pair(&acc.first, &el.first),
second: sum_pair(&acc.second, &el.second),
})
pub fn add_callback_vec(&self, #[callback_vec] elements: Vec<Pair>) -> Pair {
let start = Pair(0, 0);
elements.iter().fold(start, |acc, el| sum_pair(&acc, &el))
}
}

Expand All @@ -44,45 +49,24 @@ fn sum_pair(a: &Pair, b: &Pair) -> Pair {

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use near_abi::*;
use near_sdk::serde_json;
use crate::Pair;

#[tokio::test]
async fn embedded_abi_test() -> anyhow::Result<()> {
async fn test_add_callback_vec() -> anyhow::Result<()> {
let wasm = near_workspaces::compile_project("./").await?;
let worker = near_workspaces::sandbox().await?;
let contract = worker.dev_deploy(&wasm).await?;

let res = contract.view("__contract_abi").await?;

let abi_root =
serde_json::from_slice::<AbiRoot>(&zstd::decode_all(&res.result[..])?)?;

assert_eq!(abi_root.schema_version, "0.4.0");
assert_eq!(abi_root.metadata.name, Some("adder".to_string()));
// assert_eq!(abi_root.metadata.version, Some("0.1.0".to_string()));
// assert_eq!(
// &abi_root.metadata.authors[..],
// &["Near Inc <hello@nearprotocol.com>"]
// );
// assert_eq!(abi_root.body.functions.len(), 3);

// let add_function = &abi_root.body.functions[0];
let res = contract
.call("call_all")
.args_json(())
.gas(near_sdk::Gas::from_tgas(300))
.transact()
.await?;
println!("res: {:#?}", res);

// assert_eq!(add_function.name, "add");
// assert_eq!(add_function.doc, Some(" Adds two pairs point-wise.".to_string()));
// assert_eq!(add_function.kind, AbiFunctionKind::View);
// assert_eq!(add_function.modifiers, &[]);
// match &add_function.params {
// AbiParameters::Json { args } => {
// assert_eq!(args.len(), 2);
// assert_eq!(args[0].name, "a");
// assert_eq!(args[1].name, "b");
// }
// AbiParameters::Borsh { .. } => {
// assert!(false);
// }
// }
let result = res.json::<Pair>()?;
assert_eq!(result, Pair(21, 33));

Ok(())
}
Expand Down
54 changes: 54 additions & 0 deletions examples/callback-results/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ impl Callback {
.then(Self::ext(env::current_account_id()).handle_callbacks())
}

/// Call functions a, b, and c asynchronously and handle results with `handle_callbacks`.
pub fn call_all_reverse(fail_b: bool, c_value: u8, d_value: u8) -> Promise {
Self::ext(env::current_account_id())
.b(fail_b)
.and(Self::ext(env::current_account_id()).c(c_value))
.and(Self::ext(env::current_account_id()).d(d_value))
.and(Self::ext(env::current_account_id()).a())
.then(Self::ext(env::current_account_id()).handle_callbacks_reverse())
}

/// Calls function c with a value that will always succeed
pub fn a() -> Promise {
Self::ext(env::current_account_id()).c(A_VALUE)
Expand Down Expand Up @@ -59,6 +69,23 @@ impl Callback {
}
(b.is_err(), c.is_err(), d.is_err())
}

/// Receives the callbacks from the other promises called.
/// used in `workspaces_test_reverse` to check the same result as in `workspaces_test`
/// with respect to `callback_unwrap`/`callback_result` attributes' order in args
#[private]
pub fn handle_callbacks_reverse(
#[callback_result] b: Result<String, PromiseError>,
#[callback_result] c: Result<u8, PromiseError>,
#[callback_result] d: Result<(), PromiseError>,
#[callback_unwrap] a: u8,
) -> (bool, bool, bool) {
require!(a == A_VALUE, "Promise returned incorrect value");
if let Ok(s) = b.as_ref() {
require!(s == "Some string");
}
(b.is_err(), c.is_err(), d.is_err())
}
}

#[cfg(all(test, not(target_arch = "wasm32")))]
Expand Down Expand Up @@ -120,4 +147,31 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn workspaces_test_reverse() -> anyhow::Result<()> {
let wasm = near_workspaces::compile_project("./").await?;
let worker = near_workspaces::sandbox().await?;
let contract = worker.dev_deploy(&wasm).await?;

// No failures
let res = contract
.call("call_all_reverse")
.args_json((false, 1u8, 1u8))
.max_gas()
.transact()
.await?;
assert_eq!(res.json::<(bool, bool, bool)>()?, (false, false, false));

// Fail all
let res = contract
.call("call_all_reverse")
.args_json((true, 0u8, 0u8))
.gas(near_sdk::Gas::from_tgas(300))
.transact()
.await?;
assert_eq!(res.json::<(bool, bool, bool)>()?, (true, true, true));

Ok(())
}
}
16 changes: 14 additions & 2 deletions near-sdk/src/environment/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,7 @@ pub fn promise_batch_action_delete_account(
/// assert_eq!(promise_results_count(), 0);
/// ```
/// More low-level info here: [`near_vm_runner::logic::VMLogic::promise_results_count`]
///
/// See example of usage [here](https://github.com/near/near-sdk-rs/blob/master/examples/cross-contract-calls/low-level/src/lib.rs)
pub fn promise_results_count() -> u64 {
unsafe { sys::promise_results_count() }
Expand Down Expand Up @@ -1430,8 +1431,14 @@ pub fn promise_results_count() -> u64 {
/// }
/// };
/// ```
///
/// More low-level info here: [`near_vm_runner::logic::VMLogic::promise_result`]
/// Example usages: [one](https://github.com/near/near-sdk-rs/blob/189897180649bce47aefa4e5af03664ee525508d/near-contract-standards/src/fungible_token/core_impl.rs#L178), [two](https://github.com/near/near-sdk-rs/blob/189897180649bce47aefa4e5af03664ee525508d/near-contract-standards/src/non_fungible_token/core/core_impl.rs#L433), [three](https://github.com/near/near-sdk-rs/blob/189897180649bce47aefa4e5af03664ee525508d/examples/factory-contract/low-level/src/lib.rs#L61), [four](https://github.com/near/near-sdk-rs/blob/189897180649bce47aefa4e5af03664ee525508d/examples/cross-contract-calls/low-level/src/lib.rs#L46)
///
/// Example usages:
/// - [near-contract-standards/src/fungible_token](https://github.com/near/near-sdk-rs/blob/189897180649bce47aefa4e5af03664ee525508d/near-contract-standards/src/fungible_token/core_impl.rs#L178)
/// - [near-contract-standards/src/non_fungible_token](https://github.com/near/near-sdk-rs/blob/189897180649bce47aefa4e5af03664ee525508d/near-contract-standards/src/non_fungible_token/core/core_impl.rs#L433)
/// - [examples/factory-contract/low-level](https://github.com/near/near-sdk-rs/blob/189897180649bce47aefa4e5af03664ee525508d/examples/factory-contract/low-level/src/lib.rs#L61)
/// - [examples/cross-contract-calls/low-level](https://github.com/near/near-sdk-rs/blob/189897180649bce47aefa4e5af03664ee525508d/examples/cross-contract-calls/low-level/src/lib.rs#L46)
pub fn promise_result(result_idx: u64) -> PromiseResult {
match promise_result_internal(result_idx) {
Ok(()) => {
Expand Down Expand Up @@ -1806,8 +1813,10 @@ pub fn storage_write(key: &[u8], value: &[u8]) -> bool {
/// Reads the value stored under the given key.
///
/// # Use cases
///
/// Storage functions are typically used to upgrade/migrate a contract state, preventing errors like `Cannot deserialize the contract state` after rolling out the breaking changes to the network.
/// For practical examples, see different implementations in [this repository](https://github.com/near-examples/update-migrate-rust).
///
/// For practical examples, see different implementations in [`near-examples/update-migrate-rust` repo](https://github.com/near-examples/update-migrate-rust).
///
/// # Examples
/// ```
Expand All @@ -1817,6 +1826,9 @@ pub fn storage_write(key: &[u8], value: &[u8]) -> bool {
/// storage_write(b"key", b"value");
/// assert_eq!(storage_read(b"key").unwrap(), b"value");
/// ```
///
/// Another example:
/// - [near-contract-standards/src/upgrade](https://github.com/near/near-sdk-rs/blob/746e4280a7e25b2036bd4e2f2c186cd76e1a7cde/near-contract-standards/src/upgrade/mod.rs?plain=1#L77)
pub fn storage_read(key: &[u8]) -> Option<Vec<u8>> {
match unsafe { sys::storage_read(key.len() as _, key.as_ptr() as _, ATOMIC_OP_REGISTER) } {
0 => None,
Expand Down
45 changes: 41 additions & 4 deletions near-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,11 +447,48 @@ extern crate quickcheck;
///
/// ### Other examples within repo:
///
/// - `Cross-Contract Factorial` again [examples/cross-contract-calls](https://github.com/near/near-sdk-rs/blob/9596835369467cac6198e8de9a4b72a38deee4a5/examples/cross-contract-calls/high-level/src/lib.rs?plain=1#L26)
/// - `Cross-Contract Factorial` again [examples/cross-contract-calls](https://github.com/near/near-sdk-rs/tree/master/examples/cross-contract-calls/high-level/src/lib.rs?plain=1#L26)
/// - same example as [above](near#example-with-cross-contract-factorial), but uses [`Promise::then`] instead of [`env`](mod@env) host functions calls to set up a callback of `factorial_mult`
/// - [examples/adder](https://github.com/near/near-sdk-rs/blob/9596835369467cac6198e8de9a4b72a38deee4a5/examples/adder/src/lib.rs?plain=1#L30)
/// - [examples/adder](https://github.com/near/near-sdk-rs/blob/9596835369467cac6198e8de9a4b72a38deee4a5/examples/adder/src/lib.rs?plain=1#L31)
/// - [examples/callback-results](https://github.com/near/near-sdk-rs/blob/9596835369467cac6198e8de9a4b72a38deee4a5/examples/callback-results/src/lib.rs?plain=1#L51)
/// - [examples/callback-results](https://github.com/near/near-sdk-rs/tree/master/examples/callback-results/src/lib.rs?plain=1#L51)
///
/// ### Implementation details of `#[callback_unwrap]` macro and **host functions** calls used
///
/// ```rust
/// # use near_sdk::near;
/// # #[near(contract_state)]
/// # pub struct Contract { /* .. */ }
/// #[near]
/// impl Contract {
/// pub fn method(
/// &mut self,
/// regular: String,
/// #[callback_unwrap] one: String,
/// #[callback_unwrap] two: String
/// ) { /* .. */ }
/// }
/// ```
///
/// For above `method` using the attribute on arguments, changes the body of function generated in [`#[near]` on mutating method](near#for-above-mutating-method-near-macro-defines-the-following-function)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pr depends on #1307, as the doc anchor is first added there.
the broken link is not detected by

        RUSTDOCFLAGS: -D warnings
      run: |
        cargo doc -p near-sdk --features unstable,legacy,unit-testing,__macro-docs,__abi-generate

rust-lang/rust#137740

///
/// ```rust,no_run
/// #[no_mangle]
/// pub extern "C" fn method() { /* .. */ }
/// ```
///
/// in the following way:
///
/// 1. arguments, annotated with `#[callback_unwrap]`, are no longer expected to be included into `input`,
/// deserialized in (step **3**, [`#[near]` on mutating method](near#for-above-mutating-method-near-macro-defines-the-following-function)).
/// 2. for each argument, annotated with `#[callback_unwrap]`:
/// 1. [`env::promise_result`] host function is called with corresponding index, starting from 0
/// (`0u64` for argument `one`, `1u64` for argument `two` above), and saved into `promise_result` variable
/// 2. if the `promise_result` is a [`PromiseResult::Failed`] error, then [`env::panic_str`] host function is called to signal callback computation error
/// 3. otherwise, if the `promise_result` is a [`PromiseResult::Successful`], it's unwrapped and saved to a `data` variable
/// 4. `data` is deserialized similar to that as usual (step **3**, [`#[near]` on mutating method](near#for-above-mutating-method-near-macro-defines-the-following-function)),
/// and saved to `deserialized_n_promise` variable
/// 3. counterpart of (step **7**, [`#[near]` on mutating method](near#for-above-mutating-method-near-macro-defines-the-following-function)):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make it more compact:
2.1

[`env::promise_result`] host function is called with corresponding index

2.2

if the `promise_result` is a [`PromiseResult::Failed`] error, then [`env::panic_str`] is called

2.3 can be removed

2.4 otherwise, promise_result is deserialized ...

But your variant is also lgtm

/// original method is called `Contract::method(&mut state, deserialized_input_success.regular, deserialized_0_promise, deserialized_1_promise)`,
/// as defined in `#[near]` annotated impl block
///
/// ## `#[near(event_json(...))]` (annotates enums)
///
Expand Down
22 changes: 16 additions & 6 deletions near-sdk/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,28 @@
}

/// Returns true if promise was successful.
/// Fails if called outside a callback that received 1 promise result.
/// Uses low-level [`crate::env::promise_results_count`].
///
/// Calls [`crate::env::panic_str`] **host function** if called outside a callback that received precisely 1 promise result.
///
/// Uses low-level [`crate::env::promise_results_count`] and [`crate::env::promise_result`] **host functions**.
pub fn is_promise_success() -> bool {
require!(env::promise_results_count() == 1, "Contract expected a result on the callback");
require!(
env::promise_results_count() == 1,

Check warning on line 98 in near-sdk/src/utils/mod.rs

View check run for this annotation

Codecov / codecov/patch

near-sdk/src/utils/mod.rs#L97-L98

Added lines #L97 - L98 were not covered by tests
"Contract expected a single result on the callback"
);
env::promise_result_internal(0).is_ok()
}

/// Returns the result of the promise if successful. Otherwise returns None.
/// Fails if called outside a callback that received 1 promise result.
/// Uses low-level [`crate::env::promise_results_count`] and [`crate::env::promise_result`].
///
/// Calls [`crate::env::panic_str`] **host function** if called outside a callback that received precisely 1 promise result.
///
/// Uses low-level [`crate::env::promise_results_count`] and [`crate::env::promise_result`] **host functions**.
pub fn promise_result_as_success() -> Option<Vec<u8>> {
require!(env::promise_results_count() == 1, "Contract expected a result on the callback");
require!(
env::promise_results_count() == 1,

Check warning on line 111 in near-sdk/src/utils/mod.rs

View check run for this annotation

Codecov / codecov/patch

near-sdk/src/utils/mod.rs#L110-L111

Added lines #L110 - L111 were not covered by tests
"Contract expected a single result on the callback"
);
match env::promise_result(0) {
PromiseResult::Successful(result) => Some(result),
_ => None,
Expand Down
Loading