Skip to main content

ListDynamicFields

Query Dynamic Fields on Objects#

The ListDynamicFields method retrieves all dynamic fields attached to a specific Sui object. Dynamic fields allow objects to be extended with additional data at runtime, commonly used for collections, key-value stores, and flexible data structures. This method supports pagination for objects with many fields.

Method Signature#

Service: sui.rpc.v2beta2.LiveDataService Method: ListDynamicFields Type: Unary RPC

Parameters#

ParameterTypeRequiredDescription
object_idstringYesID of the parent object
limituint32NoMaximum fields to return (default: 50, max: 100)
page_tokenstringNoToken for pagination

Response Structure#

message ListDynamicFieldsResponse {
repeated DynamicField fields = 1;
string next_page_token = 2;
}

message DynamicField {
string name = 1;
string type = 2;
string object_type = 3;
string object_id = 4;
uint64 version = 5;
string digest = 6;
}

Field Descriptions#

  • name: Field name/key (serialized as string)
  • type: Type of the field name
  • object_type: Type of the stored value
  • object_id: ID of the field value object
  • version: Version number of the field
  • digest: Content digest

Code Examples#

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';

const ENDPOINT = 'api-sui-mainnet-full.n.dwellir.com';
const API_TOKEN = 'your_api_token_here';

const packageDefinition = protoLoader.loadSync('./protos/livedata.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
includeDirs: ['./protos']
});

const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any;
const credentials = grpc.credentials.createSsl();
const client = new protoDescriptor.sui.rpc.v2beta2.LiveDataService(ENDPOINT, credentials);

const metadata = new grpc.Metadata();
metadata.add('x-api-key', API_TOKEN);

async function listDynamicFields(
objectId: string,
limit?: number,
pageToken?: string
): Promise<any> {
return new Promise((resolve, reject) => {
const request: any = {
object_id: objectId
};

if (limit) request.limit = limit;
if (pageToken) request.page_token = pageToken;

client.ListDynamicFields(request, metadata, (error: any, response: any) => {
if (error) {
console.error('ListDynamicFields error:', error.message);
reject(error);
return;
}

resolve(response);
});
});
}

// Usage
const fields = await listDynamicFields(
'0x6a7c8c6e8d3b2f1a4e9c5d7b2f8a3c1e4d6a9b5c7e2f1a8d3c5b7e9f2a4c6d8',
50
);

console.log(`Found ${fields.fields.length} fields`);
fields.fields.forEach((field: any) => {
console.log(`Field: ${field.name}`);
console.log(` Type: ${field.type}`);
console.log(` Value Object: ${field.object_id}`);
});

if (fields.next_page_token) {
console.log('More fields available, use pagination');
}

Use Cases#

Fetch All Dynamic Fields with Pagination#

async function fetchAllDynamicFields(objectId: string): Promise<any[]> {
const allFields: any[] = [];
let pageToken: string | undefined = undefined;

do {
const response = await listDynamicFields(objectId, 50, pageToken);

allFields.push(...response.fields);
pageToken = response.next_page_token;

console.log(`Fetched ${response.fields.length} fields, total: ${allFields.length}`);

} while (pageToken);

return allFields;
}

// Usage
const allFields = await fetchAllDynamicFields(objectId);
console.log(`Total fields: ${allFields.length}`);

Explore Collection/Map Structure#

interface DynamicFieldInfo {
name: string;
type: string;
valueObjectId: string;
valueType: string;
}

async function exploreCollection(objectId: string): Promise<DynamicFieldInfo[]> {
const fields = await fetchAllDynamicFields(objectId);

return fields.map(field => ({
name: field.name,
type: field.type,
valueObjectId: field.object_id,
valueType: field.object_type
}));
}

// Usage for exploring a Map or Table
const collectionFields = await exploreCollection(tableObjectId);

console.log('Collection contains:');
collectionFields.forEach(field => {
console.log(` ${field.name}: ${field.valueType}`);
});

Fetch Field Values#

async function getDynamicFieldValue(
parentObjectId: string,
fieldName: string
): Promise<any | null> {
const fields = await fetchAllDynamicFields(parentObjectId);

const field = fields.find(f => f.name === fieldName);

if (!field) {
console.warn(`Field "${fieldName}" not found`);
return null;
}

// Fetch the value object
const valueObject = await getObject(field.object_id);

return valueObject;
}

// Usage
const userProfile = await getDynamicFieldValue(
registryObjectId,
'user_123'
);

console.log('User profile:', userProfile);

Monitor Field Changes#

class DynamicFieldMonitor {
private previousFields = new Map<string, Set<string>>();

async checkForChanges(objectId: string): Promise<{
added: string[];
removed: string[];
modified: string[];
}> {
const currentFields = await fetchAllDynamicFields(objectId);
const currentSet = new Set(
currentFields.map(f => `${f.name}:${f.version}`)
);

const previousSet = this.previousFields.get(objectId) || new Set();

const added: string[] = [];
const removed: string[] = [];
const modified: string[] = [];

// Check for added and modified fields
for (const field of currentFields) {
const key = `${field.name}:${field.version}`;
const prevKey = Array.from(previousSet).find(k => k.startsWith(`${field.name}:`));

if (!prevKey) {
added.push(field.name);
} else if (prevKey !== key) {
modified.push(field.name);
}
}

// Check for removed fields
const currentNames = new Set(currentFields.map(f => f.name));
for (const prev of previousSet) {
const name = prev.split(':')[0];
if (!currentNames.has(name)) {
removed.push(name);
}
}

this.previousFields.set(objectId, currentSet);

return { added, removed, modified };
}
}

// Usage
const monitor = new DynamicFieldMonitor();

setInterval(async () => {
const changes = await monitor.checkForChanges(objectId);

if (changes.added.length > 0) {
console.log('Fields added:', changes.added);
}
if (changes.removed.length > 0) {
console.log('Fields removed:', changes.removed);
}
if (changes.modified.length > 0) {
console.log('Fields modified:', changes.modified);
}
}, 10000);

Build Field Index#

interface FieldIndex {
[fieldName: string]: {
objectId: string;
type: string;
version: number;
};
}

async function buildFieldIndex(objectId: string): Promise<FieldIndex> {
const fields = await fetchAllDynamicFields(objectId);

const index: FieldIndex = {};

for (const field of fields) {
index[field.name] = {
objectId: field.object_id,
type: field.object_type,
version: parseInt(field.version)
};
}

return index;
}

// Usage
const index = await buildFieldIndex(objectId);

// Quick lookups
const userField = index['user_123'];
if (userField) {
console.log(`User object: ${userField.objectId}`);
}

Search Fields by Pattern#

async function searchFields(
objectId: string,
pattern: RegExp
): Promise<any[]> {
const allFields = await fetchAllDynamicFields(objectId);

return allFields.filter(field => pattern.test(field.name));
}

// Usage
const userFields = await searchFields(
registryObjectId,
/^user_\d+$/
);

console.log(`Found ${userFields.length} user fields`);

Best Practices#

Efficient Pagination#

async function fetchFieldsPage(
objectId: string,
pageSize: number = 50
): Promise<AsyncGenerator<any[], void, unknown>> {
return async function* () {
let pageToken: string | undefined = undefined;

do {
const response = await listDynamicFields(objectId, pageSize, pageToken);

yield response.fields;

pageToken = response.next_page_token;
} while (pageToken);
}();
}

// Usage with async iteration
const pages = await fetchFieldsPage(objectId);

for await (const page of pages) {
console.log(`Processing ${page.length} fields`);
// Process each page
}

Cache Field Listings#

class DynamicFieldCache {
private cache = new Map<string, { data: any[]; timestamp: number }>();
private ttl = 30000; // 30 seconds

async getFields(objectId: string): Promise<any[]> {
const cached = this.cache.get(objectId);

if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}

const fields = await fetchAllDynamicFields(objectId);

this.cache.set(objectId, {
data: fields,
timestamp: Date.now()
});

return fields;
}

invalidate(objectId: string): void {
this.cache.delete(objectId);
}
}

Handle Empty Results#

async function getDynamicFieldsSafe(objectId: string): Promise<any[]> {
try {
const fields = await fetchAllDynamicFields(objectId);

if (fields.length === 0) {
console.log(`Object ${objectId} has no dynamic fields`);
}

return fields;
} catch (error: any) {
if (error.code === grpc.status.NOT_FOUND) {
console.warn(`Object ${objectId} not found`);
return [];
}

throw error;
}
}

Performance Characteristics#

MetricValue
Typical Latency20-60ms (depends on field count)
Page Size50 recommended, 100 max
Response Size1-5KB per page
Cache RecommendedYes (30s TTL)

Common Errors#

Error CodeScenarioSolution
NOT_FOUNDObject doesn't existVerify object ID
INVALID_ARGUMENTInvalid object ID formatCheck ID format
OUT_OF_RANGELimit exceeds maximumUse limit ≤ 100
INVALID_ARGUMENTInvalid page tokenUse token from previous response

Dynamic Fields vs Static Fields#

AspectDynamic FieldsStatic Fields
Definition TimeRuntimeCompile time
FlexibilityHighLow
Query MethodListDynamicFieldsGetObject
Use CaseCollections, mapsFixed structure
Gas CostHigherLower

Need help? Contact support@dwellir.com or check the gRPC overview.