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
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!--
Expand All @@ -19,9 +19,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Added

- Use `fetchEvents()` to fetch events for a specified zkApp from a GraphQL endpoint that implements [this schema](https://github.com/o1-labs/Archive-Node-API/blob/efebc9fd3cfc028f536ae2125e0d2676e2b86cd2/src/schema.ts#L1). `Mina.Network` accepts an additional endpoint which points to a GraphQL server.
- Use the `mina` property for the Mina node.
- Use `archive` for the archive node.
- Use `fetchEvents()` to fetch events for a specified zkApp from a GraphQL endpoint that implements [this schema](https://github.com/o1-labs/Archive-Node-API/blob/efebc9fd3cfc028f536ae2125e0d2676e2b86cd2/src/schema.ts#L1). `Mina.Network` accepts an additional endpoint which points to a GraphQL server.
- Use the `mina` property for the Mina node.
- Use `archive` for the archive node.
- Use `getActions` to fetch actions for a specified zkApp from a GraphQL endpoint GraphQL endpoint that implements the same schema as `fetchEvents`.

### Fixed

Expand Down
160 changes: 149 additions & 11 deletions src/lib/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'isomorphic-fetch';
import { Field } from '../snarky.js';
import { Field, Ledger } from '../snarky.js';
import { UInt32, UInt64 } from './int.js';
import { TokenId } from './account_update.js';
import { PublicKey } from './signature.js';
Expand All @@ -21,12 +21,14 @@ export {
parseFetchedAccount,
markAccountToBeFetched,
markNetworkToBeFetched,
markActionsToBeFetched,
fetchMissingData,
fetchTransactionStatus,
TransactionStatus,
EventActionFilterOptions,
getCachedAccount,
getCachedNetwork,
getCachedActions,
addCachedAccount,
defaultGraphqlEndpoint,
archiveGraphqlEndpoint,
Expand All @@ -36,6 +38,7 @@ export {
sendZkapp,
removeJsonQuotes,
fetchEvents,
fetchActions,
};

let defaultGraphqlEndpoint = 'none';
Expand Down Expand Up @@ -82,11 +85,11 @@ async function fetchAccount(
accountInfo.publicKey instanceof PublicKey
? accountInfo.publicKey.toBase58()
: accountInfo.publicKey;
let tokenIdBase58 =
typeof accountInfo.tokenId === "string" || !accountInfo.tokenId
let tokenIdBase58 =
typeof accountInfo.tokenId === 'string' || !accountInfo.tokenId
? accountInfo.tokenId
: TokenId.toBase58(accountInfo.tokenId)
: TokenId.toBase58(accountInfo.tokenId);

return await fetchAccountInternal(
{ publicKey: publicKeyBase58, tokenId: tokenIdBase58 },
graphqlEndpoint,
Expand Down Expand Up @@ -155,11 +158,23 @@ let networkCache = {} as Record<
timestamp: number;
}
>;
let actionsCache = {} as Record<
string,
{
actions: { hash: string; actions: string[][] }[];
graphqlEndpoint: string;
timestamp: number;
}
>;
let accountsToFetch = {} as Record<
string,
{ publicKey: string; tokenId: string; graphqlEndpoint: string }
>;
let networksToFetch = {} as Record<string, { graphqlEndpoint: string }>;
let actionsToFetch = {} as Record<
string,
{ publicKey: string; tokenId: string; graphqlEndpoint: string }
>;

function markAccountToBeFetched(
publicKey: PublicKey,
Expand All @@ -177,8 +192,24 @@ function markAccountToBeFetched(
function markNetworkToBeFetched(graphqlEndpoint: string) {
networksToFetch[graphqlEndpoint] = { graphqlEndpoint };
}
function markActionsToBeFetched(
publicKey: PublicKey,
tokenId: Field,
graphqlEndpoint: string
) {
let publicKeyBase58 = publicKey.toBase58();
let tokenBase58 = TokenId.toBase58(tokenId);
actionsToFetch[`${publicKeyBase58};${tokenBase58};${graphqlEndpoint}`] = {
publicKey: publicKeyBase58,
tokenId: tokenBase58,
graphqlEndpoint,
};
}

async function fetchMissingData(graphqlEndpoint: string) {
async function fetchMissingData(
graphqlEndpoint: string,
archiveEndpoint?: string
) {
let promises = Object.entries(accountsToFetch).map(
async ([key, { publicKey, tokenId }]) => {
let response = await fetchAccountInternal(
Expand All @@ -188,6 +219,17 @@ async function fetchMissingData(graphqlEndpoint: string) {
if (response.error === undefined) delete accountsToFetch[key];
}
);
let actionPromises = Object.entries(actionsToFetch).map(
async ([key, { publicKey, tokenId }]) => {
let response = await fetchActions(
{ publicKey, tokenId },
archiveEndpoint
);
if (!('error' in response) || response.error === undefined)
delete actionsToFetch[key];
}
);
promises.push(...actionPromises);
let network = Object.entries(networksToFetch).find(([, network]) => {
return network.graphqlEndpoint === graphqlEndpoint;
});
Expand Down Expand Up @@ -217,6 +259,15 @@ function getCachedNetwork(graphqlEndpoint = defaultGraphqlEndpoint) {
return networkCache[graphqlEndpoint]?.network;
}

function getCachedActions(
publicKey: PublicKey,
tokenId: Field,
graphqlEndpoint = archiveGraphqlEndpoint
) {
return actionsCache[accountCacheKey(publicKey, tokenId, graphqlEndpoint)]
?.actions;
}

/**
* Adds an account to the local cache, indexed by a GraphQL endpoint.
*/
Expand All @@ -238,6 +289,20 @@ function addCachedAccountInternal(account: Account, graphqlEndpoint: string) {
};
}

function addCachedActionsInternal(
accountInfo: { publicKey: PublicKey; tokenId: Field },
actions: { hash: string; actions: string[][] }[],
graphqlEndpoint: string
) {
actionsCache[
accountCacheKey(accountInfo.publicKey, accountInfo.tokenId, graphqlEndpoint)
] = {
actions,
graphqlEndpoint,
timestamp: Date.now(),
};
}

function accountCacheKey(
publicKey: PublicKey,
tokenId: Field,
Expand Down Expand Up @@ -471,7 +536,7 @@ function sendZkappQuery(json: string) {
}
`;
}
type FetchedEventActionBase = {
type FetchedEvents = {
blockInfo: {
distanceFromMaxBlockHeight: number;
globalSlotSinceGenesis: number;
Expand All @@ -485,13 +550,16 @@ type FetchedEventActionBase = {
memo: string;
status: string;
};
};
type FetchedEvents = {
eventData: {
index: string;
data: string[];
}[];
} & FetchedEventActionBase;
};
type FetchedActions = {
actionState: string;
actionData: {
data: string[];
}[];
};

type EventActionFilterOptions = {
to?: UInt32;
Expand Down Expand Up @@ -532,6 +600,28 @@ const getEventsQuery = (
}
}`;
};
const getActionsQuery = (
publicKey: string,
tokenId: string,
filterOptions?: EventActionFilterOptions
) => {
const { to, from } = filterOptions ?? {};
let input = `address: "${publicKey}", tokenId: "${tokenId}"`;
if (to !== undefined) {
input += `, to: ${to}`;
}
if (from !== undefined) {
input += `, from: ${from}`;
}
return `{
actions(input: { ${input} }) {
actionState
actionData {
data
}
}
}`;
};

/**
* Asynchronously fetches event data for an account from the Mina Archive Node GraphQL API.
Expand Down Expand Up @@ -610,6 +700,54 @@ async function fetchEvents(
});
}

async function fetchActions(
accountInfo: { publicKey: string; tokenId?: string },
graphqlEndpoint = archiveGraphqlEndpoint,
filterOptions: EventActionFilterOptions = {}
) {
if (!graphqlEndpoint)
throw new Error(
'fetchEvents: Specified GraphQL endpoint is undefined. Please specify a valid endpoint.'
);
const { publicKey, tokenId } = accountInfo;
let [response, error] = await makeGraphqlRequest(
getActionsQuery(
publicKey,
tokenId ?? TokenId.toBase58(TokenId.default),
filterOptions
),
graphqlEndpoint
);
if (error) throw Error(error.statusText);
let fetchedActions = response?.data.actions as FetchedActions[];
if (fetchedActions === undefined) {
return {
error: {
statusCode: 404,
statusText: `fetchActions: Account with public key ${publicKey} with tokenId ${tokenId} does not exist.`,
},
};
}

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.

actions: action.actionData.map((actionData) => actionData.data),
};
})
.reverse(); // Reverse the order of actions since the API returns in descending order of block height while Localblockchain pushes new actions to end of array.
addCachedActionsInternal(
{
publicKey: PublicKey.fromBase58(publicKey),
tokenId: TokenId.fromBase58(tokenId ?? TokenId.toBase58(TokenId.default)),
},
actionData,
graphqlEndpoint
);
return actionData;
}

// removes the quotes on JSON keys
function removeJsonQuotes(json: string) {
let cleaned = JSON.stringify(JSON.parse(json), null, 2);
Expand Down
22 changes: 17 additions & 5 deletions src/lib/mina.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ function Network(input: { mina: string; archive: string } | string): Mina {
fetchMode: 'test',
isFinalRunOutsideCircuit: false,
});
await Fetch.fetchMissingData(graphqlEndpoint);
await Fetch.fetchMissingData(graphqlEndpoint, archiveEndpoint);
let hasProofs = tx.transaction.accountUpdates.some(
Authorization.hasLazyProof
);
Expand All @@ -797,9 +797,21 @@ function Network(input: { mina: string; archive: string } | string): Mina {
filterOptions
);
},
getActions() {
getActions(publicKey: PublicKey, tokenId: Field = TokenId.default) {
if (currentTransaction()?.fetchMode === 'test') {
Fetch.markActionsToBeFetched(publicKey, tokenId, archiveEndpoint);
let actions = Fetch.getCachedActions(publicKey, tokenId);
return actions ?? [];
}
if (
!currentTransaction.has() ||
currentTransaction.get().fetchMode === 'cached'
) {
let actions = Fetch.getCachedActions(publicKey, tokenId);
if (actions !== undefined) return actions;
}
throw Error(
'fetchEvents() is not implemented yet for remote blockchains.'
`getActions: Could not find actions for the public key ${publicKey}`
);
},
proofsEnabled: true,
Expand Down Expand Up @@ -877,7 +889,7 @@ let activeInstance: Mina = {
fetchEvents(publicKey: PublicKey, tokenId: Field = TokenId.default) {
throw Error('must call Mina.setActiveInstance first');
},
getActions() {
getActions(publicKey: PublicKey, tokenId: Field = TokenId.default) {
throw Error('must call Mina.setActiveInstance first');
},
proofsEnabled: true,
Expand Down Expand Up @@ -1023,7 +1035,7 @@ async function fetchEvents(
/**
* @return A list of emitted sequencing actions associated to the given public key.
*/
function getActions(publicKey: PublicKey, tokenId: Field) {
function getActions(publicKey: PublicKey, tokenId?: Field) {
return activeInstance.getActions(publicKey, tokenId);
}

Expand Down