Skip to content

Commit

Permalink
feat: network fees from gas station (#292)
Browse files Browse the repository at this point in the history
* feat: add gas station for networks

* refactor: add trace for gas fees

* refactor: move networks into common

* refactor: default decimals
  • Loading branch information
superical committed Nov 15, 2023
1 parent 651c475 commit 67c6a13
Show file tree
Hide file tree
Showing 29 changed files with 225 additions and 27 deletions.
92 changes: 92 additions & 0 deletions src/__tests__/gas-station.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fetch from "node-fetch";
import { gasStation } from "../common/gas-station";
import { BigNumber } from "ethers";
import { getSupportedNetwork } from "../common/networks";
import { getFeeData } from "../utils";

const mockData = {
standard: {
maxPriorityFee: 1.9666241746,
maxFee: 1.9666241895999999,
},
fast: {
maxPriorityFee: 2.5184666637333333,
maxFee: 2.518466678733333,
},
};

jest.mock("node-fetch");

jest.mock("../common/networks", () => ({
getSupportedNetworkNameFromId: jest.fn(),
getSupportedNetwork: jest.fn(),
}));

describe("gasStation", () => {
describe("fetch gas fees", () => {
it("should return undefined if no gasStationUrl is provided", async () => {
const result = await gasStation("")();
expect(result).toBeUndefined();
});

it("should fetch data from gasStationUrl and return GasStationFeeData", async () => {
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce({
json: jest.fn().mockResolvedValueOnce(mockData),
} as any);

const result = await gasStation("mock-url")();
expect(result).toEqual({
maxFeePerGas: BigNumber.from("1966624190"),
maxPriorityFeePerGas: BigNumber.from("1966624175"),
});
});

it("should throw an error if fetching fails", async () => {
(fetch as jest.MockedFunction<typeof fetch>).mockRejectedValueOnce(new Error("Fetch error"));

await expect(gasStation("mock-url")()).rejects.toThrow("Failed to fetch gas station");
});
});

describe("getFeeData", () => {
const mockProvider = {
getNetwork: jest.fn().mockResolvedValue({ chainId: "123" }),
getFeeData: jest.fn().mockResolvedValue("providerFeeData"),
};

beforeEach(() => {
mockProvider.getNetwork.mockClear();
mockProvider.getFeeData.mockClear();
});

it("should get fee data from provider if gas station is not available", async () => {
(getSupportedNetwork as jest.Mock).mockReturnValueOnce(undefined);

const res = await getFeeData(mockProvider as any);

expect(res).toBe("providerFeeData");
expect(mockProvider.getFeeData).toHaveBeenCalledTimes(1);
});

it("should use the gas station when it is available", async () => {
const mockGasStation = jest.fn().mockReturnValue("mockGasStationData");
(getSupportedNetwork as jest.Mock).mockReturnValueOnce({ gasStation: mockGasStation });

await getFeeData(mockProvider as any);

expect(mockProvider.getFeeData).not.toHaveBeenCalled();
expect(mockGasStation).toHaveBeenCalledTimes(1);
});

it("should get fee data from provider if gas station returns undefined", async () => {
const mockGasStation = jest.fn().mockReturnValue(undefined);
(getSupportedNetwork as jest.Mock).mockReturnValueOnce({ gasStation: mockGasStation });

const res = await getFeeData(mockProvider as any);

expect(mockProvider.getFeeData).toHaveBeenCalledTimes(1);
expect(mockGasStation).toHaveBeenCalledTimes(1);
expect(res).toBe("providerFeeData");
});
});
});
2 changes: 1 addition & 1 deletion src/commands/config/config.type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NetworkCmdName } from "../networks";
import { NetworkCmdName } from "../../common/networks";

export interface CreateConfigCommand {
network: NetworkCmdName;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/config/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { create } from "../../implementations/config/create";
import { getLogger } from "../../logger";
import { highlight } from "../../utils";
import { CreateConfigCommand, TestNetwork } from "./config.type";
import { NetworkCmdName } from "../networks";
import { NetworkCmdName } from "../../common/networks";

const { trace } = getLogger("config:create");

Expand Down
2 changes: 1 addition & 1 deletion src/commands/shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Argv } from "yargs";
import { supportedNetwork } from "./networks";
import { supportedNetwork } from "../common/networks";

export interface NetworkOption {
network: string;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@govtechsg/oa-verify";
import { readOpenAttestationFile } from "../implementations/utils/disk";
import { withNetworkOption } from "./shared";
import { getSupportedNetwork } from "./networks";
import { getSupportedNetwork } from "../common/networks";

export const command = "verify [options]";

Expand Down
51 changes: 51 additions & 0 deletions src/common/gas-station/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BigNumber, ethers } from "ethers";
import fetch from "node-fetch";

export type GasStationFunction = (gasStationUrl: string) => () => Promise<GasStationFeeData | undefined>;
export type GasStationFeeData = { maxPriorityFeePerGas: BigNumber | null; maxFeePerGas: BigNumber | null };

export const gasStation: GasStationFunction =
(gasStationUrl: string, decimals = 9) =>
async (): Promise<GasStationFeeData | undefined> => {
try {
if (!gasStationUrl) return undefined;
const res = await fetch(gasStationUrl);
const data = await res.json();
return {
maxPriorityFeePerGas: safeParseUnits(data.standard.maxPriorityFee.toString(), decimals),
maxFeePerGas: safeParseUnits(data.standard.maxFee.toString(), decimals),
};
} catch (e) {
throw new Error("Failed to fetch gas station");
}
};

const safeParseUnits = (_value: number | string, decimals: number): BigNumber => {
const value = String(_value);
if (!value.match(/^[0-9.]+$/)) {
throw new Error(`invalid gwei value: ${_value}`);
}

// Break into [ whole, fraction ]
const comps = value.split(".");
if (comps.length === 1) {
comps.push("");
}

// More than 1 decimal point or too many fractional positions
if (comps.length !== 2) {
throw new Error(`invalid gwei value: ${_value}`);
}

// Pad the fraction to 9 decimal places
while (comps[1].length < decimals) {
comps[1] += "0";
}

// Too many decimals and some non-zero ending, take the ceiling
if (comps[1].length > 9 && !comps[1].substring(9).match(/^0+$/)) {
comps[1] = BigNumber.from(comps[1].substring(0, 9)).add(BigNumber.from(1)).toString();
}

return ethers.utils.parseUnits(`${comps[0]}.${comps[1]}`, decimals);
};
29 changes: 21 additions & 8 deletions src/commands/networks.ts → src/common/networks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { providers } from "ethers";
import type { GasStationFunction } from "./gas-station";
import { gasStation } from "./gas-station";

export type networkCurrency = "ETH" | "MATIC" | "XDC";

type SupportedNetwork = {
explorer: string;
provider: () => providers.Provider;
networkId: number;
networkName: string;
networkName: typeof NetworkCmdName[keyof typeof NetworkCmdName];
currency: networkCurrency;
gasStation?: ReturnType<GasStationFunction>;
};

export enum NetworkCmdName {
Expand Down Expand Up @@ -37,53 +40,63 @@ export const supportedNetwork: {
explorer: "https://localhost/explorer",
provider: jsonRpcProvider("http://127.0.0.1:8545"),
networkId: 1337,
networkName: "local",
networkName: NetworkCmdName.Local,
currency: "ETH",
},
[NetworkCmdName.Mainnet]: {
explorer: "https://etherscan.io",
provider: defaultInfuraProvider("homestead"),
networkId: 1,
networkName: "homestead",
networkName: NetworkCmdName.Mainnet,
currency: "ETH",
},
[NetworkCmdName.Sepolia]: {
explorer: "https://sepolia.etherscan.io",
provider: jsonRpcProvider("https://sepolia.infura.io/v3/bb46da3f80e040e8ab73c0a9ff365d18"),
networkId: 11155111,
networkName: "sepolia",
networkName: NetworkCmdName.Sepolia,
currency: "ETH",
},
[NetworkCmdName.Matic]: {
explorer: "https://polygonscan.com",
provider: defaultInfuraProvider("matic"),
networkId: 137,
networkName: "matic",
networkName: NetworkCmdName.Matic,
currency: "MATIC",
gasStation: gasStation("https://gasstation.polygon.technology/v2"),
},
[NetworkCmdName.Maticmum]: {
explorer: "https://mumbai.polygonscan.com",
provider: defaultInfuraProvider("maticmum"),
networkId: 80001,
networkName: "maticmum",
networkName: NetworkCmdName.Maticmum,
currency: "MATIC",
gasStation: gasStation("https://gasstation-testnet.polygon.technology/v2"),
},
[NetworkCmdName.XDC]: {
explorer: "https://xdcscan.io",
provider: jsonRpcProvider("https://erpc.xinfin.network"),
networkId: 50,
networkName: "xdc",
networkName: NetworkCmdName.XDC,
currency: "XDC",
},
[NetworkCmdName.XDCApothem]: {
explorer: "https://apothem.xdcscan.io",
provider: jsonRpcProvider("https://erpc.apothem.network"),
networkId: 51,
networkName: "xdcapothem",
networkName: NetworkCmdName.XDCApothem,
currency: "XDC",
},
};

export const getSupportedNetwork = (networkCmdName: string): SupportedNetwork => {
return supportedNetwork[networkCmdName as NetworkCmdName];
};

export const getSupportedNetworkNameFromId = (networkId: number): SupportedNetwork["networkName"] => {
const network = Object.values(supportedNetwork).find((network) => network.networkId === networkId);
if (!network) {
throw new Error(`Unsupported chain id ${networkId}`);
}
return network.networkName;
};
6 changes: 3 additions & 3 deletions src/implementations/config/__tests__/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { prompt } from "inquirer";
import tmp from "tmp";
import { CreateConfigCommand } from "../../../commands/config/config.type";
import { handler as createTempDNS } from "../../../commands/dns/txt-record/create";
import { NetworkCmdName } from "../../../commands/networks";
import { deployDocumentStore } from "../../deploy/document-store/document-store";
import { deployTokenRegistry } from "../../deploy/token-registry/token-registry";
import { NetworkCmdName } from "../../../common/networks";
import { deployDocumentStore } from "../../deploy/document-store";
import { deployTokenRegistry } from "../../deploy/token-registry";
import { create as createConfig } from "../create";
import expectedConfigFileOutput from "./expected-config-file-output-v2.json";
import inputConfigFile from "./input-config-file.json";
Expand Down
2 changes: 1 addition & 1 deletion src/implementations/config/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NetworkCmdName } from "../../../commands/networks";
import { NetworkCmdName } from "../../../common/networks";
import {
getConfigFile,
getConfigWithUpdatedForms,
Expand Down
2 changes: 1 addition & 1 deletion src/implementations/config/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "./helpers";
import { Dns } from "./types";
import { getWalletOrSigner } from "../utils/wallet";
import { supportedNetwork } from "../../commands/networks";
import { supportedNetwork } from "../../common/networks";

const SANDBOX_ENDPOINT_URL = "https://sandbox.fyntech.io";

Expand Down
10 changes: 5 additions & 5 deletions src/implementations/config/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { utils, v2, v3 } from "@govtechsg/open-attestation";
import { updateFormV2, updateFormV3 } from "@govtechsg/tradetrust-config";
import fetch from "node-fetch";
import { success } from "signale";
import { NetworkCmdName, supportedNetwork, networkCurrency } from "../../commands/networks";
import { deployDocumentStore } from "../../implementations/deploy/document-store";
import { deployTokenRegistry } from "../../implementations/deploy/token-registry";
import { readFile } from "../../implementations/utils/disk";
import { NetworkCmdName, supportedNetwork, networkCurrency } from "../../common/networks";
import { deployDocumentStore } from "../deploy/document-store";
import { deployTokenRegistry } from "../deploy/token-registry";
import { readFile } from "../utils/disk";
import { highlight } from "../../utils";
import { ConfigFile, Dns, Form } from "./types";
import { Wallet } from "ethers";
import { ConnectedSigner } from "../../implementations/utils/wallet";
import { ConnectedSigner } from "../utils/wallet";

interface ConfigWithNetwork {
configFile: ConfigFile;
Expand Down
2 changes: 2 additions & 0 deletions src/implementations/deploy/document-store/document-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const deployDocumentStore = async ({
}
const factory = new DocumentStoreFactory(wallet);
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);
signale.await(`Sending transaction to pool`);
const transaction = await factory.deploy(storeName, ownerAddress, { ...gasFees });
trace(`Tx hash: ${transaction.deployTransaction.hash}`);
Expand Down
2 changes: 2 additions & 0 deletions src/implementations/deploy/token-registry/token-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export const deployTokenRegistry = async ({
}

const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);

if (!standalone) {
if (!deployerContractAddress || !implAddress) {
Expand Down
2 changes: 2 additions & 0 deletions src/implementations/document-store/grant-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const grantDocumentStoreRole = async ({
process.exit(0);
}
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);
await documentStore.callStatic.grantRole(roleString, account, { ...gasFees });
signale.await(`Sending transaction to pool`);
const transaction = await documentStore.grantRole(roleString, account, { ...gasFees });
Expand Down
2 changes: 2 additions & 0 deletions src/implementations/document-store/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const issueToDocumentStore = async ({
process.exit(0);
}
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);
await documentStore.callStatic.issue(hash, { ...gasFees });
signale.await(`Sending transaction to pool`);
const transaction = await documentStore.issue(hash, { ...gasFees });
Expand Down
2 changes: 2 additions & 0 deletions src/implementations/document-store/revoke-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const revokeDocumentStoreRole = async ({
process.exit(0);
}
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);
await documentStore.callStatic.revokeRole(roleString, account, { ...gasFees });
signale.await(`Sending transaction to pool`);
const transaction = await documentStore.revokeRole(roleString, account, { ...gasFees });
Expand Down
2 changes: 2 additions & 0 deletions src/implementations/document-store/revoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const revokeToDocumentStore = async ({
process.exit(0);
}
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);
await documentStore.callStatic.revoke(hash, { ...gasFees });
signale.await(`Sending transaction to pool`);
const transaction = await documentStore.revoke(hash, { ...gasFees });
Expand Down
2 changes: 2 additions & 0 deletions src/implementations/document-store/transfer-ownership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const transferDocumentStoreOwnership = async ({
process.exit(0);
}
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);
signale.await(`Sending transaction to pool`);
await documentStore.callStatic.grantRole(roleString, newOwner, { ...gasFees });
const grantTransaction = await documentStore.grantRole(roleString, newOwner, { ...gasFees });
Expand Down
2 changes: 2 additions & 0 deletions src/implementations/title-escrow/acceptSurrendered.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const acceptSurrendered = async ({
process.exit(0);
}
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);
await tokenRegistryInstance.callStatic.burn(tokenId, { ...gasFees });
signale.await(`Sending transaction to pool`);
const transaction = await tokenRegistryInstance.burn(tokenId, { ...gasFees });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const endorseNominatedBeneficiary = async ({
process.exit(0);
}
const gasFees = await getGasFees({ provider: wallet.provider, ...rest });
trace(`Gas maxFeePerGas: ${gasFees.maxFeePerGas}`);
trace(`Gas maxPriorityFeePerGas: ${gasFees.maxPriorityFeePerGas}`);
await titleEscrow.callStatic.transferBeneficiary(nominatedBeneficiary, { ...gasFees });
signale.await(`Sending transaction to pool`);
const transaction = await titleEscrow.transferBeneficiary(nominatedBeneficiary, { ...gasFees });
Expand Down

0 comments on commit 67c6a13

Please sign in to comment.