Skip to content

Commit

Permalink
Merge branch 'master' into pxrl/batchCompute
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholaspai committed Feb 21, 2024
2 parents daee1c6 + 351387a commit 3c9be9d
Show file tree
Hide file tree
Showing 30 changed files with 2,825 additions and 177 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- run: yarn install
- run: yarn test
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ cache
yarn-error.log
yarn-debug.log*
gasReporterOutput.json
logs
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16
18
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"copy-abi": "abi=utils/abi/contracts; dstdir=\"./dist/${DIR}/${abi}\"; mkdir -p \"${dstdir}\"; cp ./src/${abi}/*.json \"${dstdir}\"",
"test": "hardhat test",
"test:watch": "hardhat watch test",
"test:run:arweave": "npx -y arlocal",
"lint": "eslint --fix src test e2e && yarn prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"e2e/**/*.ts\"",
"lint-check": "eslint src test e2e && yarn prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"e2e/**/*.ts\"",
"prepare": "yarn build && husky install",
Expand Down Expand Up @@ -63,6 +64,7 @@
"@types/lodash.get": "^4.4.7",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"arlocal": "^1.1.65",
"chai": "^4.3.8",
"chai-exclude": "^2.1.0",
"dotenv": "^16.0.0",
Expand Down Expand Up @@ -98,11 +100,12 @@
"dependencies": {
"@across-protocol/across-token": "^1.0.0",
"@across-protocol/constants-v2": "^1.0.11",
"@across-protocol/contracts-v2": "2.5.0-beta.5",
"@across-protocol/contracts-v2": "2.5.0-beta.7",
"@eth-optimism/sdk": "^3.1.8",
"@pinata/sdk": "^2.1.0",
"@types/mocha": "^10.0.1",
"@uma/sdk": "^0.34.1",
"arweave": "^1.14.4",
"axios": "^0.27.2",
"big-number": "^2.0.0",
"decimal.js": "^10.3.1",
Expand Down
200 changes: 200 additions & 0 deletions src/caching/Arweave/ArweaveClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import Arweave from "arweave";
import { JWKInterface } from "arweave/node/lib/wallet";
import { ethers } from "ethers";
import winston from "winston";
import { isDefined, jsonReplacerWithBigNumbers, parseWinston, toBN } from "../../utils";
import { Struct, is } from "superstruct";
import { ARWEAVE_TAG_APP_NAME } from "../../constants";

export class ArweaveClient {
private client: Arweave;

public constructor(
private arweaveJWT: JWKInterface,
private logger: winston.Logger,
gatewayURL = "arweave.net",
protocol = "https",
port = 443
) {
this.client = new Arweave({
host: gatewayURL,
port,
protocol,
timeout: 20000,
logging: false,
});
this.logger.info("Arweave client initialized");
}

/**
* Stores an arbitrary record in the Arweave network. The record is stored as a JSON string and uses
* JSON.stringify to convert the record to a string. The record has all of its big numbers converted
* to strings for convenience.
* @param value The value to store
* @param topicTag An optional topic tag to add to the transaction
* @returns The transaction ID of the stored value
* @
*/
async set(value: Record<string, unknown>, topicTag?: string | undefined): Promise<string | undefined> {
const transaction = await this.client.createTransaction(
{ data: JSON.stringify(value, jsonReplacerWithBigNumbers) },
this.arweaveJWT
);

// Add tags to the transaction
transaction.addTag("Content-Type", "application/json");
transaction.addTag("App-Name", ARWEAVE_TAG_APP_NAME);
if (isDefined(topicTag)) {
transaction.addTag("Topic", topicTag);
}

// Sign the transaction
await this.client.transactions.sign(transaction, this.arweaveJWT);
// Send the transaction
const result = await this.client.transactions.post(transaction);

// Ensure that the result is successful
if (result.status !== 200) {
const message = result?.data?.error?.msg ?? "Unknown error";
this.logger.error({
at: "ArweaveClient:set",
message,
result,
txn: transaction.id,
address: await this.getAddress(),
balance: (await this.getBalance()).toString(),
});
throw new Error(message);
} else {
this.logger.debug({
at: "ArweaveClient:set",
message: `Arweave transaction posted with ${transaction.id}`,
});
}
return transaction.id;
}

/**
* Retrieves a record from the Arweave network. The record is expected to be a JSON string and is
* parsed using JSON.parse. All numeric strings are converted to big numbers for convenience.
* @param transactionID The transaction ID of the record to retrieve
* @param structValidator An optional struct validator to validate the retrieved value. If the value does not match the struct, null is returned.
* @returns The record if it exists, otherwise null
*/
async get<T>(transactionID: string, validator: Struct<T>): Promise<T | null> {
const rawData = await this.client.transactions.getData(transactionID, { decode: true, string: true });
if (!rawData) {
return null;
}
// Parse the retrieved data - if it is an Uint8Array, it is a buffer and needs to be converted to a string
const data = JSON.parse(typeof rawData === "string" ? rawData : Buffer.from(rawData).toString("utf-8"));
// Ensure that the result is successful. If it is not, the retrieved value is not our expected type
// but rather a {status: string, statusText: string} object. We can detect that and return null.
if (data.status === 400) {
return null;
}
// If the validator does not match the retrieved value, return null and log a warning
if (!is(data, validator)) {
this.logger.warn("Retrieved value from Arweave does not match the expected type");
return null;
}
return data;
}

/**
* Retrieves a list of records from the Arweave network that have a specific tag.
* The records are expected to be a JSON string and are pre-filtered by the Across
* protocol tag, the content-type tag, and this client's address. Furthermore, the
* records are expected to be an array of the given type and will be discarded if
* they do not match the given validator.
* @param tag The tag to filter all the transactions by
* @param validator The validator to validate the retrieved values
* @returns The records if they exist, otherwise an empty array
*/
async getByTopic<T>(tag: string, validator: Struct<T>): Promise<{ data: T; hash: string }[]> {
const transactions = await this.client.api.post<{
data: {
transactions: {
edges: {
node: {
id: string;
};
}[];
};
};
}>("/graphql", {
query: `
{
transactions (
owners: ["${await this.getAddress()}"]
tags: [
{ name: "App-Name", values: ["${ARWEAVE_TAG_APP_NAME}"] },
{ name: "Content-Type", values: ["application/json"] },
${tag ? `{ name: "Topic", values: ["${tag}"] } ` : ""}
]
) { edges { node { id } } }
}`,
});
const results = await Promise.all(
transactions.data.data.transactions.edges.map(async (edge) => {
const data = await this.get<T>(edge.node.id, validator);
return isDefined(data)
? {
data,
hash: edge.node.id,
}
: null;
})
);
return results.filter(isDefined);
}

/**
* Retrieves the metadata of a transaction
* @param transactionID The transaction ID of the record to retrieve
* @returns The metadata of the transaction if it exists, otherwise null
*/
async getMetadata(transactionID: string): Promise<Record<string, string> | null> {
const transaction = await this.client.transactions.get(transactionID);
if (!isDefined(transaction)) {
return null;
}
const tags = Object.fromEntries(
transaction.tags.map((tag) => [
tag.get("name", { decode: true, string: true }),
tag.get("value", { decode: true, string: true }),
])
);
return {
contentType: tags["Content-Type"],
appName: tags["App-Name"],
topic: tags.Topic,
};
}

/**
* Returns the address of the signer of the JWT
* @returns The address of the signer in this client
*/
getAddress(): Promise<string> {
return this.client.wallets.jwkToAddress(this.arweaveJWT);
}

/**
* The balance of the signer
* @returns The balance of the signer in winston units
*/
async getBalance(): Promise<ethers.BigNumber> {
const address = await this.getAddress();
const balanceInFloat = await this.client.wallets.getBalance(address);
// Sometimes the balance is returned in scientific notation, so we need to
// convert it to a BigNumber
if (balanceInFloat.includes("e")) {
const [balance, exponent] = balanceInFloat.split("e");
const resultingBN = ethers.BigNumber.from(balance).mul(toBN(10).pow(exponent.replace("+", "")));
return parseWinston(resultingBN.toString());
} else {
return parseWinston(balanceInFloat);
}
}
}
1 change: 1 addition & 0 deletions src/caching/Arweave/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ArweaveClient";
1 change: 1 addition & 0 deletions src/caching/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./IPFS";
export * from "./Arweave";
28 changes: 15 additions & 13 deletions src/clients/HubPoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,19 @@ type L1TokensToDestinationTokens = {
};

// Temporary type for v2 -> v3 transition. @todo: Remove.
export type V2PartialDepositWithBlock = Pick<
type V2PartialDepositWithBlock = Pick<
V2DepositWithBlock,
"originChainId" | "destinationChainId" | "originToken" | "amount" | "quoteTimestamp" | "blockNumber"
"originChainId" | "originToken" | "amount" | "quoteTimestamp"
>;

// Temporary type for v2 -> v3 transition. @todo: Remove.
export type V3PartialDepositWithBlock = Pick<
type V3PartialDepositWithBlock = Pick<
V3DepositWithBlock,
"originChainId" | "destinationChainId" | "inputToken" | "inputAmount" | "quoteTimestamp" | "blockNumber"
"originChainId" | "inputToken" | "inputAmount" | "quoteTimestamp"
>;

export type LpFeeRequest = (V2PartialDepositWithBlock | V3PartialDepositWithBlock) & { paymentChainId?: number };

export class HubPoolClient extends BaseAbstractClient {
// L1Token -> destinationChainId -> destinationToken
protected l1TokensToDestinationTokens: L1TokensToDestinationTokens = {};
Expand Down Expand Up @@ -340,16 +342,12 @@ export class HubPoolClient extends BaseAbstractClient {
return utilization;
}

async computeRealizedLpFeePct(
deposit: V2PartialDepositWithBlock | V3PartialDepositWithBlock
): Promise<RealizedLpFee> {
async computeRealizedLpFeePct(deposit: LpFeeRequest): Promise<RealizedLpFee> {
const [lpFee] = await this.batchComputeRealizedLpFeePct([deposit]);
return lpFee;
}

async batchComputeRealizedLpFeePct(
_deposits: (V2PartialDepositWithBlock | V3PartialDepositWithBlock)[]
): Promise<RealizedLpFee[]> {
async batchComputeRealizedLpFeePct(_deposits: LpFeeRequest[]): Promise<RealizedLpFee[]> {
assert(_deposits.length > 0, "No deposits supplied to batchComputeRealizedLpFeePct");
if (!isDefined(this.currentTime)) {
throw new Error("HubPoolClient has not set a currentTime");
Expand Down Expand Up @@ -424,15 +422,19 @@ export class HubPoolClient extends BaseAbstractClient {
};

// Helper compute the realizedLpFeePct of an individual deposit based on pre-retrieved batch data.
const computeRealizedLpFeePct = async (deposit: V3PartialDepositWithBlock) => {
const { originChainId, destinationChainId, inputAmount, quoteTimestamp } = deposit;
const computeRealizedLpFeePct = async (deposit: (typeof deposits)[0]) => {
const { originChainId, paymentChainId, inputAmount, quoteTimestamp } = deposit;
const quoteBlock = quoteBlocks[quoteTimestamp];

if (paymentChainId === undefined) {
return { quoteBlock, realizedLpFeePct: bnZero };
}

const hubPoolToken = getHubPoolToken(deposit, quoteBlock);
const rateModel = this.configStoreClient.getRateModelForBlockNumber(
hubPoolToken,
originChainId,
destinationChainId,
paymentChainId,
quoteBlock
);

Expand Down
Loading

0 comments on commit 3c9be9d

Please sign in to comment.