Trading Backend
This is the first in a three-part guide on how to build a trustless atomic swap on Sui.
This particular protocol consists of three phases:
- One party
lock
s their object, obtaining aLocked
object and itsKey
. This party canunlock
their object to preserve liveness if the other party stalls before completing the second stage. - The other party registers a publicly accessible, shared
Escrow
object. This effectively locks their object at a particular version as well, waiting for the first party to complete the swap. The second party is able to request their object is returned to them, to preserve liveness as well. - The first party sends their locked object and its key to the shared
Escrow
object. This completes the swap, as long as all conditions are met: The sender of the swap transaction is the recipient of theEscrow
, the key of the desired object (exchange_key
) in the escrow matches the key supplied in the swap, and the key supplied in the swap unlocks theLocked<U>
.
Let's create a Sui project using the terminal command sui move new escrow
. Create a file in the sources directory named shared.move
, and let's go through the code together.
shared.move
You can view the complete source code for this app example in the Sui repository.
Let's go through the code line by line:
Structs
Escrow<T>
This struct represents an object held in escrow. It contains the following fields:
id
: Unique identifier for the escrow object.sender
: Address of the owner of theescrowed
object.recipient
: Intended recipient of theescrowed
object.exchange_key
: ID of the key that opens the lock on the object sender wants from the recipient.escrowed
: The actual object held in escrow.
struct Escrow<T: key + store> has key, store {
id: UID,
sender: address,
recipient: address,
exchange_key: ID,
escrowed: T,
}
The ID of the key is used as the exchange_key
, rather than the ID of the object, to ensure that the object is not modified after a trade has been initiated.
Error codes
Two constants are defined to represent potential errors during the execution of the swap:
EMismatchedSenderRecipient
: Thesender
andrecipient
of the two escrowed objects do not match.EMismatchedExchangeObject
: Theexchange_for
fields of the two escrowed objects do not match.
const EMismatchedSenderRecipient: u64 = 0;
const EMismatchedExchangeObject: u64 = 1;
Public functions
create
This function is used to create a new escrow object. It takes four arguments: the object to be escrowed, the ID of the key that opens the lock on the object the sender wants from the recipient, the intended recipient, and the transaction context.
public fun create<T: key + store>(
escrowed: T,
exchange_key: ID,
recipient: address,
ctx: &mut TxContext
) {
let escrow = Escrow {
id: object::new(ctx),
sender: tx_context::sender(ctx),
recipient,
exchange_key,
escrowed,
};
transfer::public_share_object(escrow);
}
swap
This function is used to perform the swap operation. It takes four arguments: the escrow object, the key, the locked object, and the transaction context.
public fun swap<T: key + store, U: key + store>(
escrow: Escrow<T>,
key: Key,
locked: Locked<U>,
ctx: &TxContext,
): T {
let Escrow {
id,
sender,
recipient,
exchange_key,
escrowed,
} = escrow;
assert!(recipient == tx_context::sender(ctx), EMismatchedSenderRecipient);
assert!(exchange_key == object::id(&key), EMismatchedExchangeObject);
// Do the actual swap
transfer::public_transfer(lock::unlock(locked, key), sender);
object::delete(id);
escrowed
}
The object::delete
function call is used to delete the shared Escrow
object. Previously, Move supported only the deletion of owned objects, but shared-object deletion has since been enabled.
return_to_sender
This function is used to cancel the escrow and return the escrowed item to the sender. It takes two arguments: the escrow object and the transaction context.
public fun return_to_sender<T: key + store>(
escrow: Escrow<T>,
ctx: &TxContext
): T {
let Escrow {
id,
sender,
recipient: _,
exchange_key: _,
escrowed,
} = escrow;
assert!(sender == tx_context::sender(ctx), EMismatchedSenderRecipient);
object::delete(id);
escrowed
}
Once again, the shared Escrow
object is deleted after the escrowed item is returned to the sender.
Tests
The code includes several tests to ensure the correct functioning of the atomic swap process. These tests cover successful swaps, mismatches in sender or recipient, mismatches in the exchange object, tampering with the object, and returning the object to the sender.
In conclusion, this code provides a robust and secure way to perform atomic swaps of objects in a decentralized system, without the need for a trusted third party. It uses shared objects and a series of checks to ensure that the swap only occurs if all conditions are met.
Deployment
See Publish a Package for a more detailed guide on publishing packages or Sui Client CLI for a complete reference of client
commands in the Sui CLI.
Before publishing your code, you must first initialize the Sui Client CLI, if you haven't already. To do so, in a terminal or console at the root directory of the project enter sui client
. If you receive the following response, complete the remaining instructions:
Config file ["<FILE-PATH>/.sui/sui_config/client.yaml"] doesn't exist, do you want to connect to a Sui Full node server [y/N]?
Enter y
to proceed. You receive the following response:
Sui Full node server URL (Defaults to Sui Devnet if not specified) :
Leave this blank (press Enter). You receive the following response:
Select key scheme to generate keypair (0 for ed25519, 1 for secp256k1, 2: for secp256r1):
Select 0
. Now you should have a Sui address set up.
Before being able to publish your package to Testnet, you need Testnet SUI tokens. To get some, join the Sui Discord, complete the verification steps, enter the #testnet-faucet
channel and type !faucet <WALLET ADDRESS>
. For other ways to get SUI in your Testnet account, see Get SUI Tokens.
Now that you have an account with some Testnet SUI, you can deploy your contracts. To publish your package, use the following command in the same terminal or console:
sui client publish --gas-budget <GAS-BUDGET>
For the gas budget, use a standard value such as 20000000
.
Next steps
You have written and deployed the Move package. To turn this into a complete dApp with frontend, you need to create a frontend. For the frontend to be updated, create an indexer that listens to the blockchain as escrows are made and swaps are fulfilled.
For the next step, you create the indexing service.