diff --git a/src/App.tsx b/src/App.tsx index 1d872cc..3e68626 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -108,6 +108,7 @@ const App = () => { }> } /> } /> + } /> diff --git a/src/consts.ts b/src/consts.ts index a494332..f114a13 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -3,6 +3,7 @@ const ROUTES = { deployer: "/", jetton: "/jetton", jettonId: "/jetton/:id", + migration: "/jetton/:id/migration/:migrationId", }; const APP_GRID = 1156; diff --git a/src/lib/contracts/MigrationHelper.compiled.json b/src/lib/contracts/MigrationHelper.compiled.json new file mode 100644 index 0000000..34c22bb --- /dev/null +++ b/src/lib/contracts/MigrationHelper.compiled.json @@ -0,0 +1,3 @@ +{ + "hex": "b5ee9c7241020701000122000114ff00f4a413f4bcf2c80b010202d00302008969c08208403e29fa97232c7c4f2cfd400fe80be10b3c5be10f3c5b2c0208402faf0803e80b2c03e10f3c5b25c60063232c17e1073c5a08403b9aca03e80b2dab3325c7ec0203f7d76d176fd906ba4e000492f827001410808f0d1805cf968fcf6a2687d207d2000fc317d2000fc31ea187c142cb82a1009aa0a01e428027d012c678b00e78b666491646580897a007a00658064fc80383a6465816503e5ffe4e87c30e86981fd201800b8d8492f81f000e98f90c1083cf23a475d7187e99ffd001800c060504003af006708018c8cb05f843cf1602821011e1a300a112fa02cb6ac970fb0000320182107362d09cba945f03db31e1f84112c705b3935bdb31e0001831f84312c705b3935bdb31e0512f215e" +} diff --git a/src/lib/contracts/MigrationMaster.compiled.json b/src/lib/contracts/MigrationMaster.compiled.json new file mode 100644 index 0000000..70479bc --- /dev/null +++ b/src/lib/contracts/MigrationMaster.compiled.json @@ -0,0 +1,3 @@ +{ + "hex": "b5ee9c7241020a010001df000114ff00f4a413f4bcf2c80b01020162030200eba0df8dda89a1f481f481a9a861f050a084e0a84026a8280790a009f404b19e2c039e2d99924591960225e801e801960193f200e0e9919605940f97ff93a1f0c3f05004e0a84026a8280790a009f404b19e2c039e2d99924591960225e801e801960193f200e0e9919605940f97ff93a1f0c5f083f0850202cc0504005fdb841082caf83de64658f89659fac7d017c14678b658064b8c00c646582fc20e78b41057d78407d0165b56664b8fd8040201200706007bf3810410807c53f52e4658f8a659fa8027d0110e78b00e78b658041057d78407d01658064b8c00c646582fc21678b410802faf0807d0165b56664b8fd8040129d106ba4e000492f827001410805f5e1005cf968fcc0801fced44d0fa40fa40d4d430f828504270542013541403c85004fa0258cf1601cf16ccc922c8cb0112f400f400cb00c9f9007074c8cb02ca07cbffc9d0f861f8280270542013541403c85004fa0258cf1601cf16ccc922c8cb0112f400f400cb00c9f9007074c8cb02ca07cbffc9d0f862d0d303fa403002d31f0271b0f8411409007cc705b313b10282107362d09cbd12b1915be0d33ffa00fa403171d721547120f00702f008708018c8cb0558cf160282100bebc200a112fa02cb6ac970fb0041907573" +} diff --git a/src/lib/deploy-controller.ts b/src/lib/deploy-controller.ts index f6e7e5d..047c3cb 100644 --- a/src/lib/deploy-controller.ts +++ b/src/lib/deploy-controller.ts @@ -150,6 +150,8 @@ class JettonDeployController { toAddress: string, fromAddress: string, ownerJettonWallet: string, + customValue?: number, + customForwardValue?: number, ) { const tc = await getClient(); @@ -164,9 +166,14 @@ class JettonDeployController { messages: [ { address: ownerJettonWallet, - amount: toNano(0.05).toString(), + amount: toNano(customValue || 0.05).toString(), stateInit: undefined, - payload: transfer(Address.parse(toAddress), Address.parse(fromAddress), amount) + payload: transfer( + Address.parse(toAddress), + Address.parse(fromAddress), + amount, + customForwardValue, + ) .toBoc() .toString("base64"), }, @@ -256,6 +263,12 @@ class JettonDeployController { }; } + async getJettonMinterCode(contractAddress: Address) { + const client = getClient(); + const code = (await (await client).getContractState(contractAddress)).code!; + return code; + } + async fixFaultyJetton( contractAddress: Address, data: { diff --git a/src/lib/jetton-minter.ts b/src/lib/jetton-minter.ts index 231072d..066c1c6 100644 --- a/src/lib/jetton-minter.ts +++ b/src/lib/jetton-minter.ts @@ -235,7 +235,12 @@ export function burn(amount: BN, responseAddress: Address) { .endCell(); } -export function transfer(to: Address, from: Address, jettonAmount: BN) { +export function transfer( + to: Address, + from: Address, + jettonAmount: BN, + customForwardValue?: number, +) { return beginCell() .storeUint(OPS.Transfer, 32) .storeUint(1, 64) @@ -243,7 +248,7 @@ export function transfer(to: Address, from: Address, jettonAmount: BN) { .storeAddress(to) .storeAddress(from) .storeBit(false) - .storeCoins(toNano(0.001)) + .storeCoins(toNano(customForwardValue || 0.001)) .storeBit(false) // forward_payload in this slice, not separate cell .endCell(); } diff --git a/src/lib/migrations.ts b/src/lib/migrations.ts new file mode 100644 index 0000000..57be47d --- /dev/null +++ b/src/lib/migrations.ts @@ -0,0 +1,141 @@ +import { Cell, beginCell, Address, TonClient } from "ton"; + +import masterHex from "./contracts/MigrationMaster.compiled.json"; +import helperHex from "./contracts/MigrationHelper.compiled.json"; +import { useTonAddress, useTonConnectUI, TonConnectUI } from "@tonconnect/ui-react"; +import { getClient } from "./get-ton-client"; +import BN from "bn.js"; +import { ContractDeployer } from "./contract-deployer"; +import WalletConnection from "services/wallet-connection"; +import { waitForContractDeploy } from "./utils"; + +export const MIGRATION_MASTER_CODE = Cell.fromBoc(masterHex.hex)[0]; +export const MIGRATION_HELPER_CODE = Cell.fromBoc(helperHex.hex)[0]; // code cell from build output + +enum OPS { + Migrate = 0x79e4748e, +} + +export const MIGRATION_MASTER_DEPLOY_GAS = new BN("100000000"); +export const MIGRATION_HELPER_DEPLOY_GAS = new BN("20000000"); + +export type MigrationMasterConfig = { + oldJettonMinter: Address; + newJettonMinter: Address; + oldWalletCode?: Cell; + newWalletCode?: Cell; +}; + +export async function migrationMasterConfigToCell(config: MigrationMasterConfig): Promise { + const client = await getClient(); + const oldWalletCode = Cell.fromBoc( + Buffer.from( + (await client.callGetMethod(config.oldJettonMinter, "get_jetton_data")).stack[4][1].bytes, + "base64", + ).toString("hex"), + )[0]; + const newWalletCode = Cell.fromBoc( + Buffer.from( + (await client.callGetMethod(config.newJettonMinter, "get_jetton_data")).stack[4][1].bytes, + "base64", + ).toString("hex"), + )[0]; + + return beginCell() + .storeAddress(config.oldJettonMinter) + .storeAddress(config.newJettonMinter) + .storeRef(config.oldWalletCode || oldWalletCode) + .storeRef(config.newWalletCode || newWalletCode) + .endCell(); +} + +export type MigrationHelperConfig = { + oldJettonMinter: Address; + migrationMaster: Address; + recipient: Address; + oldWalletCode?: Cell; +}; + +export async function migrationHelperConfigToCell(config: MigrationHelperConfig): Promise { + const client = await getClient(); + const oldWalletCode = Cell.fromBoc( + Buffer.from( + (await client.callGetMethod(config.oldJettonMinter, "get_jetton_data")).stack[4][1].bytes, + "base64", + ).toString("hex"), + )[0]; + return beginCell() + .storeAddress(config.oldJettonMinter) + .storeAddress(config.migrationMaster) + .storeAddress(config.recipient) + .storeRef(config.oldWalletCode || oldWalletCode) + .endCell(); +} + +export function migrateBody(amount: BN): Cell { + return beginCell().storeUint(OPS.Migrate, 32).storeUint(1, 64).storeCoins(amount).endCell(); +} + +export async function createMigrationMaster( + config: MigrationMasterConfig, + tonConnection: TonConnectUI, + owner: Address, +): Promise
{ + const contractDeployer = new ContractDeployer(); + const tc = await getClient(); + + // params.onProgress?.(JettonDeployState.BALANCE_CHECK); + const balance = await tc.getBalance(owner); + if (balance.lt(MIGRATION_MASTER_DEPLOY_GAS)) + throw new Error("Not enough balance in deployer wallet"); + const params = { + code: MIGRATION_MASTER_CODE, + data: await migrationMasterConfigToCell(config), + deployer: owner, //anything + value: MIGRATION_MASTER_DEPLOY_GAS, + }; + const migrationMasterAddress = new ContractDeployer().addressForContract(params); + const isDeployed = tc.isContractDeployed(migrationMasterAddress); + + if (await tc.isContractDeployed(migrationMasterAddress)) { + // params.onProgress?.(JettonDeployState.ALREADY_DEPLOYED); + } else { + await contractDeployer.deployContract(params, tonConnection); + // params.onProgress?.(JettonDeployState.AWAITING_MINTER_DEPLOY); + await waitForContractDeploy(migrationMasterAddress, tc); + } + + return migrationMasterAddress; +} + +export async function createMigrationHelper( + config: MigrationHelperConfig, + tonConnection: TonConnectUI, + owner: Address, +): Promise
{ + const contractDeployer = new ContractDeployer(); + const tc = await getClient(); + + // params.onProgress?.(JettonDeployState.BALANCE_CHECK); + const balance = await tc.getBalance(owner); + if (balance.lt(MIGRATION_HELPER_DEPLOY_GAS)) + throw new Error("Not enough balance in deployer wallet"); + const params = { + code: MIGRATION_HELPER_CODE, + data: await migrationHelperConfigToCell(config), + deployer: owner, //anything + value: MIGRATION_HELPER_DEPLOY_GAS, + }; + const migrationHelperAddress = new ContractDeployer().addressForContract(params); + const isDeployed = tc.isContractDeployed(migrationHelperAddress); + + if (await tc.isContractDeployed(migrationHelperAddress)) { + // params.onProgress?.(JettonDeployState.ALREADY_DEPLOYED); + } else { + await contractDeployer.deployContract(params, tonConnection); + // params.onProgress?.(JettonDeployState.AWAITING_MINTER_DEPLOY); + await waitForContractDeploy(migrationHelperAddress, tc); + } + + return migrationHelperAddress; +} diff --git a/src/pages/deployer/index.tsx b/src/pages/deployer/index.tsx index 8aeb653..8aa8058 100644 --- a/src/pages/deployer/index.tsx +++ b/src/pages/deployer/index.tsx @@ -22,6 +22,7 @@ import { offchainFormSpec, onchainFormSpec } from "./data"; import { Form } from "components/form"; import { GithubButton } from "pages/deployer/githubButton"; import { useNavigatePreserveQuery } from "lib/hooks/useNavigatePreserveQuery"; +import { getClient } from "lib/get-ton-client"; import { useTonAddress, useTonConnectUI } from "@tonconnect/ui-react"; const DEFAULT_DECIMALS = 9; diff --git a/src/pages/jetton/dataRow/token/Token.tsx b/src/pages/jetton/dataRow/token/Token.tsx index 5080d6d..52696d8 100644 --- a/src/pages/jetton/dataRow/token/Token.tsx +++ b/src/pages/jetton/dataRow/token/Token.tsx @@ -27,6 +27,9 @@ import brokenImage from "assets/icons/question.png"; import { AppButton } from "components/appButton"; import pen from "assets/icons/pen.svg"; import { CenteringWrapper } from "components/footer/styled"; +import { MigrationPopup } from "pages/jetton/migration"; +import { UserMigrationPopup } from "pages/jetton/userMigration"; +import { useNavigate } from "react-router-dom"; export const Token = () => { const { @@ -45,8 +48,20 @@ export const Token = () => { jettonLoading, decimals, isImageBroken, + isCodeOld, + selectedWalletAddress, + isNewMinterDeployed, + isMigrationMasterDeployed, + mintedJettonsToMaster, + newMinterAddress, + migrationId, } = useJettonStore(); const [openEdit, setOpenEdit] = useState(false); + const [openMigrationPopup, setOpenMigrationPopup] = useState(false); + const [openUserMigrationPopup, setOpenUserMigrationPopup] = useState(false); + const navigate = useNavigate(); + + const isMigrationRoute: boolean = !!migrationId; return ( @@ -103,6 +118,57 @@ export const Token = () => { )} + {isMigrationRoute && !jettonLoading && ( + + setOpenUserMigrationPopup(true)}> + + Migration Icon + Migration + + + + )} + + + {!isMigrationRoute && + isCodeOld && + !jettonLoading && + selectedWalletAddress && + !(isNewMinterDeployed && isMigrationMasterDeployed && mintedJettonsToMaster) && ( + + setOpenMigrationPopup(true)}> + + Pen Icon + Migration + + + + )} {!isAdmin && isJettonDeployerFaultyOnChainData && ( diff --git a/src/pages/jetton/migration.tsx b/src/pages/jetton/migration.tsx new file mode 100644 index 0000000..34f4276 --- /dev/null +++ b/src/pages/jetton/migration.tsx @@ -0,0 +1,332 @@ +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 { Address, Cell, beginCell } from "ton"; +import { JettonDeployParams, jettonDeployController } from "lib/deploy-controller"; +import { createDeployParams } 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 { useTonAddress, useTonConnectUI, TonConnectUI } from "@tonconnect/ui-react"; +import { + MIGRATION_MASTER_CODE, + MIGRATION_MASTER_DEPLOY_GAS, + MigrationMasterConfig, + createMigrationMaster, + 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"; + +export function MigrationPopup({ + open, + setOpen, +}: { + open: boolean; + setOpen: (arg0: boolean) => void; +}) { + const { + symbol, + isNewMinterDeployed, + isMigrationMasterDeployed, + mintedJettonsToMaster, + migrationStarted, + newMinterAddress, + decimals, + name, + jettonImage, + description, + totalSupply, + jettonWalletAddress, + setNewMinterDeployed, + setMigrationMasterDeployed, + setMintedJettonsToMaster, + setMigrationStarted, + } = useJettonStore(); + const [tonconnect] = useTonConnectUI(); + const { showNotification } = useNotification(); + const { jettonAddress } = useJettonAddress(); + const walletAddress = useTonAddress(); + + const navigate = useNavigate(); + + const onClose = () => { + setOpen(false); + }; + + const onSubmit = async () => { + if (!walletAddress || !tonconnect) { + throw new Error("Wallet not connected"); + } + setMigrationStarted(true); + if (!isNewMinterDeployed) await deployNewJetton(tonconnect); + if (!isMigrationMasterDeployed) await deployMigrationMaster(tonconnect); + if (!mintedJettonsToMaster) await mintJettonsToMaster(tonconnect); + }; + + const deployNewJetton = async (connection: TonConnectUI) => { + if (!walletAddress || !connection) { + throw new Error("Wallet not connected"); + } + + const params: JettonDeployParams = { + owner: Address.parse(walletAddress), + onchainMetaData: { + name: name!, + symbol: symbol!, + image: jettonImage, + description: description, + decimals: parseInt(decimals!).toFixed(0), + }, + amountToMint: totalSupply!, + }; + + const deployParams = createDeployParams(params); + const contractAddress = new ContractDeployer().addressForContract(deployParams); + + const client = await getClient(); + const isDeployed = await client.isContractDeployed(contractAddress); + + if (isDeployed) { + setNewMinterDeployed(true); + return; + } + + try { + const result = await jettonDeployController.createJetton(params, connection, walletAddress); + setNewMinterDeployed(true); + } catch (err) { + if (err instanceof Error) { + showNotification(<>{err.message}, "error"); + onClose(); + setMigrationStarted(false); + } + } + }; + + const deployMigrationMaster = async (connection: TonConnectUI) => { + if (!walletAddress || !connection) { + throw new Error("Wallet not connected"); + } + + const parsedJettonMaster = Address.parse(jettonAddress!); + + const migrationMasterConfig: MigrationMasterConfig = { + oldJettonMinter: parsedJettonMaster, + newJettonMinter: Address.parse(newMinterAddress), + }; + const params = { + code: MIGRATION_MASTER_CODE, + data: await migrationMasterConfigToCell(migrationMasterConfig), + deployer: Address.parse(walletAddress), + value: MIGRATION_MASTER_DEPLOY_GAS, + }; + const migrationMasterAddress = new ContractDeployer().addressForContract(params); + const client = await getClient(); + const isDeployed = await client.isContractDeployed(migrationMasterAddress); + + if (isDeployed) { + setMigrationMasterDeployed(true); + return; + } + + try { + const result = await createMigrationMaster( + migrationMasterConfig, + connection, + Address.parse(walletAddress), + ); + setMigrationMasterDeployed(true); + } catch (err) { + if (err instanceof Error) { + showNotification(<>{err.message}, "error"); + onClose(); + setMigrationStarted(false); + } + } + }; + + const mintJettonsToMaster = async (connection: TonConnectUI) => { + const amount = totalSupply; + const parsedJettonMaster = Address.parse(jettonAddress!); + const migrationMasterConfig: MigrationMasterConfig = { + oldJettonMinter: parsedJettonMaster, + newJettonMinter: Address.parse(newMinterAddress), + }; + const params = { + code: MIGRATION_MASTER_CODE, + data: await migrationMasterConfigToCell(migrationMasterConfig), + deployer: Address.parse(walletAddress!), + value: MIGRATION_MASTER_DEPLOY_GAS, + }; + const migrationMasterAddress = new ContractDeployer().addressForContract(params); + + const newMinterJettonWalletAddress = await makeGetCall( + Address.parse(newMinterAddress), + "get_wallet_address", + [beginCell().storeAddress(Address.parse(walletAddress!)).endCell()], + ([addressCell]) => cellToAddress(addressCell), + await getClient(), + ); + + try { + await jettonDeployController.transfer( + connection, + amount!, + migrationMasterAddress.toFriendly(), + walletAddress!, + newMinterJettonWalletAddress.toFriendly(), + ); + setMintedJettonsToMaster(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: +
+
    +
  • + + 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, }; }