-
Notifications
You must be signed in to change notification settings - Fork 7
Feat/nft example #91
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?
Feat/nft example #91
Conversation
12a5817 to
5d3371e
Compare
9c8b0e8 to
8ee77f3
Compare
75bccaa to
2a281d5
Compare
Apply suggestions from code review Co-authored-by: Willem Wyndham <willem@wyndham.tech>
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
```
chadoh
left a comment
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.
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.
…m-sdk Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com>
2070f87 to
ccaa9ae
Compare
Looks great, thanks for pushing those changes up!
I agree! I can get on testing today. |
a268b67 to
d3acb1a
Compare
4a997a0 to
cc22d8b
Compare
| 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>, |
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.
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.
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.
I assumed that this was an intentional part of the spec, to allow rapid ecosystem experimentation and de facto standards to arise.
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.
Still annoying. You need to know how to deserialize the bytes...
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.
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? 🤔
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.
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.
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.
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.
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.
Or IsSep39WithTypedMetadata, or IsTypedMetadata, or... ¯\_(ツ)_/¯
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.
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
IsSep39Metdatasubcontract with aMetadataassociated 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
IsTypedMetadatainclude adeserialize_metadatafn that transforms theBytesmetadata 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.
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.
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,
}
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.
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())) |
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.
Wait, we are setting the admin to the contract itself? Doesn't that brick it?
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.
Why even bother implementing 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.
Is it something to do with the &mut self conversation above?
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.
Right now Default is required by the macro
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.
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(); |
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.
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?
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.
Having two different kinds of admin—"the one that updates the contract source" vs "the one that mints tokens"—doesn't seem useful?
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.
@elizabethengelman here's how @willemneal helped me implement this in EquitX, for now:
- no
adminattribute on any subcontract other than theCore/Adminone - add a
require_authfunction toContract - while implementing other subcontracts, I can then
use crate::Contractand callContract::require_auth()
While the dependencies are sort of circular, this allows one subcontract to make calls into another.
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.
awesome, thanks so much!
and use Contract::require_admin for authing protected fns
Co-authored-by: Willem Wyndham <willem@wyndham.tech>
| } | ||
|
|
||
| #[test] | ||
| fn test_nft() { |
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.
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"] |
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.
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
loam/crates/loam-soroban-sdk/src/lib.rs
Line 25 in bc4cf88
| /// 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.
Deployed contract: https://stellar.expert/explorer/testnet/contract/CDLCRZ6ICRW5KB7RFKKJEJ57JQHXLRS2MIBDZ6B4LAZGLKY2WLAJYYLO