Skip to content

Commit

Permalink
Merge pull request #21 from jmrossy/2.1.0
Browse files Browse the repository at this point in the history
Deprecate Celo legacy transaction type (0)
  • Loading branch information
jmrossy authored May 2, 2024
2 parents 91c3aba + 838b7f2 commit e0336c1
Show file tree
Hide file tree
Showing 15 changed files with 441 additions and 395 deletions.
75 changes: 75 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Contributing

Thank you for your interest in improving the celo-ethers-wrapper.

This guide is intended to help you get started with contributing. By following these steps, you will
understand the development process and workflow.

### Cloning the repository

To start contributing to the project, fork it and clone it to your local machine using git:

```sh
$ git clone https://github.com/jmrossy/celo-ethers-wrapper.git
```

Navigate to the project's root directory:

```sh
$ cd celo-ethers-wrapper
```

### Installing Node.js

We use [Node.js](https://nodejs.org/en/) to run the project locally. You need to install the
**Node.js version** specified in [package.json > engines > node](/package.json). To do so, run:

```sh
$ nvm install <specified-version>
$ nvm use <specified-version>
```

### Installing dependencies

Once in the project's root directory, run the following command to install the project's
dependencies:

```sh
$ yarn install
```

After installing the dependencies, the project is ready to be run.

### Running the test suite

1. Create an `.env.test.local` file:

```sh
$ cp tests/.env.test.example tests/.env.test.local
```

2. Generate a brand new wallet (or use an existing development wallet of yours) and paste its
mnemonic phrase into `.env.test.local`:

```txt
MNEMONIC='<PASTE YOUR MNEMONIC PHRASE HERE>'
```

3. Ensure the wallet address has at least 1 CELO and 1 USDC on the Celo Alfajores testnet.

If you need more, you can request:
1. testnet CELO at [faucet.celo.org](https://faucet.celo.org/alfajores), and
2. testnet USDC from
[faucet.circle.com](https://faucet.circle.com/).

4. Now you're ready to run tests with:
```sh
$ yarn test
```
> **INFO** Some tests are run automatically when you open a Pull Request on GitHub.
### Open a Pull Request
✅ Now you're ready to contribute to celo-ethers-wrapper!
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
"test": "jest ./tests"
},
"engines": {
"node": ">=10"
"node": ">=18.14.2"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/node": "^20.8.2",
"dotenv": "^16.4.5",
"ethers": "^6.7.1",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
Expand Down
27 changes: 17 additions & 10 deletions src/lib/CeloProvider.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
FeeData,
JsonRpcProvider,
PerformActionRequest,
TransactionResponse,
TransactionResponseParams,
getBigInt,
resolveProperties,
toBeHex,
} from "ethers";
import { CeloTransactionRequest, parseCeloTransaction } from "./transactions";

Expand All @@ -19,10 +19,7 @@ export default class CeloProvider extends JsonRpcProvider {
// If there are no EIP-1559 properties, it might be non-EIP-1559
if (tx.maxFeePerGas == null && tx.maxPriorityFeePerGas == null) {
const feeData = await this.getFeeData();
if (
feeData.maxFeePerGas == null &&
feeData.maxPriorityFeePerGas == null
) {
if (feeData.maxFeePerGas == null && feeData.maxPriorityFeePerGas == null) {
// Network doesn't know about EIP-1559 (and hence type)
req = Object.assign({}, req, {
transaction: Object.assign({}, tx, { type: undefined }),
Expand All @@ -45,9 +42,7 @@ export default class CeloProvider extends JsonRpcProvider {
* Override to handle alternative gas currencies
* prepareRequest in https://github.com/ethers-io/ethers.js/blob/master/packages/providers/src.ts/json-rpc-provider.ts
*/
getRpcRequest(
req: PerformActionRequest
): null | { method: string; args: Array<any> } {
getRpcRequest(req: PerformActionRequest): null | { method: string; args: Array<any> } {
if (req.method === "getGasPrice") {
// @ts-expect-error
const param = req.feeCurrencyAddress
Expand All @@ -61,8 +56,6 @@ export default class CeloProvider extends JsonRpcProvider {
const extraneous_keys = [
["from", (x: string) => x],
["feeCurrency", (x: string) => x],
["gatewayFeeRecipient", (x: string) => x],
["gatewayFee", toBeHex],
] as const;

const tx = {
Expand Down Expand Up @@ -93,6 +86,20 @@ export default class CeloProvider extends JsonRpcProvider {
);
}

async getFeeData(feeCurrency?: string): Promise<FeeData> {
if (!feeCurrency) {
return super.getFeeData();
}
// On Celo, `eth_gasPrice` returns the base fee for the given currency multiplied 2
// and doesn't include tips. Source: https://github.com/jmrossy/celo-ethers-wrapper/pull/20#discussion_r1579179736
const baseFeePerGasInFeeCurrency = getBigInt(await this.send("eth_gasPrice", [feeCurrency]));
const maxPriorityFeePerGasInFeeCurrency = getBigInt(
await this.send("eth_maxPriorityFeePerGas", [feeCurrency])
);
const maxFeePerGasInFeeCurrency = baseFeePerGasInFeeCurrency + maxPriorityFeePerGasInFeeCurrency;
return new FeeData(null, maxFeePerGasInFeeCurrency, maxPriorityFeePerGasInFeeCurrency);
}

async broadcastTransaction(signedTx: string): Promise<TransactionResponse> {
const { hash } = await resolveProperties({
blockNumber: this.getBlockNumber(),
Expand Down
87 changes: 32 additions & 55 deletions src/lib/CeloWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,8 @@ import {
Wordlist,
} from "ethers";
import CeloProvider from "./CeloProvider";
import { adjustForGasInflation } from "./transaction/utils";
import {
CeloTransaction,
CeloTransactionRequest,
getTxType,
serializeCeloTransaction,
} from "./transactions";
import { adjustForGasInflation, isEmpty } from "./transaction/utils";
import { CeloTransaction, CeloTransactionRequest, serializeCeloTransaction } from "./transactions";

const forwardErrors = [
"INSUFFICIENT_FUNDS",
Expand All @@ -32,33 +27,24 @@ export default class CeloWallet extends Wallet {
* Override to skip checkTransaction step which rejects Celo tx properties
* https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-signer/src.ts/index.ts
*/
async populateTransaction(
transaction: CeloTransactionRequest
): Promise<CeloTransaction> {
async populateTransaction(transaction: CeloTransactionRequest): Promise<CeloTransaction> {
let tx: any = await resolveProperties(transaction);
if (tx.to != null) {
tx.to = Promise.resolve(tx.to);
}

if (tx.from == null) {
if (isEmpty(tx.from)) {
tx.from = this.address;
}

const type = getTxType(tx);
if (!type && tx.gasPrice == null) {
tx.gasPrice = this.getGasPrice();
}

if (tx.nonce == null) {
if (isEmpty(tx.nonce)) {
tx.nonce = await this.provider?.getTransactionCount(tx.from, "pending");
}

tx = await resolveProperties(tx);
if (tx.gasLimit == null) {
tx.gasLimit = this.estimateGas(tx).catch((error) => {
if (isEmpty(tx.gasLimit)) {
try {
tx.gasLimit = await this.estimateGas(tx);
} catch (error: any) {
// If there is an error code it's an expected error
if (forwardErrors.indexOf(error.code) >= 0) {
throw error;
}
// If there is no error code it's an unexpected error
assertArgument(
false,
"cannot estimate gas; transaction may fail or may require manual gas limit",
Expand All @@ -68,26 +54,28 @@ export default class CeloWallet extends Wallet {
tx: tx,
}
);
});
}
}

if (tx.chainId == null) {
if (isEmpty(tx.maxPriorityFeePerGas) || isEmpty(tx.maxFeePerGas)) {
const { maxFeePerGas, maxPriorityFeePerGas } = (await (
this.provider as CeloProvider
)?.getFeeData(tx.feeCurrency as string | undefined))!;
tx.maxFeePerGas = maxFeePerGas;
tx.maxPriorityFeePerGas = maxPriorityFeePerGas;
}

if (isEmpty(tx.chainId)) {
tx.chainId = (await this.provider!.getNetwork()).chainId;
} else {
tx.chainId = Promise.all([
Promise.resolve(tx.chainId),
(await this.provider!.getNetwork()).chainId,
]).then((results) => {
if (results[1] !== 0n && results[0] !== results[1]) {
assertArgument(
false,
"chainId address mismatch",
"transaction",
transaction
);
tx.chainId = Promise.all([tx.chainId, (await this.provider!.getNetwork()).chainId]).then(
([txChainId, providerChainId]) => {
if (providerChainId !== 0n && txChainId !== providerChainId) {
assertArgument(false, "chainId address mismatch", "transaction", transaction);
}
return txChainId;
}
return results[0];
});
);
}
return resolveProperties<CeloTransaction>(tx);
}
Expand All @@ -111,19 +99,15 @@ export default class CeloWallet extends Wallet {
delete tx.from;
}

const signature = this.signingKey.sign(
keccak256(serializeCeloTransaction(tx))
);
const signature = this.signingKey.sign(keccak256(serializeCeloTransaction(tx)));
const serialized = serializeCeloTransaction(tx, signature);
return serialized;
}

/**
* Override to serialize transaction using custom serialize method
*/
async sendTransaction(
transaction: CeloTransactionRequest
): Promise<TransactionResponse> {
async sendTransaction(transaction: CeloTransactionRequest): Promise<TransactionResponse> {
const provider = this.provider!;

const pop = await this.populateTransaction(transaction);
Expand All @@ -150,15 +134,8 @@ export default class CeloWallet extends Wallet {
} as PerformActionRequest);
}

static fromMnemonic(
phrase: string,
path?: string,
wordlist?: Wordlist | null
) {
const hdWallet = HDNodeWallet.fromMnemonic(
Mnemonic.fromPhrase(phrase, null, wordlist),
path
);
static fromMnemonic(phrase: string, path?: string, wordlist?: Wordlist | null) {
const hdWallet = HDNodeWallet.fromMnemonic(Mnemonic.fromPhrase(phrase, null, wordlist), path);

return new CeloWallet(hdWallet.privateKey, new CeloProvider());
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/CeloscanProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class CeloscanProvider extends EtherscanProvider {
case "celo":
return "https://api.celoscan.io";
case "alfajores":
return "https://alfajores.celoscan.io";
return "https://api-alfajores.celoscan.io";
case "baklava":
// baklava is currently not supported by celoscan.io, so we use Blockscout
return "https://explorer.celo.org/baklava";
Expand Down
24 changes: 3 additions & 21 deletions src/lib/transaction/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { hexlify, BigNumberish, isBytesLike, toBeHex } from "ethers";
import { GAS_INFLATION_FACTOR } from "../../consts";

function isEmpty(value: string | BigNumberish | undefined | null) {
export function isEmpty(value: string | BigNumberish | undefined | null) {
if (value === undefined || value === null || value === "0" || value === 0n) {
return true;
}
Expand All @@ -11,30 +11,12 @@ function isEmpty(value: string | BigNumberish | undefined | null) {
return toBeHex(value) === "0x0";
}

function isPresent(value: string | BigNumberish | undefined | null) {
export function isPresent(value: string | BigNumberish | undefined | null) {
return !isEmpty(value);
}

export function isEIP1559(tx: any): boolean {
return isPresent(tx.maxFeePerGas) && isPresent(tx.maxPriorityFeePerGas);
}

export function isCIP64(tx: any) {
return (
isEIP1559(tx) &&
isPresent(tx.feeCurrency) &&
!isPresent(tx.gatewayFeeRecipient) &&
!isPresent(tx.gatewayFeeRecipient)
);
}

export function isCIP42(tx: any): boolean {
return (
isEIP1559(tx) &&
(isPresent(tx.feeCurrency) ||
isPresent(tx.gatewayFeeRecipient) ||
isPresent(tx.gatewayFee))
);
return isPresent(tx.feeCurrency);
}

export function concatHex(values: string[]): `0x${string}` {
Expand Down
Loading

0 comments on commit e0336c1

Please sign in to comment.