Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RPC] Add endpoint for fetching CoinMetadata #6281

Merged
merged 1 commit into from Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/friendly-phones-laugh.md
@@ -0,0 +1,5 @@
---
"@mysten/sui.js": patch
---

Replace `getCoinDenominationInfo` with `getCoinMetadata`
4 changes: 2 additions & 2 deletions apps/explorer/src/hooks/useFormatCoin.ts
Expand Up @@ -66,7 +66,7 @@ export function useCoinDecimals(coinType?: string | null) {
);
}

return rpc.getCoinDenominationInfo(coinType);
return rpc.getCoinMetadata(coinType);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@666lcz what happens if this method isn’t available? This is currently hardcoded for SUI so I’m worried that this will break sui display for networks that don’t yet support this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I just added the logic to return hardcoded value for SUI if the RPC version is prior to 0.17.0

},
{
// This is currently expected to fail for non-SUI tokens, so disable retries:
Expand All @@ -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', {
Expand Down
4 changes: 2 additions & 2 deletions apps/wallet/src/ui/app/hooks/useFormatCoin.ts
Expand Up @@ -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:
Expand All @@ -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
Expand Down
46 changes: 24 additions & 22 deletions crates/sui-core/tests/staged/sui.yaml
Expand Up @@ -169,78 +169,80 @@ 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:
TYPENAME: ObjectID
- 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:
Expand Down
41 changes: 41 additions & 0 deletions crates/sui-json-rpc-types/src/lib.rs
Expand Up @@ -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};
Expand Down Expand Up @@ -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<String>,
/// Object id for the CoinMetadata object
pub id: Option<ObjectID>,
}

impl TryFrom<Object> for SuiCoinMetadata {
type Error = SuiError;
fn try_from(object: Object) -> Result<Self, Self::Error> {
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 {
Expand Down
12 changes: 10 additions & 2 deletions crates/sui-json-rpc/src/api.rs
Expand Up @@ -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,
};
Expand Down Expand Up @@ -118,6 +118,14 @@ pub trait RpcFullNodeReadApi {
#[method(name = "dryRunTransaction")]
async fn dry_run_transaction(&self, tx_bytes: Base64) -> RpcResult<SuiTransactionEffects>;

/// 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<SuiCoinMetadata>;

/// Return the argument types of a Move function,
/// based on normalized Type.
#[method(name = "getMoveFunctionArgTypes")]
Expand Down
80 changes: 78 additions & 2 deletions crates/sui-json-rpc/src/read_api.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SuiCoinMetadata> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks quite expensive? We are pulling out the package object every time we need to get the coin's metadata.

if this is frequently accessed, maybe we can consider creating a mapping in AuthorityPerpetualTables?

let coin_struct = coin_type.parse::<StructTag>().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::<StructTag>().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,
Expand Down