-
Notifications
You must be signed in to change notification settings - Fork 20
Escrow proxy #191
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
base: main
Are you sure you want to change the base?
Escrow proxy #191
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@tests/src/tests/escrow_proxy/config.rs`:
- Around line 36-60: The test owner_configuration currently deploys the proxy
but lacks any verification; after calling
proxy_account.deploy_escrow_proxy(config) in the owner_configuration test, add a
view/assertion that reads the deployed contract's owner (use the contract's
public view or state accessor related to owner, e.g., the contract's owner
getter) and assert it equals owner.id().clone() (compare against
ProxyConfig.owner_id that was passed); update the owner_configuration test to
call that view on proxy_account and assert equality to ensure the owner was set
correctly.
In `@tests/src/tests/escrow/partial_fills.rs`:
- Around line 376-377: Replace the real-time sleep with the sandbox time jump so
the blockchain timestamp advances: in the test that currently calls
tokio::time::sleep(Duration::from_secs(7)).await, call sandbox.fast_forward(7)
(matching the 7-second timeout) instead so the deadline check in es_close and
other deadline-dependent logic uses the advanced chain time; locate the
invocation near the deadline wait in the partial_fills test and swap the sleep
call for sandbox.fast_forward(7).
In `@tests/src/utils/escrow_builders.rs`:
- Around line 192-202: The build method for FillMessageBuilder currently sets
deadline to DEFAULT_DEADLINE_SECS when self.deadline is None, ignoring
params.deadline; update the deadline expression in
TransferAction::Fill(FillAction { ... }) inside the build() method to use
self.deadline.unwrap_or(self.params.deadline) (or unwrap_or_else with cloning if
needed) so FillAction.deadline inherits params.deadline unless with_deadline()
overrides it, keeping price behavior unchanged.
🧹 Nitpick comments (5)
escrow-proxy/examples/escrow-swap-demo.rs (1)
235-236: Clarify NEP-141 vs NEP-245 token ID intent.
The constants are labeled NEP-245 but parsed asNep141TokenId. If these are NEP-245 tokens, consider usingNep245TokenId; otherwise rename the constants to avoid confusion.tests/src/tests/escrow/partial_fills.rs (1)
275-280: Consider moving imports to the top of the file.Placing
usestatements inside the function body is valid Rust, but unconventional. Moving these imports to the file-level (alongside lines 1-27) improves readability and consistency with the existing test pattern in this file.tests/src/tests/escrow_with_proxy/swap_with_fees.rs (1)
192-209: Assertions are weak for a comprehensive fee test.The assertions only verify balances are
> 0, but for a test exercising protocol fees (1%), surplus fees (50%), and integrator fees (2%), verifying exact expected amounts after fee deductions would increase confidence in the fee calculation logic. Consider computing and asserting the expected amounts:
- Proxy should receive:
swap_amount - (protocol_fee + surplus_fee + integrator_fee)- Maker should receive:
solver_amount(or amount after any taker-side fees)💡 Example of stronger assertions
// Calculate expected amounts after fees let protocol_fee = swap_amount * 1 / 100; // 1% let integrator_fee = swap_amount * 2 / 100; // 2% // surplus calculation depends on price difference logic let expected_proxy_src = swap_amount - protocol_fee - integrator_fee - surplus_portion; assert_eq!( env.defuse.mt_balance_of(proxy.id(), &token_a_defuse_id.to_string()).await.unwrap(), expected_proxy_src, "Proxy should receive expected src after fees" );escrow-proxy/src/state.rs (1)
3-10: Consider adding documentation for the public config struct.
ProxyConfigis publicly exported and used across multiple crates (tests, examples). Adding doc comments would clarify the purpose of each field, especiallynotifiervsauth_contractwhich may not be immediately obvious to users.📝 Suggested documentation
+/// Configuration for the escrow proxy contract. #[near(serializers = [borsh, json])] #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProxyConfig { + /// Account authorized to perform administrative operations. pub owner_id: AccountId, + /// Global contract ID for oneshot-condvar instances. pub oneshot_condvar_global_id: GlobalContractId, + /// Contract that verifies authorization (e.g., defuse). pub auth_contract: AccountId, + /// Account that triggers condvar notifications. pub notifier: AccountId, }sandbox/src/lib.rs (1)
122-136: Consider documenting the unwrap invariant on line 136.The
.unwrap()on line 136 assumes theOption<Sandbox>inside the mutex is alwaysSome. This invariant holds because:
- The sandbox is only initialized once (via
get_or_init)- The only place that calls
take()iscleanup_sandbox, which runs at program exitWhile this is safe in practice, a brief comment would clarify the invariant for future maintainers.
📝 Suggested documentation
let (sandbox_arc, root_account) = mutex .lock() .unwrap() .as_ref() + // SAFETY: The Option is always Some until atexit cleanup runs .map(|shared| (shared.sandbox.clone(), shared.root.clone())) .unwrap();
| #[tokio::test] | ||
| async fn owner_configuration() { | ||
| let sandbox = Sandbox::new("test".parse::<AccountId>().unwrap()).await; | ||
| let root = sandbox.root(); | ||
|
|
||
| let (owner, proxy_account) = futures::try_join!( | ||
| root.generate_subaccount("owner", INIT_BALANCE), | ||
| root.generate_subaccount("proxy", INIT_BALANCE), | ||
| ) | ||
| .unwrap(); | ||
|
|
||
| let config = ProxyConfig { | ||
| owner_id: owner.id().clone(), | ||
| oneshot_condvar_global_id: GlobalContractId::AccountId( | ||
| root.sub_account("oneshot_condvar_global_id") | ||
| .unwrap() | ||
| .id() | ||
| .clone(), | ||
| ), | ||
| auth_contract: root.sub_account("auth_contract").unwrap().id().clone(), | ||
| notifier: root.sub_account("notifier").unwrap().id().clone(), | ||
| }; | ||
|
|
||
| proxy_account.deploy_escrow_proxy(config).await.unwrap(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test is incomplete - missing assertion.
The owner_configuration test deploys the proxy with a different owner but doesn't verify anything after deployment. Consider adding an assertion to confirm the owner was set correctly, similar to the first test:
💡 Proposed fix to add verification
proxy_account.deploy_escrow_proxy(config).await.unwrap();
+ let actual_config = proxy_account.get_escrow_proxy_config().await.unwrap();
+ assert_eq!(actual_config.owner_id, owner.id().clone(), "owner should match");
}🤖 Prompt for AI Agents
In `@tests/src/tests/escrow_proxy/config.rs` around lines 36 - 60, The test
owner_configuration currently deploys the proxy but lacks any verification;
after calling proxy_account.deploy_escrow_proxy(config) in the
owner_configuration test, add a view/assertion that reads the deployed
contract's owner (use the contract's public view or state accessor related to
owner, e.g., the contract's owner getter) and assert it equals
owner.id().clone() (compare against ProxyConfig.owner_id that was passed);
update the owner_configuration test to call that view on proxy_account and
assert equality to ensure the owner was set correctly.
| // Wait for deadline to expire | ||
| tokio::time::sleep(Duration::from_secs(7)).await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if sandbox provides time advancement utilities
rg -n "fast.?forward|advance.?time|block.?time" --type=rust -C 3Repository: near/intents
Length of output: 6736
🏁 Script executed:
# Also check the TODO on line 208 in the same test file
sed -n '200,215p' tests/src/tests/escrow/partial_fills.rsRepository: near/intents
Length of output: 394
🏁 Script executed:
# Get context around lines 376-377 to understand the deadline test
sed -n '360,390p' tests/src/tests/escrow/partial_fills.rsRepository: near/intents
Length of output: 1090
🏁 Script executed:
# Search for how other tests handle time in this file
rg -n "sleep|block_timestamp|now|clock" tests/src/tests/escrow/partial_fills.rs --type=rustRepository: near/intents
Length of output: 159
Replace tokio::time::sleep with sandbox.fast_forward() for deadline testing.
The sandbox provides fast_forward() for advancing blockchain time. Using tokio::time::sleep here doesn't advance the blockchain timestamp, causing the deadline check in es_close to fail or behave unexpectedly. Use sandbox.fast_forward(7) instead (matching the 7-second timeout), as shown in other deadline-dependent tests throughout the codebase.
🤖 Prompt for AI Agents
In `@tests/src/tests/escrow/partial_fills.rs` around lines 376 - 377, Replace the
real-time sleep with the sandbox time jump so the blockchain timestamp advances:
in the test that currently calls
tokio::time::sleep(Duration::from_secs(7)).await, call sandbox.fast_forward(7)
(matching the 7-second timeout) instead so the deadline check in es_close and
other deadline-dependent logic uses the advanced chain time; locate the
invocation near the deadline wait in the partial_fills test and swap the sleep
call for sandbox.fast_forward(7).
| #[must_use] | ||
| pub fn build(self) -> TransferMessage { | ||
| TransferMessage { | ||
| params: self.params.clone(), | ||
| action: TransferAction::Fill(FillAction { | ||
| price: self.price.unwrap_or(self.params.price), | ||
| deadline: self.deadline.unwrap_or_else(|| { | ||
| Deadline::timeout(std::time::Duration::from_secs(DEFAULT_DEADLINE_SECS)) | ||
| }), | ||
| receive_src_to: self.receive_src_to.unwrap_or_default(), | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cd tests && find . -name "escrow_builders.rs" | head -5Repository: near/intents
Length of output: 86
🏁 Script executed:
wc -l tests/src/utils/escrow_builders.rsRepository: near/intents
Length of output: 94
🏁 Script executed:
head -250 tests/src/utils/escrow_builders.rs | tail -100Repository: near/intents
Length of output: 2855
🏁 Script executed:
# Check if Deadline is Copy/Clone and what it contains
rg "struct Deadline|pub deadline:" tests/src/utils/escrow_builders.rs -A 3Repository: near/intents
Length of output: 38
🏁 Script executed:
# Search for Deadline type definition in the codebase
fd -e rs -e toml | xargs rg "struct Deadline" | head -20Repository: near/intents
Length of output: 96
🏁 Script executed:
# Check how FillMessageBuilder is used in tests to understand intent
rg "FillMessageBuilder" tests/src/utils/escrow_builders.rs -A 2 -B 2Repository: near/intents
Length of output: 278
🏁 Script executed:
# Get the Deadline struct definition
cat deadline/src/lib.rs | head -50Repository: near/intents
Length of output: 1138
🏁 Script executed:
# Check Params struct definition to see the deadline field type
rg "struct Params" tests/src/utils/escrow_builders.rs -A 15Repository: near/intents
Length of output: 523
🏁 Script executed:
# Look for any tests that use FillMessageBuilder
rg -r "FillMessageBuilder" tests/ --type rs | head -20Repository: near/intents
Length of output: 82
🏁 Script executed:
# Search for FillMessageBuilder usage in tests
rg "FillMessageBuilder" tests/ -A 3 -B 1Repository: near/intents
Length of output: 5750
🏁 Script executed:
# Look at the Params struct definition
rg "pub struct Params" tests/src/utils/escrow_builders.rs -A 15Repository: near/intents
Length of output: 523
🏁 Script executed:
# Check if there are any cases where FillMessageBuilder is built without with_deadline()
rg "FillMessageBuilder::new" tests/ -A 5 | grep -v "with_deadline"Repository: near/intents
Length of output: 2561
🏁 Script executed:
# Get the full Params struct definition to see all fields
rg "^pub struct Params" tests/src/utils/escrow_builders.rs -A 20Repository: near/intents
Length of output: 703
🏁 Script executed:
# Check if Deadline is Copy
rg "^#\[derive.*Copy" deadline/src/lib.rs -B 2Repository: near/intents
Length of output: 138
🏁 Script executed:
# Look at one of the cases without with_deadline to understand the scenario
cat -n tests/src/tests/escrow/fees.rs | sed -n '1,100p' | grep -A 10 "FillMessageBuilder::new"Repository: near/intents
Length of output: 444
🏁 Script executed:
# Get more context on that test
rg "fn test.*fee" tests/src/tests/escrow/fees.rs | head -5Repository: near/intents
Length of output: 229
🏁 Script executed:
# Check the actual Params struct definition to see deadline field type
rg "^pub struct Params" tests/src/utils/escrow_builders.rs -A 30 | head -40Repository: near/intents
Length of output: 984
🏁 Script executed:
# Search for the actual Params struct definition
rg "^pub struct Params\s*\{" tests/src/utils/escrow_builders.rs -A 20Repository: near/intents
Length of output: 38
🏁 Script executed:
# Let me also check around line 20-50 to find Params definition
sed -n '1,100p' tests/src/utils/escrow_builders.rs | cat -nRepository: near/intents
Length of output: 3588
🏁 Script executed:
# Search for Params struct definition in the repository
fd -name "*.rs" | xargs rg "^pub struct Params\s*\{" -A 20 | head -50Repository: near/intents
Length of output: 1460
🏁 Script executed:
# Look at ParamsBuilder.build() implementation
sed -n '105,180p' tests/src/utils/escrow_builders.rs | cat -nRepository: near/intents
Length of output: 2647
🏁 Script executed:
# Check the exact lines 192-202 from the file
sed -n '192,202p' tests/src/utils/escrow_builders.rs | cat -nRepository: near/intents
Length of output: 639
FillMessageBuilder should inherit deadline from Params instead of creating a new default.
When ParamsBuilder sets a custom deadline, FillMessageBuilder currently ignores it and falls back to DEFAULT_DEADLINE_SECS. To be consistent with other fields (price), default to params.deadline unless explicitly overridden via with_deadline().
🛠️ Suggested fix
- deadline: self.deadline.unwrap_or_else(|| {
- Deadline::timeout(std::time::Duration::from_secs(DEFAULT_DEADLINE_SECS))
- }),
+ deadline: self.deadline.unwrap_or(self.params.deadline),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[must_use] | |
| pub fn build(self) -> TransferMessage { | |
| TransferMessage { | |
| params: self.params.clone(), | |
| action: TransferAction::Fill(FillAction { | |
| price: self.price.unwrap_or(self.params.price), | |
| deadline: self.deadline.unwrap_or_else(|| { | |
| Deadline::timeout(std::time::Duration::from_secs(DEFAULT_DEADLINE_SECS)) | |
| }), | |
| receive_src_to: self.receive_src_to.unwrap_or_default(), | |
| }), | |
| #[must_use] | |
| pub fn build(self) -> TransferMessage { | |
| TransferMessage { | |
| params: self.params.clone(), | |
| action: TransferAction::Fill(FillAction { | |
| price: self.price.unwrap_or(self.params.price), | |
| deadline: self.deadline.unwrap_or(self.params.deadline), | |
| receive_src_to: self.receive_src_to.unwrap_or_default(), | |
| }), |
🤖 Prompt for AI Agents
In `@tests/src/utils/escrow_builders.rs` around lines 192 - 202, The build method
for FillMessageBuilder currently sets deadline to DEFAULT_DEADLINE_SECS when
self.deadline is None, ignoring params.deadline; update the deadline expression
in TransferAction::Fill(FillAction { ... }) inside the build() method to use
self.deadline.unwrap_or(self.params.deadline) (or unwrap_or_else with cloning if
needed) so FillAction.deadline inherits params.deadline unless with_deadline()
overrides it, keeping price behavior unchanged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (1)
351-351:⚠️ Potential issue | 🟡 MinorTypo in test function name: "desposit" → "deposit".
✏️ Proposed fix
-async fn mt_desposit_resolve_can_handle_large_blob_value_returned_from_notification() { +async fn mt_deposit_resolve_can_handle_large_blob_value_returned_from_notification() {
🤖 Fix all issues with AI agents
In `@escrow-proxy/src/contract/tokens/nep245.rs`:
- Around line 7-9: MT_CHECK_AND_FORWARD_GAS is currently composed using
undefined FT_* constants; update the expression to use the MT_* constants
defined in this module (i.e., compute MT_CHECK_AND_FORWARD_GAS as
MT_RESOLVE_TRANSFER_GAS + MT_TRANSFER_CALL_GAS) so the symbol names match and
the code compiles; locate the constant MT_CHECK_AND_FORWARD_GAS and replace
references to FT_RESOLVE_TRANSFER_GAS and FT_TRANSFER_CALL_GAS with
MT_RESOLVE_TRANSFER_GAS and MT_TRANSFER_CALL_GAS respectively.
- Around line 65-67: The callback that checks bounded_promise_result::<bool>(0)
should not panic on authorization failure; instead, when unwrap_or(false) is
false return the full amounts vector so the token contract receives the refund
instructions. Replace the panic_str branch in the callback (the function that
calls bounded_promise_result::<bool>(0) and currently references amounts) with
an explicit return of amounts (or amounts converted to the expected NEP-245
return type), ensure the function signature/return type matches NEP-245 (e.g.,
Vec<U128> or Vec<Balance> as used elsewhere), and make the identical change in
nep141.rs to mirror behavior for FT callbacks.
In `@Makefile.toml`:
- Around line 142-157: In the Makefile.toml task [tasks.build-oneshot-condvar],
replace the args entry "--no-abi" with "--no-embed-abi" so the oneshot-condvar
build (which enables the abi feature) generates ABI artifacts without embedding
them in the WASM; keep the rest of the cargo args (near build
non-reproducible-wasm --locked --manifest-path ./oneshot-condvar/Cargo.toml
--features abi,contract,auth-call --out-dir ${ONESHOT_CONDVAR_DIR}) unchanged.
In `@oneshot-condvar/README.md`:
- Around line 73-97: Headings in the README incorrectly use cv_notify() instead
of the API's cv_notify_one(); update the section headings and any inline
occurrences that read "cv_notify()" to "cv_notify_one()" (e.g., the Path 1 and
Path 3 headings and their sequence diagrams) so the documentation matches the
function name cv_notify_one().
In `@tests/contracts/multi-token-receiver-stub/src/lib.rs`:
- Around line 81-82: The test stub currently uses
serde_json::from_str(&msg).unwrap_or_default() which silently maps parse errors
to MTReceiverMode::AcceptAll; change the parsing in the MTReceiverMode branch to
explicitly handle Err by panicking on malformed JSON but only treat an
empty/whitespace message as Default::default(), e.g. match
serde_json::from_str(&msg) { Ok(mode) => mode, Err(e) => if
msg.trim().is_empty() { Default::default() } else { panic!("Failed to parse
MTReceiverMode: {}", e) } }; apply the same strict parsing change to the
FTReceiverMode parsing so malformed FT messages also cause a panic while empty
messages still default.
In `@tests/src/tests/mod.rs`:
- Around line 7-9: The tests/src/tests/mod.rs file declares pub mod utils; but
utils imports crate::extensions::defuse which is feature-gated; add the same
feature gate by placing #[cfg(feature = "defuse")] immediately above the pub mod
utils; declaration (matching the existing gates used for defuse and escrow) so
the utils module is compiled only when the "defuse" feature is enabled.
🧹 Nitpick comments (6)
tests/src/tests/escrow/mod.rs (1)
1-1: Consider narrowing theallowscope to specific modules or items.
#![allow(async_fn_in_trait, dead_code)]at the module root hides warnings across all escrow tests; scoping it to the specific modules or items that need it keeps signal high.tests/Cargo.toml (1)
26-35: New dependencies align with PR objectives.The additions for
defuse-escrow-proxy,defuse-oneshot-condvar, and supporting crates (defuse-core,defuse-nep245,defuse-token-id) are appropriate for testing the new contracts introduced in this PR. Thenear-sdkfeature additions forunit-testinganddeterministic-account-idsare suitable for sandbox testing.One observation:
defuse-escrow-swapis gated behind theescrowfeature (line 17), butdefuse-escrow-proxyanddefuse-oneshot-condvarare unconditionally included. If these crates are only used when testing escrow functionality, consider making them optional as well for consistency and to reduce compile times when theescrowfeature is disabled.♻️ Optional: Gate escrow-related dependencies behind the `escrow` feature
-escrow = ["dep:defuse-escrow-swap"] +escrow = ["dep:defuse-escrow-swap", "dep:defuse-escrow-proxy", "dep:defuse-oneshot-condvar"]-defuse-escrow-proxy = { workspace = true, features = ["escrow-swap"] } +defuse-escrow-proxy = { workspace = true, features = ["escrow-swap"], optional = true }-defuse-oneshot-condvar.workspace = true +defuse-oneshot-condvar = { workspace = true, optional = true }tests/src/tests/escrow_with_proxy/swap_with_fees.rs (3)
128-142: Consider naming the salt constant for clarity.The magic number
[2u8; 32]on line 130 is used as a salt. For test readability and maintainability, consider extracting this to a named constant with a brief comment explaining its purpose.♻️ Suggested refactor
+ // Arbitrary salt for proxy message - distinct from other test salts + const PROXY_SALT: [u8; 32] = [2u8; 32]; + let proxy_msg = ProxyTransferMessage { receiver_id: escrow_instance_id.clone(), - salt: [2u8; 32], + salt: PROXY_SALT, msg: serde_json::to_string(&fill_escrow_msg).unwrap(), };
192-210: Strengthen assertions to verify fee distribution and expected amounts.The current assertions only check
> 0balances for proxy and maker, but don't verify:
- Fee collector received protocol and surplus fees
- Integrator received their 2% fee
- Expected amounts after fee deductions are correct
For a worst-case fee scenario test, verifying the fee distribution would confirm the full flow executed as intended and catch potential fee calculation bugs.
♻️ Suggested additional assertions
// Verify maker received dst tokens (minus fees) assert!( env.defuse .mt_balance_of(maker.id(), &token_b_defuse_id.to_string()) .await .unwrap() > 0, "Maker should have received token-b" ); + + // Verify fee collector received fees (protocol + surplus) + assert!( + env.defuse + .mt_balance_of(fee_collector.id(), &token_b_defuse_id.to_string()) + .await + .unwrap() + > 0, + "Fee collector should have received protocol and surplus fees" + ); + + // Verify integrator received their fee + assert!( + env.defuse + .mt_balance_of(integrator.id(), &token_b_defuse_id.to_string()) + .await + .unwrap() + > 0, + "Integrator should have received integrator fee" + );
188-190: Consider adding a gas upper bound assertion to detect regressions.Currently, the test only prints the gas consumption without asserting any bounds. For a benchmark test, adding an upper bound assertion would help catch gas regressions in CI.
♻️ Suggested addition
let total_gas = Gas::from_gas(result.total_gas_burnt.as_gas()); eprintln!("Proxy fill total gas consumed (worst case with fees): {total_gas:?}"); + + // Assert gas is within expected bounds to detect regressions + // Adjust this threshold based on baseline measurements + const MAX_EXPECTED_GAS: Gas = Gas::from_tgas(300); + assert!( + total_gas <= MAX_EXPECTED_GAS, + "Gas consumption {total_gas:?} exceeds expected maximum {MAX_EXPECTED_GAS:?}" + );tests/src/env/mod.rs (1)
157-182: Different storage deposit strategy between FT and MT methods is intentional but worth documenting.The FT method does storage deposits for all user accounts, while this MT method only deposits for
defuseandroot. This is correct since MT tokens flow through the defuse contract, but a brief doc comment explaining the difference would help future maintainers.+ /// Creates an MT token and deposits specified balances to each user via defuse. + /// + /// Unlike `create_ft_token_with_initial_balances`, this only requires storage deposits + /// for defuse and root since MT tokens are deposited through the defuse contract. pub async fn create_mt_token_with_initial_balances(
| [tasks.build-oneshot-condvar] | ||
| dependencies = ["add-cache-dir-tag"] | ||
| command = "cargo" | ||
| args = [ | ||
| "near", | ||
| "build", | ||
| "non-reproducible-wasm", | ||
| "--locked", | ||
| "--manifest-path", | ||
| "./oneshot-condvar/Cargo.toml", | ||
| "--features", | ||
| "abi,contract,auth-call", | ||
| "--out-dir", | ||
| "${ONESHOT_CONDVAR_DIR}", | ||
| "--no-abi", | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cd / && find . -name "Makefile.toml" -type f 2>/dev/null | head -5Repository: near/intents
Length of output: 89
🏁 Script executed:
cat -n Makefile.toml | sed -n '100,200p'Repository: near/intents
Length of output: 2976
🏁 Script executed:
rg "oneshot-condvar" --type-list | head -5Repository: near/intents
Length of output: 445
🏁 Script executed:
rg "oneshot-condvar" -A 3 -B 3Repository: near/intents
Length of output: 10108
🏁 Script executed:
rg "\-\-no-abi|\-\-no-embed-abi" --type-listRepository: near/intents
Length of output: 445
🏁 Script executed:
rg "\-\-no-abi|\-\-no-embed-abi"Repository: near/intents
Length of output: 554
🏁 Script executed:
cat -n oneshot-condvar/Cargo.toml | grep -A 20 "cargo-near"Repository: near/intents
Length of output: 828
Switch --no-abi to --no-embed-abi to match other build tasks and generate ABI artifacts.
The oneshot-condvar contract enables the abi feature (line 153) and is used as a dependency by escrow-proxy's abi feature flag. Using --no-abi skips ABI generation entirely, whereas --no-embed-abi (used by the contract's own Cargo.toml and all other build tasks) generates ABI without embedding it in the WASM binary.
🔧 Required fix
- "--no-abi",
+ "--no-embed-abi",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [tasks.build-oneshot-condvar] | |
| dependencies = ["add-cache-dir-tag"] | |
| command = "cargo" | |
| args = [ | |
| "near", | |
| "build", | |
| "non-reproducible-wasm", | |
| "--locked", | |
| "--manifest-path", | |
| "./oneshot-condvar/Cargo.toml", | |
| "--features", | |
| "abi,contract,auth-call", | |
| "--out-dir", | |
| "${ONESHOT_CONDVAR_DIR}", | |
| "--no-abi", | |
| ] | |
| [tasks.build-oneshot-condvar] | |
| dependencies = ["add-cache-dir-tag"] | |
| command = "cargo" | |
| args = [ | |
| "near", | |
| "build", | |
| "non-reproducible-wasm", | |
| "--locked", | |
| "--manifest-path", | |
| "./oneshot-condvar/Cargo.toml", | |
| "--features", | |
| "abi,contract,auth-call", | |
| "--out-dir", | |
| "${ONESHOT_CONDVAR_DIR}", | |
| "--no-embed-abi", | |
| ] |
🤖 Prompt for AI Agents
In `@Makefile.toml` around lines 142 - 157, In the Makefile.toml task
[tasks.build-oneshot-condvar], replace the args entry "--no-abi" with
"--no-embed-abi" so the oneshot-condvar build (which enables the abi feature)
generates ABI artifacts without embedding them in the WASM; keep the rest of the
cargo args (near build non-reproducible-wasm --locked --manifest-path
./oneshot-condvar/Cargo.toml --features abi,contract,auth-call --out-dir
${ONESHOT_CONDVAR_DIR}) unchanged.
| ### Path 1: cv_wait() then cv_notify() (Happy path) | ||
|
|
||
| ``` | ||
| Idle ──cv_wait()──► WaitingForNotification ──cv_notify_one()──► Authorized ──cv_wait_resume()──► Done | ||
| ``` | ||
|
|
||
| 1. `cv_wait()`: Creates yield promise, state → `WaitingForNotification` | ||
| 2. `cv_notify_one()`: Resumes yield, state → `Authorized` | ||
| 3. `cv_wait_resume()` callback: State → `Done`, returns `true` | ||
|
|
||
| ### Path 2: cv_wait() Timeout | ||
|
|
||
| ``` | ||
| Idle ──cv_wait()──► WaitingForNotification ──timeout──► Idle (can retry) | ||
| ``` | ||
|
|
||
| 1. `cv_wait()`: Creates yield promise, state → `WaitingForNotification` | ||
| 2. Yield times out | ||
| 3. `cv_wait_resume()` callback: State → `Idle`, emits `Timeout`, returns `false` | ||
|
|
||
| ### Path 3: cv_notify() then cv_wait() (Notify first) | ||
|
|
||
| ``` | ||
| Idle ──cv_notify_one()──► Authorized ──cv_wait()──► Done | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix headings to use cv_notify_one() consistently.
The API uses cv_notify_one(), but headings say cv_notify().
✏️ Suggested fix
-### Path 1: cv_wait() then cv_notify() (Happy path)
+### Path 1: cv_wait() then cv_notify_one() (Happy path)
@@
-### Path 3: cv_notify() then cv_wait() (Notify first)
+### Path 3: cv_notify_one() then cv_wait() (Notify first)🧰 Tools
🪛 markdownlint-cli2 (0.20.0)
[warning] 75-75: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 85-85: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 95-95: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In `@oneshot-condvar/README.md` around lines 73 - 97, Headings in the README
incorrectly use cv_notify() instead of the API's cv_notify_one(); update the
section headings and any inline occurrences that read "cv_notify()" to
"cv_notify_one()" (e.g., the Path 1 and Path 3 headings and their sequence
diagrams) so the documentation matches the function name cv_notify_one().
| match serde_json::from_str(&msg).unwrap_or_default() { | ||
| MTReceiverMode::AcceptAll => PromiseOrValue::Value(vec![U128(0); amounts.len()]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -path "*/tests/contracts/multi-token-receiver-stub/src/lib.rs" -type fRepository: near/intents
Length of output: 110
🏁 Script executed:
find . -name "lib.rs" | grep multi-token-receiver-stubRepository: near/intents
Length of output: 110
🏁 Script executed:
cat -n ./tests/contracts/multi-token-receiver-stub/src/lib.rsRepository: near/intents
Length of output: 9675
Make mode parsing strict in test stub to catch malformed inputs.
unwrap_or_default() silently treats any JSON parse error as AcceptAll, which can mask bugs where test messages are incorrectly serialized. In a test stub, stricter validation is valuable. Only default for explicitly empty messages; panic on actual JSON parse errors.
🔧 Suggested change
- match serde_json::from_str(&msg).unwrap_or_default() {
+ let mode: MTReceiverMode = if msg.is_empty() {
+ MTReceiverMode::default()
+ } else {
+ serde_json::from_str(&msg).unwrap_or_else(|e| {
+ env::panic_str(&format!("Invalid MTReceiverMode msg: {e}"))
+ })
+ };
+ match mode {Also applies to: 128-129 (FTReceiverMode)
🤖 Prompt for AI Agents
In `@tests/contracts/multi-token-receiver-stub/src/lib.rs` around lines 81 - 82,
The test stub currently uses serde_json::from_str(&msg).unwrap_or_default()
which silently maps parse errors to MTReceiverMode::AcceptAll; change the
parsing in the MTReceiverMode branch to explicitly handle Err by panicking on
malformed JSON but only treat an empty/whitespace message as Default::default(),
e.g. match serde_json::from_str(&msg) { Ok(mode) => mode, Err(e) => if
msg.trim().is_empty() { Default::default() } else { panic!("Failed to parse
MTReceiverMode: {}", e) } }; apply the same strict parsing change to the
FTReceiverMode parsing so malformed FT messages also cause a panic while empty
messages still default.
| pub mod escrow_proxy; | ||
| pub mod escrow_with_proxy; | ||
| pub mod oneshot_condvar; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# First, find the actual test modules directory and list files
echo "=== Locating test modules ==="
fd -t f '\.rs$' tests/ | grep -E '(escrow_proxy|escrow_with_proxy|oneshot_condvar|utils)' | head -20
echo -e "\n=== Checking for mod.rs in tests/src/tests ==="
ls -la tests/src/tests/ 2>/dev/null || ls -la tests/tests/ 2>/dev/null || find . -path '*/tests/tests/mod.rs' -o -path '*/tests/src/tests/mod.rs' 2>/dev/null | head -5
echo -e "\n=== Listing all .rs files in test directories ==="
find tests -name '*.rs' -type f 2>/dev/null | sortRepository: near/intents
Length of output: 3997
🏁 Script executed:
cat -n tests/src/tests/mod.rs 2>/dev/null || cat -n tests/tests/mod.rs 2>/dev/null || find . -name 'mod.rs' -path '*/tests/*' -exec cat -n {} \; 2>/dev/null | head -50Repository: near/intents
Length of output: 369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Checking escrow_proxy/mod.rs ==="
cat -n tests/src/tests/escrow_proxy/mod.rs
echo -e "\n=== Checking escrow_with_proxy/mod.rs ==="
cat -n tests/src/tests/escrow_with_proxy/mod.rs
echo -e "\n=== Checking oneshot_condvar/mod.rs ==="
cat -n tests/src/tests/oneshot_condvar/mod.rs
echo -e "\n=== Checking utils.rs ==="
cat -n tests/src/tests/utils.rsRepository: near/intents
Length of output: 19497
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Searching for feature/cfg references in unconditional modules ==="
for file in tests/src/tests/escrow_proxy/mod.rs tests/src/tests/escrow_with_proxy/mod.rs tests/src/tests/oneshot_condvar/mod.rs tests/src/tests/utils.rs; do
echo "--- $file ---"
rg -i 'cfg|feature|defuse|escrow' "$file" || echo "No matches found"
doneRepository: near/intents
Length of output: 632
🏁 Script executed:
cat -n tests/src/extensions/mod.rsRepository: near/intents
Length of output: 264
🏁 Script executed:
grep -A 20 '\[features\]' Cargo.tomlRepository: near/intents
Length of output: 38
🏁 Script executed:
find . -name 'Cargo.toml' -type f | head -5Repository: near/intents
Length of output: 161
🏁 Script executed:
cat -n tests/Cargo.toml | grep -A 30 '\[features\]'Repository: near/intents
Length of output: 1338
Add #[cfg(feature = "defuse")] gate to pub mod utils; on line 14.
The utils module imports from crate::extensions::defuse, which is feature-gated in tests/src/extensions/mod.rs. Without a matching gate on the module declaration, builds with the "defuse" feature disabled will fail. Apply the same feature guard used for other feature-dependent modules like defuse and escrow.
🤖 Prompt for AI Agents
In `@tests/src/tests/mod.rs` around lines 7 - 9, The tests/src/tests/mod.rs file
declares pub mod utils; but utils imports crate::extensions::defuse which is
feature-gated; add the same feature gate by placing #[cfg(feature = "defuse")]
immediately above the pub mod utils; declaration (matching the existing gates
used for defuse and escrow) so the utils module is compiled only when the
"defuse" feature is enabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@escrow-proxy/src/contract/tokens/nep141.rs`:
- Around line 88-90: The resolve_ft_transfer function incorrectly treats the
promise result as "used" and computes original_amount - used; change it to
return the promise result directly (the receiver's returned unused/refundable
amount per NEP-141). Replace the subtraction logic in resolve_ft_transfer with:
obtain the result via bounded_promise_result::<U128>(0).unwrap_or_default() and
return that U128 as-is (so ft_on_transfer's returned unused amount is forwarded
unchanged).
In `@escrow-proxy/src/contract/tokens/nep245.rs`:
- Around line 92-102: The variable name `used` in resolve_mt_transfer is
misleading because bounded_promise_result_with_args::<Vec<U128>>(...) returns
refund/unused amounts from mt_on_transfer; rename `used` to `refunds` (or
`unused_amounts`) and update its references in the function (in
resolve_mt_transfer, where it's filtered/unwrapped and in the zip/map) so the
variable accurately reflects that it contains returned refund amounts from
mt_on_transfer.
| pub fn resolve_ft_transfer(&self, original_amount: U128) -> U128 { | ||
| let used = bounded_promise_result::<U128>(0).unwrap_or_default(); | ||
| U128(original_amount.0.saturating_sub(used.0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for ft_on_transfer implementations in the codebase
rg -n --type=rust -C 5 "ft_on_transfer" --max-count=50Repository: near/intents
Length of output: 11030
🏁 Script executed:
#!/bin/bash
# Read the full nep141.rs file from escrow-proxy
cat -n escrow-proxy/src/contract/tokens/nep141.rsRepository: near/intents
Length of output: 4052
Escrow's resolve_ft_transfer returns the wrong amount per NEP-141 semantics.
The function receives the return value from the receiver's ft_on_transfer callback—which per NEP-141 is the unused (refundable) amount—but computes original_amount - received, treating it as used and returning the inverse. This violates NEP-141 and causes incorrect refunds: if the receiver uses 50 of 100 tokens (returning 50 as unused), the escrow returns 100 - 50 = 50 as if that were the refundable amount, causing the FT contract to refund incorrectly. Return the receiver's value directly or clarify intent: the promise result is already the unused amount per standard.
🤖 Prompt for AI Agents
In `@escrow-proxy/src/contract/tokens/nep141.rs` around lines 88 - 90, The
resolve_ft_transfer function incorrectly treats the promise result as "used" and
computes original_amount - used; change it to return the promise result directly
(the receiver's returned unused/refundable amount per NEP-141). Replace the
subtraction logic in resolve_ft_transfer with: obtain the result via
bounded_promise_result::<U128>(0).unwrap_or_default() and return that U128 as-is
(so ft_on_transfer's returned unused amount is forwarded unchanged).
| #[private] | ||
| pub fn resolve_mt_transfer(&self, original_amounts: Vec<U128>) -> Vec<U128> { | ||
| let used = bounded_promise_result_with_args::<Vec<U128>>(0, original_amounts.len()) | ||
| .filter(|v| v.len() == original_amounts.len()) | ||
| .unwrap_or(vec![U128(0); original_amounts.len()]); | ||
|
|
||
| original_amounts | ||
| .iter() | ||
| .zip(used.iter()) | ||
| .map(|(original, transferred)| U128(original.0.saturating_sub(transferred.0))) | ||
| .collect() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n --type=rust -C5 "mt_on_transfer" --max-count=50Repository: near/intents
Length of output: 38314
🏁 Script executed:
sed -n '80,115p' escrow-proxy/src/contract/tokens/nep245.rsRepository: near/intents
Length of output: 952
🏁 Script executed:
rg -n --type=rust -B10 "resolve_mt_transfer" escrow-proxy/src/contract/tokens/nep245.rsRepository: near/intents
Length of output: 685
🏁 Script executed:
rg -n --type=rust "mt_batch_transfer_call" escrow-proxy/src/contract/tokens/nep245.rs -A5 -B5Repository: near/intents
Length of output: 549
🏁 Script executed:
rg -n "mt_batch_transfer_call" defuse/src/contract/tokens/nep245/core.rs -A10 -B5Repository: near/intents
Length of output: 1770
🏁 Script executed:
rg -n "mt_resolve_transfer" defuse/src/contract/tokens/nep245/ -A10 -B2Repository: near/intents
Length of output: 4993
🏁 Script executed:
rg -n "mt_batch_transfer_call.*returns" defuse/src/contract/tokens/nep245/ -A3Repository: near/intents
Length of output: 522
🏁 Script executed:
rg -n "fn mt_batch_transfer_call" defuse/src/contract/tokens/nep245/core.rs -A40Repository: near/intents
Length of output: 1363
🏁 Script executed:
rg -n "internal_mt_batch_transfer_call" defuse/src/contract/tokens/nep245/core.rs -A50 | head -80Repository: near/intents
Length of output: 2503
🏁 Script executed:
rg -n "fn notify_and_resolve_transfer" defuse/src/contract/tokens/nep245/core.rs -A40Repository: near/intents
Length of output: 1785
🏁 Script executed:
sed -n '14,80p' defuse/src/contract/tokens/nep245/resolver.rsRepository: near/intents
Length of output: 2879
🏁 Script executed:
rg -n "mt_on_transfer" nep245/src/receiver.rs -A10Repository: near/intents
Length of output: 315
Variable naming is misleading; clarify that mt_on_transfer returns unused amounts.
The code correctly calculates the used amounts (original - refund = used), but the variable used actually holds the unused/refund amounts returned by mt_on_transfer. mt_on_transfer semantics return refund amounts (confirmed by NEP-245 receiver implementations), so the calculation is correct. However, rename used to refunds or similar to accurately reflect what the variable contains and prevent future confusion or misuse.
🤖 Prompt for AI Agents
In `@escrow-proxy/src/contract/tokens/nep245.rs` around lines 92 - 102, The
variable name `used` in resolve_mt_transfer is misleading because
bounded_promise_result_with_args::<Vec<U128>>(...) returns refund/unused amounts
from mt_on_transfer; rename `used` to `refunds` (or `unused_amounts`) and update
its references in the function (in resolve_mt_transfer, where it's
filtered/unwrapped and in the zip/map) so the variable accurately reflects that
it contains returned refund amounts from mt_on_transfer.
Summary
This PR introduces a bus proxy architecture for atomic cross-contract token swaps
New Crates:
Escrow-Swap Enhancements:
Testing:
Summary by CodeRabbit
New Features
Examples
Tests
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.