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 fetch events to network API #749

Merged
merged 31 commits into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c5f7512
Add getEventsQuery definition
MartinMinkov Feb 23, 2023
c22a985
feat: Naive implementation for fetch events
MartinMinkov Feb 24, 2023
18abcac
Merge branch 'main' into feat/add-fetch-events
MartinMinkov Feb 27, 2023
898bc8e
Add functionality to remove best tip blocks
MartinMinkov Feb 27, 2023
2758ae6
Remove unused fields on fetchEvents query
MartinMinkov Feb 27, 2023
94b3fec
Add archiveGraphql Endpoint to Mina Network
MartinMinkov Mar 1, 2023
f839961
Add filter options to FetchEvents
MartinMinkov Mar 1, 2023
4279ba8
Switch to use map in Fetch.fetchEvents
MartinMinkov Mar 1, 2023
bddebc6
Add method overloading for Mina.Network
MartinMinkov Mar 2, 2023
c29f1ae
Merge branch 'main' into feat/add-fetch-events
MartinMinkov Mar 2, 2023
416e8eb
Add TODO for best tip issue
MartinMinkov Mar 2, 2023
4e15641
Changelog
MartinMinkov Mar 2, 2023
e7ce0d5
Add comments for fetchEvents
MartinMinkov Mar 2, 2023
3c82101
Remove unneeded line to method overload declaration
MartinMinkov Mar 2, 2023
2cde7f3
Reword Changelog
MartinMinkov Mar 2, 2023
07d2389
Update example using zkapp.fetchEvents
MartinMinkov Mar 2, 2023
f718457
Use real Mina Address in fetchEvents docs
MartinMinkov Mar 2, 2023
b7c41df
Feedback
MartinMinkov Mar 2, 2023
08342ec
Update comments to use unified style
MartinMinkov Mar 8, 2023
5e57898
Add better type saftey
MartinMinkov Mar 8, 2023
68eece9
Add additional block and transaction info to Fetch.fetchEvents
MartinMinkov Mar 8, 2023
dc17392
Add additional network info to zkApp.fetchEvents w/ better typing
MartinMinkov Mar 8, 2023
dc7fa1d
Match local and network versions of fetchEvents
MartinMinkov Mar 8, 2023
fc9c680
Remove toString from localblockchain events
MartinMinkov Mar 8, 2023
b81ad38
Undo previous removal of toString and call toString on network fevents
MartinMinkov Mar 8, 2023
bdcff43
Add toString() to Fetch.fetchEvents to match other APIs
MartinMinkov Mar 8, 2023
0cb36fe
minor
mitschabaude Mar 9, 2023
8aec801
Use SnarkyJS number types
MartinMinkov Mar 9, 2023
591576f
Filter out index type in Fetch.fetchEvents if only 0 is found
MartinMinkov Mar 9, 2023
dff9c56
Merge branch 'main' into feat/add-fetch-events
MartinMinkov Mar 9, 2023
fae7f57
Remove index from events
MartinMinkov Mar 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

> No unreleased changes yet

### Added

- Added a new feature to the library: `fetchEvents` can now be used to fetch events for a specified zkApp from a GraphQL endpoint that implements the schema specified [here](https://github.com/o1-labs/Archive-Node-API/blob/efebc9fd3cfc028f536ae2125e0d2676e2b86cd2/src/schema.ts#L1).
MartinMinkov marked this conversation as resolved.
Show resolved Hide resolved

## [0.9.2](https://github.com/o1-labs/snarkyjs/compare/9c44b9c2...1abdfb70)

### Added
Expand Down
8 changes: 4 additions & 4 deletions src/examples/zkapps/local_events_zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@ await tx.prove();
await tx.sign([feePayerKey]).send();

console.log('---- emitted events: ----');
// fetches all events from zkapp starting slot 0
// fetches all events from zkapp starting block height 0
let events = await zkapp.fetchEvents(UInt32.from(0));
console.log(events);
console.log('---- emitted events: ----');
// fetches all events
events = await zkapp.fetchEvents();
// fetches all events from zkapp starting block height 0 and ending at block height 10
events = await zkapp.fetchEvents(UInt32.from(0), UInt64.from(10));
console.log(events);
console.log('---- emitted events: ----');
// fetches all events second time
// fetches all events
events = await zkapp.fetchEvents();
console.log(events);
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export {
fetchAccount,
fetchLastBlock,
fetchTransactionStatus,
fetchEvents,
TransactionStatus,
addCachedAccount,
setGraphqlEndpoint,
Expand Down
116 changes: 116 additions & 0 deletions src/lib/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,38 @@ export {
fetchMissingData,
fetchTransactionStatus,
TransactionStatus,
EventActionFilterOptions,
getCachedAccount,
getCachedNetwork,
addCachedAccount,
defaultGraphqlEndpoint,
archiveGraphqlEndpoint,
setGraphqlEndpoint,
setArchiveGraphqlEndpoint,
sendZkappQuery,
sendZkapp,
removeJsonQuotes,
fetchEvents,
};

let defaultGraphqlEndpoint = 'none';
let archiveGraphqlEndpoint = 'none';
/**
* Specifies the default GraphQL endpoint.
*/
function setGraphqlEndpoint(graphqlEndpoint: string) {
defaultGraphqlEndpoint = graphqlEndpoint;
}

/**
* Sets up a GraphQL endpoint to be used for fetching information from an Archive Node.
*
* @param {string} - A GraphQL endpoint.
MartinMinkov marked this conversation as resolved.
Show resolved Hide resolved
*/
function setArchiveGraphqlEndpoint(graphqlEndpoint: string) {
archiveGraphqlEndpoint = graphqlEndpoint;
}

/**
* Gets account information on the specified publicKey by performing a GraphQL query
* to the specified endpoint. This will call the 'GetAccountInfo' query which fetches
Expand Down Expand Up @@ -452,6 +466,108 @@ function sendZkappQuery(json: string) {
`;
}

const getEventsQuery = (
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 `{
events(input: { ${input} }) {
blockInfo {
distanceFromMaxBlockHeight
height
}
eventData {
index
data
}
}
}
`;
};

type EventActionFilterOptions = {
to?: UInt32;
from?: UInt32;
};

/**
Asynchronously fetches event data for an account from the Mina Archive Node GraphQL API.
@async
@param {object} accountInfo - The account information object.
@param {string} accountInfo.publicKey - The account public key.
@param {string} [accountInfo.tokenId] - The optional token ID for the account.
@param {string} [graphqlEndpoint=archiveGraphqlEndpoint] - The GraphQL endpoint to query. Defaults to the Archive Node GraphQL API.
@param {object} [filterOptions={}] - The optional filter options object.
@returns {Promise<Array>} A promise that resolves to an array of objects containing event data and block height for the account.
@throws {Error} If the GraphQL request fails or the response is invalid.
@example
const accountInfo = { publicKey: 'B62qiwmXrWn7Cok5VhhB3KvCwyZ7NHHstFGbiU5n7m8s2RqqNW1p1wF' };
const events = await fetchEvents(accountInfo);
console.log(events);
*/
MartinMinkov marked this conversation as resolved.
Show resolved Hide resolved
async function fetchEvents(
accountInfo: { publicKey: string; tokenId?: string },
graphqlEndpoint = archiveGraphqlEndpoint,
MartinMinkov marked this conversation as resolved.
Show resolved Hide resolved
filterOptions: EventActionFilterOptions = {}
): Promise<any> {
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
const { publicKey, tokenId } = accountInfo;
let [response, error] = await makeGraphqlRequest(
getEventsQuery(
publicKey,
tokenId ?? TokenId.toBase58(TokenId.default),
filterOptions
),
graphqlEndpoint
);
if (error) throw Error(error.statusText);
let fetchedEvents = response?.data.events;
if (fetchedEvents === undefined) {
throw Error(
`Failed to fetch events data. Account: ${publicKey} Token: ${tokenId}`
);
}

// TODO: This is a temporary fix. We should be able to fetch the event/action data from any block at the best tip.
// Once https://github.com/o1-labs/Archive-Node-API/issues/7 is resolved, we can remove this.
// If we have multiple blocks returned at the best tip (e.g. distanceFromMaxBlockHeight === 0),
// then filter out the blocks at the best tip. This is because we cannot guarantee that every block
// at the best tip will have the correct event data or guarantee that the specific block data will not
// fork in anyway. If this happens, we delay fetching event data until another block has been added to the network.
let numberOfBestTipBlocks = 0;
for (let i = 0; i < fetchedEvents.length; i++) {
if (fetchedEvents[i].blockInfo.distanceFromMaxBlockHeight === 0) {
numberOfBestTipBlocks++;
}
if (numberOfBestTipBlocks > 1) {
fetchedEvents = fetchedEvents.filter((event: any) => {
return event.blockInfo.distanceFromMaxBlockHeight !== 0;
});
break;
}
}

return fetchedEvents.map((event: any) => {
const events = event.eventData.map(
(eventData: { index: string; data: string[] }) => {
return [eventData.index].concat(eventData.data);
MartinMinkov marked this conversation as resolved.
Show resolved Hide resolved
}
);
return {
events,
height: event.blockInfo.height,
};
});
}

// removes the quotes on JSON keys
function removeJsonQuotes(json: string) {
let cleaned = JSON.stringify(JSON.parse(json), null, 2);
Expand Down
50 changes: 40 additions & 10 deletions src/lib/mina.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,11 @@ interface Mina {
};
accountCreationFee(): UInt64;
sendTransaction(transaction: Transaction): Promise<TransactionId>;
fetchEvents: (publicKey: PublicKey, tokenId?: Field) => any;
fetchEvents: (
publicKey: PublicKey,
tokenId?: Field,
filterOptions?: Fetch.EventActionFilterOptions
) => any;
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
getActions: (
publicKey: PublicKey,
tokenId?: Field
Expand Down Expand Up @@ -460,7 +464,7 @@ function LocalBlockchain({
}
events[addr][tokenId].push({
events: p.body.events,
slot: networkState.globalSlotSinceGenesis.toString(),
height: networkState.blockchainLength.toString(),
});
}

Expand Down Expand Up @@ -587,9 +591,22 @@ LocalBlockchain satisfies (...args: any) => Mina;
/**
* Represents the Mina blockchain running on a real network
*/
function Network(graphqlEndpoint: string): Mina {
function Network(graphqlEndpoint: string): Mina;
function Network(graphqlEndpoints: { mina: string; archive: string }): Mina;
function Network(input: { mina: string; archive: string } | string): Mina {
let accountCreationFee = UInt64.from(defaultAccountCreationFee);
Fetch.setGraphqlEndpoint(graphqlEndpoint);
let graphqlEndpoint: string;
let archiveEndpoint: string;

if (typeof input === 'string') {
graphqlEndpoint = input;
Fetch.setGraphqlEndpoint(graphqlEndpoint);
} else {
graphqlEndpoint = input.mina;
archiveEndpoint = input.archive;
Fetch.setGraphqlEndpoint(graphqlEndpoint);
Fetch.setArchiveGraphqlEndpoint(archiveEndpoint);
}

// copied from mina/genesis_ledgers/berkeley.json
// TODO fetch from graphql instead of hardcoding
Expand Down Expand Up @@ -755,9 +772,18 @@ function Network(graphqlEndpoint: string): Mina {
isFinalRunOutsideCircuit: !hasProofs,
});
},
async fetchEvents() {
throw Error(
'fetchEvents() is not implemented yet for remote blockchains.'
async fetchEvents(
publicKey: PublicKey,
tokenId: Field = TokenId.default,
filterOptions: Fetch.EventActionFilterOptions = {}
) {
let pubKey = publicKey.toBase58();
let token = TokenId.toBase58(tokenId);

return await Fetch.fetchEvents(
{ publicKey: pubKey, tokenId: token },
archiveEndpoint,
filterOptions
);
},
getActions() {
Expand Down Expand Up @@ -837,7 +863,7 @@ let activeInstance: Mina = {
async transaction(sender: DeprecatedFeePayerSpec, f: () => void) {
return createTransaction(sender, f, 0);
},
fetchEvents() {
fetchEvents(publicKey: PublicKey, tokenId: Field = TokenId.default) {
throw Error('must call Mina.setActiveInstance first');
},
getActions() {
Expand Down Expand Up @@ -975,8 +1001,12 @@ async function sendTransaction(txn: Transaction) {
/**
* @return A list of emitted events associated to the given public key.
*/
async function fetchEvents(publicKey: PublicKey, tokenId: Field) {
return await activeInstance.fetchEvents(publicKey, tokenId);
async function fetchEvents(
publicKey: PublicKey,
tokenId: Field,
filterOptions: Fetch.EventActionFilterOptions = {}
) {
return await activeInstance.fetchEvents(publicKey, tokenId, filterOptions);
}

/**
Expand Down
29 changes: 22 additions & 7 deletions src/lib/zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,21 +1036,36 @@ super.init();
}

/**
* Fetches a list of events that have been emitted by this {@link SmartContract}.
*/
Asynchronously fetches events emitted by this {@link SmartContract} and returns an array of events with their corresponding types.
@async
@param {UInt32} [start=UInt32.from(0)] - The start height of the events to fetch.
@param {UInt32} [end] - The end height of the events to fetch. If not provided, fetches events up to the latest height.
@returns {Promise<Array>} A promise that resolves to an array of objects, each containing the event type and event data for the specified range.
@throws {Error} If there is an error fetching events from the Mina network.
@example
const startHeight = UInt32.from(1000);
const endHeight = UInt32.from(2000);
const events = await myZkapp.fetchEvents(startHeight, endHeight);
console.log(events);
*/
MartinMinkov marked this conversation as resolved.
Show resolved Hide resolved
async fetchEvents(
start: UInt32 = UInt32.from(0),
end?: UInt32
): Promise<{ type: string; event: ProvablePure<any> }[]> {
// filters all elements so that they are within the given range
// only returns { type: "", event: [] } in a flat format
let events = (await Mina.fetchEvents(this.address, this.self.body.tokenId))
let events = (
await Mina.fetchEvents(this.address, this.self.body.tokenId, {
from: start,
to: end,
})
)
.filter((el: any) => {
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
let slot = UInt32.from(el.slot);
let height = UInt32.from(el.height);
return end === undefined
? start.lessThanOrEqual(slot).toBoolean()
: start.lessThanOrEqual(slot).toBoolean() &&
slot.lessThanOrEqual(end).toBoolean();
? start.lessThanOrEqual(height).toBoolean()
: start.lessThanOrEqual(height).toBoolean() &&
height.lessThanOrEqual(end).toBoolean();
})
.map((el: any) => el.events)
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
.flat();
Expand Down