Skip to content

Commit

Permalink
No template op stack script (#42)
Browse files Browse the repository at this point in the history
* bump sdk

* Revert "bump sdk"

This reverts commit d17fded.

* abstract class

* add op redeemer

* op test passing

* update devnode addresses and dont use waitToProve
  • Loading branch information
godzillaba authored Jan 15, 2025
1 parent 48378d3 commit 2105b6a
Show file tree
Hide file tree
Showing 6 changed files with 674 additions and 62 deletions.
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;
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

0 comments on commit 2105b6a

Please sign in to comment.