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
c8a68b7d
Unverified
Commit
c8a68b7d
authored
Dec 21, 2021
by
Kelvin Fichter
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement getMessageReceipt and tests
parent
9f970f0c
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
416 additions
and
10 deletions
+416
-10
cross-chain-provider.ts
packages/sdk/src/cross-chain-provider.ts
+108
-1
cross-chain-provider.ts
packages/sdk/src/interfaces/cross-chain-provider.ts
+12
-0
types.ts
packages/sdk/src/interfaces/types.ts
+1
-2
index.ts
packages/sdk/src/utils/index.ts
+1
-0
misc-utils.ts
packages/sdk/src/utils/misc-utils.ts
+17
-0
cross-chain-provider.spec.ts
packages/sdk/test/cross-chain-provider.spec.ts
+277
-7
No files found.
packages/sdk/src/cross-chain-provider.ts
View file @
c8a68b7d
...
...
@@ -19,6 +19,7 @@ import {
MessageStatus
,
TokenBridgeMessage
,
MessageReceipt
,
MessageReceiptStatus
,
CustomBridges
,
CustomBridgesLike
,
}
from
'
./interfaces
'
...
...
@@ -29,6 +30,7 @@ import {
DeepPartial
,
getAllOEContracts
,
getCustomBridges
,
hashCrossChainMessage
,
}
from
'
./utils
'
export
class
CrossChainProvider
implements
ICrossChainProvider
{
...
...
@@ -278,6 +280,59 @@ export class CrossChainProvider implements ICrossChainProvider {
})
}
public
async
toCrossChainMessage
(
message
:
MessageLike
):
Promise
<
CrossChainMessage
>
{
// TODO: Convert these checks into proper type checks.
if
((
message
as
CrossChainMessage
).
message
)
{
return
message
as
CrossChainMessage
}
else
if
(
(
message
as
TokenBridgeMessage
).
l1Token
&&
(
message
as
TokenBridgeMessage
).
l2Token
&&
(
message
as
TokenBridgeMessage
).
transactionHash
)
{
const
messages
=
await
this
.
getMessagesByTransaction
(
(
message
as
TokenBridgeMessage
).
transactionHash
)
// The `messages` object corresponds to a list of SentMessage events that were triggered by
// the same transaction. We want to find the specific SentMessage event that corresponds to
// the TokenBridgeMessage (either a ETHDepositInitiated, ERC20DepositInitiated, or
// WithdrawalInitiated event). We expect the behavior of bridge contracts to be that these
// TokenBridgeMessage events are triggered and then a SentMessage event is triggered. Our
// goal here is therefore to find the first SentMessage event that comes after the input
// event.
const
found
=
messages
.
sort
((
a
,
b
)
=>
{
// Sort all messages in ascending order by log index.
return
a
.
logIndex
-
b
.
logIndex
})
.
find
((
m
)
=>
{
return
m
.
logIndex
>
(
message
as
TokenBridgeMessage
).
logIndex
})
if
(
!
found
)
{
throw
new
Error
(
`could not find SentMessage event for message`
)
}
return
found
}
else
{
// TODO: Explicit TransactionLike check and throw if not TransactionLike
const
messages
=
await
this
.
getMessagesByTransaction
(
message
as
TransactionLike
)
// We only want to treat TransactionLike objects as MessageLike if they only emit a single
// message (very common). It's unintuitive to treat a TransactionLike as a MessageLike if
// they emit more than one message (which message do you pick?), so we throw an error.
if
(
messages
.
length
!==
1
)
{
throw
new
Error
(
`expected 1 message, got
${
messages
.
length
}
`
)
}
return
messages
[
0
]
}
}
public
async
getMessageStatus
(
message
:
MessageLike
):
Promise
<
MessageStatus
>
{
throw
new
Error
(
'
Not implemented
'
)
}
...
...
@@ -285,7 +340,59 @@ export class CrossChainProvider implements ICrossChainProvider {
public
async
getMessageReceipt
(
message
:
MessageLike
):
Promise
<
MessageReceipt
>
{
throw
new
Error
(
'
Not implemented
'
)
const
resolved
=
await
this
.
toCrossChainMessage
(
message
)
const
messageHash
=
hashCrossChainMessage
(
resolved
)
// Here we want the messenger that will receive the message, not the one that sent it.
const
messenger
=
resolved
.
direction
===
MessageDirection
.
L1_TO_L2
?
this
.
contracts
.
l2
.
L2CrossDomainMessenger
:
this
.
contracts
.
l1
.
L1CrossDomainMessenger
const
relayedMessageEvents
=
await
messenger
.
queryFilter
(
messenger
.
filters
.
RelayedMessage
(
messageHash
)
)
// Great, we found the message. Convert it into a transaction receipt.
if
(
relayedMessageEvents
.
length
===
1
)
{
return
{
receiptStatus
:
MessageReceiptStatus
.
RELAYED_SUCCEEDED
,
transactionReceipt
:
await
relayedMessageEvents
[
0
].
getTransactionReceipt
(),
}
}
else
if
(
relayedMessageEvents
.
length
>
1
)
{
// Should never happen!
throw
new
Error
(
`multiple successful relays for message`
)
}
// We didn't find a transaction that relayed the message. We now attempt to find
// FailedRelayedMessage events instead.
const
failedRelayedMessageEvents
=
await
messenger
.
queryFilter
(
messenger
.
filters
.
FailedRelayedMessage
(
messageHash
)
)
// A transaction can fail to be relayed multiple times. We'll always return the last
// transaction that attempted to relay the message.
// TODO: Is this the best way to handle this?
if
(
failedRelayedMessageEvents
.
length
>
0
)
{
return
{
receiptStatus
:
MessageReceiptStatus
.
RELAYED_FAILED
,
transactionReceipt
:
await
failedRelayedMessageEvents
[
failedRelayedMessageEvents
.
length
-
1
].
getTransactionReceipt
(),
}
}
// TODO: If the user doesn't provide enough gas then there's a chance that FailedRelayedMessage
// will never be triggered. We should probably fix this at the contract level by requiring a
// minimum amount of input gas and designing the contracts such that the gas will always be
// enough to trigger the event. However, for now we need a temporary way to find L1 => L2
// transactions that fail but don't alert us because they didn't provide enough gas.
// TODO: Talk with the systems and protocol team about coordinating a hard fork that fixes this
// on both L1 and L2.
// Just return null if we didn't find a receipt. Slightly nicer than throwing an error.
return
null
}
public
async
waitForMessageReciept
(
...
...
packages/sdk/src/interfaces/cross-chain-provider.ts
View file @
c8a68b7d
...
...
@@ -145,6 +145,18 @@ export interface ICrossChainProvider {
}
):
Promise
<
TokenBridgeMessage
[]
>
/**
* Resolves a MessageLike into a CrossChainMessage object.
* Unlike other coercion functions, this function is stateful and requires making additional
* requests. For now I'm going to keep this function here, but we could consider putting a
* similar function inside of utils/coercion.ts if people want to use this without having to
* create an entire CrossChainProvider object.
*
* @param message MessageLike to resolve into a CrossChainMessage.
* @returns Message coerced into a CrossChainMessage.
*/
toCrossChainMessage
(
message
:
MessageLike
):
Promise
<
CrossChainMessage
>
/**
* Retrieves the status of a particular message as an enum.
*
...
...
packages/sdk/src/interfaces/types.ts
View file @
c8a68b7d
...
...
@@ -188,15 +188,14 @@ export interface TokenBridgeMessage {
* Enum describing the status of a CrossDomainMessage message receipt.
*/
export
enum
MessageReceiptStatus
{
RELAYED_SUCCEEDED
,
RELAYED_FAILED
,
RELAYED_SUCCEEDED
,
}
/**
* CrossDomainMessage receipt.
*/
export
interface
MessageReceipt
{
messageHash
:
string
receiptStatus
:
MessageReceiptStatus
transactionReceipt
:
TransactionReceipt
}
...
...
packages/sdk/src/utils/index.ts
View file @
c8a68b7d
...
...
@@ -2,3 +2,4 @@ export * from './coercion'
export
*
from
'
./contracts
'
export
*
from
'
./message-encoding
'
export
*
from
'
./type-utils
'
export
*
from
'
./misc-utils
'
packages/sdk/src/utils/misc-utils.ts
0 → 100644
View file @
c8a68b7d
// TODO: A lot of this stuff could probably live in core-utils instead.
// Review this file eventually for stuff that could go into core-utils.
/**
* Returns a copy of the given object ({ ...obj }) with the given keys omitted.
*
* @param obj Object to return with the keys omitted.
* @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
[])
=>
{
const
copy
=
{
...
obj
}
for
(
const
key
of
keys
)
{
delete
copy
[
key
]
}
return
copy
}
packages/sdk/test/cross-chain-provider.spec.ts
View file @
c8a68b7d
...
...
@@ -7,6 +7,8 @@ import {
CrossChainProvider
,
MessageDirection
,
CONTRACT_ADDRESSES
,
hashCrossChainMessage
,
omit
,
}
from
'
../src
'
describe
(
'
CrossChainProvider
'
,
()
=>
{
...
...
@@ -644,6 +646,141 @@ describe('CrossChainProvider', () => {
})
})
describe
(
'
toCrossChainMessage
'
,
()
=>
{
let
l1Bridge
:
Contract
let
l2Bridge
:
Contract
let
l1Messenger
:
Contract
let
l2Messenger
:
Contract
let
provider
:
CrossChainProvider
beforeEach
(
async
()
=>
{
l1Messenger
=
(
await
(
await
ethers
.
getContractFactory
(
'
MockMessenger
'
)
).
deploy
())
as
any
l2Messenger
=
(
await
(
await
ethers
.
getContractFactory
(
'
MockMessenger
'
)
).
deploy
())
as
any
l1Bridge
=
(
await
(
await
ethers
.
getContractFactory
(
'
MockBridge
'
)
).
deploy
(
l1Messenger
.
address
))
as
any
l2Bridge
=
(
await
(
await
ethers
.
getContractFactory
(
'
MockBridge
'
)
).
deploy
(
l2Messenger
.
address
))
as
any
provider
=
new
CrossChainProvider
({
l1Provider
:
ethers
.
provider
,
l2Provider
:
ethers
.
provider
,
l1ChainId
:
31337
,
contracts
:
{
l1
:
{
L1CrossDomainMessenger
:
l1Messenger
.
address
,
L1StandardBridge
:
l1Bridge
.
address
,
},
l2
:
{
L2CrossDomainMessenger
:
l2Messenger
.
address
,
L2StandardBridge
:
l2Bridge
.
address
,
},
},
})
})
describe
(
'
when the input is a CrossChainMessage
'
,
()
=>
{
it
(
'
should return the input
'
,
async
()
=>
{
const
message
=
{
direction
:
MessageDirection
.
L1_TO_L2
,
target
:
'
0x
'
+
'
11
'
.
repeat
(
20
),
sender
:
'
0x
'
+
'
22
'
.
repeat
(
20
),
message
:
'
0x
'
+
'
33
'
.
repeat
(
64
),
messageNonce
:
1234
,
logIndex
:
0
,
blockNumber
:
1234
,
transactionHash
:
'
0x
'
+
'
44
'
.
repeat
(
32
),
}
expect
(
await
provider
.
toCrossChainMessage
(
message
)).
to
.
deep
.
equal
(
message
)
})
})
describe
(
'
when the input is a TokenBridgeMessage
'
,
()
=>
{
// TODO: There are some edge cases here with custom bridges that conform to the interface but
// not to the behavioral spec. Possibly worth testing those. For now this is probably
// sufficient.
it
(
'
should return the sent message event that came after the deposit or withdrawal
'
,
async
()
=>
{
const
from
=
'
0x
'
+
'
99
'
.
repeat
(
20
)
const
deposit
=
{
l1Token
:
'
0x
'
+
'
11
'
.
repeat
(
20
),
l2Token
:
'
0x
'
+
'
22
'
.
repeat
(
20
),
from
,
to
:
'
0x
'
+
'
44
'
.
repeat
(
20
),
amount
:
ethers
.
BigNumber
.
from
(
1234
),
data
:
'
0x1234
'
,
}
const
tx
=
await
l1Bridge
.
emitERC20DepositInitiated
(
deposit
)
const
foundCrossChainMessages
=
await
provider
.
getMessagesByTransaction
(
tx
)
const
foundTokenBridgeMessages
=
await
provider
.
getTokenBridgeMessagesByAddress
(
from
)
const
resolved
=
await
provider
.
toCrossChainMessage
(
foundTokenBridgeMessages
[
0
]
)
expect
(
resolved
).
to
.
deep
.
equal
(
foundCrossChainMessages
[
0
])
})
})
describe
(
'
when the input is a TransactionLike
'
,
()
=>
{
describe
(
'
when the transaction sent exactly one message
'
,
()
=>
{
it
(
'
should return the CrossChainMessage sent in the transaction
'
,
async
()
=>
{
const
message
=
{
target
:
'
0x
'
+
'
11
'
.
repeat
(
20
),
sender
:
'
0x
'
+
'
22
'
.
repeat
(
20
),
message
:
'
0x
'
+
'
33
'
.
repeat
(
64
),
messageNonce
:
1234
,
gasLimit
:
100000
,
}
const
tx
=
await
l1Messenger
.
triggerSentMessageEvents
([
message
])
const
foundCrossChainMessages
=
await
provider
.
getMessagesByTransaction
(
tx
)
const
resolved
=
await
provider
.
toCrossChainMessage
(
tx
)
expect
(
resolved
).
to
.
deep
.
equal
(
foundCrossChainMessages
[
0
])
})
})
describe
(
'
when the transaction sent more than one message
'
,
()
=>
{
it
(
'
should throw an error
'
,
async
()
=>
{
const
messages
=
[...
Array
(
2
)].
map
(()
=>
{
return
{
target
:
'
0x
'
+
'
11
'
.
repeat
(
20
),
sender
:
'
0x
'
+
'
22
'
.
repeat
(
20
),
message
:
'
0x
'
+
'
33
'
.
repeat
(
64
),
messageNonce
:
1234
,
gasLimit
:
100000
,
}
})
const
tx
=
await
l1Messenger
.
triggerSentMessageEvents
(
messages
)
await
expect
(
provider
.
toCrossChainMessage
(
tx
)).
to
.
be
.
rejectedWith
(
'
expected 1 message, got 2
'
)
})
})
describe
(
'
when the transaction sent no messages
'
,
()
=>
{
it
(
'
should throw an error
'
,
async
()
=>
{
const
tx
=
await
l1Messenger
.
triggerSentMessageEvents
([])
await
expect
(
provider
.
toCrossChainMessage
(
tx
)).
to
.
be
.
rejectedWith
(
'
expected 1 message, got 0
'
)
})
})
})
})
describe
(
'
getMessageStatus
'
,
()
=>
{
describe
(
'
when the message is an L1 => L2 message
'
,
()
=>
{
describe
(
'
when the message has not been executed on L2 yet
'
,
()
=>
{
...
...
@@ -689,27 +826,160 @@ describe('CrossChainProvider', () => {
})
describe
(
'
getMessageReceipt
'
,
()
=>
{
let
l1Bridge
:
Contract
let
l2Bridge
:
Contract
let
l1Messenger
:
Contract
let
l2Messenger
:
Contract
let
provider
:
CrossChainProvider
beforeEach
(
async
()
=>
{
l1Messenger
=
(
await
(
await
ethers
.
getContractFactory
(
'
MockMessenger
'
)
).
deploy
())
as
any
l2Messenger
=
(
await
(
await
ethers
.
getContractFactory
(
'
MockMessenger
'
)
).
deploy
())
as
any
l1Bridge
=
(
await
(
await
ethers
.
getContractFactory
(
'
MockBridge
'
)
).
deploy
(
l1Messenger
.
address
))
as
any
l2Bridge
=
(
await
(
await
ethers
.
getContractFactory
(
'
MockBridge
'
)
).
deploy
(
l2Messenger
.
address
))
as
any
provider
=
new
CrossChainProvider
({
l1Provider
:
ethers
.
provider
,
l2Provider
:
ethers
.
provider
,
l1ChainId
:
31337
,
contracts
:
{
l1
:
{
L1CrossDomainMessenger
:
l1Messenger
.
address
,
L1StandardBridge
:
l1Bridge
.
address
,
},
l2
:
{
L2CrossDomainMessenger
:
l2Messenger
.
address
,
L2StandardBridge
:
l2Bridge
.
address
,
},
},
})
})
describe
(
'
when the message has been relayed
'
,
()
=>
{
describe
(
'
when the relay was successful
'
,
()
=>
{
it
(
'
should return the receipt of the transaction that relayed the message
'
,
()
=>
{})
it
(
'
should return the receipt of the transaction that relayed the message
'
,
async
()
=>
{
const
message
=
{
direction
:
MessageDirection
.
L1_TO_L2
,
target
:
'
0x
'
+
'
11
'
.
repeat
(
20
),
sender
:
'
0x
'
+
'
22
'
.
repeat
(
20
),
message
:
'
0x
'
+
'
33
'
.
repeat
(
64
),
messageNonce
:
1234
,
logIndex
:
0
,
blockNumber
:
1234
,
transactionHash
:
'
0x
'
+
'
44
'
.
repeat
(
32
),
}
const
tx
=
await
l2Messenger
.
triggerRelayedMessageEvents
([
hashCrossChainMessage
(
message
),
])
const
messageReceipt
=
await
provider
.
getMessageReceipt
(
message
)
expect
(
messageReceipt
.
receiptStatus
).
to
.
equal
(
1
)
expect
(
omit
(
messageReceipt
.
transactionReceipt
,
'
confirmations
'
)
).
to
.
deep
.
equal
(
omit
(
await
ethers
.
provider
.
getTransactionReceipt
(
tx
.
hash
),
'
confirmations
'
)
)
})
})
describe
(
'
when the relay failed
'
,
()
=>
{
it
(
'
should return the receipt of the transaction that attempted to relay the message
'
,
()
=>
{})
it
(
'
should return the receipt of the transaction that attempted to relay the message
'
,
async
()
=>
{
const
message
=
{
direction
:
MessageDirection
.
L1_TO_L2
,
target
:
'
0x
'
+
'
11
'
.
repeat
(
20
),
sender
:
'
0x
'
+
'
22
'
.
repeat
(
20
),
message
:
'
0x
'
+
'
33
'
.
repeat
(
64
),
messageNonce
:
1234
,
logIndex
:
0
,
blockNumber
:
1234
,
transactionHash
:
'
0x
'
+
'
44
'
.
repeat
(
32
),
}
const
tx
=
await
l2Messenger
.
triggerFailedRelayedMessageEvents
([
hashCrossChainMessage
(
message
),
])
const
messageReceipt
=
await
provider
.
getMessageReceipt
(
message
)
expect
(
messageReceipt
.
receiptStatus
).
to
.
equal
(
0
)
expect
(
omit
(
messageReceipt
.
transactionReceipt
,
'
confirmations
'
)
).
to
.
deep
.
equal
(
omit
(
await
ethers
.
provider
.
getTransactionReceipt
(
tx
.
hash
),
'
confirmations
'
)
)
})
})
describe
(
'
when the relay failed more than once
'
,
()
=>
{
it
(
'
should return the receipt of the last transaction that attempted to relay the message
'
,
()
=>
{})
it
(
'
should return the receipt of the last transaction that attempted to relay the message
'
,
async
()
=>
{
const
message
=
{
direction
:
MessageDirection
.
L1_TO_L2
,
target
:
'
0x
'
+
'
11
'
.
repeat
(
20
),
sender
:
'
0x
'
+
'
22
'
.
repeat
(
20
),
message
:
'
0x
'
+
'
33
'
.
repeat
(
64
),
messageNonce
:
1234
,
logIndex
:
0
,
blockNumber
:
1234
,
transactionHash
:
'
0x
'
+
'
44
'
.
repeat
(
32
),
}
await
l2Messenger
.
triggerFailedRelayedMessageEvents
([
hashCrossChainMessage
(
message
),
])
const
tx
=
await
l2Messenger
.
triggerFailedRelayedMessageEvents
([
hashCrossChainMessage
(
message
),
])
const
messageReceipt
=
await
provider
.
getMessageReceipt
(
message
)
expect
(
messageReceipt
.
receiptStatus
).
to
.
equal
(
0
)
expect
(
omit
(
messageReceipt
.
transactionReceipt
,
'
confirmations
'
)
).
to
.
deep
.
equal
(
omit
(
await
ethers
.
provider
.
getTransactionReceipt
(
tx
.
hash
),
'
confirmations
'
)
)
})
})
})
describe
(
'
when the message has not been relayed
'
,
()
=>
{
it
(
'
should return null
'
,
()
=>
{})
it
(
'
should return null
'
,
async
()
=>
{
const
message
=
{
direction
:
MessageDirection
.
L1_TO_L2
,
target
:
'
0x
'
+
'
11
'
.
repeat
(
20
),
sender
:
'
0x
'
+
'
22
'
.
repeat
(
20
),
message
:
'
0x
'
+
'
33
'
.
repeat
(
64
),
messageNonce
:
1234
,
logIndex
:
0
,
blockNumber
:
1234
,
transactionHash
:
'
0x
'
+
'
44
'
.
repeat
(
32
),
}
await
l2Messenger
.
doNothing
()
const
messageReceipt
=
await
provider
.
getMessageReceipt
(
message
)
expect
(
messageReceipt
).
to
.
equal
(
null
)
})
})
describe
(
'
when the message does not exist
'
,
()
=>
{
it
(
'
should throw an error
'
,
()
=>
{})
})
// TODO: Go over all of these tests and remove the empty functions so we can accurately keep
// track of
})
describe
(
'
waitForMessageReciept
'
,
()
=>
{
...
...
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