diff --git a/CHANGELOG.md b/CHANGELOG.md index 908631c47c..d6a8f20ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - `setFee` and `setFeePerWU` for `Transaction` and `PendingTransaction` +### Changed +- Sort order for actions now includes the transaction sequence number and the exact account id sequence https://github.com/o1-labs/o1js/pull/1917 + ## [2.2.0](https://github.com/o1-labs/o1js/compare/e1bac02...b857516) - 2024-12-10 ### Added diff --git a/src/lib/mina/fetch.ts b/src/lib/mina/fetch.ts index 8165bad6f7..f2269001f8 100644 --- a/src/lib/mina/fetch.ts +++ b/src/lib/mina/fetch.ts @@ -641,9 +641,11 @@ function sendZkapp( * @returns A promise that resolves to an array of objects containing event data, block information and transaction information for the account. * @throws If the GraphQL request fails or the response is invalid. * @example + * ```ts * const accountInfo = { publicKey: 'B62qiwmXrWn7Cok5VhhB3KvCwyZ7NHHstFGbiU5n7m8s2RqqNW1p1wF' }; * const events = await fetchEvents(accountInfo); * console.log(events); + * ``` */ async function fetchEvents( accountInfo: { publicKey: string; tokenId?: string }, @@ -691,10 +693,32 @@ async function fetchEvents( }); } +/** + * Fetches account actions for a specified public key and token ID by performing a GraphQL query. + * + * @param accountInfo - An {@link ActionsQueryInputs} containing the public key, and optional query parameters for the actions query + * @param graphqlEndpoint - The GraphQL endpoint to fetch from. Defaults to the configured Mina endpoint. + * + * @returns A promise that resolves to an object containing the final actions hash for the account, and a list of actions + * @throws Will throw an error if the GraphQL endpoint is invalid or if the fetch request fails. + * + * @example + * ```ts + * const accountInfo = { publicKey: 'B62qiwmXrWn7Cok5VhhB3KvCwyZ7NHHstFGbiU5n7m8s2RqqNW1p1wF' }; + * const actionsList = await fetchAccount(accountInfo); + * console.log(actionsList); + * ``` + */ async function fetchActions( accountInfo: ActionsQueryInputs, graphqlEndpoint = networkConfig.archiveEndpoint -) { +): Promise< + | { + actions: string[][]; + hash: string; + }[] + | { error: FetchError } +> { if (!graphqlEndpoint) throw Error( '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( graphqlEndpoint, networkConfig.archiveFallbackEndpoints ); - if (error) throw Error(error.statusText); + // As of 2025-01-07, minascan is running a version of the node which supports `sequenceNumber` and `zkappAccountUpdateIds` fields + // We could consider removing this fallback since no other nodes are widely used + if (error) { + const originalError = error; + [response, error] = await makeGraphqlRequest( + getActionsQuery( + publicKey, + actionStates, + tokenId, + /* _filterOptions= */ undefined, + /* _excludeTransactionInfo= */ true + ), + graphqlEndpoint, + networkConfig.archiveFallbackEndpoints + ); + if (error) + throw Error( + `ORIGINAL ERROR: ${originalError.statusText} \n\nRETRY ERROR: ${error.statusText}` + ); + } let fetchedActions = response?.data.actions; if (fetchedActions === undefined) { return { @@ -757,9 +800,33 @@ export function createActionsList( `No action data was found for the account ${publicKey} with the latest action state ${actionState}` ); - actionData = actionData.sort((a1, a2) => { - return Number(a1.accountUpdateId) < Number(a2.accountUpdateId) ? -1 : 1; - }); + // DEPRECATED: In case the archive node is running an out-of-date version, best guess is to sort by the account update id + // As of 2025-01-07, minascan is running a version of the node which supports `sequenceNumber` and `zkappAccountUpdateIds` fields + // We could consider removing this fallback since no other nodes are widely used + if (!actionData[0].transactionInfo) { + actionData = actionData.sort((a1, a2) => { + return Number(a1.accountUpdateId) - Number(a2.accountUpdateId); + }); + } else { + // sort actions within one block by transaction sequence number and account update sequence + actionData = actionData.sort((a1, a2) => { + const a1TxSequence = a1.transactionInfo!.sequenceNumber; + const a2TxSequence = a2.transactionInfo!.sequenceNumber; + if (a1TxSequence === a2TxSequence) { + const a1AuSequence = + a1.transactionInfo!.zkappAccountUpdateIds.indexOf( + Number(a1.accountUpdateId) + ); + const a2AuSequence = + a2.transactionInfo!.zkappAccountUpdateIds.indexOf( + Number(a2.accountUpdateId) + ); + return a1AuSequence - a2AuSequence; + } else { + return a1TxSequence - a2TxSequence; + } + }); + } // split actions by account update let actionsByAccountUpdate: string[][][] = []; diff --git a/src/lib/mina/fetch.unit-test.ts b/src/lib/mina/fetch.unit-test.ts index 3f0d662cec..d3273e9b5e 100644 --- a/src/lib/mina/fetch.unit-test.ts +++ b/src/lib/mina/fetch.unit-test.ts @@ -1,6 +1,7 @@ import { PrivateKey, TokenId } from 'o1js'; import { createActionsList } from './fetch.js'; -import { mockFetchActionsResponse } from './fixtures/fetch-actions-response.js'; +import { mockFetchActionsResponse as fetchResponseWithTxInfo } from './fixtures/fetch-actions-response-with-transaction-info.js'; +import { mockFetchActionsResponse as fetchResponseNoTxInfo } from './fixtures/fetch-actions-response-without-transaction-info.js'; import { test, describe } from 'node:test'; import { removeJsonQuotes } from './graphql.js'; import { expect } from 'expect'; @@ -123,8 +124,8 @@ expect(actual).toEqual(expected); console.log('regex tests complete 🎉'); -describe('Fetch', async (t) => { - describe('#createActionsList with default params', async (t) => { +describe('Fetch', () => { + describe('#createActionsList with default params', () => { const defaultPublicKey = PrivateKey.random().toPublicKey().toBase58(); const defaultActionStates = { fromActionState: undefined, @@ -136,96 +137,50 @@ describe('Fetch', async (t) => { tokenId: TokenId.default.toString(), }; - const actionsList = createActionsList( - defaultAccountInfo, - mockFetchActionsResponse.data.actions - ); - - await test('orders the actions correctly', async () => { - expect(actionsList).toEqual([ - { - actions: [ - [ - '20374659537065244088703638031937922870146667362923279084491778322749365537089', - '1', - ], - ], - hash: '10619825168606131449407092474314250900469658818945385329390497057469974757422', - }, - { - actions: [ - [ - '20503089751358270987184701275168489753952341816059774976784079526478451099801', - '1', - ], - ], - hash: '25525130517416993227046681664758665799110129890808721833148757111140891481208', - }, - { - actions: [ - [ - '3374074164183544078218789545772953663729921088152354292852793744356608231707', - '0', - ], - ], - hash: '290963518424616502946790040851348455652296009700336010663574777600482385855', - }, - { - actions: [ - [ - '12630758077588166643924428865613845067150916064939816120404808842510620524633', - '1', - ], - ], - hash: '20673199655841577810393943638910551364027795297920791498278816237738641857371', - }, - { - actions: [ - [ - '5643224648393140391519847064914429159616501351124129591669928700148350171602', - '0', - ], - ], - hash: '5284016523143033193387918577616839424871122381326995145988133445906503263869', - }, - { - actions: [ - [ - '15789351988619560045401465240113496854401074115453702466673859303925517061263', - '0', - ], - ], - hash: '16944163018367910067334012882171366051616125936127175065464614786387687317044', - }, - { - actions: [ - [ - '27263309408256888453299195755797013857604561285332380691270111409680109142128', - '1', - ], - ], - hash: '23662159967366296714544063539035629952291787828104373633198732070740691309118', - }, - { - actions: [ - [ - '3378367318331499715304980508337843233019278703665446829424824679144818589558', - '1', - ], - ], - hash: '1589729766029695153975344283092689798747741638003354620355672853210932754595', - }, - { - actions: [ - [ - '17137397755795687855356639427474789131368991089558570411893673365904353943290', - '1', - ], - ], - hash: '10964420428484427410756859799314206378989718180435238943573393516522086219419', - }, - ]); + describe('with a payload that is missing transaction info', () => { + const actionsList = createActionsList( + defaultAccountInfo, + fetchResponseNoTxInfo.data.actions + ); + + test('orders the actions correctly', () => { + const correctActionsHashes = [ + '10619825168606131449407092474314250900469658818945385329390497057469974757422', + '25525130517416993227046681664758665799110129890808721833148757111140891481208', + '290963518424616502946790040851348455652296009700336010663574777600482385855', + '20673199655841577810393943638910551364027795297920791498278816237738641857371', + '5284016523143033193387918577616839424871122381326995145988133445906503263869', + '16944163018367910067334012882171366051616125936127175065464614786387687317044', + '23662159967366296714544063539035629952291787828104373633198732070740691309118', + '1589729766029695153975344283092689798747741638003354620355672853210932754595', + '10964420428484427410756859799314206378989718180435238943573393516522086219419', + ]; + expect(actionsList.map(({ hash }) => hash)).toEqual( + correctActionsHashes + ); + }); + }); + + describe('with a payload that includes transaction info', () => { + const actionsList = createActionsList( + defaultAccountInfo, + fetchResponseWithTxInfo.data.actions + ); + + test('orders the actions correctly', () => { + const correctActionsHashes = [ + '23562173419146814432140831830018386191372262558717813981702672868292521523493', + '17091049856171838105194364005412166905307014398334933913160405653259432088216', + '17232885850087529233459756382038742870248640044940153006158312935267918515979', + '12636308717155378495657553296284990333618148856424346334743675423201692801125', + '17082487567758469425757467457967473265642001333824907522427890208991758759731', + '14226491442770650712364681911870921131508915865197379983185088742764625929348', + '13552033292375176242184292341671233419412691991179711376625259275814019808194', + ]; + expect(actionsList.map(({ hash }) => hash)).toEqual( + correctActionsHashes + ); + }); }); }); }); -``; diff --git a/src/lib/mina/fixtures/fetch-actions-response-with-transaction-info.ts b/src/lib/mina/fixtures/fetch-actions-response-with-transaction-info.ts new file mode 100644 index 0000000000..de1df5d8c9 --- /dev/null +++ b/src/lib/mina/fixtures/fetch-actions-response-with-transaction-info.ts @@ -0,0 +1,160 @@ +export { mockFetchActionsResponse }; + +const mockFetchActionsResponse = { + data: { + actions: [ + { + blockInfo: { + distanceFromMaxBlockHeight: 10044, + }, + actionState: { + actionStateOne: + '12636308717155378495657553296284990333618148856424346334743675423201692801125', + actionStateTwo: + '13532287841471317150184319478504392572847735859185693513218934441300016387637', + }, + actionData: [ + { + accountUpdateId: '40576', + data: [ + '7', + '5', + '9', + '4', + '7', + '6', + '15661593045172372274349484602975176160403678593916071611247487060653084577990', + '0', + '1', + ], + transactionInfo: { + sequenceNumber: 7, + zkappAccountUpdateIds: [40576, 6396, 33066], + }, + }, + { + accountUpdateId: '40577', + data: [ + '2', + '7', + '4', + '3', + '1', + '3', + '15661593045172372274349484602975176160403678593916071611247487060653084577990', + '0', + '1', + ], + transactionInfo: { + sequenceNumber: 8, + zkappAccountUpdateIds: [40577, 6396, 33066], + }, + }, + { + accountUpdateId: '40586', + data: [ + '1', + '6', + '3', + '5', + '2', + '7', + '15661593045172372274349484602975176160403678593916071611247487060653084577990', + '0', + '1', + ], + transactionInfo: { + sequenceNumber: 12, + zkappAccountUpdateIds: [40586, 6396, 33066], + }, + }, + { + accountUpdateId: '40587', + data: [ + '4', + '7', + '5', + '1', + '2', + '2', + '15661593045172372274349484602975176160403678593916071611247487060653084577990', + '0', + '1', + ], + transactionInfo: { + sequenceNumber: 13, + zkappAccountUpdateIds: [40587, 6396, 33066], + }, + }, + ], + }, + { + blockInfo: { + distanceFromMaxBlockHeight: 10043, + }, + actionState: { + actionStateOne: + '13552033292375176242184292341671233419412691991179711376625259275814019808194', + actionStateTwo: + '12636308717155378495657553296284990333618148856424346334743675423201692801125', + }, + actionData: [ + { + accountUpdateId: '40589', + data: [ + '3', + '1', + '3', + '1', + '5', + '6', + '12165186923319687572900094011821565862668411524381584956461194876028064035764', + '0', + '1', + ], + transactionInfo: { + sequenceNumber: 4, + zkappAccountUpdateIds: [40589, 40590, 40591], + }, + }, + { + accountUpdateId: '40592', + data: [ + '1', + '1', + '3', + '4', + '9', + '4', + '12956991225953906921841684099823101706501308776705520473434038962466907995970', + '0', + '1', + ], + transactionInfo: { + sequenceNumber: 5, + zkappAccountUpdateIds: [40592, 8848, 8849], + }, + }, + { + accountUpdateId: '40593', + data: [ + '9', + '7', + '1', + '1', + '8', + '6', + '12444248802460880132081449291546340439683979052479458915102943284375240795308', + '1', + '1', + ], + transactionInfo: { + sequenceNumber: 6, + zkappAccountUpdateIds: [40593, 40594, 40595], + }, + }, + ], + }, + ], + }, +}; diff --git a/src/lib/mina/fixtures/fetch-actions-response-without-transaction-info.ts b/src/lib/mina/fixtures/fetch-actions-response-without-transaction-info.ts new file mode 100644 index 0000000000..f098fddc39 --- /dev/null +++ b/src/lib/mina/fixtures/fetch-actions-response-without-transaction-info.ts @@ -0,0 +1,123 @@ +export { mockFetchActionsResponse }; + +const mockFetchActionsResponse = { + data: { + actions: [ + { + blockInfo: { + distanceFromMaxBlockHeight: 11, + }, + actionState: { + actionStateOne: + '25525130517416993227046681664758665799110129890808721833148757111140891481208', + actionStateTwo: + '25079927036070901246064867767436987657692091363973573142121686150614948079097', + }, + actionData: [ + { + accountUpdateId: '5', + data: [ + '20503089751358270987184701275168489753952341816059774976784079526478451099801', + '1', + ], + }, + { + accountUpdateId: '3', + data: [ + '20374659537065244088703638031937922870146667362923279084491778322749365537089', + '1', + ], + }, + ], + }, + { + blockInfo: { + distanceFromMaxBlockHeight: 5, + }, + actionState: { + actionStateOne: + '290963518424616502946790040851348455652296009700336010663574777600482385855', + actionStateTwo: + '25525130517416993227046681664758665799110129890808721833148757111140891481208', + }, + actionData: [ + { + accountUpdateId: '7', + data: [ + '3374074164183544078218789545772953663729921088152354292852793744356608231707', + '0', + ], + }, + ], + }, + { + blockInfo: { + distanceFromMaxBlockHeight: 3, + }, + actionState: { + actionStateOne: + '20673199655841577810393943638910551364027795297920791498278816237738641857371', + actionStateTwo: + '290963518424616502946790040851348455652296009700336010663574777600482385855', + }, + actionData: [ + { + accountUpdateId: '9', + data: [ + '12630758077588166643924428865613845067150916064939816120404808842510620524633', + '1', + ], + }, + ], + }, + { + blockInfo: { + distanceFromMaxBlockHeight: 2, + }, + actionState: { + actionStateOne: + '10964420428484427410756859799314206378989718180435238943573393516522086219419', + actionStateTwo: + '20673199655841577810393943638910551364027795297920791498278816237738641857371', + }, + actionData: [ + { + accountUpdateId: '19', + data: [ + '17137397755795687855356639427474789131368991089558570411893673365904353943290', + '1', + ], + }, + { + accountUpdateId: '17', + data: [ + '3378367318331499715304980508337843233019278703665446829424824679144818589558', + '1', + ], + }, + { + accountUpdateId: '15', + data: [ + '27263309408256888453299195755797013857604561285332380691270111409680109142128', + '1', + ], + }, + { + accountUpdateId: '13', + data: [ + '15789351988619560045401465240113496854401074115453702466673859303925517061263', + '0', + ], + }, + { + accountUpdateId: '11', + data: [ + '5643224648393140391519847064914429159616501351124129591669928700148350171602', + '0', + ], + }, + ], + }, + ], + }, +}; diff --git a/src/lib/mina/fixtures/fetch-actions-response.ts b/src/lib/mina/fixtures/fetch-actions-response.ts deleted file mode 100644 index 3e280bbff2..0000000000 --- a/src/lib/mina/fixtures/fetch-actions-response.ts +++ /dev/null @@ -1,118 +0,0 @@ -export { - mockFetchActionsResponse -} - -const mockFetchActionsResponse = -{ - "data": { - "actions": [ - { - "blockInfo": { - "distanceFromMaxBlockHeight": 11 - }, - "actionState": { - "actionStateOne": "25525130517416993227046681664758665799110129890808721833148757111140891481208", - "actionStateTwo": "25079927036070901246064867767436987657692091363973573142121686150614948079097" - }, - "actionData": [ - { - "accountUpdateId": "5", - "data": [ - "20503089751358270987184701275168489753952341816059774976784079526478451099801", - "1" - ] - }, - { - "accountUpdateId": "3", - "data": [ - "20374659537065244088703638031937922870146667362923279084491778322749365537089", - "1" - ] - } - ] - }, - { - "blockInfo": { - "distanceFromMaxBlockHeight": 5 - }, - "actionState": { - "actionStateOne": "290963518424616502946790040851348455652296009700336010663574777600482385855", - "actionStateTwo": "25525130517416993227046681664758665799110129890808721833148757111140891481208" - }, - "actionData": [ - { - "accountUpdateId": "7", - "data": [ - "3374074164183544078218789545772953663729921088152354292852793744356608231707", - "0" - ] - } - ] - }, - { - "blockInfo": { - "distanceFromMaxBlockHeight": 3 - }, - "actionState": { - "actionStateOne": "20673199655841577810393943638910551364027795297920791498278816237738641857371", - "actionStateTwo": "290963518424616502946790040851348455652296009700336010663574777600482385855" - }, - "actionData": [ - { - "accountUpdateId": "9", - "data": [ - "12630758077588166643924428865613845067150916064939816120404808842510620524633", - "1" - ] - } - ] - }, - { - "blockInfo": { - "distanceFromMaxBlockHeight": 2 - }, - "actionState": { - "actionStateOne": "10964420428484427410756859799314206378989718180435238943573393516522086219419", - "actionStateTwo": "20673199655841577810393943638910551364027795297920791498278816237738641857371" - }, - "actionData": [ - { - "accountUpdateId": "19", - "data": [ - "17137397755795687855356639427474789131368991089558570411893673365904353943290", - "1" - ] - }, - { - "accountUpdateId": "17", - "data": [ - "3378367318331499715304980508337843233019278703665446829424824679144818589558", - "1" - ] - }, - { - "accountUpdateId": "15", - "data": [ - "27263309408256888453299195755797013857604561285332380691270111409680109142128", - "1" - ] - }, - { - "accountUpdateId": "13", - "data": [ - "15789351988619560045401465240113496854401074115453702466673859303925517061263", - "0" - ] - }, - { - "accountUpdateId": "11", - "data": [ - "5643224648393140391519847064914429159616501351124129591669928700148350171602", - "0" - ] - } - ] - } - ] - } -} \ No newline at end of file diff --git a/src/lib/mina/graphql.ts b/src/lib/mina/graphql.ts index 04e0340a61..0dfb34e637 100644 --- a/src/lib/mina/graphql.ts +++ b/src/lib/mina/graphql.ts @@ -262,6 +262,10 @@ type FetchedAction = { actionData: { accountUpdateId: string; data: string[]; + transactionInfo?: { + sequenceNumber: number; + zkappAccountUpdateIds: number[]; + }; }[]; }; @@ -317,7 +321,11 @@ const getActionsQuery = ( publicKey: string, actionStates: ActionStatesStringified, tokenId: string, - _filterOptions?: EventActionFilterOptions + _filterOptions?: EventActionFilterOptions, + // As of 2025-01-07 minascan is running a version of the node API which supports `sequenceNumber` and `zkappAccountUpdateIds` fields + // In case a user tries to access an older API version, we support making the query without these fields, but can't guarantee action ordering + // Transaction sequence info is required to be 100% certain of action order + _excludeTransactionInfo: boolean = false ) => { const { fromActionState, endActionState } = actionStates ?? {}; let input = `address: "${publicKey}", tokenId: "${tokenId}"`; @@ -339,6 +347,11 @@ const getActionsQuery = ( actionData { accountUpdateId data + ${ + _excludeTransactionInfo + ? '' + : 'transactionInfo { sequenceNumber zkappAccountUpdateIds }' + } } } }`;