SuiGrpcState
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.
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
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const ENDPOINT = 'api-sui-mainnet-full.n.dwellir.com:443';
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:443'
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.