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

Fix fetchActions, part 2 #854

Merged
merged 7 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 17 additions & 38 deletions src/examples/zkapps/voting/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,12 @@ export async function testSet(
);
console.log('checking that the tx is valid using default verification key');

let m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15));
verificationKeySet.Local.addAccount(m.publicKey, m.balance.toString());

await assertValidTx(
true,
() => {
let m = Member.from(
PrivateKey.random().toPublicKey(),

UInt64.from(15)
);
verificationKeySet.Local.addAccount(m.publicKey, m.balance.toString());

verificationKeySet.voting.voterRegistration(m);
},
verificationKeySet.feePayer
Expand All @@ -109,16 +105,12 @@ export async function testSet(
verificationKeySet.feePayer
);

m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15));
verificationKeySet.Local.addAccount(m.publicKey, m.balance.toString());

await assertValidTx(
false,
() => {
let m = Member.from(
PrivateKey.random().toPublicKey(),

UInt64.from(15)
);
verificationKeySet.Local.addAccount(m.publicKey, m.balance.toString());

verificationKeySet.voting.voterRegistration(m);
},
verificationKeySet.feePayer,
Expand Down Expand Up @@ -159,16 +151,12 @@ export async function testSet(
);
console.log('checking that the tx is valid using default permissions');

let m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15));
permissionedSet.Local.addAccount(m.publicKey, m.balance.toString());

await assertValidTx(
true,
() => {
let m = Member.from(
PrivateKey.random().toPublicKey(),

UInt64.from(15)
);
permissionedSet.Local.addAccount(m.publicKey, m.balance.toString());

permissionedSet.voting.voterRegistration(m);
},
permissionedSet.feePayer
Expand All @@ -192,16 +180,12 @@ export async function testSet(

console.log('trying to invoke method with invalid permissions...');

m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15));
permissionedSet.Local.addAccount(m.publicKey, m.balance.toString());

await assertValidTx(
false,
() => {
let m = Member.from(
PrivateKey.random().toPublicKey(),

UInt64.from(15)
);
permissionedSet.Local.addAccount(m.publicKey, m.balance.toString());

permissionedSet.voting.voterRegistration(m);
},
permissionedSet.feePayer,
Expand Down Expand Up @@ -240,24 +224,19 @@ export async function testSet(

console.log('trying to invoke invalid contract method...');

let m = Member.from(PrivateKey.random().toPublicKey(), UInt64.from(15));
invalidSet.Local.addAccount(m.publicKey, m.balance.toString());

try {
let tx = await Mina.transaction(invalidSet.feePayer.toPublicKey(), () => {
let m = Member.from(
PrivateKey.random().toPublicKey(),

UInt64.from(15)
);
invalidSet.Local.addAccount(m.publicKey, m.balance.toString());

invalidSet.voting.voterRegistration(m);
});

await tx.prove();
await tx.sign([invalidSet.feePayer]).send();
} catch (err: any) {
if (!err.toString().includes('precondition_unsatisfied')) {
if (!err.toString().includes('fromActionState not found')) {
throw Error(
`Transaction should have failed but went through! Error: ${err}`
`Transaction should have failed, but failed with an unexpected error! ${err}`
);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export {
TransactionStatus,
addCachedAccount,
setGraphqlEndpoint,
setArchiveGraphqlEndpoint,
sendZkapp,
} from './lib/fetch.js';
export * as Encryption from './lib/encryption.js';
Expand Down
131 changes: 52 additions & 79 deletions src/lib/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'isomorphic-fetch';
import { Field, Ledger } from '../snarky.js';
import { Field } from '../snarky.js';
import { UInt32, UInt64 } from './int.js';
import { Actions, TokenId } from './account_update.js';
import { PublicKey } from './signature.js';
Expand Down Expand Up @@ -311,14 +311,12 @@ function addCachedAccountInternal(account: Account, graphqlEndpoint: string) {
};
}

function addCachedActionsInternal(
accountInfo: { publicKey: PublicKey; tokenId: Field },
function addCachedActions(
{ publicKey, tokenId }: { publicKey: string; tokenId: string },
actions: { hash: string; actions: string[][] }[],
graphqlEndpoint: string
) {
actionsCache[
accountCacheKey(accountInfo.publicKey, accountInfo.tokenId, graphqlEndpoint)
] = {
actionsCache[`${publicKey};${tokenId};${graphqlEndpoint}`] = {
actions,
graphqlEndpoint,
timestamp: Date.now(),
Expand Down Expand Up @@ -751,13 +749,13 @@ async function fetchActions(
throw new Error(
'fetchActions: Specified GraphQL endpoint is undefined. Please specify a valid endpoint.'
);
const { publicKey, actionStates, tokenId } = accountInfo;
const {
publicKey,
actionStates,
tokenId = TokenId.toBase58(TokenId.default),
} = accountInfo;
let [response, error] = await makeGraphqlRequest(
getActionsQuery(
publicKey,
actionStates,
tokenId ?? TokenId.toBase58(TokenId.default)
),
getActionsQuery(publicKey, actionStates, tokenId),
graphqlEndpoint
);
if (error) throw Error(error.statusText);
Expand Down Expand Up @@ -789,96 +787,71 @@ async function fetchActions(
break;
}
}

const processActionData = (
currentActionList: string[][],
latestActionsHash: Field
) => {
const actionsHash = Actions.hash(
currentActionList.map((e) => e.map((f) => Field(f)))
);
return Actions.updateSequenceState(latestActionsHash, actionsHash);
};

// Archive Node API returns actions in the latest order, so we reverse the array to get the actions in chronological order.
fetchedActions.reverse();
let actionsList: { actions: string[][]; hash: string }[] = [];

fetchedActions.forEach((fetchedAction) => {
let { actionData } = fetchedAction;
let latestActionsHash = Field(fetchedAction.actionState.actionStateTwo);
let actionState = Field(fetchedAction.actionState.actionStateOne);
// correct for archive node sending one block too many
if (
fetchedActions.length !== 0 &&
fetchedActions[0].actionState.actionStateOne ===
actionStates.fromActionState
) {
fetchedActions = fetchedActions.slice(1);
}

fetchedActions.forEach((actionBlock) => {
let { actionData } = actionBlock;
let latestActionState = Field(actionBlock.actionState.actionStateTwo);
let actionState = actionBlock.actionState.actionStateOne;

if (actionData.length === 0)
throw new Error(
throw Error(
`No action data was found for the account ${publicKey} with the latest action state ${actionState}`
);

actionData.reverse();
let { accountUpdateId: currentAccountUpdateId } = actionData[0];
let currentActionList: string[][] = [];



actionData.forEach((action, i) => {
const { accountUpdateId, data } = action;
const isLastAction = i === actionData.length - 1;
const isSameAccountUpdate = accountUpdateId === currentAccountUpdateId;

if (isSameAccountUpdate && !isLastAction) {
currentActionList.push(data);
return;
} else if (isSameAccountUpdate && isLastAction) {
currentActionList.push(data);
} else if (!isSameAccountUpdate && isLastAction) {
latestActionsHash = processActionData(
currentActionList,
latestActionsHash
);
actionsList.push({
actions: currentActionList,
hash: Ledger.fieldToBase58(Field(latestActionsHash)),
});

currentActionList = [data];
}

latestActionsHash = processActionData(
currentActionList,
latestActionsHash
);
actionsList.push({
actions: currentActionList,
hash: Ledger.fieldToBase58(Field(latestActionsHash)),
});
currentAccountUpdateId = accountUpdateId;
currentActionList = [data];
// split actions by account update
let actionsByAccountUpdate: string[][][] = [];
let currentAccountUpdateId = 'none';
let currentActions: string[][];
actionData.forEach(({ accountUpdateId, data }) => {
if (accountUpdateId === currentAccountUpdateId) {
currentActions.push(data);
} else {
currentAccountUpdateId = accountUpdateId;
currentActions = [data];
actionsByAccountUpdate.push(currentActions);
}
});

const currentActionHash = Ledger.fieldToBase58(Field(latestActionsHash));
const expectedActionHash = Ledger.fieldToBase58(Field(actionState));
// re-hash actions
for (let actions of actionsByAccountUpdate) {
latestActionState = updateActionState(actions, latestActionState);
actionsList.push({ actions, hash: latestActionState.toString() });
}

const finalActionState = latestActionState.toString();
const expectedActionState = actionState;

if (currentActionHash !== expectedActionHash) {
if (finalActionState !== expectedActionState) {
throw new Error(
`Failed to derive correct actions hash for ${publicKey}.
Derived hash: ${currentActionHash}, expected hash: ${expectedActionHash}).
Derived hash: ${finalActionState}, expected hash: ${expectedActionState}).
All action hashes derived: ${JSON.stringify(actionsList, null, 2)}
Please try a different Archive Node API endpoint.
`
);
}
});
addCachedActionsInternal(
{
publicKey: PublicKey.fromBase58(publicKey),
tokenId: TokenId.fromBase58(tokenId ?? TokenId.toBase58(TokenId.default)),
},
actionsList,
graphqlEndpoint
);
addCachedActions({ publicKey, tokenId }, actionsList, graphqlEndpoint);
return actionsList;
}

function updateActionState(actions: string[][], actionState: Field) {
let actionsHash = Actions.fromJSON(actions).hash;
return Actions.updateSequenceState(actionState, actionsHash);
}

// removes the quotes on JSON keys
function removeJsonQuotes(json: string) {
let cleaned = JSON.stringify(JSON.parse(json), null, 2);
Expand Down