From 27dff728a4c9cb65cd5d92a574105df20cb51887 Mon Sep 17 00:00:00 2001 From: Xun Li Date: Wed, 27 Apr 2022 21:48:38 -0700 Subject: [PATCH] Support take immutable and shared in separate APIs in test scenario (#1558) --- .../examples/basics/sources/Counter.move | 39 ++-- .../examples/basics/sources/Lock.move | 9 +- .../examples/basics/sources/Sandwich.move | 48 ++--- .../examples/defi/tests/FlashLenderTests.move | 20 +- .../examples/defi/tests/SharedEscrowTest.move | 14 +- .../fungible_tokens/tests/BASKETTests.move | 13 +- .../games/tests/SharedTicTacToeTests.move | 9 +- .../examples/nfts/sources/Marketplace.move | 56 +++--- .../nfts/tests/SharedAuctionTests.move | 21 ++- .../objects_tutorial/sources/ColorObject.move | 26 +-- .../framework/sources/TestScenario.move | 123 +++++++++--- .../framework/src/natives/mod.rs | 14 +- .../framework/src/natives/test_scenario.rs | 175 +++++++++++++----- .../framework/tests/CollectionTests.move | 4 +- .../framework/tests/TestScenarioTests.move | 77 +++++--- 15 files changed, 424 insertions(+), 224 deletions(-) diff --git a/sui_programmability/examples/basics/sources/Counter.move b/sui_programmability/examples/basics/sources/Counter.move index 48393b92c6c44..8d76638994476 100644 --- a/sui_programmability/examples/basics/sources/Counter.move +++ b/sui_programmability/examples/basics/sources/Counter.move @@ -71,41 +71,44 @@ module Basics::CounterTest { TestScenario::next_tx(scenario, &user1); { - let counter = TestScenario::take_object(scenario); + let counter_wrapper = TestScenario::take_shared_object(scenario); + let counter = TestScenario::borrow_mut(&mut counter_wrapper); - assert!(Counter::owner(&counter) == owner, 0); - assert!(Counter::value(&counter) == 0, 1); + assert!(Counter::owner(counter) == owner, 0); + assert!(Counter::value(counter) == 0, 1); - Counter::increment(&mut counter, TestScenario::ctx(scenario)); - Counter::increment(&mut counter, TestScenario::ctx(scenario)); - Counter::increment(&mut counter, TestScenario::ctx(scenario)); - TestScenario::return_object(scenario, counter); + Counter::increment(counter, TestScenario::ctx(scenario)); + Counter::increment(counter, TestScenario::ctx(scenario)); + Counter::increment(counter, TestScenario::ctx(scenario)); + TestScenario::return_shared_object(scenario, counter_wrapper); }; TestScenario::next_tx(scenario, &owner); { - let counter = TestScenario::take_object(scenario); + let counter_wrapper = TestScenario::take_shared_object(scenario); + let counter = TestScenario::borrow_mut(&mut counter_wrapper); - assert!(Counter::owner(&counter) == owner, 0); - assert!(Counter::value(&counter) == 3, 1); + assert!(Counter::owner(counter) == owner, 0); + assert!(Counter::value(counter) == 3, 1); - Counter::set_value(&mut counter, 100, TestScenario::ctx(scenario)); + Counter::set_value(counter, 100, TestScenario::ctx(scenario)); - TestScenario::return_object(scenario, counter); + TestScenario::return_shared_object(scenario, counter_wrapper); }; TestScenario::next_tx(scenario, &user1); { - let counter = TestScenario::take_object(scenario); + let counter_wrapper = TestScenario::take_shared_object(scenario); + let counter = TestScenario::borrow_mut(&mut counter_wrapper); - assert!(Counter::owner(&counter) == owner, 0); - assert!(Counter::value(&counter) == 100, 1); + assert!(Counter::owner(counter) == owner, 0); + assert!(Counter::value(counter) == 100, 1); - Counter::increment(&mut counter, TestScenario::ctx(scenario)); + Counter::increment(counter, TestScenario::ctx(scenario)); - assert!(Counter::value(&counter) == 101, 2); + assert!(Counter::value(counter) == 101, 2); - TestScenario::return_object(scenario, counter); + TestScenario::return_shared_object(scenario, counter_wrapper); }; } } diff --git a/sui_programmability/examples/basics/sources/Lock.move b/sui_programmability/examples/basics/sources/Lock.move index d51d2fc60c7ad..ecd9974a50a41 100644 --- a/sui_programmability/examples/basics/sources/Lock.move +++ b/sui_programmability/examples/basics/sources/Lock.move @@ -135,14 +135,15 @@ module Basics::LockTest { // User2 is impatient and he decides to take the treasure. TestScenario::next_tx(scenario, &user2); { - let lock = TestScenario::take_object>(scenario); + let lock_wrapper = TestScenario::take_shared_object>(scenario); + let lock = TestScenario::borrow_mut(&mut lock_wrapper); let key = TestScenario::take_object>(scenario); let ctx = TestScenario::ctx(scenario); - Lock::take(&mut lock, &key, ctx); + Lock::take(lock, &key, ctx); - TestScenario::return_object>(scenario, lock); - TestScenario::return_object>(scenario, key); + TestScenario::return_shared_object(scenario, lock_wrapper); + TestScenario::return_object(scenario, key); }; } } diff --git a/sui_programmability/examples/basics/sources/Sandwich.move b/sui_programmability/examples/basics/sources/Sandwich.move index ce4df96a22634..dded3a1dc2c28 100644 --- a/sui_programmability/examples/basics/sources/Sandwich.move +++ b/sui_programmability/examples/basics/sources/Sandwich.move @@ -46,7 +46,7 @@ module Basics::Sandwich { /// On module init, create a grocery fun init(ctx: &mut TxContext) { - Transfer::share_object(Grocery { + Transfer::share_object(Grocery { id: TxContext::new_id(ctx), profits: Coin::zero(ctx) }); @@ -58,8 +58,8 @@ module Basics::Sandwich { /// Exchange `c` for some ham public(script) fun buy_ham( - grocery: &mut Grocery, - c: Coin, + grocery: &mut Grocery, + c: Coin, ctx: &mut TxContext ) { assert!(Coin::value(&c) == HAM_PRICE, EINSUFFICIENT_FUNDS); @@ -69,8 +69,8 @@ module Basics::Sandwich { /// Exchange `c` for some bread public(script) fun buy_bread( - grocery: &mut Grocery, - c: Coin, + grocery: &mut Grocery, + c: Coin, ctx: &mut TxContext ) { assert!(Coin::value(&c) == BREAD_PRICE, EINSUFFICIENT_FUNDS); @@ -97,7 +97,7 @@ module Basics::Sandwich { /// Owner of the grocery can collect profits by passing his capability public(script) fun collect_profits(_cap: &GroceryOwnerCapability, grocery: &mut Grocery, ctx: &mut TxContext) { let amount = Coin::value(&grocery.profits); - + assert!(amount > 0, ENO_PROFITS); let coin = Coin::withdraw(&mut grocery.profits, amount, ctx); @@ -116,7 +116,7 @@ module Basics::TestSandwich { use Sui::TestScenario; use Sui::Coin::{Self}; use Sui::SUI::SUI; - + #[test] public(script) fun test_make_sandwich() { let owner = @0x1; @@ -130,45 +130,45 @@ module Basics::TestSandwich { TestScenario::next_tx(scenario, &the_guy); { - let grocery = TestScenario::take_object(scenario); + let grocery_wrapper = TestScenario::take_shared_object(scenario); + let grocery = TestScenario::borrow_mut(&mut grocery_wrapper); let ctx = TestScenario::ctx(scenario); - + Sandwich::buy_ham( - &mut grocery, - Coin::mint_for_testing(10, ctx), + grocery, + Coin::mint_for_testing(10, ctx), ctx ); - + Sandwich::buy_bread( - &mut grocery, - Coin::mint_for_testing(2, ctx), + grocery, + Coin::mint_for_testing(2, ctx), ctx ); - - TestScenario::return_object(scenario, grocery); + + TestScenario::return_shared_object(scenario, grocery_wrapper); }; TestScenario::next_tx(scenario, &the_guy); { - let grocery = TestScenario::take_object(scenario); let ham = TestScenario::take_object(scenario); let bread = TestScenario::take_object(scenario); Sandwich::make_sandwich(ham, bread, TestScenario::ctx(scenario)); - TestScenario::return_object(scenario, grocery); }; TestScenario::next_tx(scenario, &owner); - { - let grocery = TestScenario::take_object(scenario); + { + let grocery_wrapper = TestScenario::take_shared_object(scenario); + let grocery = TestScenario::borrow_mut(&mut grocery_wrapper); let capability = TestScenario::take_object(scenario); - assert!(Sandwich::profits(&grocery) == 12, 0); - Sandwich::collect_profits(&capability, &mut grocery, TestScenario::ctx(scenario)); - assert!(Sandwich::profits(&grocery) == 0, 0); + assert!(Sandwich::profits(grocery) == 12, 0); + Sandwich::collect_profits(&capability, grocery, TestScenario::ctx(scenario)); + assert!(Sandwich::profits(grocery) == 0, 0); TestScenario::return_object(scenario, capability); - TestScenario::return_object(scenario, grocery); + TestScenario::return_shared_object(scenario, grocery_wrapper); }; } } diff --git a/sui_programmability/examples/defi/tests/FlashLenderTests.move b/sui_programmability/examples/defi/tests/FlashLenderTests.move index 6d899f2547d90..05b3d19e36b26 100644 --- a/sui_programmability/examples/defi/tests/FlashLenderTests.move +++ b/sui_programmability/examples/defi/tests/FlashLenderTests.move @@ -23,36 +23,38 @@ module DeFi::FlashLenderTests { // borrower requests and repays a loan of 10 coins + the fee TestScenario::next_tx(scenario, &borrower); { - let lender = TestScenario::take_object>(scenario); + let lender_wrapper = TestScenario::take_shared_object>(scenario); + let lender = TestScenario::borrow_mut(&mut lender_wrapper); let ctx = TestScenario::ctx(scenario); - let (loan, receipt) = FlashLender::loan(&mut lender, 10, ctx); + let (loan, receipt) = FlashLender::loan(lender, 10, ctx); // in practice, borrower does something (e.g., arbitrage) to make a profit from the loan. // simulate this by min ting the borrower 5 coins. let profit = Coin::mint_for_testing(5, ctx); Coin::join(&mut profit, loan); let to_keep = Coin::withdraw(&mut profit, 4, ctx); Coin::keep(to_keep, ctx); - FlashLender::repay(&mut lender, profit, receipt); + FlashLender::repay(lender, profit, receipt); - TestScenario::return_object(scenario, lender); + TestScenario::return_shared_object(scenario, lender_wrapper); }; // admin withdraws the 1 coin profit from lending TestScenario::next_tx(scenario, &admin); { - let lender = TestScenario::take_object>(scenario); + let lender_wrapper = TestScenario::take_shared_object>(scenario); + let lender = TestScenario::borrow_mut(&mut lender_wrapper); let admin_cap = TestScenario::take_object(scenario); let ctx = TestScenario::ctx(scenario); // max loan size should have increased because of the fee payment - assert!(FlashLender::max_loan(&lender) == 101, 0); + assert!(FlashLender::max_loan(lender) == 101, 0); // withdraw 1 coin from the pool available for lending - let coin = FlashLender::withdraw(&mut lender, &admin_cap, 1, ctx); + let coin = FlashLender::withdraw(lender, &admin_cap, 1, ctx); // max loan size should decrease accordingly - assert!(FlashLender::max_loan(&lender) == 100, 0); + assert!(FlashLender::max_loan(lender) == 100, 0); Coin::keep(coin, ctx); - TestScenario::return_object(scenario, lender); + TestScenario::return_shared_object(scenario, lender_wrapper); TestScenario::return_object(scenario, admin_cap); } } diff --git a/sui_programmability/examples/defi/tests/SharedEscrowTest.move b/sui_programmability/examples/defi/tests/SharedEscrowTest.move index 79ff5fa4f5599..76db3d72faa05 100644 --- a/sui_programmability/examples/defi/tests/SharedEscrowTest.move +++ b/sui_programmability/examples/defi/tests/SharedEscrowTest.move @@ -118,23 +118,25 @@ module DeFi::SharedEscrowTests { public(script) fun cancel(scenario: &mut Scenario, initiator: &address) { TestScenario::next_tx(scenario, initiator); { - let escrow = TestScenario::take_object>(scenario); + let escrow_wrapper = TestScenario::take_shared_object>(scenario); + let escrow = TestScenario::borrow_mut(&mut escrow_wrapper); let ctx = TestScenario::ctx(scenario); - SharedEscrow::cancel(&mut escrow, ctx); - TestScenario::return_object(scenario, escrow); + SharedEscrow::cancel(escrow, ctx); + TestScenario::return_shared_object(scenario, escrow_wrapper); }; } public(script) fun exchange(scenario: &mut Scenario, bob: &address, item_b_verioned_id: VersionedID) { TestScenario::next_tx(scenario, bob); { - let escrow = TestScenario::take_object>(scenario); + let escrow_wrapper = TestScenario::take_shared_object>(scenario); + let escrow = TestScenario::borrow_mut(&mut escrow_wrapper); let item_b = ItemB { id: item_b_verioned_id }; let ctx = TestScenario::ctx(scenario); - SharedEscrow::exchange(item_b, &mut escrow, ctx); - TestScenario::return_object(scenario, escrow); + SharedEscrow::exchange(item_b, escrow, ctx); + TestScenario::return_shared_object(scenario, escrow_wrapper); }; } diff --git a/sui_programmability/examples/fungible_tokens/tests/BASKETTests.move b/sui_programmability/examples/fungible_tokens/tests/BASKETTests.move index 80d9f9ad7fe36..c14992aa245a9 100644 --- a/sui_programmability/examples/fungible_tokens/tests/BASKETTests.move +++ b/sui_programmability/examples/fungible_tokens/tests/BASKETTests.move @@ -20,24 +20,25 @@ module FungibleTokens::BASKETTests { }; TestScenario::next_tx(scenario, &user); { - let reserve = TestScenario::take_object(scenario); + let reserve_wrapper = TestScenario::take_shared_object(scenario); + let reserve = TestScenario::borrow_mut(&mut reserve_wrapper); let ctx = TestScenario::ctx(scenario); - assert!(BASKET::total_supply(&reserve) == 0, 0); + assert!(BASKET::total_supply(reserve) == 0, 0); let num_coins = 10; let sui = Coin::mint_for_testing(num_coins, ctx); let managed = Coin::mint_for_testing(num_coins, ctx); - let basket = BASKET::mint(&mut reserve, sui, managed, ctx); + let basket = BASKET::mint(reserve, sui, managed, ctx); assert!(Coin::value(&basket) == num_coins, 1); - assert!(BASKET::total_supply(&reserve) == num_coins, 2); + assert!(BASKET::total_supply(reserve) == num_coins, 2); - let (sui, managed) = BASKET::burn(&mut reserve, basket, ctx); + let (sui, managed) = BASKET::burn(reserve, basket, ctx); assert!(Coin::value(&sui) == num_coins, 3); assert!(Coin::value(&managed) == num_coins, 4); Coin::keep(sui, ctx); Coin::keep(managed, ctx); - TestScenario::return_object(scenario, reserve); + TestScenario::return_shared_object(scenario, reserve_wrapper); } } diff --git a/sui_programmability/examples/games/tests/SharedTicTacToeTests.move b/sui_programmability/examples/games/tests/SharedTicTacToeTests.move index 491b4fb0fa45f..1b15ceddf0908 100644 --- a/sui_programmability/examples/games/tests/SharedTicTacToeTests.move +++ b/sui_programmability/examples/games/tests/SharedTicTacToeTests.move @@ -199,10 +199,11 @@ module Games::SharedTicTacToeTests { TestScenario::next_tx(scenario, player); let status; { - let game = TestScenario::take_object(scenario); - SharedTicTacToe::place_mark(&mut game, row, col, TestScenario::ctx(scenario)); - status = SharedTicTacToe::get_status(&game); - TestScenario::return_object(scenario, game); + let game_wrapper = TestScenario::take_shared_object(scenario); + let game = TestScenario::borrow_mut(&mut game_wrapper); + SharedTicTacToe::place_mark(game, row, col, TestScenario::ctx(scenario)); + status = SharedTicTacToe::get_status(game); + TestScenario::return_shared_object(scenario, game_wrapper); }; status } diff --git a/sui_programmability/examples/nfts/sources/Marketplace.move b/sui_programmability/examples/nfts/sources/Marketplace.move index 49696212117cf..fd9bdfdc47d0a 100644 --- a/sui_programmability/examples/nfts/sources/Marketplace.move +++ b/sui_programmability/examples/nfts/sources/Marketplace.move @@ -139,6 +139,8 @@ module NFTs::MarketplaceTests { use Sui::TestScenario::{Self, Scenario}; use NFTs::Marketplace::{Self, Marketplace, Listing}; + use Std::Debug; + // Simple Kitty-NFT data structure. struct Kitty has key, store { id: VersionedID, @@ -172,12 +174,13 @@ module NFTs::MarketplaceTests { // SELLER lists Kitty at the Marketplace for 100 SUI. public(script) fun list_kitty(scenario: &mut Scenario) { TestScenario::next_tx(scenario, &SELLER); - let mkp = TestScenario::take_object(scenario); - let bag = TestScenario::take_nested_object(scenario, &mkp); + let mkp_wrapper = TestScenario::take_shared_object(scenario); + let mkp = TestScenario::borrow_mut(&mut mkp_wrapper); + let bag = TestScenario::take_child_object(scenario, mkp); let nft = TestScenario::take_object(scenario); - Marketplace::list(&mkp, &mut bag, nft, 100, TestScenario::ctx(scenario)); - TestScenario::return_object(scenario, mkp); + Marketplace::list(mkp, &mut bag, nft, 100, TestScenario::ctx(scenario)); + TestScenario::return_shared_object(scenario, mkp_wrapper); TestScenario::return_object(scenario, bag); } @@ -191,17 +194,18 @@ module NFTs::MarketplaceTests { TestScenario::next_tx(scenario, &SELLER); { - let mkp = TestScenario::take_object(scenario); - let bag = TestScenario::take_nested_object(scenario, &mkp); - let listing = TestScenario::take_nested_object>(scenario, &bag); + let mkp_wrapper = TestScenario::take_shared_object(scenario); + let mkp = TestScenario::borrow_mut(&mut mkp_wrapper); + let bag = TestScenario::take_child_object(scenario, mkp); + let listing = TestScenario::take_child_object>(scenario, &bag); // Do the delist operation on a Marketplace. - let nft = Marketplace::delist(&mkp, &mut bag, listing, TestScenario::ctx(scenario)); + let nft = Marketplace::delist(mkp, &mut bag, listing, TestScenario::ctx(scenario)); let kitty_id = burn_kitty(nft); assert!(kitty_id == 1, 0); - TestScenario::return_object(scenario, mkp); + TestScenario::return_shared_object(scenario, mkp_wrapper); TestScenario::return_object(scenario, bag); }; } @@ -219,15 +223,16 @@ module NFTs::MarketplaceTests { // BUYER attempts to delist Kitty and he has no right to do so. :( TestScenario::next_tx(scenario, &BUYER); { - let mkp = TestScenario::take_object(scenario); - let bag = TestScenario::take_nested_object(scenario, &mkp); - let listing = TestScenario::take_nested_object>(scenario, &bag); + let mkp_wrapper = TestScenario::take_shared_object(scenario); + let mkp = TestScenario::borrow_mut(&mut mkp_wrapper); + let bag = TestScenario::take_child_object(scenario, mkp); + let listing = TestScenario::take_child_object>(scenario, &bag); // Do the delist operation on a Marketplace. - let nft = Marketplace::delist(&mkp, &mut bag, listing, TestScenario::ctx(scenario)); + let nft = Marketplace::delist(mkp, &mut bag, listing, TestScenario::ctx(scenario)); let _ = burn_kitty(nft); - TestScenario::return_object(scenario, mkp); + TestScenario::return_shared_object(scenario, mkp_wrapper); TestScenario::return_object(scenario, bag); }; } @@ -236,18 +241,24 @@ module NFTs::MarketplaceTests { public(script) fun buy_kitty() { let scenario = &mut TestScenario::begin(&ADMIN); + Debug::print(&0); create_marketplace(scenario); + Debug::print(&1); mint_some_coin(scenario); + Debug::print(&2); mint_kitty(scenario); + Debug::print(&3); list_kitty(scenario); + Debug::print(&4); // BUYER takes 100 SUI from his wallet and purchases Kitty. TestScenario::next_tx(scenario, &BUYER); { let coin = TestScenario::take_object>(scenario); - let mkp = TestScenario::take_object(scenario); - let bag = TestScenario::take_nested_object(scenario, &mkp); - let listing = TestScenario::take_nested_object>(scenario, &bag); + let mkp_wrapper = TestScenario::take_shared_object(scenario); + let mkp = TestScenario::borrow_mut(&mut mkp_wrapper); + let bag = TestScenario::take_child_object(scenario, mkp); + let listing = TestScenario::take_child_object>(scenario, &bag); let payment = Coin::withdraw(&mut coin, 100, TestScenario::ctx(scenario)); // Do the buy call and expect successful purchase. @@ -256,7 +267,7 @@ module NFTs::MarketplaceTests { assert!(kitty_id == 1, 0); - TestScenario::return_object(scenario, mkp); + TestScenario::return_shared_object(scenario, mkp_wrapper); TestScenario::return_object(scenario, bag); TestScenario::return_object(scenario, coin); }; @@ -276,9 +287,10 @@ module NFTs::MarketplaceTests { TestScenario::next_tx(scenario, &BUYER); { let coin = TestScenario::take_object>(scenario); - let mkp = TestScenario::take_object(scenario); - let bag = TestScenario::take_nested_object(scenario, &mkp); - let listing = TestScenario::take_nested_object>(scenario, &bag); + let mkp_wrapper = TestScenario::take_shared_object(scenario); + let mkp = TestScenario::borrow_mut(&mut mkp_wrapper); + let bag = TestScenario::take_child_object(scenario, mkp); + let listing = TestScenario::take_child_object>(scenario, &bag); // AMOUNT here is 10 while expected is 100. let payment = Coin::withdraw(&mut coin, 10, TestScenario::ctx(scenario)); @@ -287,7 +299,7 @@ module NFTs::MarketplaceTests { let nft = Marketplace::buy(&mut bag, listing, payment); let _ = burn_kitty(nft); - TestScenario::return_object(scenario, mkp); + TestScenario::return_shared_object(scenario, mkp_wrapper); TestScenario::return_object(scenario, bag); TestScenario::return_object(scenario, coin); }; diff --git a/sui_programmability/examples/nfts/tests/SharedAuctionTests.move b/sui_programmability/examples/nfts/tests/SharedAuctionTests.move index d23bfdc4d2d2b..362603cf19404 100644 --- a/sui_programmability/examples/nfts/tests/SharedAuctionTests.move +++ b/sui_programmability/examples/nfts/tests/SharedAuctionTests.move @@ -69,11 +69,12 @@ module NFTs::SharedAuctionTests { TestScenario::next_tx(scenario, &bidder1); { let coin = TestScenario::take_object>(scenario); - let auction = TestScenario::take_object>(scenario); + let auction_wrapper = TestScenario::take_shared_object>(scenario); + let auction = TestScenario::borrow_mut(&mut auction_wrapper); - SharedAuction::bid(coin, &mut auction, TestScenario::ctx(scenario)); + SharedAuction::bid(coin, auction, TestScenario::ctx(scenario)); - TestScenario::return_object(scenario, auction); + TestScenario::return_shared_object(scenario, auction_wrapper); }; // a transaction by the second bidder to put a bid (a bid will @@ -82,11 +83,12 @@ module NFTs::SharedAuctionTests { TestScenario::next_tx(scenario, &bidder2); { let coin = TestScenario::take_object>(scenario); - let auction = TestScenario::take_object>(scenario); + let auction_wrapper = TestScenario::take_shared_object>(scenario); + let auction = TestScenario::borrow_mut(&mut auction_wrapper); - SharedAuction::bid(coin, &mut auction, TestScenario::ctx(scenario)); + SharedAuction::bid(coin, auction, TestScenario::ctx(scenario)); - TestScenario::return_object(scenario, auction); + TestScenario::return_shared_object(scenario, auction_wrapper); }; // a transaction by the second bidder to verify that the funds @@ -103,13 +105,14 @@ module NFTs::SharedAuctionTests { // a transaction by the owner to end auction TestScenario::next_tx(scenario, &owner); { - let auction = TestScenario::take_object>(scenario); + let auction_wrapper = TestScenario::take_shared_object>(scenario); + let auction = TestScenario::borrow_mut(&mut auction_wrapper); // pass auction as mutable reference as its a shared // object that cannot be deleted - SharedAuction::end_auction(&mut auction, TestScenario::ctx(scenario)); + SharedAuction::end_auction(auction, TestScenario::ctx(scenario)); - TestScenario::return_object(scenario, auction); + TestScenario::return_shared_object(scenario, auction_wrapper); }; // a transaction to check if the first bidder won (as the diff --git a/sui_programmability/examples/objects_tutorial/sources/ColorObject.move b/sui_programmability/examples/objects_tutorial/sources/ColorObject.move index 7da37c95f66f1..f1cc8db72499c 100644 --- a/sui_programmability/examples/objects_tutorial/sources/ColorObject.move +++ b/sui_programmability/examples/objects_tutorial/sources/ColorObject.move @@ -207,30 +207,16 @@ module Tutorial::ColorObjectTests { }; TestScenario::next_tx(scenario, &sender1); { - assert!(TestScenario::can_take_object(scenario), 0); + assert!(!TestScenario::can_take_object(scenario), 0); }; let sender2 = @0x2; TestScenario::next_tx(scenario, &sender2); { - assert!(TestScenario::can_take_object(scenario), 0); - }; - } - - #[test] - #[expected_failure(abort_code = 101)] - public(script) fun test_mutate_immutable() { - let sender1 = @0x1; - let scenario = &mut TestScenario::begin(&sender1); - { - let ctx = TestScenario::ctx(scenario); - ColorObject::create_immutable(255, 0, 255, ctx); - }; - TestScenario::next_tx(scenario, &sender1); - { - let object = TestScenario::take_object(scenario); - let ctx = TestScenario::ctx(scenario); - ColorObject::update(&mut object, 0, 0, 0, ctx); - TestScenario::return_object(scenario, object); + let object_wrapper = TestScenario::take_immutable_object(scenario); + let object = TestScenario::borrow(&object_wrapper); + let (red, green, blue) = ColorObject::get_color(object); + assert!(red == 255 && green == 0 && blue == 255, 0); + TestScenario::return_immutable_object(scenario, object_wrapper); }; } } diff --git a/sui_programmability/framework/sources/TestScenario.move b/sui_programmability/framework/sources/TestScenario.move index cc42f50543f9d..f5238a7d87bae 100644 --- a/sui_programmability/framework/sources/TestScenario.move +++ b/sui_programmability/framework/sources/TestScenario.move @@ -74,6 +74,16 @@ module Sui::TestScenario { event_start_indexes: vector, } + /// A wrapper for TestScenario to return an immutable object from the inventory + struct ImmutableWrapper { + object: T, + } + + /// A wrapper for TestScenario to return a shared object from the inventory + struct SharedWrapper { + object: T, + } + /// Begin a new multi-transaction test scenario in a context where `sender` is the tx sender public fun begin(sender: &address): Scenario { Scenario { @@ -112,26 +122,74 @@ module Sui::TestScenario { /// Remove the object of type `T` from the inventory of the current tx sender in `scenario`. /// An object is in the sender's inventory if: /// - The object is in the global event log - /// - The sender owns the object, or the object is immutable + /// - The sender owns the object /// - If the object was previously removed, it was subsequently replaced via a call to `return_object`. /// Aborts if there is no object of type `T` in the inventory of the tx sender /// Aborts if there is >1 object of type `T` in the inventory of the tx sender--this function /// only succeeds when the object to choose is unambiguous. In cases where there are multiple `T`'s, /// the caller should resolve the ambiguity by using `take_object_by_id`. public fun take_object(scenario: &mut Scenario): T { - let sender = sender(scenario); - remove_unique_object(scenario, sender) + let signer_address = sender(scenario); + let objects: vector = get_account_owned_inventory( + signer_address, + last_tx_start_index(scenario) + ); + remove_unique_object_from_inventory(scenario, objects) + } + + /// Similar to take_object, but only return objects that are immutable with type `T`. + /// In this case, the sender is irrelevant. + /// Returns a wrapper that only supports a `borrow` API to get the read-only reference. + public fun take_immutable_object(scenario: &mut Scenario): ImmutableWrapper { + let objects: vector = get_unowned_inventory( + true /* immutable */, + last_tx_start_index(scenario), + ); + let object = remove_unique_object_from_inventory(scenario, objects); + ImmutableWrapper { + object, + } + } + + /// Returns the underlying reference of an immutable object wrapper returned above. + public fun borrow(wrapper: &ImmutableWrapper): &T { + &wrapper.object + } + + /// Similar to take_object, but only return objects that are shared with type `T`. + /// In this case, the sender is irrelevant. + /// Returns a wrapper that only supports a `borrow_mut` API to get the mutable reference. + public fun take_shared_object(scenario: &mut Scenario): SharedWrapper { + let objects: vector = get_unowned_inventory( + false /* immutable */, + last_tx_start_index(scenario), + ); + let object = remove_unique_object_from_inventory(scenario, objects); + SharedWrapper { + object, + } + } + + /// Returns the underlying mutable reference of a shared object. + public fun borrow_mut(wrapper: &mut SharedWrapper): &mut T { + &mut wrapper.object } /// Remove and return the child object of type `T2` owned by `parent_obj`. /// Aborts if there is no object of type `T2` owned by `parent_obj` /// Aborts if there is >1 object of type `T2` owned by `parent_obj`--this function /// only succeeds when the object to choose is unambiguous. In cases where there are are multiple `T`'s - /// owned by `parent_obj`, the caller should resolve the ambiguity using `take_nested_object_by_id`. - public fun take_nested_object( + /// owned by `parent_obj`, the caller should resolve the ambiguity using `take_child_object_by_id`. + public fun take_child_object( scenario: &mut Scenario, parent_obj: &T1 ): T2 { - remove_unique_object(scenario, ID::id_address(ID::id(parent_obj))) + let signer_address = sender(scenario); + let objects = get_object_owned_inventory( + signer_address, + ID::id_address(ID::id(parent_obj)), + last_tx_start_index(scenario), + ); + remove_unique_object_from_inventory(scenario, objects) } /// Same as `take_object`, but returns the object of type `T` with object ID `id`. @@ -167,7 +225,7 @@ module Sui::TestScenario { /// Same as `take_nested_object`, but returns the child object of type `T` with object ID `id`. /// Should only be used in cases where the parent object has more than one child of type `T`. - public fun take_nested_object_by_id( + public fun take_child_object_by_id( _scenario: &mut Scenario, _parent_obj: &T1, _child_id: ID ): T2 { // TODO: implement me @@ -194,9 +252,21 @@ module Sui::TestScenario { update_object(t) } + /// Similar to return_object, return a shared object to the inventory. + public fun return_shared_object(scenario: &mut Scenario, object_wrapper: SharedWrapper) { + let SharedWrapper { object } = object_wrapper; + return_object(scenario, object) + } + + /// Return an immutable object to the inventory. + public fun return_immutable_object(scenario: &mut Scenario, object_wrapper: ImmutableWrapper) { + let ImmutableWrapper { object } = object_wrapper; + return_object(scenario, object) + } + /// Return `true` if a call to `take_object(scenario)` will succeed public fun can_take_object(scenario: &Scenario): bool { - let objects: vector = get_inventory( + let objects: vector = get_account_owned_inventory( sender(scenario), last_tx_start_index(scenario) ); @@ -242,24 +312,13 @@ module Sui::TestScenario { *Vector::borrow(idxs, Vector::length(idxs) - 1) } - /// Remove and return the unique object of type `T` that can be accessed by `signer_address` - /// Aborts if there are no objects of type `T` that can be be accessed by `signer_address` - /// Aborts if there is >1 object of type `T` that can be accessed by `signer_address` - fun remove_unique_object(scenario: &mut Scenario, signer_address: address): T { - let num_concluded_txes = num_concluded_txes(scenario); - // Can't remove objects transferred by previous transactions if there are none - assert!(num_concluded_txes != 0, ENO_CONCLUDED_TRANSACTIONS); - - let objects: vector = get_inventory( - signer_address, - last_tx_start_index(scenario) - ); - let objects_len = Vector::length(&objects); + fun remove_unique_object_from_inventory(scenario: &mut Scenario, inventory: vector): T { + let objects_len = Vector::length(&inventory); if (objects_len == 1) { // found a unique object. ensure that it hasn't already been removed, then return it - let t = Vector::pop_back(&mut objects); + let t = Vector::pop_back(&mut inventory); let id = ID::id(&t); - Vector::destroy_empty(objects); + Vector::destroy_empty(inventory); assert!(!Vector::contains(&scenario.removed, id), EALREADY_REMOVED_OBJECT); Vector::push_back(&mut scenario.removed, *id); @@ -273,7 +332,7 @@ module Sui::TestScenario { fun find_object_by_id_in_inventory(scenario: &Scenario, id: &ID): Option { let sender = sender(scenario); - let objects: vector = get_inventory( + let objects: vector = get_account_owned_inventory( sender, last_tx_start_index(scenario) ); @@ -300,7 +359,21 @@ module Sui::TestScenario { /// Return all live objects of type `T` that can be accessed by `signer_address` in the current transaction /// Events at or beyond `tx_end_index` in the log should not be processed to build this inventory - native fun get_inventory(signer_address: address, tx_end_index: u64): vector; + native fun get_account_owned_inventory(signer_address: address, tx_end_index: u64): vector; + + /// Return all live objects of type `T` that's owned by another object `parent_object_id`, with + /// signer account `signer_address`. + /// Events at or beyond `tx_end_index` in the log should not be processed to build this inventory + native fun get_object_owned_inventory( + signer_address: address, + parent_object_id: address, + tx_end_index: u64, + ): vector; + + /// Return all live objects of type `T` that's not owned, i.e. either immutable or shared. + /// `immutable` indicates whether we want to return immutable object or shared. + /// Events at or beyond `tx_end_index` in the log should not be processed to build this inventory + native fun get_unowned_inventory(immutable: bool, tx_end_index: u64): vector; /// Test-only function for discarding an arbitrary object. /// Useful for eliminating objects without the `drop` ability. diff --git a/sui_programmability/framework/src/natives/mod.rs b/sui_programmability/framework/src/natives/mod.rs index 39f2c644587ee..8ca37c5be7954 100644 --- a/sui_programmability/framework/src/natives/mod.rs +++ b/sui_programmability/framework/src/natives/mod.rs @@ -33,8 +33,18 @@ pub fn all_natives( ), ( "TestScenario", - "get_inventory", - test_scenario::get_inventory, + "get_account_owned_inventory", + test_scenario::get_account_owned_inventory, + ), + ( + "TestScenario", + "get_object_owned_inventory", + test_scenario::get_object_owned_inventory, + ), + ( + "TestScenario", + "get_unowned_inventory", + test_scenario::get_unowned_inventory, ), ("TestScenario", "num_events", test_scenario::num_events), ( diff --git a/sui_programmability/framework/src/natives/test_scenario.rs b/sui_programmability/framework/src/natives/test_scenario.rs index 714562750b8f2..7809188e1172e 100644 --- a/sui_programmability/framework/src/natives/test_scenario.rs +++ b/sui_programmability/framework/src/natives/test_scenario.rs @@ -28,21 +28,23 @@ type Event = (Vec, u64, Type, MoveTypeLayout, Value); const WRAPPED_OBJECT_EVENT: u64 = 255; const UPDATE_OBJECT_EVENT: u64 = 254; -/// Trying to transfer an unowned object, which is not allowed. -/// This won't be detected right away when a transfer is happening. -/// Instead, it's detected when processing the inventory events. -const ETRANSFER_UNOWNED_OBJECT: u64 = 100; - -/// Trying to mutating an immutable object. -/// This is detected when returning an immutable object back to -/// the inventory. -const EMUTATING_IMMUTABLE_OBJECT: u64 = 101; +/// When transfer an object to a parent object, the parent object +/// is not found in the inventory. +const EPARENT_OBJECT_NOT_FOUND: u64 = 100; #[derive(Debug)] struct OwnedObj { value: Value, type_: Type, + /// Owner is the direct owner of the object. owner: Owner, + /// Signer is the ultimate owner of the object potentially through + /// chains of object ownership. + /// e.g. If account A ownd object O1, O1 owns O2. Then + /// O2's owner is O1, and signer is A. + /// signer will always be set eventually, but it needs to be optional first + /// since we may not know its signer initially. + signer: Option, } /// Set of all live objects in the current test scenario @@ -74,10 +76,22 @@ fn get_object_id_from_event(event_type_byte: u64, val: &Value) -> Option().unwrap().as_slice()).unwrap()) } +fn account_to_sui_address(address: AccountAddress) -> SuiAddress { + SuiAddress::try_from(address.as_slice()).unwrap() +} + /// Process the event log to determine the global set of live objects /// Returns the abort_code if an error is encountered. fn get_global_inventory(events: &[Event]) -> Result { let mut inventory = Inventory::new(); + // Since we allow transfer object to ID, it's possible that when we transfer + // an object to a parenet object, the parent object does not yet exist in the event log. + // And without the parent object we cannot know the ultimate signer. + // To handle this, for child objects whose parent is not yet known, we add them + // to the unresolved_signer_parents map, which maps from parent object ID + // to the list of child objects it has. Whenever a new object is seen, we check the map + // and resolve if the object is an unresolved parent. + let mut unresolved_signer_parents: BTreeMap> = BTreeMap::new(); for (recipient, event_type_byte, type_, _layout, val) in events { let obj_id = if let Some(obj_id) = get_object_id_from_event(*event_type_byte, val) { obj_id @@ -93,9 +107,6 @@ fn get_global_inventory(events: &[Event]) -> Result { if *event_type_byte == UPDATE_OBJECT_EVENT { if let Some(cur) = inventory.get_mut(&obj_id) { let new_value = val.copy_value().unwrap(); - if cur.owner == Owner::Immutable && !cur.value.equals(&new_value).unwrap() { - return Err(EMUTATING_IMMUTABLE_OBJECT); - } // Update the object content since it may have been mutated. cur.value = new_value; } @@ -108,7 +119,28 @@ fn get_global_inventory(events: &[Event]) -> Result { | EventType::TransferToObject | EventType::FreezeObject | EventType::ShareObject => { - let owner = get_new_owner(&inventory, &obj_id, event_type, recipient.clone())?; + let owner = get_new_owner(&event_type, recipient.clone()); + let signer = if event_type == EventType::TransferToObject { + let parent_id = ObjectID::try_from(recipient.as_slice()).unwrap(); + if let Some(parent_obj) = inventory.get(&parent_id) { + parent_obj.signer + } else { + unresolved_signer_parents + .entry(parent_id) + .or_default() + .insert(obj_id); + None + } + } else { + Some(owner) + }; + if signer.is_some() { + if let Some(children) = unresolved_signer_parents.remove(&obj_id) { + for child in children { + inventory.get_mut(&child).unwrap().signer = signer; + } + } + } // note; may overwrite older values of the object, which is intended inventory.insert( obj_id, @@ -116,6 +148,7 @@ fn get_global_inventory(events: &[Event]) -> Result { value: Value::copy_value(val).unwrap(), type_: type_.clone(), owner, + signer, }, ); } @@ -126,22 +159,16 @@ fn get_global_inventory(events: &[Event]) -> Result { EventType::User => (), } } - Ok(inventory) + if unresolved_signer_parents.is_empty() { + Ok(inventory) + } else { + Err(EPARENT_OBJECT_NOT_FOUND) + } } -fn get_new_owner( - inventory: &Inventory, - obj_id: &ObjectID, - event_type: EventType, - recipient: Vec, -) -> Result { - if let Some(existing) = inventory.get(obj_id) { - if !existing.owner.is_owned() { - // Unowned objects are not allowed to be transferred. - return Err(ETRANSFER_UNOWNED_OBJECT); - } - } - Ok(match event_type { +/// Return the new owner of the object after the transfer event. +fn get_new_owner(event_type: &EventType, recipient: Vec) -> Owner { + match event_type { EventType::FreezeObject => Owner::Immutable, EventType::ShareObject => Owner::Shared, EventType::TransferToAddress => { @@ -149,35 +176,33 @@ fn get_new_owner( } EventType::TransferToObject => Owner::ObjectOwner(SuiAddress::try_from(recipient).unwrap()), _ => panic!("Unrecognized event_type"), - }) + } } /// Get the objects of type `type_` that can be spent by `addr` /// Returns the abort_code if an error is encountered. fn get_inventory_for( - addr: &AccountAddress, + signer: Owner, + parent_object: Option, type_: &Type, tx_end_index: usize, events: &[Event], ) -> Result, u64> { let inventory = get_global_inventory(&events[..tx_end_index])?; - let sui_addr = SuiAddress::try_from(addr.to_vec()).unwrap(); Ok(inventory .into_iter() - .filter_map(|(_, obj)| { - // TODO: We should also be able to include objects indirectly owned by the - // requested address through owning other objects. - // https://github.com/MystenLabs/sui/issues/673 - if (obj.owner == Owner::AddressOwner(sui_addr) - || obj.owner == Owner::ObjectOwner(sui_addr) - || !obj.owner.is_owned()) - && &obj.type_ == type_ - { - Some(obj.value) - } else { - None - } + .filter(|(_, obj)| { + &obj.type_ == type_ + && if let Some(parent) = parent_object { + let obj_signer = obj.signer.unwrap(); + obj.owner + == Owner::ObjectOwner(SuiAddress::try_from(parent.as_slice()).unwrap()) + && (!obj_signer.is_owned() || obj_signer == signer) + } else { + obj.owner == signer + } }) + .map(|(_, obj)| obj.value) .collect()) } @@ -241,7 +266,7 @@ pub fn num_events( } /// Return all the values of type `T` in the inventory of `owner_address` -pub fn get_inventory( +pub fn get_account_owned_inventory( context: &mut NativeContext, ty_args: Vec, mut args: VecDeque, @@ -253,7 +278,67 @@ pub fn get_inventory( let owner_address = pop_arg!(args, AccountAddress); let cost = native_gas(context.cost_table(), NativeCostIndex::EMIT_EVENT, 0); - match get_inventory_for(&owner_address, &ty_args[0], tx_end_index, context.events()) { + match get_inventory_for( + Owner::AddressOwner(account_to_sui_address(owner_address)), + None, + &ty_args[0], + tx_end_index, + context.events(), + ) { + Ok(inventory) => Ok(NativeResult::ok( + cost, + smallvec![Value::vector_for_testing_only(inventory)], + )), + Err(abort_code) => Ok(NativeResult::err(cost, abort_code)), + } +} + +pub fn get_unowned_inventory( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert_eq!(ty_args.len(), 1); + debug_assert_eq!(args.len(), 2); + + let tx_end_index = pop_arg!(args, u64) as usize; + let immutable = pop_arg!(args, bool); + let owner = if immutable { + Owner::Immutable + } else { + Owner::Shared + }; + + let cost = native_gas(context.cost_table(), NativeCostIndex::EMIT_EVENT, 0); + match get_inventory_for(owner, None, &ty_args[0], tx_end_index, context.events()) { + Ok(inventory) => Ok(NativeResult::ok( + cost, + smallvec![Value::vector_for_testing_only(inventory)], + )), + Err(abort_code) => Ok(NativeResult::err(cost, abort_code)), + } +} + +pub fn get_object_owned_inventory( + context: &mut NativeContext, + ty_args: Vec, + mut args: VecDeque, +) -> PartialVMResult { + debug_assert_eq!(ty_args.len(), 1); + debug_assert_eq!(args.len(), 3); + + let tx_end_index = pop_arg!(args, u64) as usize; + let parent_object = pop_arg!(args, AccountAddress); + let signer_address = pop_arg!(args, AccountAddress); + + let cost = native_gas(context.cost_table(), NativeCostIndex::EMIT_EVENT, 0); + match get_inventory_for( + Owner::AddressOwner(account_to_sui_address(signer_address)), + Some(parent_object), + &ty_args[0], + tx_end_index, + context.events(), + ) { Ok(inventory) => Ok(NativeResult::ok( cost, smallvec![Value::vector_for_testing_only(inventory)], diff --git a/sui_programmability/framework/tests/CollectionTests.move b/sui_programmability/framework/tests/CollectionTests.move index f8c02251aabef..b6ef99bb4d886 100644 --- a/sui_programmability/framework/tests/CollectionTests.move +++ b/sui_programmability/framework/tests/CollectionTests.move @@ -72,7 +72,7 @@ module Sui::CollectionTests { { let collection = TestScenario::take_object>(scenario); let bag = TestScenario::take_object(scenario); - let obj = TestScenario::take_nested_object, Object>(scenario, &collection); + let obj = TestScenario::take_child_object, Object>(scenario, &collection); let id = *ID::id(&obj); let (obj, child_ref) = Collection::remove(&mut collection, obj); @@ -91,7 +91,7 @@ module Sui::CollectionTests { { let collection = TestScenario::take_object>(scenario); let bag = TestScenario::take_object(scenario); - let obj = TestScenario::take_nested_object(scenario, &bag); + let obj = TestScenario::take_child_object(scenario, &bag); let id = *ID::id(&obj); let obj = Bag::remove(&mut bag, obj); diff --git a/sui_programmability/framework/tests/TestScenarioTests.move b/sui_programmability/framework/tests/TestScenarioTests.move index 07be31b4ba6c7..0a8048198b89e 100644 --- a/sui_programmability/framework/tests/TestScenarioTests.move +++ b/sui_programmability/framework/tests/TestScenarioTests.move @@ -4,8 +4,8 @@ #[test_only] module Sui::TestScenarioTests { use Sui::ID; - use Sui::TestScenario; - use Sui::Transfer; + use Sui::TestScenario::{Self, Scenario}; + use Sui::Transfer::{Self, ChildRef}; use Sui::TxContext; const ID_BYTES_MISMATCH: u64 = 0; @@ -22,6 +22,11 @@ module Sui::TestScenarioTests { child: Object, } + struct Parent has key { + id: ID::VersionedID, + child: ChildRef, + } + #[test] fun test_wrap_unwrap() { let sender = @0x0; @@ -264,43 +269,59 @@ module Sui::TestScenarioTests { } #[test] - #[expected_failure(abort_code = 100 /* ETRANSFER_SHARED_OBJECT */)] - fun test_freeze_then_transfer() { + fun test_take_child_object() { let sender = @0x0; let scenario = TestScenario::begin(&sender); - { - let obj = Object { id: TestScenario::new_id(&mut scenario), value: 100 }; - Transfer::freeze_object(obj); - }; - TestScenario::next_tx(&mut scenario, &sender); - { - let obj = TestScenario::take_object(&mut scenario); - // Transfer an immutable object, this won't fail right away. - Transfer::transfer(obj, copy sender); - }; + create_parent_and_object(&mut scenario); + TestScenario::next_tx(&mut scenario, &sender); { - // while removing the object, test scenario will read the inventory, - // and discover that we transferred an immutable object. - let obj = TestScenario::take_object(&mut scenario); - TestScenario::return_object(&mut scenario, obj); + // sender cannot take object directly. + assert!(!TestScenario::can_take_object(&scenario), 0); + // sender can take parent, however. + assert!(TestScenario::can_take_object(&scenario), 0); + + let parent = TestScenario::take_object(&mut scenario); + // Make sure we can take the child object with the parent object. + let child = TestScenario::take_child_object(&mut scenario, &parent); + TestScenario::return_object(&mut scenario, parent); + TestScenario::return_object(&mut scenario, child); }; } + #[expected_failure(abort_code = 3 /* EMPTY_INVENTORY */)] #[test] - #[expected_failure(abort_code = 101 /* EMUTATING_IMMUTABLE_OBJECT */)] - fun test_freeze_then_mutate() { + fun test_take_child_object_incorrect_signer() { let sender = @0x0; let scenario = TestScenario::begin(&sender); - { - let obj = Object { id: TestScenario::new_id(&mut scenario), value: 100 }; - Transfer::freeze_object(obj); - }; + create_parent_and_object(&mut scenario); + TestScenario::next_tx(&mut scenario, &sender); - { - let obj = TestScenario::take_object(&mut scenario); - obj.value = 200; - TestScenario::return_object(&mut scenario, obj); + let parent = TestScenario::take_object(&mut scenario); + + let another = @0x1; + TestScenario::next_tx(&mut scenario, &another); + // This should fail even though we have parent object here. + // Because the signer doesn't match. + let child = TestScenario::take_child_object(&mut scenario, &parent); + TestScenario::return_object(&mut scenario, child); + + TestScenario::return_object(&mut scenario, parent); + } + + /// Create object and parent. object is a child of parent. + /// parent is owned by sender of `scenario`. + fun create_parent_and_object(scenario: &mut Scenario) { + let parent_id = TestScenario::new_id(scenario); + let object = Object { + id: TestScenario::new_id(scenario), + value: 10, + }; + let (parent_id, child) = Transfer::transfer_to_object_id(object, parent_id); + let parent = Parent { + id: parent_id, + child, }; + Transfer::transfer(parent, TestScenario::sender(scenario)); } }