= ({ spinning, description }) => (
+
+
+ {description}
+
+ );
+
+ const TransactionProgress = () => {
+ return (
+
+
+ Migration process
+
+
+
+ Do not close this page until you finish the process.
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+ {!migrationStarted ? (
+ <>
+
+
+ Initiate migration
+
+
+ This operation will initiate the migration process of the
token{" "}
+ {symbol}. This means:
+
+
+ -
+
+ New Jetton contract will be deployed with the same settings
+
+
+ -
+
+ Users will need to migrate their {symbol}{" "}
+ manually{" "}
+
+
+ -
+
+ Your project should support the new version of the Jetton
+
+
+
+
+ You should consider these points before initiating the migration.
+
+
+
+
+ onClose()}>
+ Cancel
+
+
+ {
+ onSubmit();
+ }}>
+ Migration
+
+
+ >
+ ) : (
+ <>
+
+ {isNewMinterDeployed && isMigrationMasterDeployed && mintedJettonsToMaster && (
+ {
+ onClose();
+ navigate(`/jetton/${newMinterAddress}`);
+ }}>
+ Go to the new Jetton
+
+ )}
+ >
+ )}
+
+ );
+}
+
+function Spinner({ spinning }: { spinning: boolean }) {
+ return (
+
+ {spinning ? : }
+
+ );
+}
diff --git a/src/pages/jetton/userMigration.tsx b/src/pages/jetton/userMigration.tsx
new file mode 100644
index 0000000..3f2c2a7
--- /dev/null
+++ b/src/pages/jetton/userMigration.tsx
@@ -0,0 +1,302 @@
+import useJettonStore from "store/jetton-store/useJettonStore";
+import { AppButton } from "components/appButton";
+import { CenteringWrapper } from "components/footer/styled";
+import { Popup } from "components/Popup";
+import { Typography } from "@mui/material";
+import bullet from "assets/icons/bullet.svg";
+import { Box } from "@mui/system";
+import CircularProgress from "@mui/material/CircularProgress";
+import CheckCircleIcon from "@mui/icons-material/CheckCircle";
+import { useNavigate } from "react-router-dom";
+import WalletConnection from "services/wallet-connection";
+import { useTonAddress, useTonConnectUI, TonConnectUI } from "@tonconnect/ui-react";
+import { Address, Cell, beginCell } from "ton";
+import { JettonDeployParams, jettonDeployController } from "lib/deploy-controller";
+import { createDeployParams, sleep } from "lib/utils";
+import BN from "bn.js";
+import { ContractDeployer } from "lib/contract-deployer";
+import { toDecimalsBN } from "utils";
+import analytics from "services/analytics";
+import useNotification from "hooks/useNotification";
+import {
+ MIGRATION_HELPER_CODE,
+ MIGRATION_HELPER_DEPLOY_GAS,
+ MIGRATION_MASTER_CODE,
+ MIGRATION_MASTER_DEPLOY_GAS,
+ MigrationHelperConfig,
+ MigrationMasterConfig,
+ createMigrationHelper,
+ createMigrationMaster,
+ migrationHelperConfigToCell,
+ migrationMasterConfigToCell,
+} from "lib/migrations";
+import { useJettonAddress } from "hooks/useJettonAddress";
+import { transfer } from "lib/jetton-minter";
+import { cellToAddress, makeGetCall } from "lib/make-get-call";
+import { getClient } from "lib/get-ton-client";
+import BigNumberDisplay from "components/BigNumberDisplay";
+
+export function UserMigrationPopup({
+ open,
+ setOpen,
+ jettonMinter,
+ migrationMaster,
+}: {
+ open: boolean;
+ setOpen: (arg0: boolean) => void;
+ jettonMinter: string;
+ migrationMaster: string;
+}) {
+ const {
+ symbol,
+ isNewMinterDeployed,
+ isMigrationMasterDeployed,
+ mintedJettonsToMaster,
+ migrationStarted,
+ newMinterAddress,
+ decimals,
+ name,
+ jettonImage,
+ description,
+ totalSupply,
+ balance,
+ jettonWalletAddress,
+ isMigrationHelperDeployed,
+ migrationHelperBalance,
+ migrationHelper,
+ transferredJettonsToHelper,
+ setNewMinterDeployed,
+ setMigrationMasterDeployed,
+ setMintedJettonsToMaster,
+ setMigrationStarted,
+ setMigrationHelperDeployed,
+ setMigrationHelperBalance,
+ setTransferredJettonsToHelper,
+ } = useJettonStore();
+ const { showNotification } = useNotification();
+ const { jettonAddress } = useJettonAddress();
+ const [tonconnect] = useTonConnectUI();
+ const address = useTonAddress();
+
+ const navigate = useNavigate();
+
+ const onClose = () => {
+ setOpen(false);
+ };
+
+ const onSubmit = async () => {
+ const connection = tonconnect;
+ if (!address || !connection) {
+ throw new Error("Wallet not connected");
+ }
+ setMigrationStarted(true);
+ if (!isMigrationHelperDeployed) await deployMigrationHelper(connection);
+ await transferJettonsToHelper(connection);
+ };
+
+ const deployMigrationHelper = async (connection: TonConnectUI) => {
+ if (!address || !connection) {
+ throw new Error("Wallet not connected");
+ }
+
+ const parsedJettonMaster = Address.parse(jettonAddress!);
+
+ const migrationHelperConfig: MigrationHelperConfig = {
+ oldJettonMinter: parsedJettonMaster,
+ migrationMaster: Address.parse(migrationMaster),
+ recipient: Address.parse(address),
+ };
+ const params = {
+ code: MIGRATION_HELPER_CODE,
+ data: await migrationHelperConfigToCell(migrationHelperConfig),
+ deployer: Address.parse(address),
+ value: MIGRATION_HELPER_DEPLOY_GAS,
+ };
+ const migrationHelperAddress = new ContractDeployer().addressForContract(params);
+
+ const client = await getClient();
+ const isDeployed = await client.isContractDeployed(migrationHelperAddress);
+
+ if (isDeployed) {
+ setMigrationHelperDeployed(true);
+ return;
+ }
+
+ try {
+ const result = await createMigrationHelper(
+ migrationHelperConfig,
+ connection,
+ Address.parse(address),
+ );
+ setMigrationHelperDeployed(true);
+ } catch (err) {
+ if (err instanceof Error) {
+ showNotification(<>{err.message}>, "error");
+ onClose();
+ setMigrationStarted(false);
+ }
+ }
+ };
+
+ const transferJettonsToHelper = async (connection: TonConnectUI) => {
+ const amount = balance!;
+ const parsedJettonWallet = Address.parse(jettonWalletAddress!);
+
+ try {
+ await jettonDeployController.transfer(
+ connection,
+ amount!,
+ migrationHelper!,
+ address!,
+ parsedJettonWallet.toFriendly(),
+ 0.35,
+ 0.3,
+ );
+ setTransferredJettonsToHelper(true);
+ } catch (error) {
+ if (error instanceof Error) {
+ showNotification(error.message, "error");
+ onClose();
+ setMigrationStarted(false);
+ }
+ }
+ };
+
+ interface TransactionStepProps {
+ spinning: boolean;
+ description: string;
+ }
+
+ const TransactionStep: React.FC = ({ spinning, description }) => (
+
+
+ {description}
+
+ );
+
+ const TransactionProgress = () => {
+ return (
+
+
+ Migration process
+
+
+
+ Do not close this page until you finish the process.
+
+
+
+
+
+ );
+ };
+
+ return (
+
+ {!migrationStarted ? (
+ <>
+
+
+ Initiate migration
+
+
+ This operation will initiate the migration process of the
token{" "}
+ {symbol}. This means:
+
+
+ -
+
+ You should only migrate your tokens if it is neccessary
+
+
+ -
+
+ You will lose all your old {symbol}
+
+
+ -
+
+ You will receive{" "}
+
+
+ {" "}
+ tokens in the new version of {symbol}{" "}
+
+
+ -
+
+ You will not be able to get back to the old version
+
+
+
+
+ You should consider these points before initiating the migration.
+
+
+
+
+ onClose()}>
+ Cancel
+
+
+ {
+ onSubmit();
+ }}>
+ Migration
+
+
+ >
+ ) : (
+ <>
+
+ {isMigrationHelperDeployed && transferredJettonsToHelper && (
+ {
+ onClose();
+ navigate(`/jetton/${newMinterAddress}`);
+ }}>
+ Go to the new Jetton
+
+ )}
+ >
+ )}
+
+ );
+}
+
+function Spinner({ spinning }: { spinning: boolean }) {
+ return (
+
+ {spinning ? : }
+
+ );
+}
diff --git a/src/store/jetton-store/index.ts b/src/store/jetton-store/index.ts
index 8395480..ff8ee37 100644
--- a/src/store/jetton-store/index.ts
+++ b/src/store/jetton-store/index.ts
@@ -21,6 +21,17 @@ export interface JettonStoreState {
jettonLoading: boolean;
isMyWallet: boolean;
selectedWalletAddress?: string | null;
+ isCodeOld: boolean;
+ isNewMinterDeployed: boolean;
+ isMigrationMasterDeployed: boolean;
+ mintedJettonsToMaster: boolean;
+ migrationStarted: boolean;
+ newMinterAddress: string;
+ migrationId?: string;
+ migrationHelper?: string;
+ migrationHelperBalance?: BN;
+ isMigrationHelperDeployed: boolean;
+ transferredJettonsToHelper: boolean;
}
const jettonStateAtom = atom({
@@ -44,6 +55,17 @@ const jettonStateAtom = atom({
isJettonDeployerFaultyOnChainData: false,
isMyWallet: false,
selectedWalletAddress: undefined,
+ isCodeOld: false,
+ isNewMinterDeployed: false,
+ isMigrationMasterDeployed: false,
+ mintedJettonsToMaster: false,
+ migrationStarted: false,
+ newMinterAddress: "",
+ migrationId: "",
+ migrationHelper: "",
+ migrationHelperBalance: undefined,
+ isMigrationHelperDeployed: false,
+ transferredJettonsToHelper: false,
},
});
diff --git a/src/store/jetton-store/useJettonStore.ts b/src/store/jetton-store/useJettonStore.ts
index 72a2194..b9648ae 100644
--- a/src/store/jetton-store/useJettonStore.ts
+++ b/src/store/jetton-store/useJettonStore.ts
@@ -1,13 +1,27 @@
-import { jettonDeployController } from "lib/deploy-controller";
-import { zeroAddress } from "lib/utils";
+import { JettonDeployParams, jettonDeployController } from "lib/deploy-controller";
+import { createDeployParams, zeroAddress } from "lib/utils";
import { useRecoilState, useResetRecoilState } from "recoil";
-import { Address } from "ton";
+import WalletConnection from "services/wallet-connection";
+import { Address, Cell } from "ton";
import { jettonStateAtom } from ".";
import QuestiomMarkImg from "assets/icons/question.png";
import { useCallback } from "react";
import useNotification from "hooks/useNotification";
import { getUrlParam, isValidAddress } from "utils";
import { useJettonAddress } from "hooks/useJettonAddress";
+import { JETTON_MINTER_CODE } from "lib/jetton-minter";
+import BN from "bn.js";
+import { ContractDeployer } from "lib/contract-deployer";
+import {
+ MIGRATION_HELPER_CODE,
+ MIGRATION_MASTER_CODE,
+ MigrationHelperConfig,
+ MigrationMasterConfig,
+ migrationHelperConfigToCell,
+ migrationMasterConfigToCell,
+} from "lib/migrations";
+import { getClient } from "lib/get-ton-client";
+import { useParams } from "react-router-dom";
import { useTonAddress, useTonConnectUI } from "@tonconnect/ui-react";
function useJettonStore() {
@@ -16,6 +30,77 @@ function useJettonStore() {
const { showNotification } = useNotification();
const connectedWalletAddress = useTonAddress();
const { jettonAddress } = useJettonAddress();
+ const { migrationId }: { migrationId?: string } = useParams();
+
+ const setNewMinterDeployed = useCallback(
+ (newValue: boolean) => {
+ setState((prevState) => ({
+ ...prevState,
+ isNewMinterDeployed: newValue,
+ }));
+ },
+ [setState],
+ );
+
+ const setMigrationMasterDeployed = useCallback(
+ (newValue: boolean) => {
+ setState((prevState) => ({
+ ...prevState,
+ isMigrationMasterDeployed: newValue,
+ }));
+ },
+ [setState],
+ );
+
+ const setMintedJettonsToMaster = useCallback(
+ (newValue: boolean) => {
+ setState((prevState) => ({
+ ...prevState,
+ mintedJettonsToMaster: newValue,
+ }));
+ },
+ [setState],
+ );
+
+ const setMigrationStarted = useCallback(
+ (newValue: boolean) => {
+ setState((prevState) => ({
+ ...prevState,
+ migrationStarted: newValue,
+ }));
+ },
+ [setState],
+ );
+
+ const setMigrationHelperBalance = useCallback(
+ (newValue: BN) => {
+ setState((prevState) => ({
+ ...prevState,
+ migrationHelperBalance: newValue,
+ }));
+ },
+ [setState],
+ );
+
+ const setMigrationHelperDeployed = useCallback(
+ (newValue: boolean) => {
+ setState((prevState) => ({
+ ...prevState,
+ isMigrationHelperDeployed: newValue,
+ }));
+ },
+ [setState],
+ );
+
+ const setTransferredJettonsToHelper = useCallback(
+ (newValue: boolean) => {
+ setState((prevState) => ({
+ ...prevState,
+ transferredJettonsToHelper: newValue,
+ }));
+ },
+ [setState],
+ );
const getJettonDetails = useCallback(async () => {
let queryAddress = getUrlParam("address");
@@ -87,27 +172,122 @@ function useJettonStore() {
}
}
+ const minterCode = Cell.fromBoc(
+ await jettonDeployController.getJettonMinterCode(parsedJettonMaster),
+ )[0];
+
+ const name = result.minter.metadata.name;
+ const symbol = result.minter.metadata.symbol;
+ const jettonImage = image ?? QuestiomMarkImg;
+ const description = result.minter.metadata.description;
+ const decimals = result.minter.metadata.decimals || "9";
+
setState((prevState) => {
return {
...prevState,
isJettonDeployerFaultyOnChainData: result.minter.isJettonDeployerFaultyOnChainData,
persistenceType: result.minter.persistenceType,
- description: result.minter.metadata.description,
- jettonImage: image ?? QuestiomMarkImg,
+ description,
+ jettonImage,
totalSupply: result.minter.totalSupply,
- name: result.minter.metadata.name,
- symbol: result.minter.metadata.symbol,
+ name,
+ symbol,
adminRevokedOwnership: _adminAddress === zeroAddress().toFriendly(),
isAdmin: admin,
- decimals: result.minter.metadata.decimals || "9",
+ decimals,
adminAddress: _adminAddress,
balance: result.jettonWallet ? result.jettonWallet.balance : undefined,
jettonWalletAddress: result.jettonWallet?.jWalletAddress.toFriendly(),
jettonMaster: jettonAddress,
isMyWallet,
selectedWalletAddress: address,
+ isCodeOld: !minterCode.equals(JETTON_MINTER_CODE),
};
});
+
+ if (address) {
+ const minterParams: JettonDeployParams = {
+ owner: Address.parse(address),
+ onchainMetaData: {
+ name: name!,
+ symbol: symbol!,
+ image: jettonImage,
+ description: description,
+ decimals: parseInt(decimals!).toFixed(0),
+ },
+ amountToMint: new BN(0),
+ };
+ const minterDeployParams = createDeployParams(minterParams);
+ const newMinterAddress = new ContractDeployer().addressForContract(minterDeployParams);
+
+ const client = await getClient();
+ const isNewMinterDeployed = await client.isContractDeployed(newMinterAddress);
+ let isMigrationMasterDeployed = false;
+ let mintedJettonsToMaster = false;
+
+ if (isNewMinterDeployed) {
+ const migrationMasterConfig: MigrationMasterConfig = {
+ oldJettonMinter: parsedJettonMaster,
+ newJettonMinter: newMinterAddress,
+ };
+ const migrationMasterAddress = new ContractDeployer().addressForContract({
+ code: MIGRATION_MASTER_CODE,
+ data: await migrationMasterConfigToCell(migrationMasterConfig),
+ deployer: Address.parse(address), //anything
+ value: new BN(0), //anything
+ });
+ isMigrationMasterDeployed = await client.isContractDeployed(migrationMasterAddress);
+
+ if (isMigrationMasterDeployed) {
+ const result = await jettonDeployController.getJettonDetails(
+ newMinterAddress,
+ migrationMasterAddress,
+ );
+
+ if (result) {
+ const migrationMasterJettonBalance = result.jettonWallet?.balance;
+
+ if (migrationMasterJettonBalance?.gt(new BN(0))) {
+ mintedJettonsToMaster = true;
+ }
+ }
+ }
+ }
+
+ let migrationHelperAddress: Address;
+ let isMigrationHelperDeployed: boolean;
+ let migrationHelperBalance: BN;
+
+ if (migrationId) {
+ const migrationHelperConfig: MigrationHelperConfig = {
+ oldJettonMinter: parsedJettonMaster,
+ migrationMaster: Address.parse(migrationId),
+ recipient: Address.parse(address),
+ };
+ migrationHelperAddress = new ContractDeployer().addressForContract({
+ code: MIGRATION_HELPER_CODE,
+ data: await migrationHelperConfigToCell(migrationHelperConfig),
+ deployer: Address.parse(address), //anything
+ value: new BN(0), //anything
+ });
+ isMigrationHelperDeployed = await client.isContractDeployed(migrationHelperAddress);
+
+ if (isMigrationHelperDeployed) {
+ migrationHelperBalance = await client.getBalance(migrationHelperAddress);
+ }
+ }
+
+ setState((prevState) => ({
+ ...prevState,
+ isNewMinterDeployed,
+ newMinterAddress: newMinterAddress.toString(),
+ isMigrationMasterDeployed,
+ mintedJettonsToMaster,
+ migrationHelper: migrationHelperAddress ? migrationHelperAddress.toString() : undefined,
+ isMigrationHelperDeployed,
+ migrationHelperBalance,
+ }));
+ }
} catch (error) {
if (error instanceof Error) {
showNotification(
@@ -127,8 +307,16 @@ function useJettonStore() {
return {
...state,
+ migrationId,
getJettonDetails,
reset,
+ setNewMinterDeployed,
+ setMigrationMasterDeployed,
+ setMintedJettonsToMaster,
+ setMigrationStarted,
+ setMigrationHelperDeployed,
+ setMigrationHelperBalance,
+ setTransferredJettonsToHelper,
};
}