diff --git a/generate-keys.js b/generate-keys.js new file mode 100755 index 0000000000..ef1a4d7b9b --- /dev/null +++ b/generate-keys.js @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import Client from './dist/node/mina-signer/MinaSigner.js'; + +let client = new Client({ network: 'testnet' }); + +console.log(client.genKeys()); diff --git a/src/examples/regression_test.json b/src/examples/regression_test.json index b99fd64e1c..decfe554a7 100644 --- a/src/examples/regression_test.json +++ b/src/examples/regression_test.json @@ -1,8 +1,8 @@ { "Voting_": { "verificationKey": { - "data": "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", - "hash": "12792560556693539606735923311811117433165369543125266619542306386624586253107" + "data": "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", + "hash": "19310027642437930290458048212755059674159518924616933538143563269561536287877" } }, "Membership_": { @@ -19,14 +19,14 @@ }, "TokenContract": { "verificationKey": { - "data": "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", - "hash": "25233369077755956166656912091919770106000926128666369624140027897897542472859" + "data": "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", + "hash": "1805618078121285332343482592243061319759675662039725571222151161035831907635" } }, "Dex": { "verificationKey": { - "data": "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", - "hash": "11656004533948220170811542827386442823924717815066547341670031403039013683568" + "data": "AAB/NGDe0JYaUYfM5WHeBCkGQYC3FrfjJLGmqLeijiHXAcIuBsafC4UZm8c9DLHcYhWfj07F5y+A6A1eRAhHrVM49UTYdICY860XcUD5ZE8OcZiYJytGcYa6jqow+aqxGBrySmO2Ju5Cr4HnVITFHjzpxcm4cnUwhhkpdsaohkjqBeLkjHRv5ofeGQIHG6X/Y3vE1cjd4iNHecTvwsSyHb891vIaRNmj77BeKoqLDnX8cAhnUEKbfb0uUzvqGSSnnQE5E5UJqJYjMI4pYSUOYcExsOGdXhHUEACizSFvZwyCL3Mt1XAhgdkOfFs9x/wI9Un/ZAt1TQGdgrS05Sg8N00I8zA3UfkVn47jbVVb6gLPqTCZvkTdpDEvU9fMsZimUDtnse6U3vMWvsN1AoMcIJwRTQkoCw14x29I9N2o1B7qIQoXCFe6+5UOXo4HDl/rufzVHwg7UZBfpNiTc/xB45IIlvHSi14lzZS/kv1zJS3jvuRCwTN6bLLyMNqqvc6/uRb5vO38aMWbWcWtBR7WH+GqXEm+9/c/WO0nHYIY74SnD49jixkUh4Ba9RC7dvbBCNtYwG5OwDds5oIOOvqM39UqAGSWB03B4qh2Zbp9e5gmH1Lf26e6dhKipcYbVYJotNsM06UeUvi1gPYr9nk3BQHIOzHO6Qh7RrMW5EQODch2Zg/dckTeGOcjJn+lFwPadIRUykL/9aQb3SS5qx8bLny0LsWunWyaFIQ/bGQtiI/e/KlHszmFroiG+yw9Bs4uvj8DA91U189BybK3rcC/2tvxbRhsBKw3tAqCB4SLjJQjxRNroY4ImKEf18vqUK+6EkACrC9rQ6bH40nUmCBZ8YW8MmRaVa1l/UZefoo26g2ju7zi/Eef4zjcJNURDXRdPJ8jDuzX1sEzAh3QW5acY/1qgRpB4WAg707Ith92D698YgGEKx4VtrCQovlaMvqeJzmYvy5M+oKUI9KRBTQYR7mVH68ofIloPbKoqXvMDzEEnhR7uR67GdyR42Ok8GgchsQYeKmEmxaj8n+bzF13KT+0zqoc3d9YTh6oabTQEMKRlRkZ7C4ZlEkm2hrwsHLhC5gWxNPaKKZnuVsa85zPIWQeL9vmbTYEBObiK+Kp0Nl1HBJa4BjzgF3SEFvIeDjI4i4EzApKHs9eeMXH/GbBXEqxtOk7rXzi+aj37Htv75q0LzeKltOcwDtGh5JIbs3SuS86isOqVtCDY04/Ckoa0yM3OaL5A+sVvI+7qJDSMOjY3Lug6g0H5m93+af4tMSACxwnET1VxQBEGySS9SvL59zxB+AEcjyB40HuL+mcqyF41wxMfSlFYysZFqS5t/STD0eT2N5mmZtn1rf/ZoIFJ19NNunsMr79tAL+rmynQZePSiYR0eUwiNrkYNig9jrL/cEoIcVQUzgxY3QbVrJuhcSk6LoOjZBack31iN0KP/JEOhzZyH0eJ7eG39fwK1gAGCHvUDuLPw97wix4o5f4nvqqLJAlbbCT1ZqE3zcbd/JCY3vmGbDJDw5Rsm0UrA/Qb+ggchb8SScWoK6+dNDByqgANu+ybjiX6H+lh/DivkRotjE2bSNs/M0A2wplSdJQM9Lne7PK1rxzJrX21GEr417FMe8G2R4OH3q9p64Ls2OyJNVSf+V/RVNGkpH3x2b2smQfe+cu2H9D6dlP91mlFTuS1D+1iDhJtGQDaxsYJhguvjEqzn8AkS2iaF6PDvLHNsUoZIV4gaap7VN//V2qFlN9OrVt0wi96i5eRNdLpQVXxUR0+oVdaXu7tV94EyEl7y0M5/YobR6MuhLbfu7cChP8PXZM9ea5eGtT67g19d5oXT2jxdP0gE2hMri+z4DePlNb0L5VQ0YwAFishEZ7o7oZKACGZq8OLuw1vchKwjAAUtIv7rNVHsAX0/SXK2DUoMdSHq+HqojKdP0uHmjT8o8DdzhmapkMbE4jONSwIaYm/Xcq6O275kwuofUT3oKbczGJydZw8/R/RuZjk0t5RHmr8TIDqQwHS37bZ5Jmij+g7aaPu65s3iuVYxDifrYihjwNLJEfHJAm1I+4PvELNypIFLiS/I4UvdDCnhvzkr1rj90zkG+GCy1rmsPsNPl1/P4QjEVDifpGPIBvESVh30xHwQCSdkq+wxzkZPXleTeo89oGQIuCuaaSS3lN1aNyExdDOLCbz2WWoc9bm3gSHMDZlswx+MdKqqpPjGeG4lteR+8i3VY2ff5BKVtelspWW0ABRjsWHkwv5tZGtEKKMN0akzA58Z5H03oIvq0z3rXYxSlcKje5GMoMqEpQAiFRONzuHFyVxONKKS6ZkLuIKgI8+Sbe2sQkUNB63EVhQzdkaDsa3R1YgiFPMuIeAPPy3NhvfX2CxSlTbRx6xneOCfqh0iY=", + "hash": "19435468715062623397587806151775069885208933390199296510404720462609868425905" } } } \ No newline at end of file diff --git a/src/examples/zkapps/dex/arbitrary_token_interaction.ts b/src/examples/zkapps/dex/arbitrary_token_interaction.ts index 139dc4c560..f4d92a17bd 100644 --- a/src/examples/zkapps/dex/arbitrary_token_interaction.ts +++ b/src/examples/zkapps/dex/arbitrary_token_interaction.ts @@ -4,7 +4,7 @@ import { AccountUpdate, UInt64, shutdown, - Token, + TokenId, } from 'snarkyjs'; import { TokenContract, addresses, keys, tokenIds } from './dex.js'; @@ -22,7 +22,7 @@ console.log('-------------------------------------------------'); console.log('TOKEN X ADDRESS\t', addresses.tokenX.toBase58()); console.log('USER ADDRESS\t', userAddress.toBase58()); console.log('-------------------------------------------------'); -console.log('TOKEN X ID\t', Token.Id.toBase58(tokenIds.X)); +console.log('TOKEN X ID\t', TokenId.toBase58(tokenIds.X)); console.log('-------------------------------------------------'); // compile & deploy all 5 zkApps diff --git a/src/examples/zkapps/dex/dex-with-actions.ts b/src/examples/zkapps/dex/dex-with-actions.ts new file mode 100644 index 0000000000..232a1fe4ef --- /dev/null +++ b/src/examples/zkapps/dex/dex-with-actions.ts @@ -0,0 +1,375 @@ +/** + * This DEX implementation differs from ./dex.ts in two ways: + * - More minimal & realistic; stuff designed only for testing protocol features was removed + * - Uses an async pattern with actions that lets users claim funds later and reduces account updates + */ +import { + Account, + Circuit, + method, + AccountUpdate, + PublicKey, + SmartContract, + UInt64, + Struct, + State, + state, + TokenId, + Reducer, + Field, + Permissions, + isReady, + Mina, + InferProvable, +} from 'snarkyjs'; + +import { TokenContract, randomAccounts } from './dex.js'; + +export { Dex, DexTokenHolder, addresses, keys, tokenIds, getTokenBalances }; + +class RedeemAction extends Struct({ address: PublicKey, dl: UInt64 }) {} + +class Dex extends SmartContract { + // addresses of token contracts are constants + tokenX = addresses.tokenX; + tokenY = addresses.tokenY; + + /** + * state that keeps track of total lqXY supply -- this is needed to calculate what to return when redeeming liquidity + * + * total supply is initially zero; it increases when supplying liquidity and decreases when redeeming it + */ + @state(UInt64) totalSupply = State(); + + /** + * redeeming liquidity is a 2-step process leveraging actions, to get past the account update limit + */ + reducer = Reducer({ actionType: RedeemAction }); + + events = { + 'supply-liquidity': Struct({ address: PublicKey, dx: UInt64, dy: UInt64 }), + 'redeem-liquidity': Struct({ address: PublicKey, dl: UInt64 }), + }; + // better-typed wrapper for `this.emitEvent()`. TODO: remove after fixing event typing + get typedEvents() { + return getTypedEvents(this); + } + + /** + * Initialization. _All_ permissions are set to impossible except the explicitly required permissions. + */ + init() { + super.init(); + let proof = Permissions.proof(); + this.account.permissions.set({ + ...Permissions.allImpossible(), + access: proof, + editState: proof, + editActionState: proof, + send: proof, + }); + } + + @method createAccount() { + this.token.mint({ address: this.sender, amount: UInt64.from(0) }); + } + + /** + * Mint liquidity tokens in exchange for X and Y tokens + * @param dx input amount of X tokens + * @param dy input amount of Y tokens + * @return output amount of lqXY tokens + * + * This function fails if the X and Y token amounts don't match the current X/Y ratio in the pool. + * This can also be used if the pool is empty. In that case, there is no check on X/Y; + * instead, the input X and Y amounts determine the initial ratio. + */ + @method supplyLiquidityBase(dx: UInt64, dy: UInt64): UInt64 { + let user = this.sender; + let tokenX = new TokenContract(this.tokenX); + let tokenY = new TokenContract(this.tokenY); + + // get balances of X and Y token + let dexX = AccountUpdate.create(this.address, tokenX.token.id); + let x = dexX.account.balance.getAndAssertEquals(); + + let dexY = AccountUpdate.create(this.address, tokenY.token.id); + let y = dexY.account.balance.getAndAssertEquals(); + + // // assert dy === [dx * y/x], or x === 0 + let isXZero = x.equals(UInt64.zero); + let xSafe = Circuit.if(isXZero, UInt64.one, x); + let isDyCorrect = dy.equals(dx.mul(y).div(xSafe)); + isDyCorrect.or(isXZero).assertTrue(); + + tokenX.transfer(user, dexX, dx); + tokenY.transfer(user, dexY, dy); + + // calculate liquidity token output simply as dl = dx + dx + // => maintains ratio x/l, y/l + let dl = dy.add(dx); + this.token.mint({ address: user, amount: dl }); + + // update l supply + let l = this.totalSupply.get(); + this.totalSupply.assertEquals(l); + this.totalSupply.set(l.add(dl)); + + // emit event + this.typedEvents.emit('supply-liquidity', { address: user, dx, dy }); + return dl; + } + + /** + * Mint liquidity tokens in exchange for X and Y tokens + * @param dx input amount of X tokens + * @return output amount of lqXY tokens + * + * This uses supplyLiquidityBase as the circuit, but for convenience, + * the input amount of Y tokens is calculated automatically from the X tokens. + * Fails if the liquidity pool is empty, so can't be used for the first deposit. + */ + supplyLiquidity(dx: UInt64): UInt64 { + // calculate dy outside circuit + let x = Account(this.address, TokenId.derive(this.tokenX)).balance.get(); + let y = Account(this.address, TokenId.derive(this.tokenY)).balance.get(); + if (x.value.isConstant() && x.value.isZero().toBoolean()) { + throw Error( + 'Cannot call `supplyLiquidity` when reserves are zero. Use `supplyLiquidityBase`.' + ); + } + let dy = dx.mul(y).div(x); + return this.supplyLiquidityBase(dx, dy); + } + + /** + * Burn liquidity tokens to get back X and Y tokens + * @param dl input amount of lqXY token + * + * The transaction needs to be signed by the user's private key. + * + * NOTE: this does not give back tokens in return for liquidity right away. + * to get back the tokens, you have to call {@link DexTokenHolder}.redeemFinalize() + * on both token holder contracts, after `redeemInitialize()` has been accepted into a block. + * + * @emits RedeemAction - action on the Dex account that will make the token holder + * contracts pay you tokens when reducing the action. + */ + @method redeemInitialize(dl: UInt64) { + this.reducer.dispatch(new RedeemAction({ address: this.sender, dl })); + this.token.burn({ address: this.sender, amount: dl }); + // TODO: preconditioning on the state here ruins concurrent interactions, + // there should be another `finalize` DEX method which reduces actions & updates state + this.totalSupply.set(this.totalSupply.getAndAssertEquals().sub(dl)); + + // emit event + this.typedEvents.emit('redeem-liquidity', { address: this.sender, dl }); + } + + /** + * Helper for `DexTokenHolder.redeemFinalize()` which adds preconditions on + * the current action state and token supply + */ + @method assertActionsAndSupply(actionState: Field, totalSupply: UInt64) { + this.account.actionState.assertEquals(actionState); + this.totalSupply.assertEquals(totalSupply); + } + + /** + * Swap X tokens for Y tokens + * @param dx input amount of X tokens + * @return output amount Y tokens + * + * The transaction needs to be signed by the user's private key. + * + * Note: this is not a `@method`, since it doesn't do anything beyond + * the called methods which requires proof authorization. + */ + swapX(dx: UInt64): UInt64 { + let tokenY = new TokenContract(this.tokenY); + let dexY = new DexTokenHolder(this.address, tokenY.token.id); + let dy = dexY.swap(this.sender, dx, this.tokenX); + tokenY.approveUpdateAndSend(dexY.self, this.sender, dy); + return dy; + } + + /** + * Swap Y tokens for X tokens + * @param dy input amount of Y tokens + * @return output amount Y tokens + * + * The transaction needs to be signed by the user's private key. + * + * Note: this is not a `@method`, since it doesn't do anything beyond + * the called methods which requires proof authorization. + */ + swapY(dy: UInt64): UInt64 { + let tokenX = new TokenContract(this.tokenX); + let dexX = new DexTokenHolder(this.address, tokenX.token.id); + let dx = dexX.swap(this.sender, dy, this.tokenY); + tokenX.approveUpdateAndSend(dexX.self, this.sender, dx); + return dx; + } + + @method transfer(from: PublicKey, to: PublicKey, amount: UInt64) { + this.token.send({ from, to, amount }); + } +} + +class DexTokenHolder extends SmartContract { + @state(Field) redeemActionState = State(); + static redeemActionBatchSize = 5; + + events = { + swap: Struct({ address: PublicKey, dx: UInt64 }), + }; + // better-typed wrapper for `this.emitEvent()`. TODO: remove after fixing event typing + get typedEvents() { + return getTypedEvents(this); + } + + init() { + super.init(); + this.redeemActionState.set(Reducer.initialActionsHash); + } + + @method redeemLiquidityFinalize() { + // get redeem actions + let dex = new Dex(this.address); + let fromActionState = this.redeemActionState.getAndAssertEquals(); + let actions = dex.reducer.getActions({ fromActionState }); + + // get total supply of liquidity tokens _before_ applying these actions + // (each redeem action _decreases_ the supply, so we increase it here) + let l = Circuit.witness(UInt64, (): UInt64 => { + let l = dex.totalSupply.get().toBigInt(); + // dex.totalSupply.assertNothing(); + for (let [action] of actions) { + l += action.dl.toBigInt(); + } + return UInt64.from(l); + }); + + // get our token balance + let x = this.account.balance.getAndAssertEquals(); + + let redeemActionState = dex.reducer.forEach( + actions, + ({ address, dl }) => { + // for every user that redeemed liquidity, we calculate the token output + // and create a child account update which pays the user + let dx = x.mul(dl).div(l); + let receiver = this.send({ to: address, amount: dx }); + // note: this should just work when the reducer gives us dummy data + + // important: these child account updates inherit token permission from us + receiver.body.mayUseToken = AccountUpdate.MayUseToken.InheritFromParent; + + // update l and x accordingly + l = l.sub(dl); + x = x.add(dx); + }, + fromActionState, + { + maxTransactionsWithActions: DexTokenHolder.redeemActionBatchSize, + // DEX contract doesn't allow setting preconditions from outside (= w/o proof) + skipActionStatePrecondition: true, + } + ); + + // update action state so these payments can't be triggered a 2nd time + this.redeemActionState.set(redeemActionState); + + // precondition on the DEX contract, to prove we used the right actions & token supply + dex.assertActionsAndSupply(redeemActionState, l); + } + + // this works for both directions (in our case where both tokens use the same contract) + @method swap( + user: PublicKey, + otherTokenAmount: UInt64, + otherTokenAddress: PublicKey + ): UInt64 { + // we're writing this as if our token === y and other token === x + let dx = otherTokenAmount; + let tokenX = new TokenContract(otherTokenAddress); + + // get balances of X and Y token + let dexX = AccountUpdate.create(this.address, tokenX.token.id); + let x = dexX.account.balance.getAndAssertEquals(); + let y = this.account.balance.getAndAssertEquals(); + + // send x from user to us (i.e., to the same address as this but with the other token) + tokenX.transfer(user, dexX, dx); + + // compute and send dy + let dy = y.mul(dx).div(x.add(dx)); + // just subtract dy balance and let adding balance be handled one level higher + this.balance.subInPlace(dy); + + // emit event + this.typedEvents.emit('swap', { address: this.sender, dx }); + + return dy; + } +} + +await isReady; +let { keys, addresses } = randomAccounts( + false, + 'tokenX', + 'tokenY', + 'dex', + 'user' +); +let tokenIds = { + X: TokenId.derive(addresses.tokenX), + Y: TokenId.derive(addresses.tokenY), + lqXY: TokenId.derive(addresses.dex), +}; + +/** + * Helper to get the various token balances for checks in tests + */ +function getTokenBalances() { + let balances = { + user: { MINA: 0n, X: 0n, Y: 0n, lqXY: 0n }, + dex: { X: 0n, Y: 0n, lqXYSupply: 0n }, + }; + for (let user of ['user'] as const) { + try { + balances[user].MINA = + Mina.getBalance(addresses[user]).toBigInt() / 1_000_000_000n; + } catch {} + for (let token of ['X', 'Y', 'lqXY'] as const) { + try { + balances[user][token] = Mina.getBalance( + addresses[user], + tokenIds[token] + ).toBigInt(); + } catch {} + } + } + try { + balances.dex.X = Mina.getBalance(addresses.dex, tokenIds.X).toBigInt(); + } catch {} + try { + balances.dex.Y = Mina.getBalance(addresses.dex, tokenIds.Y).toBigInt(); + } catch {} + try { + let dex = new Dex(addresses.dex); + balances.dex.lqXYSupply = dex.totalSupply.get().toBigInt(); + } catch {} + return balances; +} + +function getTypedEvents(contract: Contract) { + return { + emit( + key: Key, + event: InferProvable + ) { + contract.emitEvent(key, event); + }, + }; +} diff --git a/src/examples/zkapps/dex/dex.ts b/src/examples/zkapps/dex/dex.ts index ad92f85c2c..39f27f2007 100644 --- a/src/examples/zkapps/dex/dex.ts +++ b/src/examples/zkapps/dex/dex.ts @@ -13,16 +13,16 @@ import { PrivateKey, PublicKey, SmartContract, - Token, UInt64, VerificationKey, Struct, State, state, UInt32, + TokenId, } from 'snarkyjs'; -export { createDex, TokenContract, keys, addresses, tokenIds }; +export { createDex, TokenContract, keys, addresses, tokenIds, randomAccounts }; class UInt64x2 extends Struct([UInt64, UInt64]) {} @@ -59,8 +59,11 @@ function createDex({ // get balances of X and Y token // TODO: this creates extra account updates. we need to reuse these by passing them to or returning them from transfer() // but for that, we need the @method argument generalization - let dexXBalance = tokenX.getBalance(this.address); - let dexYBalance = tokenY.getBalance(this.address); + let dexXUpdate = AccountUpdate.create(this.address, tokenX.token.id); + let dexXBalance = dexXUpdate.account.balance.getAndAssertEquals(); + + let dexYUpdate = AccountUpdate.create(this.address, tokenY.token.id); + let dexYBalance = dexYUpdate.account.balance.getAndAssertEquals(); // // assert dy === [dx * y/x], or x === 0 let isXZero = dexXBalance.equals(UInt64.zero); @@ -68,8 +71,8 @@ function createDex({ let isDyCorrect = dy.equals(dx.mul(dexYBalance).div(xSafe)); isDyCorrect.or(isXZero).assertTrue(); - tokenX.transfer(user, this.address, dx); - tokenY.transfer(user, this.address, dy); + tokenX.transfer(user, dexXUpdate, dx); + tokenY.transfer(user, dexYUpdate, dy); // calculate liquidity token output simply as dl = dx + dx // => maintains ratio x/l, y/l @@ -115,8 +118,8 @@ function createDex({ */ supplyLiquidity(dx: UInt64): UInt64 { // calculate dy outside circuit - let x = Account(this.address, Token.getId(this.tokenX)).balance.get(); - let y = Account(this.address, Token.getId(this.tokenY)).balance.get(); + let x = Account(this.address, TokenId.derive(this.tokenX)).balance.get(); + let y = Account(this.address, TokenId.derive(this.tokenY)).balance.get(); if (x.value.isConstant() && x.value.isZero().toBoolean()) { throw Error( 'Cannot call `supplyLiquidity` when reserves are zero. Use `supplyLiquidityBase`.' @@ -132,8 +135,11 @@ function createDex({ * @return output amount of X and Y tokens, as a tuple [outputX, outputY] * * The transaction needs to be signed by the user's private key. + * + * Note: this is not a `@method` because there's nothing to prove which isn't already proven + * by the called methods */ - @method redeemLiquidity(dl: UInt64) { + redeemLiquidity(dl: UInt64) { // call the token X holder inside a token X-approved callback let tokenX = new TokenContract(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.token.id); @@ -254,9 +260,6 @@ function createDex({ // just subtract the balance, user gets their part one level higher this.balance.subInPlace(dx); - // be approved by the token owner parent - this.self.body.mayUseToken = AccountUpdate.MayUseToken.ParentsOwnToken; - return [dx, dy]; } @@ -279,8 +282,6 @@ function createDex({ let dy = y.mul(dx).div(x.add(dx)); // just subtract dy balance and let adding balance be handled one level higher this.balance.subInPlace(dy); - // be approved by the token owner parent - this.self.body.mayUseToken = AccountUpdate.MayUseToken.ParentsOwnToken; return dy; } } @@ -305,8 +306,6 @@ function createDex({ let dy = y.mul(dx).div(x.add(dx)).add(15); this.balance.subInPlace(dy); - // be approved by the token owner parent - this.self.body.mayUseToken = AccountUpdate.MayUseToken.ParentsOwnToken; return dy; } } @@ -425,6 +424,17 @@ class TokenContract extends SmartContract { zkapp.requireSignature(); } + @method approveUpdate(zkappUpdate: AccountUpdate) { + this.approve(zkappUpdate); + let balanceChange = Int64.fromObject(zkappUpdate.body.balanceChange); + balanceChange.assertEquals(Int64.from(0)); + } + + // FIXME: remove this + @method approveAny(zkappUpdate: AccountUpdate) { + this.approve(zkappUpdate, AccountUpdate.Layout.AnyChildren); + } + // let a zkapp send tokens to someone, provided the token supply stays constant @method approveUpdateAndSend( zkappUpdate: AccountUpdate, @@ -450,7 +460,16 @@ class TokenContract extends SmartContract { this.token.mint({ address: to, amount }); } - @method transfer(from: PublicKey, to: PublicKey, value: UInt64) { + transfer(from: PublicKey, to: PublicKey | AccountUpdate, amount: UInt64) { + if (to instanceof PublicKey) + return this.transferToAddress(from, to, amount); + if (to instanceof AccountUpdate) + return this.transferToUpdate(from, to, amount); + } + @method transferToAddress(from: PublicKey, to: PublicKey, value: UInt64) { + this.token.send({ from, to, amount: value }); + } + @method transferToUpdate(from: PublicKey, to: AccountUpdate, value: UInt64) { this.token.send({ from, to, amount: value }); } @@ -464,8 +483,18 @@ class TokenContract extends SmartContract { } } +const savedKeys = [ + 'EKFcUu4FLygkyZR8Ch4F8hxuJps97GCfiMRSWXDP55sgvjcmNGHc', + 'EKENfq7tEdTf5dnNxUgVo9dUnAqrEaB9syTgFyuRWinR5gPuZtbG', + 'EKEPVj2PDzQUrMwL2yeUikoQYXvh4qrkSxsDa7gegVcDvNjAteS5', + 'EKDm7SHWHEP5xiSbu52M1Z4rTFZ5Wx7YMzeaC27BQdPvvGvF42VH', + 'EKEuJJmmHNVHD1W2qmwExDyGbkSoKdKmKNPZn8QbqybVfd2Sd4hs', + 'EKEyPVU37EGw8CdGtUYnfDcBT2Eu7B6rSdy64R68UHYbrYbVJett', +]; + await isReady; let { keys, addresses } = randomAccounts( + false, 'tokenX', 'tokenY', 'dex', @@ -474,9 +503,9 @@ let { keys, addresses } = randomAccounts( 'user3' ); let tokenIds = { - X: Token.getId(addresses.tokenX), - Y: Token.getId(addresses.tokenY), - lqXY: Token.getId(addresses.dex), + X: TokenId.derive(addresses.tokenX), + Y: TokenId.derive(addresses.tokenY), + lqXY: TokenId.derive(addresses.dex), }; /** @@ -496,19 +525,16 @@ function balanceSum(accountUpdate: AccountUpdate, tokenId: Field) { * Predefined accounts keys, labeled by the input strings. Useful for testing/debugging with consistent keys. */ function randomAccounts( + createNewAccounts: boolean, ...names: [K, ...K[]] ): { keys: Record; addresses: Record } { - let savedKeys = [ - 'EKFV5T1zG13ksXKF4kDFx4bew2w4t27V3Hx1VTsbb66AKYVGL1Eu', - 'EKFE2UKugtoVMnGTxTakF2M9wwL9sp4zrxSLhuzSn32ZAYuiKh5R', - 'EKEn2s1jSNADuC8CmvCQP5CYMSSoNtx5o65H7Lahqkqp2AVdsd12', - 'EKE21kTAb37bekHbLvQpz2kvDYeKG4hB21x8VTQCbhy6m2BjFuxA', - 'EKF9JA8WiEAk7o3ENnvgMHg5XKwgQfyMowNFFrEDCevoSozSgLTn', - 'EKFZ41h3EDiTXAkwD3Mh2gVfy4CdeRGUzDPrEfXPgZR85J3KZ3WA', - ]; - + let base58Keys = createNewAccounts + ? Array(6) + .fill('') + .map(() => PrivateKey.random().toBase58()) + : savedKeys; let keys = Object.fromEntries( - names.map((name, idx) => [name, PrivateKey.fromBase58(savedKeys[idx])]) + names.map((name, idx) => [name, PrivateKey.fromBase58(base58Keys[idx])]) ) as Record; let addresses = Object.fromEntries( names.map((name) => [name, keys[name].toPublicKey()]) diff --git a/src/examples/zkapps/dex/happy-path-with-actions.ts b/src/examples/zkapps/dex/happy-path-with-actions.ts new file mode 100644 index 0000000000..a8b6f9f23f --- /dev/null +++ b/src/examples/zkapps/dex/happy-path-with-actions.ts @@ -0,0 +1,173 @@ +import { isReady, Mina, AccountUpdate, UInt64 } from 'snarkyjs'; +import { + Dex, + DexTokenHolder, + addresses, + keys, + tokenIds, + getTokenBalances, +} from './dex-with-actions.js'; +import { TokenContract } from './dex.js'; +import { expect } from 'expect'; +import { tic, toc } from '../tictoc.js'; + +await isReady; + +let proofsEnabled = false; + +tic('Happy path with actions'); +console.log(); + +let Local = Mina.LocalBlockchain({ + proofsEnabled, + enforceTransactionLimits: true, +}); +Mina.setActiveInstance(Local); +let accountFee = Mina.accountCreationFee(); +let [{ privateKey: feePayerKey, publicKey: feePayerAddress }] = + Local.testAccounts; +let tx, balances, oldBalances; + +if (proofsEnabled) { + tic('compile (token)'); + await TokenContract.compile(); + toc(); + tic('compile (dex token holder)'); + await DexTokenHolder.compile(); + toc(); + tic('compile (dex main contract)'); + await Dex.compile(); + toc(); +} + +let tokenX = new TokenContract(addresses.tokenX); +let tokenY = new TokenContract(addresses.tokenY); +let dex = new Dex(addresses.dex); +let dexTokenHolderX = new DexTokenHolder(addresses.dex, tokenIds.X); +let dexTokenHolderY = new DexTokenHolder(addresses.dex, tokenIds.Y); + +tic('deploy & init token contracts'); +tx = await Mina.transaction(feePayerAddress, () => { + // pay fees for creating 2 token contract accounts, and fund them so each can create 1 account themselves + let feePayerUpdate = AccountUpdate.createSigned(feePayerAddress); + feePayerUpdate.balance.subInPlace(accountFee.mul(2)); + feePayerUpdate.send({ to: addresses.tokenX, amount: accountFee }); + feePayerUpdate.send({ to: addresses.tokenY, amount: accountFee }); + tokenX.deploy(); + tokenY.deploy(); +}); +await tx.prove(); +await tx.sign([feePayerKey, keys.tokenX, keys.tokenY]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); + +tic('deploy dex contracts'); +tx = await Mina.transaction(feePayerAddress, () => { + // pay fees for creating 3 dex accounts + AccountUpdate.createSigned(feePayerAddress).balance.subInPlace( + accountFee.mul(3) + ); + dex.deploy(); + dexTokenHolderX.deploy(); + tokenX.approveUpdate(dexTokenHolderX.self); + dexTokenHolderY.deploy(); + tokenY.approveUpdate(dexTokenHolderY.self); +}); +await tx.prove(); +await tx.sign([feePayerKey, keys.dex]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); + +tic('transfer tokens to user'); +let USER_DX = 1_000n; +tx = await Mina.transaction(feePayerAddress, () => { + // pay fees for creating 3 user accounts + let feePayer = AccountUpdate.fundNewAccount(feePayerAddress, 3); + feePayer.send({ to: addresses.user, amount: 20e9 }); // give users MINA to pay fees + tokenX.transfer(addresses.tokenX, addresses.user, UInt64.from(USER_DX)); + tokenY.transfer(addresses.tokenY, addresses.user, UInt64.from(USER_DX)); +}); +await tx.prove(); +await tx.sign([feePayerKey, keys.tokenX, keys.tokenY]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); + +// this is done in advance to avoid account update limit in `supply` +tic("create user's lq token account"); +tx = await Mina.transaction(addresses.user, () => { + AccountUpdate.fundNewAccount(addresses.user); + dex.createAccount(); +}); +await tx.prove(); +await tx.sign([keys.user]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); + +[oldBalances, balances] = [balances, getTokenBalances()]; +expect(balances.user.X).toEqual(USER_DX); +console.log(balances); + +tic('supply liquidity'); +tx = await Mina.transaction(addresses.user, () => { + dex.supplyLiquidityBase(UInt64.from(USER_DX), UInt64.from(USER_DX)); +}); +await tx.prove(); +await tx.sign([keys.user]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); +[oldBalances, balances] = [balances, getTokenBalances()]; +expect(balances.user.X).toEqual(0n); +console.log(balances); + +tic('redeem liquidity, step 1'); +let USER_DL = 100n; +tx = await Mina.transaction(addresses.user, () => { + dex.redeemInitialize(UInt64.from(USER_DL)); +}); +await tx.prove(); +await tx.sign([keys.user]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); +console.log(getTokenBalances()); + +tic('redeem liquidity, step 2a (get back token X)'); +tx = await Mina.transaction(addresses.user, () => { + dexTokenHolderX.redeemLiquidityFinalize(); + tokenX.approveAny(dexTokenHolderX.self); +}); +await tx.prove(); +await tx.sign([keys.user]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); +console.log(getTokenBalances()); + +tic('redeem liquidity, step 2b (get back token Y)'); +tx = await Mina.transaction(addresses.user, () => { + dexTokenHolderY.redeemLiquidityFinalize(); + tokenY.approveAny(dexTokenHolderY.self); +}); +await tx.prove(); +await tx.sign([keys.user]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); +console.log(getTokenBalances()); + +[oldBalances, balances] = [balances, getTokenBalances()]; +expect(balances.user.X).toEqual(USER_DL / 2n); + +tic('swap 10 X for Y'); +USER_DX = 10n; +tx = await Mina.transaction(addresses.user, () => { + dex.swapX(UInt64.from(USER_DX)); +}); +await tx.prove(); +await tx.sign([keys.user]).send(); +toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); + +[oldBalances, balances] = [balances, getTokenBalances()]; +expect(balances.user.X).toEqual(oldBalances.user.X - USER_DX); +console.log(balances); + +toc(); +console.log('dex happy path with actions was successful! 🎉'); diff --git a/src/examples/zkapps/dex/happy-path-with-proofs.ts b/src/examples/zkapps/dex/happy-path-with-proofs.ts index bfbe70faba..e129ce559a 100644 --- a/src/examples/zkapps/dex/happy-path-with-proofs.ts +++ b/src/examples/zkapps/dex/happy-path-with-proofs.ts @@ -1,5 +1,5 @@ import { isReady, Mina, AccountUpdate, UInt64 } from 'snarkyjs'; -import { createDex, TokenContract, addresses, keys } from './dex.js'; +import { createDex, TokenContract, addresses, keys, tokenIds } from './dex.js'; import { expect } from 'expect'; import { tic, toc } from '../tictoc.js'; import { getProfiler } from '../../profiler.js'; @@ -8,13 +8,13 @@ await isReady; const TokenProfiler = getProfiler('Token with Proofs'); TokenProfiler.start('Token with proofs test flow'); -let doProofs = true; +let proofsEnabled = true; tic('Happy path with proofs'); console.log(); let Local = Mina.LocalBlockchain({ - proofsEnabled: doProofs, + proofsEnabled, enforceTransactionLimits: false, }); Mina.setActiveInstance(Local); @@ -29,19 +29,23 @@ TokenContract.analyzeMethods(); DexTokenHolder.analyzeMethods(); Dex.analyzeMethods(); -tic('compile (token)'); -await TokenContract.compile(); -toc(); -tic('compile (dex token holder)'); -await DexTokenHolder.compile(); -toc(); -tic('compile (dex main contract)'); -await Dex.compile(); -toc(); +if (proofsEnabled) { + tic('compile (token)'); + await TokenContract.compile(); + toc(); + tic('compile (dex token holder)'); + await DexTokenHolder.compile(); + toc(); + tic('compile (dex main contract)'); + await Dex.compile(); + toc(); +} let tokenX = new TokenContract(addresses.tokenX); let tokenY = new TokenContract(addresses.tokenY); let dex = new Dex(addresses.dex); +let dexTokenHolderX = new DexTokenHolder(addresses.dex, tokenIds.X); +let dexTokenHolderY = new DexTokenHolder(addresses.dex, tokenIds.Y); tic('deploy & init token contracts'); tx = await Mina.transaction(feePayerAddress, () => { @@ -56,6 +60,7 @@ tx = await Mina.transaction(feePayerAddress, () => { await tx.prove(); await tx.sign([feePayerKey, keys.tokenX, keys.tokenY]).send(); toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); tic('deploy dex contracts'); tx = await Mina.transaction(feePayerAddress, () => { @@ -64,12 +69,15 @@ tx = await Mina.transaction(feePayerAddress, () => { accountFee.mul(3) ); dex.deploy(); - tokenX.deployZkapp(addresses.dex, DexTokenHolder._verificationKey!); - tokenY.deployZkapp(addresses.dex, DexTokenHolder._verificationKey!); + dexTokenHolderX.deploy(); + tokenX.approveUpdate(dexTokenHolderX.self); + dexTokenHolderY.deploy(); + tokenY.approveUpdate(dexTokenHolderY.self); }); await tx.prove(); await tx.sign([feePayerKey, keys.dex]).send(); toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); tic('transfer tokens to user'); let USER_DX = 1_000n; @@ -83,6 +91,7 @@ tx = await Mina.transaction(feePayerAddress, () => { await tx.prove(); await tx.sign([feePayerKey, keys.tokenX, keys.tokenY]).send(); toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); [oldBalances, balances] = [balances, getTokenBalances()]; expect(balances.user.X).toEqual(USER_DX); @@ -94,6 +103,7 @@ tx = await Mina.transaction(addresses.user, () => { await tx.prove(); await tx.sign([keys.user]).send(); toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); [oldBalances, balances] = [balances, getTokenBalances()]; expect(balances.user.X).toEqual(0n); @@ -105,6 +115,7 @@ tx = await Mina.transaction(addresses.user, () => { await tx.prove(); await tx.sign([keys.user]).send(); toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); [oldBalances, balances] = [balances, getTokenBalances()]; expect(balances.user.X).toEqual(USER_DL / 2n); @@ -116,6 +127,7 @@ tx = await Mina.transaction(addresses.user, () => { await tx.prove(); await tx.sign([keys.user]).send(); toc(); +console.log('account updates length', tx.transaction.accountUpdates.length); [oldBalances, balances] = [balances, getTokenBalances()]; expect(balances.user.X).toEqual(oldBalances.user.X - USER_DX); diff --git a/src/examples/zkapps/dex/run-berkeley.ts b/src/examples/zkapps/dex/run-berkeley.ts new file mode 100644 index 0000000000..3f4f7f9cc6 --- /dev/null +++ b/src/examples/zkapps/dex/run-berkeley.ts @@ -0,0 +1,324 @@ +import { + isReady, + Mina, + AccountUpdate, + UInt64, + PrivateKey, + fetchAccount, +} from 'snarkyjs'; +import { + Dex, + DexTokenHolder, + addresses, + keys, + tokenIds, +} from './dex-with-actions.js'; +import { TokenContract } from './dex.js'; +import { expect } from 'expect'; +import { tic, toc } from '../tictoc.js'; + +await isReady; + +// setting this to a higher number allows you to skip a few transactions, to pick up after an error +const successfulTransactions = 0; + +tic('Run DEX with actions, happy path, on Berkeley'); +console.log(); + +let Berkeley = Mina.Network({ + mina: 'https://berkeley.minascan.io/graphql', + archive: 'https://archive-node-api.p42.xyz', +}); +Mina.setActiveInstance(Berkeley); +let accountFee = Mina.accountCreationFee(); + +let tx, pendingTx: Mina.TransactionId, balances, oldBalances; + +// compile contracts & wait for fee payer to be funded +let { sender, senderKey } = await ensureFundedAccount( + 'EKDrVGPC6iVRqB2bMMakNBTdEi8M1TqMn5TViLe9bafcpEExPYui' +); + +TokenContract.analyzeMethods(); +DexTokenHolder.analyzeMethods(); +Dex.analyzeMethods(); + +tic('compile (token)'); +await TokenContract.compile(); +toc(); +tic('compile (dex token holder)'); +await DexTokenHolder.compile(); +toc(); +tic('compile (dex main contract)'); +await Dex.compile(); +toc(); + +let tokenX = new TokenContract(addresses.tokenX); +let tokenY = new TokenContract(addresses.tokenY); +let dex = new Dex(addresses.dex); +let dexTokenHolderX = new DexTokenHolder(addresses.dex, tokenIds.X); +let dexTokenHolderY = new DexTokenHolder(addresses.dex, tokenIds.Y); + +let senderSpec = { sender, fee: 0.1e9 }; +let userSpec = { sender: addresses.user, fee: 0.1e9 }; + +if (successfulTransactions <= 0) { + tic('deploy & init token contracts'); + tx = await Mina.transaction(senderSpec, () => { + // pay fees for creating 2 token contract accounts, and fund them so each can create 1 account themselves + let feePayerUpdate = AccountUpdate.createSigned(sender); + feePayerUpdate.balance.subInPlace(accountFee.mul(2)); + feePayerUpdate.send({ to: addresses.tokenX, amount: accountFee }); + feePayerUpdate.send({ to: addresses.tokenY, amount: accountFee }); + tokenX.deploy(); + tokenY.deploy(); + }); + await tx.prove(); + pendingTx = await tx.sign([senderKey, keys.tokenX, keys.tokenY]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); +} + +if (successfulTransactions <= 1) { + tic('deploy dex contracts'); + tx = await Mina.transaction(senderSpec, () => { + // pay fees for creating 3 dex accounts + AccountUpdate.createSigned(sender).balance.subInPlace(accountFee.mul(3)); + dex.deploy(); + dexTokenHolderX.deploy(); + tokenX.approveUpdate(dexTokenHolderX.self); + dexTokenHolderY.deploy(); + tokenY.approveUpdate(dexTokenHolderY.self); + }); + await tx.prove(); + pendingTx = await tx.sign([senderKey, keys.dex]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); +} + +let USER_DX = 1_000n; + +if (successfulTransactions <= 2) { + tic('transfer tokens to user'); + tx = await Mina.transaction(senderSpec, () => { + // pay fees for creating 3 user accounts + let feePayer = AccountUpdate.fundNewAccount(sender, 3); + feePayer.send({ to: addresses.user, amount: 8e9 }); // give users MINA to pay fees + tokenX.transfer(addresses.tokenX, addresses.user, UInt64.from(USER_DX)); + tokenY.transfer(addresses.tokenY, addresses.user, UInt64.from(USER_DX)); + }); + await tx.prove(); + pendingTx = await tx.sign([senderKey, keys.tokenX, keys.tokenY]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); +} + +if (successfulTransactions <= 3) { + // this is done in advance to avoid account update limit in `supply` + tic("create user's lq token account"); + tx = await Mina.transaction(userSpec, () => { + AccountUpdate.fundNewAccount(addresses.user); + dex.createAccount(); + }); + await tx.prove(); + pendingTx = await tx.sign([keys.user]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); + + [oldBalances, balances] = [balances, await getTokenBalances()]; + expect(balances.user.X).toEqual(USER_DX); + console.log(balances); +} + +if (successfulTransactions <= 4) { + tic('supply liquidity'); + tx = await Mina.transaction(userSpec, () => { + dex.supplyLiquidityBase(UInt64.from(USER_DX), UInt64.from(USER_DX)); + }); + await tx.prove(); + pendingTx = await tx.sign([keys.user]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); + + [oldBalances, balances] = [balances, await getTokenBalances()]; + expect(balances.user.X).toEqual(0n); + console.log(balances); +} + +let USER_DL = 100n; + +if (successfulTransactions <= 5) { + tic('redeem liquidity, step 1'); + tx = await Mina.transaction(userSpec, () => { + dex.redeemInitialize(UInt64.from(USER_DL)); + }); + await tx.prove(); + pendingTx = await tx.sign([keys.user]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); + + console.log(await getTokenBalances()); +} + +if (successfulTransactions <= 6) { + tic('redeem liquidity, step 2a (get back token X)'); + tx = await Mina.transaction(userSpec, () => { + dexTokenHolderX.redeemLiquidityFinalize(); + tokenX.approveAny(dexTokenHolderX.self); + }); + await tx.prove(); + pendingTx = await tx.sign([keys.user]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); + + console.log(await getTokenBalances()); +} + +if (successfulTransactions <= 7) { + tic('redeem liquidity, step 2b (get back token Y)'); + tx = await Mina.transaction(userSpec, () => { + dexTokenHolderY.redeemLiquidityFinalize(); + tokenY.approveAny(dexTokenHolderY.self); + }); + await tx.prove(); + pendingTx = await tx.sign([keys.user]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); + + [oldBalances, balances] = [balances, await getTokenBalances()]; + expect(balances.user.X).toEqual(USER_DL / 2n); + console.log(balances); +} + +if (successfulTransactions <= 8) { + oldBalances = await getTokenBalances(); + + tic('swap 10 X for Y'); + USER_DX = 10n; + tx = await Mina.transaction(userSpec, () => { + dex.swapX(UInt64.from(USER_DX)); + }); + await tx.prove(); + pendingTx = await tx.sign([keys.user]).send(); + toc(); + console.log('account updates length', tx.transaction.accountUpdates.length); + logPendingTransaction(pendingTx); + tic('waiting'); + await pendingTx.wait(); + await sleep(10); + toc(); + + balances = await getTokenBalances(); + expect(balances.user.X).toEqual(oldBalances.user.X - USER_DX); + console.log(balances); +} + +toc(); +console.log('dex happy path with actions was successful! 🎉'); + +async function ensureFundedAccount(privateKeyBase58: string) { + let senderKey = PrivateKey.fromBase58(privateKeyBase58); + let sender = senderKey.toPublicKey(); + let result = await fetchAccount({ publicKey: sender }); + let balance = result.account?.balance.toBigInt(); + if (balance === undefined || balance <= 15_000_000_000n) { + await Mina.faucet(sender); + await sleep(1); + } + return { senderKey, sender }; +} + +function logPendingTransaction(pendingTx: Mina.TransactionId) { + if (!pendingTx.isSuccess) throw Error('transaction failed'); + console.log( + `tx sent: https://berkeley.minaexplorer.com/transaction/${pendingTx.hash()}` + ); +} + +async function getTokenBalances() { + // fetch accounts + await Promise.all( + [ + { publicKey: addresses.user }, + { publicKey: addresses.user, tokenId: tokenIds.X }, + { publicKey: addresses.user, tokenId: tokenIds.Y }, + { publicKey: addresses.user, tokenId: tokenIds.lqXY }, + { publicKey: addresses.dex }, + { publicKey: addresses.dex, tokenId: tokenIds.X }, + { publicKey: addresses.dex, tokenId: tokenIds.Y }, + ].map((a) => fetchAccount(a)) + ); + + let balances = { + user: { MINA: 0n, X: 0n, Y: 0n, lqXY: 0n }, + dex: { X: 0n, Y: 0n, lqXYSupply: 0n }, + }; + let user = 'user' as const; + try { + balances.user.MINA = + Mina.getBalance(addresses[user]).toBigInt() / 1_000_000_000n; + } catch {} + for (let token of ['X', 'Y', 'lqXY'] as const) { + try { + balances[user][token] = Mina.getBalance( + addresses[user], + tokenIds[token] + ).toBigInt(); + } catch {} + } + try { + balances.dex.X = Mina.getBalance(addresses.dex, tokenIds.X).toBigInt(); + } catch {} + try { + balances.dex.Y = Mina.getBalance(addresses.dex, tokenIds.Y).toBigInt(); + } catch {} + try { + let dex = new Dex(addresses.dex); + balances.dex.lqXYSupply = dex.totalSupply.get().toBigInt(); + } catch {} + return balances; +} + +async function sleep(sec: number) { + return new Promise((r) => setTimeout(r, sec * 1000)); +} diff --git a/src/examples/zkapps/dex/run.ts b/src/examples/zkapps/dex/run.ts index 0bd7791c5c..2fba10cfc0 100644 --- a/src/examples/zkapps/dex/run.ts +++ b/src/examples/zkapps/dex/run.ts @@ -4,8 +4,8 @@ import { AccountUpdate, UInt64, shutdown, - Token, Permissions, + TokenId, } from 'snarkyjs'; import { createDex, TokenContract, addresses, keys, tokenIds } from './dex.js'; import { expect } from 'expect'; @@ -13,10 +13,10 @@ import { expect } from 'expect'; import { getProfiler } from '../../profiler.js'; await isReady; -let doProofs = false; +let proofsEnabled = false; let Local = Mina.LocalBlockchain({ - proofsEnabled: doProofs, + proofsEnabled, enforceTransactionLimits: false, }); Mina.setActiveInstance(Local); @@ -32,18 +32,21 @@ console.log('TOKEN Y ADDRESS\t', addresses.tokenY.toBase58()); console.log('DEX ADDRESS\t', addresses.dex.toBase58()); console.log('USER ADDRESS\t', addresses.user.toBase58()); console.log('-------------------------------------------------'); -console.log('TOKEN X ID\t', Token.Id.toBase58(tokenIds.X)); -console.log('TOKEN Y ID\t', Token.Id.toBase58(tokenIds.Y)); +console.log('TOKEN X ID\t', TokenId.toBase58(tokenIds.X)); +console.log('TOKEN Y ID\t', TokenId.toBase58(tokenIds.Y)); console.log('-------------------------------------------------'); -console.log('compile (token)...'); -await TokenContract.compile(); +TokenContract.analyzeMethods(); +if (proofsEnabled) { + console.log('compile (token)...'); + await TokenContract.compile(); +} await main({ withVesting: false }); // swap out ledger so we can start fresh Local = Mina.LocalBlockchain({ - proofsEnabled: doProofs, + proofsEnabled, enforceTransactionLimits: false, }); Mina.setActiveInstance(Local); @@ -69,15 +72,19 @@ async function main({ withVesting }: { withVesting: boolean }) { DexTokenHolder.analyzeMethods(); Dex.analyzeMethods(); - // compile & deploy all zkApps - console.log('compile (dex token holder)...'); - await DexTokenHolder.compile(); - console.log('compile (dex main contract)...'); - await Dex.compile(); + if (proofsEnabled) { + // compile & deploy all zkApps + console.log('compile (dex token holder)...'); + await DexTokenHolder.compile(); + console.log('compile (dex main contract)...'); + await Dex.compile(); + } let tokenX = new TokenContract(addresses.tokenX); let tokenY = new TokenContract(addresses.tokenY); let dex = new Dex(addresses.dex); + let dexTokenHolderX = new DexTokenHolder(addresses.dex, tokenIds.X); + let dexTokenHolderY = new DexTokenHolder(addresses.dex, tokenIds.Y); console.log('deploy & init token contracts...'); tx = await Mina.transaction(feePayerAddress, () => { @@ -103,8 +110,10 @@ async function main({ withVesting }: { withVesting: boolean }) { // pay fees for creating 3 dex accounts AccountUpdate.fundNewAccount(feePayerAddress, 3); dex.deploy(); - tokenX.deployZkapp(addresses.dex, DexTokenHolder._verificationKey!); - tokenY.deployZkapp(addresses.dex, DexTokenHolder._verificationKey!); + dexTokenHolderX.deploy(); + tokenX.approveUpdate(dexTokenHolderX.self); + dexTokenHolderY.deploy(); + tokenY.approveUpdate(dexTokenHolderY.self); }); await tx.prove(); tx.sign([feePayerKey, keys.dex]); diff --git a/src/examples/zkapps/dex/upgradability.ts b/src/examples/zkapps/dex/upgradability.ts index edd9e7ebf1..31d4812a51 100644 --- a/src/examples/zkapps/dex/upgradability.ts +++ b/src/examples/zkapps/dex/upgradability.ts @@ -6,13 +6,13 @@ import { PrivateKey, UInt64, } from 'snarkyjs'; -import { createDex, TokenContract, addresses, keys } from './dex.js'; +import { createDex, TokenContract, addresses, keys, tokenIds } from './dex.js'; import { expect } from 'expect'; import { getProfiler } from '../../profiler.js'; await isReady; -let doProofs = false; +let proofsEnabled = false; console.log('starting upgradeability tests'); @@ -33,7 +33,7 @@ async function atomicActionsTest({ withVesting }: { withVesting: boolean }) { const DexProfiler = getProfiler('DEX profiler atomic actions'); DexProfiler.start('DEX test flow'); let Local = Mina.LocalBlockchain({ - proofsEnabled: doProofs, + proofsEnabled, enforceTransactionLimits: false, }); Mina.setActiveInstance(Local); @@ -49,15 +49,19 @@ async function atomicActionsTest({ withVesting }: { withVesting: boolean }) { DexTokenHolder.analyzeMethods(); Dex.analyzeMethods(); - // compile & deploy all zkApps - console.log('compile (dex token holder)...'); - await DexTokenHolder.compile(); - console.log('compile (dex main contract)...'); - await Dex.compile(); + if (proofsEnabled) { + // compile & deploy all zkApps + console.log('compile (dex token holder)...'); + await DexTokenHolder.compile(); + console.log('compile (dex main contract)...'); + await Dex.compile(); + } let tokenX = new TokenContract(addresses.tokenX); let tokenY = new TokenContract(addresses.tokenY); let dex = new Dex(addresses.dex); + let dexTokenHolderX = new DexTokenHolder(addresses.dex, tokenIds.X); + let dexTokenHolderY = new DexTokenHolder(addresses.dex, tokenIds.Y); console.log('deploy & init token contracts...'); tx = await Mina.transaction(feePayerAddress, () => { @@ -97,8 +101,10 @@ async function atomicActionsTest({ withVesting }: { withVesting: boolean }) { // pay fees for creating 3 dex accounts AccountUpdate.fundNewAccount(feePayerAddress, 3); dex.deploy(); - tokenX.deployZkapp(addresses.dex, DexTokenHolder._verificationKey!); - tokenY.deployZkapp(addresses.dex, DexTokenHolder._verificationKey!); + dexTokenHolderX.deploy(); + tokenX.approveUpdate(dexTokenHolderX.self); + dexTokenHolderY.deploy(); + tokenY.approveUpdate(dexTokenHolderY.self); console.log('manipulating setDelegate field to impossible...'); // setting the setDelegate permission field to impossible let dexAccount = AccountUpdate.create(addresses.dex); @@ -231,7 +237,7 @@ async function upgradeabilityTests({ withVesting }: { withVesting: boolean }) { const DexProfiler = getProfiler('DEX profiler upgradeability tests'); DexProfiler.start('DEX test flow'); let Local = Mina.LocalBlockchain({ - proofsEnabled: doProofs, + proofsEnabled: proofsEnabled, enforceTransactionLimits: false, }); Mina.setActiveInstance(Local); diff --git a/src/index.ts b/src/index.ts index 5cada122e7..2f858274f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ export { Struct, FlexibleProvable, FlexibleProvablePure, + InferProvable, } from './lib/circuit_value.js'; export { UInt32, UInt64, Int64, Sign } from './lib/int.js'; export { Types } from './provable/types.js'; @@ -38,6 +39,7 @@ export { Proof, SelfProof, verify } from './lib/proof_system.js'; export { Token, + TokenId, AccountUpdate, Permissions, ZkappPublicInput, diff --git a/src/lib/account_update.ts b/src/lib/account_update.ts index 4a1e42405f..f0f5c4c3b2 100644 --- a/src/lib/account_update.ts +++ b/src/lib/account_update.ts @@ -47,16 +47,19 @@ export { createChildAccountUpdate, AccountUpdatesLayout, zkAppProver, + SmartContractContext, }; const ZkappStateLength = 8; -let smartContractContext = Context.create<{ +type SmartContractContext = { this: SmartContract; methodCallDepth: number; - isCallback: boolean; selfUpdate: AccountUpdate; -}>(); +}; +let smartContractContext = Context.create({ + default: null, +}); let zkAppProver = Prover<{ transaction: ZkappCommand; @@ -292,6 +295,22 @@ let Permissions = { setTiming: Permission.none(), }), + allImpossible: (): Permissions => ({ + editState: Permission.impossible(), + send: Permission.impossible(), + receive: Permission.impossible(), + access: Permission.impossible(), + setDelegate: Permission.impossible(), + setPermissions: Permission.impossible(), + setVerificationKey: Permission.impossible(), + setZkappUri: Permission.impossible(), + editActionState: Permission.impossible(), + setTokenSymbol: Permission.impossible(), + incrementNonce: Permission.impossible(), + setVotingFor: Permission.impossible(), + setTiming: Permission.impossible(), + }), + fromString: (permission: AuthRequired): Permission => { switch (permission) { case 'None': @@ -577,23 +596,28 @@ const TokenId = { get default() { return Field(1); }, + derive(tokenOwner: PublicKey, parentTokenId = Field(1)) { + if (tokenOwner.isConstant() && parentTokenId.isConstant()) { + return Ledger.customTokenId(tokenOwner, parentTokenId); + } else { + return Ledger.customTokenIdChecked(tokenOwner, parentTokenId); + } + }, }; +/** + * @deprecated use `TokenId` instead of `Token.Id` and `TokenId.derive()` instead of `Token.getId()` + */ class Token { - readonly id: Field; - readonly parentTokenId: Field; - readonly tokenOwner: PublicKey; - static Id = TokenId; static getId(tokenOwner: PublicKey, parentTokenId = TokenId.default) { - if (tokenOwner.isConstant() && parentTokenId.isConstant()) { - return Ledger.customTokenId(tokenOwner, parentTokenId); - } else { - return Ledger.customTokenIdChecked(tokenOwner, parentTokenId); - } + return TokenId.derive(tokenOwner, parentTokenId); } + readonly id: Field; + readonly parentTokenId: Field; + readonly tokenOwner: PublicKey; constructor({ tokenOwner, parentTokenId = TokenId.default, @@ -604,7 +628,7 @@ class Token { this.parentTokenId = parentTokenId; this.tokenOwner = tokenOwner; try { - this.id = Token.getId(tokenOwner, parentTokenId); + this.id = TokenId.derive(tokenOwner, parentTokenId); } catch (e) { throw new Error( `Could not create a custom token id:\nError: ${(e as Error).message}` @@ -612,6 +636,7 @@ class Token { } } } + /** * An {@link AccountUpdate} is a set of instructions for the Mina network. * It includes {@link Preconditions} and a list of state updates, which need to @@ -686,88 +711,94 @@ class AccountUpdate implements Types.AccountUpdate { token() { let thisAccountUpdate = this; - let customToken = new Token({ - tokenOwner: thisAccountUpdate.body.publicKey, - parentTokenId: thisAccountUpdate.body.tokenId, - }); + let tokenOwner = this.publicKey; + let parentTokenId = this.tokenId; + let id = TokenId.derive(tokenOwner, parentTokenId); + + function getApprovedAccountUpdate( + accountLike: PublicKey | AccountUpdate | SmartContract, + label: string + ) { + if (accountLike instanceof SmartContract) { + accountLike = accountLike.self; + } + if (accountLike instanceof AccountUpdate) { + accountLike.tokenId.assertEquals(id); + thisAccountUpdate.approve(accountLike); + } + if (accountLike instanceof PublicKey) { + accountLike = AccountUpdate.defaultAccountUpdate(accountLike, id); + makeChildAccountUpdate(thisAccountUpdate, accountLike); + } + if (!accountLike.label) + accountLike.label = `${ + thisAccountUpdate.label ?? 'Unlabeled' + }.${label}`; + return accountLike; + } return { - id: customToken.id, - parentTokenId: customToken.parentTokenId, - tokenOwner: customToken.tokenOwner, + id, + parentTokenId, + tokenOwner, + /** + * Mints token balance to `address`. Returns the mint account update. + */ mint({ address, amount, }: { - address: PublicKey; + address: PublicKey | AccountUpdate | SmartContract; amount: number | bigint | UInt64; }) { - let receiver = AccountUpdate.defaultAccountUpdate(address, this.id); - thisAccountUpdate.approve(receiver); - // Add the amount to mint to the receiver's account - receiver.body.balanceChange = Int64.fromObject( - receiver.body.balanceChange - ).add(amount); + let receiver = getApprovedAccountUpdate(address, 'token.mint()'); + receiver.balance.addInPlace(amount); return receiver; }, + /** + * Burn token balance on `address`. Returns the burn account update. + */ burn({ address, amount, }: { - address: PublicKey; + address: PublicKey | AccountUpdate | SmartContract; amount: number | bigint | UInt64; }) { - let sender = AccountUpdate.defaultAccountUpdate(address, this.id); - thisAccountUpdate.approve(sender); - sender.body.useFullCommitment = Bool(true); - sender.body.implicitAccountCreationFee = Bool(false); + let sender = getApprovedAccountUpdate(address, 'token.burn()'); // Sub the amount to burn from the sender's account - sender.body.balanceChange = Int64.fromObject( - sender.body.balanceChange - ).sub(amount); + sender.balance.subInPlace(amount); // Require signature from the sender account being deducted + sender.body.useFullCommitment = Bool(true); Authorization.setLazySignature(sender); + return sender; }, + /** + * Move token balance from `from` to `to`. Returns the `to` account update. + */ send({ from, to, amount, }: { - from: PublicKey; - to: PublicKey; + from: PublicKey | AccountUpdate | SmartContract; + to: PublicKey | AccountUpdate | SmartContract; amount: number | bigint | UInt64; }) { - // Create a new accountUpdate for the sender to send the amount to the - // receiver - let sender = AccountUpdate.defaultAccountUpdate(from, this.id); - thisAccountUpdate.approve(sender); + let sender = getApprovedAccountUpdate(from, 'token.send() (sender)'); + sender.balance.subInPlace(amount); sender.body.useFullCommitment = Bool(true); - sender.body.implicitAccountCreationFee = Bool(false); - sender.body.balanceChange = Int64.fromObject( - sender.body.balanceChange - ).sub(amount); - - // Require signature from the sender accountUpdate Authorization.setLazySignature(sender); - let receiverAccountUpdate = createChildAccountUpdate( - thisAccountUpdate, - to, - this.id - ); + let receiver = getApprovedAccountUpdate(to, 'token.send() (receiver)'); + receiver.balance.addInPlace(amount); - // Add the amount to send to the receiver's account - let i1 = receiverAccountUpdate.body.balanceChange; - receiverAccountUpdate.body.balanceChange = new Int64( - i1.magnitude, - i1.sgn - ).add(amount); - return receiverAccountUpdate; + return receiver; }, }; } @@ -805,6 +836,7 @@ class AccountUpdate implements Types.AccountUpdate { receiver.body.tokenId.assertEquals(this.body.tokenId); } else { receiver = AccountUpdate.defaultAccountUpdate(to, this.body.tokenId); + receiver.label = `${this.label ?? 'Unlabeled'}.send()`; this.approve(receiver); } @@ -941,8 +973,7 @@ class AccountUpdate implements Types.AccountUpdate { */ sign(privateKey?: PrivateKey) { let { nonce, isSameAsFeePayer } = AccountUpdate.getSigningInfo(this); - // if this account is the same as the fee payer, we use the "full - // commitment" for replay protection + // if this account is the same as the fee payer, we use the "full commitment" for replay protection this.body.useFullCommitment = isSameAsFeePayer; this.body.implicitAccountCreationFee = Bool(false); // otherwise, we increment the nonce @@ -1087,10 +1118,16 @@ class AccountUpdate implements Types.AccountUpdate { */ static create(publicKey: PublicKey, tokenId?: Field) { let accountUpdate = AccountUpdate.defaultAccountUpdate(publicKey, tokenId); - if (smartContractContext.has()) { - smartContractContext.get().this.self.approve(accountUpdate); + let insideContract = smartContractContext.get(); + if (insideContract) { + let self = insideContract.this.self; + self.approve(accountUpdate); + accountUpdate.label = `${ + self.label || 'Unlabeled' + } > AccountUpdate.create()`; } else { Mina.currentTransaction()?.accountUpdates.push(accountUpdate); + accountUpdate.label = `Mina.transaction > AccountUpdate.create()`; } return accountUpdate; } @@ -1099,13 +1136,14 @@ class AccountUpdate implements Types.AccountUpdate { * -- if in a smart contract, to its children */ static attachToTransaction(accountUpdate: AccountUpdate) { - if (smartContractContext.has()) { - let selfUpdate = smartContractContext.get().this.self; + let insideContract = smartContractContext.get(); + if (insideContract) { + let selfUpdate = insideContract.this.self; // avoid redundant attaching & cycle in account update structure, happens // when calling attachToTransaction(this.self) inside a @method // TODO avoid account update cycles more generally if (selfUpdate === accountUpdate) return; - smartContractContext.get().this.self.approve(accountUpdate); + insideContract.this.self.approve(accountUpdate); } else { if (!Mina.currentTransaction.has()) return; let updates = Mina.currentTransaction.get().accountUpdates; @@ -1153,6 +1191,10 @@ class AccountUpdate implements Types.AccountUpdate { let publicKey = signer instanceof PrivateKey ? signer.toPublicKey() : signer; let accountUpdate = AccountUpdate.create(publicKey, tokenId); + accountUpdate.label = accountUpdate.label.replace( + '.create()', + '.createSigned()' + ); if (signer instanceof PrivateKey) { accountUpdate.sign(signer); } else { @@ -1193,6 +1235,7 @@ class AccountUpdate implements Types.AccountUpdate { numberOfAccounts?: number | { initialBalance: number | string | UInt64 } ) { let accountUpdate = AccountUpdate.createSigned(feePayer as PrivateKey); + accountUpdate.label = 'AccountUpdate.fundNewAccount()'; let fee = Mina.accountCreationFee(); numberOfAccounts ??= 1; if (typeof numberOfAccounts === 'number') fee = fee.mul(numberOfAccounts); @@ -1567,7 +1610,7 @@ const CallForest = { TokenId.default ) ); - let self = Token.getId(update.body.publicKey, update.body.tokenId); + let self = TokenId.derive(update.body.publicKey, update.body.tokenId); let childContext = { caller, self }; withCallers.push({ accountUpdate: update, @@ -1603,7 +1646,7 @@ const CallForest = { } else if (!update.body.mayUseToken.inheritFromParent.toBoolean()) { context.caller = TokenId.default; } - context.self = Token.getId(update.body.publicKey, update.body.tokenId); + context.self = TokenId.derive(update.body.publicKey, update.body.tokenId); } return context; }, @@ -1697,7 +1740,7 @@ const ZkappCommand = { feePayer.body.authorization = '..' + feePayer.authorization.slice(-4); if (feePayer.body.validUntil === null) delete feePayer.body.validUntil; return [ - feePayer.body, + { label: 'feePayer', ...feePayer.body }, ...transaction.accountUpdates.map((a) => a.toPretty()), ]; }, diff --git a/src/lib/caller.unit-test.ts b/src/lib/caller.unit-test.ts index c6f20856ea..28eaec3303 100644 --- a/src/lib/caller.unit-test.ts +++ b/src/lib/caller.unit-test.ts @@ -1,5 +1,5 @@ import { isReady, shutdown } from '../snarky.js'; -import { AccountUpdate, Token } from './account_update.js'; +import { AccountUpdate, TokenId } from './account_update.js'; import * as Mina from './mina.js'; import { expect } from 'expect'; @@ -10,7 +10,7 @@ Mina.setActiveInstance(Local); let [{ privateKey, publicKey }] = Local.testAccounts; -let parentId = Token.getId(publicKey); +let parentId = TokenId.derive(publicKey); /** * tests whether the following two account updates gives the child token permissions: diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index cfc89bb964..75c312e91b 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -836,12 +836,15 @@ function cloneCircuitValue(obj: T): T { // primitive JS types and functions aren't cloned if (typeof obj !== 'object' || obj === null) return obj; - // HACK: callbacks + // HACK: callbacks, account udpates if ( ['GenericArgument', 'Callback'].includes((obj as any).constructor?.name) ) { return obj; } + if (['AccountUpdate'].includes((obj as any).constructor?.name)) { + return (obj as any).constructor.clone(obj); + } // built-in JS datatypes with custom cloning strategies if (Array.isArray(obj)) return obj.map(cloneCircuitValue) as any as T; diff --git a/src/lib/global-context.ts b/src/lib/global-context.ts index d5a0596699..9049ab794f 100644 --- a/src/lib/global-context.ts +++ b/src/lib/global-context.ts @@ -9,10 +9,10 @@ namespace Context { get(): Context; has(): boolean; - runWith( + runWith( context: Context, - func: (context: Context) => Result - ): [Context, Result]; + func: (context: C) => Result + ): [C, Result]; runWithAsync( context: Context, func: (context: Context) => Promise @@ -39,8 +39,8 @@ function create( allowsNesting: options.allowsNesting ?? true, get: () => get(t), has: () => t.data.length !== 0, - runWith: (context: C, func: (context: C) => R) => - runWith(t, context, func), + runWith: (context: C0, func: (context: C0) => R) => + runWith(t, context, func), runWithAsync: (context: C, func: (context: C) => Promise) => runWithAsync(t, context, func), enter: (context: C) => enter(t, context), diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 952333b4b3..53ffe197b1 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -438,8 +438,7 @@ function LocalBlockchain({ JSON.stringify(ZkappCommand.toJSON(txn.transaction)) ); - if (enforceTransactionLimits) - verifyTransactionLimits(txn.transaction.accountUpdates); + if (enforceTransactionLimits) verifyTransactionLimits(txn.transaction); for (const update of txn.transaction.accountUpdates) { let accountJson = ledger.getAccount( @@ -747,7 +746,7 @@ function Network(input: { mina: string; archive: string } | string): Mina { async sendTransaction(txn: Transaction) { txn.sign(); - verifyTransactionLimits(txn.transaction.accountUpdates); + verifyTransactionLimits(txn.transaction); let [response, error] = await Fetch.sendZkapp(txn.toJSON()); let errors: any[] | undefined; @@ -1133,7 +1132,7 @@ async function fetchEvents( */ async function fetchActions( publicKey: PublicKey, - actionStates: ActionStates, + actionStates?: ActionStates, tokenId?: Field ) { return await activeInstance.fetchActions(publicKey, actionStates, tokenId); @@ -1144,7 +1143,7 @@ async function fetchActions( */ function getActions( publicKey: PublicKey, - actionStates: ActionStates, + actionStates?: ActionStates, tokenId?: Field ) { return activeInstance.getActions(publicKey, actionStates, tokenId); @@ -1355,7 +1354,7 @@ async function verifyAccountUpdate( } } -function verifyTransactionLimits(accountUpdates: AccountUpdate[]) { +function verifyTransactionLimits({ accountUpdates }: ZkappCommand) { // constants used to calculate cost of a transaction - originally defined in the genesis_constants file in the mina repo const proofCost = 10.26; const signedPairCost = 10.08; @@ -1368,14 +1367,25 @@ function verifyTransactionLimits(accountUpdates: AccountUpdate[]) { let eventElements = { events: 0, actions: 0 }; - let authTypes = filterGroups( - accountUpdates.map((update) => { - let json = update.toJSON(); - eventElements.events += countEventElements(update.body.events); - eventElements.actions += countEventElements(update.body.actions); - return json.body.authorizationKind; - }) - ); + let authKinds = accountUpdates.map((update) => { + eventElements.events += countEventElements(update.body.events); + eventElements.actions += countEventElements(update.body.actions); + let { isSigned, isProved, verificationKeyHash } = + update.body.authorizationKind; + return { + isSigned: isSigned.toBoolean(), + isProved: isProved.toBoolean(), + verificationKeyHash: verificationKeyHash.toString(), + }; + }); + // insert entry for the fee payer + authKinds.unshift({ + isSigned: true, + isProved: false, + verificationKeyHash: '', + }); + let authTypes = filterGroups(authKinds); + /* np := proof n2 := signedPair diff --git a/src/lib/precondition.ts b/src/lib/precondition.ts index dd021aea59..730e6220ba 100644 --- a/src/lib/precondition.ts +++ b/src/lib/precondition.ts @@ -48,6 +48,10 @@ function Network(accountUpdate: AccountUpdate): Network { let slot = network.globalSlotSinceGenesis.get(); return globalSlotToTimestamp(slot); }, + getAndAssertEquals() { + let slot = network.globalSlotSinceGenesis.getAndAssertEquals(); + return globalSlotToTimestamp(slot); + }, assertEquals(value: UInt64) { let { genesisTimestamp, slotTime } = Mina.activeInstance.getNetworkConstants(); @@ -213,7 +217,7 @@ function preconditionSubclass< if (fieldType === undefined) { throw Error(`this.${longKey}: fieldType undefined`); } - return { + let obj = { get() { if (unimplementedPreconditions.includes(longKey)) { let self = context.isSelf ? 'this' : 'accountUpdate'; @@ -227,6 +231,11 @@ function preconditionSubclass< fieldType )) as U; }, + getAndAssertEquals() { + let value = obj.get(); + obj.assertEquals(value); + return value; + }, assertEquals(value: U) { context.constrained.add(longKey); let property = getPath( @@ -249,6 +258,7 @@ function preconditionSubclass< context.constrained.add(longKey); }, }; + return obj; } function getVariable( @@ -441,6 +451,7 @@ type PreconditionBaseTypes = { type PreconditionSubclassType = { get(): U; + getAndAssertEquals(): U; assertEquals(value: U): void; assertNothing(): void; }; diff --git a/src/lib/state.ts b/src/lib/state.ts index c2ddc6b39c..a053aaa08f 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -21,11 +21,57 @@ export { assertStatePrecondition, cleanStatePrecondition }; * Gettable and settable state that can be checked for equality. */ type State = { + /** + * Get the current on-chain state. + * + * Caution: If you use this method alone inside a smart contract, it does not prove that your contract uses the current on-chain state. + * To successfully prove that your contract uses the current on-chain state, you must add an additional `.assertEquals()` statement or use `.getAndAssertEquals()`: + * + * ```ts + * let x = this.x.get(); + * this.x.assertEquals(x); + * ``` + * + * OR + * + * ```ts + * let x = this.x.getAndAssertEquals(); + * ``` + */ get(): A; + /** + * Get the current on-chain state and prove it really has to equal the on-chain state, + * by adding a precondition which the verifying Mina node will check before accepting this transaction. + */ + getAndAssertEquals(): A; + /** + * Set the on-chain state to a new value. + */ set(a: A): void; + /** + * Asynchronously fetch the on-chain state. This is intended for getting the state outside a smart contract. + */ fetch(): Promise; + /** + * Prove that the on-chain state has to equal the given state, + * by adding a precondition which the verifying Mina node will check before accepting this transaction. + */ assertEquals(a: A): void; + /** + * **DANGER ZONE**: Override the error message that warns you when you use `.get()` without adding a precondition. + */ assertNothing(): void; + /** + * Get the state from the raw list of field elements on a zkApp account, for example: + * + * ```ts + * let myContract = new MyContract(address); + * let account = Mina.getAccount(address); + * + * let x = myContract.x.fromAppState(account.zkapp!.appState); + * ``` + */ + fromAppState(appState: Field[]): A; }; function State(): State { return createState(); @@ -245,6 +291,12 @@ function createState(): InternalStateType { return state; }, + getAndAssertEquals() { + let state = this.get(); + this.assertEquals(state); + return state; + }, + async fetch() { if (this._contract === undefined) throw Error( @@ -272,6 +324,19 @@ function createState(): InternalStateType { } return this._contract.stateType.fromFields(stateAsFields); }, + + fromAppState(appState: Field[]) { + if (this._contract === undefined) + throw Error( + 'fromAppState() can only be called when the State is assigned to a SmartContract @state.' + ); + let layout = getLayoutPosition(this._contract); + let stateAsFields: Field[] = []; + for (let i = 0; i < layout.length; ++i) { + stateAsFields.push(appState[layout.offset + i]); + } + return this._contract.stateType.fromFields(stateAsFields); + }, }; } diff --git a/src/lib/token.test.ts b/src/lib/token.test.ts index 4fe31bddb2..263184d3cc 100644 --- a/src/lib/token.test.ts +++ b/src/lib/token.test.ts @@ -12,11 +12,11 @@ import { method, PublicKey, Permissions, - Token, VerificationKey, Field, Experimental, Int64, + TokenId, } from 'snarkyjs'; const tokenSymbol = 'TOKEN'; @@ -229,21 +229,13 @@ describe('Token', () => { }); test('correct token id can be derived with an existing token owner', () => { - expect(tokenId).toEqual(Token.getId(tokenZkappAddress)); + expect(tokenId).toEqual(TokenId.derive(tokenZkappAddress)); }); test('deployed token contract exists in the ledger', () => { expect(Mina.getAccount(tokenZkappAddress, tokenId)).toBeDefined(); }); - test('create a valid token with a different parentTokenId', async () => { - const newTokenId = new Token({ - tokenOwner: zkAppBAddress, - parentTokenId: tokenId, - }).id; - expect(newTokenId).toBeDefined(); - }); - test('setting a valid token symbol on a token contract', async () => { await ( await Mina.transaction({ sender: feePayer }, () => { diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 795fd71c19..e9f49a8e07 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -27,6 +27,7 @@ import { zkAppProver, ZkappPublicInput, ZkappStateLength, + SmartContractContext, } from './account_update.js'; import { Circuit, @@ -55,7 +56,6 @@ import { GenericArgument, getPreviousProofsForProver, inAnalyze, - inCheckedComputation, inCompile, inProver, isAsFields, @@ -171,19 +171,16 @@ function wrapMethod( // TODO: the callback case is actually more similar to the composability // case below, should reconcile with that to get the same callData hashing - if (!smartContractContext.has() || smartContractContext()?.isCallback) { - return smartContractContext.runWith( - smartContractContext() ?? { + let insideContract = smartContractContext.get(); + if (!insideContract) { + return smartContractContext.runWith( + { this: this, methodCallDepth: 0, - isCallback: false, selfUpdate: selfAccountUpdate(this, methodName), }, (context) => { - if ( - (inCompile() || inProver() || inAnalyze()) && - !context.isCallback - ) { + if (inCompile() || inProver() || inAnalyze()) { // important to run this with a fresh accountUpdate everytime, otherwise compile messes up our circuits // because it runs this multiple times let proverData = inProver() ? zkAppProver.getData() : undefined; @@ -211,7 +208,7 @@ function wrapMethod( }; let [, result] = memoizationContext.runWith( { ...context, blindingValue }, - () => method.apply(this, actualArgs) + () => method.apply(this, actualArgs.map(cloneCircuitValue)) ); // connects our input + result with callData, so this method can be called @@ -304,9 +301,7 @@ function wrapMethod( // called smart contract at the top level, in a transaction! // => attach ours to the current list of account updates let accountUpdate = context.selfUpdate; - if (!context.isCallback) { - Mina.currentTransaction()?.accountUpdates.push(accountUpdate); - } + Mina.currentTransaction()?.accountUpdates.push(accountUpdate); // first, clone to protect against the method modifying arguments! // TODO: double-check that this works on all possible inputs, e.g. CircuitValue, snarkyjs primitives @@ -338,7 +333,7 @@ function wrapMethod( // connect our input + result with callData, so this method can be called let callDataFields = computeCallData( methodIntf, - actualArgs, + clonedArgs, result, blindingValue ); @@ -369,16 +364,15 @@ function wrapMethod( } // if we're here, this method was called inside _another_ smart contract method - let parentAccountUpdate = smartContractContext.get().this.self; - let methodCallDepth = smartContractContext.get().methodCallDepth; - let [, result] = smartContractContext.runWith( + let parentAccountUpdate = insideContract.this.self; + let methodCallDepth = insideContract.methodCallDepth; + let [, result] = smartContractContext.runWith( { this: this, methodCallDepth: methodCallDepth + 1, - isCallback: false, selfUpdate: selfAccountUpdate(this, methodName), }, - () => { + (innerContext) => { // if the call result is not undefined but there's no known returnType, the returnType was probably not annotated properly, // so we have to explain to the user how to do that let { returnType } = methodIntf; @@ -413,7 +407,7 @@ function wrapMethod( currentIndex: 0, blindingValue: constantBlindingValue, }, - () => method.apply(this, constantArgs) + () => method.apply(this, constantArgs.map(cloneCircuitValue)) ); assertStatePrecondition(this); @@ -469,7 +463,7 @@ function wrapMethod( // we're back in the _caller's_ circuit now, where we assert stuff about the method call // overwrite this.self with the witnessed update, so it's this one we access later in the caller method - smartContractContext.get().selfUpdate = accountUpdate; + innerContext.selfUpdate = accountUpdate; // connect accountUpdate to our own. outside Circuit.witness so compile knows the right structure when hashing children accountUpdate.body.callDepth = parentAccountUpdate.body.callDepth + 1; @@ -492,27 +486,6 @@ function wrapMethod( ); let callData = Poseidon.hash(callDataFields); accountUpdate.body.callData.assertEquals(callData); - - // caller circuits should be Delegate_call by default, except if they're called at the top level - let isTopLevel = Circuit.witness(Bool, () => { - // TODO: this logic is fragile.. need better way of finding out if parent is the prover account update or not - let isProverUpdate = - inProver() && - zkAppProver - .getData() - .accountUpdate.body.publicKey.equals( - parentAccountUpdate.body.publicKey - ) - .toBoolean(); - let parentCallDepth = isProverUpdate - ? zkAppProver.getData().accountUpdate.body.callDepth - : CallForest.computeCallDepth(parentAccountUpdate); - return Bool(parentCallDepth === 0); - }); - parentAccountUpdate.body.mayUseToken = { - parentsOwnToken: isTopLevel.not(), - inheritFromParent: Bool(false), - }; return result; } ); @@ -739,10 +712,11 @@ class SmartContract { * Deploys a {@link SmartContract}. * * ```ts - * let tx = await Mina.transaction(feePayer, () => { - * AccountUpdate.fundNewAccount(feePayer, { initialBalance }); - * zkapp.deploy({ zkappKey }); + * let tx = await Mina.transaction(sender, () => { + * AccountUpdate.fundNewAccount(sender); + * zkapp.deploy(); * }); + * tx.sign([senderKey, zkAppKey]); * ``` */ deploy({ @@ -859,7 +833,7 @@ super.init(); */ get self(): AccountUpdate { let inTransaction = Mina.currentTransaction.has(); - let inSmartContract = smartContractContext.has(); + let inSmartContract = smartContractContext.get(); if (!inTransaction && !inSmartContract) { // TODO: it's inefficient to return a fresh account update everytime, would be better to return a constant "non-writable" account update, // or even expose the .get() methods independently of any account update (they don't need one) @@ -870,8 +844,8 @@ super.init(); // this logic also implies that when calling `this.self` inside a method on `this`, it will always // return the same account update uniquely associated with that method call. // it won't create new updates and add them to a transaction implicitly - if (inSmartContract && smartContractContext.get().this === this) { - let accountUpdate = smartContractContext.get().selfUpdate; + if (inSmartContract && inSmartContract.this === this) { + let accountUpdate = inSmartContract.selfUpdate; this.#executionState = { accountUpdate, transactionId }; return accountUpdate; } @@ -1199,28 +1173,35 @@ super.init(); (err as any).bootstrap = () => ZkappClass.analyzeMethods(); throw err; } - for (let methodIntf of methodIntfs) { - let accountUpdate: AccountUpdate; - let { rows, digest, result, gates } = analyzeMethod( - ZkappPublicInput, - methodIntf, - (publicInput, publicKey, tokenId, ...args) => { - let instance: SmartContract = new ZkappClass(publicKey, tokenId); - let result = (instance as any)[methodIntf.methodName]( - publicInput, - ...args - ); - accountUpdate = instance.#executionState!.accountUpdate; - return result; - } - ); - ZkappClass._methodMetadata[methodIntf.methodName] = { - actions: accountUpdate!.body.actions.data.length, - rows, - digest, - hasReturn: result !== undefined, - gates, - }; + let id: number; + let insideSmartContract = !!smartContractContext.get(); + if (insideSmartContract) id = smartContractContext.enter(null); + try { + for (let methodIntf of methodIntfs) { + let accountUpdate: AccountUpdate; + let { rows, digest, result, gates } = analyzeMethod( + ZkappPublicInput, + methodIntf, + (publicInput, publicKey, tokenId, ...args) => { + let instance: SmartContract = new ZkappClass(publicKey, tokenId); + let result = (instance as any)[methodIntf.methodName]( + publicInput, + ...args + ); + accountUpdate = instance.#executionState!.accountUpdate; + return result; + } + ); + ZkappClass._methodMetadata[methodIntf.methodName] = { + actions: accountUpdate!.body.actions.data.length, + rows, + digest, + hasReturn: result !== undefined, + gates, + }; + } + } finally { + if (insideSmartContract) smartContractContext.leave(id!); } } return ZkappClass._methodMetadata; @@ -1282,8 +1263,26 @@ type ReducerReturn = { stateType: Provable, reduce: (state: State, action: Action) => State, initial: { state: State; actionsHash: Field }, - options?: { maxTransactionsWithActions?: number } + options?: { + maxTransactionsWithActions?: number; + skipActionStatePrecondition?: boolean; + } ): { state: State; actionsHash: Field }; + /** + * Perform circuit logic for every {@link Action} in the list. + * + * This is a wrapper around {@link reduce} for when you don't need `state`. + * Accepts the `fromActionState` and returns the updated action state. + */ + forEach( + actions: Action[][], + reduce: (action: Action) => void, + fromActionState: Field, + options?: { + maxTransactionsWithActions?: number; + skipActionStatePrecondition?: boolean; + } + ): Field; /** * Fetches the list of previously emitted {@link Action}s by this {@link SmartContract}. * ```ts @@ -1295,7 +1294,7 @@ type ReducerReturn = { getActions({ fromActionState, endActionState, - }: { + }?: { fromActionState?: Field; endActionState?: Field; }): Action[][]; @@ -1341,7 +1340,10 @@ class ${contract.constructor.name} extends SmartContract { stateType: Provable, reduce: (state: S, action: A) => S, { state, actionsHash }: { state: S; actionsHash: Field }, - { maxTransactionsWithActions = 32 } = {} + { + maxTransactionsWithActions = 32, + skipActionStatePrecondition = false, + } = {} ): { state: S; actionsHash: Field } { if (actionLists.length > maxTransactionsWithActions) { throw Error( @@ -1400,13 +1402,33 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number // update state state = Circuit.switch(lengths, stateType, newStates); } - contract.account.actionState.assertEquals(actionsHash); + if (!skipActionStatePrecondition) { + contract.account.actionState.assertEquals(actionsHash); + } return { state, actionsHash }; }, - getActions({ - fromActionState, - endActionState, - }: { + + forEach( + actionLists: A[][], + callback: (action: A) => void, + fromActionState: Field, + config + ): Field { + const stateType = provable(undefined); + let { actionsHash } = this.reduce( + actionLists, + stateType, + (_, action) => { + callback(action); + return undefined; + }, + { state: undefined, actionsHash: fromActionState }, + config + ); + return actionsHash; + }, + + getActions(config?: { fromActionState?: Field; endActionState?: Field; }): A[][] { @@ -1414,7 +1436,7 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number Circuit.asProver(() => { let actions = Mina.getActions( contract.address, - { fromActionState, endActionState }, + config, contract.self.tokenId ); actionsForAccount = actions.map((event) => @@ -1428,16 +1450,13 @@ Use the optional \`maxTransactionsWithActions\` argument to increase this number }); return actionsForAccount; }, - async fetchActions({ - fromActionState, - endActionState, - }: { + async fetchActions(config?: { fromActionState?: Field; endActionState?: Field; }): Promise { let result = await Mina.fetchActions( contract.address, - { fromActionState, endActionState }, + config, contract.self.tokenId ); if ('error' in result) { @@ -1483,7 +1502,7 @@ type DeployArgs = | undefined; function Account(address: PublicKey, tokenId?: Field) { - if (smartContractContext.has()) { + if (smartContractContext.get()) { return AccountUpdate.create(address, tokenId).account; } else { return AccountUpdate.defaultAccountUpdate(address, tokenId).account;