Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
e4fbde24
Unverified
Commit
e4fbde24
authored
Apr 06, 2022
by
Mark Tyneway
Committed by
GitHub
Apr 06, 2022
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2430 from ethereum-optimism/develop
Develop -> Master PR
parents
71240b1d
1d2567cd
Changes
25
Hide whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
358 additions
and
270 deletions
+358
-270
lazy-mangos-relax.md
.changeset/lazy-mangos-relax.md
+6
-0
mean-spies-trade.md
.changeset/mean-spies-trade.md
+5
-0
smart-foxes-shop.md
.changeset/smart-foxes-shop.md
+5
-0
labeler.yml
.github/labeler.yml
+3
-1
changesets.yml
.github/workflows/changesets.yml
+14
-0
close-stale.yml
.github/workflows/close-stale.yml
+1
-1
release.yml
.github/workflows/release.yml
+2
-2
ovm-gas-oracle.sh
packages/contracts/scripts/ovm-gas-oracle.sh
+44
-0
index.ts
packages/contracts/tasks/index.ts
+0
-1
l2-gasprice.ts
packages/contracts/tasks/l2-gasprice.ts
+0
-103
AddressDictator.spec.ts
...acts/test/contracts/L1/deployment/AddressDictator.spec.ts
+33
-40
ChugSplashDictator.spec.ts
...s/test/contracts/L1/deployment/ChugSplashDictator.spec.ts
+65
-0
L1ChugSplashProxy.spec.ts
...racts/test/contracts/chugsplash/L1ChugSplashProxy.spec.ts
+9
-11
deploy.ts
packages/contracts/test/helpers/utils/deploy.ts
+12
-0
index.ts
packages/contracts/test/helpers/utils/index.ts
+1
-0
dai-bridge.ts
packages/sdk/src/adapters/dai-bridge.ts
+12
-12
eth-bridge.ts
packages/sdk/src/adapters/eth-bridge.ts
+1
-1
cross-chain-messenger.ts
packages/sdk/src/cross-chain-messenger.ts
+4
-7
l2-provider.ts
packages/sdk/src/interfaces/l2-provider.ts
+6
-0
types.ts
packages/sdk/src/interfaces/types.ts
+10
-0
l2-provider.ts
packages/sdk/src/l2-provider.ts
+31
-10
chain-constants.ts
packages/sdk/src/utils/chain-constants.ts
+10
-16
coercion.ts
packages/sdk/src/utils/coercion.ts
+3
-4
contracts.ts
packages/sdk/src/utils/contracts.ts
+76
-59
misc-utils.ts
packages/sdk/src/utils/misc-utils.ts
+5
-2
No files found.
.changeset/lazy-mangos-relax.md
0 → 100644
View file @
e4fbde24
---
'
@eth-optimism/go-builder'
:
patch
'
@eth-optimism/js-builder'
:
patch
---
Trigger releases
.changeset/mean-spies-trade.md
0 → 100644
View file @
e4fbde24
---
'
@eth-optimism/sdk'
:
minor
---
New isL2Provider helper function. Internal cleanups.
.changeset/smart-foxes-shop.md
0 → 100644
View file @
e4fbde24
---
'
@eth-optimism/contracts'
:
patch
---
Remove l2 gas price hardhat task
.github/labeler.yml
View file @
e4fbde24
...
...
@@ -37,4 +37,6 @@ M-ops:
-
any
:
[
'
ops/**/*'
]
C-Protocol-Critical
:
-
any
:
[
'
packages/data-transport-layer/**/*.ts'
,
'
packages/contracts/**/*.sol'
,
'
l2geth/**/*.go'
]
-
'
packages/data-transport-layer/**/*.ts'
-
'
packages/contracts/**/*.sol'
-
'
l2geth/**/*.go'
.github/workflows/changesets.yml
0 → 100644
View file @
e4fbde24
---
name
:
'
Changeset
integrity
checker'
on
:
-
pull_request_target
jobs
:
changesets-integrity-checker
:
runs-on
:
ubuntu-latest
steps
:
-
name
:
Checkout
uses
:
actions/checkout@v2
-
name
:
'
changeset
status'
run
:
npx changeset status
.github/workflows/close-stale.yml
View file @
e4fbde24
...
...
@@ -12,6 +12,6 @@ jobs:
stale-pr-message
:
'
This
PR
is
stale
because
it
has
been
open
14
days
with
no
activity.
Remove
stale
label
or
comment
or
this
will
be
closed
in
5
days.'
exempt-pr-labels
:
exempt-stale
days-before-issue-stale
:
999
da
t
s-before-pr-stale
:
14
da
y
s-before-pr-stale
:
14
days-before-close
:
5
repo-token
:
${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
.github/workflows/release.yml
View file @
e4fbde24
...
...
@@ -152,7 +152,7 @@ jobs:
go-builder
:
name
:
Publish go-builder ${{ needs.release.outputs.go-builder }}
needs
:
release
if
:
needs.release.go-builder != ''
if
:
needs.release.
outputs.
go-builder != ''
runs-on
:
ubuntu-latest
steps
:
-
name
:
Checkout
...
...
@@ -178,7 +178,7 @@ jobs:
js-builder
:
name
:
Publish js-builder ${{ needs.release.outputs.js-builder }}
needs
:
release
if
:
needs.release.js-builder != ''
if
:
needs.release.
outputs.
js-builder != ''
runs-on
:
ubuntu-latest
steps
:
-
name
:
Checkout
...
...
packages/contracts/scripts/ovm-gas-oracle.sh
0 → 100755
View file @
e4fbde24
#!/bin/sh
set
-e
RPC_URL
=
${
RPC_URL
:-
http
://localhost:8545
}
OVM_GAS_ORACLE
=
0x420000000000000000000000000000000000000F
function
send_tx
()
{
cast send
--rpc-url
$RPC_URL
\
--private-key
$PRIVATE_KEY
\
--legacy
\
--gas-price
0
\
$OVM_GAS_ORACLE
\
$1
\
$2
}
function
call
()
{
cast call
--rpc-url
$RPC_URL
\
$OVM_GAS_ORACLE
\
$1
}
echo
"Scalar:
$(
call
'scalar()(uint256)'
)
"
echo
"L2 gas price:
$(
call
'gasPrice()(uint256)'
)
"
echo
"Overhead:
$(
call
'overhead()(uint256)'
)
"
if
[[
!
-z
$PRIVATE_KEY
]]
;
then
if
[[
!
-z
$SCALAR
]]
;
then
echo
"Setting scalar to
$SCALAR
"
send_tx
'setScalar(uint256)'
$SCALAR
fi
if
[[
!
-z
$OVERHEAD
]]
;
then
echo
"Setting overhead to
$OVERHEAD
"
send_tx
'setOverhead(uint256)'
$OVERHEAD
fi
if
[[
!
-z
$L2_GAS_PRICE
]]
;
then
echo
"Setting L2 gas price to
$L2_GAS_PRICE
"
send_tx
'setGasPrice(uint256)'
$L2_GAS_PRICE
fi
fi
packages/contracts/tasks/index.ts
View file @
e4fbde24
export
*
from
'
./l2-gasprice
'
export
*
from
'
./set-owner
'
export
*
from
'
./take-dump
'
export
*
from
'
./validate-address-dictator
'
...
...
packages/contracts/tasks/l2-gasprice.ts
deleted
100644 → 0
View file @
71240b1d
/* Imports: External */
import
{
ethers
}
from
'
ethers
'
import
{
task
}
from
'
hardhat/config
'
import
*
as
types
from
'
hardhat/internal/core/params/argumentTypes
'
import
{
predeploys
}
from
'
../src/predeploys
'
import
{
getContractDefinition
}
from
'
../src/contract-defs
'
task
(
'
set-l2-gasprice
'
)
.
addOptionalParam
(
'
l2GasPrice
'
,
'
Gas Price to set on L2
'
,
undefined
,
types
.
int
)
.
addOptionalParam
(
'
transactionGasPrice
'
,
'
tx.gasPrice
'
,
undefined
,
types
.
int
)
.
addOptionalParam
(
'
overhead
'
,
'
amortized additional gas used by each batch that users must pay for
'
,
undefined
,
types
.
int
)
.
addOptionalParam
(
'
scalar
'
,
'
amount to scale up the gas to charge
'
,
undefined
,
types
.
int
)
.
addOptionalParam
(
'
contractsRpcUrl
'
,
'
Sequencer HTTP Endpoint
'
,
process
.
env
.
CONTRACTS_RPC_URL
,
types
.
string
)
.
addOptionalParam
(
'
contractsDeployerKey
'
,
'
Private Key
'
,
process
.
env
.
CONTRACTS_DEPLOYER_KEY
,
types
.
string
)
.
setAction
(
async
(
args
)
=>
{
const
provider
=
new
ethers
.
providers
.
JsonRpcProvider
(
args
.
contractsRpcUrl
)
const
signer
=
new
ethers
.
Wallet
(
args
.
contractsDeployerKey
).
connect
(
provider
)
const
GasPriceOracleArtifact
=
getContractDefinition
(
'
OVM_GasPriceOracle
'
)
const
GasPriceOracle
=
new
ethers
.
Contract
(
predeploys
.
OVM_GasPriceOracle
,
GasPriceOracleArtifact
.
abi
,
signer
)
const
addr
=
await
signer
.
getAddress
()
console
.
log
(
`Using signer
${
addr
}
`
)
const
owner
=
await
GasPriceOracle
.
callStatic
.
owner
()
if
(
owner
!==
addr
)
{
throw
new
Error
(
`Incorrect key. Owner
${
owner
}
, Signer
${
addr
}
`
)
}
// List the current values
const
gasPrice
=
await
GasPriceOracle
.
callStatic
.
gasPrice
()
const
scalar
=
await
GasPriceOracle
.
callStatic
.
scalar
()
const
overhead
=
await
GasPriceOracle
.
callStatic
.
overhead
()
console
.
log
(
'
Current values:
'
)
console
.
log
(
`Gas Price:
${
gasPrice
.
toString
()}
`
)
console
.
log
(
`Scalar:
${
scalar
.
toString
()}
`
)
console
.
log
(
`Overhead:
${
overhead
.
toString
()}
`
)
if
(
args
.
l2GasPrice
!==
undefined
)
{
console
.
log
(
`Setting gas price to
${
args
.
l2GasPrice
}
`
)
const
tx
=
await
GasPriceOracle
.
connect
(
signer
).
setGasPrice
(
args
.
l2GasPrice
,
{
gasPrice
:
args
.
transactionGasPrice
}
)
const
receipt
=
await
tx
.
wait
()
console
.
log
(
`Success -
${
receipt
.
transactionHash
}
`
)
}
if
(
args
.
scalar
!==
undefined
)
{
console
.
log
(
`Setting scalar to
${
args
.
scalar
}
`
)
const
tx
=
await
GasPriceOracle
.
connect
(
signer
).
setScalar
(
args
.
scalar
,
{
gasPrice
:
args
.
transactionGasPrice
,
})
const
receipt
=
await
tx
.
wait
()
console
.
log
(
`Success -
${
receipt
.
transactionHash
}
`
)
}
if
(
args
.
overhead
!==
undefined
)
{
console
.
log
(
`Setting overhead to
${
args
.
overhead
}
`
)
const
tx
=
await
GasPriceOracle
.
connect
(
signer
).
setOverhead
(
args
.
overhead
,
{
gasPrice
:
args
.
transactionGasPrice
}
)
const
receipt
=
await
tx
.
wait
()
console
.
log
(
`Success -
${
receipt
.
transactionHash
}
`
)
}
})
packages/contracts/test/contracts/L1/deployment/AddressDictator.spec.ts
View file @
e4fbde24
/* External Imports */
import
{
ethers
}
from
'
hardhat
'
import
{
Contract
,
Signer
,
ContractFactory
}
from
'
ethers
'
import
{
Contract
,
Signer
}
from
'
ethers
'
/* Internal Imports */
import
{
expect
}
from
'
../../../setup
'
import
{
NON_ZERO_ADDRESS
}
from
'
../../../helpers
'
import
{
deploy
,
NON_ZERO_ADDRESS
}
from
'
../../../helpers
'
describe
(
'
AddressDictator
'
,
()
=>
{
let
signer
:
Signer
let
otherSigner
:
Signer
let
signerAddress
:
string
let
Factory__AddressDictator
:
ContractFactory
let
Factory__Lib_AddressManager
:
ContractFactory
let
signer1
:
Signer
let
signer2
:
Signer
before
(
async
()
=>
{
;[
signer
,
otherSigner
]
=
await
ethers
.
getSigners
()
Factory__AddressDictator
=
await
ethers
.
getContractFactory
(
'
AddressDictator
'
)
Factory__Lib_AddressManager
=
await
ethers
.
getContractFactory
(
'
Lib_AddressManager
'
)
signerAddress
=
await
signer
.
getAddress
()
;[
signer1
,
signer2
]
=
await
ethers
.
getSigners
()
})
let
AddressDictator
:
Contract
let
Lib_AddressManager
:
Contract
beforeEach
(
async
()
=>
{
Lib_AddressManager
=
await
Factory__Lib_AddressManager
.
connect
(
signer
).
deploy
(
)
Lib_AddressManager
=
await
deploy
(
'
Lib_AddressManager
'
,
{
signer
:
signer1
,
}
)
AddressDictator
=
await
Factory__AddressDictator
.
connect
(
signer
).
deploy
(
Lib_AddressManager
.
address
,
signerAddress
,
[
'
addr1
'
],
[
NON_ZERO_ADDRESS
]
)
AddressDictator
=
await
deploy
(
'
AddressDictator
'
,
{
signer
:
signer1
,
args
:
[
Lib_AddressManager
.
address
,
await
signer1
.
getAddress
(),
[
'
addr1
'
],
[
NON_ZERO_ADDRESS
],
],
})
Lib_AddressManager
.
transferOwnership
(
AddressDictator
.
address
)
Lib_AddressManager
.
connect
(
signer1
).
transferOwnership
(
AddressDictator
.
address
)
})
describe
(
'
initialize
'
,
()
=>
{
it
(
'
should revert when providing wrong arguments
'
,
async
()
=>
{
await
expect
(
Factory__AddressDictator
.
connect
(
signer
).
deploy
(
Lib_AddressManager
.
address
,
signerAddress
,
[
'
addr1
'
,
'
addr2
'
],
[
NON_ZERO_ADDRESS
]
)
deploy
(
'
AddressDictator
'
,
{
signer
:
signer1
,
args
:
[
Lib_AddressManager
.
address
,
await
signer1
.
getAddress
(),
[
'
addr1
'
,
'
addr2
'
],
[
NON_ZERO_ADDRESS
],
],
})
).
to
.
be
.
revertedWith
(
'
AddressDictator: Must provide an equal number of names and addresses.
'
)
...
...
@@ -61,7 +54,7 @@ describe('AddressDictator', () => {
describe
(
'
setAddresses
'
,
async
()
=>
{
it
(
'
should change the addresses associated with a name
'
,
async
()
=>
{
await
AddressDictator
.
setAddresses
()
expect
(
await
Lib_AddressManager
.
getAddress
(
'
addr1
'
)).
to
.
be
.
equal
(
expect
(
await
Lib_AddressManager
.
getAddress
(
'
addr1
'
)).
to
.
equal
(
NON_ZERO_ADDRESS
)
})
...
...
@@ -69,7 +62,7 @@ describe('AddressDictator', () => {
describe
(
'
getNamedAddresses
'
,
()
=>
{
it
(
'
should return all the addresses and their names
'
,
async
()
=>
{
expect
(
await
AddressDictator
.
getNamedAddresses
()).
to
.
be
.
deep
.
equal
([
expect
(
await
AddressDictator
.
getNamedAddresses
()).
to
.
deep
.
equal
([
[
'
addr1
'
,
NON_ZERO_ADDRESS
],
])
})
...
...
@@ -77,13 +70,13 @@ describe('AddressDictator', () => {
describe
(
'
returnOwnership
'
,
()
=>
{
it
(
'
should transfer contract ownership to finalOwner
'
,
async
()
=>
{
await
expect
(
AddressDictator
.
connect
(
signer
).
returnOwnership
()).
to
.
not
.
be
await
expect
(
AddressDictator
.
connect
(
signer
1
).
returnOwnership
()).
to
.
not
.
be
.
reverted
})
it
(
'
should revert when called by non-owner
'
,
async
()
=>
{
await
expect
(
AddressDictator
.
connect
(
otherSigner
).
returnOwnership
()
AddressDictator
.
connect
(
signer2
).
returnOwnership
()
).
to
.
be
.
revertedWith
(
'
AddressDictator: only callable by finalOwner
'
)
})
})
...
...
packages/contracts/test/contracts/L1/deployment/ChugSplashDictator.spec.
.
ts
→
packages/contracts/test/contracts/L1/deployment/ChugSplashDictator.spec.ts
View file @
e4fbde24
/* External Imports */
import
{
ethers
}
from
'
hardhat
'
import
{
Contract
,
Signer
,
ContractFactory
}
from
'
ethers
'
import
{
Contract
,
Signer
}
from
'
ethers
'
/* Internal Imports */
import
{
expect
}
from
'
../../../setup
'
import
{
deploy
}
from
'
../../../helpers
'
describe
(
'
ChugSplashDictator
'
,
()
=>
{
let
signer
:
Signer
let
otherSigner
:
Signer
let
signerAddress
:
string
let
Factory__L1ChugSplashProxy
:
ContractFactory
let
Factory__ChugSplashDictator
:
ContractFactory
let
signer1
:
Signer
let
signer2
:
Signer
before
(
async
()
=>
{
;[
signer
,
otherSigner
]
=
await
ethers
.
getSigners
()
Factory__L1ChugSplashProxy
=
await
ethers
.
getContractFactory
(
'
L1ChugSplashProxy
'
)
Factory__ChugSplashDictator
=
await
ethers
.
getContractFactory
(
'
ChugSplashDictator
'
)
signerAddress
=
await
signer
.
getAddress
()
;[
signer1
,
signer2
]
=
await
ethers
.
getSigners
()
})
let
L1ChugSplashProxy
:
Contract
let
ChugSplashDictator
:
Contract
beforeEach
(
async
()
=>
{
L1ChugSplashProxy
=
await
Factory__L1ChugSplashProxy
.
connect
(
signer
).
deploy
(
signerAddress
)
L1ChugSplashProxy
=
await
deploy
(
'
L1ChugSplashProxy
'
,
{
signer
:
signer1
,
args
:
[
await
signer1
.
getAddress
()],
})
ChugSplashDictator
=
await
Factory__ChugSplashDictator
.
connect
(
signer
).
deploy
(
L1ChugSplashProxy
.
address
,
signerAddress
,
ethers
.
utils
.
keccak256
(
'
0x1111
'
),
ethers
.
utils
.
keccak256
(
'
0x1234
'
),
ethers
.
utils
.
keccak256
(
'
0x5678
'
),
ethers
.
utils
.
keccak256
(
'
0x1234
'
),
ethers
.
utils
.
keccak256
(
'
0x1234
'
)
)
ChugSplashDictator
=
await
deploy
(
'
ChugSplashDictator
'
,
{
signer
:
signer1
,
args
:
[
L1ChugSplashProxy
.
address
,
await
signer1
.
getAddress
(),
ethers
.
utils
.
keccak256
(
'
0x1111
'
),
ethers
.
utils
.
keccak256
(
'
0x1234
'
),
ethers
.
utils
.
keccak256
(
'
0x5678
'
),
ethers
.
utils
.
keccak256
(
'
0x1234
'
),
ethers
.
utils
.
keccak256
(
'
0x1234
'
),
],
})
await
L1ChugSplashProxy
.
connect
(
signer
).
setOwner
(
ChugSplashDictator
.
address
)
await
L1ChugSplashProxy
.
connect
(
signer1
).
setOwner
(
ChugSplashDictator
.
address
)
})
describe
(
'
doActions
'
,
()
=>
{
...
...
@@ -56,20 +45,20 @@ describe('ChugSplashDictator', () => {
})
it
(
'
should set the proxy code, storage & owner
'
,
async
()
=>
{
await
expect
(
ChugSplashDictator
.
connect
(
signer
).
doActions
(
'
0x1111
'
)).
to
await
expect
(
ChugSplashDictator
.
connect
(
signer
1
).
doActions
(
'
0x1111
'
)).
to
.
not
.
be
.
reverted
})
})
describe
(
'
returnOwnership
'
,
()
=>
{
it
(
'
should transfer contractc ownership to finalOwner
'
,
async
()
=>
{
await
expect
(
ChugSplashDictator
.
connect
(
signer
).
returnOwnership
()).
to
.
not
await
expect
(
ChugSplashDictator
.
connect
(
signer
1
).
returnOwnership
()).
to
.
not
.
be
.
reverted
})
it
(
'
should revert when called by non-owner
'
,
async
()
=>
{
await
expect
(
ChugSplashDictator
.
connect
(
otherSigner
).
returnOwnership
()
ChugSplashDictator
.
connect
(
signer2
).
returnOwnership
()
).
to
.
be
.
revertedWith
(
'
ChugSplashDictator: only callable by finalOwner
'
)
})
})
...
...
packages/contracts/test/contracts/chugsplash/L1ChugSplashProxy.spec.ts
View file @
e4fbde24
/* Imports: External */
import
hre
from
'
hardhat
'
import
{
Contract
,
Signer
}
from
'
ethers
'
import
{
smock
}
from
'
@defi-wonderland/smock
'
/* Imports: Internal */
import
{
expect
}
from
'
../../setup
'
import
{
getContractInterface
}
from
'
../../../src
'
import
{
deploy
}
from
'
../../helpers
'
describe
(
'
L1ChugSplashProxy
'
,
()
=>
{
let
signer1
:
Signer
...
...
@@ -16,12 +15,9 @@ describe('L1ChugSplashProxy', () => {
let
L1ChugSplashProxy
:
Contract
beforeEach
(
async
()
=>
{
const
Factory__L1ChugSplashProxy
=
await
hre
.
ethers
.
getContractFactory
(
'
L1ChugSplashProxy
'
)
L1ChugSplashProxy
=
await
Factory__L1ChugSplashProxy
.
deploy
(
await
signer1
.
getAddress
()
)
L1ChugSplashProxy
=
await
deploy
(
'
L1ChugSplashProxy
'
,
{
args
:
[
await
signer1
.
getAddress
()],
})
})
describe
(
'
getOwner
'
,
()
=>
{
...
...
@@ -173,14 +169,16 @@ describe('L1ChugSplashProxy', () => {
const
owner
=
await
smock
.
fake
<
Contract
>
(
getContractInterface
(
'
iL1ChugSplashDeployer
'
)
)
const
factory
=
await
hre
.
ethers
.
getContractFactory
(
'
L1ChugSplashProxy
'
)
const
proxy
=
await
factory
.
deploy
(
owner
.
address
)
L1ChugSplashProxy
=
await
deploy
(
'
L1ChugSplashProxy
'
,
{
args
:
[
owner
.
address
],
})
owner
.
isUpgrading
.
returns
(
true
)
await
expect
(
owner
.
wallet
.
sendTransaction
({
to
:
p
roxy
.
address
,
to
:
L1ChugSplashP
roxy
.
address
,
data
:
'
0x
'
,
})
).
to
.
be
.
revertedWith
(
...
...
packages/contracts/test/helpers/utils/deploy.ts
0 → 100644
View file @
e4fbde24
import
hre
from
'
hardhat
'
export
const
deploy
=
async
(
name
:
string
,
opts
?:
{
args
?:
any
[]
signer
?:
any
}
)
=>
{
const
factory
=
await
hre
.
ethers
.
getContractFactory
(
name
,
opts
?.
signer
)
return
factory
.
deploy
(...(
opts
?.
args
||
[]))
}
packages/contracts/test/helpers/utils/index.ts
View file @
e4fbde24
export
*
from
'
./eth-time
'
export
*
from
'
./deploy
'
packages/sdk/src/adapters/dai-bridge.ts
View file @
e4fbde24
...
...
@@ -20,29 +20,29 @@ export class DAIBridgeAdapter extends StandardBridgeAdapter {
[
{
inputs
:
[],
name
:
'
l1Token
'
,
name
:
'
l1Token
'
as
const
,
outputs
:
[
{
internalType
:
'
address
'
,
name
:
''
,
type
:
'
address
'
,
internalType
:
'
address
'
as
const
,
name
:
''
as
const
,
type
:
'
address
'
as
const
,
},
],
stateMutability
:
'
view
'
,
type
:
'
function
'
,
stateMutability
:
'
view
'
as
const
,
type
:
'
function
'
as
const
,
},
{
inputs
:
[],
name
:
'
l2Token
'
,
name
:
'
l2Token
'
as
const
,
outputs
:
[
{
internalType
:
'
address
'
,
name
:
''
,
type
:
'
address
'
,
internalType
:
'
address
'
as
const
,
name
:
''
as
const
,
type
:
'
address
'
as
const
,
},
],
stateMutability
:
'
view
'
,
type
:
'
function
'
,
stateMutability
:
'
view
'
as
const
,
type
:
'
function
'
as
const
,
},
],
this
.
messenger
.
l1Provider
...
...
packages/sdk/src/adapters/eth-bridge.ts
View file @
e4fbde24
...
...
@@ -119,7 +119,7 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter {
opts
?:
{
overrides
?:
Overrides
}
):
Promise
<
TransactionRequest
>
=>
{
):
Promise
<
never
>
=>
{
throw
new
Error
(
`approvals not necessary for ETH bridge`
)
},
...
...
packages/sdk/src/cross-chain-messenger.ts
View file @
e4fbde24
...
...
@@ -109,7 +109,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
if
(
Provider
.
isProvider
(
this
.
l1SignerOrProvider
))
{
return
this
.
l1SignerOrProvider
}
else
{
return
this
.
l1SignerOrProvider
.
provider
as
any
return
this
.
l1SignerOrProvider
.
provider
}
}
...
...
@@ -117,7 +117,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
if
(
Provider
.
isProvider
(
this
.
l2SignerOrProvider
))
{
return
this
.
l2SignerOrProvider
}
else
{
return
this
.
l2SignerOrProvider
.
provider
as
any
return
this
.
l2SignerOrProvider
.
provider
}
}
...
...
@@ -144,10 +144,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
}
=
{}
):
Promise
<
CrossChainMessage
[]
>
{
// Wait for the transaction receipt if the input is waitable.
// TODO: Maybe worth doing this with more explicit typing but whatever for now.
if
(
typeof
(
transaction
as
any
).
wait
===
'
function
'
)
{
await
(
transaction
as
any
).
wait
()
}
await
(
transaction
as
TransactionResponse
).
wait
?.()
// Convert the input to a transaction hash.
const
txHash
=
toTransactionHash
(
transaction
)
...
...
@@ -821,7 +818,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
)
const
stateTrieProof
=
await
makeStateTrieProof
(
this
.
l2Provider
as
any
,
this
.
l2Provider
as
ethers
.
providers
.
JsonRpcProvider
,
resolved
.
blockNumber
,
this
.
contracts
.
l2
.
OVM_L2ToL1MessagePasser
.
address
,
messageSlot
...
...
packages/sdk/src/interfaces/l2-provider.ts
View file @
e4fbde24
...
...
@@ -83,4 +83,10 @@ export type L2Provider<TProvider extends Provider> = TProvider & {
* @returns Estimated total gas cost.
*/
estimateTotalGasCost
(
tx
:
TransactionRequest
):
Promise
<
BigNumber
>
/**
* Internal property to determine if a provider is a L2Provider
* You are likely looking for the isL2Provider function
*/
_isL2Provider
:
true
}
packages/sdk/src/interfaces/types.ts
View file @
e4fbde24
...
...
@@ -9,6 +9,16 @@ import { Contract, BigNumber } from 'ethers'
import
{
ICrossChainMessenger
}
from
'
./cross-chain-messenger
'
import
{
IBridgeAdapter
}
from
'
./bridge-adapter
'
/**
* Commonly used Chain IDs
*/
export
enum
Chain
{
MAINNET
=
1
,
GOERLI
=
5
,
KOVAN
=
42
,
HARDHAT_LOCAL
=
31337
,
}
/**
* L1 contract references.
*/
...
...
packages/sdk/src/l2-provider.ts
View file @
e4fbde24
import
assert
from
'
assert
'
import
{
Provider
,
TransactionRequest
}
from
'
@ethersproject/abstract-provider
'
import
{
serialize
}
from
'
@ethersproject/transactions
'
import
{
Contract
,
BigNumber
}
from
'
ethers
'
...
...
@@ -7,6 +9,8 @@ import cloneDeep from 'lodash/cloneDeep'
import
{
L2Provider
,
ProviderLike
,
NumberLike
}
from
'
./interfaces
'
import
{
toProvider
,
toNumber
,
toBigNumber
}
from
'
./utils
'
type
ProviderTypeIsWrong
=
any
/**
* Returns a Contract object for the GasPriceOracle.
*
...
...
@@ -115,6 +119,24 @@ export const estimateTotalGasCost = async (
return
l1GasCost
.
add
(
l2GasCost
)
}
/**
* Determines if a given Provider is an L2Provider. Will coerce type
* if true
*
* @param provider The provider to check
* @returns Boolean
* @example
* if (isL2Provider(provider)) {
* // typescript now knows it is of type L2Provider
* const gasPrice = await provider.estimateL2GasPrice(tx)
* }
*/
export
const
isL2Provider
=
<
TProvider
extends
Provider
>
(
provider
:
TProvider
):
provider
is
L2Provider
<
TProvider
>
=>
{
return
Boolean
((
provider
as
L2Provider
<
TProvider
>
).
_isL2Provider
)
}
/**
* Returns an provider wrapped as an Optimism L2 provider. Adds a few extra helper functions to
* simplify the process of estimating the gas usage for a transaction on Optimism. Returns a COPY
...
...
@@ -126,22 +148,21 @@ export const estimateTotalGasCost = async (
export
const
asL2Provider
=
<
TProvider
extends
Provider
>
(
provider
:
TProvider
):
L2Provider
<
TProvider
>
=>
{
// Make a copy of the provider since we'll be modifying some internals and don't want to mess
// with the original object.
const
l2Provider
=
cloneDeep
(
provider
)
as
any
// Skip if we've already wrapped this provider.
if
(
l2Provider
.
_isL2Provider
)
{
return
l2P
rovider
if
(
isL2Provider
(
provider
)
)
{
return
p
rovider
}
// Make a copy of the provider since we'll be modifying some internals and don't want to mess
// with the original object.
const
l2Provider
=
cloneDeep
(
provider
)
as
L2Provider
<
TProvider
>
// Not exactly sure when the provider wouldn't have a formatter function, but throw an error if
// it doesn't have one. The Provider type doesn't define it but every provider I've dealt with
// seems to have it.
const
formatter
=
l2Provider
.
formatter
if
(
formatter
===
undefined
)
{
throw
new
Error
(
`provider.formatter must be defined`
)
}
// TODO this may be fixed if library has gotten updated since
const
formatter
=
(
l2Provider
as
ProviderTypeIsWrong
).
formatter
assert
(
formatter
,
`provider.formatter must be defined`
)
// Modify the block formatter to return the state root. Not strictly related to Optimism, just a
// generally useful thing that really should've been on the Ethers block object to begin with.
...
...
packages/sdk/src/utils/chain-constants.ts
View file @
e4fbde24
import
{
Chain
}
from
'
../interfaces
'
export
const
DEPOSIT_CONFIRMATION_BLOCKS
=
{
// Mainnet
1
:
50
,
// Goerli
5
:
12
,
// Kovan
42
:
12
,
// Hardhat Local
[
Chain
.
MAINNET
]:
50
as
const
,
[
Chain
.
GOERLI
]:
12
as
const
,
[
Chain
.
KOVAN
]:
12
as
const
,
// 2 just for testing purposes
31337
:
2
,
[
Chain
.
HARDHAT_LOCAL
]:
2
as
const
,
}
export
const
CHAIN_BLOCK_TIMES
=
{
// Mainnet
1
:
13
,
// Goerli
5
:
15
,
// Kovan
42
:
4
,
// Hardhat Local
31337
:
1
,
[
Chain
.
MAINNET
]:
13
as
const
,
[
Chain
.
GOERLI
]:
15
as
const
,
[
Chain
.
KOVAN
]:
4
as
const
,
[
Chain
.
HARDHAT_LOCAL
]:
1
as
const
,
}
packages/sdk/src/utils/coercion.ts
View file @
e4fbde24
...
...
@@ -29,9 +29,9 @@ export const toSignerOrProvider = (
if
(
typeof
signerOrProvider
===
'
string
'
)
{
return
new
ethers
.
providers
.
JsonRpcProvider
(
signerOrProvider
)
}
else
if
(
Provider
.
isProvider
(
signerOrProvider
))
{
return
signerOrProvider
as
Provider
return
signerOrProvider
}
else
if
(
Signer
.
isSigner
(
signerOrProvider
))
{
return
signerOrProvider
as
Signer
return
signerOrProvider
}
else
{
throw
new
Error
(
'
Invalid provider
'
)
}
...
...
@@ -48,7 +48,7 @@ export const toProvider = (provider: ProviderLike): Provider => {
if
(
typeof
provider
===
'
string
'
)
{
return
new
ethers
.
providers
.
JsonRpcProvider
(
provider
)
}
else
if
(
Provider
.
isProvider
(
provider
))
{
return
provider
as
Provider
return
provider
}
else
{
throw
new
Error
(
'
Invalid provider
'
)
}
...
...
@@ -66,7 +66,6 @@ export const toTransactionHash = (transaction: TransactionLike): string => {
ethers
.
utils
.
isHexString
(
transaction
,
32
),
'
Invalid transaction hash
'
)
return
transaction
}
else
if
((
transaction
as
TransactionReceipt
).
transactionHash
)
{
return
(
transaction
as
TransactionReceipt
).
transactionHash
...
...
packages/sdk/src/utils/contracts.ts
View file @
e4fbde24
...
...
@@ -13,6 +13,7 @@ import {
BridgeAdapters
,
BridgeAdapterData
,
ICrossChainMessenger
,
Chain
,
}
from
'
../interfaces
'
import
{
StandardBridgeAdapter
,
...
...
@@ -40,9 +41,9 @@ export const DEFAULT_L2_CONTRACT_ADDRESSES: OEL2ContractsLike = {
* back to the original contract names so we can look them up.
*/
const
NAME_REMAPPING
=
{
AddressManager
:
'
Lib_AddressManager
'
,
OVM_L1BlockNumber
:
'
iOVM_L1BlockNumber
'
,
WETH
:
'
WETH9
'
,
AddressManager
:
'
Lib_AddressManager
'
as
const
,
OVM_L1BlockNumber
:
'
iOVM_L1BlockNumber
'
as
const
,
WETH
:
'
WETH9
'
as
const
,
}
/**
...
...
@@ -53,51 +54,59 @@ const NAME_REMAPPING = {
export
const
CONTRACT_ADDRESSES
:
{
[
l1ChainId
:
number
]:
OEContractsLike
}
=
{
// Mainnet
1
:
{
[
Chain
.
MAINNET
]:
{
l1
:
{
AddressManager
:
'
0xdE1FCfB0851916CA5101820A69b13a4E276bd81F
'
,
L1CrossDomainMessenger
:
'
0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1
'
,
L1StandardBridge
:
'
0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1
'
,
StateCommitmentChain
:
'
0xBe5dAb4A2e9cd0F27300dB4aB94BeE3A233AEB19
'
,
CanonicalTransactionChain
:
'
0x5E4e65926BA27467555EB562121fac00D24E9dD2
'
,
BondManager
:
'
0xcd626E1328b41fCF24737F137BcD4CE0c32bc8d1
'
,
AddressManager
:
'
0xdE1FCfB0851916CA5101820A69b13a4E276bd81F
'
as
const
,
L1CrossDomainMessenger
:
'
0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1
'
as
const
,
L1StandardBridge
:
'
0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1
'
as
const
,
StateCommitmentChain
:
'
0xBe5dAb4A2e9cd0F27300dB4aB94BeE3A233AEB19
'
as
const
,
CanonicalTransactionChain
:
'
0x5E4e65926BA27467555EB562121fac00D24E9dD2
'
as
const
,
BondManager
:
'
0xcd626E1328b41fCF24737F137BcD4CE0c32bc8d1
'
as
const
,
},
l2
:
DEFAULT_L2_CONTRACT_ADDRESSES
,
},
// Kovan
42
:
{
[
Chain
.
KOVAN
]:
{
l1
:
{
AddressManager
:
'
0x100Dd3b414Df5BbA2B542864fF94aF8024aFdf3a
'
,
L1CrossDomainMessenger
:
'
0x4361d0F75A0186C05f971c566dC6bEa5957483fD
'
,
L1StandardBridge
:
'
0x22F24361D548e5FaAfb36d1437839f080363982B
'
,
StateCommitmentChain
:
'
0xD7754711773489F31A0602635f3F167826ce53C5
'
,
CanonicalTransactionChain
:
'
0xf7B88A133202d41Fe5E2Ab22e6309a1A4D50AF74
'
,
BondManager
:
'
0xc5a603d273E28185c18Ba4d26A0024B2d2F42740
'
,
AddressManager
:
'
0x100Dd3b414Df5BbA2B542864fF94aF8024aFdf3a
'
as
const
,
L1CrossDomainMessenger
:
'
0x4361d0F75A0186C05f971c566dC6bEa5957483fD
'
as
const
,
L1StandardBridge
:
'
0x22F24361D548e5FaAfb36d1437839f080363982B
'
as
const
,
StateCommitmentChain
:
'
0xD7754711773489F31A0602635f3F167826ce53C5
'
as
const
,
CanonicalTransactionChain
:
'
0xf7B88A133202d41Fe5E2Ab22e6309a1A4D50AF74
'
as
const
,
BondManager
:
'
0xc5a603d273E28185c18Ba4d26A0024B2d2F42740
'
as
const
,
},
l2
:
DEFAULT_L2_CONTRACT_ADDRESSES
,
},
// Goerli
5
:
{
[
Chain
.
GOERLI
]:
{
l1
:
{
AddressManager
:
'
0x2F7E3cAC91b5148d336BbffB224B4dC79F09f01D
'
,
L1CrossDomainMessenger
:
'
0xEcC89b9EDD804850C4F343A278Be902be11AaF42
'
,
L1StandardBridge
:
'
0x73298186A143a54c20ae98EEE5a025bD5979De02
'
,
StateCommitmentChain
:
'
0x1afcA918eff169eE20fF8AB6Be75f3E872eE1C1A
'
,
CanonicalTransactionChain
:
'
0x2ebA8c4EfDB39A8Cd8f9eD65c50ec079f7CEBD81
'
,
BondManager
:
'
0xE5AE60bD6F8DEe4D0c2BC9268e23B92F1cacC58F
'
,
AddressManager
:
'
0x2F7E3cAC91b5148d336BbffB224B4dC79F09f01D
'
as
const
,
L1CrossDomainMessenger
:
'
0xEcC89b9EDD804850C4F343A278Be902be11AaF42
'
as
const
,
L1StandardBridge
:
'
0x73298186A143a54c20ae98EEE5a025bD5979De02
'
as
const
,
StateCommitmentChain
:
'
0x1afcA918eff169eE20fF8AB6Be75f3E872eE1C1A
'
as
const
,
CanonicalTransactionChain
:
'
0x2ebA8c4EfDB39A8Cd8f9eD65c50ec079f7CEBD81
'
as
const
,
BondManager
:
'
0xE5AE60bD6F8DEe4D0c2BC9268e23B92F1cacC58F
'
as
const
,
},
l2
:
DEFAULT_L2_CONTRACT_ADDRESSES
,
},
// Hardhat local
31337
:
{
[
Chain
.
HARDHAT_LOCAL
]:
{
l1
:
{
AddressManager
:
'
0x5FbDB2315678afecb367f032d93F642f64180aa3
'
,
L1CrossDomainMessenger
:
'
0x8A791620dd6260079BF849Dc5567aDC3F2FdC318
'
,
L1StandardBridge
:
'
0x610178dA211FEF7D417bC0e6FeD39F05609AD788
'
,
StateCommitmentChain
:
'
0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
'
,
CanonicalTransactionChain
:
'
0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
'
,
BondManager
:
'
0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
'
,
AddressManager
:
'
0x5FbDB2315678afecb367f032d93F642f64180aa3
'
as
const
,
L1CrossDomainMessenger
:
'
0x8A791620dd6260079BF849Dc5567aDC3F2FdC318
'
as
const
,
L1StandardBridge
:
'
0x610178dA211FEF7D417bC0e6FeD39F05609AD788
'
as
const
,
StateCommitmentChain
:
'
0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
'
as
const
,
CanonicalTransactionChain
:
'
0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
'
as
const
,
BondManager
:
'
0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
'
as
const
,
},
l2
:
DEFAULT_L2_CONTRACT_ADDRESSES
,
},
...
...
@@ -111,7 +120,7 @@ export const BRIDGE_ADAPTER_DATA: {
}
=
{
// TODO: Maybe we can pull these automatically from the token list?
// Alternatively, check against the token list in CI.
1
:
{
[
Chain
.
MAINNET
]
:
{
Standard
:
{
Adapter
:
StandardBridgeAdapter
,
l1Bridge
:
CONTRACT_ADDRESSES
[
1
].
l1
.
L1StandardBridge
,
...
...
@@ -124,16 +133,16 @@ export const BRIDGE_ADAPTER_DATA: {
},
BitBTC
:
{
Adapter
:
StandardBridgeAdapter
,
l1Bridge
:
'
0xaBA2c5F108F7E820C049D5Af70B16ac266c8f128
'
,
l2Bridge
:
'
0x158F513096923fF2d3aab2BcF4478536de6725e2
'
,
l1Bridge
:
'
0xaBA2c5F108F7E820C049D5Af70B16ac266c8f128
'
as
const
,
l2Bridge
:
'
0x158F513096923fF2d3aab2BcF4478536de6725e2
'
as
const
,
},
DAI
:
{
Adapter
:
DAIBridgeAdapter
,
l1Bridge
:
'
0x10E6593CDda8c58a1d0f14C5164B376352a55f2F
'
,
l2Bridge
:
'
0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65
'
,
l1Bridge
:
'
0x10E6593CDda8c58a1d0f14C5164B376352a55f2F
'
as
const
,
l2Bridge
:
'
0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65
'
as
const
,
},
},
42
:
{
[
Chain
.
KOVAN
]
:
{
Standard
:
{
Adapter
:
StandardBridgeAdapter
,
l1Bridge
:
CONTRACT_ADDRESSES
[
42
].
l1
.
L1StandardBridge
,
...
...
@@ -146,21 +155,21 @@ export const BRIDGE_ADAPTER_DATA: {
},
BitBTC
:
{
Adapter
:
StandardBridgeAdapter
,
l1Bridge
:
'
0x0b651A42F32069d62d5ECf4f2a7e5Bd3E9438746
'
,
l2Bridge
:
'
0x0CFb46528a7002a7D8877a5F7a69b9AaF1A9058e
'
,
l1Bridge
:
'
0x0b651A42F32069d62d5ECf4f2a7e5Bd3E9438746
'
as
const
,
l2Bridge
:
'
0x0CFb46528a7002a7D8877a5F7a69b9AaF1A9058e
'
as
const
,
},
USX
:
{
Adapter
:
StandardBridgeAdapter
,
l1Bridge
:
'
0x40E862341b2416345F02c41Ac70df08525150dC7
'
,
l2Bridge
:
'
0xB4d37826b14Cd3CB7257A2A5094507d701fe715f
'
,
l1Bridge
:
'
0x40E862341b2416345F02c41Ac70df08525150dC7
'
as
const
,
l2Bridge
:
'
0xB4d37826b14Cd3CB7257A2A5094507d701fe715f
'
as
const
,
},
DAI
:
{
Adapter
:
DAIBridgeAdapter
,
l1Bridge
:
'
0xb415e822C4983ecD6B1c1596e8a5f976cf6CD9e3
'
,
l2Bridge
:
'
0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65
'
,
l1Bridge
:
'
0xb415e822C4983ecD6B1c1596e8a5f976cf6CD9e3
'
as
const
,
l2Bridge
:
'
0x467194771dAe2967Aef3ECbEDD3Bf9a310C76C65
'
as
const
,
},
},
5
:
{
[
Chain
.
GOERLI
]
:
{
Standard
:
{
Adapter
:
StandardBridgeAdapter
,
l1Bridge
:
CONTRACT_ADDRESSES
[
5
].
l1
.
L1StandardBridge
,
...
...
@@ -172,7 +181,7 @@ export const BRIDGE_ADAPTER_DATA: {
l2Bridge
:
predeploys
.
L2StandardBridge
,
},
},
31337
:
{
[
Chain
.
HARDHAT_LOCAL
]
:
{
Standard
:
{
Adapter
:
StandardBridgeAdapter
,
l1Bridge
:
CONTRACT_ADDRESSES
[
31337
].
l1
.
L1StandardBridge
,
...
...
@@ -274,21 +283,29 @@ export const getAllOEContracts = (
}
// Attach all L1 contracts.
const
l1Contracts
:
OEL1Contracts
=
{}
as
any
const
l1Contracts
=
{}
as
OEL1Contracts
for
(
const
[
contractName
,
contractAddress
]
of
Object
.
entries
(
addresses
.
l1
))
{
l1Contracts
[
contractName
]
=
getOEContract
(
contractName
as
any
,
l1ChainId
,
{
address
:
opts
.
overrides
?.
l1
?.[
contractName
]
||
contractAddress
,
signerOrProvider
:
opts
.
l1SignerOrProvider
,
})
l1Contracts
[
contractName
]
=
getOEContract
(
contractName
as
keyof
OEL1Contracts
,
l1ChainId
,
{
address
:
opts
.
overrides
?.
l1
?.[
contractName
]
||
contractAddress
,
signerOrProvider
:
opts
.
l1SignerOrProvider
,
}
)
}
// Attach all L2 contracts.
const
l2Contracts
:
OEL2Contracts
=
{}
as
any
const
l2Contracts
=
{}
as
OEL2Contracts
for
(
const
[
contractName
,
contractAddress
]
of
Object
.
entries
(
addresses
.
l2
))
{
l2Contracts
[
contractName
]
=
getOEContract
(
contractName
as
any
,
l1ChainId
,
{
address
:
opts
.
overrides
?.
l2
?.[
contractName
]
||
contractAddress
,
signerOrProvider
:
opts
.
l2SignerOrProvider
,
})
l2Contracts
[
contractName
]
=
getOEContract
(
contractName
as
keyof
OEL2Contracts
,
l1ChainId
,
{
address
:
opts
.
overrides
?.
l2
?.[
contractName
]
||
contractAddress
,
signerOrProvider
:
opts
.
l2SignerOrProvider
,
}
)
}
return
{
...
...
packages/sdk/src/utils/misc-utils.ts
View file @
e4fbde24
...
...
@@ -8,10 +8,13 @@
* @param keys Keys to omit from the returned object.
* @returns A copy of the given object with the given keys omitted.
*/
export
const
omit
=
(
obj
:
any
,
...
keys
:
string
[])
=>
{
export
const
omit
=
<
T
extends
object
,
K
extends
string
|
number
|
symbol
>
(
obj
:
T
,
...
keys
:
K
[]
):
Omit
<
T
,
K
>
=>
{
const
copy
=
{
...
obj
}
for
(
const
key
of
keys
)
{
delete
copy
[
key
]
delete
copy
[
key
as
string
]
}
return
copy
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment