@ethereum-waffle/provider
Version:
A mock provider for your blockchain testing needs.
138 lines • 5.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CallHistory = void 0;
const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");
/**
* CallHistory gathers a log of queries and transactions
* sent to a blockchain provider.
* It is used by the `calledOnContract` matcher.
*/
class CallHistory {
constructor() {
this.recordedCalls = [];
}
clear() {
this.recordedCalls = [];
}
getCalls() {
return this.recordedCalls;
}
record(provider) {
// Required for the Proxy object.
// eslint-disable-next-line @typescript-eslint/no-this-alias
const callHistory = this;
/**
* One needs to register any `ganache:*` event handler
* after a `connect` event is emitted.
* After that we can consider the provider to be initialized.
* Otherwise some internal object might not have been created yet,
* and there is a silently ignored error deep in ganache / ethereum VM.
*/
provider.on('connect', () => {
/**
* A single step over a single opcode inside the EVM.
* We use it to intercept `CALL` and `STATICCALL` opcodes,
* and track a history of internal calls between smart contracts.
*/
provider.on('ganache:vm:tx:step', (args) => {
if (['CALL', 'STATICCALL'].includes(args.data.opcode.name)) {
try {
callHistory.recordedCalls.push(toRecordedCall(decodeCallData(args.data)));
}
catch (err) {
console.log(err);
}
}
});
});
/**
* We override the ganache provider with a proxy,
* that hooks into a `provider.request()` method.
*
* All other methods and properties are left intact.
*/
return new Proxy(provider, {
get(target, prop, receiver) {
const original = target[prop];
if (typeof original !== 'function') {
// Some non-method property - returned as-is.
return original;
}
// Return a function override.
return function (...args) {
var _a, _b, _c, _d, _e;
// Get a function result from the original provider.
const originalResult = original.apply(target, args);
// Every method other than `provider.request()` left intact.
if (prop !== 'request')
return originalResult;
const method = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.method;
/**
* A method can be:
* - `eth_call` - a query to the node,
* - `eth_sendRawTransaction` - a raw transaction,
* - `eth_sendTransaction` - a transaction,
* - `eth_estimateGas` - gas estimation, typically precedes `eth_sendRawTransaction`.
*/
if (method === 'eth_call' || method === 'eth_sendTransaction') { // Record a query or a transaction.
callHistory.recordedCalls.push(toRecordedCall((_c = (_b = args[0]) === null || _b === void 0 ? void 0 : _b.params) === null || _c === void 0 ? void 0 : _c[0]));
}
else if (method === 'eth_sendRawTransaction') { // Record a raw transaction.
const parsedTx = (0, utils_1.parseTransaction)((_e = (_d = args[0]) === null || _d === void 0 ? void 0 : _d.params) === null || _e === void 0 ? void 0 : _e[0]);
callHistory.recordedCalls.push(toRecordedCall(parsedTx));
}
return originalResult;
};
}
});
}
}
exports.CallHistory = CallHistory;
function toRecordedCall(message) {
return {
address: message.to ? decodeAddress(message.to) : undefined,
data: message.data ? ethers_1.utils.hexlify(message.data) : '0x'
};
}
/**
* Decodes the arguments of CALLs and STATICCALLs taken from a traced step in EVM execution.
* Source of the arguments: ethervm.io
*/
function decodeCallData(callData) {
let addr, argsOffset, argsLength;
if (callData.opcode.name === 'CALL') {
[, addr, , argsOffset, argsLength] = [...callData.stack].reverse();
}
else if (callData.opcode.name === 'STATICCALL') {
[, addr, argsOffset, argsLength] = [...callData.stack].reverse();
}
else {
throw new Error(`Unsupported call type for decoding call data: ${callData.opcode.name}`);
}
const decodedCallData = callData.memory
.slice(decodeNumber(argsOffset), decodeNumber(argsOffset) + decodeNumber(argsLength));
return {
to: addr,
data: decodedCallData
};
}
/**
* Decodes a number taken from EVM execution step
* into a JS number.
*/
function decodeNumber(data) {
const newData = Buffer.concat([data, Buffer.alloc(32, 0)]);
return newData.readUInt32LE();
}
/**
* Decodes a address taken from EVM execution step
* into a checksumAddress.
*/
function decodeAddress(data) {
if (data.length < 20) {
data = Buffer.concat([Buffer.alloc(20 - data.length, 0), data]);
}
return ethers_1.utils.getAddress(ethers_1.utils.hexlify(data));
}
//# sourceMappingURL=CallHistory.js.map