Skip to content

Commit

Permalink
feat(wallet-dashboard): update migration portfolio to include shared …
Browse files Browse the repository at this point in the history
…objects (#4974)

* feat(core): add StardustIndexerClient

* feat(dashboard): add stardust objects from indexer.

* feat(core): enhance StardustIndexerClient with basic output mapping and new types

* feat(dashboard): add NFT resolved outputs retrieval and mapping functions

* refactor(core, dashboard): unify output types and enhance mapping functions

* feat(wallet-dashboard, core): enhance Stardust output handling and add pagination support

* fix(core, dashboard): pr fixes

* feat(sdk): add metadata configuration for stardustIndexer in .env.defaults

* refactor(core): rename limit parameter to page_size in StardustIndexerClient

* refactor(core): update PageParams interface and adjust pagination parameters in StardustIndexerClient

* refactor(core): update return_amount

* refactor(core): update StardustIndexerClient methods and rename hook for clarity

* refactor(dashboard): remove unused variables in useGetAllStardustSharedObjects hook

* fix: add missing 0x to stardust package id

* refactor(core, dashboard): update Stardust package ID format, improve metadata handling in hooks, rename variables.

---------

Co-authored-by: cpl121 <[email protected]>
Co-authored-by: Begoña Alvarez <[email protected]>
  • Loading branch information
3 people authored Jan 29, 2025
1 parent 26ca13a commit 9d988ec
Show file tree
Hide file tree
Showing 18 changed files with 397 additions and 35 deletions.
72 changes: 72 additions & 0 deletions apps/core/src/api/StardustIndexerClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { StardustIndexerOutput } from '../utils';

export interface PageParams {
page?: number;
pageSize?: number;
}

export class StardustIndexerClient {
private baseUrl: string;

constructor(baseUrl?: string) {
if (!baseUrl) {
throw new Error('Base URL for IndexerAPI is required.');
}
this.baseUrl = baseUrl;
}

private async request<T>(
endpoint: string,
options?: RequestInit,
params?: Record<string, string | number | undefined>,
): Promise<T> {
const url = new URL(`${this.baseUrl}${endpoint}`);

// Append query parameters if provided
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, value.toString());
}
});
}

const response = await fetch(url, {
...(options ?? {}),
headers: {
'Content-Type': 'application/json',
...(options?.headers || {}),
},
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`API Error: ${response.status} ${response.statusText} - ${errorText}`);
}

return response.json();
}

public getBasicResolvedOutputs = async (
address: string,
params?: PageParams,
): Promise<StardustIndexerOutput[]> => {
return this.request(`/v1/basic/resolved/${address}`, undefined, {
page: params?.page,
page_size: params?.pageSize,
});
};

public getNftResolvedOutputs = async (
address: string,
params?: PageParams,
): Promise<StardustIndexerOutput[]> => {
return this.request(`/v1/nft/resolved/${address}`, undefined, {
page: params?.page,
page_size: params?.pageSize,
});
};
}
1 change: 1 addition & 0 deletions apps/core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
// SPDX-License-Identifier: Apache-2.0

export * from './SentryHttpTransport';
export * from './StardustIndexerClient';
5 changes: 4 additions & 1 deletion apps/core/src/constants/migration.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';

export const STARDUST_PACKAGE_ID =
'000000000000000000000000000000000000000000000000000000000000107a';
'0x000000000000000000000000000000000000000000000000000000000000107a';
export const STARDUST_BASIC_OUTPUT_TYPE = `${STARDUST_PACKAGE_ID}::basic_output::BasicOutput<${IOTA_TYPE_ARG}>`;
export const STARDUST_NFT_OUTPUT_TYPE = `${STARDUST_PACKAGE_ID}::nft_output::NftOutput<${IOTA_TYPE_ARG}>`;
export const STARDUST_EXPIRATION_UNLOCK_CONDITION_TYPE = `${STARDUST_PACKAGE_ID}::expiration_unlock_condition::ExpirationUnlockCondition`;
export const STARDUST_STORAGE_DEPOSIT_RETURN_UC_TYPE = `${STARDUST_PACKAGE_ID}::storage_deposit_return_unlock_condition::StorageDepositReturnUnlockCondition`;
export const STARDUST_TIMELOCK_TYPE = `${STARDUST_PACKAGE_ID}::timelock_unlock_condition::TimelockUnlockCondition`;
40 changes: 40 additions & 0 deletions apps/core/src/contexts/StardustIndexerClientContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useContext, createContext, useMemo } from 'react';
import { StardustIndexerClient } from '../';
import { getNetwork } from '@iota/iota-sdk/client';

type StardustIndexerClientContextType = {
stardustIndexerClient: StardustIndexerClient | null;
};

export const StardustIndexerClientContext = createContext<StardustIndexerClientContextType | null>(
null,
);

export function useStardustIndexerClientContext(): StardustIndexerClientContextType {
const context = useContext(StardustIndexerClientContext);
if (!context) {
throw new Error('useStardustIndexerClient must be used within a StardustClientProvider');
}
return context;
}

export function useStardustIndexerClient(network?: string) {
const { metadata } = getNetwork<{
stardustIndexer?: string;
}>(network || '');

const stardustIndexerClient = useMemo(() => {
if (metadata?.stardustIndexer) {
return new StardustIndexerClient(metadata?.stardustIndexer);
} else {
return null;
}
}, [metadata?.stardustIndexer]);

return {
stardustIndexerClient,
};
}
1 change: 1 addition & 0 deletions apps/core/src/contexts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

export * from './ThemeContext';
export * from './HiddenAssetsProvider';
export * from './StardustIndexerClientContext';
1 change: 1 addition & 0 deletions apps/core/src/utils/migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

export * from './createMigrationTransaction';
export * from './types';
export * from './mapStardustIndexerOutputs';
83 changes: 83 additions & 0 deletions apps/core/src/utils/migration/mapStardustIndexerOutputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { IotaObjectData } from '@iota/iota-sdk/client';
import {
STARDUST_BASIC_OUTPUT_TYPE,
STARDUST_EXPIRATION_UNLOCK_CONDITION_TYPE,
STARDUST_NFT_OUTPUT_TYPE,
STARDUST_STORAGE_DEPOSIT_RETURN_UC_TYPE,
STARDUST_TIMELOCK_TYPE,
} from '../../constants';
import { StardustIndexerOutput } from './types';

type MapStardustOutput = (output: StardustIndexerOutput, type: string) => IotaObjectData;

export function mapStardustBasicOutputs(output: StardustIndexerOutput) {
return mapStardustOutput(output, STARDUST_BASIC_OUTPUT_TYPE);
}

export function mapStardustNftOutputs(output: StardustIndexerOutput): IotaObjectData {
return mapStardustOutput(output, STARDUST_NFT_OUTPUT_TYPE);
}

const mapStardustOutput: MapStardustOutput = function (
output: StardustIndexerOutput,
type: string,
) {
return {
objectId: output.id,
digest: '',
version: '',
type: type,
content: {
dataType: 'moveObject' as const,
type: type,
fields: {
balance: output.balance.value,
expiration_uc: output.expiration
? {
type: STARDUST_EXPIRATION_UNLOCK_CONDITION_TYPE,
fields: {
owner: output.expiration.owner,
return_address: output.expiration.return_address,
unix_time: output.expiration.unix_time,
},
}
: null,
id: {
id: output.id,
},
metadata: output.metadata,
native_tokens: {
type: '0x2::bag::Bag',
fields: {
id: {
id: output.native_tokens.id,
},
size: output.native_tokens.size,
},
},
sender: output.sender,
storage_deposit_return_uc: output.storage_deposit_return
? {
type: STARDUST_STORAGE_DEPOSIT_RETURN_UC_TYPE,
fields: {
return_address: output.storage_deposit_return.return_address,
return_amount: output.storage_deposit_return.return_amount,
},
}
: null,
tag: output.tag,
timelock_uc: output.timelock
? {
fields: {
unix_time: output.timelock.unix_time,
},
type: STARDUST_TIMELOCK_TYPE,
}
: null,
},
},
};
};
39 changes: 35 additions & 4 deletions apps/core/src/utils/migration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
// SPDX-License-Identifier: Apache-2.0

import { z } from 'zod';
import { STARDUST_PACKAGE_ID } from '../../constants';
import { STARDUST_EXPIRATION_UNLOCK_CONDITION_TYPE, STARDUST_PACKAGE_ID } from '../../constants';

const ExpirationUnlockConditionSchema = z.object({
type: z.literal(
`${STARDUST_PACKAGE_ID}::expiration_unlock_condition::ExpirationUnlockCondition`,
),
type: z.literal(STARDUST_EXPIRATION_UNLOCK_CONDITION_TYPE),
fields: z.object({
owner: z.string(),
return_address: z.string(),
Expand Down Expand Up @@ -60,6 +58,38 @@ export const BasicOutputObjectSchema = CommonOutputObjectWithUcSchema.extend({
sender: z.string().nullable().optional(),
});

const StardustIndexerOutputSchema = z.object({
id: z.string(),
balance: z.object({
value: z.number(),
}),
native_tokens: z.object({
id: z.string(),
size: z.number(),
}),
storage_deposit_return: z
.object({
return_address: z.string(),
return_amount: z.number(),
})
.nullable(),
timelock: z
.object({
unix_time: z.number(),
})
.nullable(),
expiration: z
.object({
owner: z.string(),
return_address: z.string(),
unix_time: z.number(),
})
.nullable(),
metadata: z.array(z.number()).nullable(),
tag: z.string().nullable(),
sender: z.string().nullable(),
});

export const NftOutputObjectSchema = CommonOutputObjectWithUcSchema;

export type ExpirationUnlockCondition = z.infer<typeof ExpirationUnlockConditionSchema>;
Expand All @@ -71,3 +101,4 @@ export type CommonOutputObject = z.infer<typeof CommonOutputObjectSchema>;
export type CommonOutputObjectWithUc = z.infer<typeof CommonOutputObjectWithUcSchema>;
export type BasicOutputObject = z.infer<typeof BasicOutputObjectSchema>;
export type NftOutputObject = z.infer<typeof NftOutputObjectSchema>;
export type StardustIndexerOutput = z.infer<typeof StardustIndexerOutputSchema>;
14 changes: 11 additions & 3 deletions apps/wallet-dashboard/app/(protected)/migrations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import {
} from '@iota/apps-ui-kit';
import { Assets, IotaLogoMark, Tokens } from '@iota/apps-ui-icons';
import { useCurrentAccount, useIotaClient } from '@iota/dapp-kit';
import { STARDUST_BASIC_OUTPUT_TYPE, STARDUST_NFT_OUTPUT_TYPE, useFormatCoin } from '@iota/core';
import {
STARDUST_BASIC_OUTPUT_TYPE,
STARDUST_NFT_OUTPUT_TYPE,
useFormatCoin,
useStardustIndexerClientContext,
} from '@iota/core';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import { StardustOutputMigrationStatus } from '@/lib/enums';
import { MigrationObjectsPanel, MigrationDialog } from '@/components';
Expand All @@ -37,7 +42,7 @@ function MigrationDashboardPage(): JSX.Element {
const [selectedStardustObjectsCategory, setSelectedStardustObjectsCategory] = useState<
StardustOutputMigrationStatus | undefined
>(undefined);

const { stardustIndexerClient } = useStardustIndexerClientContext();
const { data: stardustMigrationObjects, isPlaceholderData } =
useGetStardustMigratableObjects(address);
const {
Expand Down Expand Up @@ -111,9 +116,12 @@ function MigrationDashboardPage(): JSX.Element {
queryClient.invalidateQueries({
queryKey: ['migration-transaction', address],
});
queryClient.invalidateQueries({
queryKey: ['stardust-shared-objects', address, stardustIndexerClient],
});
});
},
[iotaClient, queryClient, address],
[iotaClient, queryClient, address, stardustIndexerClient],
);

const MIGRATION_CARDS: MigrationDisplayCardProps[] = [
Expand Down
77 changes: 77 additions & 0 deletions apps/wallet-dashboard/hooks/useGetAllStardustSharedObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useQuery } from '@tanstack/react-query';
import {
mapStardustBasicOutputs,
mapStardustNftOutputs,
PageParams,
StardustIndexerOutput,
TimeUnit,
useStardustIndexerClientContext,
} from '@iota/core';
import { IotaObjectData } from '@iota/iota-sdk/client';

const LIMIT_PER_REQ = 50;

export function useGetAllStardustSharedObjects(address: string) {
const { stardustIndexerClient } = useStardustIndexerClientContext();

const fetchPaginatedStardustSharedObjects = async (
mapFn: (output: StardustIndexerOutput) => IotaObjectData,
fetchFn: (address: string, params: PageParams) => Promise<StardustIndexerOutput[]>,
) => {
const allData: StardustIndexerOutput[] = [];
let page = 1;

try {
do {
const data = await fetchFn(address, { page, pageSize: LIMIT_PER_REQ });

if (!data || !data.length) {
break;
}

allData.push(...data);
page++;
} while (page);
} catch (e) {
console.error(e);
}

return allData.map(mapFn);
};

return useQuery({
queryKey: ['stardust-shared-objects', address, stardustIndexerClient],
queryFn: async () => {
if (!stardustIndexerClient) {
return {
basic: [],
nfts: [],
};
}

const basicOutputs = await fetchPaginatedStardustSharedObjects(
mapStardustBasicOutputs,
stardustIndexerClient.getBasicResolvedOutputs,
);

const nftOutputs = await fetchPaginatedStardustSharedObjects(
mapStardustNftOutputs,
stardustIndexerClient.getNftResolvedOutputs,
);

return {
basic: basicOutputs,
nfts: nftOutputs,
};
},
enabled: !!address,
staleTime: TimeUnit.ONE_SECOND * TimeUnit.ONE_MINUTE * 5,
placeholderData: {
basic: [],
nfts: [],
},
});
}
Loading

0 comments on commit 9d988ec

Please sign in to comment.