Skip to content
This repository has been archived by the owner on Aug 30, 2024. It is now read-only.

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeychr committed Dec 15, 2023
1 parent 3a7a779 commit eac3070
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 50 deletions.
2 changes: 1 addition & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import "@nomicfoundation/hardhat-ethers"

const config: HardhatUserConfig = {
solidity: "0.8.19",
// defaultNetwork: 'localhost'
defaultNetwork: 'localhost'
};
export default config;
237 changes: 211 additions & 26 deletions src/chain-evm/broadcaster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,41 @@ import hre from 'hardhat';
import "@nomiclabs/hardhat-web3";
import * as hh from "@nomicfoundation/hardhat-network-helpers";
import { TransactionSender } from './sender';
import { DefaultEvmFeeManager } from './fees/manager';
import { DefaultEvmFeeManager, GasCategory } from './fees/manager';
import Web3 from 'web3';
import "@nomicfoundation/hardhat-ethers"
import { expect } from 'chai';
import "@nomicfoundation/hardhat-chai-matchers"
import { createTestFrameworkLogger, getSigners } from '../../tests/util';
import { Account, createTestFrameworkLogger, getSigners } from '../../tests/util';
import { Logger } from 'pino';
import { TransactionResponse } from 'ethers';


declare module "mocha" {
interface Context {
signer: Account;
logger: Logger
}
}

describe(`Broadcaster`, function () {
beforeEach(async () => {
await hh.reset();

// setup web3 wallet
const [signer] = await getSigners()
this.ctx.signer = signer;
hre.web3.eth.accounts.wallet.add(signer);
hre.web3.eth.defaultAccount = signer.address;

// logger
this.ctx.logger = createTestFrameworkLogger()
})

it('Smoke test', async () => {
// await hh.reset();
const [signer, recipient] = await getSigners()
const amount = 1n * 10n ** 18n
xit('Must accept transaction immediately', async () => {
const [signer, recipient] = await getSigners();

const amount = BigInt(await hre.web3.eth.getBalance(signer.address)) - 10n ** 18n
const sender = new TransactionSender(
{
from: signer.address,
Expand All @@ -29,26 +47,21 @@ describe(`Broadcaster`, function () {
hre.web3,
new DefaultEvmFeeManager(hre.network.config.chainId || 1, hre.web3, {}),
async (tx) => (await signer.signTransaction(tx)).rawTransaction || "0x",
createTestFrameworkLogger(),
this.ctx.logger,
);

// const tx = await sender.send();
await expect(sender.send())
.not.rejected
// await expect(async () => hre.ethers.provider.getTransaction(await sender.send()))
// .to.changeEtherBalances(
// [signer.address, recipient.address],
// [-amount, amount],
// )
await expect(async () => hre.ethers.provider.getTransaction(await sender.send()))
.to.changeEtherBalances(
[signer.address, recipient.address],
[-amount, amount],
)
});

it('Smoke test 2', async () => {
// await hh.reset();
const logger = createTestFrameworkLogger();
xit('Must broadcast transaction w/increased nonce when nonce is too low', async () => {
const {logger} = this.ctx
const [signer, recipient] = await getSigners()
hre.web3.eth.accounts.wallet.add(signer);
hre.web3.eth.defaultAccount = signer.address;
const amount = 1n * 10n ** 18n

const amount = BigInt(await hre.web3.eth.getBalance(signer.address)) - 10n ** 18n
let nonceInjected = false;
const sender = new TransactionSender(
{
Expand Down Expand Up @@ -76,10 +89,182 @@ describe(`Broadcaster`, function () {
logger,
);

await expect(async () => hre.ethers.provider.getTransaction(await sender.send()))
.to.changeEtherBalances(
[signer.address, recipient.address],
[-amount, amount],
)
const txHashPromise = sender.send();
await expect(txHashPromise)
.to.not.rejected;

const txHash = await txHashPromise;
const tx = await hre.web3.eth.getTransaction(txHash);
expect(tx.nonce)
.to.eq(1)

const receipt = await hre.web3.eth.getTransactionReceipt(txHash);
expect(receipt.status)
.to.eq(true);
});

xit('Must replace transaction when it is underpriced', async () => {
const {logger} = this.ctx
const [signer, recipient] = await getSigners()

const amount = BigInt(await hre.web3.eth.getBalance(signer.address)) - 10n ** 18n
let nonceInjected = false;
let lastRecordedGasPrice: BigInt;
const sender = new TransactionSender(
{
from: signer.address,
to: recipient.address,
data: "0x",
value: amount.toString()
},
hre.web3,
new DefaultEvmFeeManager(hre.network.config.chainId || 1, hre.web3, {}, {
legacyFeeFetcher: async (gasCategory, connection) => {
const gasPrice = BigInt(await connection.eth.getGasPrice())
if (!nonceInjected) {
const reducedGasPrice = gasPrice / 10n;
nonceInjected = true;
return reducedGasPrice;
}
lastRecordedGasPrice = gasPrice;
return gasPrice
},
}),
async (tx) => {
return (await signer.signTransaction(tx)).rawTransaction || "0x"
},
logger,
);

const txHashPromise = sender.send();
await expect(txHashPromise)
.to.not.rejected;

const txHash = await txHashPromise;
const tx = await hre.web3.eth.getTransaction(txHash);
expect(tx.nonce)
.to.eq(0)

expect(tx.gasPrice)
.to.eq(lastRecordedGasPrice!)

const receipt = await hre.web3.eth.getTransactionReceipt(txHash);
expect(receipt.status)
.to.eq(true);
})

xit('Must replace transaction when it is underpriced (last attempt only)', async () => {
const {logger} = this.ctx
const [signer, recipient] = await getSigners()

// we must ensure that the blockchain accepted our final txn (with normal gas)
let lastRecordedGasPrice: BigInt;

// we must ensure that the Broadcaster succeded at the last attempt (not earlier or later)
let attempts = 0;
const sendMaxAttempts = 3;


const amount = BigInt(await hre.web3.eth.getBalance(signer.address)) - 10n ** 18n
const sender = new TransactionSender(
{
from: signer.address,
to: recipient.address,
data: "0x",
value: amount.toString()
},
hre.web3,
new DefaultEvmFeeManager(hre.network.config.chainId || 1, hre.web3, {}, {
legacyFeeFetcher: async (gasCategory, connection) => {
attempts++;
const gasPrice = BigInt(await connection.eth.getGasPrice());
if (gasCategory !== GasCategory.HIGH) {
const reducedGasPrice = gasPrice / 10n;
return reducedGasPrice;
}
lastRecordedGasPrice = gasPrice;
return gasPrice
},
}),
async (tx) => {
return (await signer.signTransaction(tx)).rawTransaction || "0x"
},
logger,
{
sendMaxAttempts
}
);

const txHashPromise = sender.send();
await expect(txHashPromise)
.to.not.rejected;

const txHash = await txHashPromise;
const tx = await hre.web3.eth.getTransaction(txHash);
expect(tx.nonce)
.to.eq(0)

expect(tx.gasPrice)
.to.eq(lastRecordedGasPrice!)

expect(attempts)
.to.eq(sendMaxAttempts)

const receipt = await hre.web3.eth.getTransactionReceipt(txHash);
expect(receipt.status)
.to.eq(true);
})

it('Must replace transaction when it is stuck', async () => {
const {logger} = this.ctx
const [signer, recipient] = await getSigners()

hre.ethers.provider.send('evm_setAutomine', [false])

const amount = BigInt(await hre.web3.eth.getBalance(signer.address)) - 10n ** 18n
let firstAttemptPassed = false;
let lastRecordedGasPrice: BigInt;
const sender = new TransactionSender(
{
from: signer.address,
to: recipient.address,
data: "0x",
value: amount.toString()
},
hre.web3,
new DefaultEvmFeeManager(hre.network.config.chainId || 1, hre.web3, {}, {}),
async (tx) => {
if (!firstAttemptPassed) {
firstAttemptPassed = true;
}
else {
logger.debug("enabling automine")
setTimeout(() => hre.ethers.provider.send('evm_setAutomine', [true]), 400)
}
return (await signer.signTransaction(tx)).rawTransaction || "0x"
},
logger,
{
pollingIntervalMs: 100
}
);

const txHashPromise = sender.send();
await expect(txHashPromise)
.to.not.rejected;

const txHash = await txHashPromise;
const tx = await hre.web3.eth.getTransaction(txHash);
expect(tx.nonce)
.to.eq(0)

expect(tx.gasPrice)
.to.eq(lastRecordedGasPrice!)

const receipt = await hre.web3.eth.getTransactionReceipt(txHash);
expect(receipt.status)
.to.eq(true);
})

it('Must replace transaction when it is stuck (last attempt only)', async () => {})
});
71 changes: 49 additions & 22 deletions src/chain-evm/sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
LegacyGasExtension,
TransactionTemplate,
} from './fees/manager';
import { AbstractProvider } from 'web3-core';

const EVM_BROADCAST_IMMEDIATE_POLL = getBoolean('EVM_BROADCAST_IMMEDIATE_POLL', getBoolean('DLN_TAKER_LOCAL_MODE', false) === true);

Expand All @@ -28,8 +29,8 @@ enum EvmRpcError {

function stringToError(error: any): EvmRpcError {
const stringifiedError = `${error}`;
if (stringifiedError.match(/too low/i)) return EvmRpcError.NonceTooLow;
if (stringifiedError.match(/underpriced/i)) return EvmRpcError.TransactionUnderpriced;
if (stringifiedError.match(/too low/i) && stringifiedError.match(/nonce/i)) return EvmRpcError.NonceTooLow;
if (stringifiedError.match(/underpriced/i) || stringifiedError.match(/too low/i)) return EvmRpcError.TransactionUnderpriced;
if (stringifiedError.match(/not mined/i)) return EvmRpcError.Stuck;
// else if (stringifiedError.match(/execution reverted/i)) return Error.EstimationReverted;
// else if (stringifiedError.match(/Transaction has been reverted by the EVM/i)) return Error.Reverted;
Expand Down Expand Up @@ -88,15 +89,15 @@ export class TransactionSender {
manager: FeeManagerV2,
signer: Signer,
logger: Logger,
opts?: TransactionSenderOpts,
opts?: Partial<TransactionSenderOpts>,
) {
this.#inputTx = tx;
this.#connection = connection;
this.#feeManager = manager;
this.#signer = signer;
this.#id = new Date().getTime();
this.#logger = logger.child({ [TransactionSender.name]: this.#id });
this.#opts = { ...(opts || {}), ...defaultOpts };
this.#opts = Object.assign({}, defaultOpts, opts || {});
}

async send(): Promise<string> {
Expand Down Expand Up @@ -173,25 +174,51 @@ export class TransactionSender {

// use setTimeout rather than setInterval is because we want to perform the first check immediately
const poller = () => {
this.#connection.eth.getTransactionReceipt(hash).then(
(transactionReceiptResult) => {
if (transactionReceiptResult.status === true) {
resolve(transactionReceiptResult);
} else if (transactionReceiptResult?.status === false) {
reject(new Error(`tx ${hash} reverted`));
}
else if (pollingAttemptsLeft-- < 0) {
this.#logger.debug(`polling... attempt ${pollingAttemptsLeft}`);

const rerun = () => {
if (pollingAttemptsLeft-- < 0) {
this.#logger.debug(`poller reached max attempts, trying to replace txn`);
resolve(this.pushReplacement(broadcast));
}
else {
setTimeout(poller, this.#opts.pollingIntervalMs)
}
},
(err) => {
this.#logger.debug(`unable to get txn receipt: ${err}, still working...`);
},
);
}
else setTimeout(poller, this.#opts.pollingIntervalMs)
}

const provider = <AbstractProvider>this.#connection.eth.currentProvider;
assert(typeof provider === 'object', '_ob');
assert(typeof provider.send === 'function', '_fn');
provider.send!({
method: "eth_getTransactionReceipt",
params: [hash],
jsonrpc: "2.0",
id: new Date().getTime()
}, (err, resp) => {

if (err || resp?.error) {
this.#logger.debug(`an error occured while trying to get txn receipt: ${err || resp?.error}, still working...`);
this.#logger.error(err || resp?.error)
rerun()
}
else if (!resp?.result) {
this.#logger.debug(`receipt not available: ${resp?.result}, still working...`);
rerun()
}
else {
const rcp = (<Web3TransactionReceipt>resp.result);
if (rcp.status === true) {
resolve(rcp)
return
}
else if (rcp.status === false) {
reject(new Error(`tx ${hash} reverted`));
return
}
else {
this.#logger.debug(`result is not boolean, but ${typeof rcp.status}, retrying`)
rerun();
}
}
});
}

// call the check right away!
Expand Down Expand Up @@ -259,7 +286,7 @@ export class TransactionSender {
}
};

this.#logger.debug(`broadcasting (attempt#${broadcast.attempt}): ${signedTransactionData}`);
this.#logger.debug(`broadcast${broadcast.attempt} signed transaction data ${signedTransactionData}`);

// kinda weird code below: THREE checks
try {
Expand Down
Loading

0 comments on commit eac3070

Please sign in to comment.