diff --git a/apps/core/src/constants/migration.constants.ts b/apps/core/src/constants/migration.constants.ts index 6086a758e0f..45072d1d652 100644 --- a/apps/core/src/constants/migration.constants.ts +++ b/apps/core/src/constants/migration.constants.ts @@ -1,7 +1,9 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; + export const STARDUST_PACKAGE_ID = '000000000000000000000000000000000000000000000000000000000000107a'; -export const STARDUST_BASIC_OUTPUT_TYPE = `${STARDUST_PACKAGE_ID}::basic_output::BasicOutput<0x2::iota::IOTA>`; -export const STARDUST_NFT_OUTPUT_TYPE = `${STARDUST_PACKAGE_ID}::nft_output::NftOutput<0x2::iota::IOTA>`; +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}>`; diff --git a/apps/core/src/utils/index.ts b/apps/core/src/utils/index.ts index fb8bee8590c..8cbb31afa73 100644 --- a/apps/core/src/utils/index.ts +++ b/apps/core/src/utils/index.ts @@ -25,3 +25,4 @@ export * from './getExplorerLink'; export * from './stake'; export * from './transaction'; export * from './validation'; +export * from './migration'; diff --git a/apps/core/src/utils/migration/createMigrationTransaction.ts b/apps/core/src/utils/migration/createMigrationTransaction.ts new file mode 100644 index 00000000000..8519a950831 --- /dev/null +++ b/apps/core/src/utils/migration/createMigrationTransaction.ts @@ -0,0 +1,164 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { IotaClient, IotaObjectData } from '@iota/iota-sdk/client'; +import { Transaction } from '@iota/iota-sdk/transactions'; +import { STARDUST_PACKAGE_ID } from '../../constants/migration.constants'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { + BasicOutputObject, + BasicOutputObjectSchema, + NftOutputObject, + NftOutputObjectSchema, +} from './types'; + +type NestedResultType = { + $kind: 'NestedResult'; + NestedResult: [number, number]; +}; + +export async function getNativeTokenTypesFromBag( + bagId: string, + client: IotaClient, +): Promise { + const nativeTokenDynamicFields = await client.getDynamicFields({ + parentId: bagId, + }); + const nativeTokenTypes: string[] = []; + for (const nativeToken of nativeTokenDynamicFields.data) { + nativeTokenTypes.push(nativeToken?.name?.value as string); + } + + return nativeTokenTypes; +} + +export function validateBasicOutputObject(outputObject: IotaObjectData): BasicOutputObject { + if (outputObject.content?.dataType !== 'moveObject') { + throw new Error('Invalid basic output object'); + } + const result = BasicOutputObjectSchema.safeParse(outputObject.content.fields); + if (!result.success) { + throw new Error('Invalid basic output object content'); + } + return result.data; +} + +export function validateNftOutputObject(outputObject: IotaObjectData): NftOutputObject { + if (outputObject.content?.dataType !== 'moveObject') { + throw new Error('Invalid nft output object'); + } + const result = NftOutputObjectSchema.safeParse(outputObject.content.fields); + if (!result.success) { + throw new Error('Invalid nft output object content'); + } + return result.data; +} + +export async function createMigrationTransaction( + client: IotaClient, + address: string, + basicOutputs: IotaObjectData[] = [], + nftOutputs: IotaObjectData[] = [], +): Promise { + const ptb = new Transaction(); + + const coinsFromBasicOutputs: NestedResultType[] = []; + + // Basic Outputs + for (const basicOutputObject of basicOutputs) { + const validatedOutputObject = validateBasicOutputObject(basicOutputObject); + const basicOutputObjectId = validatedOutputObject.id.id; + const bagId = validatedOutputObject.native_tokens.fields.id.id; + const bagSize = validatedOutputObject.native_tokens.fields.size; + const nativeTokenTypes: string[] = + Number(bagSize) > 0 ? await getNativeTokenTypesFromBag(bagId, client) : []; + + const migratableResult = ptb.moveCall({ + target: `${STARDUST_PACKAGE_ID}::basic_output::extract_assets`, + typeArguments: [IOTA_TYPE_ARG], + arguments: [ptb.object(basicOutputObjectId)], + }); + + const balance = migratableResult[0]; + let nativeTokensBag = migratableResult[1]; + + // Convert Balance in Coin + const [coin] = ptb.moveCall({ + target: '0x02::coin::from_balance', + typeArguments: [IOTA_TYPE_ARG], + arguments: [ptb.object(balance)], + }); + + coinsFromBasicOutputs.push(coin); + + for (const nativeTokenType of nativeTokenTypes) { + [nativeTokensBag] = ptb.moveCall({ + target: '0x107a::utilities::extract_and_send_to', + typeArguments: [nativeTokenType], + arguments: [ptb.object(nativeTokensBag), ptb.pure.address(address)], + }); + } + + ptb.moveCall({ + target: '0x02::bag::destroy_empty', + arguments: [ptb.object(nativeTokensBag)], + }); + } + + // NFT Outputs + const coinsFromNftOutputs: NestedResultType[] = []; + const nftsFromNftOutputs: NestedResultType[] = []; + + for (const nftOutputObject of nftOutputs) { + const validatedOutputObject = validateNftOutputObject(nftOutputObject); + const nftOutputObjectId = validatedOutputObject.id.id; + const bagId = validatedOutputObject.native_tokens.fields.id.id; + const bagSize = validatedOutputObject.native_tokens.fields.size; + const nativeTokenTypes: string[] = + Number(bagSize) > 0 ? await getNativeTokenTypesFromBag(bagId, client) : []; + + const migratableResult = ptb.moveCall({ + target: `${STARDUST_PACKAGE_ID}::nft_output::extract_assets`, + typeArguments: [IOTA_TYPE_ARG], + arguments: [ptb.object(nftOutputObjectId)], + }); + + const balance = migratableResult[0]; + let nativeTokensBag = migratableResult[1]; + const nft = migratableResult[2]; + + nftsFromNftOutputs.push(nft); + + // Convert Balance in Coin + const [coin] = ptb.moveCall({ + target: '0x02::coin::from_balance', + typeArguments: [IOTA_TYPE_ARG], + arguments: [ptb.object(balance)], + }); + coinsFromNftOutputs.push(coin); + + for (const nativeTokenType of nativeTokenTypes) { + [nativeTokensBag] = ptb.moveCall({ + target: '0x107a::utilities::extract_and_send_to', + typeArguments: [nativeTokenType], + arguments: [ptb.object(nativeTokensBag), ptb.pure.address(address)], + }); + } + + ptb.moveCall({ + target: '0x02::bag::destroy_empty', + arguments: [ptb.object(nativeTokensBag)], + }); + } + + const coinOne = coinsFromBasicOutputs.shift() || coinsFromNftOutputs.shift(); + const remainingCoins = [...coinsFromBasicOutputs, ...coinsFromNftOutputs]; + if (coinOne) { + if (remainingCoins.length > 0) { + ptb.mergeCoins(coinOne, remainingCoins); + } + ptb.transferObjects([coinOne, ...nftsFromNftOutputs], ptb.pure.address(address)); + } + + return ptb; +} diff --git a/apps/core/src/utils/migration/index.ts b/apps/core/src/utils/migration/index.ts new file mode 100644 index 00000000000..1f09b64ac4a --- /dev/null +++ b/apps/core/src/utils/migration/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './createMigrationTransaction'; +export * from './types'; diff --git a/apps/core/src/utils/migration/types.ts b/apps/core/src/utils/migration/types.ts new file mode 100644 index 00000000000..6f078b149f6 --- /dev/null +++ b/apps/core/src/utils/migration/types.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { z } from 'zod'; +import { STARDUST_PACKAGE_ID } from '../../constants'; + +const ExpirationUnlockConditionSchema = z.object({ + type: z.literal( + `${STARDUST_PACKAGE_ID}::expiration_unlock_condition::ExpirationUnlockCondition`, + ), + fields: z.object({ + owner: z.string(), + return_address: z.string(), + unix_time: z.number(), + }), +}); + +const StorageDepositReturnUnlockConditionSchema = z.object({ + type: z.literal( + `${STARDUST_PACKAGE_ID}::storage_deposit_return_unlock_condition::StorageDepositReturnUnlockCondition`, + ), + fields: z.object({ + return_address: z.string(), + return_amount: z.string(), + }), +}); + +const TimelockUnlockConditionSchema = z.object({ + type: z.literal(`${STARDUST_PACKAGE_ID}::timelock_unlock_condition::TimelockUnlockCondition`), + fields: z.object({ + unix_time: z.number(), + }), +}); + +const CommonOutputObjectSchema = z.object({ + id: z.object({ + id: z.string(), + }), + balance: z.string(), + native_tokens: z.object({ + type: z.literal('0x2::bag::Bag'), + fields: z.object({ + id: z.object({ + id: z.string(), + }), + size: z.string(), + }), + }), +}); + +const CommonOutputObjectWithUcSchema = CommonOutputObjectSchema.extend({ + expiration_uc: ExpirationUnlockConditionSchema.nullable().optional(), + storage_deposit_return_uc: StorageDepositReturnUnlockConditionSchema.nullable().optional(), + timelock_uc: TimelockUnlockConditionSchema.nullable().optional(), +}); + +export const BasicOutputObjectSchema = CommonOutputObjectWithUcSchema.extend({ + metadata: z.array(z.number()).nullable().optional(), + tag: z.array(z.number()).nullable().optional(), + sender: z.string().nullable().optional(), +}); + +export const NftOutputObjectSchema = CommonOutputObjectWithUcSchema; + +export type ExpirationUnlockCondition = z.infer; +export type StorageDepositReturnUnlockCondition = z.infer< + typeof StorageDepositReturnUnlockConditionSchema +>; +export type TimelockUnlockCondition = z.infer; +export type CommonOutputObject = z.infer; +export type CommonOutputObjectWithUc = z.infer; +export type BasicOutputObject = z.infer; +export type NftOutputObject = z.infer; diff --git a/apps/wallet-dashboard/components/Popup/Popups/MigratePopup.tsx b/apps/wallet-dashboard/components/Popup/Popups/MigratePopup.tsx index e11b66c190a..627fb860f3f 100644 --- a/apps/wallet-dashboard/components/Popup/Popups/MigratePopup.tsx +++ b/apps/wallet-dashboard/components/Popup/Popups/MigratePopup.tsx @@ -106,6 +106,7 @@ function MigratePopup({ ) : null } + iconAfterText /> ); diff --git a/apps/wallet-dashboard/hooks/useMigrationTransaction.ts b/apps/wallet-dashboard/hooks/useMigrationTransaction.ts index 10f87cad28e..b36c5b991fb 100644 --- a/apps/wallet-dashboard/hooks/useMigrationTransaction.ts +++ b/apps/wallet-dashboard/hooks/useMigrationTransaction.ts @@ -3,8 +3,8 @@ import { useIotaClient } from '@iota/dapp-kit'; import { IotaObjectData } from '@iota/iota-sdk/client'; -import { Transaction } from '@iota/iota-sdk/transactions'; import { useQuery } from '@tanstack/react-query'; +import { createMigrationTransaction } from '@iota/core'; export function useMigrationTransaction( address: string, @@ -16,7 +16,12 @@ export function useMigrationTransaction( // eslint-disable-next-line @tanstack/query/exhaustive-deps queryKey: ['migration-transaction', address], queryFn: async () => { - const transaction = new Transaction(); + const transaction = await createMigrationTransaction( + client, + address, + basicOutputObjects, + nftOutputObjects, + ); transaction.setSender(address); await transaction.build({ client }); return transaction; diff --git a/apps/wallet-dashboard/lib/interfaces/index.ts b/apps/wallet-dashboard/lib/interfaces/index.ts index db2d1f2c7be..6999e34dd61 100644 --- a/apps/wallet-dashboard/lib/interfaces/index.ts +++ b/apps/wallet-dashboard/lib/interfaces/index.ts @@ -3,7 +3,6 @@ export * from './transactions.interface'; export * from './timelock.interface'; -export * from './migration.interface'; export * from './vesting.interface'; export * from './appRoute.interface'; export * from './dialogView.interface'; diff --git a/apps/wallet-dashboard/lib/interfaces/migration.interface.ts b/apps/wallet-dashboard/lib/interfaces/migration.interface.ts deleted file mode 100644 index df35ae29112..00000000000 --- a/apps/wallet-dashboard/lib/interfaces/migration.interface.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -type ExpirationUnlockCondition = { - owner: string; - return_address: string; - unix_time: number; -}; -type StorageDepositReturnUnlockCondition = { - return_address: string; - return_amount: string; -}; -type TimelockUnlockCondition = { - unix_time: number; -}; - -export type CommonOutputObject = { - id: { id: string }; - balance: string; - native_tokens: { - type: string; - fields: { id: { id: string }; size: string }; - }; -}; - -export interface CommonOutputObjectWithUc extends CommonOutputObject { - expiration_uc?: { - type: string; - fields: ExpirationUnlockCondition; - }; - storage_deposit_return_uc?: { - type: string; - fields: StorageDepositReturnUnlockCondition; - }; - timelock_uc?: { - type: string; - fields: TimelockUnlockCondition; - }; -} - -export interface BasicOutputObject extends CommonOutputObjectWithUc { - metadata?: number[]; - tag?: number[]; - sender?: string; -} - -export interface NftOutputObject extends CommonOutputObjectWithUc {} diff --git a/apps/wallet-dashboard/lib/utils/migration.ts b/apps/wallet-dashboard/lib/utils/migration.ts index e4a5cfa1f30..49f54b89867 100644 --- a/apps/wallet-dashboard/lib/utils/migration.ts +++ b/apps/wallet-dashboard/lib/utils/migration.ts @@ -1,8 +1,8 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { CommonOutputObjectWithUc } from '@iota/core'; import { IotaObjectData } from '@iota/iota-sdk/client'; -import { CommonOutputObjectWithUc } from '../interfaces/migration.interface'; export type StardustMigrationGroupedObjects = { migratable: IotaObjectData[];