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
b6074f52
Unverified
Commit
b6074f52
authored
Feb 01, 2022
by
smartcontracts
Committed by
GitHub
Feb 01, 2022
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2113 from ethereum-optimism/sc/sdk-finalize-message
feat(sdk): implement finalize message
parents
f5691302
400175c9
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
204 additions
and
23 deletions
+204
-23
package.json
packages/sdk/package.json
+3
-1
standard-bridge.ts
packages/sdk/src/adapters/standard-bridge.ts
+1
-1
cross-chain-messenger.ts
packages/sdk/src/cross-chain-messenger.ts
+24
-15
cross-chain-provider.ts
packages/sdk/src/cross-chain-provider.ts
+56
-4
cross-chain-provider.ts
packages/sdk/src/interfaces/cross-chain-provider.ts
+9
-0
types.ts
packages/sdk/src/interfaces/types.ts
+16
-2
index.ts
packages/sdk/src/utils/index.ts
+1
-0
merkle-utils.ts
packages/sdk/src/utils/merkle-utils.ts
+76
-0
yarn.lock
yarn.lock
+18
-0
No files found.
packages/sdk/package.json
View file @
b6074f52
...
@@ -63,6 +63,8 @@
...
@@ -63,6 +63,8 @@
"@eth-optimism/core-utils"
:
"0.7.5"
,
"@eth-optimism/core-utils"
:
"0.7.5"
,
"@ethersproject/abstract-provider"
:
"^5.5.1"
,
"@ethersproject/abstract-provider"
:
"^5.5.1"
,
"@ethersproject/abstract-signer"
:
"^5.5.0"
,
"@ethersproject/abstract-signer"
:
"^5.5.0"
,
"ethers"
:
"^5.5.2"
"ethers"
:
"^5.5.2"
,
"merkletreejs"
:
"^0.2.27"
,
"rlp"
:
"^2.2.7"
}
}
}
}
packages/sdk/src/adapters/standard-bridge.ts
View file @
b6074f52
...
@@ -245,7 +245,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
...
@@ -245,7 +245,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
throw
new
Error
(
`token pair not supported by bridge`
)
throw
new
Error
(
`token pair not supported by bridge`
)
}
}
return
this
.
l1Bridge
.
depositERC20
(
return
this
.
l1Bridge
.
populateTransaction
.
depositERC20
(
toAddress
(
l1Token
),
toAddress
(
l1Token
),
toAddress
(
l2Token
),
toAddress
(
l2Token
),
amount
,
amount
,
...
...
packages/sdk/src/cross-chain-messenger.ts
View file @
b6074f52
/* eslint-disable @typescript-eslint/no-unused-vars */
import
{
ethers
,
Overrides
,
Signer
,
BigNumber
}
from
'
ethers
'
import
{
ethers
,
Overrides
,
Signer
,
BigNumber
}
from
'
ethers
'
import
{
import
{
TransactionRequest
,
TransactionRequest
,
...
@@ -10,7 +9,6 @@ import {
...
@@ -10,7 +9,6 @@ import {
CrossChainMessageRequest
,
CrossChainMessageRequest
,
ICrossChainMessenger
,
ICrossChainMessenger
,
ICrossChainProvider
,
ICrossChainProvider
,
IBridgeAdapter
,
MessageLike
,
MessageLike
,
NumberLike
,
NumberLike
,
AddressLike
,
AddressLike
,
...
@@ -77,7 +75,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
...
@@ -77,7 +75,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides
?:
Overrides
overrides
?:
Overrides
}
}
):
Promise
<
TransactionResponse
>
{
):
Promise
<
TransactionResponse
>
{
throw
new
Error
(
'
Not implemented
'
)
return
this
.
l1Signer
.
sendTransaction
(
await
this
.
populateTransaction
.
finalizeMessage
(
message
,
opts
)
)
}
}
public
async
depositETH
(
public
async
depositETH
(
...
@@ -149,9 +149,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
...
@@ -149,9 +149,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
}
}
):
Promise
<
TransactionRequest
>
=>
{
):
Promise
<
TransactionRequest
>
=>
{
if
(
message
.
direction
===
MessageDirection
.
L1_TO_L2
)
{
if
(
message
.
direction
===
MessageDirection
.
L1_TO_L2
)
{
return
this
.
provider
.
contracts
.
l1
.
L1CrossDomainMessenger
.
connect
(
return
this
.
provider
.
contracts
.
l1
.
L1CrossDomainMessenger
.
populateTransaction
.
sendMessage
(
this
.
l1Signer
).
populateTransaction
.
sendMessage
(
message
.
target
,
message
.
target
,
message
.
message
,
message
.
message
,
opts
?.
l2GasLimit
||
opts
?.
l2GasLimit
||
...
@@ -159,9 +157,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
...
@@ -159,9 +157,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
opts
?.
overrides
||
{}
opts
?.
overrides
||
{}
)
)
}
else
{
}
else
{
return
this
.
provider
.
contracts
.
l2
.
L2CrossDomainMessenger
.
connect
(
return
this
.
provider
.
contracts
.
l2
.
L2CrossDomainMessenger
.
populateTransaction
.
sendMessage
(
this
.
l2Signer
).
populateTransaction
.
sendMessage
(
message
.
target
,
message
.
target
,
message
.
message
,
message
.
message
,
0
,
// Gas limit goes unused when sending from L2 to L1
0
,
// Gas limit goes unused when sending from L2 to L1
...
@@ -182,9 +178,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
...
@@ -182,9 +178,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
throw
new
Error
(
`cannot resend L2 to L1 message`
)
throw
new
Error
(
`cannot resend L2 to L1 message`
)
}
}
return
this
.
provider
.
contracts
.
l1
.
L1CrossDomainMessenger
.
connect
(
return
this
.
provider
.
contracts
.
l1
.
L1CrossDomainMessenger
.
populateTransaction
.
replayMessage
(
this
.
l1Signer
).
populateTransaction
.
replayMessage
(
resolved
.
target
,
resolved
.
target
,
resolved
.
sender
,
resolved
.
sender
,
resolved
.
message
,
resolved
.
message
,
...
@@ -201,7 +195,20 @@ export class CrossChainMessenger implements ICrossChainMessenger {
...
@@ -201,7 +195,20 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides
?:
Overrides
overrides
?:
Overrides
}
}
):
Promise
<
TransactionRequest
>
=>
{
):
Promise
<
TransactionRequest
>
=>
{
throw
new
Error
(
'
Not implemented
'
)
const
resolved
=
await
this
.
provider
.
toCrossChainMessage
(
message
)
if
(
resolved
.
direction
===
MessageDirection
.
L1_TO_L2
)
{
throw
new
Error
(
`cannot finalize L1 to L2 message`
)
}
const
proof
=
await
this
.
provider
.
getMessageProof
(
resolved
)
return
this
.
provider
.
contracts
.
l1
.
L1CrossDomainMessenger
.
populateTransaction
.
relayMessage
(
resolved
.
target
,
resolved
.
sender
,
resolved
.
message
,
resolved
.
messageNonce
,
proof
,
opts
?.
overrides
||
{}
)
},
},
depositETH
:
async
(
depositETH
:
async
(
...
@@ -297,7 +304,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
...
@@ -297,7 +304,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides
?:
Overrides
overrides
?:
Overrides
}
}
):
Promise
<
BigNumber
>
=>
{
):
Promise
<
BigNumber
>
=>
{
throw
new
Error
(
'
Not implemented
'
)
return
this
.
provider
.
l1Provider
.
estimateGas
(
await
this
.
populateTransaction
.
finalizeMessage
(
message
,
opts
)
)
},
},
depositETH
:
async
(
depositETH
:
async
(
...
@@ -332,7 +341,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
...
@@ -332,7 +341,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides
?:
Overrides
overrides
?:
Overrides
}
}
):
Promise
<
BigNumber
>
=>
{
):
Promise
<
BigNumber
>
=>
{
return
this
.
provider
.
l
2
Provider
.
estimateGas
(
return
this
.
provider
.
l
1
Provider
.
estimateGas
(
await
this
.
populateTransaction
.
depositERC20
(
await
this
.
populateTransaction
.
depositERC20
(
l1Token
,
l1Token
,
l2Token
,
l2Token
,
...
...
packages/sdk/src/cross-chain-provider.ts
View file @
b6074f52
...
@@ -5,7 +5,7 @@ import {
...
@@ -5,7 +5,7 @@ import {
TransactionReceipt
,
TransactionReceipt
,
}
from
'
@ethersproject/abstract-provider
'
}
from
'
@ethersproject/abstract-provider
'
import
{
ethers
,
BigNumber
}
from
'
ethers
'
import
{
ethers
,
BigNumber
}
from
'
ethers
'
import
{
sleep
}
from
'
@eth-optimism/core-utils
'
import
{
sleep
,
remove0x
}
from
'
@eth-optimism/core-utils
'
import
{
import
{
ICrossChainProvider
,
ICrossChainProvider
,
...
@@ -19,6 +19,7 @@ import {
...
@@ -19,6 +19,7 @@ import {
ProviderLike
,
ProviderLike
,
CrossChainMessage
,
CrossChainMessage
,
CrossChainMessageRequest
,
CrossChainMessageRequest
,
CrossChainMessageProof
,
MessageDirection
,
MessageDirection
,
MessageStatus
,
MessageStatus
,
TokenBridgeMessage
,
TokenBridgeMessage
,
...
@@ -38,6 +39,9 @@ import {
...
@@ -38,6 +39,9 @@ import {
getAllOEContracts
,
getAllOEContracts
,
getBridgeAdapters
,
getBridgeAdapters
,
hashCrossChainMessage
,
hashCrossChainMessage
,
makeMerkleTreeProof
,
makeStateTrieProof
,
encodeCrossChainMessage
,
}
from
'
./utils
'
}
from
'
./utils
'
export
class
CrossChainProvider
implements
ICrossChainProvider
{
export
class
CrossChainProvider
implements
ICrossChainProvider
{
...
@@ -302,7 +306,7 @@ export class CrossChainProvider implements ICrossChainProvider {
...
@@ -302,7 +306,7 @@ export class CrossChainProvider implements ICrossChainProvider {
}
else
{
}
else
{
const
challengePeriod
=
await
this
.
getChallengePeriodSeconds
()
const
challengePeriod
=
await
this
.
getChallengePeriodSeconds
()
const
targetBlock
=
await
this
.
l1Provider
.
getBlock
(
const
targetBlock
=
await
this
.
l1Provider
.
getBlock
(
stateRoot
.
blockNumber
stateRoot
.
b
atch
.
b
lockNumber
)
)
const
latestBlock
=
await
this
.
l1Provider
.
getBlock
(
'
latest
'
)
const
latestBlock
=
await
this
.
l1Provider
.
getBlock
(
'
latest
'
)
if
(
targetBlock
.
timestamp
+
challengePeriod
>
latestBlock
.
timestamp
)
{
if
(
targetBlock
.
timestamp
+
challengePeriod
>
latestBlock
.
timestamp
)
{
...
@@ -492,9 +496,9 @@ export class CrossChainProvider implements ICrossChainProvider {
...
@@ -492,9 +496,9 @@ export class CrossChainProvider implements ICrossChainProvider {
}
}
return
{
return
{
blockNumber
:
stateRootBatch
.
blockNumber
,
header
:
stateRootBatch
.
header
,
stateRoot
:
stateRootBatch
.
stateRoots
[
indexInBatch
],
stateRoot
:
stateRootBatch
.
stateRoots
[
indexInBatch
],
stateRootIndexInBatch
:
indexInBatch
,
batch
:
stateRootBatch
,
}
}
}
}
...
@@ -599,4 +603,52 @@ export class CrossChainProvider implements ICrossChainProvider {
...
@@ -599,4 +603,52 @@ export class CrossChainProvider implements ICrossChainProvider {
},
},
}
}
}
}
public
async
getMessageProof
(
message
:
MessageLike
):
Promise
<
CrossChainMessageProof
>
{
const
resolved
=
await
this
.
toCrossChainMessage
(
message
)
if
(
resolved
.
direction
===
MessageDirection
.
L1_TO_L2
)
{
throw
new
Error
(
`can only generate proofs for L2 to L1 messages`
)
}
const
stateRoot
=
await
this
.
getMessageStateRoot
(
resolved
)
if
(
stateRoot
===
null
)
{
throw
new
Error
(
`state root for message not yet published`
)
}
// We need to calculate the specific storage slot that demonstrates that this message was
// actually included in the L2 chain. The following calculation is based on the fact that
// messages are stored in the following mapping on L2:
// https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/L2/predeploys/OVM_L2ToL1MessagePasser.sol#L23
// You can read more about how Solidity storage slots are computed for mappings here:
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const
messageSlot
=
ethers
.
utils
.
keccak256
(
ethers
.
utils
.
keccak256
(
encodeCrossChainMessage
(
resolved
)
+
remove0x
(
this
.
contracts
.
l2
.
L2CrossDomainMessenger
.
address
)
)
+
'
00
'
.
repeat
(
32
)
)
const
stateTrieProof
=
await
makeStateTrieProof
(
this
.
l2Provider
as
any
,
resolved
.
blockNumber
,
this
.
contracts
.
l2
.
OVM_L2ToL1MessagePasser
.
address
,
messageSlot
)
return
{
stateRoot
:
stateRoot
.
stateRoot
,
stateRootBatchHeader
:
stateRoot
.
batch
.
header
,
stateRootProof
:
{
index
:
stateRoot
.
stateRootIndexInBatch
,
siblings
:
makeMerkleTreeProof
(
stateRoot
.
batch
.
stateRoots
,
stateRoot
.
stateRootIndexInBatch
),
},
stateTrieWitness
:
stateTrieProof
.
accountProof
,
storageTrieWitness
:
stateTrieProof
.
storageProof
,
}
}
}
}
packages/sdk/src/interfaces/cross-chain-provider.ts
View file @
b6074f52
...
@@ -8,6 +8,7 @@ import {
...
@@ -8,6 +8,7 @@ import {
AddressLike
,
AddressLike
,
NumberLike
,
NumberLike
,
CrossChainMessage
,
CrossChainMessage
,
CrossChainMessageProof
,
MessageDirection
,
MessageDirection
,
MessageStatus
,
MessageStatus
,
TokenBridgeMessage
,
TokenBridgeMessage
,
...
@@ -287,4 +288,12 @@ export interface ICrossChainProvider {
...
@@ -287,4 +288,12 @@ export interface ICrossChainProvider {
getStateRootBatchByTransactionIndex
(
getStateRootBatchByTransactionIndex
(
transactionIndex
:
number
transactionIndex
:
number
):
Promise
<
StateRootBatch
|
null
>
):
Promise
<
StateRootBatch
|
null
>
/**
* Generates the proof required to finalize an L2 to L1 message.
*
* @param message Message to generate a proof for.
* @returns Proof that can be used to finalize the message.
*/
getMessageProof
(
message
:
MessageLike
):
Promise
<
CrossChainMessageProof
>
}
}
packages/sdk/src/interfaces/types.ts
View file @
b6074f52
...
@@ -216,9 +216,9 @@ export interface StateRootBatchHeader {
...
@@ -216,9 +216,9 @@ export interface StateRootBatchHeader {
* Information about a state root, including header, block number, and root iself.
* Information about a state root, including header, block number, and root iself.
*/
*/
export
interface
StateRoot
{
export
interface
StateRoot
{
blockNumber
:
number
header
:
StateRootBatchHeader
stateRoot
:
string
stateRoot
:
string
stateRootIndexInBatch
:
number
batch
:
StateRootBatch
}
}
/**
/**
...
@@ -230,6 +230,20 @@ export interface StateRootBatch {
...
@@ -230,6 +230,20 @@ export interface StateRootBatch {
stateRoots
:
string
[]
stateRoots
:
string
[]
}
}
/**
* Proof data required to finalize an L2 to L1 message.
*/
export
interface
CrossChainMessageProof
{
stateRoot
:
string
stateRootBatchHeader
:
StateRootBatchHeader
stateRootProof
:
{
index
:
number
siblings
:
string
[]
}
stateTrieWitness
:
string
storageTrieWitness
:
string
}
/**
/**
* Stuff that can be coerced into a transaction.
* Stuff that can be coerced into a transaction.
*/
*/
...
...
packages/sdk/src/utils/index.ts
View file @
b6074f52
...
@@ -3,3 +3,4 @@ export * from './contracts'
...
@@ -3,3 +3,4 @@ export * from './contracts'
export
*
from
'
./message-encoding
'
export
*
from
'
./message-encoding
'
export
*
from
'
./type-utils
'
export
*
from
'
./type-utils
'
export
*
from
'
./misc-utils
'
export
*
from
'
./misc-utils
'
export
*
from
'
./merkle-utils
'
packages/sdk/src/utils/merkle-utils.ts
0 → 100644
View file @
b6074f52
/* Imports: External */
import
{
ethers
}
from
'
ethers
'
import
{
fromHexString
,
toHexString
,
toRpcHexString
,
}
from
'
@eth-optimism/core-utils
'
import
{
MerkleTree
}
from
'
merkletreejs
'
import
rlp
from
'
rlp
'
/**
* Generates a Merkle proof (using the particular scheme we use within Lib_MerkleTree).
*
* @param leaves Leaves of the merkle tree.
* @param index Index to generate a proof for.
* @returns Merkle proof sibling leaves, as hex strings.
*/
export
const
makeMerkleTreeProof
=
(
leaves
:
string
[],
index
:
number
):
string
[]
=>
{
// Our specific Merkle tree implementation requires that the number of leaves is a power of 2.
// If the number of given leaves is less than a power of 2, we need to round up to the next
// available power of 2. We fill the remaining space with the hash of bytes32(0).
const
correctedTreeSize
=
Math
.
pow
(
2
,
Math
.
ceil
(
Math
.
log2
(
leaves
.
length
)))
const
parsedLeaves
=
[]
for
(
let
i
=
0
;
i
<
correctedTreeSize
;
i
++
)
{
if
(
i
<
leaves
.
length
)
{
parsedLeaves
.
push
(
leaves
[
i
])
}
else
{
parsedLeaves
.
push
(
ethers
.
utils
.
keccak256
(
'
0x
'
+
'
00
'
.
repeat
(
32
)))
}
}
// merkletreejs prefers things to be Buffers.
const
bufLeaves
=
parsedLeaves
.
map
(
fromHexString
)
const
tree
=
new
MerkleTree
(
bufLeaves
,
(
el
:
Buffer
|
string
):
Buffer
=>
{
return
fromHexString
(
ethers
.
utils
.
keccak256
(
el
))
})
const
proof
=
tree
.
getProof
(
bufLeaves
[
index
],
index
).
map
((
element
:
any
)
=>
{
return
toHexString
(
element
.
data
)
})
return
proof
}
/**
* Generates a Merkle-Patricia trie proof for a given account and storage slot.
*
* @param provider RPC provider attached to an EVM-compatible chain.
* @param blockNumber Block number to generate the proof at.
* @param address Address to generate the proof for.
* @param slot Storage slot to generate the proof for.
* @returns Account proof and storage proof.
*/
export
const
makeStateTrieProof
=
async
(
provider
:
ethers
.
providers
.
JsonRpcProvider
,
blockNumber
:
number
,
address
:
string
,
slot
:
string
):
Promise
<
{
accountProof
:
string
storageProof
:
string
}
>
=>
{
const
proof
=
await
provider
.
send
(
'
eth_getProof
'
,
[
address
,
[
slot
],
toRpcHexString
(
blockNumber
),
])
return
{
accountProof
:
toHexString
(
rlp
.
encode
(
proof
.
accountProof
)),
storageProof
:
toHexString
(
rlp
.
encode
(
proof
.
storageProof
[
0
].
proof
)),
}
}
yarn.lock
View file @
b6074f52
...
@@ -10920,6 +10920,17 @@ merkletreejs@^0.2.18:
...
@@ -10920,6 +10920,17 @@ merkletreejs@^0.2.18:
treeify "^1.1.0"
treeify "^1.1.0"
web3-utils "^1.3.4"
web3-utils "^1.3.4"
merkletreejs@^0.2.27:
version "0.2.27"
resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.2.27.tgz#0691df1e1c80ebea7e35439dca5d9abd843b21d3"
integrity sha512-6fPGBdfbDyTiprK5JBBAxg+0u33xI3UM8EOeIz7Zy+5czuXH8vOhLMK1hMZFWPdCNgETWkpj+GOMKKhKZPOvaQ==
dependencies:
bignumber.js "^9.0.1"
buffer-reverse "^1.0.1"
crypto-js "^3.1.9-1"
treeify "^1.1.0"
web3-utils "^1.3.4"
methods@^1.1.2, methods@~1.1.2:
methods@^1.1.2, methods@~1.1.2:
version "1.1.2"
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
...
@@ -13643,6 +13654,13 @@ rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3, rlp@^2.2.4, rlp@^2.2.6:
...
@@ -13643,6 +13654,13 @@ rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3, rlp@^2.2.4, rlp@^2.2.6:
dependencies:
dependencies:
bn.js "^4.11.1"
bn.js "^4.11.1"
rlp@^2.2.7:
version "2.2.7"
resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf"
integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==
dependencies:
bn.js "^5.2.0"
run-async@^2.2.0, run-async@^2.4.0:
run-async@^2.2.0, run-async@^2.4.0:
version "2.4.1"
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
...
...
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