Ether Swap Tutorial
Last updated
Was this helpful?
Last updated
Was this helpful?
In this tutorial, we'll integrate the REDSHIFT Javascript SDK into a Vue frontend that can be used to execute a Kovan Testnet Ether (kETH) <-> Lightning Testnet Bitcoin (ltBTC) submarine swap.
MetaMask is an Ethereum wallet and web3 provider running inside a browser extension. Install MetaMask if not already installed. You can request testnet ether from https://faucet.kovan.network if you do not have any.
In this scenario, a user arrives at the app with a Lightning invoice that they would like REDSHIFT to pay in exchange for ETH. The exchange is atomic and completely trustless. The user will send their ETH to the swap contract, which can only be claimed by REDSHIFT once the invoice has been paid. If REDSHIFT fails to pay the invoice, the user can reclaim their funds from the contract after a couple hours.
We'll begin by taking a look at the Start page.
First, we must fetch two important pieces of information from REDSHIFT: The active markets and market requirements.
The markets are rather self-explanatory. They tell us which markets REDSHIFT is currently servicing.
The market requirements can help inform us if the invoice will be rejected by REDSHIFT without making a round-trip to the server. This call is optional as the server will return the same validation error, but we can offer a better user experience by performing this check on the client-side. The market requirements include the minimum time until invoice expiration, minimum invoice amount, and maximum invoice amount that will be accepted by REDSHIFT.
In our app, we make both calls at the same time as the page is created:
Now that we have the markets and market requirements, we'll collect information from the user required to perform the swap.
At a minimum, we require the invoice that REDSHIFT will pay. If your application only supports one on-chain payment asset, ETH for example, then you will not need to collect the payment asset from the user.
We also support bitcoin payments in our app, so the user is required to select a payment asset.
To provide a better user experience, we'll validate the invoice on the client-side. To do so, we use bolt11-decoder, a lighter version of bolt11, to decode the invoice.
If the invoice does not decode successfully, then it is invalid:
Once the user has input a valid invoice and selected a market, we have enough information to check if the market requirements have been met.
We'll perform this check when the user clicks Pay Invoice
. Alternatively, you could run this validation when the input or select change event fires.
You can see this call in action inside the initiateSwap
method:
If the invoice does not meet the market requirements, the error is set on the input and code execution is stopped.
Once we've validated the information provided by the user, we're ready to request a quote from REDSHIFT.
This is a simple process that involves two steps; establish a WebSocket connection and request the quote:
The quote response will look like this:
Quote Field
Description
orderId
A unique identifier for your order. This is required to execute a refund transaction in the event REDSHIFT fails to pay the invoice.
expiryTimestampMs
The timestamp in milliseconds that the quote will expire if the user does not take action. We use this value to implement the quote expiration timer in the app. The action required to stop the quote expiration timer varies based on the payment asset. For Bitcoin, the timer will stop once a funding transaction is seen in the mempool. For Ethereum assets, the timer will stop when a funding transaction confirms.
amount
The amount the user must pay denominated in the payment asset that they selected (tBTC or kETH in this sample).
details
Quote details that are specific to the chosen market.
unsignedFundingTx
The unsigned Ethereum funding transaction. When using metamask, this object can be passed directly into web3.sendTransaction
to initiate payment.
We now have everything that we need to request payment from the user. Move to the Payment page for this part of the tutorial.
To provide a good UX, we'll subscribe to order state updates and present them to the user.
You can subscribe to state updates using the following method:
Once subscribed, we must attach an event handler that gets fired when the order state changes. In this sample, we'll feed the state update event to a method called handleStateChange
that will update the state for display, increase the progress bar completion percentage, and populate the payment proof once complete:
As you may have gathered from the above description, not all state updates share the same schema. There are three types of state updates.
Update Type
Description
GeneralStateUpdate
This is the most basic state update, which contains the orderId
and state
. General state updates are returned when the update did not involve a transaction confirmation or invoice payment. Both TxConfirmedStateUpdate
and SwapCompleteStateUpdate
extend this type.
TxConfirmedStateUpdate
In addition to the orderId
and state
fields, this update type returns a transactionId
. This type is used when notifying the subscriber of a partial fund, fund, or refund transaction confirmation.
SwapCompleteStateUpdate
In addition to the orderId
and state
fields, this update type returns a preimage
. This type is used when notifying the subscriber of an invoice payment. The preimage
is the proof of payment.
Now that our state update listener is hooked up, we're ready to accept payment from the user.
In this example, all MetaMask interactions are handled through the metamask object. We'll skip over many of the actions required to connect and communicate with MetaMask as they are not specific to REDSHIFT.
When the user clicks the Send Payment
button, we need to pass the unsigned funding transaction to MetaMask using the sendTransaction
RPC call. This will pop up the MetaMask window so the user can sign the transaction.
In this example, we use the MetaMask provider to make the RPC call directly:
Note that this code can be simplified by using a library like web3 or ethers.js.
Once signed, MetaMask will broadcast the transaction automatically. The order state update listener will take over from here. Upon invoice payment, the progress bar will be set to 100%, the proof of payment will be populated, and the Start Another Swap
button will be visible.
NOTE: In a real application, the refund details should be provided to the user as a file download before they're allowed to fund the swap. This sample only demos the refund flow when navigating directly from a failed swap.
If REDSHIFT fails to pay the invoice, the user must be able to reclaim the funds that they sent to the swap contract.
Open the Refund page to view the sample refund flow.
In this example, the user is responsible for refund transaction submission. This is not strictly required. Any address is capable of signing and broadcasting the refund transaction. Regardless of who broadcasts this transaction, the funds will always be returned to the address that initially funded the swap. This could be used to submit the refund transaction on behalf of the user when the timelock expires, which offers a better UX.
We cannot allow the user to broadcast the refund transaction immediately following invoice payment failure. Any refund transaction mined before the block timelock is met will fail. The ether swap timelock is currently set to 480 blocks, which means that the user must wait roughly 2 hours before refund transaction submission.
To begin the process, we'll fetch the refund details using the order id of the failed swap:
The ether refund details contain the following information:
Refund Field
Description
market
The market of the order, which contains the on-chain and off-chain asset tickers. ETH_LBTC
, for example.
state
The active order state. REDSHIFT will return refund details regardless of the order state. This field can be used in client-side validations to prevent the user from submitting a refund transaction for an order that's already complete or refunded.
blocksRemaining
The number of blocks remaining until the timelock expires and the refund transaction can be submitted.
refundableAtBlockHeight
The block height at which the timelock expires and the refund transaction can be submitted.
refundableBalance
The balance that is available for refund. Note that this field will not be decreased once the refund is complete.
details
This field contains the network-specific details that are necessary to submit the refund transaction. In this case, it contains two properties: to
and data
. These can be passed into sendTransaction
in the same way as the funding details to sign and broadcast the refund transaction.
If blocksRemaining
is greater than 0 then we know that the refund transaction cannot be submitted yet. Instead, we'll display a block countdown to timelock expiration.
To accomplish this, we'll subscribe to the Ethereum block height using the REDSHIFT WebSocket API and update the UI when a new block is mined:
Once blocksUntilRefundable
is less than or equal to 0, we can enable the Get Refund
button and allow the user to submit the refund transaction.
From here, we use the same approach as the funding transaction. The refund transaction details are passed to the sendTransaction
RPC call and the progess bar is updated using the order state subscription. Upon refund confirmation, the progress bar will be set to 100% and the Start Another Swap button will be visible.