Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"prepare": "forge install && cd lib/arbitrum-sdk && yarn",
"prepublishOnly": "make clean && make build",
"gen-recipients": "make install && hardhat run src-ts/getRecipientData.ts",
"test:e2e": "./test/e2e/test-e2e.bash"
"test:e2e": "./test/e2e/test-e2e.bash",
"test:op-e2e": "mocha test/op-e2e/OpChildToParentRewardRouter.test.ts --timeout 300000"
},
"private": false,
"devDependencies": {
Expand All @@ -52,7 +53,8 @@
"solidity-coverage": "^0.8.5",
"ts-node": "^10.9.1",
"typechain": "^8.1.0",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"viem": "^2.22.8"
},
"dependencies": {
"@types/yargs": "^17.0.32",
Expand Down
219 changes: 183 additions & 36 deletions src-ts/FeeRouter/ChildToParentMessageRedeemer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JsonRpcProvider } from "@ethersproject/providers";
import { JsonRpcProvider, Log } from "@ethersproject/providers";
import { Wallet } from "ethers";
import {
ChildToParentRewardRouter__factory,
Expand All @@ -12,72 +12,112 @@ import {
L2ToL1Message,
L2ToL1MessageStatus,
} from "../../lib/arbitrum-sdk/src";

import {
Chain,
ChainContract,
createPublicClient,
createWalletClient,
Hex,
http,
publicActions,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import {
getWithdrawals,
GetWithdrawalStatusReturnType,
publicActionsL1,
publicActionsL2,
walletActionsL1,
} from 'viem/op-stack'

const wait = async (ms: number) => new Promise((res) => setTimeout(res, ms));

export default class ChildToParentMessageRedeemer {
public startBlock: number;
public childToParentRewardRouter: ChildToParentRewardRouter;
public readonly retryDelay: number;
export abstract class ChildToParentMessageRedeemer {
constructor(
public readonly childChainProvider: JsonRpcProvider,
public readonly parentChainSigner: Wallet,
public readonly childChainRpc: string,
public readonly parentChainRpc: string,
protected readonly parentChainPrivateKey: string,
public readonly childToParentRewardRouterAddr: string,
public readonly blockLag: number,
initialStartBlock: number,
retryDelay = 1000 * 60 * 10
) {
this.startBlock = initialStartBlock;
this.childToParentRewardRouter = ChildToParentRewardRouter__factory.connect(
childToParentRewardRouterAddr,
childChainProvider
);
this.retryDelay = retryDelay;
}
public startBlock: number = 0,
public readonly retryDelay = 1000 * 60 * 10
) {}

protected abstract _handleLogs(logs: Log[], oneOff: boolean): Promise<void>;

public async redeemChildToParentMessages(oneOff = false) {
const childChainProvider = new JsonRpcProvider(this.childChainRpc);

const toBlock =
(await this.childChainProvider.getBlockNumber()) - this.blockLag;
const logs = await this.childChainProvider.getLogs({
(await childChainProvider.getBlockNumber()) - this.blockLag;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe make it max of this or 0, otherwise it might cause problems in test environments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good flag, i think the block lag is unnecessary in oneOff mode, so I'll remove it when we get rid of !oneOff mode today or tomorrow

const logs = await childChainProvider.getLogs({
fromBlock: this.startBlock,
toBlock: toBlock,
...this.childToParentRewardRouter.filters.FundsRouted(),
address: this.childToParentRewardRouterAddr,
topics: [ChildToParentRewardRouter__factory.createInterface().getEventTopic('FundsRouted')],
});
if (logs.length) {
console.log(
`Found ${logs.length} route events between blocks ${this.startBlock} and ${toBlock}`
);
}
await this._handleLogs(logs, oneOff);
return toBlock
}

public async run(oneOff = false) {
while (true) {
let toBlock = 0
try {
toBlock = await this.redeemChildToParentMessages(oneOff);
} catch (err) {
console.log("err", err);
}
if (oneOff) {
break;
} else {
this.startBlock = toBlock + 1;
await wait(1000 * 60 * 60);
}
}
}
}

export class ArbChildToParentMessageRedeemer extends ChildToParentMessageRedeemer {
protected async _handleLogs(logs: Log[], oneOff: boolean): Promise<void> {
const childChainProvider = new JsonRpcProvider(this.childChainRpc);
const parentChainSigner = new Wallet(this.parentChainPrivateKey, new JsonRpcProvider(this.parentChainRpc));
for (let log of logs) {
const arbTransactionRec = new L2TransactionReceipt(
await this.childChainProvider.getTransactionReceipt(log.transactionHash)
await childChainProvider.getTransactionReceipt(log.transactionHash)
);
let l2ToL1Events =
(await arbTransactionRec.getL2ToL1Events()) as EventArgs<L2ToL1TxEvent>[];
arbTransactionRec.getL2ToL1Events() as EventArgs<L2ToL1TxEvent>[];

if (l2ToL1Events.length != 1) {
throw new Error("Only 1 l2 to l1 message per tx supported");
}

for (let l2ToL1Event of l2ToL1Events) {
const l2ToL1Message = L2ToL1Message.fromEvent(
this.parentChainSigner,
parentChainSigner,
l2ToL1Event
);
if (!oneOff) {
console.log(`Waiting for ${l2ToL1Event.hash} to be ready:`);
await l2ToL1Message.waitUntilReadyToExecute(
this.childChainProvider,
childChainProvider,
this.retryDelay
);
}

const status = await l2ToL1Message.status(this.childChainProvider);
const status = await l2ToL1Message.status(childChainProvider);
switch (status) {
case L2ToL1MessageStatus.CONFIRMED: {
console.log(l2ToL1Event.hash, "confirmed; executing:");
const rec = await (
await l2ToL1Message.execute(this.childChainProvider)
await l2ToL1Message.execute(childChainProvider)
).wait(2);
console.log(`${l2ToL1Event.hash} executed:`, rec.transactionHash);
break;
Expand All @@ -96,21 +136,128 @@ export default class ChildToParentMessageRedeemer {
}
}
}
this.startBlock = toBlock;
}
}

public async run(oneOff = false) {
while (true) {

export type OpChildChainConfig = Chain & {
contracts: {
portal: { [x: number]: ChainContract }
disputeGameFactory: { [x: number]: ChainContract }
}
}

export class OpChildToParentMessageRedeemer extends ChildToParentMessageRedeemer {
public readonly childChainViemProvider
public readonly parentChainViemSigner

constructor(
childChainRpc: string,
parentChainRpc: string,
parentChainPrivateKey: string,
childToParentRewardRouterAddr: string,
blockLag: number,
startBlock: number = 0,
public readonly childChainViem: OpChildChainConfig,
public readonly parentChainViem: Chain,
retryDelay = 1000 * 60 * 10,
) {
super(
childChainRpc,
parentChainRpc,
parentChainPrivateKey,
childToParentRewardRouterAddr,
blockLag,
startBlock,
retryDelay
)

this.childChainViemProvider = createPublicClient({
chain: childChainViem,
transport: http(childChainRpc),
}).extend(publicActionsL2())

this.parentChainViemSigner = createWalletClient({
chain: parentChainViem,
account: privateKeyToAccount(
parentChainPrivateKey as `0x${string}`
),
transport: http(parentChainRpc),
})
.extend(publicActions)
.extend(walletActionsL1())
.extend(publicActionsL1())
}

protected async _handleLogs(logs: Log[], oneOff: boolean): Promise<void> {
if (!oneOff) throw new Error('OpChildToParentMessageRedeemer only supports one-off mode')
for (const log of logs) {
const receipt = await this.childChainViemProvider.getTransactionReceipt({
hash: log.transactionHash as Hex,
})

// 'waiting-to-prove'
// 'ready-to-prove'
// 'waiting-to-finalize'
// 'ready-to-finalize'
// 'finalized'
let status: GetWithdrawalStatusReturnType;
try {
await this.redeemChildToParentMessages(oneOff);
} catch (err) {
console.log("err", err);
status = await this.parentChainViemSigner.getWithdrawalStatus({
receipt,
targetChain: this.childChainViemProvider.chain,
})
} catch (e: any) {
// workaround
if (e.metaMessages[0] === 'Error: Unproven()') {
status = 'ready-to-prove'
}
else {
throw e;
}
}
if (oneOff) {
break;
} else {
await wait(1000 * 60 * 60);

console.log(`${log.transactionHash} ${status}`)

if (status === 'ready-to-prove') {
// 1. Get withdrawal information
const [withdrawal] = getWithdrawals(receipt)
const output = await this.parentChainViemSigner.getL2Output({
l2BlockNumber: receipt.blockNumber,
targetChain: this.childChainViem,
})
// 2. Build parameters to prove the withdrawal on the L2.
const args = await this.childChainViemProvider.buildProveWithdrawal({
output,
withdrawal,
})
// 3. Prove the withdrawal on the L1.
const hash = await this.parentChainViemSigner.proveWithdrawal(args)
// 4. Wait until the prove withdrawal is processed.
await this.parentChainViemSigner.waitForTransactionReceipt({
hash,
})

console.log(`${log.transactionHash} proved:`, hash)
} else if (status === 'ready-to-finalize') {
const [withdrawal] = getWithdrawals(receipt)

// 1. Wait until the withdrawal is ready to finalize. (done)

// 2. Finalize the withdrawal.
const hash = await this.parentChainViemSigner.finalizeWithdrawal({
targetChain: this.childChainViemProvider.chain,
withdrawal,
})

// 3. Wait until the withdrawal is finalized.
await this.parentChainViemSigner.waitForTransactionReceipt({
hash,
})

console.log(`${log.transactionHash} finalized:`, hash)
}
}
}
}

62 changes: 45 additions & 17 deletions src-ts/cli/childToParentRedeemer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import dotenv from "dotenv";
import yargs from "yargs";
import ChildToParentMessageRedeemer from "../FeeRouter/ChildToParentMessageRedeemer";
import { JsonRpcProvider } from "@ethersproject/providers";
import yargs, { option } from "yargs";
import {
ArbChildToParentMessageRedeemer,
OpChildChainConfig,
OpChildToParentMessageRedeemer,
ChildToParentMessageRedeemer
} from '../FeeRouter/ChildToParentMessageRedeemer';
import { Wallet } from "ethers";
import { JsonRpcProvider } from "@ethersproject/providers";
import chains from 'viem/chains';

dotenv.config();

Expand All @@ -24,23 +30,45 @@ const options = yargs(process.argv.slice(2))
description:
"Runs continuously if false, runs once and terminates if true",
},
opStack: { type: 'boolean', demandOption: false, default: false },
})
.parseSync();

(async () => {
const parentChildSigner = new Wallet(
PARENT_CHAIN_PK,
new JsonRpcProvider(options.parentRPCUrl)
);
console.log(`Signing with ${parentChildSigner.address} on parent chain
${(await parentChildSigner.provider.getNetwork()).chainId}'`);

const redeemer = new ChildToParentMessageRedeemer(
new JsonRpcProvider(options.childRPCUrl),
parentChildSigner,
options.childToParentRewardRouterAddr,
options.blockLag,
options.childChainStartBlock
);
const parentChildSigner = new Wallet(PARENT_CHAIN_PK, new JsonRpcProvider(options.parentRPCUrl));
const childChainProvider = new JsonRpcProvider(options.childRPCUrl);
const parentChainId = (await parentChildSigner.provider.getNetwork()).chainId;
const childChainId = (await childChainProvider.getNetwork()).chainId;
console.log(`Signing with ${parentChildSigner.address} on parent chain ${parentChainId}'`);

let redeemer: ChildToParentMessageRedeemer;
if (options.opStack) {
const childChain = Object.values(chains).find(c => c.id === childChainId)
const parentChain = Object.values(chains).find(c => c.id === parentChainId)

if (!childChain || !parentChain) {
throw new Error('Unsupported chain')
}

redeemer = new OpChildToParentMessageRedeemer(
options.childRPCUrl,
options.parentRPCUrl,
PARENT_CHAIN_PK,
options.childToParentRewardRouterAddr,
options.blockLag,
options.childChainStartBlock,
childChain as OpChildChainConfig,
parentChain
)
} else {
redeemer = new ArbChildToParentMessageRedeemer(
options.childRPCUrl,
options.parentRPCUrl,
PARENT_CHAIN_PK,
options.childToParentRewardRouterAddr,
options.blockLag,
options.childChainStartBlock
);
}
await redeemer.run(options.oneOff);
})();
Loading
Loading