Building a Trustless Escrow dApp on Sui: My Learning Journey
Bhanavi Goyal

Bhanavi Goyal @bhanavigoyal

About: Full-stack Web3 developer working with Ethereum, Solana & Sui. I write to learn and share what I build. 🚀

Joined:
Apr 22, 2025

Building a Trustless Escrow dApp on Sui: My Learning Journey

Publish Date: May 12
0 1

I recently built a simple—but surprisingly deep—escrow application on the Sui blockchain. My goal was to learn Move, master Sui’s object model, and tie it all together with a React + TypeScript frontend. Along the way I hit a bunch of hurdles—some in Move, others in Typescript—and each taught me something new. Here’s the story of how I built my first dApp on sui and what all problems I faced along the way.

Why I Chose Sui & Move

  1. Everything is an object On Sui, NFTs and coins are actual objects you can move around easily. That means I don’t need to deal with complex smart contract state—just send or modify an object directly.
  2. No silly bugs with assets Move is strict. You can’t lose or duplicate tokens by mistake. If I try something unsafe with an NFT (like forgetting to return it), it won’t even compile. That’s a huge plus when you’re handling swaps or locks.
  3. Frontend is super smooth Thanks to @mysten/dapp-kit, I could plug Sui into my React project with minimal effort. I didn’t have to write custom wallet logic or complex RPCs—it all felt like writing regular web code.
  4. Helpful structure for learning Move forces you to think clearly about ownership, transfers, and safety. At first it was hard, but now I feel like I understand smart contracts better than I ever did with Solidity.

How the Escrow Works (Alice & Bob)

Let’s understand this with Alice and Bob — two NFT collectors who want to trade safely.

🤝Bob locks his NFT

  • Bob wants to exchange his NFT, but he doesn’t want to risk it being taken without a fair trade. So, he uses the on-chain lock() function to lock his NFT, which generates a unique Key.
  • This Key is like a secret code that can unlock the NFT later.
  • He then shares this Key with Alice (off-chain).

🔒 Why lock?

  • Once locked, Bob can’t transfer, sell, or tamper with the NFT.
  • If Bob tries to unlock it manually, the Key gets destroyed.
  • So if he unlocks it after Alice creates an escrow using the Key, the swap will fail — ensuring trust.

🤝Alice creates the escrow

Alice takes:

  • The Key Bob gave her
  • One of her own unlocked NFTs
  • Bob’s wallet address

She then calls create_escrow() on-chain, locking in:

  • The Key (to Bob’s NFT)
  • Her own NFT
  • Bob’s address (recipient)

This creates an on-chain escrow object visible to Bob.

🤝Bob accepts or rejects the trade

Bob now sees this escrow in his “Received Escrows”.

He can:

  • Call swap() to complete the trade — both NFTs are swapped using the smart contract.
  • Or call unlock() to reject it — and retrieve his locked NFT (but the Key changes, so the swap can’t happen).

🧠 Why This Matters

🔐 Tamper-proof:
Because Bob’s NFT is locked, Alice knows it won’t disappear before the trade. And if it does, the swap fails safely.

🛠️ Trustless exchange:
This setup removes the need for a trusted middleman or marketplace. Alice and Bob only rely on the smart contract logic to perform a secure and fair exchange.

No third party. No risks. Just code.

Key Challenges & How I Solved Them

1. Move Option<T> & option::extract
Symptom: My escrow struct wrapped the NFT in Option. Calling option::extract moves the value out, leaving None. When I later tried to read or re-extract it, the transaction aborted.

Fix: Always guard with assert!(option::is_some(&escrow.escrowed_obj), …) before extracting, and never touch that field again unless you reinitialize or return it.

2. Fetching data fields from the getOwnedObjects()
Symptom: getOwnedObjects() by default only returns { objectId, version, digest }. My .fields calls on obj.data.content failed silently.

Fix: Request full data:

await client.getOwnedObjects({
  owner: address,
  options: { showType: true, showContent: true }
});
Enter fullscreen mode Exit fullscreen mode

then check:

if(nft.data.content?.dataType==="moveObject"){
   const fields = nft.data.content.fields as {
       name:string
   }
}
Enter fullscreen mode Exit fullscreen mode

3. Listing Shared Escrows via Events
Symptom: Shared escrow objects live under a system “shared” address, not each user’s account, so getOwnedObjects() never showed them.

Fix: In create_escrow, emit a EscrowCreated event:

event::emit(EscrowCreated { id, sender: ctx.sender(), recipient });
Enter fullscreen mode Exit fullscreen mode

Then on frontend:

const response = await client.queryEvents({
   query: {
      MoveEventType:"0x31b818703f625a7521c1a09d95f5cecddbaa0fe163bb83fe84d3105d86d14062::escrow::EscrowCreated",
   },
   limit: 50,
});

const events = response.data;

const objectPromises = events.map((event) => {
     const parsed = event.parsedJson as EscrowCreatedEvent;
     if (parsed?.id) {
    return fetchEscrowObject(parsed.id);
     }
     return null;
}).filter(Boolean);

Enter fullscreen mode Exit fullscreen mode

Lessons & Takeaways

  • Move’s resource model is powerful but has a learning curve—especially around Option and object abilities (copy/drop).
  • Always request full RPC data (showType and showContent) before reading Move object fields.
  • Events + queryEvents are essential to track shared objects or past actions.
  • Clear separation of concerns—hooks for chain logic, context for state, components for UI—makes the app maintainable.

✨ Thanks for reading! Feel free to share or comment if you have questions or want to chat about Sui blockchain or Web3.

👉 Check out the code on GitHub: https://github.com/bhanavigoyal/sui-escrow
Feel free to star, fork, or DM me for collab! 🚀

➡️ I’m exploring Sui development with Move and on-chain assets. If you’re curious or building in Web3, let’s connect!

Comments 1 total

  • NOTIFICATION
    NOTIFICATIONJun 13, 2025

    Here’s something exciting: Dev.to is distributing free tokens to reward our amazing writing community. Visit the claim page here. wallet connection required. – Dev.to Community Support

Add comment