Skip to content

Commit

Permalink
feat(wallet-dashboard): Add migration PTB (#3908)
Browse files Browse the repository at this point in the history
* feat: fetch stardust basic and nft objects

* feat: organize stardust objects on migratable and unmigratable

* fix: migration constants and variable nameing

* fix: variable naming

* feat:add migration popup

* fix: move constants to core

* fix: evert adding routes to barrel

* feat: add migration ptb

* fix: remove leftover testing code

* fix: remove undefined object check

* fix: add schemas for migration transaction

* fix: change migration popup params and add loader to button

* remove migration inteface

* fix: remove empty spaces and add iota coin type constant migration constants

* fix: exporting type and eslint

* fix: migration object schema

* fix: add missing exports

* fix: remove IOTA_COIN_TYPE and use sdk constant IOTA_TYPE_ARG

* fix: move migration types to dedicated file and add literal strings for UC types

---------

Co-authored-by: Marc Espin <[email protected]>
  • Loading branch information
brancoder and marc2332 authored Nov 21, 2024
1 parent 266d761 commit 391a28b
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 53 deletions.
6 changes: 4 additions & 2 deletions apps/core/src/constants/migration.constants.ts
Original file line number Diff line number Diff line change
@@ -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}>`;
1 change: 1 addition & 0 deletions apps/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export * from './getExplorerLink';
export * from './stake';
export * from './transaction';
export * from './validation';
export * from './migration';
164 changes: 164 additions & 0 deletions apps/core/src/utils/migration/createMigrationTransaction.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> {
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<Transaction> {
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;
}
5 changes: 5 additions & 0 deletions apps/core/src/utils/migration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './createMigrationTransaction';
export * from './types';
73 changes: 73 additions & 0 deletions apps/core/src/utils/migration/types.ts
Original file line number Diff line number Diff line change
@@ -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<typeof ExpirationUnlockConditionSchema>;
export type StorageDepositReturnUnlockCondition = z.infer<
typeof StorageDepositReturnUnlockConditionSchema
>;
export type TimelockUnlockCondition = z.infer<typeof TimelockUnlockConditionSchema>;
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>;
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ function MigratePopup({
<Loader className="h-4 w-4 animate-spin" />
) : null
}
iconAfterText
/>
</div>
);
Expand Down
9 changes: 7 additions & 2 deletions apps/wallet-dashboard/hooks/useMigrationTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
1 change: 0 additions & 1 deletion apps/wallet-dashboard/lib/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
47 changes: 0 additions & 47 deletions apps/wallet-dashboard/lib/interfaces/migration.interface.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/wallet-dashboard/lib/utils/migration.ts
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down

0 comments on commit 391a28b

Please sign in to comment.