diff --git a/.changeset/friendly-phones-laugh.md b/.changeset/friendly-phones-laugh.md new file mode 100644 index 0000000000000..e6e299df47678 --- /dev/null +++ b/.changeset/friendly-phones-laugh.md @@ -0,0 +1,5 @@ +--- +"@mysten/sui.js": patch +--- + +Replace `getCoinDenominationInfo` with `getCoinMetadata` diff --git a/apps/explorer/src/hooks/useFormatCoin.ts b/apps/explorer/src/hooks/useFormatCoin.ts index 6a157a4ce9006..6f4661c5728cb 100644 --- a/apps/explorer/src/hooks/useFormatCoin.ts +++ b/apps/explorer/src/hooks/useFormatCoin.ts @@ -66,7 +66,7 @@ export function useCoinDecimals(coinType?: string | null) { ); } - return rpc.getCoinDenominationInfo(coinType); + return rpc.getCoinMetadata(coinType); }, { // This is currently expected to fail for non-SUI tokens, so disable retries: @@ -80,7 +80,7 @@ export function useCoinDecimals(coinType?: string | null) { } ); - return [queryResult.data?.decimalNumber || 0, queryResult] as const; + return [queryResult.data?.decimals || 0, queryResult] as const; } const numberFormatter = new Intl.NumberFormat('en', { diff --git a/apps/wallet/src/ui/app/hooks/useFormatCoin.ts b/apps/wallet/src/ui/app/hooks/useFormatCoin.ts index 594bc21ea082d..9c9239ca02d83 100644 --- a/apps/wallet/src/ui/app/hooks/useFormatCoin.ts +++ b/apps/wallet/src/ui/app/hooks/useFormatCoin.ts @@ -55,7 +55,7 @@ export function useCoinDecimals(coinType?: string | null) { ); } - return api.instance.fullNode.getCoinDenominationInfo(coinType); + return api.instance.fullNode.getCoinMetadata(coinType); }, { // This is currently expected to fail for non-SUI tokens, so disable retries: @@ -69,7 +69,7 @@ export function useCoinDecimals(coinType?: string | null) { } ); - return [queryResult.data?.decimalNumber || 0, queryResult] as const; + return [queryResult.data?.decimals || 0, queryResult] as const; } // TODO: This handles undefined values to make it easier to integrate with the reset of the app as it is diff --git a/crates/sui-core/tests/staged/sui.yaml b/crates/sui-core/tests/staged/sui.yaml index 72972e7d17782..9b88ae92b735c 100644 --- a/crates/sui-core/tests/staged/sui.yaml +++ b/crates/sui-core/tests/staged/sui.yaml @@ -169,45 +169,47 @@ ExecutionFailureStatus: 10: InvalidCoinObject: UNIT 11: - EmptyInputCoins: UNIT + InvalidCoinMetadataObject: UNIT 12: - EmptyRecipients: UNIT + EmptyInputCoins: UNIT 13: - RecipientsAmountsArityMismatch: UNIT + EmptyRecipients: UNIT 14: - InsufficientBalance: UNIT + RecipientsAmountsArityMismatch: UNIT 15: - CoinTypeMismatch: UNIT + InsufficientBalance: UNIT 16: - NonEntryFunctionInvoked: UNIT + CoinTypeMismatch: UNIT 17: - EntryTypeArityMismatch: UNIT + NonEntryFunctionInvoked: UNIT 18: + EntryTypeArityMismatch: UNIT + 19: EntryArgumentError: NEWTYPE: TYPENAME: EntryArgumentError - 19: + 20: EntryTypeArgumentError: NEWTYPE: TYPENAME: EntryTypeArgumentError - 20: + 21: CircularObjectOwnership: NEWTYPE: TYPENAME: CircularObjectOwnership - 21: + 22: InvalidChildObjectArgument: NEWTYPE: TYPENAME: InvalidChildObjectArgument - 22: + 23: InvalidSharedByValue: NEWTYPE: TYPENAME: InvalidSharedByValue - 23: + 24: TooManyChildObjects: STRUCT: - object: TYPENAME: ObjectID - 24: + 25: InvalidParentDeletion: STRUCT: - parent: @@ -215,32 +217,32 @@ ExecutionFailureStatus: - kind: OPTION: TYPENAME: DeleteKind - 25: + 26: InvalidParentFreezing: STRUCT: - parent: TYPENAME: ObjectID - 26: - PublishErrorEmptyPackage: UNIT 27: - PublishErrorNonZeroAddress: UNIT + PublishErrorEmptyPackage: UNIT 28: - PublishErrorDuplicateModule: UNIT + PublishErrorNonZeroAddress: UNIT 29: - SuiMoveVerificationError: UNIT + PublishErrorDuplicateModule: UNIT 30: + SuiMoveVerificationError: UNIT + 31: MovePrimitiveRuntimeError: NEWTYPE: OPTION: TYPENAME: MoveLocation - 31: + 32: MoveAbort: TUPLE: - TYPENAME: MoveLocation - U64 - 32: - VMVerificationOrDeserializationError: UNIT 33: + VMVerificationOrDeserializationError: UNIT + 34: VMInvariantViolation: UNIT ExecutionStatus: ENUM: diff --git a/crates/sui-json-rpc-types/src/lib.rs b/crates/sui-json-rpc-types/src/lib.rs index 7f2c50716fcb5..f3c1e71393461 100644 --- a/crates/sui-json-rpc-types/src/lib.rs +++ b/crates/sui-json-rpc-types/src/lib.rs @@ -27,6 +27,7 @@ use serde::Deserialize; use serde::Serialize; use serde_json::Value; use serde_with::serde_as; +use sui_types::coin::CoinMetadata; use tracing::warn; use fastcrypto::encoding::{Base64, Encoding}; @@ -412,6 +413,46 @@ impl SuiExecuteTransactionResponse { } } +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct SuiCoinMetadata { + /// Number of decimal places the coin uses. + pub decimals: u8, + /// Name for the token + pub name: String, + /// Symbol for the token + pub symbol: String, + /// Description of the token + pub description: String, + /// URL for the token logo + pub icon_url: Option, + /// Object id for the CoinMetadata object + pub id: Option, +} + +impl TryFrom for SuiCoinMetadata { + type Error = SuiError; + fn try_from(object: Object) -> Result { + let metadata: CoinMetadata = object.try_into()?; + let CoinMetadata { + decimals, + name, + symbol, + description, + icon_url, + id, + } = metadata; + Ok(Self { + id: Some(*id.object_id()), + decimals, + name, + symbol, + description, + icon_url, + }) + } +} + #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct SuiParsedSplitCoinResponse { diff --git a/crates/sui-json-rpc/src/api.rs b/crates/sui-json-rpc/src/api.rs index 187b9410398b5..9c1be669ee3c9 100644 --- a/crates/sui-json-rpc/src/api.rs +++ b/crates/sui-json-rpc/src/api.rs @@ -11,8 +11,8 @@ use fastcrypto::encoding::Base64; use sui_json::SuiJsonValue; use sui_json_rpc_types::{ EventPage, GetObjectDataResponse, GetPastObjectDataResponse, GetRawObjectDataResponse, - MoveFunctionArgType, RPCTransactionRequestParams, SuiEventEnvelope, SuiEventFilter, - SuiExecuteTransactionResponse, SuiGasCostSummary, SuiMoveNormalizedFunction, + MoveFunctionArgType, RPCTransactionRequestParams, SuiCoinMetadata, SuiEventEnvelope, + SuiEventFilter, SuiExecuteTransactionResponse, SuiGasCostSummary, SuiMoveNormalizedFunction, SuiMoveNormalizedModule, SuiMoveNormalizedStruct, SuiObjectInfo, SuiTransactionEffects, SuiTransactionFilter, SuiTransactionResponse, SuiTypeTag, TransactionBytes, TransactionsPage, }; @@ -118,6 +118,14 @@ pub trait RpcFullNodeReadApi { #[method(name = "dryRunTransaction")] async fn dry_run_transaction(&self, tx_bytes: Base64) -> RpcResult; + /// Return metadata(e.g., symbol, decimals) for a coin + #[method(name = "getCoinMetadata")] + async fn get_coin_metadata( + &self, + /// fully qualified type names for the coin (e.g., 0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC) + coin_type: String, + ) -> RpcResult; + /// Return the argument types of a Move function, /// based on normalized Type. #[method(name = "getMoveFunctionArgTypes")] diff --git a/crates/sui-json-rpc/src/read_api.rs b/crates/sui-json-rpc/src/read_api.rs index cacb1b391acb9..ef446f3446de5 100644 --- a/crates/sui-json-rpc/src/read_api.rs +++ b/crates/sui-json-rpc/src/read_api.rs @@ -7,16 +7,20 @@ use jsonrpsee::core::RpcResult; use jsonrpsee_core::server::rpc_module::RpcModule; use move_binary_format::normalized::{Module as NormalizedModule, Type}; use move_core_types::identifier::Identifier; +use move_core_types::language_storage::StructTag; use std::collections::BTreeMap; use std::sync::Arc; +use sui_types::coin::CoinMetadata; +use sui_types::event::Event; +use sui_types::gas_coin::GAS; use tap::TapFallible; use fastcrypto::encoding::Base64; use sui_core::authority::AuthorityState; use sui_json_rpc_types::{ GetObjectDataResponse, GetPastObjectDataResponse, MoveFunctionArgType, ObjectValueKind, Page, - SuiMoveNormalizedFunction, SuiMoveNormalizedModule, SuiMoveNormalizedStruct, SuiObjectInfo, - SuiTransactionEffects, SuiTransactionResponse, TransactionsPage, + SuiCoinMetadata, SuiMoveNormalizedFunction, SuiMoveNormalizedModule, SuiMoveNormalizedStruct, + SuiObjectInfo, SuiTransactionEffects, SuiTransactionResponse, TransactionsPage, }; use sui_open_rpc::Module; use sui_types::base_types::SequenceNumber; @@ -151,6 +155,78 @@ impl RpcFullNodeReadApiServer for FullNodeApi { Ok(self.state.dry_exec_transaction(tx_data, txn_digest).await?) } + async fn get_coin_metadata(&self, coin_type: String) -> RpcResult { + let coin_struct = coin_type.parse::().map_err(|e| anyhow!("{e}"))?; + if GAS::is_gas(&coin_struct) { + // TODO: We need to special case for `CoinMetadata<0x2::sui::SUI> because `get_transaction` + // will fail for genesis transaction. However, instead of hardcoding the values here, We + // can store the object id for `CoinMetadata<0x2::sui::SUI>` in the Sui System object + return Ok(SuiCoinMetadata { + id: None, + decimals: 9, + symbol: "SUI".to_string(), + name: "Sui".to_string(), + description: "".to_string(), + icon_url: None, + }); + } + let publish_txn_digest = self + .state + .get_object_read(&coin_struct.address.into()) + .await + .map_err(|e| anyhow!("{e}"))? + .into_object() + .map_err(|e| anyhow!("{e}"))? + .previous_transaction; + let (_, effects) = self.state.get_transaction(publish_txn_digest).await?; + let event = effects + .events + .into_iter() + .find(|e| { + if let Event::NewObject { object_type, .. } = e { + return object_type.parse::().map_or(false, |tag| { + CoinMetadata::is_coin_metadata(&tag) + && tag.type_params.len() == 1 + && tag.type_params[0].to_canonical_string() + == coin_struct.to_canonical_string() + }); + } + false + }) + .ok_or(0) + .map_err(|_| anyhow!("No NewObject event was emitted for CoinMetaData"))?; + + let metadata_object_id = event + .object_id() + .ok_or(0) + .map_err(|_| anyhow!("No object id is found in NewObject event"))?; + + Ok(self + .state + .get_object_read(&metadata_object_id) + .await + .map_err(|e| { + debug!(?metadata_object_id, "Failed to get object: {:?}", e); + anyhow!("{e}") + })? + .into_object() + .map_err(|e| { + debug!( + ?metadata_object_id, + "Failed to convert ObjectRead to Object: {:?}", e + ); + anyhow!("{e}") + })? + .try_into() + .map_err(|e| { + debug!( + ?metadata_object_id, + "Failed to convert object to CoinMetadata: {:?}", e + ); + anyhow!("{e}") + })?) + } + async fn get_normalized_move_modules_by_package( &self, package: ObjectID, diff --git a/crates/sui-open-rpc/spec/openrpc.json b/crates/sui-open-rpc/spec/openrpc.json index f19644b7ba438..0101881c26994 100644 --- a/crates/sui-open-rpc/spec/openrpc.json +++ b/crates/sui-open-rpc/spec/openrpc.json @@ -355,6 +355,32 @@ } ] }, + { + "name": "sui_getCoinMetadata", + "tags": [ + { + "name": "Full Node API" + } + ], + "description": "Return metadata(e.g., symbol, decimals) for a coin", + "params": [ + { + "name": "coin_type", + "description": "fully qualified type names for the coin (e.g., 0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "SuiCoinMetadata", + "required": true, + "schema": { + "$ref": "#/components/schemas/SuiCoinMetadata" + } + } + }, { "name": "sui_getCommitteeInfo", "tags": [ @@ -3799,6 +3825,53 @@ } } }, + "SuiCoinMetadata": { + "type": "object", + "required": [ + "decimals", + "description", + "name", + "symbol" + ], + "properties": { + "decimals": { + "description": "Number of decimal places the coin uses.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "description": { + "description": "Description of the token", + "type": "string" + }, + "iconUrl": { + "description": "URL for the token logo", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Object id for the CoinMetadata object", + "anyOf": [ + { + "$ref": "#/components/schemas/ObjectID" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "Name for the token", + "type": "string" + }, + "symbol": { + "description": "Symbol for the token", + "type": "string" + } + } + }, "SuiExecuteTransactionResponse": { "oneOf": [ { diff --git a/crates/sui-types/src/coin.rs b/crates/sui-types/src/coin.rs index d4e7346af5e8a..649d1673950b3 100644 --- a/crates/sui-types/src/coin.rs +++ b/crates/sui-types/src/coin.rs @@ -9,7 +9,6 @@ use move_core_types::{ }; use serde::{Deserialize, Serialize}; -use crate::base_types::TransactionDigest; use crate::object::{MoveObject, Owner, OBJECT_START_VERSION}; use crate::storage::{DeleteKind, SingleTxContext, WriteKind}; use crate::temporary_store::TemporaryStore; @@ -18,6 +17,7 @@ use crate::{ error::{ExecutionError, ExecutionErrorKind}, object::{Data, Object}, }; +use crate::{base_types::TransactionDigest, error::SuiError}; use crate::{ base_types::{ObjectID, SuiAddress}, id::UID, @@ -27,6 +27,7 @@ use schemars::JsonSchema; pub const COIN_MODULE_NAME: &IdentStr = ident_str!("coin"); pub const COIN_STRUCT_NAME: &IdentStr = ident_str!("Coin"); +pub const COIN_METADATA_STRUCT_NAME: &IdentStr = ident_str!("CoinMetadata"); pub const PAY_MODULE_NAME: &IdentStr = ident_str!("pay"); pub const PAY_JOIN_FUNC_NAME: &IdentStr = ident_str!("join"); @@ -59,7 +60,8 @@ impl Coin { /// Is this other StructTag representing a Coin? pub fn is_coin(other: &StructTag) -> bool { - other.module.as_ident_str() == COIN_MODULE_NAME + other.address == SUI_FRAMEWORK_ADDRESS + && other.module.as_ident_str() == COIN_MODULE_NAME && other.name.as_ident_str() == COIN_STRUCT_NAME } @@ -196,3 +198,53 @@ pub fn update_input_coins( ) } } + +// Rust version of the Move sui::coin::CoinMetadata type +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)] +pub struct CoinMetadata { + pub id: UID, + /// Number of decimal places the coin uses. + pub decimals: u8, + /// Name for the token + pub name: String, + /// Symbol for the token + pub symbol: String, + /// Description of the token + pub description: String, + /// URL for the token logo + pub icon_url: Option, +} + +impl CoinMetadata { + /// Is this other StructTag representing a CoinMetadata? + pub fn is_coin_metadata(other: &StructTag) -> bool { + other.address == SUI_FRAMEWORK_ADDRESS + && other.module.as_ident_str() == COIN_MODULE_NAME + && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME + } + + /// Create a coin from BCS bytes + pub fn from_bcs_bytes(content: &[u8]) -> Result { + bcs::from_bytes(content).map_err(|err| SuiError::TypeError { + error: format!("Unable to deserialize CoinMetadata object: {:?}", err), + }) + } +} + +impl TryFrom for CoinMetadata { + type Error = SuiError; + fn try_from(object: Object) -> Result { + match &object.data { + Data::Move(o) => { + if CoinMetadata::is_coin_metadata(&o.type_) { + return CoinMetadata::from_bcs_bytes(o.contents()); + } + } + Data::Package(_) => {} + } + + Err(SuiError::TypeError { + error: format!("Object type is not a CoinMetadata: {:?}", object), + }) + } +} diff --git a/crates/sui-types/src/gas_coin.rs b/crates/sui-types/src/gas_coin.rs index a5d981c379f91..3ee441b489d14 100644 --- a/crates/sui-types/src/gas_coin.rs +++ b/crates/sui-types/src/gas_coin.rs @@ -37,6 +37,10 @@ impl GAS { pub fn type_tag() -> TypeTag { TypeTag::Struct(Self::type_()) } + + pub fn is_gas(other: &StructTag) -> bool { + &Self::type_() == other + } } /// Rust version of the Move sui::coin::Coin type diff --git a/crates/sui-types/src/messages.rs b/crates/sui-types/src/messages.rs index e8c34a2311068..dd7991b6b6265 100644 --- a/crates/sui-types/src/messages.rs +++ b/crates/sui-types/src/messages.rs @@ -1240,6 +1240,7 @@ pub enum ExecutionFailureStatus { InvalidTransferSui, InvalidTransferSuiInsufficientBalance, InvalidCoinObject, + InvalidCoinMetadataObject, // // Pay errors @@ -1523,6 +1524,9 @@ impl Display for ExecutionFailureStatus { ExecutionFailureStatus::VMInvariantViolation => { write!(f, "MOVE VM INVARIANT VIOLATION.") } + ExecutionFailureStatus::InvalidCoinMetadataObject => { + write!(f, "Invalid CoinMetadata type") + } } } } diff --git a/sdk/typescript/src/providers/json-rpc-provider.ts b/sdk/typescript/src/providers/json-rpc-provider.ts index c392b21cf8f08..e38e8839f7991 100644 --- a/sdk/typescript/src/providers/json-rpc-provider.ts +++ b/sdk/typescript/src/providers/json-rpc-provider.ts @@ -17,11 +17,11 @@ import { isSuiMoveNormalizedStruct, isSuiTransactionResponse, isTransactionEffects, + isCoinMetadata, } from '../types/index.guard'; import { Coin, ExecuteTransactionRequestType, - CoinDenominationInfoResponse, GatewayTxSeqNumber, GetObjectDataResponse, getObjectReference, @@ -44,7 +44,6 @@ import { TransactionDigest, TransactionQuery, SUI_TYPE_ARG, - normalizeSuiAddress, RpcApiVersion, parseVersionFromString, EventQuery, @@ -53,6 +52,9 @@ import { FaucetResponse, Order, TransactionEffects, + CoinMetadata, + versionToString, + normalizeSuiAddress, } from '../types'; import { SignatureScheme } from '../cryptography/publickey'; import { @@ -62,6 +64,7 @@ import { } from '../rpc/websocket-client'; import { ApiEndpoints, Network, NETWORK_TO_API } from '../utils/api-endpoints'; import { requestSuiFromFaucet } from '../rpc/faucet-client'; +import { lt } from '@suchipi/femver'; const isNumber = (val: any): val is number => typeof val === 'number'; const isAny = (_val: any): _val is any => true; @@ -167,6 +170,41 @@ export class JsonRpcProvider extends Provider { return undefined; } + async getCoinMetadata(coinType: string): Promise { + try { + const version = await this.getRpcApiVersion(); + // TODO: clean up after 0.17.0 is deployed on both DevNet and TestNet + if (version && lt(versionToString(version), '0.17.0')) { + const [packageId, module, symbol] = coinType.split('::'); + if ( + normalizeSuiAddress(packageId) !== normalizeSuiAddress('0x2') || + module != 'sui' || + symbol !== 'SUI' + ) { + throw new Error( + 'only SUI coin is supported in getCoinMetadata for RPC version priort to 0.17.0.' + ); + } + return { + decimals: 9, + name: 'Sui', + symbol: 'SUI', + description: '', + iconUrl: null, + id: null, + }; + } + return await this.client.requestWithType( + 'sui_getCoinMetadata', + [coinType], + isCoinMetadata, + this.options.skipDataValidation + ); + } catch (err) { + throw new Error(`Error fetching CoinMetadata for ${coinType}: ${err}`); + } + } + async requestSuiFromFaucet( recipient: SuiAddress, httpHeaders?: HttpHeaders @@ -294,25 +332,6 @@ export class JsonRpcProvider extends Provider { return objects.filter((obj: SuiObjectInfo) => Coin.isSUI(obj)); } - getCoinDenominationInfo(coinType: string): CoinDenominationInfoResponse { - const [packageId, module, symbol] = coinType.split('::'); - if ( - normalizeSuiAddress(packageId) !== normalizeSuiAddress('0x2') || - module != 'sui' || - symbol !== 'SUI' - ) { - throw new Error( - 'only SUI coin is supported in getCoinDenominationInfo for now.' - ); - } - - return { - coinType: coinType, - basicUnit: 'MIST', - decimalNumber: 9, - }; - } - async getCoinBalancesOwnedByAddress( address: string, typeArg?: string @@ -577,21 +596,21 @@ export class JsonRpcProvider extends Provider { // Events async getEvents( - query: EventQuery, - cursor: EventId | null, - limit: number | null, - order: Order = 'descending' + query: EventQuery, + cursor: EventId | null, + limit: number | null, + order: Order = 'descending' ): Promise { try { return await this.client.requestWithType( - 'sui_getEvents', - [query, cursor, limit, order === 'descending'], - isPaginatedEvents, - this.options.skipDataValidation + 'sui_getEvents', + [query, cursor, limit, order === 'descending'], + isPaginatedEvents, + this.options.skipDataValidation ); } catch (err) { throw new Error( - `Error getting events for query: ${err} for query ${query}` + `Error getting events for query: ${err} for query ${query}` ); } } @@ -617,7 +636,9 @@ export class JsonRpcProvider extends Provider { ); return resp; } catch (err) { - throw new Error(`Error dry running transaction with request type: ${err}`); + throw new Error( + `Error dry running transaction with request type: ${err}` + ); } } } diff --git a/sdk/typescript/src/providers/provider.ts b/sdk/typescript/src/providers/provider.ts index e58989cf73be9..9bfdbb5b0bbb4 100644 --- a/sdk/typescript/src/providers/provider.ts +++ b/sdk/typescript/src/providers/provider.ts @@ -4,7 +4,6 @@ import { SignatureScheme } from '../cryptography/publickey'; import { HttpHeaders } from '../rpc/client'; import { - CoinDenominationInfoResponse, GetObjectDataResponse, SuiObjectInfo, GatewayTxSeqNumber, @@ -32,6 +31,7 @@ import { FaucetResponse, Order, TransactionEffects, + CoinMetadata, } from '../types'; /////////////////////////////// @@ -46,6 +46,15 @@ export abstract class Provider { */ abstract getRpcApiVersion(): Promise; + /** + * Fetch CoinMetadata for a given coin type + * + * @param coinType fully qualified type names for the coin (e.g., + * 0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC) + * + */ + abstract getCoinMetadata(coinType: string): Promise; + // Faucet /** * Request gas tokens from a faucet server @@ -113,25 +122,6 @@ export abstract class Provider { exclude: ObjectId[] ): Promise; - /** - * Method to look up denomination of a specific type of coin. - * TODO: now only SUI coins are supported, will scale to other types - * based on their definitions in Move. - * - * @param coin_type coin type, e.g., '0x2::sui::SUI' - * @return denomination info of the coin including, - * coin type, the same as input coin type - * basic unit, the min unit of the coin, e.g., MIST; - * canonical unit, the commonly used unit, e.g., SUI; - * denomination, the value of 1 canonical over 1 basic unit, - * for example 1_000_000_000 = 1 SUI / 1 MIST; - * decimal number, the number of zeros in the denomination, - * e.g., 9 here for SUI coin. - */ - abstract getCoinDenominationInfo( - coin_type: string - ): CoinDenominationInfoResponse; - /** * Get details about an object */ @@ -235,10 +225,10 @@ export abstract class Provider { * @param order - event query ordering */ abstract getEvents( - query: EventQuery, - cursor: EventId | null, - limit: number | null, - order: Order, + query: EventQuery, + cursor: EventId | null, + limit: number | null, + order: Order ): Promise; /** diff --git a/sdk/typescript/src/providers/void-provider.ts b/sdk/typescript/src/providers/void-provider.ts index a532686ab7ec7..5483a4aa2978c 100644 --- a/sdk/typescript/src/providers/void-provider.ts +++ b/sdk/typescript/src/providers/void-provider.ts @@ -5,7 +5,6 @@ import { SignatureScheme } from '../cryptography/publickey'; import { HttpHeaders } from '../rpc/client'; import { CertifiedTransaction, - CoinDenominationInfoResponse, TransactionDigest, GetTxnDigestsResponse, GatewayTxSeqNumber, @@ -33,6 +32,7 @@ import { FaucetResponse, Order, TransactionEffects, + CoinMetadata, } from '../types'; import { Provider } from './provider'; @@ -42,6 +42,10 @@ export class VoidProvider extends Provider { throw this.newError('getRpcApiVersion'); } + getCoinMetadata(_coinType: string): Promise { + throw new Error('getCoinMetadata'); + } + // Faucet async requestSuiFromFaucet( _recipient: SuiAddress, @@ -61,10 +65,6 @@ export class VoidProvider extends Provider { throw this.newError('getGasObjectsOwnedByAddress'); } - getCoinDenominationInfo(_coin_type: string): CoinDenominationInfoResponse { - throw this.newError('getCoinDenominationInfo'); - } - async getCoinBalancesOwnedByAddress( _address: string, _typeArg?: string @@ -171,8 +171,6 @@ export class VoidProvider extends Provider { throw this.newError('syncAccountState'); } - - async subscribeEvent( _filter: SuiEventFilter, _onMessage: (event: SuiEventEnvelope) => void @@ -189,19 +187,19 @@ export class VoidProvider extends Provider { } async getTransactions( - _query: TransactionQuery, - _cursor: TransactionDigest | null, - _limit: number | null, - _order: Order + _query: TransactionQuery, + _cursor: TransactionDigest | null, + _limit: number | null, + _order: Order ): Promise { throw this.newError('getTransactions'); } async getEvents( - _query: EventQuery, - _cursor: EventId | null, - _limit: number | null, - _order: Order + _query: EventQuery, + _cursor: EventId | null, + _limit: number | null, + _order: Order ): Promise { throw this.newError('getEvents'); } diff --git a/sdk/typescript/src/types/framework.ts b/sdk/typescript/src/types/framework.ts index b7bf4b629f09e..8616b4701c69a 100644 --- a/sdk/typescript/src/types/framework.ts +++ b/sdk/typescript/src/types/framework.ts @@ -37,6 +37,15 @@ export const COIN_TYPE_ARG_REGEX = /^0x2::coin::Coin<(.+)>$/; type ObjectData = ObjectDataFull | SuiObjectInfo; type ObjectDataFull = GetObjectDataResponse | SuiMoveObject; +export type CoinMetadata = { + decimals: number; + name: string; + symbol: string; + description: string; + iconUrl: string | null; + id: ObjectId | null; +}; + /** * Utility class for 0x2::coin * as defined in https://github.com/MystenLabs/sui/blob/ca9046fd8b1a9e8634a4b74b0e7dabdc7ea54475/sui_programmability/framework/sources/Coin.move#L4 diff --git a/sdk/typescript/src/types/index.guard.ts b/sdk/typescript/src/types/index.guard.ts index 8d6bb67351b2a..31871620d2e25 100644 --- a/sdk/typescript/src/types/index.guard.ts +++ b/sdk/typescript/src/types/index.guard.ts @@ -7,7 +7,7 @@ * Generated type guards for "index.ts". * WARNING: Do not manually change this file. */ -import { TransactionDigest, SuiAddress, ObjectOwner, SuiObjectRef, SuiObjectInfo, ObjectContentFields, MovePackageContent, SuiData, SuiMoveObject, CoinDenominationInfoResponse, SuiMovePackage, SuiMoveFunctionArgTypesResponse, SuiMoveFunctionArgType, SuiMoveFunctionArgTypes, SuiMoveNormalizedModules, SuiMoveNormalizedModule, SuiMoveModuleId, SuiMoveNormalizedStruct, SuiMoveStructTypeParameter, SuiMoveNormalizedField, SuiMoveNormalizedFunction, SuiMoveVisibility, SuiMoveTypeParameterIndex, SuiMoveAbilitySet, SuiMoveNormalizedType, SuiMoveNormalizedTypeParameterType, SuiMoveNormalizedStructType, SuiObject, ObjectStatus, ObjectType, GetOwnedObjectsResponse, GetObjectDataResponse, ObjectDigest, ObjectId, SequenceNumber, Order, MoveEvent, PublishEvent, CoinBalanceChangeEvent, TransferObjectEvent, MutateObjectEvent, DeleteObjectEvent, NewObjectEvent, SuiEvent, MoveEventField, EventQuery, EventId, PaginatedEvents, EventType, BalanceChangeType, SuiEventFilter, SuiEventEnvelope, SuiEvents, SubscriptionId, SubscriptionEvent, TransferObject, SuiTransferSui, SuiChangeEpoch, Pay, PaySui, PayAllSui, ExecuteTransactionRequestType, TransactionKindName, SuiTransactionKind, SuiTransactionData, EpochId, GenericAuthoritySignature, AuthorityQuorumSignInfo, CertifiedTransaction, GasCostSummary, ExecutionStatusType, ExecutionStatus, OwnedObjectRef, TransactionEffects, SuiTransactionResponse, SuiCertifiedTransactionEffects, SuiExecuteTransactionResponse, GatewayTxSeqNumber, GetTxnDigestsResponse, PaginatedTransactionDigests, TransactionQuery, MoveCall, SuiJsonValue, EmptySignInfo, AuthorityName, AuthoritySignature, TransactionBytes, SuiParsedMergeCoinResponse, SuiParsedSplitCoinResponse, SuiParsedPublishResponse, SuiPackage, SuiParsedTransactionResponse, DelegationData, DelegationSuiObject, TransferObjectTx, TransferSuiTx, PayTx, PaySuiTx, PayAllSuiTx, PublishTx, SharedObjectRef, ObjectArg, CallArg, StructTag, TypeTag, MoveCallTx, Transaction, TransactionKind, TransactionData, RpcApiVersion, FaucetCoinInfo, FaucetResponse } from "./index"; +import { TransactionDigest, SuiAddress, ObjectOwner, SuiObjectRef, SuiObjectInfo, ObjectContentFields, MovePackageContent, SuiData, SuiMoveObject, SuiMovePackage, SuiMoveFunctionArgTypesResponse, SuiMoveFunctionArgType, SuiMoveFunctionArgTypes, SuiMoveNormalizedModules, SuiMoveNormalizedModule, SuiMoveModuleId, SuiMoveNormalizedStruct, SuiMoveStructTypeParameter, SuiMoveNormalizedField, SuiMoveNormalizedFunction, SuiMoveVisibility, SuiMoveTypeParameterIndex, SuiMoveAbilitySet, SuiMoveNormalizedType, SuiMoveNormalizedTypeParameterType, SuiMoveNormalizedStructType, SuiObject, ObjectStatus, ObjectType, GetOwnedObjectsResponse, GetObjectDataResponse, ObjectDigest, ObjectId, SequenceNumber, Order, MoveEvent, PublishEvent, CoinBalanceChangeEvent, TransferObjectEvent, MutateObjectEvent, DeleteObjectEvent, NewObjectEvent, SuiEvent, MoveEventField, EventQuery, EventId, PaginatedEvents, EventType, BalanceChangeType, SuiEventFilter, SuiEventEnvelope, SuiEvents, SubscriptionId, SubscriptionEvent, TransferObject, SuiTransferSui, SuiChangeEpoch, Pay, PaySui, PayAllSui, ExecuteTransactionRequestType, TransactionKindName, SuiTransactionKind, SuiTransactionData, EpochId, GenericAuthoritySignature, AuthorityQuorumSignInfo, CertifiedTransaction, GasCostSummary, ExecutionStatusType, ExecutionStatus, OwnedObjectRef, TransactionEffects, SuiTransactionResponse, SuiCertifiedTransactionEffects, SuiExecuteTransactionResponse, GatewayTxSeqNumber, GetTxnDigestsResponse, PaginatedTransactionDigests, TransactionQuery, MoveCall, SuiJsonValue, EmptySignInfo, AuthorityName, AuthoritySignature, TransactionBytes, SuiParsedMergeCoinResponse, SuiParsedSplitCoinResponse, SuiParsedPublishResponse, SuiPackage, SuiParsedTransactionResponse, CoinMetadata, DelegationData, DelegationSuiObject, TransferObjectTx, TransferSuiTx, PayTx, PaySuiTx, PayAllSuiTx, PublishTx, SharedObjectRef, ObjectArg, CallArg, StructTag, TypeTag, MoveCallTx, Transaction, TransactionKind, TransactionData, RpcApiVersion, FaucetCoinInfo, FaucetResponse } from "./index"; export function isTransactionDigest(obj: any, _argumentName?: string): obj is TransactionDigest { return ( @@ -114,18 +114,6 @@ export function isSuiMoveObject(obj: any, _argumentName?: string): obj is SuiMov ) } -export function isCoinDenominationInfoResponse(obj: any, _argumentName?: string): obj is CoinDenominationInfoResponse { - return ( - (obj !== null && - typeof obj === "object" || - typeof obj === "function") && - isTransactionDigest(obj.coinType) as boolean && - (typeof obj.basicUnit === "undefined" || - isTransactionDigest(obj.basicUnit) as boolean) && - isSuiMoveTypeParameterIndex(obj.decimalNumber) as boolean - ) -} - export function isSuiMovePackage(obj: any, _argumentName?: string): obj is SuiMovePackage { return ( (obj !== null && @@ -1289,6 +1277,22 @@ export function isSuiParsedTransactionResponse(obj: any, _argumentName?: string) ) } +export function isCoinMetadata(obj: any, _argumentName?: string): obj is CoinMetadata { + return ( + (obj !== null && + typeof obj === "object" || + typeof obj === "function") && + isSuiMoveTypeParameterIndex(obj.decimals) as boolean && + isTransactionDigest(obj.name) as boolean && + isTransactionDigest(obj.symbol) as boolean && + isTransactionDigest(obj.description) as boolean && + (obj.iconUrl === null || + isTransactionDigest(obj.iconUrl) as boolean) && + (obj.id === null || + isTransactionDigest(obj.id) as boolean) + ) +} + export function isDelegationData(obj: any, _argumentName?: string): obj is DelegationData { return ( isSuiMoveObject(obj) as boolean && diff --git a/sdk/typescript/src/types/objects.ts b/sdk/typescript/src/types/objects.ts index dc14155b6bdfa..f783ef9ee9e02 100644 --- a/sdk/typescript/src/types/objects.ts +++ b/sdk/typescript/src/types/objects.ts @@ -38,16 +38,6 @@ export type SuiMoveObject = { export const MIST_PER_SUI: BigInt = BigInt(1000000000); -export type CoinDenominationInfoResponse = { - /** Coin type like "0x2::sui::SUI" */ - coinType: string; - /** min unit, like MIST */ - basicUnit?: string; - /** number of zeros in the denomination, - * e.g., 9 here for SUI. */ - decimalNumber: number; -}; - export type SuiMovePackage = { /** A mapping from module name to disassembled Move bytecode */ disassembled: MovePackageContent; @@ -246,7 +236,7 @@ export function getSharedObjectInitialVersion( export function isSharedObject(resp: GetObjectDataResponse): boolean { const owner = getObjectOwner(resp); - return (typeof owner === 'object' && 'Shared' in owner); + return typeof owner === 'object' && 'Shared' in owner; } export function isImmutableObject(resp: GetObjectDataResponse): boolean { diff --git a/sdk/typescript/src/types/version.ts b/sdk/typescript/src/types/version.ts index 05401b9198e9d..89dfe83dfba2a 100644 --- a/sdk/typescript/src/types/version.ts +++ b/sdk/typescript/src/types/version.ts @@ -14,3 +14,8 @@ export function parseVersionFromString( ): RpcApiVersion | undefined { return parse(version); } + +export function versionToString(version: RpcApiVersion): string { + const { major, minor, patch } = version; + return `${major}.${minor}.${patch}`; +} diff --git a/sdk/typescript/test/e2e/coin-metadata.test.ts b/sdk/typescript/test/e2e/coin-metadata.test.ts index 529acc8865875..8fad1dc89f439 100644 --- a/sdk/typescript/test/e2e/coin-metadata.test.ts +++ b/sdk/typescript/test/e2e/coin-metadata.test.ts @@ -40,30 +40,12 @@ describe('Test Coin Metadata', () => { if (shouldSkip) { return; } - // TODO: add a new RPC endpoint for fetching coin metadata - const objectResponse = await toolbox.provider.getObject(packageId); - const publishTxnDigest = - getObjectExistsResponse(objectResponse)!.previousTransaction; - const publishTxn = await toolbox.provider.getTransactionWithEffects( - publishTxnDigest + const coinMetadata = await signer.provider.getCoinMetadata( + `${packageId}::test::TEST` ); - const coinMetadataId = getEvents(publishTxn)! - .map((event) => { - if ( - 'newObject' in event && - event.newObject.objectType.includes('CoinMetadata') - ) { - return event.newObject.objectId; - } - return undefined; - }) - .filter((e) => e)[0]!; - const coinMetadata = getObjectFields( - await toolbox.provider.getObject(coinMetadataId) - )!; expect(coinMetadata.decimals).to.equal(2); expect(coinMetadata.name).to.equal('Test Coin'); expect(coinMetadata.description).to.equal('Test coin metadata'); - expect(coinMetadata.icon_url).to.equal('http://sui.io'); + expect(coinMetadata.iconUrl).to.equal('http://sui.io'); }); });