-
Notifications
You must be signed in to change notification settings - Fork 122
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
Tokens #273
Tokens #273
Conversation
065f60b
to
a714246
Compare
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.
started review, will continue tomorrow!
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.
Great work Martin! I just had a couple very minor comments/questions.
4b9b9f7
to
0b6be23
Compare
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.
Finished reviewing! Just a couple of smaller suggestions from me -- great work!
The most important change I'm suggesting is to remove the tokenId argument in token(tokenId?)
, because it has to be the same as this.tokenId
anyway. (Correct me if I'm wrong.)
src/lib/party.ts
Outdated
@@ -516,8 +577,105 @@ class Party { | |||
return new (Party as any)(body, authorization, party.isSelf); | |||
} | |||
|
|||
token(tokenId?: Field) { |
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 rename this argument to parentTokenId
for clarity?
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.
oh wait -- doesn't the parentTokenId
have to equal this.self.body.tokenId
? Otherwise the this
account isn't the owner and can't do this? maybe we don't need that argument after all??
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.
This comment is a little confusing to me, I'll have to come back to it :P
e9dd4e1
to
fe55f6e
Compare
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.
lgtm now!
b9919ab
to
39281e7
Compare
it('should have a valid custom token id', async () => { | ||
const tokenId = zkapp.token().id; | ||
const expectedTokenId = new Token({ tokenOwner: zkappAddress }).id; | ||
expect(tokenId).toBeDefined(); |
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 delete this line expect(tokenId).toBeDefined();
given the following assertion, no? Or is there a chance it returns undefined?
|
||
@method init() { | ||
this.tokenSymbol.set(tokenSymbol); | ||
this.totalAmountInCirculation.set(UInt64.zero); |
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 think we should consider using the term Quantity, instead of Amount, everywhere. On trading dashboards, qty refers to the number of tokens and amount prefers to price * amount
, which would be USD etc. when trading on fiat markets. So qty is more consistent with how it's used on most financial platforms.
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 also typically see (and we use consistently in the backend code) the term "totalSupply" for this -- probably for consistency we should go with that.
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.
This looks great! I just have a few questions that either you can just explain away in comments or perhaps I found a bug
src/lib/fetch.ts
Outdated
const accountQuery = (publicKey: string) => `{ | ||
account(publicKey: "${publicKey}") { | ||
const accountQuery = (publicKey: string, tokenId: string) => `{ | ||
account(publicKey: "${publicKey}" tokenId: "${tokenId}") { |
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.
don't you need a comma between the publicKey and tokenId in the query?
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.
Good catch!
src/lib/mina.ts
Outdated
if (ledgerAccount == undefined) { | ||
throw new Error( | ||
`getAccount: Could not find account for public key ${publicKey.toBase58()}` | ||
`getAccount: Could not find account for public key ${publicKey.toBase58()} with the tokenId ${Ledger.fieldToBase58( |
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.
can you omit the with the tokenId ...
in the case that the tokenId is the default one?
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.
Good call, fixed!
src/lib/mina.ts
Outdated
if (account !== undefined) return account; | ||
} | ||
throw Error( | ||
`getAccount: Could not find account for public key ${publicKey.toBase58()}.\nGraphql endpoint: ${graphqlEndpoint}` | ||
`getAccount: Could not find account for public key ${publicKey.toBase58()} with the tokenId ${Ledger.fieldToBase58( |
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.
same here
src/lib/mina.ts
Outdated
Fetch.defaultGraphqlEndpoint | ||
); | ||
if (account === undefined) | ||
throw Error( | ||
`getAccount: Could not find account for public key ${publicKey.toBase58()}. Either call Mina.setActiveInstance first or explicitly add the account with addCachedAccount` | ||
`getAccount: Could not find account for public key ${publicKey.toBase58()} with the tokenId ${Ledger.fieldToBase58( |
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.
same here (should this string be a constant stored somewhere instead of inlined all the time?)
src/lib/party.ts
Outdated
tokenOwner.toFields().every((x) => x.isConstant()) && | ||
parentTokenId.isConstant() | ||
) { | ||
this.id = Ledger.customTokenIdChecked(tokenOwner, this.parentTokenId); |
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.
What's the difference between these branches?
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.
hey @MartinMinkov, I think these branches are wrong.. the else branch should use the checked version, not the if branch
@bkase, if the token owner or parent token id is a variable then we need to derive the token id in-snark. (Not sure if we could always use the checked version, depends on how flexible it is written, but IIRC checked hashing returns a new variable even when the inputs are constant, which wouldn't work outside the circuit)
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.
Yikes! Good catch, switched these. 😅
receiverParty.body.balanceChange = | ||
receiverParty.body.balanceChange.sub(amount); | ||
|
||
// Require signature from the receiver account being deducted |
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 this always true? Can't certain tokens be burned via proof?
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.
Good question, I'm not sure how this would work with the current SnarkyJS API. @mitschabaude, is there a way to require either a proof or signature? Also, how would one construct a proof to do this (in the context of a developer creating a proof with SnarkyJS)?
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.
Aaah yes. I forgot about that. It depends on the permissions on the sending party's account whether it needs a proof or a signature.
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.
This is a puzzle 🤔 because different zkApps have completely different ways of authorizing transactions. So my fear is token contracts will typically impose a certain interface on zkApps for that token.
I'm pretty sure @mrmr1993 has thought deeper about this
In any case, I think there's no easy solution here. The token()
API we designed seems fundamentally not flexible enough to handle parties with proofs, because they are all different. The API must be one where users provide their own parties, not where we create parties for them. But even then it seems very tricky to make a circuit for the token owner contract that's flexible enough to handle arbitrary zkApp children. So I'm not even sure we can ever provide a ready-made, general implementation for parties with proofs.
That makes it all the more important to at least expose the low-level pieces to create token contracts -- I think that's the Token
class (which I still think should be just a function)
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.
Maybe a fairly general solution is not that hard to build after all.
The token() functions must witness their child parties instead of hardcoding them into the circuit, and only assert the properties of them that they care about - balanceChanges and tokenId.
The challenge is how to create a good API for this. I imagine its functions must take a party as input, which might mean we have to make Party one of the possible arguments of a smart contract method. A smart contract could sort of "call" a token contract with 'this' as an argument, and that token contract would make the smart contract its child. A sort of inverted zkapp composability
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 guess the natural API from the protocol perspective would be if the token contract methods would accept a callback, which is like a transaction block in that you can run a (or possibly multiple) smart contract methods in it, which would create parties that are authorized with a proof. And the token contract would then assert certain conditions on the resulting parties, for example
- They can't have children
- There must be exactly / at most X of them
- The balances must cancel in send
- In mint / burn, conditions X, Y, Z must hold
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 created a separate issue (#300) so that we can discuss the priority & path forward on this. It's out of scope for this PR, it's already nice to have token transfers authorized by a signature
senderParty.body.balanceChange = | ||
senderParty.body.balanceChange.sub(amount); | ||
|
||
// Require signature from the sender party |
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.
same here -- aren't there certain senders that need a proof instead?
|
||
@method init() { | ||
this.tokenSymbol.set(tokenSymbol); | ||
this.totalAmountInCirculation.set(UInt64.zero); |
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 also typically see (and we use consistently in the backend code) the term "totalSupply" for this -- probably for consistency we should go with that.
Description
This PR adds support for custom tokens and token symbols for SnarkyJS. It implements the requirements mentioned in the following RFC: #233
Mina PR: MinaProtocol/mina#11449
To enable minting, burning, and sending, changes to the Party structure was made to support
this.token()...
as well asthis.tokenSymbol.set(...)
. Additionally, changes to account fetching have been made to support querying for a specific token id in addition to a public key.Tested
Implemented unit tests in
token.test.ts
to check different behaviors that tokens offer. These tests are only related to the local ledger and thus are not tested with a live network yet. To test with a live network, a new QA network should be deployed and then a zkApp deployed that acts similar to the zkApp in the test file.