Skip to content

chore: nonfungible example contract tests with pop-drink #565

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

Open
wants to merge 28 commits into
base: main
Choose a base branch
from

Conversation

chungquantin
Copy link
Collaborator

@chungquantin chungquantin commented May 12, 2025

The PR add drink tests to nonfungible example contract.

Requires: r0gue-io/pop-drink#31

@chungquantin chungquantin self-assigned this May 12, 2025
@codecov-commenter
Copy link

codecov-commenter commented May 12, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 63.47%. Comparing base (e356fb4) to head (13d33dd).

@@           Coverage Diff           @@
##             main     #565   +/-   ##
=======================================
  Coverage   63.47%   63.47%           
=======================================
  Files         121      121           
  Lines       23838    23838           
  Branches    23838    23838           
=======================================
  Hits        15130    15130           
  Misses       7664     7664           
  Partials     1044     1044           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chungquantin chungquantin marked this pull request as ready for review May 13, 2025 13:40
@chungquantin chungquantin requested review from Daanvdplas and a team May 13, 2025 14:48
@evilrobot-01
Copy link
Collaborator

Please target this to the fungibles PR branch, so that we dont have to review the same changes which appear in that PR again here.

@chungquantin chungquantin changed the base branch from main to chungquantin/api/fungible/chore-bump_example_drink May 15, 2025 04:04
}

/// Returns the owner of an item, if any.
#[ink(message)]
pub fn owner_of(&self, item: ItemId) -> Result<Option<AccountId>> {
api::owner_of(self.id, item).map_err(Psp34Error::from)
pub fn owner_of(&self, item: ItemId) -> Option<AccountId> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To keep it consistent with the fungibles example

Base automatically changed from chungquantin/api/fungible/chore-bump_example_drink to main May 16, 2025 06:16
@evilrobot-01
Copy link
Collaborator

@chungquantin please can you kindly resolve the merge conflicts?

* fix visibility for pallet_nfts types used as call arguments

Changes from the commit: paritytech/polkadot-sdk#3634

* [pallet-nfts, pallet_uniques] - Expose private structs

Changes made from upstream: paritytech/polkadot-sdk#6087

* Adds BlockNumberProvider in multisig, proxy and nft pallets

Upstream commit: paritytech/polkadot-sdk#5723

* Removed unused dependencies (partial progress)

Upstream commit: paritytech/polkadot-sdk#7329

* implement DecodeWithMemTracking for frame pallets

paritytech/polkadot-sdk#7598

* chore: bump pallet-nfts version to 34.1.0

* feat: adds BlockNumberProvider in nonfungbiles pallet

* chore: update lock file of pop-api to sync nfts

* chore: BlockNumberProvider for devnet nonfungibles

* chore: fix benchmarking to use new BlockNumberFor

* chore: revert changes

* fix: missing DecodeWithMemTracking

* chore: update weights of pallets

* refactor(nonfungibles): remove unused BlockNumberProvider type

* chore: remove unused Zero trait

* chore: revert non-relevant changes

* chore: revert non-relevant changes

This reverts commit 182486a.

* chore: benchmarking replace with BlockNumberProvider

* chore(pallet-nfts): update weights
* chore(api/examples/fungible): bump drink version

* chore(ci): test contract in examples

* chore: update drink dependency branch

* chore: resolve review comments
@@ -10,6 +10,12 @@ pop-api = { path = "../../../pop-api", default-features = false, features = [
"nonfungibles",
] }

[dev-dependencies]
drink = { package = "pop-drink", git = "https://github.com/r0gue-io/pop-drink", branch = "chungquantin/feat-nfts", features = [ "devnet" ] }
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be merged to main and updated here before being merged here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

PR approved, so once merged this can be updated.

// The contract bundle provider.
//
// See https://github.com/r0gue-io/pop-drink/blob/main/crates/drink/drink/test-macro/src/lib.rs for more information.
#[drink::contract_bundle_provider]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this definition be moved within the deploy helper function below? It eliminates the complexity/noise of having yet another thing to define, where someone can just copy the deploy function in its entirey and get this definition included.

The separate usage on 77 should then be using the deploy helper.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea! I have moved it to the deploy method in 7607a1a


// Deployment and constructor method tests.

fn deploy_with_default(session: &mut Session<Pop>) -> Result<AccountId> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would move all the helpers to the bottom after tests. I see value in firstly showing how the sandbox is defined/initialised, but then it should be showing the tests and not more boiler plate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Move this method into Contract struct and rename it to default. Hence, Contract::default() means deploying a contract by default to keep it short.


// A set of helper methods to test the contract calls.

fn collection_id(session: &mut Session<Pop>) -> CollectionId {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would suggest a pattern like

struct Contract {
address: AccountId32,
id: AccountId32,
}
impl Contract {
fn new() -> Self {
let address = instantiate(CONTRACT, INIT_VALUE, vec![]);
Self { address: address.clone(), id: address }
}
fn ismp_get(
&self,
id: MessageId,
request: Get,
fee: Balance,
callback: bool,
) -> Result<(), Error> {
let result = self.call("ismp_get", (id, request, fee, callback).encode(), 0);
<Result<(), Error>>::decode(&mut &result.data[1..])
.unwrap_or_else(|_| panic!("Contract reverted: {:?}", result))
}
fn ismp_post(
&self,
id: MessageId,
request: Post,
fee: Balance,
callback: bool,
) -> Result<(), Error> {
let result = self.call("ismp_post", (id, request, fee, callback).encode(), 0);
<Result<(), Error>>::decode(&mut &result.data[1..])
.unwrap_or_else(|_| panic!("Contract reverted: {:?}", result))
}
fn xcm_new_query(
&self,
id: MessageId,
responder: Location,
timeout: BlockNumber,
callback: bool,
) -> Result<Option<QueryId>, Error> {
let result = self.call("xcm_new_query", (id, responder, timeout, callback).encode(), 0);
<Result<Option<QueryId>, Error>>::decode(&mut &result.data[1..])
.unwrap_or_else(|_| panic!("Contract reverted: {:?}", result))
}
fn poll(&self, request: MessageId) -> Result<Option<Status>, Error> {
let result = self.call("poll", request.encode(), 0);
Result::<Option<Status>, Error>::decode(&mut &result.data[1..])
.unwrap_or_else(|_| panic!("Contract reverted: {:?}", result))
}
fn get(&self, request: MessageId) -> Result<Option<Vec<u8>>, Error> {
let result = self.call("get", request.encode(), 0);
Result::<Option<Vec<u8>>, Error>::decode(&mut &result.data[1..])
.unwrap_or_else(|_| panic!("Contract reverted: {:?}", result))
}
fn remove(&self, request: MessageId) -> Result<(), Error> {
let result = self.call("remove", request.encode(), 0);
<Result<(), Error>>::decode(&mut &result.data[1..])
.unwrap_or_else(|_| panic!("Contract reverted: {:?}", result))
}
fn call(&self, function: &str, params: Vec<u8>, value: u128) -> ExecReturnValue {
let function = function_selector(function);
let params = [function, params].concat();
bare_call(self.address.clone(), params, value).expect("should work")
}
fn last_event(&self) -> Vec<u8> {
let events = System::read_events_for_pallet::<pallet_contracts::Event<Runtime>>();
let contract_events = events
.iter()
.filter_map(|event| match event {
pallet_contracts::Event::<Runtime>::ContractEmitted { contract, data, .. }
if contract == &self.address =>
Some(data.as_slice()),
_ => None,
})
.collect::<Vec<&[u8]>>();
contract_events.last().unwrap().to_vec()
}
}
instead, so it then becomes obvious in the tests that one is calling a function on a contract via contract.collection_id

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, I have refactored to bring all helpers method under the Contract struct in 3dd2ad4

@evilrobot-01
Copy link
Collaborator

@chungquantin Please could you kindly rebase again to address the CI issues and then once all green I can review again.

@chungquantin chungquantin requested a review from evilrobot-01 May 19, 2025 23:31
Copy link
Collaborator

@evilrobot-01 evilrobot-01 left a comment

Choose a reason for hiding this comment

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

A few improvement suggestions, along with identification of a few possible flaws.

@@ -10,6 +10,12 @@ pop-api = { path = "../../../pop-api", default-features = false, features = [
"nonfungibles",
] }

[dev-dependencies]
drink = { package = "pop-drink", git = "https://github.com/r0gue-io/pop-drink", branch = "chungquantin/feat-nfts", features = [ "devnet" ] }
Copy link
Collaborator

Choose a reason for hiding this comment

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

PR approved, so once merged this can be updated.

fn new_constructor_works(mut session: Session) {
let _ = env_logger::try_init();
// Deploys a new contract.
let contract = Contract::default(&mut session).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let contract = Contract::default(&mut session).unwrap();
let contract = Contract::new(&mut session, None).unwrap();

Why not new, deploy, or instantiate? Default doesnt imply a deployment action to me, but rather returning a default value of the Contract struct. I get it is a helper, but it doesnt convey the right intent imo.

Would just use the new() function with None instead, only as that most closely matches the contract constructor definition, which is the goal with the Contract struct - a type-safe wrapper for invoking calls on the contract.

Copy link
Collaborator Author

@chungquantin chungquantin May 22, 2025

Choose a reason for hiding this comment

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

Thanks. I have removed the method default, agree that it makes the code readers confusing if we deploy inside the method. I changed it to new as you suggested. Resolved in 3e1b812

// Deploys a new contract.
let contract = Contract::default(&mut session).unwrap();
// Collection exists after the deployment.
assert_eq!(session.sandbox().collection_owner(&COLLECTION), Some(contract.address.clone()));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldnt need to clone, can probably use .collection_owner(&COLLECTION).as_ref()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Resolved in 3e1b812

format!("{:?}", DestroyWitness { item_metadatas: 0, item_configs: 0, attributes: 0 });

// Error returned "Not enough data to fill buffer" due to contract termination.
assert!(session.call::<String, ()>("destroy", &[witness_string], None).is_err());
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could be improved imo. it seems to stem from the fact that the call impl in pop-drink panics, eliminating the possibility of handling this cleanly. This means that every contract will have this messy approach to termination.

The "Not enough data to fill buffer" should not be leaked out beyond pop-drink - perhaps it can be converted to a known error type?

Copy link
Collaborator Author

@chungquantin chungquantin May 22, 2025

Choose a reason for hiding this comment

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

Resolved in dd7ca2e. I investigated and seems like the data returned is empty if the contract is terminated: https://github.com/use-ink/ink/blob/master/integration-tests/public/contract-terminate/lib.rs#L81 which also needs this PR in drink to be merged to support that: r0gue-io/pop-drink#35

// A set of helper methods to test the contract deployment and calls.

// Deploys the contract with `NO_SALT and `INIT_VALUE`.
fn deploy(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems to only be used within Contract::new() now, so why not eliminate this function and just define it there instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agree, I have moved its logic to the new method: 3e1b812

Ok(Self { address: contract })
}

fn default(session: &mut Session<Pop>) -> Result<Self> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would eliminate and just use new(session, None) personally. Its a few extra characters but conveys intent better.

Copy link
Collaborator Author

@chungquantin chungquantin May 22, 2025

Choose a reason for hiding this comment

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

Agree, I have removed the method in 3e1b812

// Deploy a new contract.
fn new(session: &mut Session<Pop>, max_supply: Option<u32>) -> Result<Self> {
let contract = deploy(session, "new", vec![format!("{:?}", max_supply)], NO_SALT)?;
Ok(Self { address: contract })
Copy link
Collaborator

Choose a reason for hiding this comment

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

Address is never used, which indicates a flaw.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

With 76080ba that refactored to use call_with_address, this is now valid.

}

fn collection_id(&self, session: &mut Session<Pop>) -> CollectionId {
call::<Pop, CollectionId, Psp34Error>(session, "collection_id", vec![], None).unwrap()
Copy link
Collaborator

Choose a reason for hiding this comment

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

None of these functions use the contract address, indicating that the pop-drink::call function is flawed in a multi-contract setting. See https://github.com/use-ink/drink/blob/94835ac3124cc02d556be1d9ff9fdf419fc24465/examples/quick-start-with-drink/lib.rs#L169

My assumption is that currently if I deploy two instances via Contract::new(), creating two collections, I will only ever be able to operate on the first collection despite calling methods on the second contract instance.

Copy link
Collaborator Author

@chungquantin chungquantin May 22, 2025

Choose a reason for hiding this comment

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

Resolved in r0gue-io/pop-drink#35 and 76080ba with new call_with_address added.

@chungquantin chungquantin force-pushed the chungquantin/chore-nonfungible_tests branch from 76080ba to 20af3a3 Compare May 22, 2025 07:56
al3mart and others added 9 commits May 22, 2025 19:33
* ci: build runtimes using production profile

* docs(ci): Explain the usage of production profile
* chore: sync with polkadot-stable2503-1

* fix: integration-tests as per sdk#7913

* fix(integration-tests): accomodate for different xcm weights between runtime

* chore(pop-api): update Cargo.lock
* chore: depend on polkadot-stable2503-4 github tag

* chore: revert subxt version

* chore: update sc-network dependency

* chore(pop-api): update Cargo.lock
* chore(testnet): bump versions to v0.5.3

* chore: point drink to stable2503-4
@chungquantin chungquantin requested a review from evilrobot-01 May 22, 2025 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants