Skip to content
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

Add get actions to network API #788

Merged
merged 11 commits into from
Mar 15, 2023
Merged

Add get actions to network API #788

merged 11 commits into from
Mar 15, 2023

Conversation

MartinMinkov
Copy link
Contributor

@MartinMinkov MartinMinkov commented Mar 13, 2023

Description

Adds getActions to the network API. Uses a GraphQL server implements the following schema

getActions is used in methods, so it cannot be asynchronous. Instead, we use the caching system already wired up to fetch account and network data. We create a new action cache and add it to the promises executed in fetchMissingData. When fetchMissingData is called, fetchActions queries the GraphQL schema and then caches the results, which can then be used by methods of a smart contract in a non-asynchronous way.

Tested

Manual Testing at these addresses:
https://berkeley.minaexplorer.com/wallet/B62qnL4b3wuAPwFb5E7ChYgDJFdxZgmud2kkx2N8RzNaq4ykagfzumy
https://berkeley.minaexplorer.com/wallet/B62qp7bmbYVpuhqRFmmMDGWcZQBavf9T5Lo1Jfj7yjSaPx2awYcTTvF
https://berkeley.minaexplorer.com/wallet/B62qp2SfYeLBo3p1ZwMS33qtqn6qCnAjAMXzTy6ZAYzYG8DLTr8xqhS

Discussion

Do we want to change the return types for actions to include the extra data returned from the GraphQL server? I left the data inside the query, but if it's not essential, I will remove it to save on network traffic.

Example Code

import {
  Circuit,
  Field,
  state,
  State,
  method,
  PrivateKey,
  SmartContract,
  isReady,
  PublicKey,
  Permissions,
  Reducer,
} from 'snarkyjs';

await isReady;
let initialState = Field(1);
const INCREMENT = Field(1);

export class SimpleZkapp extends SmartContract {
  @state(Field) x = State<Field>();
  @state(Field) y = State<Field>();

  @state(Field) counter = State<Field>();
  @state(Field) actionsHash = State<Field>();

  events = {
    updateValue: Field,
    newState: Field,
    address: PublicKey,
  };

  reducer = Reducer({ actionType: Field });

  init(zkappKey: PrivateKey) {
    super.init(zkappKey);
    this.x.set(initialState);
    this.y.set(Field(0));
    this.counter.set(Field(0));
    this.actionsHash.set(Reducer.initialActionsHash);
    this.account.permissions.set({
      ...Permissions.default(),
      send: Permissions.proofOrSignature(),
      receive: Permissions.proofOrSignature(),
      editSequenceState: Permissions.proofOrSignature(),
    });
  }

  @method rollupIncrements() {
    // get previous counter & actions hash, assert that they're the same as on-chain values
    let counter = this.counter.get();
    this.counter.assertEquals(counter);
    let actionsHash = this.actionsHash.get();
    this.actionsHash.assertEquals(actionsHash);

    Circuit.log('actionsHash', actionsHash);

    // compute the new counter and hash from pending actions
    let pendingActions = this.reducer.getActions({
      fromActionHash: actionsHash,
    });

    Circuit.log('pendingActions', pendingActions);

    let { state: newCounter, actionsHash: newActionsHash } =
      this.reducer.reduce(
        pendingActions,
        // state type
        Field,
        // function that says how to apply an action
        (state: Field, _action: Field) => {
          return state.add(1);
        },
        { state: counter, actionsHash }
      );

    // update on-chain state
    this.counter.set(newCounter);
    this.actionsHash.set(newActionsHash);
  }

  @method update(z: Field): Field {
    this.emitEvent('updateValue', z);
    let x = this.x.get();
    this.x.assertEquals(x);
    let newX = x.add(z);
    this.x.set(newX);
    this.y.set(z);
    this.emitEvent('newState', newX);
    this.emitEvent('address', this.address);

    this.reducer.dispatch(INCREMENT);

    return newX;
  }
}
import { Field, PrivateKey, Mina, isReady, shutdown } from 'snarkyjs';

await isReady;

import { SimpleZkapp } from './SimpleZkApp.js';
let Berkeley = Mina.Network({
  mina: 'https://proxy.berkeley.minaexplorer.com/graphql',
  archive: 'https://archive.berkeley.minaexplorer.com/', // https://api.minascan.io/archive/berkeley/v1/graphql/ or https://archive.berkeley.minaexplorer.com/
});
Mina.setActiveInstance(Berkeley);

async function run(keypair: { privateKey: string; publicKey: string }) {
  let { privateKey } = keypair;
  let senderKey = PrivateKey.fromBase58(privateKey);
  let sender = senderKey.toPublicKey();

  // the zkapp account
  let zkappKey = senderKey;
  let zkappAddress = sender;

  let zkapp = new SimpleZkapp(zkappAddress);

  console.log('compile');
  let { verificationKey } = await SimpleZkapp.compile();

  console.log('done compiling');

  let x = await zkapp.x.fetch();
  let isDeployed = x?.equals(0).not().toBoolean() ?? false;
  let transactionFee = 200_000_000;

  let rollUp = true;

  console.log('deploying? ', isDeployed);

  if (isDeployed) {
    console.log('Updating state...');
    let x = zkapp.x.get();
    let transaction;
    if (rollUp) {
      console.log(`Found deployed zkapp, autobots, rollout`);
      transaction = await Mina.transaction(
        { sender: zkappAddress, fee: transactionFee },
        () => {
          zkapp.rollupIncrements();
        }
      );
    } else {
      console.log(`Found deployed zkapp, updating state ${x} -> ${x.add(10)}.`);
      transaction = await Mina.transaction(
        { sender: zkappAddress, fee: transactionFee },
        () => {
          zkapp.update(Field(10));
        }
      );
    }

    console.log('Creating an execution proof...');
    await transaction.prove();
    console.log('Sending the transaction...');
    await transaction.sign([zkappKey]).send();
  } else {
    console.log(`Deploying zkapp for public key ${zkappAddress.toBase58()}.`);
    // the `transaction()` interface is the same as when testing with a local blockchain
    let transaction = await Mina.transaction(
      { sender: zkappAddress, fee: transactionFee },
      () => {
        zkapp.deploy({ verificationKey });
      }
    );
    console.log('Sending the transaction...');
    let transactionId = await transaction.sign([zkappKey]).send();
    console.log('Transaction sent, id: ', transactionId.hash());
  }
}

@MartinMinkov MartinMinkov marked this pull request as ready for review March 14, 2023 03:57
@MartinMinkov MartinMinkov requested review from mitschabaude, Trivo25 and bkase and removed request for mitschabaude and Trivo25 March 14, 2023 03:59
@Trivo25
Copy link
Member

Trivo25 commented Mar 14, 2023

Do we want to change the return types for actions to include the extra data returned from the GraphQL server? I left the data inside the query, but if it's not essential, I will remove it to save on network traffic.

I would approach this similar to fetchEvents. When someone invokes Mina.fetchEvents, they get some more information whereas when they invoke it within a smart contract, we would just get the action data. Either way, imo this can be something we can add later as well

Copy link
Member

@Trivo25 Trivo25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

src/lib/fetch.ts Outdated Show resolved Hide resolved
Copy link
Collaborator

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look great! Approving in advance with one comment that needs to be addressed

src/lib/fetch.ts Outdated Show resolved Hide resolved
src/lib/mina.ts Outdated Show resolved Hide resolved
const actionData = fetchedActions
.map((action) => {
return {
hash: Ledger.fieldToBase58(Field(action.actionState)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't need to be fixed here, but it seems to me the field => base58 conversion is done both and in the local blockchain version, and would save computation to just leave the hash as a field element in both cases

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good idea, but I have to change a bunch of type definitions to make that work :/ I'll create an issue so it can be addressed later.

@MartinMinkov MartinMinkov merged commit 9acec55 into main Mar 15, 2023
@MartinMinkov MartinMinkov deleted the feat/add-actions branch March 15, 2023 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants