diff --git a/apps/core/src/utils/stake/index.ts b/apps/core/src/utils/stake/index.ts index e742b3377ec..f798cc6214a 100644 --- a/apps/core/src/utils/stake/index.ts +++ b/apps/core/src/utils/stake/index.ts @@ -11,3 +11,4 @@ export * from './getStakeDetailsFromEvent'; export * from './checkIfIsTimelockedStaking'; export * from './getUnstakeDetailsFromEvent'; export * from './getTransactionAmountForTimelocked'; +export * from './types'; diff --git a/apps/core/src/utils/stake/types.ts b/apps/core/src/utils/stake/types.ts new file mode 100644 index 00000000000..b59797d6237 --- /dev/null +++ b/apps/core/src/utils/stake/types.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +import { z } from 'zod'; +import { TIMELOCK_IOTA_TYPE } from '../../constants/timelock.constants'; + +const UidSchema = z.object({ + id: z.string(), +}); + +const BalanceSchema = z.object({ + value: z.bigint(), +}); + +export const TimelockedObjectSchema = z.object({ + id: UidSchema, + locked: BalanceSchema, + expirationTimestampMs: z.number(), + label: z.string().optional(), +}); + +export const TimelockedObjectFieldsSchema = z.object({ + id: UidSchema, + locked: z.string(), + expiration_timestamp_ms: z.string(), + label: z.string().optional(), +}); + +const TimelockedObjectContentSchema = z.object({ + dataType: z.string().optional(), + type: z.literal(TIMELOCK_IOTA_TYPE).optional(), + fields: TimelockedObjectFieldsSchema, +}); + +export const TimelockedIotaObjectSchema = z.object({ + objectId: z.string(), + version: z.string(), + digest: z.string(), + type: z.literal(TIMELOCK_IOTA_TYPE), + display: z.object({ + data: z.nullable(z.any()), + error: z.nullable(z.any()), + }), + content: TimelockedObjectContentSchema.optional(), +}); + +const BaseTimelockedStakeSchema = z.object({ + expirationTimestampMs: z.string(), + label: z.string().nullable().optional(), + principal: z.string(), + stakeActiveEpoch: z.string(), + stakeRequestEpoch: z.string(), + timelockedStakedIotaId: z.string(), +}); + +const PendingTimelockedStakeSchema = BaseTimelockedStakeSchema.extend({ + status: z.literal('Pending'), +}); + +const ActiveTimelockedStakeSchema = BaseTimelockedStakeSchema.extend({ + status: z.literal('Active'), + estimatedReward: z.string(), +}); + +const UnstakedTimelockedStakeSchema = BaseTimelockedStakeSchema.extend({ + status: z.literal('Unstaked'), +}); + +const TimelockedStakeSchema = z.discriminatedUnion('status', [ + PendingTimelockedStakeSchema, + ActiveTimelockedStakeSchema, + UnstakedTimelockedStakeSchema, +]); + +export const DelegatedTimelockedStakeSchema = z.object({ + validatorAddress: z.string().min(1, { message: 'Validator address cannot be empty' }), + stakingPool: z.string().min(1, { message: 'Staking pool cannot be empty' }), + stakes: z.array(TimelockedStakeSchema), +}); diff --git a/apps/wallet-dashboard/jest.config.ts b/apps/wallet-dashboard/jest.config.ts index 99fd527034f..e7d961f89ed 100644 --- a/apps/wallet-dashboard/jest.config.ts +++ b/apps/wallet-dashboard/jest.config.ts @@ -8,6 +8,7 @@ const config: Config = { }, moduleNameMapper: { '^@iota/core/constants/(.*)$': '/../core/src/constants/$1', + '^@iota/core/utils/(.*)$': '/../core/src/utils/$1', }, }; diff --git a/apps/wallet-dashboard/lib/utils/timelock.ts b/apps/wallet-dashboard/lib/utils/timelock.ts index f8e06340922..de99d9d6c15 100644 --- a/apps/wallet-dashboard/lib/utils/timelock.ts +++ b/apps/wallet-dashboard/lib/utils/timelock.ts @@ -1,8 +1,12 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 - import { DelegatedTimelockedStake, TimelockedStake, IotaObjectData } from '@iota/iota-sdk/client'; import { TimelockedIotaResponse, TimelockedObject } from '../interfaces'; +import { + TimelockedIotaObjectSchema, + TimelockedObjectFieldsSchema, + DelegatedTimelockedStakeSchema, +} from '@iota/core/utils/stake/types'; export type ExtendedDelegatedTimelockedStake = TimelockedStake & { validatorAddress: string; @@ -39,6 +43,12 @@ export function isTimelockedUnlockable( export function mapTimelockObjects(iotaObjects: IotaObjectData[]): TimelockedObject[] { return iotaObjects.map((iotaObject) => { + const validationObject = TimelockedIotaObjectSchema.safeParse(iotaObject); + + if (!validationObject.success) { + throw new Error('Invalid TimelockedObject'); + } + if (!iotaObject?.content?.dataType || iotaObject.content.dataType !== 'moveObject') { return { id: { id: '' }, @@ -47,6 +57,13 @@ export function mapTimelockObjects(iotaObjects: IotaObjectData[]): TimelockedObj }; } const fields = iotaObject.content.fields as unknown as TimelockedIotaResponse; + + const validationFields = TimelockedObjectFieldsSchema.safeParse(fields); + + if (!validationFields.success) { + throw new Error('Invalid TimelockedObject content fields'); + } + return { id: fields.id, locked: { value: BigInt(fields.locked) }, @@ -60,6 +77,13 @@ export function formatDelegatedTimelockedStake( delegatedTimelockedStakeData: DelegatedTimelockedStake[], ): ExtendedDelegatedTimelockedStake[] { return delegatedTimelockedStakeData.flatMap((delegatedTimelockedStake) => { + const validatedDelegatedTimelockedStake = + DelegatedTimelockedStakeSchema.safeParse(delegatedTimelockedStake); + + if (!validatedDelegatedTimelockedStake.success) { + throw new Error('Invalid DelegatedTimelockedStake'); + } + return delegatedTimelockedStake.stakes.map((stake) => { return { validatorAddress: delegatedTimelockedStake.validatorAddress, diff --git a/apps/wallet-dashboard/tsconfig.json b/apps/wallet-dashboard/tsconfig.json index 03253aade61..8b0158eab97 100644 --- a/apps/wallet-dashboard/tsconfig.json +++ b/apps/wallet-dashboard/tsconfig.json @@ -25,7 +25,8 @@ "@/components/*": ["./components/*"], "@/hooks/*": ["./hooks/*"], "@/stores/*": ["./stores/*"], - "@iota/core/constants/*": ["./../core/src/constants/*"] + "@iota/core/constants/*": ["./../core/src/constants/*"], + "@iota/core/utils/*": ["./../core/src/utils/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],