-
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 RFC] - Research desired functionality to expose #233
Comments
Tokens RFCOverviewCustom tokens are supported at a protocol level, and we wish to bring that functionality to SnarkyJS. Custom tokens are denoted by a token identifier which is used to distinguish between different tokens and a token account. A token account is similar to a regular account, but its balance is stored in the chosen custom token. One may use the same public key for multiple token accounts for different custom tokens. In addition to token accounts, there is a notion of a token owner account. The token owner account is the owner of a custom token and supervises the behavior of accounts whose balance is stored in that custom token. They are the only account that can provide certain invariants on a custom token before the supervised accounts are updated. The token owner must be included in the Party's transaction layout of a custom token transaction for the update to be authorized. Token owner accounts are created by specifying a public key and a token identifier. For the purposes of most token owner accounts, using the default, MINA token identifier can be used to derive a new custom token identifier. In this RFC, we propose a bare-bones API for SnarkyJS to support creating token accounts and transacting in custom tokens. Note: Tokens cannot be used to pay for fees or be used in stake delegations. Creating Token AccountsGiven an existing account, a new token account is created by simply receiving a payment denoted in a custom token. When such a transaction occurs, the protocol will create a token account for the public key receiving the new custom token. When one wants to create a new custom token, it must be derived from an existing account and an existing token identifier. If an existing public key is specified in deriving a new custom token, that public key will be the token owner. The native MINA token identifier can be used when setting an existing token identifier. In this model, developers will want their zkApp to be the token owner of their custom token to automate any authorization for other users to interact. To derive a new custom token identifier from a public key and an existing token identifier, we can expose a function that will accept a public key and token identifier and spit out a new custom token identifier. This new custom token identifier can be used by the specified public key to mint new tokens. import { createNewTokenId } from "snarkyjs";
// Create a new token identifier
const newToken = createNewTokenId({
owner: PrivateKey.random().toPublicKey(),
tokenId: Token.defaultTokenId, // This can be optional
});
// New token will be encoded as a base58 field element
console.log(newToken); One thing to note about Transferring tokensTo support an easy way for zkApps to send transactions to other accounts, we should expose a function that abstracts the details of creating a Parties transaction manually for balance changes. Such a function could look like this: class TransferExample extends SmartContract {
...
@method sendTokens(recieverAddress: PublicKey) {
// Create a Party structure for the receiever
let receiverParty = Party.createUnsigned(recieverAddress);
// Inside a zkApp smart contract method, send to the 'receiverAddress'
this.transfer({
to: receiverParty,
amount: 100_000,
})
}
}
...
let zkapp = new TransferExample(zkappAddress);
// Create a transaction specifying a transfer
txn = await Local.transaction(feePayer, () => {
zkapp.sendTokens(privilegedKey);
zkapp.sign(zkappKey);
});
await tx.send(); This Minting/Burning TokensThe token owner account has permission to mint/burn from any account that holds a balance for its custom token. Given that the token owner must be the one to authorize such transactions, the token owner must be specified in the Party's structure when it's sent to the protocol layer. To enable minting/burning, we can simply reuse the // ... code as before
let customToken = createNewTokenId({
address: this.address,
tokenId: defaultMinaTokenId(),
});
// This will mint 100,000 tokens to the receiverParty address
this.transfer({
to: receiverParty,
amount: 100_000, // To burn, we simply negate this value
tokenId: customToken, // Will default to the default MINA ID if not used
}); This Note: If the token owner zkApp attempts to burn more tokens than what the balance is, the transaction will fail. Let's assume that account @method sendCustomTokens(senderAddress: PublicKey, recieverAddress: PublicKey) {
let senderParty = Party.createUnsigned(senderAddress);
let receiverParty = Party.createUnsigned(recieverAddress);
// Derive a custom token ID that is owned by this operating zkApp
let customToken = createNewTokenId({
address: this.address,
tokenId: defaultMinaTokenId()
})
this.transfer({
to: receiverParty,
from: senderParty,
amount: 100_000,
tokenId: customToken
})
} AssertingOne can ensure certain preconditions on their custom token transfers by adding @method mintIfNonceIsTen(recieverAddress: PublicKey) {
let receiverParty = Party.createUnsigned(recieverAddress);
receiverParty.account.nonce.assertEquals(new UInt32(10));
receiverParty.body.incrementNonce = Bool(true);
let customToken = createNewTokenId({
address: this.address,
tokenID: defaultMinaTokenId()
})
this.transfer({
to: receiverParty,
amount: 100_000,
tokenId: customToken
})
} This sort of approach is easily extensible to whatever type of functionality you want to enable for your custom token transfers. Open Questions
TODO List[] - Implement |
Nit: free functions are too hard to discover via intellisense we should export something more discoverable this is great! We should include a discussion around which parts of erc20 and erc777 we are explicitly supporting or choosing not to support and why. From a skim, i think we should build an abstraction that fires relevant erc777 events automatically for you and maybe have this on by default to maximize interoperability for clients |
Updated APIAfter some internal discussions, we have decided we want to compare/contrast different styles of APIs to understand what would be most ergonomic for users of SnarkyJS. Therefore, the core feature set is unchanged, but instead, we are proposing a different "wrapper" to interact with tokens. Tokens namespace in SnarkyJSThere was a discussion about creating a namespace for tokens inside SnarkyJS to abstract out some of these functions so that they are not "free functions" by themselves. In addition, putting tokens inside a namespace inside SnarkyJS will allow for a better IntelliSense/discoverability experience for zkApp developers, as "free functions" will be somewhat hard to find with IntelliSense. The following code shows the potential differences: // Old proposal
import { createNewTokenId, getOwner, getAccounts, getBalance } from "snarkyjs";
const newToken = createNewTokenId({...});
const owner = getOwner(newToken); // Gets the account ID that owns the specified token
const accounts = getAccounts(newToken); // Find all accounts for a specified token
const balance = getBalance({
address,
tokenId,
}); // Get balance of a specified account denoted in the custom token ID
// New Proposal
import { Token } from "snarkyjs";
const newToken = Token.createNewTokenId({});
// Extra methods to be added in the namespace, maybe this makes sense to live in the Ledger namespace instead?
const owner = Token.getOwner(newToken); // Gets the account ID that owns the specified token
const accounts = Token.getAccounts(newToken); // Find all accounts for a specified token
const balance = Token.getBalance({
address,
tokenId,
}); // Get balance of a specified account denoted in the custom token ID With the new proposal, users can type Another option is instead of using functions, we adopt a class-based approach for using tokens. The following code shows potential differences: import { Token } from "snarkyjs";
const newToken = new Token({...});
const id = newToken.id;
const ownerAddress = newToken.owner;
// The following methods should be static because different token ids and addresses should be allowed to query
const owner = Token.getOwner(newToken);
const accounts = Token.getAccounts(newToken);
const balance = Token.getBalance({
address,
tokenId,
}); Splitting up
|
Very nice! Some notes:
I think
I don't think we should be able to pass in a token id here, and it also shouldn't default to the MINA token. That's confusing if Also, I think this method should require the To send MINA, I think we should have the following API, separate from the token namespace: this.send({to: someAdress, amount: 10e9 }); // send MINA from this SmartContract
let party = Party.createSigned(privateKey); // create party from a private key
party.send({ to: someAdress, amount: 10e9 }); // send MINA from the party's account the with the Party.createSigned(privateKey).send({ to: this.address, amount: 10e9 }) a shortcut for that could be: this.receive({ from: privateKey, amount: 10e9 }); |
Got it! So we will have two API calls here. // Sends MINA
this.send({to: someAdress, amount: 10e9 });
// Sends custom token derived from `this.address` and the MINA token identifier
this.transfer({
to: someAddress,
from: this.address,
amount: 10e9
}) Did I get this correct? In addition to |
yes, except that it should be I don't have strong feelings about what to call |
I like where this has come / is going!
I agree I don't have a strong opinion either way, just want to be sure we've recognize this as a potential failure case and are ok with it.
+1 // send MINA
this.send({to: someAdress, amount: 10e9 });
// Sends custom token derived from `this.address` and the MINA token identifier
this.token().send({
// from: this.address, // make default, if not provided?
to: someAddress,
amount: 10e9
});
|
I think it's too unintuitive to default I agree with Jason, shouldn't we stick to the same name? (transfer is best I think) |
I'm fine to start with it being required too. It's easy to make something optional later, harder to go from optional to required. |
TestingTo ensure that the SnarkyJS implementation of tokens is sound, there should some sort of testing story that exists. Currently, there is no integration infrastructure in the CI workflow, so we will ignore that part of the testing story for now. This leaves us with unit tests to ensure that the tokens implementation is working correctly. We can add a new test file in the SnarkyJS repo which holds all the unit tests needed for tokens. The unit tests should confirm the following behaviors:
These unit tests can be run when the main branch is updated in the SnarkyJS repo. This will also ensure that any new changes to the SnarkyJS repo will behave up to spec. |
Implemented |
Description
Before implementing token functionality in SnarkyJS, it is important that we align on what we intended to expose and how it works. Some pre-homework to this task is to do a quick survey of existing token implementations in other chains. Following that, this issue has been broken into 3 steps.
The text was updated successfully, but these errors were encountered: