This repository has been archived by the owner on Nov 5, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add getOperationResults to clients to make it easier to decode operat…
…ion errors. Change existing contract test case to use getOperationResults. Add typemoq to allow typed mocking. Bump clients patch version.
- Loading branch information
Showing
7 changed files
with
328 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
]; | ||
}, | ||
[], | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.