// SPDX-License-Identifier: Apache 2 /// This module implements serialization and deserialization for asset metadata, /// which is a specific Wormhole message payload for Token Bridge. module token_bridge::asset_meta { use std::string::{Self, String}; use std::vector::{Self}; use sui::coin::{Self, CoinMetadata}; use wormhole::bytes::{Self}; use wormhole::bytes32::{Self}; use wormhole::external_address::{Self, ExternalAddress}; use wormhole::cursor::{Self}; use wormhole::state::{chain_id}; use token_bridge::native_asset::{Self}; friend token_bridge::attest_token; friend token_bridge::create_wrapped; friend token_bridge::wrapped_asset; /// Message payload is not `AssetMeta`. const E_INVALID_PAYLOAD: u64 = 0; /// Message identifier. const PAYLOAD_ID: u8 = 2; /// Container that warehouses asset metadata information. This struct is /// used only by `attest_token` and `create_wrapped` modules. struct AssetMeta { /// Address of the token. token_address: ExternalAddress, /// Chain ID of the token. token_chain: u16, /// Number of decimals of the token. native_decimals: u8, /// Symbol of the token (UTF-8). /// TODO(csongor): maybe turn these into String32s? symbol: String, /// Name of the token (UTF-8). name: String, } public(friend) fun from_metadata(metadata: &CoinMetadata): AssetMeta { AssetMeta { token_address: native_asset::canonical_address(metadata), token_chain: chain_id(), native_decimals: coin::get_decimals(metadata), symbol: string::from_ascii(coin::get_symbol(metadata)), name: coin::get_name(metadata) } } #[test_only] public fun from_metadata_test_only(metadata: &CoinMetadata): AssetMeta { from_metadata(metadata) } public(friend) fun unpack( meta: AssetMeta ): ( ExternalAddress, u16, u8, String, String ) { let AssetMeta { token_address, token_chain, native_decimals, symbol, name } = meta; ( token_address, token_chain, native_decimals, symbol, name ) } #[test_only] public fun unpack_test_only( meta: AssetMeta ): ( ExternalAddress, u16, u8, String, String ) { unpack(meta) } public fun token_chain(self: &AssetMeta): u16 { self.token_chain } public fun token_address(self: &AssetMeta): ExternalAddress { self.token_address } public(friend) fun serialize(meta: AssetMeta): vector { let ( token_address, token_chain, native_decimals, symbol, name ) = unpack(meta); let buf = vector::empty(); bytes::push_u8(&mut buf, PAYLOAD_ID); vector::append(&mut buf, external_address::to_bytes(token_address)); bytes::push_u16_be(&mut buf, token_chain); bytes::push_u8(&mut buf, native_decimals); vector::append( &mut buf, bytes32::to_bytes(bytes32::from_utf8(symbol)) ); vector::append( &mut buf, bytes32::to_bytes(bytes32::from_utf8(name)) ); buf } #[test_only] public fun serialize_test_only(meta: AssetMeta): vector { serialize(meta) } public(friend) fun deserialize(buf: vector): AssetMeta { let cur = cursor::new(buf); assert!(bytes::take_u8(&mut cur) == PAYLOAD_ID, E_INVALID_PAYLOAD); let token_address = external_address::take_bytes(&mut cur); let token_chain = bytes::take_u16_be(&mut cur); let native_decimals = bytes::take_u8(&mut cur); let symbol = bytes32::to_utf8(bytes32::take_bytes(&mut cur)); let name = bytes32::to_utf8(bytes32::take_bytes(&mut cur)); cursor::destroy_empty(cur); AssetMeta { token_address, token_chain, native_decimals, symbol, name } } #[test_only] public fun deserialize_test_only(buf: vector): AssetMeta { deserialize(buf) } #[test_only] public fun new( token_address: ExternalAddress, token_chain: u16, native_decimals: u8, symbol: String, name: String, ): AssetMeta { AssetMeta { token_address, token_chain, native_decimals, symbol, name } } #[test_only] public fun native_decimals(self: &AssetMeta): u8 { self.native_decimals } #[test_only] public fun symbol(self: &AssetMeta): String { self.symbol } #[test_only] public fun name(self: &AssetMeta): String { self.name } #[test_only] public fun destroy(token_meta: AssetMeta) { unpack(token_meta); } #[test_only] public fun payload_id(): u8 { PAYLOAD_ID } } #[test_only] module token_bridge::asset_meta_tests { use std::string::{Self}; use wormhole::external_address::{Self}; use wormhole::vaa::{Self}; use token_bridge::asset_meta::{Self}; #[test] fun test_serialize_deserialize() { let token_address = external_address::from_address(@0x1122); let symbol = string::utf8(b"a creative symbol"); let name = string::utf8(b"a creative name"); let asset_meta = asset_meta::new( token_address, //token address 3, // token chain 4, //native decimals symbol, // symbol name, // name ); // Serialize and deserialize TransferWithPayload object. let se = asset_meta::serialize_test_only(asset_meta); let de = asset_meta::deserialize_test_only(se); // Test that the object fields are unchanged. assert!(asset_meta::token_chain(&de) == 3, 0); assert!(asset_meta::token_address(&de) == token_address, 0); assert!(asset_meta::native_decimals(&de) == 4, 0); assert!(asset_meta::symbol(&de) == symbol, 0); assert!(asset_meta::name(&de) == name, 0); // Clean up. asset_meta::destroy(de); } #[test] fun test_create_wrapped_12() { use token_bridge::dummy_message::{encoded_asset_meta_vaa_foreign_12}; let payload = vaa::peel_payload_from_vaa(&encoded_asset_meta_vaa_foreign_12()); let token_meta = asset_meta::deserialize_test_only(payload); let serialized = asset_meta::serialize_test_only(token_meta); assert!(payload == serialized, 0); } }