ListDynamicFields - Query Object Dynamic Fields
Retrieve dynamic fields attached to Sui objects via gRPC with pagination support. Essential for exploring extensible objects and collections with Dwellir.
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.
Overview
Sui's object-centric model allows objects to carry dynamic fields--additional key-value pairs that can be added, modified, or removed at runtime without changing the object's Move struct definition. This mechanism underpins Sui's collection types (Table, Bag, ObjectTable, ObjectBag) and is commonly used for on-chain registries, user profiles, game inventories, and any pattern requiring flexible storage.
There are two types of dynamic fields on Sui: dynamic fields (where the value is stored inline) and dynamic object fields (where the value is a standalone object that can be accessed independently). The ListDynamicFields response includes both types, with the kind field distinguishing between them. Understanding this distinction is important because dynamic object fields can be transferred or accessed directly by their object ID, while regular dynamic fields can only be accessed through the parent object.
Key Capabilities
- Collection Browsing: Enumerate all entries in
Table,Bag, and custom collection types - Pagination Support: Handle objects with thousands of dynamic fields through page-based iteration
- Field Type Inspection: Discover the key type and value type of each dynamic field
- Object Discovery: Find child object IDs stored in dynamic object fields
- Change Monitoring: Track additions, removals, and modifications to an object's dynamic fields over time
Method Signature
Service: sui.rpc.v2.StateService
Method: ListDynamicFields
Type: Unary RPC
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
| Metric | Value |
|---|---|
| Typical Latency | 20-60ms (depends on field count) |
| Page Size | 50 recommended, 100 max |
| Response Size | 1-5KB per page |
| Cache Recommended | Yes (30s TTL) |
Common Errors
| Error Code | Scenario | Solution |
|---|---|---|
NOT_FOUND | Object doesn't exist | Verify object ID |
INVALID_ARGUMENT | Invalid object ID format | Check ID format |
OUT_OF_RANGE | Limit exceeds maximum | Use limit ≤ 100 |
INVALID_ARGUMENT | Invalid page token | Use token from previous response |
Dynamic Fields vs Static Fields
| Aspect | Dynamic Fields | Static Fields |
|---|---|---|
| Definition Time | Runtime | Compile time |
| Flexibility | High | Low |
| Query Method | ListDynamicFields | GetObject |
| Use Case | Collections, maps | Fixed structure |
| Gas Cost | Higher | Lower |
Related Methods
- GetObject - Get object with static fields
- ListOwnedObjects - List objects owned by address
Need help? Contact support@dwellir.com or check the gRPC overview.
ListBalances
Retrieve all token balances for a Sui address via gRPC including multiple coin types. Essential for portfolio displays and multi-token wallets with Dwellir.
ListOwnedObjects
Query all objects owned by a Sui address via gRPC with pagination support. Essential for wallet interfaces, portfolio tracking, and asset discovery with Dwellir's infrastructure.