ListOwnedObjects
Discover All Assets Owned by an Address#
The ListOwnedObjects method returns a paginated list of all objects owned by a specific Sui address. This is fundamental for wallet applications, portfolio dashboards, and any interface that needs to display a user's blockchain assets including NFTs, coins, and custom objects.
Overview#
Every user on Sui owns a collection of objects—from SUI coins and custom tokens to NFTs and smart contract instances. The ListOwnedObjects method provides efficient enumeration of these assets with built-in pagination for handling large collections. Applications use this to build asset lists, calculate portfolio values, and enable user interactions with their blockchain holdings.
Method Signature#
Service: sui.rpc.v2beta2.LiveDataService
Method: ListOwnedObjects
Type: Unary RPC with pagination
Parameters#
| Parameter | Type | Required | Description |
|---|---|---|---|
owner | string | Yes | Address to query (66-char hex with 0x prefix) |
page_size | uint32 | No | Objects per page (default: 50, max: 200) |
page_token | string | No | Continuation token from previous response |
read_mask | FieldMask | No | Fields to include for each object |
object_type | string | No | Filter by object type |
Field Mask Options#
| Path | Description |
|---|---|
object_id | Object identifier |
version | Version number |
digest | Object digest |
owner | Ownership details |
object_type | Type classification |
has_public_transfer | Transfer capability |
contents | Object data fields |
Response Structure#
message ListOwnedObjectsResponse {
repeated Object objects = 1;
string next_page_token = 2;
}
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/live_data.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);
// List objects with pagination
async function listOwnedObjects(
owner: string,
pageSize: number = 50,
pageToken?: string
): Promise<{ objects: any[]; nextPageToken: string }> {
return new Promise((resolve, reject) => {
const request: any = {
owner: owner,
page_size: pageSize,
read_mask: {
paths: [
'object_id',
'version',
'digest',
'owner',
'object_type',
'has_public_transfer'
]
}
};
if (pageToken) {
request.page_token = pageToken;
}
client.ListOwnedObjects(request, metadata, (error: any, response: any) => {
if (error) {
console.error('ListOwnedObjects error:', error.message);
reject(error);
return;
}
resolve({
objects: response.objects || [],
nextPageToken: response.next_page_token || ''
});
});
});
}
// Fetch all owned objects (handles pagination automatically)
async function fetchAllOwnedObjects(owner: string): Promise<any[]> {
const allObjects: any[] = [];
let pageToken: string | undefined = undefined;
let pageCount = 0;
do {
const { objects, nextPageToken } = await listOwnedObjects(
owner,
50,
pageToken
);
allObjects.push(...objects);
pageToken = nextPageToken;
pageCount++;
console.log(`Fetched page ${pageCount}: ${objects.length} objects`);
} while (pageToken);
console.log(`\n✓ Total objects: ${allObjects.length}`);
return allObjects;
}
// Categorize objects by type
interface CategorizedAssets {
nfts: any[];
coins: any[];
packages: any[];
other: any[];
}
async function categorizeUserAssets(owner: string): Promise<CategorizedAssets> {
const objects = await fetchAllOwnedObjects(owner);
const assets: CategorizedAssets = {
nfts: [],
coins: [],
packages: [],
other: []
};
objects.forEach(obj => {
const type = obj.object_type.toLowerCase();
if (type.includes('::coin::coin')) {
assets.coins.push(obj);
} else if (type.includes('::package::')) {
assets.packages.push(obj);
} else if (obj.has_public_transfer && !type.includes('::coin::')) {
assets.nfts.push(obj);
} else {
assets.other.push(obj);
}
});
console.log('\nAsset Summary:');
console.log('==============');
console.log(`Coins: ${assets.coins.length}`);
console.log(`NFTs: ${assets.nfts.length}`);
console.log(`Packages: ${assets.packages.length}`);
console.log(`Other: ${assets.other.length}`);
return assets;
}
// Filter by object type
async function listObjectsByType(
owner: string,
objectType: string
): Promise<any[]> {
return new Promise((resolve, reject) => {
const request = {
owner: owner,
object_type: objectType,
page_size: 100,
read_mask: {
paths: ['object_id', 'object_type', 'contents']
}
};
client.ListOwnedObjects(request, metadata, (error: any, response: any) => {
if (error) reject(error);
else resolve(response.objects || []);
});
});
}
// Usage examples
const userAddress = '0xac5bceec1b789ff840d7d4e6ce4ce61c90d190a7f8c4f4ddf0bff6ee2413c33c';
// List first page
const firstPage = await listOwnedObjects(userAddress);
console.log(`Page 1: ${firstPage.objects.length} objects`);
// Fetch all objects
const allObjects = await fetchAllOwnedObjects(userAddress);
// Categorize assets
const categorized = await categorizeUserAssets(userAddress);
// Filter by type
const suiCoins = await listObjectsByType(userAddress, '0x2::coin::Coin<0x2::sui::SUI>');
console.log(`SUI coins: ${suiCoins.length}`);
import grpc
from typing import List, Dict, Optional
from google.protobuf import field_mask_pb2
import live_data_service_pb2
import live_data_service_pb2_grpc
ENDPOINT = 'api-sui-mainnet-full.n.dwellir.com'
API_TOKEN = 'your_api_token_here'
class OwnedObjectsLister:
def __init__(self, endpoint: str, api_token: str):
self.endpoint = endpoint
self.api_token = api_token
self.channel = None
self.client = None
def connect(self):
"""Establish gRPC connection"""
credentials = grpc.ssl_channel_credentials()
self.channel = grpc.secure_channel(self.endpoint, credentials)
self.client = live_data_service_pb2_grpc.LiveDataServiceStub(self.channel)
def list_owned_objects(
self,
owner: str,
page_size: int = 50,
page_token: Optional[str] = None,
fields: Optional[List[str]] = None
):
"""List objects owned by address with pagination"""
if not self.client:
self.connect()
metadata = [('x-api-key', self.api_token)]
if fields is None:
fields = [
'object_id',
'version',
'digest',
'owner',
'object_type',
'has_public_transfer'
]
request = live_data_service_pb2.ListOwnedObjectsRequest(
owner=owner,
page_size=page_size,
read_mask=field_mask_pb2.FieldMask(paths=fields)
)
if page_token:
request.page_token = page_token
try:
response = self.client.ListOwnedObjects(request, metadata=metadata)
return {
'objects': list(response.objects),
'next_page_token': response.next_page_token
}
except grpc.RpcError as e:
print(f'Error: {e.code()}: {e.details()}')
raise
def fetch_all_owned_objects(self, owner: str) -> List:
"""Fetch all objects with automatic pagination"""
all_objects = []
page_token = None
page_count = 0
while True:
result = self.list_owned_objects(owner, page_size=50, page_token=page_token)
objects = result['objects']
all_objects.extend(objects)
page_count += 1
print(f'Fetched page {page_count}: {len(objects)} objects')
page_token = result['next_page_token']
if not page_token:
break
print(f'\n✓ Total objects: {len(all_objects)}')
return all_objects
def categorize_assets(self, owner: str) -> Dict[str, List]:
"""Categorize user assets by type"""
objects = self.fetch_all_owned_objects(owner)
categories = {
'nfts': [],
'coins': [],
'packages': [],
'other': []
}
for obj in objects:
obj_type = obj.object_type.lower()
if '::coin::coin' in obj_type:
categories['coins'].append(obj)
elif '::package::' in obj_type:
categories['packages'].append(obj)
elif obj.has_public_transfer and '::coin::' not in obj_type:
categories['nfts'].append(obj)
else:
categories['other'].append(obj)
print('\nAsset Summary:')
print('=' * 40)
print(f'Coins: {len(categories["coins"])}')
print(f'NFTs: {len(categories["nfts"])}')
print(f'Packages: {len(categories["packages"])}')
print(f'Other: {len(categories["other"])}')
return categories
def close(self):
if self.channel:
self.channel.close()
# Usage
def main():
lister = OwnedObjectsLister(ENDPOINT, API_TOKEN)
try:
address = '0xac5bceec1b789ff840d7d4e6ce4ce61c90d190a7f8c4f4ddf0bff6ee2413c33c'
# List first page
first_page = lister.list_owned_objects(address)
print(f'First page: {len(first_page["objects"])} objects')
# Fetch all objects
all_objects = lister.fetch_all_owned_objects(address)
# Categorize
categories = lister.categorize_assets(address)
# Print NFTs
print('\nNFTs:')
for nft in categories['nfts'][:5]: # First 5
print(f' {nft.object_id}: {nft.object_type}')
except Exception as e:
print(f'Error: {e}')
finally:
lister.close()
if __name__ == '__main__':
main()
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 address - use an address that owns objects
owner := "0x0000000000000000000000000000000000000000000000000000000000000000"
fmt.Printf("Listing owned objects for: %s\n\n", owner)
pageSize := uint32(10)
var pageToken []byte
totalObjects := 0
pageNum := 1
// Specify which fields to retrieve
fields := []string{
"object_id",
"version",
"object_type",
"digest",
}
for {
request := &pb.ListOwnedObjectsRequest{
Owner: &owner,
PageSize: &pageSize,
ReadMask: &fieldmaskpb.FieldMask{Paths: fields},
}
if pageToken != nil {
request.PageToken = pageToken
}
response, err := client.ListOwnedObjects(ctx, request)
if err != nil {
log.Fatalf("Failed to list owned objects: %v", err)
}
objects := response.GetObjects()
if len(objects) == 0 {
if totalObjects == 0 {
fmt.Println("No objects found (address may not own any objects)")
}
break
}
fmt.Printf("Page %d - Found %d object(s):\n", pageNum, len(objects))
fmt.Println("====================================")
for i, obj := range objects {
fmt.Printf("%d. Object ID: %s\n", totalObjects+i+1, obj.GetObjectId())
fmt.Printf(" Version: %d\n", obj.GetVersion())
if digest := obj.GetDigest(); digest != "" {
fmt.Printf(" Digest: %s\n", digest)
}
if objType := obj.GetObjectType(); objType != "" {
fmt.Printf(" Type: %s\n", objType)
}
fmt.Println()
}
totalObjects += len(objects)
// 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 objects found: %d\n", totalObjects)
}
Use Cases#
1. Wallet Asset Display#
Build comprehensive wallet interfaces:
interface WalletAsset {
id: string;
type: string;
category: 'coin' | 'nft' | 'other';
displayName: string;
imageUrl?: string;
value?: string;
}
async function buildWalletView(owner: string): Promise<WalletAsset[]> {
const objects = await fetchAllOwnedObjects(owner);
return objects.map(obj => ({
id: obj.object_id,
type: obj.object_type,
category: categorizeObject(obj),
displayName: extractDisplayName(obj),
imageUrl: extractImage(obj),
value: extractValue(obj)
}));
}
function categorizeObject(obj: any): 'coin' | 'nft' | 'other' {
if (obj.object_type.includes('::coin::Coin')) return 'coin';
if (obj.has_public_transfer) return 'nft';
return 'other';
}
2. NFT Gallery#
Display user's NFT collection:
interface NFTDisplay {
id: string;
name: string;
image: string;
collection: string;
attributes: Record<string, any>;
}
async function loadNFTGallery(owner: string): Promise<NFTDisplay[]> {
const allObjects = await fetchAllOwnedObjects(owner);
const nfts = allObjects.filter(obj =>
obj.has_public_transfer &&
!obj.object_type.includes('::coin::')
);
return nfts.map(nft => ({
id: nft.object_id,
name: nft.contents?.fields?.name || 'Unnamed',
image: nft.contents?.fields?.url || '',
collection: extractCollection(nft.object_type),
attributes: nft.contents?.fields || {}
}));
}
function extractCollection(objectType: string): string {
const parts = objectType.split('::');
return parts[parts.length - 1];
}
3. Portfolio Value Calculation#
Calculate total portfolio value:
async function calculatePortfolioValue(owner: string): Promise<number> {
const objects = await fetchAllOwnedObjects(owner);
const coins = objects.filter(obj =>
obj.object_type.includes('::coin::Coin')
);
let totalValue = 0;
for (const coin of coins) {
const coinType = extractCoinType(coin.object_type);
const balance = await getBalance(owner, coinType);
const price = await getCoinPrice(coinType);
totalValue += (parseInt(balance) / 1e9) * price;
}
return totalValue;
}
4. Asset Transfer Preparation#
Prepare objects for batch transfer:
async function prepareTransferables(owner: string): Promise<string[]> {
const objects = await fetchAllOwnedObjects(owner);
return objects
.filter(obj => obj.has_public_transfer)
.map(obj => obj.object_id);
}
Pagination Best Practices#
Efficient Pagination#
Handle large collections efficiently:
async function* paginatedObjectStream(
owner: string,
pageSize: number = 50
): AsyncGenerator<any[], void, unknown> {
let pageToken: string | undefined = undefined;
do {
const { objects, nextPageToken } = await listOwnedObjects(
owner,
pageSize,
pageToken
);
yield objects;
pageToken = nextPageToken;
} while (pageToken);
}
// Usage
for await (const objectsPage of paginatedObjectStream(userAddress)) {
console.log(`Processing ${objectsPage.length} objects...`);
// Process page
}
Progressive Loading#
Load assets progressively for better UX:
async function loadAssetsProgressively(
owner: string,
onPageLoaded: (objects: any[], totalSoFar: number) => void
): Promise<any[]> {
const allObjects: any[] = [];
let pageToken: string | undefined = undefined;
do {
const { objects, nextPageToken } = await listOwnedObjects(
owner,
50,
pageToken
);
allObjects.push(...objects);
onPageLoaded(objects, allObjects.length);
pageToken = nextPageToken;
} while (pageToken);
return allObjects;
}
// Usage
await loadAssetsProgressively(userAddress, (page, total) => {
console.log(`Loaded ${page.length} more. Total: ${total}`);
updateUI(page); // Update UI incrementally
});
Performance Optimization#
Caching Strategy#
Cache object lists with TTL:
interface CachedObjectList {
objects: any[];
timestamp: number;
owner: string;
}
const cache = new Map<string, CachedObjectList>();
const CACHE_TTL = 30000; // 30 seconds
async function getCachedOwnedObjects(owner: string): Promise<any[]> {
const cached = cache.get(owner);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
console.log('✓ Using cached data');
return cached.objects;
}
const objects = await fetchAllOwnedObjects(owner);
cache.set(owner, {
objects,
timestamp: Date.now(),
owner
});
return objects;
}
Error Handling#
async function safeListOwnedObjects(owner: string): Promise<any[]> {
try {
return await fetchAllOwnedObjects(owner);
} catch (error: any) {
if (error.code === grpc.status.INVALID_ARGUMENT) {
console.error('Invalid address format');
return [];
}
if (error.code === grpc.status.NOT_FOUND) {
console.log('Address has no objects');
return [];
}
throw error;
}
}
Related Methods#
- GetObject - Get detailed object information
- BatchGetObjects - Fetch multiple objects
Need help with asset enumeration? Contact support@dwellir.com or check the gRPC overview.