Skip to content
This repository has been archived by the owner on Nov 5, 2023. It is now read-only.

Commit

Permalink
Add getOperationResults to clients to make it easier to decode operat…
Browse files Browse the repository at this point in the history
…ion errors.

Change existing contract test case to use getOperationResults.
Add typemoq to allow typed mocking.
Bump clients patch version.
  • Loading branch information
jacque006 committed Oct 27, 2022
1 parent 3cfabaa commit f7d4d1a
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 10 deletions.
15 changes: 15 additions & 0 deletions contracts/clients/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ const verificationGateway = VerificationGateway__factory.connect(
await verificationGateway.processBundle(bundle);
```

You can get the results of the operations in a bundle using `getOperationResults`.

```ts
import { getOperationResults } from 'bls-wallet-clients';
...
const txn = await verificationGateway.processBundle(bundle);
const txnReceipt = txn.wait();
const opResults = getOperationResults(txnReceipt);
// Includes data from WalletOperationProcessed event,
// as well as parsed errors with action index
const { error } = opResults[0];
console.log(error?.actionIndex); // ex. 0 (as BigNumber)
console.log(error?.message); // ex. "some require failure message"
```

## Signer

Utilities for signing, aggregating and verifying transaction bundles using the
Expand Down
3 changes: 2 additions & 1 deletion contracts/clients/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bls-wallet-clients",
"version": "0.7.3",
"version": "0.7.4",
"description": "Client libraries for interacting with BLS Wallet components",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
Expand Down Expand Up @@ -33,6 +33,7 @@
"mocha": "^10.0.0",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.1",
"typemoq": "^2.1.0",
"typescript": "^4.8.2"
}
}
111 changes: 111 additions & 0 deletions contracts/clients/src/OperationResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@

import { BigNumber, ContractReceipt, utils } from "ethers";
import { ActionData } from "./signer";

type OperationResultError = {
actionIndex: BigNumber;
message: string;
};

export type OperationResult = {
walletAddress: string;
nonce: BigNumber;
actions: ActionData[];
success: Boolean;
results: string[];
error?: OperationResultError;
};

const getError = (
success: boolean,
results: string[],
): OperationResultError | undefined => {
if (success) {
return undefined;
}

// Single event "WalletOperationProcessed(address indexed wallet, uint256 nonce, bool success, bytes[] results)"
// Get the first (only) result from "results" argument.
const [errorResult] = results;
// remove methodId (4bytes after 0x)
const errorArgBytesString = `0x${errorResult.substring(10)}`;
const errorString = utils.defaultAbiCoder.decode(
["string"],
errorArgBytesString,
)[0]; // decoded bytes is a string of the action index that errored.

const splitErrorString = errorString.split(" - ");
if (splitErrorString.length !== 2) {
throw new Error("unexpected error message format");
}

return {
actionIndex: BigNumber.from(splitErrorString[0]),
message: splitErrorString[1],
};
};

/**
* Gets the results of operations (and actions) run through VerificationGateway.processBundle.
* Decodes unsuccessful operations into an error message and the index of the action that failed.
*
* @param transactionReceipt Transaction receipt from a VerificationGateway.processBundle transaction
* @returns An array of decoded operation results
*/
export const getOperationResults = (
transactionReceipt: ContractReceipt,
): OperationResult[] => {
if (!transactionReceipt.events || !transactionReceipt.events.length) {
throw new Error(
`no events found in transaction ${transactionReceipt.transactionHash}`,
);
}

const walletOpProcessedEvents = transactionReceipt.events.filter(
(e) => e.event === "WalletOperationProcessed",
);
if (!walletOpProcessedEvents.length) {
throw new Error(
`no WalletOperationProcessed events found in transaction ${transactionReceipt.transactionHash}`,
);
}

return walletOpProcessedEvents.reduce<OperationResult[]>(
(opResults, { args }) => {
if (!args) {
throw new Error("WalletOperationProcessed event missing args");
}
const { wallet, nonce, actions: rawActions, success, results } = args;

const actions = rawActions.map(
({
ethValue,
contractAddress,
encodedFunction,
}: {
ethValue: BigNumber;
contractAddress: string;
encodedFunction: string;
}) => ({
ethValue,
contractAddress,
encodedFunction,
}),
);
const error = getError(success, results);

return [
...opResults,
{
walletAddress: wallet,
nonce,
actions,
success,
results,
error,
},
];
},
[],
);
};
4 changes: 4 additions & 0 deletions contracts/clients/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
validateMultiConfig,
} from "./MultiNetworkConfig";

import { OperationResult, getOperationResults } from "./OperationResults";

export * from "./signer";

export {
Expand All @@ -35,6 +37,8 @@ export {
MultiNetworkConfig,
getMultiConfig,
validateMultiConfig,
OperationResult,
getOperationResults,
// eslint-disable-next-line camelcase
VerificationGateway__factory,
VerificationGateway,
Expand Down
165 changes: 165 additions & 0 deletions contracts/clients/test/OpertionResults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { expect } from "chai";
import { BigNumber, ContractReceipt, Event, utils } from "ethers";
import * as TypeMoq from "typemoq";
import { getOperationResults } from "../src";

class MockResult extends Array<unknown> {
[key: string]: unknown;

constructor(obj: Record<string, unknown>) {
super();
for (const k in obj) {
this[k] = obj[k];
}
}
}

const getErrorResult = (actionIndex: number, message: string): string => {
const fullErrorMessage = `${actionIndex} - ${message}`;
const encodedErrorMessage = utils.defaultAbiCoder.encode(
["string"],
[fullErrorMessage],
);
// Add empty methodId (4 bytes, 8 chars), remove leading 0
return `0x${"0".repeat(8)}${encodedErrorMessage.substring(2)}`;
};

describe("OperationResults", () => {
describe("getOperationResults", () => {
it("fails if no events are in transaction", () => {
const hash = "0xabc123";
const txnReceiptMock = TypeMoq.Mock.ofType<ContractReceipt>();
txnReceiptMock.setup((r) => r.transactionHash).returns(() => hash);
txnReceiptMock.setup((r) => r.events).returns(() => undefined);

expect(() => getOperationResults(txnReceiptMock.object)).to.throw(
`no events found in transaction ${hash}`,
);
});

it("fails if events are empty in transaction", () => {
const hash = "0xdef456";
const txnReceiptMock = TypeMoq.Mock.ofType<ContractReceipt>();
txnReceiptMock.setup((r) => r.transactionHash).returns(() => hash);
txnReceiptMock.setup((r) => r.events).returns(() => []);

expect(() => getOperationResults(txnReceiptMock.object)).to.throw(
`no events found in transaction ${hash}`,
);
});

it("fails if no WalletOperationProcessed events are in transaction", () => {
const eventMock = TypeMoq.Mock.ofType<Event>();
eventMock.setup((e) => e.event).returns(() => "Other");

const hash = "0x123456";
const txnReceiptMock = TypeMoq.Mock.ofType<ContractReceipt>();
txnReceiptMock.setup((r) => r.transactionHash).returns(() => hash);
txnReceiptMock.setup((r) => r.events).returns(() => [eventMock.object]);

expect(() => getOperationResults(txnReceiptMock.object)).to.throw(
`no WalletOperationProcessed events found in transaction ${hash}`,
);
});

it("fails when WalletOperationProcessed event is missing args", () => {
const eventMock = TypeMoq.Mock.ofType<Event>();
eventMock.setup((e) => e.event).returns(() => "WalletOperationProcessed");
eventMock.setup((e) => e.args).returns(() => undefined);

const txnReceiptMock = TypeMoq.Mock.ofType<ContractReceipt>();
txnReceiptMock.setup((r) => r.events).returns(() => [eventMock.object]);

expect(() => getOperationResults(txnReceiptMock.object)).to.throw(
"WalletOperationProcessed event missing args",
);
});

it("decodes WalletOperationProcessed events", () => {
const otherEventMock = TypeMoq.Mock.ofType<Event>();
otherEventMock.setup((e) => e.event).returns(() => "Other");

const errorActionIndex = 0;
const errorMessage = "halt and catch fire";
const errorResult = getErrorResult(errorActionIndex, errorMessage);
const failedEventArgs = new MockResult({
wallet: "0x01",
nonce: BigNumber.from(0),
actions: [
{
ethValue: BigNumber.from(0),
contractAddress: "0xaabbcc",
encodedFunction: "0xddeeff",
},
],
success: false,
results: [errorResult],
});
const failedOperationEventMock = TypeMoq.Mock.ofType<Event>();
failedOperationEventMock
.setup((e) => e.event)
.returns(() => "WalletOperationProcessed");
failedOperationEventMock
.setup((e) => e.args)
.returns(() => failedEventArgs);

const successfulEventArgs = new MockResult({
wallet: "0x02",
nonce: BigNumber.from(1),
actions: [
{
ethValue: BigNumber.from(0),
contractAddress: "0xabcabc",
encodedFunction: "0xdefdef",
},
{
ethValue: BigNumber.from(42),
contractAddress: "0x123123",
encodedFunction: "0x456456",
},
],
success: true,
results: ["0x2A", "0x539"],
});
const successfulOperationEventMock = TypeMoq.Mock.ofType<Event>();
successfulOperationEventMock
.setup((e) => e.event)
.returns(() => "WalletOperationProcessed");
successfulOperationEventMock
.setup((e) => e.args)
.returns(() => successfulEventArgs);

const txnReceiptMock = TypeMoq.Mock.ofType<ContractReceipt>();
txnReceiptMock
.setup((r) => r.events)
.returns(() => [
otherEventMock.object,
failedOperationEventMock.object,
successfulOperationEventMock.object,
]);

const opResults = getOperationResults(txnReceiptMock.object);
expect(opResults).to.have.lengthOf(2);

const [r1, r2] = opResults;
expect(r1.walletAddress).to.eql(failedEventArgs.wallet);
expect(r1.nonce.toNumber()).to.eql(
BigNumber.from(failedEventArgs.nonce).toNumber(),
);
expect(r1.actions).to.deep.equal(failedEventArgs.actions);
expect(r1.success).to.eql(failedEventArgs.success);
expect(r1.results).to.deep.equal(failedEventArgs.results);
expect(r1.error?.actionIndex.toNumber()).to.eql(errorActionIndex);
expect(r1.error?.message).to.eql(errorMessage);

expect(r2.walletAddress).to.eql(successfulEventArgs.wallet);
expect(r2.nonce.toNumber()).to.eql(
BigNumber.from(successfulEventArgs.nonce).toNumber(),
);
expect(r2.actions).to.deep.equal(successfulEventArgs.actions);
expect(r2.success).to.eql(successfulEventArgs.success);
expect(r2.results).to.deep.equal(successfulEventArgs.results);
expect(r2.error).to.be.undefined;
});
});
});
24 changes: 24 additions & 0 deletions contracts/clients/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,11 @@ chokidar@3.5.3:
optionalDependencies:
fsevents "~2.3.2"

circular-json@^0.3.1:
version "0.3.3"
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==

cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
Expand Down Expand Up @@ -1265,6 +1270,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"

lodash@^4.17.4:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

log-symbols@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
Expand Down Expand Up @@ -1403,6 +1413,11 @@ picomatch@^2.0.4, picomatch@^2.2.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==

postinstall-build@^5.0.1:
version "5.0.3"
resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7"
integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==

randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
Expand Down Expand Up @@ -1518,6 +1533,15 @@ type-detect@^4.0.0, type-detect@^4.0.5:
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==

typemoq@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8"
integrity sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==
dependencies:
circular-json "^0.3.1"
lodash "^4.17.4"
postinstall-build "^5.0.1"

typescript@^4.8.2:
version "4.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"
Expand Down
Loading

0 comments on commit f7d4d1a

Please sign in to comment.