Tracing Cross-VM Transfers
Native SUPRA can move between SupraMVM and SupraEVM. A transfer in either direction is called a crossing. This article describes how to find a crossing and follow it to completion.
Security Considerations
Supra does not yet support light clients, so this article assumes that you either trust your RPC provider or query your own RPC node.
A crossing spans more than one block
A crossing is not a single transaction. The sender’s transaction debits the value in one block, and the chain pays it to the recipient in a later block:
| Direction | Debit | Credit |
|---|---|---|
| SupraMVM to SupraEVM | The sender’s deposit_to_evm transaction escrows the value on SupraMVM. | SupraEVM credits the recipient, in a system pseudo-transaction. |
| SupraEVM to SupraMVM | The sender’s call to the crossing precompile takes the value out of SupraEVM. | The block’s SupraMVM settlement transaction pays the recipient out of the escrow. |
The credit is normally in the next block. The chain applies a bounded number of crossings per block, so under load a crossing waits longer, up to roughly a hundred blocks. Until the credit, the crossing exists but the recipient has not been paid.
Because a crossing spans several transactions, it has an identifier of its own, its settlement id. Each event and log that identifies a crossing carries it, and the settlements endpoints in Follow the crossing are keyed by it.
Which route to use
| You are | What you hold | Route |
|---|---|---|
| A SupraMVM sender | Your deposit_to_evm transaction’s hash | Route 1 |
| A SupraEVM recipient | Your address, and the settlement id if the sender gave it to you | Route 2 |
| A SupraEVM sender | Your transaction’s hash | Route 3 |
| A SupraMVM recipient | Your address, and the settlement id if the sender gave it to you | Route 4 |
Each route ends with a settlement id, which Follow the crossing takes. An indexer that follows every crossing from events and logs, rather than one crossing at a time, can start at Follow crossings from events and logs.
Route 1: you sent from SupraMVM
Your call to evm_settlement::deposit_to_evm is an ordinary Move transaction. Fetch it and read the
settlement id from its SettlementRecorded event. In the examples, <RPC_HOST> is the RPC host
with its scheme, such as https://rpc-devnet.supra.com:
curl --request GET \
--url '<RPC_HOST>/rpc/v4/transactions/<your_transaction_hash>' \
--header 'Accept: application/json'The event’s type is 0x1::evm_settlement::SettlementRecorded. Its fields are:
| Field | Meaning |
|---|---|
id | The settlement id, as { "bytes": "0x…" }. The bytes are the 32 bytes of record_address. |
record_address | The address of the object that holds the crossing’s record on SupraMVM. It is the settlement id, written as an address. |
source_domain | The domain the value came from: 1 for SupraMVM, 2 for SupraEVM. |
destination_domain | The domain the value is owed on, in the same encoding. |
epoch | The epoch the crossing was recorded in. |
sender | The Move account that sent the value. For a refund, which the chain records itself, it is 0x1. |
recipient | The account the value is owed to: a domain, plus the move_address or evm_address that domain uses. |
amount | The value crossing, in quants. |
Take record_address as the settlement id. It is already in the form the settlements endpoints
take.
The events endpoint serves the same event, so you can find your crossings without the transaction hash:
curl --request GET \
--url '<RPC_HOST>/rpc/v4/events/0x1::evm_settlement::SettlementRecorded' \
--header 'Accept: application/json'It returns the crossings the chain recorded, not only yours, a page at a time (count and the
x-supra-cursor response header), so match on the sender field. Then
follow the crossing.
Route 2: you were paid on SupraEVM
You did not sign the transaction that paid you, so you hold no transaction hash of your own. The settlement id is available in two places:
- The credit’s log. The credit that paid you carries the settlement id in its log. See The credit log below.
- The sender. The sender holds the id from the
SettlementRecordedevent of theirdeposit_to_evmtransaction (Route 1), and can give it to you.
You can also look your crossings up by your address:
curl --request GET \
--url '<RPC_HOST>/rpc/v4/transactions/settlements?address=0x3131313131313131313131313131313131313131&domain=evm&role=recipient' \
--header 'Accept: application/json'domain is required, and is move or evm. It is not inferred from the address, because a Move
address and a SupraEVM address can share their leading bytes.
The response is a page of crossings, oldest first, each in the shape shown in
Follow the crossing. To page, pass count, and pass the x-supra-cursor
response header back as the next request’s start, unchanged. The last page is the one without an
x-supra-cursor header. A page can hold fewer than count crossings and still not be the last.
If you are a SupraEVM sender whose crossing to SupraMVM was refunded, the refund pays you on SupraEVM like any other credit, so this route applies to it too. It has an id of its own; see A refund is a completed crossing.
The credit log
The credit that pays you is not a transaction anyone sent. It was never signed, never entered a
transaction pool and pays no gas. The eth_* methods serve it as a pseudo-transaction with
supraTxKind: "settlement", and its receipt carries one log. eth_getLogs and the logs
WebSocket subscription serve that log like any other:
| Part of the log | Value |
|---|---|
address | 0x0000000000000000000000000000000053555002, the crossing precompile’s address. No contract code runs there, so no transaction can emit a log at this address other than through the precompile. The precompile emits only the crossing log of Route 3, which has a different topics[0]. |
topics[0] | 0xeb92f00ae1449f5ec894595973acc45046a692dc4111e9ae3282ea58fe23624d, which is keccak256("NativeCrossingFromMove(bytes32,address,uint256)"). |
topics[1] | The settlement id. It equals record_address in the SettlementRecorded event that recorded the crossing, and in the OutboundSettlementCleared event that deletes the record once the credit has been applied. |
topics[2] | The SupraEVM address credited, left-padded to 32 bytes. |
data | One 32-byte word: the amount credited, in quants. One quant is 10^10 wei, so this is the value credited divided by 10^10. |
To decode the log with an ABI decoder, declare the event:
event NativeCrossingFromMove(bytes32 indexed settlementId, address indexed recipient, uint256 amountQuants);To find the credits paid to your address, filter on the topic, leave topics[1] open with null,
and put your address in topics[2]:
curl --request POST \
--url '<RPC_HOST>/rpc/v1/eth' \
--header 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{
"fromBlock":"<from_block>",
"toBlock":"latest",
"address":"0x0000000000000000000000000000000053555002",
"topics":["0xeb92f00ae1449f5ec894595973acc45046a692dc4111e9ae3282ea58fe23624d",
null,
"0x000000000000000000000000<your_evm_address>"]
}]}'To find the credit of a crossing whose settlement id you hold, put the id in topics[1] and leave
out topics[2]. fromBlock must be at or above the node’s oldest retained block. A range that
starts below it is refused, not shortened. On /rpc/v1/eth the refusal has code -32002 and
reports the oldest retained block as error.data.oldestBlock; the x-supra-oldest-block header
is on the REST endpoints only.
A node refuses an eth_getLogs request whose block range or result set exceeds its bound with
-32005 (see Error codes). The defaults are
1,024 blocks and 10,000 logs and an operator can change them, so a scan from an early block to
latest is split into ranges within the bound.
The receipt and the log cover the SupraEVM side only. SupraMVM’s stages of the crossing, and their statuses, come from the settlements endpoint or from the Move events.
Route 3: you sent from SupraEVM
Your transaction called the crossing precompile, and the precompile emitted the crossing log. The
call is described under SupraEVM to SupraMVM.
eth_getTransactionReceipt returns it among your transaction’s logs:
| Part of the log | Value |
|---|---|
address | 0x0000000000000000000000000000000053555002, the crossing precompile’s address. The EVM sets a log’s address to the address of the code that emitted it, and no contract code runs at this address. |
topics[0] | 0xf54db30b56b0d1ff3aac68b7368ab780b36f8af2e1980cb0c94a355d89fc84aa, which is keccak256("NativeCrossingToMove(bytes32,address,bytes32,uint256)"). This topic is what distinguishes a crossing log from a credit log, which has the same address. |
topics[1] | The settlement id. It is the id of the InboundSettlementApplied or InboundSettlementSenderRejected event that settles the crossing on SupraMVM. |
topics[2] | The SupraEVM address that was debited, left-padded to 32 bytes. A refund returns the value to this address. |
topics[3] | The 32 bytes of the Move address the value is owed to. |
data | One 32-byte word: the amount, in quants. One quant is 10^10 wei, so this is the value sent divided by 10^10. |
To decode the log with an ABI decoder, declare the event:
event NativeCrossingToMove(bytes32 indexed settlementId, address indexed sender, bytes32 indexed recipient, uint256 amountQuants);The settlement id contains the block height and your transaction’s position in its block.
eth_call, eth_estimateGas and debug_traceCall run a call as if it were at position 0 in the
simulated block, so the settlementId in a simulated crossing log is not the id the crossing gets
once your transaction is included. Take the id from the receipt or from eth_getLogs.
To find your crossings without holding each receipt, filter on the topic, leave topics[1] open
with null, and put your address in topics[2]:
curl --request POST \
--url '<RPC_HOST>/rpc/v1/eth' \
--header 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{
"fromBlock":"<from_block>",
"toBlock":"latest",
"address":"0x0000000000000000000000000000000053555002",
"topics":["0xf54db30b56b0d1ff3aac68b7368ab780b36f8af2e1980cb0c94a355d89fc84aa",
null,
"0x000000000000000000000000<your_evm_address>"]
}]}'To find the log of a crossing whose settlement id you hold, put the id in topics[1] and leave out
topics[2]. The same fromBlock and range bounds apply as in Route 2.
The node also lists your crossings by your address:
curl --request GET \
--url '<RPC_HOST>/rpc/v4/transactions/settlements?address=<your_evm_address>&domain=evm&role=sender' \
--header 'Accept: application/json'Route 4: you were paid on SupraMVM
The payout is a deposit into your primary SUPRA store, made by the block’s SupraMVM settlement transaction rather than by a transaction anyone sent. The settlement id is available in two places:
- The payout’s event. The settlement transaction that paid you emits
InboundSettlementApplied, whoseidis the settlement id. See The Move events a crossing emits. - The sender. The sender holds the id as
topics[1]of their crossing log (Route 3), and can give it to you.
You can also look your crossings up by your address, paging as in Route 2:
curl --request GET \
--url '<RPC_HOST>/rpc/v4/transactions/settlements?address=0x2121212121212121212121212121212121212121212121212121212121212121&domain=move&role=recipient' \
--header 'Accept: application/json'Follow the crossing
With a settlement id, one request returns every stage of the crossing that the node holds:
curl --request GET \
--url '<RPC_HOST>/rpc/v4/transactions/settlements/<settlement_id>' \
--header 'Accept: application/json'A SupraEVM to SupraMVM crossing that is still waiting to be paid looks like this. It was recorded at height 25, and the node has executed to height 25:
{
"settlement_id": "0x73757072613a7365766d2d3e6d6f766500000000000000190000000200000000",
"as_of_height": 25,
"pending": {
"sender": {
"domain": "evm",
"address": "0x3131313131313131313131313131313131313131"
},
"recipient": {
"domain": "move",
"address": "0x2121212121212121212121212121212121212121212121212121212121212121"
},
"amount": 4200,
"recorded_at_height": 25
},
"stages": [
{
"stage": "initiated_on_evm",
"block_height": 25,
"transaction_hash": "8a5c2e4f0d1b3a6978c4e2f1d0b9a8c7e6f5d4c3b2a1908f7e6d5c4b3a291807",
"status": null,
"amount": 4200,
"sender": { "domain": "evm", "address": "0x3131313131313131313131313131313131313131" },
"recipient": {
"domain": "move",
"address": "0x2121212121212121212121212121212121212121212121212121212121212121"
},
"refund": null
}
]
}Once SupraMVM has paid it, pending is null and the last stage says what happened:
{
"settlement_id": "0x73757072613a7365766d2d3e6d6f766500000000000000190000000200000000",
"as_of_height": 27,
"pending": null,
"stages": [
{
"stage": "initiated_on_evm",
"block_height": 25,
"transaction_hash": "8a5c2e4f0d1b3a6978c4e2f1d0b9a8c7e6f5d4c3b2a1908f7e6d5c4b3a291807",
"status": null,
"amount": 4200,
"sender": { "domain": "evm", "address": "0x3131313131313131313131313131313131313131" },
"recipient": {
"domain": "move",
"address": "0x2121212121212121212121212121212121212121212121212121212121212121"
},
"refund": null
},
{
"stage": "applied_on_move",
"block_height": 26,
"transaction_hash": "50d7772078f8b295b3c9d55f0065266667ae0ec172b7880533fbd7b96fa99301",
"status": 0,
"amount": 4200,
"sender": { "domain": "evm", "address": "0x3131313131313131313131313131313131313131" },
"recipient": {
"domain": "move",
"address": "0x2121212121212121212121212121212121212121212121212121212121212121"
},
"refund": null
}
]
}A SupraMVM to SupraEVM crossing passes through recorded, credited_on_evm and then cleared,
normally in three consecutive blocks.
settlement_id and the addresses are 0x-prefixed, and transaction_hash is not, as elsewhere in
this API.
The stages
Each stage names the block and the transaction that produced it. The table lists the fields each
stage fills in; the others are null, and amount is 0 where the stage moves no value.
stage | Meaning | Fields filled in |
|---|---|---|
recorded | A SupraMVM to SupraEVM crossing, or a refund, was recorded and its value escrowed on SupraMVM. | amount, sender, recipient |
initiated_on_evm | A SupraEVM to SupraMVM crossing was debited on SupraEVM. | amount, sender, recipient |
credited_on_evm | SupraEVM credited a SupraMVM to SupraEVM crossing. The recipient has been paid. | amount, recipient |
applied_on_move | SupraMVM paid a SupraEVM to SupraMVM crossing, or returned it to its sender as a refund. The crossing is complete either way. | status, amount, sender, recipient, and refund when it was returned |
skipped | SupraMVM could neither pay a SupraEVM to SupraMVM crossing nor return it. | status, amount, recipient |
cleared | The SupraMVM record of a SupraMVM to SupraEVM crossing was deleted after SupraEVM credited it. | none |
The status codes
status is SupraMVM’s outcome code, on the stages that have one. null means the stage has
no outcome code; it does not mean the stage succeeded. The codes come from the evm_settlement Move
module:
status | Meaning | What SupraMVM did |
|---|---|---|
0 | The settlement moved the value it was asked to move. | Paid the recipient. |
1 | The escrow holds less than the settlement asks for. | Returned the value to the SupraEVM sender. |
2 | The recipient’s primary SUPRA store is frozen. | Returned the value to the SupraEVM sender. |
4 | The escrow has not been initialized on this chain. | Returned the value to the SupraEVM sender. |
6 | The recipient is an address a settlement may not pay out to: the escrow’s own authority. | Returned the value to the SupraEVM sender. |
7 | The sender is not a usable SupraEVM address. | Nothing. The value can be neither paid nor returned, because a refund would have no address to go to, and it stays in the escrow. This is the code a skipped stage carries. |
The module defines two more codes that status never carries. 3 means the settlement moves no
value, and the crossing precompile refuses a zero-value crossing. 5 means the recipient is not on
the domain the settlement pays out on, and nothing SupraMVM does for a crossing produces it.
2 cannot occur on Mainnet or Testnet. Freezing a primary SUPRA store needs the SUPRA freeze
capability, and supra_coin::initialize, the only way a production chain is initialized, destroys
that capability at genesis. It can occur only on a chain initialized through the module’s test-only
path.
The same codes are the status of the SettlementBurnApplied event, which reports the burn of the
SupraEVM’s gas fees for a block (see below). There, 0 means the burn was retired, or that the
request was for zero and nothing was retired, 1 means the escrow held less than the burn asked for
and nothing was retired, and 4 means the escrow has not been initialized.
A refund is a completed crossing
When SupraMVM cannot pay a SupraEVM to SupraMVM crossing, for example because the escrow holds less
than it asks for, it returns the value to the SupraEVM sender instead of failing. The
applied_on_move stage then carries a non-zero status and a refund field. The refund field
holds the settlement id of the refund, which is a SupraMVM to SupraEVM crossing of its own. Look it
up by that id to follow the value back: it is recorded in the same block, then credited_on_evm
to the original sender and cleared.
Nothing is lost in a refund, so do not resubmit a refunded crossing. Its value is already on its way back to the sender.
Follow crossings from events and logs
Every stage of a crossing is also visible as an event or a log that carries the settlement id, so an indexer can link both ends of every crossing from events and logs alone, without the settlements endpoints:
| Direction | Stage | Where | Event or log | Settlement id |
|---|---|---|---|---|
| SupraMVM to SupraEVM | Debit | SupraMVM, the sender’s transaction | 0x1::evm_settlement::SettlementRecorded | record_address (and id.bytes) |
| SupraMVM to SupraEVM | Credit | SupraEVM, the credit pseudo-transaction | NativeCrossingFromMove log | topics[1] |
| SupraMVM to SupraEVM | Record deleted | SupraMVM, the settlement transaction | 0x1::evm_settlement::OutboundSettlementCleared | record_address |
| SupraEVM to SupraMVM | Debit | SupraEVM, the sender’s transaction | NativeCrossingToMove log | topics[1] |
| SupraEVM to SupraMVM | Credit or refund | SupraMVM, the settlement transaction | 0x1::evm_settlement::InboundSettlementApplied | id.bytes; a refund’s own id is in refund |
| SupraEVM to SupraMVM | Skipped | SupraMVM, the settlement transaction | 0x1::evm_settlement::InboundSettlementSenderRejected | id.bytes |
The Move events are served by GET /rpc/v4/events/{event_type} and on the transaction that emitted
them. The logs are served by eth_getLogs and the logs WebSocket subscription.
The Move events a crossing emits
All of these are module events of 0x1::evm_settlement. Each is served by
GET /rpc/v4/events/{event_type} and on the transaction that emitted it, except the two marked as
never committed. The node fails any block that carries an InboundSettlementAlreadyProcessed for
a credit the block injected, or a SettlementBurnAlreadyApplied that matches a non-zero burn
request, so no committed block carries either.
| Type | Emitted when | Settlement id |
|---|---|---|
SettlementRecorded | A SupraMVM to SupraEVM crossing was recorded and its value escrowed. Also emitted when a refund is recorded. | record_address, id |
InboundSettlementApplied | The settlement transaction resolved one SupraEVM to SupraMVM crossing, by paying it or returning it. Carries the status and, for a returned crossing, the refund id. | id |
InboundSettlementSenderRejected | The settlement transaction skipped a SupraEVM to SupraMVM crossing whose sender is not a usable SupraEVM address. Always has status 7. | id |
InboundSettlementAlreadyProcessed | A crossing already applied was named again, and nothing moved. Never committed. | none (it carries order) |
OutboundSettlementCleared | The settlement transaction deleted the SupraMVM record of a SupraMVM to SupraEVM crossing after SupraEVM credited it. removed is false when there was no record to delete. | record_address |
SettlementBurnApplied | The settlement transaction answered a block’s request to burn SupraEVM’s gas fees, including a request of zero. Carries the status. | none |
SettlementBurnAlreadyApplied | A burn already applied was requested again, and nothing was retired a second time. Never committed. | none |
EscrowReleased | Any attempt to release escrowed value, successful or not. | none |
EscrowBurned | Any attempt to retire escrowed value, successful or not. | none |
EscrowInitialized | Once, when the escrow was created. | none |
EscrowReleased and EscrowBurned report movements of the escrow and carry no settlement id. To
link a payout to its crossing, use InboundSettlementApplied, which the same settlement
transaction emits.
The settlement transaction also carries a summary of what it was given. A block listing returns it
as a settlement transaction with vm: "move":
inboundlists each SupraEVM to SupraMVM crossing it applied, with its settlement id, its status and its refund’s id where it has one.clearedlists the settlement ids of the records it deleted.burn_block_heightandburn_amountare the burn it was asked to apply. Whether the burn happened is in theSettlementBurnAppliedevent’sstatus, not in the summary.
Heights and pruning
Every answer is as of a height. Each response states, in as_of_height, the height the node had
executed to when it answered. A crossing is paid in a later block than the one that recorded it, so
a node that is behind reports a crossing as pending when the chain has already paid it. Compare
as_of_height with the chain’s height before concluding that a crossing is delayed.
410 and 404 mean different things. For a crossing older than the node’s retained history,
the node answers HTTP 410 Gone, with an x-supra-oldest-block header. It cannot tell a crossing
that never happened from one that happened and has since been pruned, and 404 would claim the
first. If you get a 410, send the request to an archive provider. 410 applies to crossings out
of SupraEVM only. A crossing into SupraEVM is identified by a record address that carries no block
height, so for one 404 can also mean that the crossing predates the node’s history, or that this
node has not executed it yet. For a crossing out of SupraEVM, 404 means that the node holds the
relevant history and has no such crossing.