Skip to content

Commit 2f4d437

Browse files
authored
Merge pull request #1917 from o1-labs/2024-11-actions-sort-by-tx-sequence
Sort actions by tx sequence when available
2 parents a5c15ad + 5ef0bdc commit 2f4d437

File tree

7 files changed

+420
-217
lines changed

7 files changed

+420
-217
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1717

1818
## [Unreleased](https://github.com/o1-labs/o1js/compare/b857516...HEAD)
1919

20+
### Changed
21+
- Sort order for actions now includes the transaction sequence number and the exact account id sequence https://github.com/o1-labs/o1js/pull/1917
22+
2023
## [2.2.0](https://github.com/o1-labs/o1js/compare/e1bac02...b857516) - 2024-12-10
2124

2225
### Added

src/lib/mina/fetch.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,11 @@ function sendZkapp(
641641
* @returns A promise that resolves to an array of objects containing event data, block information and transaction information for the account.
642642
* @throws If the GraphQL request fails or the response is invalid.
643643
* @example
644+
* ```ts
644645
* const accountInfo = { publicKey: 'B62qiwmXrWn7Cok5VhhB3KvCwyZ7NHHstFGbiU5n7m8s2RqqNW1p1wF' };
645646
* const events = await fetchEvents(accountInfo);
646647
* console.log(events);
648+
* ```
647649
*/
648650
async function fetchEvents(
649651
accountInfo: { publicKey: string; tokenId?: string },
@@ -691,10 +693,32 @@ async function fetchEvents(
691693
});
692694
}
693695

696+
/**
697+
* Fetches account actions for a specified public key and token ID by performing a GraphQL query.
698+
*
699+
* @param accountInfo - An {@link ActionsQueryInputs} containing the public key, and optional query parameters for the actions query
700+
* @param graphqlEndpoint - The GraphQL endpoint to fetch from. Defaults to the configured Mina endpoint.
701+
*
702+
* @returns A promise that resolves to an object containing the final actions hash for the account, and a list of actions
703+
* @throws Will throw an error if the GraphQL endpoint is invalid or if the fetch request fails.
704+
*
705+
* @example
706+
* ```ts
707+
* const accountInfo = { publicKey: 'B62qiwmXrWn7Cok5VhhB3KvCwyZ7NHHstFGbiU5n7m8s2RqqNW1p1wF' };
708+
* const actionsList = await fetchAccount(accountInfo);
709+
* console.log(actionsList);
710+
* ```
711+
*/
694712
async function fetchActions(
695713
accountInfo: ActionsQueryInputs,
696714
graphqlEndpoint = networkConfig.archiveEndpoint
697-
) {
715+
): Promise<
716+
| {
717+
actions: string[][];
718+
hash: string;
719+
}[]
720+
| { error: FetchError }
721+
> {
698722
if (!graphqlEndpoint)
699723
throw Error(
700724
'fetchActions: Specified GraphQL endpoint is undefined. When using actions, you must set the archive node endpoint in Mina.Network(). Please ensure your Mina.Network() configuration includes an archive node endpoint.'
@@ -710,7 +734,26 @@ async function fetchActions(
710734
graphqlEndpoint,
711735
networkConfig.archiveFallbackEndpoints
712736
);
713-
if (error) throw Error(error.statusText);
737+
// As of 2025-01-07, minascan is running a version of the node which supports `sequenceNumber` and `zkappAccountUpdateIds` fields
738+
// We could consider removing this fallback since no other nodes are widely used
739+
if (error) {
740+
const originalError = error;
741+
[response, error] = await makeGraphqlRequest<ActionQueryResponse>(
742+
getActionsQuery(
743+
publicKey,
744+
actionStates,
745+
tokenId,
746+
/* _filterOptions= */ undefined,
747+
/* _excludeTransactionInfo= */ true
748+
),
749+
graphqlEndpoint,
750+
networkConfig.archiveFallbackEndpoints
751+
);
752+
if (error)
753+
throw Error(
754+
`ORIGINAL ERROR: ${originalError.statusText} \n\nRETRY ERROR: ${error.statusText}`
755+
);
756+
}
714757
let fetchedActions = response?.data.actions;
715758
if (fetchedActions === undefined) {
716759
return {
@@ -757,9 +800,33 @@ export function createActionsList(
757800
`No action data was found for the account ${publicKey} with the latest action state ${actionState}`
758801
);
759802

760-
actionData = actionData.sort((a1, a2) => {
761-
return Number(a1.accountUpdateId) < Number(a2.accountUpdateId) ? -1 : 1;
762-
});
803+
// DEPRECATED: In case the archive node is running an out-of-date version, best guess is to sort by the account update id
804+
// As of 2025-01-07, minascan is running a version of the node which supports `sequenceNumber` and `zkappAccountUpdateIds` fields
805+
// We could consider removing this fallback since no other nodes are widely used
806+
if (!actionData[0].transactionInfo) {
807+
actionData = actionData.sort((a1, a2) => {
808+
return Number(a1.accountUpdateId) - Number(a2.accountUpdateId);
809+
});
810+
} else {
811+
// sort actions within one block by transaction sequence number and account update sequence
812+
actionData = actionData.sort((a1, a2) => {
813+
const a1TxSequence = a1.transactionInfo!.sequenceNumber;
814+
const a2TxSequence = a2.transactionInfo!.sequenceNumber;
815+
if (a1TxSequence === a2TxSequence) {
816+
const a1AuSequence =
817+
a1.transactionInfo!.zkappAccountUpdateIds.indexOf(
818+
Number(a1.accountUpdateId)
819+
);
820+
const a2AuSequence =
821+
a2.transactionInfo!.zkappAccountUpdateIds.indexOf(
822+
Number(a2.accountUpdateId)
823+
);
824+
return a1AuSequence - a2AuSequence;
825+
} else {
826+
return a1TxSequence - a2TxSequence;
827+
}
828+
});
829+
}
763830

764831
// split actions by account update
765832
let actionsByAccountUpdate: string[][][] = [];

src/lib/mina/fetch.unit-test.ts

Lines changed: 48 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PrivateKey, TokenId } from 'o1js';
22
import { createActionsList } from './fetch.js';
3-
import { mockFetchActionsResponse } from './fixtures/fetch-actions-response.js';
3+
import { mockFetchActionsResponse as fetchResponseWithTxInfo } from './fixtures/fetch-actions-response-with-transaction-info.js';
4+
import { mockFetchActionsResponse as fetchResponseNoTxInfo } from './fixtures/fetch-actions-response-without-transaction-info.js';
45
import { test, describe } from 'node:test';
56
import { removeJsonQuotes } from './graphql.js';
67
import { expect } from 'expect';
@@ -123,8 +124,8 @@ expect(actual).toEqual(expected);
123124

124125
console.log('regex tests complete 🎉');
125126

126-
describe('Fetch', async (t) => {
127-
describe('#createActionsList with default params', async (t) => {
127+
describe('Fetch', () => {
128+
describe('#createActionsList with default params', () => {
128129
const defaultPublicKey = PrivateKey.random().toPublicKey().toBase58();
129130
const defaultActionStates = {
130131
fromActionState: undefined,
@@ -136,96 +137,50 @@ describe('Fetch', async (t) => {
136137
tokenId: TokenId.default.toString(),
137138
};
138139

139-
const actionsList = createActionsList(
140-
defaultAccountInfo,
141-
mockFetchActionsResponse.data.actions
142-
);
143-
144-
await test('orders the actions correctly', async () => {
145-
expect(actionsList).toEqual([
146-
{
147-
actions: [
148-
[
149-
'20374659537065244088703638031937922870146667362923279084491778322749365537089',
150-
'1',
151-
],
152-
],
153-
hash: '10619825168606131449407092474314250900469658818945385329390497057469974757422',
154-
},
155-
{
156-
actions: [
157-
[
158-
'20503089751358270987184701275168489753952341816059774976784079526478451099801',
159-
'1',
160-
],
161-
],
162-
hash: '25525130517416993227046681664758665799110129890808721833148757111140891481208',
163-
},
164-
{
165-
actions: [
166-
[
167-
'3374074164183544078218789545772953663729921088152354292852793744356608231707',
168-
'0',
169-
],
170-
],
171-
hash: '290963518424616502946790040851348455652296009700336010663574777600482385855',
172-
},
173-
{
174-
actions: [
175-
[
176-
'12630758077588166643924428865613845067150916064939816120404808842510620524633',
177-
'1',
178-
],
179-
],
180-
hash: '20673199655841577810393943638910551364027795297920791498278816237738641857371',
181-
},
182-
{
183-
actions: [
184-
[
185-
'5643224648393140391519847064914429159616501351124129591669928700148350171602',
186-
'0',
187-
],
188-
],
189-
hash: '5284016523143033193387918577616839424871122381326995145988133445906503263869',
190-
},
191-
{
192-
actions: [
193-
[
194-
'15789351988619560045401465240113496854401074115453702466673859303925517061263',
195-
'0',
196-
],
197-
],
198-
hash: '16944163018367910067334012882171366051616125936127175065464614786387687317044',
199-
},
200-
{
201-
actions: [
202-
[
203-
'27263309408256888453299195755797013857604561285332380691270111409680109142128',
204-
'1',
205-
],
206-
],
207-
hash: '23662159967366296714544063539035629952291787828104373633198732070740691309118',
208-
},
209-
{
210-
actions: [
211-
[
212-
'3378367318331499715304980508337843233019278703665446829424824679144818589558',
213-
'1',
214-
],
215-
],
216-
hash: '1589729766029695153975344283092689798747741638003354620355672853210932754595',
217-
},
218-
{
219-
actions: [
220-
[
221-
'17137397755795687855356639427474789131368991089558570411893673365904353943290',
222-
'1',
223-
],
224-
],
225-
hash: '10964420428484427410756859799314206378989718180435238943573393516522086219419',
226-
},
227-
]);
140+
describe('with a payload that is missing transaction info', () => {
141+
const actionsList = createActionsList(
142+
defaultAccountInfo,
143+
fetchResponseNoTxInfo.data.actions
144+
);
145+
146+
test('orders the actions correctly', () => {
147+
const correctActionsHashes = [
148+
'10619825168606131449407092474314250900469658818945385329390497057469974757422',
149+
'25525130517416993227046681664758665799110129890808721833148757111140891481208',
150+
'290963518424616502946790040851348455652296009700336010663574777600482385855',
151+
'20673199655841577810393943638910551364027795297920791498278816237738641857371',
152+
'5284016523143033193387918577616839424871122381326995145988133445906503263869',
153+
'16944163018367910067334012882171366051616125936127175065464614786387687317044',
154+
'23662159967366296714544063539035629952291787828104373633198732070740691309118',
155+
'1589729766029695153975344283092689798747741638003354620355672853210932754595',
156+
'10964420428484427410756859799314206378989718180435238943573393516522086219419',
157+
];
158+
expect(actionsList.map(({ hash }) => hash)).toEqual(
159+
correctActionsHashes
160+
);
161+
});
162+
});
163+
164+
describe('with a payload that includes transaction info', () => {
165+
const actionsList = createActionsList(
166+
defaultAccountInfo,
167+
fetchResponseWithTxInfo.data.actions
168+
);
169+
170+
test('orders the actions correctly', () => {
171+
const correctActionsHashes = [
172+
'23562173419146814432140831830018386191372262558717813981702672868292521523493',
173+
'17091049856171838105194364005412166905307014398334933913160405653259432088216',
174+
'17232885850087529233459756382038742870248640044940153006158312935267918515979',
175+
'12636308717155378495657553296284990333618148856424346334743675423201692801125',
176+
'17082487567758469425757467457967473265642001333824907522427890208991758759731',
177+
'14226491442770650712364681911870921131508915865197379983185088742764625929348',
178+
'13552033292375176242184292341671233419412691991179711376625259275814019808194',
179+
];
180+
expect(actionsList.map(({ hash }) => hash)).toEqual(
181+
correctActionsHashes
182+
);
183+
});
228184
});
229185
});
230186
});
231-
``;

0 commit comments

Comments
 (0)