Get the CoinMetadata from a coin object

I am building a smart contract to allow a member to receive different types of coins using dynamic_object_field with the coin metadata as the name and the coin value as the value.

// Make sure metadata_id: ID is the id of the CoinMetadata from the client
// eq: metadata: &CoinMatadata and metadata_id: object::id(metadata)
// It is easy to mock an ID object than constructing a real CoinMetadata object
public fun receive_coin<T>(member: &mut Member, coin: Coin<T>, metadata_id: ID, ctx: &TxContext){
    let member_id = object::id(member);
    let amount = coin::value(&coin);
    let received_at = tx_context::epoch(ctx);

    let from_amount = if(dynamic_object_field::exists_<ID>(&member.id, metadata_id)) {
      let existing_coin = dynamic_object_field::borrow_mut<ID, Coin<T>>(&mut member.id, metadata_id);
      coin::join(existing_coin, coin);
      coin::value(existing_coin)
    }else {
      dynamic_object_field::add<ID, Coin<T>>(&mut member.id, metadata_id, coin);
      0u64
    };

    let to_amount = from_amount + amount;
    let received_coin_event = MemberReceivedCoinEvent {
      metadata_id,
      member_id,
      amount,
      from_amount,
      to_amount,
      received_at
    };

    event::emit(received_coin_event);
  }

The problem I am facing is I have to pass the metadata_id: ID ( or supposedly metadata: &CoinMetadata ) as an argument in the function signature. I decide to use metadata_id: ID instead of metadata: &CoinMetadata because it is easier to write a test but passing metadata_id: ID still requires some validation to make sure that the metadata_id is really the ID of the CoinMetaData.

My question is, is it possible to get metadata from the coin object at the API level on-chain? The coin and metadata are highly correlated, passing the two arguments in a fun signature is very painful.

3 Likes

My question is, is it possible to get metadata from the coin object at the API level on-chain? The coin and metadata are highly correlated, passing the two arguments in a fun signature is very painful.

This is not possible. However, I think you can accomplish your goal (allow a member to receive different types of coins, and aggregate coins of the same type) by using the type of the coin instead of the metadata object ID as your dynamic field key, like this:

use std::type_name;

public fun receive_coin<T>(member: &mut Member, coin: Coin<T>, ctx: &TxContext) {
    let member_id = object::id(member);
    let amount = coin::value(&coin);
    let received_at = tx_context::epoch(ctx);
    let coin_type = type_name::get<T>();

    let from_amount = if (dynamic_object_field::exists_(&member.id, coin_type)) {
      let existing_coin = dynamic_object_field::borrow_mut(&mut member.id, coin_type);
      coin::join(existing_coin, coin);
      coin::value(existing_coin)
    } else {
      dynamic_object_field::add(&mut member.id, coin_type, coin);
      0u64
    };
...
4 Likes

Thank you @shb. This solution is elegant.

3 Likes

In my use cases, I need the CoinMetadata name to match the string in one of my structs.
From sui/crates/sui-framework/packages/move-stdlib/sources/type_name.move at main · MystenLabs/sui · GitHub there is no way to get just the name of the type: eg: 00000000000000000000000000000000::loyaltychain::loy::LOY => LOY. I leave some snippets here as ref in case the community need to do the same.

sources/modules/util.move

public fun get_name_as_bytes<T>(): vector<u8>{
    let name: std::ascii::String = type_name::into_string(type_name::get<T>());
    let value = std::ascii::into_bytes(name);
    get_type_from_bytes(value)
  }

  public fun get_name_as_string<T>(): String{
    let name: vector<u8> = get_name_as_bytes<T>();
    string::utf8(name)
  }

  // let value = b"00000000000000000000000000000001::loyaltychain::loy::LOY";
  // let expected = b"LOY";
  public fun get_type_from_bytes(name: vector<u8>): vector<u8> {
    // https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/packages/move-stdlib/sources/type_name.move#L11
    let ascii_colon = 58u8;
    let count = std::vector::length(&name);

    let i = count;
    let type_name = b"";

    while(i > 0 ) {
      let char = std::vector::borrow(&name, i-1);
      if (*char == ascii_colon) {
        break
      };

      std::vector::insert(&mut type_name, *char, 0);
      i = i - 1;
    };
    type_name
  }

and the test in tests/modules/util_test.move

#[test]
  public fun test_get_name_as_bytes(){
    use loyaltychain::util;
    use loyaltychain::loy::{LOY};

    let expected = b"LOY";
    let result = util::get_name_as_bytes<LOY>();

    assert!(result == expected, 0);
  }

  #[test]
  public fun test_get_name_as_string(){
    use loyaltychain::util;
    use loyaltychain::loy::{LOY};

    let expected = string::utf8(b"LOY");
    let result = util::get_name_as_string<LOY>();

    assert!(result == expected, 0);
  }

  #[test]
 public fun test_get_type_from_bytes(){
    use loyaltychain::util;

    //It returns LOY without a fully qualified name
    {
      let expected = b"LOY";
      let value = b"00000000000000000000000000000000::loyaltychain::loy::LOY";

      let result = util::get_type_from_bytes(value);
      std::debug::print(&string::utf8(result));
      assert!(result == expected, 0);
    };

    //It returns LOY
    {
      let expected = b"LOY";
      let value = b"LOY";

      let result = util::get_type_from_bytes(value);
      std::debug::print(&string::utf8(result));
      assert!(result == expected, 0);
    };

  }
2 Likes

Thanks! We should probably add these sorts of functions to TypeName

3 Likes

@shb that would be really great!

1 Like