Skip to content

Commit 12bac31

Browse files
committed
feat: add commands to configure and use the Arbitrum bridge on the CLI
We also add Goerli and Arbitrum Nitro Devnet configurations (though the CLI commands don't support Nitro yet).
1 parent f6a86e4 commit 12bac31

23 files changed

+3326
-1987
lines changed

arbitrum-addresses.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"source": "https://github.com/OffchainLabs/arbitrum/tree/f54baf10871ee86aedca4880796342ef9bd0b0ab/packages/",
3+
"1": {
4+
"L1GatewayRouter": {
5+
"address": "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef"
6+
},
7+
"IInbox": {
8+
"address": "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f"
9+
}
10+
},
11+
"4": {
12+
"L1GatewayRouter": {
13+
"address": "0x70C143928eCfFaf9F5b406f7f4fC28Dc43d68380"
14+
},
15+
"IInbox": {
16+
"address": "0x578BAde599406A8fE3d24Fd7f7211c0911F5B29e"
17+
}
18+
},
19+
"5": {
20+
"L1GatewayRouter": {
21+
"address": "0x8BDFa67ace22cE2BFb2fFebe72f0c91CDA694d4b"
22+
},
23+
"IInbox": {
24+
"address": "0x1FdBBcC914e84aF593884bf8e8Dd6877c29035A2"
25+
}
26+
},
27+
"42161": {
28+
"L2GatewayRouter": {
29+
"address": "0x5288c571Fd7aD117beA99bF60FE0846C4E84F933"
30+
}
31+
},
32+
"421611": {
33+
"L2GatewayRouter": {
34+
"address": "0x9413AD42910c1eA60c737dB5f58d1C504498a3cD"
35+
}
36+
},
37+
"421612": {
38+
"L2GatewayRouter": {
39+
"address": "0xC502Ded1EE1d616B43F7f20Ebde83Be1A275ca3c"
40+
}
41+
}
42+
}

cli/address-book.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface AddressBook {
2828
}
2929

3030
export const getAddressBook = (path: string, chainId: string): AddressBook => {
31-
if (!path) throw new Error(`A path the the address book file is required.`)
31+
if (!path) throw new Error(`A path to the address book file is required.`)
3232
if (!chainId) throw new Error(`A chainId is required.`)
3333

3434
const addressBook = JSON.parse(fs.readFileSync(path, 'utf8') || '{}') as AddressBookJson

cli/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { proxyCommand } from './commands/proxy'
88
import { protocolCommand } from './commands/protocol'
99
import { contractsCommand } from './commands/contracts'
1010
import { airdropCommand } from './commands/airdrop'
11+
import { bridgeCommand } from './commands/bridge'
1112

1213
import { cliOpts } from './defaults'
1314

@@ -27,11 +28,13 @@ yargs
2728
.option('m', cliOpts.mnemonic)
2829
.option('p', cliOpts.providerUrl)
2930
.option('n', cliOpts.accountNumber)
31+
.option('r', cliOpts.arbitrumAddressBook)
3032
.command(deployCommand)
3133
.command(migrateCommand)
3234
.command(proxyCommand)
3335
.command(protocolCommand)
3436
.command(contractsCommand)
3537
.command(airdropCommand)
38+
.command(bridgeCommand)
3639
.demandCommand(1, 'Choose a command from the above list')
3740
.help().argv

cli/commands/bridge/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import yargs, { Argv } from 'yargs'
2+
3+
import { redeemSendToL2Command, sendToL2Command } from './to-l2'
4+
import { startSendToL1Command, finishSendToL1Command, waitFinishSendToL1Command } from './to-l1'
5+
import { cliOpts } from '../../defaults'
6+
7+
export const bridgeCommand = {
8+
command: 'bridge',
9+
describe: 'Graph token bridge actions.',
10+
builder: (yargs: Argv): yargs.Argv => {
11+
return yargs
12+
.option('-l', cliOpts.l2ProviderUrl)
13+
.command(sendToL2Command)
14+
.command(redeemSendToL2Command)
15+
.command(startSendToL1Command)
16+
.command(finishSendToL1Command)
17+
.command(waitFinishSendToL1Command)
18+
},
19+
handler: (): void => {
20+
yargs.showHelp()
21+
},
22+
}

cli/commands/bridge/to-l1.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { loadEnv, CLIArgs, CLIEnvironment } from '../../env'
2+
import { logger } from '../../logging'
3+
import { getAddressBook } from '../../address-book'
4+
import { getProvider, sendTransaction, toGRT } from '../../network'
5+
import { chainIdIsL2 } from '../../utils'
6+
import { loadAddressBookContract } from '../../contracts'
7+
import {
8+
L2TransactionReceipt,
9+
getL2Network,
10+
L2ToL1MessageStatus,
11+
L2ToL1MessageWriter,
12+
} from '@arbitrum/sdk'
13+
import { L2GraphTokenGateway } from '../../../build/types/L2GraphTokenGateway'
14+
import { BigNumber } from 'ethers'
15+
import { JsonRpcProvider } from '@ethersproject/providers'
16+
17+
const FOURTEEN_DAYS_IN_SECONDS = 24 * 3600 * 14
18+
19+
const BLOCK_SEARCH_THRESHOLD = 6 * 3600
20+
const searchForArbBlockByTimestamp = async (
21+
l2Provider: JsonRpcProvider,
22+
timestamp: number,
23+
): Promise<number> => {
24+
let step = 131072
25+
let block = await l2Provider.getBlock('latest')
26+
while (block.timestamp > timestamp) {
27+
while (block.number - step < 0) {
28+
step = Math.round(step / 2)
29+
}
30+
block = await l2Provider.getBlock(block.number - step)
31+
}
32+
while (step > 1 && Math.abs(block.timestamp - timestamp) > BLOCK_SEARCH_THRESHOLD) {
33+
step = Math.round(step / 2)
34+
if (block.timestamp - timestamp > 0) {
35+
block = await l2Provider.getBlock(block.number - step)
36+
} else {
37+
block = await l2Provider.getBlock(block.number + step)
38+
}
39+
}
40+
return block.number
41+
}
42+
43+
const wait = (ms: number): Promise<void> => {
44+
return new Promise((res) => setTimeout(res, ms))
45+
}
46+
47+
const waitUntilOutboxEntryCreatedWithCb = async (
48+
msg: L2ToL1MessageWriter,
49+
retryDelay: number,
50+
callback: () => void,
51+
) => {
52+
let done = false
53+
while (!done) {
54+
const status = await msg.status(null)
55+
if (status == L2ToL1MessageStatus.CONFIRMED) {
56+
done = true
57+
} else {
58+
callback()
59+
await wait(retryDelay)
60+
}
61+
}
62+
}
63+
64+
export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise<void> => {
65+
logger.info(`>>> Sending tokens to L1 <<<\n`)
66+
const l2Provider = getProvider(cliArgs.l2ProviderUrl)
67+
const l2ChainId = (await l2Provider.getNetwork()).chainId
68+
if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) {
69+
throw new Error(
70+
'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url',
71+
)
72+
}
73+
74+
const l1GRT = cli.contracts['GraphToken']
75+
const l1GRTAddress = l1GRT.address
76+
const amount = toGRT(cliArgs.amount)
77+
const recipient = cliArgs.recipient ? cliArgs.recipient : cli.wallet.address
78+
const l2Wallet = cli.wallet.connect(l2Provider)
79+
const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString())
80+
81+
const gateway = loadAddressBookContract('L2GraphTokenGateway', l2AddressBook, l2Wallet)
82+
const l2GRT = loadAddressBookContract('L2GraphToken', l2AddressBook, l2Wallet)
83+
84+
const l1Gateway = cli.contracts['L1GraphTokenGateway']
85+
logger.info(`Will send ${cliArgs.amount} GRT to ${recipient}`)
86+
logger.info(`Using L2 gateway ${gateway.address} and L1 gateway ${l1Gateway.address}`)
87+
88+
const params = [l1GRTAddress, recipient, amount, '0x']
89+
logger.info('Approving token transfer')
90+
await sendTransaction(l2Wallet, l2GRT, 'approve', [gateway.address, amount])
91+
logger.info('Sending outbound transfer transaction')
92+
const receipt = await sendTransaction(
93+
l2Wallet,
94+
gateway,
95+
'outboundTransfer(address,address,uint256,bytes)',
96+
params,
97+
)
98+
const l2Receipt = new L2TransactionReceipt(receipt)
99+
const l2ToL1Message = (
100+
await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider))
101+
)[0]
102+
103+
logger.info(
104+
`The transaction generated an outbox message with batch number ${l2ToL1Message.batchNumber}`,
105+
)
106+
logger.info(`and index in batch ${l2ToL1Message.indexInBatch}.`)
107+
logger.info(
108+
`After the dispute period is finalized (in ~1 week), you can finalize this by calling`,
109+
)
110+
logger.info(`finish-send-to-l1 with the following txhash:`)
111+
logger.info(l2Receipt.transactionHash)
112+
}
113+
114+
export const finishSendToL1 = async (
115+
cli: CLIEnvironment,
116+
cliArgs: CLIArgs,
117+
wait: boolean,
118+
): Promise<void> => {
119+
logger.info(`>>> Finishing transaction sending tokens to L1 <<<\n`)
120+
const l2Provider = getProvider(cliArgs.l2ProviderUrl)
121+
const l2ChainId = (await l2Provider.getNetwork()).chainId
122+
if (chainIdIsL2(cli.chainId) || !chainIdIsL2(l2ChainId)) {
123+
throw new Error(
124+
'Please use an L1 provider in --provider-url, and an L2 provider in --l2-provider-url',
125+
)
126+
}
127+
128+
const l2AddressBook = getAddressBook(cliArgs.addressBook, l2ChainId.toString())
129+
130+
const gateway = loadAddressBookContract(
131+
'L2GraphTokenGateway',
132+
l2AddressBook,
133+
l2Provider,
134+
) as L2GraphTokenGateway
135+
let txHash: string
136+
if (cliArgs.txHash) {
137+
txHash = cliArgs.txHash
138+
} else {
139+
logger.info(
140+
`Looking for withdrawals initiated by ${cli.wallet.address} in roughly the last 14 days`,
141+
)
142+
const fromBlock = await searchForArbBlockByTimestamp(
143+
l2Provider,
144+
Math.round(Date.now() / 1000) - FOURTEEN_DAYS_IN_SECONDS,
145+
)
146+
const filt = gateway.filters.WithdrawalInitiated(null, cli.wallet.address)
147+
const allEvents = await gateway.queryFilter(filt, BigNumber.from(fromBlock).toHexString())
148+
if (allEvents.length == 0) {
149+
throw new Error('No withdrawals found')
150+
}
151+
txHash = allEvents[allEvents.length - 1].transactionHash
152+
}
153+
logger.info(`Getting receipt from transaction ${txHash}`)
154+
const receipt = await l2Provider.getTransactionReceipt(txHash)
155+
156+
const l2Receipt = new L2TransactionReceipt(receipt)
157+
logger.info(`Getting L2 to L1 message...`)
158+
const l2ToL1Message = (
159+
await l2Receipt.getL2ToL1Messages(cli.wallet, await getL2Network(l2Provider))
160+
)[0]
161+
162+
if (wait) {
163+
const retryDelayMs = cliArgs.retryDelaySeconds ? cliArgs.retryDelaySeconds * 1000 : 60000
164+
logger.info('Waiting for outbox entry to be created, this can take a full week...')
165+
await waitUntilOutboxEntryCreatedWithCb(l2ToL1Message, retryDelayMs, () => {
166+
logger.info('Still waiting...')
167+
})
168+
} else {
169+
logger.info('Checking if L2 to L1 message is confirmed...')
170+
const status = await l2ToL1Message.status(null)
171+
if (status != L2ToL1MessageStatus.CONFIRMED) {
172+
throw new Error(
173+
`Transaction is not confirmed, status is ${status} when it should be ${L2ToL1MessageStatus.CONFIRMED}. Has the dispute period passed?`,
174+
)
175+
}
176+
}
177+
logger.info('Getting proof to execute message')
178+
const proofInfo = await l2ToL1Message.tryGetProof(l2Provider)
179+
180+
if (await l2ToL1Message.hasExecuted(proofInfo)) {
181+
throw new Error('Message already executed!')
182+
}
183+
184+
logger.info('Executing outbox transaction')
185+
const tx = await l2ToL1Message.execute(proofInfo)
186+
const outboxExecuteReceipt = await tx.wait()
187+
logger.info('Transaction succeeded! tx hash:')
188+
logger.info(outboxExecuteReceipt.transactionHash)
189+
}
190+
191+
export const startSendToL1Command = {
192+
command: 'start-send-to-l1 <amount> [recipient]',
193+
describe: 'Start an L2-to-L1 Graph Token transaction',
194+
handler: async (argv: CLIArgs): Promise<void> => {
195+
return startSendToL1(await loadEnv(argv), argv)
196+
},
197+
}
198+
199+
export const finishSendToL1Command = {
200+
command: 'finish-send-to-l1 [txHash]',
201+
describe:
202+
'Finish an L2-to-L1 Graph Token transaction. L2 dispute period must have completed. ' +
203+
'If txHash is not specified, the last withdrawal from the main account in the past 14 days will be redeemed.',
204+
handler: async (argv: CLIArgs): Promise<void> => {
205+
return finishSendToL1(await loadEnv(argv), argv, false)
206+
},
207+
}
208+
209+
export const waitFinishSendToL1Command = {
210+
command: 'wait-finish-send-to-l1 [txHash] [retryDelaySeconds]',
211+
describe:
212+
"Wait for an L2-to-L1 Graph Token transaction's dispute period to complete (which takes about a week), and then finalize it. " +
213+
'If txHash is not specified, the last withdrawal from the main account in the past 14 days will be redeemed.',
214+
handler: async (argv: CLIArgs): Promise<void> => {
215+
return finishSendToL1(await loadEnv(argv), argv, true)
216+
},
217+
}

0 commit comments

Comments
 (0)