Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat/nft example #91

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

Conversation

elizabethengelman
Copy link
Contributor

@elizabethengelman elizabethengelman commented Jun 27, 2024

@elizabethengelman elizabethengelman marked this pull request as ready for review June 27, 2024 22:07
@elizabethengelman elizabethengelman force-pushed the feat/nft-example branch 2 times, most recently from 12a5817 to 5d3371e Compare June 28, 2024 16:12
@elizabethengelman elizabethengelman force-pushed the feat/nft-example branch 3 times, most recently from 9c8b0e8 to 8ee77f3 Compare July 2, 2024 16:03
@elizabethengelman elizabethengelman force-pushed the feat/nft-example branch 2 times, most recently from 75bccaa to 2a281d5 Compare July 3, 2024 15:44
chadoh added 2 commits July 15, 2024 14:19
Now when I run this:

```
just build && \
    stellar contract deploy --wasm target/loam/example_nft.wasm --source alice --network local --alias nft && \
    stellar contract invoke --id nft -- --help
```

I get this output:

```
Commands:
  admin_get                Get current admin
  admin_set                Transfer to new admin
                               Should be called in the same transaction as deploying the contract to ensure that
                               a different account try to become admin
  redeploy                 Admin can redepoly the contract with given hash.
  mint                     Mint a new NFT with the given ID, owner, and metadata
  transfer                 Transfer an NFT with the given ID from current_owner to new_owner
  get_nft                  Get the NFT with the given ID
  get_owner                Find the owner of the NFT with the given ID
  get_total_count          Get the total count of NFTs
  get_collection_by_owner  Get all of the NFTs owned by the given address
  nft_init                 Initialize the NFT contract with the given admin and name
```
Copy link
Contributor

@chadoh chadoh left a comment

Choose a reason for hiding this comment

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

So clean! I confess I found the Map<u32, ()> pretty surprising; glad I found the comment on it in the previous discussion (it's the same as a Set!).

I pushed up a couple changes, to update the Cargo.lock as happens when you run just build now. And I also deployed the contract locally to check the help info and found that it was absent, so I fixed that. Check my commit messages for more info.

Should we add some tests? We could test that the docs I checked manually. Maybe we could also test some common NFT scenarios. We should assume that people will (blindly) copy-paste this into their own project, so we should make sure it works correctly.

crates/loam-cli/examples/soroban/nft/Cargo.toml Outdated Show resolved Hide resolved
crates/loam-cli/examples/soroban/nft/src/nft.rs Outdated Show resolved Hide resolved
crates/loam-cli/examples/soroban/nft/src/nft.rs Outdated Show resolved Hide resolved
crates/loam-cli/examples/soroban/nft/src/nft.rs Outdated Show resolved Hide resolved
@elizabethengelman
Copy link
Contributor Author

I pushed up a couple changes, to update the Cargo.lock as happens when you run just build now. And I also deployed the contract locally to check the help info and found that it was absent, so I fixed that. Check my commit messages for more info.

Looks great, thanks for pushing those changes up!

Should we add some tests? We could test that the docs I checked manually. Maybe we could also test some common NFT scenarios. We should assume that people will (blindly) copy-paste this into their own project, so we should make sure it works correctly.

I agree! I can get on testing today.

total_count: u128,
owners_to_nft_ids: Map<Address, Map<u128, ()>>, // the owner's collection
nft_ids_to_owners: Map<u128, Address>,
nft_ids_to_metadata: Map<u128, Bytes>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm it seems annoying that the metadata is untyped. Would be nice for consumers of the contract to know what the shape of the metadata is. But if is the current standard it's fine, but it's something we should push on to get agreement.

Copy link
Contributor

Choose a reason for hiding this comment

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

I assumed that this was an intentional part of the spec, to allow rapid ecosystem experimentation and de facto standards to arise.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Still annoying. You need to know how to deserialize the bytes...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's a great point. From what I can tell from SEP-0039 best practices are to include a few key fields in the metadata: name, description, URL, issue, and code.

What if we start with those fields in a Metadata struct, and then if/when a standard emerges we can update the type? Not sure how difficult it will be to upgrade subcontracts, though with the ability to redeploy, that seems doable.

pub struct Metadata {
    name: Bytes,
    description: Bytes,
    url: Bytes,
    issuer: Address,
    code: Bytes,
}

alternatively we could have them all be optional fields? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

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

Personally I think the first three should be required. And Perhaps we use String?. It's again annoying because the all seem like that is what they will be. So perhaps we just put this forward as our standard. Other contracts will convert from Bytes to string anyway so we are just skipping a step.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could we implement it as two separate subcontracts, IsSep39 and IsSep39Extension, or something like that? Implementing IsSep39Extension would override all instances of metadata from Bytes to this suggested Metadata struct.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or IsSep39WithTypedMetadata, or IsTypedMetadata, or... ¯\_(ツ)_/¯

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Finally getting back to addressing this.

I like the idea of creating an IsSep39Metadata subcontract, but I'm unsure how to implement it.

  • One way I tried was to have an IsSep39Metdata subcontract with a Metadata associated type:
impl IsTypedMetadata for MyNonFungibleToken {
    type Metadata = MyNonFungibleTokenMetadata;
}

The issue I ran into with this is that our derive_contract doesn't generate the associate type over to the generated trait TypedMetadata. It just adds the Impl generated type. I'm not sure if this is something we actually want to add to the macro, or if there is a better way to do this, but I moved on and tried a different idea...

  • The other idea was to have IsTypedMetadata include a deserialize_metadata fn that transforms the Bytes metadata into the MyNonFungibleTokenMetadata type. But that is still annoying because the user needs to serialize the metadata into Bytes when minting, etc.

I'll keep thinking on this, but I could probably use another set of eyes on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, for now I've just added a Metadata type like the following, maybe we can move forward with that and then continue to improve the standard/make sure that the Metadata can be flexible going forward.

pub struct Metadata {
    pub(crate) name: String,
    pub(crate) description: String,
    pub(crate) url: String,
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

With #[contracttype] the name of the type exported, so perhaps we could just called it Sep39Metadata?


impl Default for MyNonFungibleToken {
fn default() -> Self {
MyNonFungibleToken::new(env().current_contract_address(), Bytes::new(env()))
Copy link
Contributor

@chadoh chadoh Jul 18, 2024

Choose a reason for hiding this comment

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

Wait, we are setting the admin to the contract itself? Doesn't that brick it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Why even bother implementing Default?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it something to do with the &mut self conversation above?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right now Default is required by the macro

Copy link
Contributor

Choose a reason for hiding this comment

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

it's a nonsensical default. i don't like it. Will this fix it?


impl IsInitable for MyNonFungibleToken {
fn nft_init(&self, admin: Address, name: Bytes) {
self.admin.require_auth();
Copy link
Contributor

Choose a reason for hiding this comment

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

Wait, how is self.admin already set? And why allow admin to be passed in here, if it's already set? Should admin even be part of the nft_init params?

Copy link
Contributor

Choose a reason for hiding this comment

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

Having two different kinds of admin—"the one that updates the contract source" vs "the one that mints tokens"—doesn't seem useful?

Copy link
Contributor

Choose a reason for hiding this comment

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

@elizabethengelman here's how @willemneal helped me implement this in EquitX, for now:

While the dependencies are sort of circular, this allows one subcontract to make calls into another.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

awesome, thanks so much!

}

#[test]
fn test_nft() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

While testing with Env, I had trouble getting the tests to pass while running in parallel. ChatGpt told me that the errors could be caused by a "context already borrowed" issue. It kind of feels like the env isn't being reset in between tests, even though I am presumably creating a new env for each test.

These tests do pass when I run them with just one thread:

cargo test --package example-nft --lib -- test --test-threads=1

@willemneal have you experienced anything like this while testing with env?


#[test]
#[should_panic]
#[ignore = "This should panic, but it's not"]
Copy link
Contributor Author

@elizabethengelman elizabethengelman Jul 26, 2024

Choose a reason for hiding this comment

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

I expect that these two tests should panic because we are trying to set/get storage state before the contract is initialized. And I though that it would panic because of this

/// This function will panic if the environment has not been initialized.

I could use another set of eyes on this in case I'm missing something.

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.

3 participants