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#
| Parameter | Type | Required | Description |
|---|---|---|---|
object_id | string | Yes | ID of the parent object |
limit | uint32 | No | Maximum fields to return (default: 50, max: 100) |
page_token | string | No | Token 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#
- TypeScript
- Python
- Go
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');
}
import grpc
import livedata_service_pb2
import livedata_service_pb2_grpc
ENDPOINT = 'api-sui-mainnet-full.n.dwellir.com'
API_TOKEN = 'your_api_token_here'
def list_dynamic_fields(
object_id: str,
limit: int = 50,
page_token: str = None
):
credentials = grpc.ssl_channel_credentials()
channel = grpc.secure_channel(ENDPOINT, credentials)
client = livedata_service_pb2_grpc.LiveDataServiceStub(channel)
request = livedata_service_pb2.ListDynamicFieldsRequest(
object_id=object_id,
limit=limit
)
if page_token:
request.page_token = page_token
metadata = [('x-api-key', API_TOKEN)]
response = client.ListDynamicFields(request, metadata=metadata)
print(f'Found {len(response.fields)} fields')
for field in response.fields:
print(f'Field: {field.name}')
print(f' Type: {field.type}')
print(f' Object ID: {field.object_id}')
print('---')
channel.close()
return response
# Usage
object_id = '0x6a7c8c6e8d3b2f1a4e9c5d7b2f8a3c1e4d6a9b5c7e2f1a8d3c5b7e9f2a4c6d8'
result = list_dynamic_fields(object_id)
package main
import (
"context"
"fmt"
"log"
"time"
"sui-grpc-client/config"
pb "sui-grpc-client/sui/rpc/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
func main() {
// Load configuration from .env file
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Create TLS credentials
creds := credentials.NewClientTLSFromCert(nil, "")
// Connect to Dwellir
conn, err := grpc.NewClient(
cfg.Endpoint,
grpc.WithTransportCredentials(creds),
)
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// Create state service client
client := pb.NewStateServiceClient(conn)
// Add authentication
ctx := metadata.AppendToOutgoingContext(
context.Background(),
"x-api-key", cfg.APIKey,
)
// Set timeout
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Example parent object with dynamic fields
// Using the 0x5 system state object which has dynamic fields
parent := "0x0000000000000000000000000000000000000000000000000000000000000005"
fmt.Printf("Listing dynamic fields for parent: %s\n\n", parent)
pageSize := uint32(10)
var pageToken []byte
totalFields := 0
pageNum := 1
// Specify which fields to retrieve
fields := []string{
"kind",
"parent",
"field_id",
"value_type",
}
for {
request := &pb.ListDynamicFieldsRequest{
Parent: &parent,
PageSize: &pageSize,
ReadMask: &fieldmaskpb.FieldMask{Paths: fields},
}
if pageToken != nil {
request.PageToken = pageToken
}
response, err := client.ListDynamicFields(ctx, request)
if err != nil {
log.Fatalf("Failed to list dynamic fields: %v", err)
}
dynamicFields := response.GetDynamicFields()
if len(dynamicFields) == 0 {
if totalFields == 0 {
fmt.Println("No dynamic fields found (object may not have dynamic fields)")
}
break
}
fmt.Printf("Page %d - Found %d dynamic field(s):\n", pageNum, len(dynamicFields))
fmt.Println("====================================")
for i, field := range dynamicFields {
fmt.Printf("%d. Field ID: %s\n", totalFields+i+1, field.GetFieldId())
// Display field kind
kind := field.GetKind()
switch kind {
case pb.DynamicField_FIELD:
fmt.Printf(" Kind: FIELD\n")
case pb.DynamicField_OBJECT:
fmt.Printf(" Kind: OBJECT\n")
default:
fmt.Printf(" Kind: UNKNOWN\n")
}
if parent := field.GetParent(); parent != "" {
fmt.Printf(" Parent: %s\n", parent)
}
if valueType := field.GetValueType(); valueType != "" {
fmt.Printf(" Value Type: %s\n", valueType)
}
if childId := field.GetChildId(); childId != "" {
fmt.Printf(" Child ID: %s\n", childId)
}
fmt.Println()
}
totalFields += len(dynamicFields)
// Check if there are more pages
nextPageToken := response.GetNextPageToken()
if len(nextPageToken) == 0 {
break
}
pageToken = nextPageToken
pageNum++
// Limit to first 3 pages for demonstration
if pageNum > 3 {
fmt.Println("(Stopping after 3 pages for demonstration)")
break
}
}
fmt.Printf("\nTotal dynamic fields found: %d\n", totalFields)
}
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.