Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): add validation schemas for timelocked objects #4838

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions apps/core/src/utils/stake/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './getStakeDetailsFromEvent';
export * from './checkIfIsTimelockedStaking';
export * from './getUnstakeDetailsFromEvent';
export * from './getTransactionAmountForTimelocked';
export * from './types';
78 changes: 78 additions & 0 deletions apps/core/src/utils/stake/types.ts
Original file line number Diff line number Diff line change
@@ -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),
});
1 change: 1 addition & 0 deletions apps/wallet-dashboard/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const config: Config = {
},
moduleNameMapper: {
'^@iota/core/constants/(.*)$': '<rootDir>/../core/src/constants/$1',
'^@iota/core/utils/(.*)$': '<rootDir>/../core/src/utils/$1',
},
};

Expand Down
26 changes: 25 additions & 1 deletion apps/wallet-dashboard/lib/utils/timelock.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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: '' },
Expand All @@ -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) },
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion apps/wallet-dashboard/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
Loading