-
Notifications
You must be signed in to change notification settings - Fork 7
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
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 <[email protected]>
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 ```
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 <[email protected]>
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
IsSep39Metdata
subcontract with aMetadata
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 adeserialize_metadata
fn that transforms theBytes
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.
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
admin
attribute on any subcontract other than theCore
/Admin
one - add a
require_auth
function toContract
- while implementing other subcontracts, I can then
use crate::Contract
and 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
} | ||
|
||
#[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