Joining two coins of custom currency

Hello SUI Move comrades. Please lend me a helping hand if you will. I am creating a currency of mine, minting it, splitting it and burning it as per code below:

      module try_nft::dnr;
                  
      const COIN_URL: vector<u8> = b"xyz";
        
      use sui::coin::{Self, Coin, TreasuryCap};
      use sui::url;
        
      public struct DNR has drop {}
        
      fun init(witness: DNR, ctx: &mut TxContext) {
          let (treasury_cap, metadata) = coin::create_currency<DNR>(
              witness,
              9,
              b"My SUI Coin",
              b"Denero",
              b"Coin worth crickets",
              option::some(url::new_unsafe_from_bytes(COIN_URL)),
              ctx,
          );
        
          transfer::public_freeze_object(metadata);
          transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
      }
        
      public entry fun mint_coin(
          treasury_cap: &mut TreasuryCap<DNR>,
          amount: u64,
          recipient: address,
          ctx: &mut TxContext) {
              coin::mint_and_transfer(treasury_cap, amount, recipient, ctx);
      }
        
      public entry fun burn_coin(
          treasury_cap: &mut TreasuryCap<DNR>,
          coin: Coin<DNR>) {
              coin::burn(treasury_cap, coin);
      }
        
      public entry fun split_coin(
          self: &mut Coin<DNR>,
          amount: u64,
          recipient: address,
          ctx: & mut TxContext) {
              let new_split_coin = coin::split(self, amount, ctx);
              transfer::public_transfer(new_split_coin, recipient);
      }

It compiles just fine and works as a peach when tested on SuiScan. However, when I try to join the two splitted coins, with the snippet below,

      public entry fun join_coin(
          self: &mut Coin<DNR>,
          c: Coin<DNR>,
          recipient: address) {
              let new_join_coin = coin::join(self, c);
              transfer::public_transfer(new_join_coin, recipient);
      }

I get a compilation error as below,

error[E04005]: expected a single type
┌─ ./sources/try_nft.move:61:13

61 │ let new_join_coin = coin::join(self, c);
│ ^^^^^^^^^^^^^ Invalid type for local

┌─ /home/plyoteo/.move/https___github_com_MystenLabs_sui_git_2c930c25f8d3/crates/sui-framework/packages/sui-framework/sources/coin.move:165:18

165 │ public entry fun join(self: &mut Coin, c: Coin) {
│ ---- Expected a single type, but found expression list type: ‘()’

Any ideas? I think that I am using the join fun exactly as outlined in sui::coin::join documentation.

I thank you in advance

1 Like

Understanding the Coin Join and Transfer in SUI Move

The Problem

The original implementation had two key issues:

  1. Misunderstanding of coin::join’s return value (it returns ())
  2. Attempting to transfer a mutable reference (&mut Coin) which violates Sui’s ownership model

Why the Original Code Failed

public entry fun join_coin(
    self: &mut Coin<DNR>,
    c: Coin<DNR>,
    recipient: address
) {
    let new_join_coin = coin::join(self, c); // Problem 1: No return value
    transfer::public_transfer(new_join_coin, recipient); // Problem 2: Can't transfer reference
}

Key problems:

  1. coin::join modifies self in-place and doesn’t return a new coin
  2. public_transfer requires an owned object with key + store abilities, not a reference

Corrected Solutions

Option 1: Simple Join (Recommended)

public entry fun join_coins(
    coin1: &mut Coin<DNR>, // First coin to merge into
    coin2: Coin<DNR>       // Second coin to be consumed
) {
    coin::join(coin1, coin2);
}

Option 2: Join and Transfer

public entry fun join_and_transfer(
    treasury_cap: &mut TreasuryCap<DNR>, // Needed for potential re-minting
    coin1: Coin<DNR>,                    // First coin (will be merged)
    coin2: Coin<DNR>,                    // Second coin (will be consumed)
    recipient: address,
    ctx: &mut TxContext
) {
    // Merge coins
    let mut merged_coin = coin1;
    coin::join(&mut merged_coin, coin2);
    
    // Transfer the merged coin
    transfer::public_transfer(merged_coin, recipient);
}

Key Corrections and Insights

  1. Ownership Matters:

    • You can only transfer owned objects, not references
    • Coin<DNR> has both key and store abilities by default
  2. Proper Entry Function Design:

    • Entry functions should typically take owned objects
    • If you need to modify an object, extract it first then modify

Important Notes

  1. The TreasuryCap parameter in Option 2 is optional but recommended if you might need to mint/burn later

  2. Always consider:

    • What objects need to be owned vs. referenced
    • Which objects will be consumed (like coin2 in the merge)
    • Whether you need to preserve the original object structure
  3. Remember that after transfer:

    • The recipient gets full ownership
    • The original object becomes inaccessible to the sender

DevDanny,

Thank you so much, both for the two solutions but mostly for the deep insights into the inner workings of move. I am a 63 yr old programming hobbyist coming from a Python/NodeJS background, so statically typed, compiled languages give me a bit of the chills. After working out the bare essential of Rust I took a head-first plunge into Move. Your response is probably a good reason I wont give up trying.

Thank you again,
Theodoros

Στις Παρ 1 Αυγ 2025, 19:52 ο χρήστης DevDanny via Sui Developer Forum <notifications@sui.discoursemail.com> έγραψε:

1 Like