diff --git a/.changeset/silver-geckos-look.md b/.changeset/silver-geckos-look.md new file mode 100644 index 0000000000000..c5916dc2f8f88 --- /dev/null +++ b/.changeset/silver-geckos-look.md @@ -0,0 +1,5 @@ +--- +"@mysten/sui.js": patch +--- + +Add optional parameter for filtering object by type in getOwnedObjectsByAddress diff --git a/crates/sui-indexer/src/apis/read_api.rs b/crates/sui-indexer/src/apis/read_api.rs index bdc9f46f29301..4e6a498d99a7f 100644 --- a/crates/sui-indexer/src/apis/read_api.rs +++ b/crates/sui-indexer/src/apis/read_api.rs @@ -213,6 +213,13 @@ impl ReadApiServer for ReadApi { async fn get_raw_object(&self, object_id: ObjectID) -> RpcResult { self.fullnode.get_raw_object(object_id).await } + + async fn get_display_deprecated( + &self, + object_id: ObjectID, + ) -> RpcResult> { + self.fullnode.get_display_deprecated(object_id).await + } } impl SuiRpcModule for ReadApi { diff --git a/crates/sui-json-rpc/src/api/read.rs b/crates/sui-json-rpc/src/api/read.rs index 13240b90e1d9f..fad6dfb5c829a 100644 --- a/crates/sui-json-rpc/src/api/read.rs +++ b/crates/sui-json-rpc/src/api/read.rs @@ -201,4 +201,13 @@ pub trait ReadApi { /// the id of the object object_id: ObjectID, ) -> RpcResult; + + // TODO: this will be replaced by the new queryObjects API + /// Return the Display string of a object + #[method(name = "getDisplayDeprecated")] + async fn get_display_deprecated( + &self, + /// the id of the object + object_id: ObjectID, + ) -> RpcResult>; } diff --git a/crates/sui-json-rpc/src/read_api.rs b/crates/sui-json-rpc/src/read_api.rs index bc32c4cf674db..12a04c1895353 100644 --- a/crates/sui-json-rpc/src/read_api.rs +++ b/crates/sui-json-rpc/src/read_api.rs @@ -6,8 +6,12 @@ use async_trait::async_trait; use jsonrpsee::core::RpcResult; use move_binary_format::normalized::{Module as NormalizedModule, Type}; use move_core_types::identifier::Identifier; +use move_core_types::language_storage::StructTag; +use move_core_types::value::{MoveStruct, MoveValue}; use std::collections::BTreeMap; use std::sync::Arc; +use sui_types::collection_types::VecMap; +use sui_types::display::{DisplayCreatedEvent, DisplayObject}; use sui_types::intent::{AppId, Intent, IntentMessage, IntentScope, IntentVersion}; use tap::TapFallible; @@ -17,9 +21,9 @@ use jsonrpsee::RpcModule; use sui_core::authority::AuthorityState; use sui_json_rpc_types::{ Checkpoint, CheckpointId, DynamicFieldPage, GetObjectDataResponse, GetPastObjectDataResponse, - GetRawObjectDataResponse, MoveFunctionArgType, ObjectValueKind, Page, - SuiMoveNormalizedFunction, SuiMoveNormalizedModule, SuiMoveNormalizedStruct, SuiObjectInfo, - SuiTransactionEffects, SuiTransactionResponse, TransactionsPage, + GetRawObjectDataResponse, MoveFunctionArgType, ObjectValueKind, Page, SuiEvent, + SuiMoveNormalizedFunction, SuiMoveNormalizedModule, SuiMoveNormalizedStruct, SuiMoveStruct, + SuiMoveValue, SuiObjectInfo, SuiTransactionEffects, SuiTransactionResponse, TransactionsPage, }; use sui_open_rpc::Module; use sui_types::base_types::SequenceNumber; @@ -32,7 +36,7 @@ use sui_types::messages_checkpoint::{ }; use sui_types::move_package::normalize_modules; use sui_types::object::{Data, ObjectRead}; -use sui_types::query::TransactionQuery; +use sui_types::query::{EventQuery, TransactionQuery}; use sui_types::dynamic_field::DynamicFieldName; use tracing::debug; @@ -41,6 +45,8 @@ use crate::api::cap_page_limit; use crate::error::Error; use crate::SuiRpcModule; +const MAX_DISPLAY_NESTED_LEVEL: usize = 10; + // An implementation of the read portion of the JSON-RPC interface intended for use in // Fullnodes. pub struct ReadApi { @@ -372,6 +378,15 @@ impl ReadApiServer for ReadApi { .map_err(|e| anyhow!("{e}"))? .try_into()?) } + + async fn get_display_deprecated( + &self, + object_id: ObjectID, + ) -> RpcResult> { + let (object_type, move_struct) = get_object_type_and_struct(self, object_id).await?; + let display_object = get_display_object(self, object_type).await?; + Ok(get_rendered_fields(display_object.fields, &move_struct).map_err(|e| anyhow!("{e}"))?) + } } impl SuiRpcModule for ReadApi { @@ -384,6 +399,84 @@ impl SuiRpcModule for ReadApi { } } +async fn get_display_object( + fullnode_api: &ReadApi, + object_type: StructTag, +) -> RpcResult { + let display_object_id = get_display_object_id(fullnode_api, object_type).await?; + if let ObjectRead::Exists(_, display_object, _) = fullnode_api + .state + .get_object_read(&display_object_id) + .await + .map_err(|e| anyhow!("Failed to fetch display object {display_object_id}: {e}"))? + { + let move_object = display_object + .data + .try_as_move() + .ok_or_else(|| anyhow!("Failed to extract Move object from {display_object_id}"))?; + Ok(bcs::from_bytes::(move_object.contents()) + .map_err(|e| anyhow!("Failed to deserialize DisplayObject {display_object_id}: {e}"))?) + } else { + Err(anyhow!("Display object {display_object_id} does not exist"))? + } +} + +async fn get_display_object_id( + fullnode_api: &ReadApi, + object_type: StructTag, +) -> RpcResult { + let display_created_event = fullnode_api + .state + .get_events( + EventQuery::MoveEvent(DisplayCreatedEvent::type_(&object_type).to_string()), + /* cursor */ None, + /* limit */ 1, + /* descending */ false, + ) + .await?; + if display_created_event.is_empty() { + return Err(anyhow!( + "Failed to find DisplayCreated event for {object_type}" + ))?; + } + if let SuiEvent::MoveEvent { bcs, .. } = display_created_event[0].clone().1.event { + let display_object_id = bcs::from_bytes::(&bcs) + .map_err(|e| anyhow!("Failed to deserialize DisplayCreatedEvent: {e}"))? + .id + .bytes; + Ok(display_object_id) + } else { + Err(anyhow!("Failed to extract display object id from event"))? + } +} + +async fn get_object_type_and_struct( + fullnode_api: &ReadApi, + object_id: ObjectID, +) -> RpcResult<(StructTag, MoveStruct)> { + let object_read = fullnode_api + .state + .get_object_read(&object_id) + .await + .map_err(|e| anyhow!("Failed to fetch {object_id}: {e}"))?; + if let ObjectRead::Exists(_, o, layout) = object_read { + let layout = layout.ok_or_else(|| anyhow!("Failed to extract layout"))?; + let object_type = o + .type_() + .ok_or_else(|| anyhow!("Failed to extract object type"))? + .clone(); + let move_struct = o + .data + .try_as_move() + .ok_or_else(|| anyhow!("Failed to extract Move object from {object_id}"))? + .to_move_struct(&layout) + .map_err(|err| anyhow!("{err}"))?; + Ok((object_type, move_struct)) + } else { + Err(anyhow!("Object {object_id} does not exist"))? + } +} + pub async fn get_move_module( fullnode_api: &ReadApi, package: ObjectID, @@ -433,3 +526,107 @@ pub fn get_transaction_data_and_digest( let txn_digest = TransactionDigest::new(sha3_hash(&intent_msg.value)); Ok((intent_msg.value, txn_digest)) } + +pub fn get_rendered_fields( + fields: VecMap, + move_struct: &MoveStruct, +) -> RpcResult> { + let sui_move_value: SuiMoveValue = MoveValue::Struct(move_struct.clone()).into(); + if let SuiMoveValue::Struct(move_struct) = sui_move_value { + return fields + .contents + .iter() + .map(|entry| match parse_template(&entry.value, &move_struct) { + Ok(value) => Ok((entry.key.clone(), value)), + Err(e) => Err(e), + }) + .collect::>>(); + } + Err(anyhow!("Failed to parse move struct"))? +} + +fn parse_template(template: &str, move_struct: &SuiMoveStruct) -> RpcResult { + let mut output = template.to_string(); + let mut var_name = String::new(); + let mut in_braces = false; + let mut escaped = false; + + for ch in template.chars() { + match ch { + '\\' => { + escaped = true; + continue; + } + '{' if !escaped => { + in_braces = true; + var_name.clear(); + } + '}' if !escaped => { + in_braces = false; + let value = get_value_from_move_struct(move_struct, &var_name)?; + output = output.replace(&format!("{{{}}}", var_name), &value.to_string()); + } + _ if !escaped => { + if in_braces { + var_name.push(ch); + } + } + _ => {} + } + escaped = false; + } + + Ok(output.replace('\\', "")) +} + +fn get_value_from_move_struct(move_struct: &SuiMoveStruct, var_name: &str) -> RpcResult { + let parts: Vec<&str> = var_name.split('.').collect(); + if parts.is_empty() { + return Err(anyhow!("Display template value cannot be empty"))?; + } + if parts.len() > MAX_DISPLAY_NESTED_LEVEL { + return Err(anyhow!( + "Display template value nested depth cannot exist {}", + MAX_DISPLAY_NESTED_LEVEL + ))?; + } + let mut current_value = &SuiMoveValue::Struct(move_struct.clone()); + // iterate over the parts and try to access the corresponding field + for part in parts { + match current_value { + SuiMoveValue::Struct(move_struct) => { + if let SuiMoveStruct::WithTypes { type_: _, fields } + | SuiMoveStruct::WithFields(fields) = move_struct + { + if let Some(value) = fields.get(part) { + current_value = value; + } else { + return Err(anyhow!( + "Field value {} cannot be found in struct", + var_name + ))?; + } + } else { + return Err(anyhow!( + "Unexpected move struct type for field {}", + var_name + ))?; + } + } + _ => return Err(anyhow!("Unexpected move value type for field {}", var_name))?, + } + } + + match current_value { + SuiMoveValue::Option(move_option) => match move_option.as_ref() { + Some(move_value) => Ok(move_value.to_string()), + None => Ok("".to_string()), + }, + SuiMoveValue::Vector(_) => Err(anyhow!( + "Vector is not supported as a Display value {}", + var_name + ))?, + + _ => Ok(current_value.to_string()), + } +} diff --git a/crates/sui-open-rpc/spec/openrpc.json b/crates/sui-open-rpc/spec/openrpc.json index 009ea6bfa105b..82d88486096d2 100644 --- a/crates/sui-open-rpc/spec/openrpc.json +++ b/crates/sui-open-rpc/spec/openrpc.json @@ -823,6 +823,35 @@ } } }, + { + "name": "sui_getDisplayDeprecated", + "tags": [ + { + "name": "Read API" + } + ], + "description": "Return the Display string of a object", + "params": [ + { + "name": "object_id", + "description": "the id of the object", + "required": true, + "schema": { + "$ref": "#/components/schemas/ObjectID" + } + } + ], + "result": { + "name": "BTreeMap", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, { "name": "sui_getDynamicFieldObject", "tags": [ diff --git a/crates/sui-types/src/collection_types.rs b/crates/sui-types/src/collection_types.rs index a1203b0f7bd08..866c02555340b 100644 --- a/crates/sui-types/src/collection_types.rs +++ b/crates/sui-types/src/collection_types.rs @@ -13,8 +13,8 @@ pub struct VecMap { /// Rust version of the Move sui::vec_map::Entry type #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)] pub struct Entry { - key: K, - value: V, + pub key: K, + pub value: V, } /// Rust version of the Move sui::vec_set::VecSet type diff --git a/crates/sui-types/src/display.rs b/crates/sui-types/src/display.rs new file mode 100644 index 0000000000000..05ab87ce06bcc --- /dev/null +++ b/crates/sui-types/src/display.rs @@ -0,0 +1,39 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::collection_types::VecMap; +use crate::id::{ID, UID}; +use crate::SUI_FRAMEWORK_ADDRESS; +use move_core_types::ident_str; +use move_core_types::identifier::IdentStr; +use move_core_types::language_storage::StructTag; +use serde::Deserialize; + +pub const DISPLAY_MODULE_NAME: &IdentStr = ident_str!("display"); +pub const DISPLAY_CREATED_EVENT_NAME: &IdentStr = ident_str!("DisplayCreated"); + +// TODO: add tests to keep in sync +/// Rust version of the Move sui::display::Display type +#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] +pub struct DisplayObject { + pub id: UID, + pub fields: VecMap, + pub version: u16, +} + +#[derive(Deserialize, Debug)] +pub struct DisplayCreatedEvent { + // The Object ID of Display Object + pub id: ID, +} + +impl DisplayCreatedEvent { + pub fn type_(inner: &StructTag) -> StructTag { + StructTag { + address: SUI_FRAMEWORK_ADDRESS, + name: DISPLAY_CREATED_EVENT_NAME.to_owned(), + module: DISPLAY_MODULE_NAME.to_owned(), + type_params: vec![inner.clone().into()], + } + } +} diff --git a/crates/sui-types/src/lib.rs b/crates/sui-types/src/lib.rs index 71cc07c91e451..2be679375e764 100644 --- a/crates/sui-types/src/lib.rs +++ b/crates/sui-types/src/lib.rs @@ -29,6 +29,7 @@ pub mod collection_types; pub mod committee; pub mod crypto; pub mod digests; +pub mod display; pub mod dynamic_field; pub mod event; pub mod filter; diff --git a/sdk/typescript/src/providers/json-rpc-provider-with-cache.ts b/sdk/typescript/src/providers/json-rpc-provider-with-cache.ts new file mode 100644 index 0000000000000..8ab67d394ab42 --- /dev/null +++ b/sdk/typescript/src/providers/json-rpc-provider-with-cache.ts @@ -0,0 +1,113 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { + GetObjectDataResponse, + SuiObjectInfo, + SuiObjectRef, + getObjectReference, + TransactionEffects, + normalizeSuiObjectId, + ExecuteTransactionRequestType, + SuiExecuteTransactionResponse, + getTransactionEffects, +} from '../types'; +import { JsonRpcProvider } from './json-rpc-provider'; +import { is } from 'superstruct'; +import { SerializedSignature } from '../cryptography/signature'; + +export class JsonRpcProviderWithCache extends JsonRpcProvider { + /** + * A list of object references which are being tracked. + * + * Whenever an object is fetched or updated within the transaction, + * its record gets updated. + */ + private objectRefs: Map = new Map(); + + // Objects + async getObjectsOwnedByAddress( + address: string, + typefilter?: string, + ): Promise { + const resp = await super.getObjectsOwnedByAddress(address, typefilter); + resp.forEach((r) => this.updateObjectRefCache(r)); + return resp; + } + + async getObject(objectId: string): Promise { + const resp = await super.getObject(objectId); + this.updateObjectRefCache(resp); + return resp; + } + + async getObjectRef( + objectId: string, + skipCache = false, + ): Promise { + const normalizedId = normalizeSuiObjectId(objectId); + if (!skipCache && this.objectRefs.has(normalizedId)) { + return this.objectRefs.get(normalizedId); + } + + const ref = await super.getObjectRef(objectId); + this.updateObjectRefCache(ref); + return ref; + } + + async getObjectBatch(objectIds: string[]): Promise { + const resp = await super.getObjectBatch(objectIds); + resp.forEach((r) => this.updateObjectRefCache(r)); + return resp; + } + + // Transactions + + async executeTransaction( + txnBytes: Uint8Array | string, + signature: SerializedSignature, + requestType: ExecuteTransactionRequestType = 'WaitForEffectsCert', + ): Promise { + if (requestType !== 'WaitForEffectsCert') { + console.warn( + `It's not recommended to use JsonRpcProviderWithCache with the request ` + + `type other than 'WaitForEffectsCert' for executeTransaction. Using ` + + `the '${requestType}' may result in stale cache and a failure in subsequent transactions.`, + ); + } + const resp = await super.executeTransaction( + txnBytes, + signature, + requestType, + ); + const effects = getTransactionEffects(resp); + if (effects != null) { + this.updateObjectRefCacheFromTransactionEffects(effects); + } + return resp; + } + + private updateObjectRefCache( + newData: GetObjectDataResponse | SuiObjectRef | undefined, + ) { + if (newData == null) { + return; + } + const ref = is(newData, SuiObjectRef) + ? newData + : getObjectReference(newData); + if (ref != null) { + this.objectRefs.set(ref.objectId, ref); + } + } + + private updateObjectRefCacheFromTransactionEffects( + effects: TransactionEffects, + ) { + effects.created?.forEach((r) => this.updateObjectRefCache(r.reference)); + effects.mutated?.forEach((r) => this.updateObjectRefCache(r.reference)); + effects.unwrapped?.forEach((r) => this.updateObjectRefCache(r.reference)); + effects.wrapped?.forEach((r) => this.updateObjectRefCache(r)); + effects.deleted?.forEach((r) => this.objectRefs.delete(r.objectId)); + } +} diff --git a/sdk/typescript/src/providers/json-rpc-provider.ts b/sdk/typescript/src/providers/json-rpc-provider.ts index 0ca200786d785..25bddd5524264 100644 --- a/sdk/typescript/src/providers/json-rpc-provider.ts +++ b/sdk/typescript/src/providers/json-rpc-provider.ts @@ -404,17 +404,26 @@ export class JsonRpcProvider extends Provider { // Objects async getObjectsOwnedByAddress( address: SuiAddress, + typeFilter?: string, ): Promise { try { if (!address || !isValidSuiAddress(normalizeSuiAddress(address))) { throw new Error('Invalid Sui address'); } - return await this.client.requestWithType( + const objects = await this.client.requestWithType( 'sui_getObjectsOwnedByAddress', [address], GetOwnedObjectsResponse, this.options.skipDataValidation, ); + // TODO: remove this once we migrated to the new queryObject API + if (typeFilter) { + return objects.filter( + (obj: SuiObjectInfo) => + obj.type === typeFilter || obj.type.startsWith(typeFilter + '<'), + ); + } + return objects; } catch (err) { throw new Error( `Error fetching owned object: ${err} for address ${address}`, diff --git a/sdk/typescript/src/providers/provider.ts b/sdk/typescript/src/providers/provider.ts index 1afa2ab941ff1..e516eaa6578c2 100644 --- a/sdk/typescript/src/providers/provider.ts +++ b/sdk/typescript/src/providers/provider.ts @@ -138,8 +138,14 @@ export abstract class Provider { /** * Get all objects owned by an address */ + /** + * @param addressOrObjectId owner address or object id + * @param typeFilter? a fully qualified type name for the object(e.g., 0x2::coin::Coin<0x2::sui::SUI>) + * or type name without generics (e.g., 0x2::coin::Coin will match all 0x2::coin::Coin) + */ abstract getObjectsOwnedByAddress( addressOrObjectId: string, + typeFilter?: string, ): Promise; /** diff --git a/sdk/typescript/src/providers/void-provider.ts b/sdk/typescript/src/providers/void-provider.ts index 1d69c37cb4022..92d6ee06a9fa3 100644 --- a/sdk/typescript/src/providers/void-provider.ts +++ b/sdk/typescript/src/providers/void-provider.ts @@ -126,7 +126,10 @@ export class VoidProvider extends Provider { } // Objects - async getObjectsOwnedByAddress(_address: string): Promise { + async getObjectsOwnedByAddress( + _address: string, + _typefilter?: string, + ): Promise { throw this.newError('getObjectsOwnedByAddress'); } diff --git a/sdk/typescript/test/e2e/data/display_test/Move.toml b/sdk/typescript/test/e2e/data/display_test/Move.toml new file mode 100644 index 0000000000000..fc4063c993e5c --- /dev/null +++ b/sdk/typescript/test/e2e/data/display_test/Move.toml @@ -0,0 +1,12 @@ +[package] +name = "display_test" +version = "0.0.1" + +[dependencies] +Sui = { local = "../../../../../../crates/sui-framework" } +# Using a local dep for the Move stdlib instead of a git dep to avoid the overhead of fetching the git dep in +# CI. The local dep is an unmodified version of the upstream stdlib +MoveStdlib = { local = "../../../../../../crates/sui-framework/deps/move-stdlib" } + +[addresses] +display_test = "0x0" diff --git a/sdk/typescript/test/e2e/data/display_test/sources/display_test.move b/sdk/typescript/test/e2e/data/display_test/sources/display_test.move new file mode 100644 index 0000000000000..417ddea2e5306 --- /dev/null +++ b/sdk/typescript/test/e2e/data/display_test/sources/display_test.move @@ -0,0 +1,92 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module display_test::boars { + use sui::object::{Self, UID}; + use std::option::{Self, Option}; + use sui::tx_context::{TxContext, sender}; + use sui::transfer::transfer; + use sui::publisher; + use sui::url::{Self, Url}; + use sui::display; + use std::string::{utf8, String}; + + /// For when a witness type passed is not an OTW. + const ENotOneTimeWitness: u64 = 0; + + /// An OTW to use when creating a Publisher + struct BOARS has drop {} + + struct Boar has key, store { + id: UID, + img_url: String, + name: String, + description: String, + creator: Option, + price: Option, + metadata: Metadata, + buyer: address, + full_url: Url, + } + + struct Metadata has store { + age: u64, + } + + fun init(otw: BOARS, ctx: &mut TxContext) { + assert!(sui::types::is_one_time_witness(&otw), ENotOneTimeWitness); + + let pub = publisher::claim(otw, ctx); + let display = display::new(&pub, ctx); + + display::add_multiple(&mut display, vector[ + utf8(b"name"), + utf8(b"description"), + utf8(b"img_url"), + utf8(b"creator"), + utf8(b"price"), + utf8(b"project_url"), + utf8(b"age"), + utf8(b"buyer"), + utf8(b"full_url"), + utf8(b"escape_syntax"), + ], vector[ + utf8(b"{name}"), + // test multiple fields and UID + utf8(b"Unique Boar from the Boars collection with {name} and {id}"), + utf8(b"https://get-a-boar.com/{img_url}"), + // test option::some + utf8(b"{creator}"), + // test option::none + utf8(b"{price}"), + // test no template value + utf8(b"https://get-a-boar.com/"), + // test nested field + utf8(b"{metadata.age}"), + // test address + utf8(b"{buyer}"), + // test Url type + utf8(b"{full_url}"), + // test escape syntax + utf8(b"\\{name\\}"), + ]); + + transfer(display, sender(ctx)); + transfer(pub, sender(ctx)); + + let boar = Boar { + id: object::new(ctx), + img_url: utf8(b"first.png"), + name: utf8(b"First Boar"), + description: utf8(b"First Boar from the Boars collection!"), + creator: option::some(utf8(b"Chris")), + price: option::none(), + metadata: Metadata { + age: 10, + }, + buyer: sender(ctx), + full_url: url::new_unsafe_from_bytes(b"https://get-a-boar.fullurl.com/"), + }; + transfer(boar, sender(ctx)) + } +} diff --git a/sdk/typescript/test/e2e/object-display-standard.test.ts b/sdk/typescript/test/e2e/object-display-standard.test.ts new file mode 100644 index 0000000000000..11b5f25fb4970 --- /dev/null +++ b/sdk/typescript/test/e2e/object-display-standard.test.ts @@ -0,0 +1,47 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it, expect, beforeAll } from 'vitest'; +import { LocalTxnDataSerializer, ObjectId, RawSigner } from '../../src'; +import { publishPackage, setup, TestToolbox } from './utils/setup'; + +describe('Test Object Display Standard', () => { + let toolbox: TestToolbox; + let signer: RawSigner; + let packageId: ObjectId; + + beforeAll(async () => { + toolbox = await setup(); + signer = new RawSigner( + toolbox.keypair, + toolbox.provider, + new LocalTxnDataSerializer(toolbox.provider), + ); + const packagePath = __dirname + '/./data/display_test'; + packageId = await publishPackage(signer, true, packagePath); + }); + + it('Test getting Display Object', async () => { + const boarId = ( + await toolbox.provider.getObjectsOwnedByAddress( + toolbox.address(), + `${packageId}::boars::Boar`, + ) + )[0].objectId; + const display = await toolbox.provider.call('sui_getDisplayDeprecated', [ + boarId, + ]); + expect(display).toEqual({ + age: '10', + buyer: `0x${toolbox.address()}`, + creator: 'Chris', + description: `Unique Boar from the Boars collection with First Boar and ${boarId}`, + img_url: 'https://get-a-boar.com/first.png', + name: 'First Boar', + price: '', + project_url: 'https://get-a-boar.com/', + full_url: 'https://get-a-boar.fullurl.com/', + escape_syntax: '{name}', + }); + }); +}); diff --git a/sdk/typescript/test/e2e/utils/setup.ts b/sdk/typescript/test/e2e/utils/setup.ts index 662e6df6b59b6..1a881abc3559d 100644 --- a/sdk/typescript/test/e2e/utils/setup.ts +++ b/sdk/typescript/test/e2e/utils/setup.ts @@ -93,5 +93,9 @@ export async function publishPackage( const publishEvent = getEvents(publishTxn)?.find((e) => 'publish' in e); // @ts-ignore: Publish not narrowed: - return publishEvent?.publish.packageId.replace(/^(0x)(0+)/, '0x'); + const packageId = publishEvent?.publish.packageId.replace(/^(0x)(0+)/, '0x'); + console.info( + `Published package ${packageId} from address ${await signer.getAddress()}}`, + ); + return packageId; }