diff --git a/apps/namadillo/src/App/Masp/MaspShield.tsx b/apps/namadillo/src/App/Masp/MaspShield.tsx index 7382a3f42..0f4a55795 100644 --- a/apps/namadillo/src/App/Masp/MaspShield.tsx +++ b/apps/namadillo/src/App/Masp/MaspShield.tsx @@ -9,16 +9,23 @@ import { import { allDefaultAccountsAtom } from "atoms/accounts"; import { namadaTransparentAssetsAtom } from "atoms/balance/atoms"; import { chainParametersAtom } from "atoms/chain/atoms"; +import { shieldTxAtom } from "atoms/shield/atoms"; import BigNumber from "bignumber.js"; import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; +import { useTransactionActions } from "hooks/useTransactionActions"; import { wallets } from "integrations"; import { getAssetImageUrl } from "integrations/utils"; import { useAtomValue } from "jotai"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import namadaChain from "registry/namada.json"; -import { Address } from "types"; +import { + Address, + PartialTransferTransactionData, + TransferStep, + TransferTransactionData, +} from "types"; import { MaspTopHeader } from "./MaspTopHeader"; export const MaspShield: React.FC = () => { @@ -26,13 +33,23 @@ export const MaspShield: React.FC = () => { const chainParameters = useAtomValue(chainParametersAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); - const { data: availableAssets, isLoading: isLoadingBalances } = useAtomValue( + const { data: availableAssets, isLoading: isLoadingAssets } = useAtomValue( namadaTransparentAssetsAtom ); + const performShieldTransfer = useAtomValue(shieldTxAtom); const [currentStep, setCurrentStep] = useState(0); const [generalErrorMessage, setGeneralErrorMessage] = useState(""); + const [transaction, setTransaction] = + useState(); + + const { + transactions: myTransactions, + findByHash, + storeTransaction, + } = useTransactionActions(); + const chainId = chainParameters.data?.chainId; const sourceAddress = defaultAccounts.data?.find( @@ -54,6 +71,15 @@ export const MaspShield: React.FC = () => { const assetImage = selectedAsset ? getAssetImageUrl(selectedAsset.asset) : ""; + useEffect(() => { + if (transaction?.hash) { + const tx = findByHash(transaction.hash); + if (tx) { + setTransaction(tx); + } + } + }, [myTransactions]); + const onChangeSelectedAsset = (address?: Address): void => { setSearchParams( (currentParams) => { @@ -93,10 +119,39 @@ export const MaspShield: React.FC = () => { throw new Error("No transaction fee is set"); } - // TODO do the transaction - alert( - "// TODO \n" + JSON.stringify({ amount, destinationAddress }, null, 2) - ); + setTransaction({ + type: "TransparentToShielded", + currentStep: TransferStep.Sign, + asset: selectedAsset.asset, + chainId, + }); + + const txResponse = await performShieldTransfer.mutateAsync({ + sourceAddress, + destinationAddress, + tokenAddress: selectedAsset.originalAddress, + amount, + }); + + // TODO review and improve this data to be more precise and full of details + const tx: TransferTransactionData = { + type: "TransparentToShielded", + currentStep: TransferStep.Complete, + sourceAddress, + destinationAddress, + asset: selectedAsset.asset, + amount, + rpc: txResponse.msg.payload.chain.rpcUrl, + chainId: txResponse.msg.payload.chain.chainId, + hash: txResponse.encodedTx.txs[0]?.hash, + feePaid: txResponse.encodedTx.wrapperTxProps.feeAmount, + resultTxHash: txResponse.encodedTx.txs[0]?.innerTxHashes[0], + status: "success", + createdAt: new Date(), + updatedAt: new Date(), + }; + setTransaction(tx); + storeTransaction(tx); setCurrentStep(2); } catch (err) { @@ -121,7 +176,7 @@ export const MaspShield: React.FC = () => { > { isShielded: true, }} transactionFee={transactionFee} - // TODO - // isSubmitting={something.isPending} + isSubmitting={performShieldTransfer.isPending} errorMessage={generalErrorMessage} onSubmitTransfer={onSubmitTransfer} /> diff --git a/apps/namadillo/src/App/Masp/MaspUnshield.tsx b/apps/namadillo/src/App/Masp/MaspUnshield.tsx index ff64a3a06..47cb7dba8 100644 --- a/apps/namadillo/src/App/Masp/MaspUnshield.tsx +++ b/apps/namadillo/src/App/Masp/MaspUnshield.tsx @@ -9,16 +9,23 @@ import { import { allDefaultAccountsAtom } from "atoms/accounts"; import { namadaShieldedAssetsAtom } from "atoms/balance/atoms"; import { chainParametersAtom } from "atoms/chain/atoms"; +import { unshieldTxAtom } from "atoms/shield/atoms"; import BigNumber from "bignumber.js"; import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; +import { useTransactionActions } from "hooks/useTransactionActions"; import { wallets } from "integrations"; import { getAssetImageUrl } from "integrations/utils"; import { useAtomValue } from "jotai"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import namadaChain from "registry/namada.json"; -import { Address } from "types"; +import { + Address, + PartialTransferTransactionData, + TransferStep, + TransferTransactionData, +} from "types"; import { MaspTopHeader } from "./MaspTopHeader"; export const MaspUnshield: React.FC = () => { @@ -26,13 +33,23 @@ export const MaspUnshield: React.FC = () => { const chainParameters = useAtomValue(chainParametersAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); - const { data: availableAssets, isLoading: isLoadingBalances } = useAtomValue( + const { data: availableAssets, isLoading: isLoadingAssets } = useAtomValue( namadaShieldedAssetsAtom ); + const performUnshieldTransfer = useAtomValue(unshieldTxAtom); const [currentStep, setCurrentStep] = useState(0); const [generalErrorMessage, setGeneralErrorMessage] = useState(""); + const [transaction, setTransaction] = + useState(); + + const { + transactions: myTransactions, + findByHash, + storeTransaction, + } = useTransactionActions(); + const chainId = chainParameters.data?.chainId; const sourceAddress = defaultAccounts.data?.find( @@ -54,6 +71,15 @@ export const MaspUnshield: React.FC = () => { const assetImage = selectedAsset ? getAssetImageUrl(selectedAsset.asset) : ""; + useEffect(() => { + if (transaction?.hash) { + const tx = findByHash(transaction.hash); + if (tx) { + setTransaction(tx); + } + } + }, [myTransactions]); + const onChangeSelectedAsset = (address?: Address): void => { setSearchParams( (currentParams) => { @@ -93,10 +119,39 @@ export const MaspUnshield: React.FC = () => { throw new Error("No transaction fee is set"); } - // TODO do the transaction - alert( - "// TODO \n" + JSON.stringify({ amount, destinationAddress }, null, 2) - ); + setTransaction({ + type: "ShieldedToTransparent", + asset: selectedAsset.asset, + chainId, + currentStep: TransferStep.Sign, + }); + + const txResponse = await performUnshieldTransfer.mutateAsync({ + sourceAddress, + destinationAddress, + tokenAddress: selectedAsset.originalAddress, + amount, + }); + + // TODO review and improve this data to be more precise and full of details + const tx: TransferTransactionData = { + type: "ShieldedToTransparent", + currentStep: TransferStep.Complete, + sourceAddress, + destinationAddress, + asset: selectedAsset.asset, + amount, + rpc: txResponse.msg.payload.chain.rpcUrl, + chainId: txResponse.msg.payload.chain.chainId, + hash: txResponse.encodedTx.txs[0]?.hash, + feePaid: txResponse.encodedTx.wrapperTxProps.feeAmount, + resultTxHash: txResponse.encodedTx.txs[0]?.innerTxHashes[0], + status: "success", + createdAt: new Date(), + updatedAt: new Date(), + }; + setTransaction(tx); + storeTransaction(tx); setCurrentStep(2); } catch (err) { @@ -121,7 +176,7 @@ export const MaspUnshield: React.FC = () => { > { isShielded: false, }} transactionFee={transactionFee} - // TODO - // isSubmitting={something.isPending} + isSubmitting={performUnshieldTransfer.isPending} errorMessage={generalErrorMessage} onSubmitTransfer={onSubmitTransfer} /> diff --git a/apps/namadillo/src/atoms/shield/atoms.ts b/apps/namadillo/src/atoms/shield/atoms.ts new file mode 100644 index 000000000..25170cd12 --- /dev/null +++ b/apps/namadillo/src/atoms/shield/atoms.ts @@ -0,0 +1,38 @@ +import { defaultAccountAtom } from "atoms/accounts"; +import { chainAtom } from "atoms/chain"; +import { indexerUrlAtom, rpcUrlAtom } from "atoms/settings"; +import { atomWithMutation } from "jotai-tanstack-query"; +import { + ShieldTransferParams, + submitShieldTx, + submitUnshieldTx, + UnshieldTransferParams, +} from "./services"; + +export const shieldTxAtom = atomWithMutation((get) => { + const rpcUrl = get(rpcUrlAtom); + const { data: account } = get(defaultAccountAtom); + const { data: chain } = get(chainAtom); + const indexerUrl = get(indexerUrlAtom); + + return { + mutationKey: ["shield-tx"], + mutationFn: async (params: ShieldTransferParams) => { + return await submitShieldTx(rpcUrl, account!, chain!, indexerUrl, params); + }, + }; +}); + +export const unshieldTxAtom = atomWithMutation((get) => { + const rpcUrl = get(rpcUrlAtom); + const { data: account } = get(defaultAccountAtom); + const { data: chain } = get(chainAtom); + const indexerUrl = get(indexerUrlAtom); + + return { + mutationKey: ["unshield-tx"], + mutationFn: async (params: UnshieldTransferParams) => { + return submitUnshieldTx(rpcUrl, account!, chain!, indexerUrl, params); + }, + }; +}); diff --git a/apps/namadillo/src/atoms/shield/services.ts b/apps/namadillo/src/atoms/shield/services.ts new file mode 100644 index 000000000..01bab6788 --- /dev/null +++ b/apps/namadillo/src/atoms/shield/services.ts @@ -0,0 +1,153 @@ +import { + Account, + ShieldingTransferMsgValue, + UnshieldingTransferMsgValue, +} from "@namada/types"; +import BigNumber from "bignumber.js"; +import * as Comlink from "comlink"; +import { EncodedTxData, signTx } from "lib/query"; +import { Address, ChainSettings } from "types"; +import { Shield } from "workers/ShieldMessages"; +import { + registerTransferHandlers as shieldRegisterTransferHandlers, + Worker as ShieldWorkerApi, +} from "workers/ShieldWorker"; +import ShieldWorker from "workers/ShieldWorker?worker"; +import { Unshield } from "workers/UnshieldMessages"; +import { + registerTransferHandlers as unshieldRegisterTransferHandlers, + Worker as UnshieldWorkerApi, +} from "workers/UnshieldWorker"; +import UnshieldWorker from "workers/UnshieldWorker?worker"; + +export type ShieldTransferParams = { + sourceAddress: Address; + destinationAddress: Address; + tokenAddress: Address; + amount: BigNumber; +}; + +export type UnshieldTransferParams = { + sourceAddress: Address; + destinationAddress: Address; + tokenAddress: Address; + amount: BigNumber; +}; + +export const submitShieldTx = async ( + rpcUrl: string, + account: Account, + chain: ChainSettings, + indexerUrl: string, + params: ShieldTransferParams +): Promise<{ + msg: Shield; + encodedTx: EncodedTxData; +}> => { + const { + sourceAddress: source, + destinationAddress: target, + tokenAddress: token, + amount, + } = params; + + shieldRegisterTransferHandlers(); + const worker = new ShieldWorker(); + const shieldWorker = Comlink.wrap(worker); + await shieldWorker.init({ type: "init", payload: { rpcUrl, token } }); + + const shieldingMsgValue = new ShieldingTransferMsgValue({ + target, + data: [{ source, token, amount }], + }); + + const msg: Shield = { + type: "shield", + payload: { + account, + // TODO + gasConfig: { + gasLimit: BigNumber(250000), + gasPrice: BigNumber(0.000001), + }, + shieldingProps: [shieldingMsgValue], + chain, + indexerUrl, + }, + }; + + const { payload: encodedTx } = await shieldWorker.shield(msg); + + const signedTxs = await signTx("namada", encodedTx, source); + + await shieldWorker.broadcast({ + type: "broadcast", + payload: { + encodedTx, + signedTxs, + }, + }); + + worker.terminate(); + + return { msg, encodedTx }; +}; + +export const submitUnshieldTx = async ( + rpcUrl: string, + account: Account, + chain: ChainSettings, + indexerUrl: string, + params: UnshieldTransferParams +): Promise<{ + msg: Unshield; + encodedTx: EncodedTxData; +}> => { + const { + sourceAddress: source, + destinationAddress: target, + tokenAddress: token, + amount, + } = params; + + unshieldRegisterTransferHandlers(); + const worker = new UnshieldWorker(); + const unshieldWorker = Comlink.wrap(worker); + await unshieldWorker.init({ type: "init", payload: { rpcUrl, token } }); + + const unshieldingMsgValue = new UnshieldingTransferMsgValue({ + source, + data: [{ target, token, amount }], + }); + + const msg: Unshield = { + type: "unshield", + payload: { + account, + // TODO + gasConfig: { + gasLimit: BigNumber(250000), + gasPrice: BigNumber(0.000001), + }, + unshieldingProps: [unshieldingMsgValue], + chain, + indexerUrl, + }, + }; + + const { payload: encodedTx } = await unshieldWorker.unshield(msg); + + const signedTxs = await signTx("namada", encodedTx, source); + + await unshieldWorker.broadcast({ + type: "broadcast", + payload: { + encodedTx, + signedTxs, + }, + }); + + worker.terminate(); + + return { msg, encodedTx }; +}; diff --git a/apps/namadillo/src/types/txKind.ts b/apps/namadillo/src/types/txKind.ts index 2ff38547a..cbd6ec677 100644 --- a/apps/namadillo/src/types/txKind.ts +++ b/apps/namadillo/src/types/txKind.ts @@ -7,6 +7,8 @@ export const txKinds = [ "VoteProposal", "RevealPk", "IbcTransfer", + "Shield", + "Unshield", "Unknown", ] as const; diff --git a/apps/namadillo/src/workers/UnshieldMessages.ts b/apps/namadillo/src/workers/UnshieldMessages.ts new file mode 100644 index 000000000..bf65d08a2 --- /dev/null +++ b/apps/namadillo/src/workers/UnshieldMessages.ts @@ -0,0 +1,47 @@ +import BigNumber from "bignumber.js"; + +import { + Account, + TxResponseMsgValue, + UnshieldingTransferMsgValue, +} from "@namada/types"; +import { EncodedTxData } from "lib/query"; +import { ChainSettings } from "types"; +import { WebWorkerMessage } from "./utils"; + +type InitPayload = { + rpcUrl: string; + token: string; +}; + +export type Init = WebWorkerMessage<"init", InitPayload>; +export type InitDone = WebWorkerMessage<"init-done", null>; + +type UnshieldPayload = { + account: Account; + gasConfig: { + gasLimit: BigNumber; + gasPrice: BigNumber; + }; + unshieldingProps: UnshieldingTransferMsgValue[]; + chain: ChainSettings; + indexerUrl: string; +}; +export type Unshield = WebWorkerMessage<"unshield", UnshieldPayload>; +export type UnshieldDone = WebWorkerMessage< + "unshield-done", + EncodedTxData +>; + +type BroadcastPayload = { + encodedTx: EncodedTxData; + signedTxs: Uint8Array[]; +}; +export type Broadcast = WebWorkerMessage<"broadcast", BroadcastPayload>; +export type BroadcastDone = WebWorkerMessage< + "broadcast-done", + TxResponseMsgValue[] +>; + +export type UnshieldMessageIn = Unshield | Broadcast | Init; +export type UnshieldMessageOut = UnshieldDone | BroadcastDone | InitDone; diff --git a/apps/namadillo/src/workers/UnshieldWorker.ts b/apps/namadillo/src/workers/UnshieldWorker.ts new file mode 100644 index 000000000..6c8592a49 --- /dev/null +++ b/apps/namadillo/src/workers/UnshieldWorker.ts @@ -0,0 +1,109 @@ +import { Configuration, DefaultApi } from "@namada/indexer-client"; +import { initMulticore } from "@namada/sdk/inline-init"; +import { getSdk, Sdk } from "@namada/sdk/web"; +import { TxResponseMsgValue, UnshieldingTransferMsgValue } from "@namada/types"; +import * as Comlink from "comlink"; +import { buildTx, EncodedTxData } from "lib/query"; +import { + Broadcast, + BroadcastDone, + Init, + InitDone, + Unshield, + UnshieldDone, +} from "./UnshieldMessages"; +import { registerBNTransferHandler } from "./utils"; + +export class Worker { + private sdk: Sdk | undefined; + + async init(m: Init): Promise { + const { cryptoMemory } = await initMulticore(); + this.sdk = newSdk(cryptoMemory, m.payload); + return { type: "init-done", payload: null }; + } + + async unshield(m: Unshield): Promise { + if (!this.sdk) { + throw new Error("SDK is not initialized"); + } + return { + type: "unshield-done", + payload: await unshield(this.sdk, m.payload), + }; + } + + async broadcast(m: Broadcast): Promise { + if (!this.sdk) { + throw new Error("SDK is not initialized"); + } + + const res = await broadcast(this.sdk, m.payload); + + return { type: "broadcast-done", payload: res }; + } +} + +async function unshield( + sdk: Sdk, + payload: Unshield["payload"] +): Promise> { + const { indexerUrl, account, gasConfig, chain, unshieldingProps } = payload; + + const configuration = new Configuration({ basePath: indexerUrl }); + const api = new DefaultApi(configuration); + const publicKeyRevealed = ( + await api.apiV1RevealedPublicKeyAddressGet(account.address) + ).data.publicKey; + + await sdk.masp.loadMaspParams(""); + const encodedTxData = await buildTx( + sdk, + account, + gasConfig, + chain, + unshieldingProps, + sdk.tx.buildUnshieldingTransfer, + Boolean(publicKeyRevealed) + ); + + return encodedTxData; +} + +// TODO: We will probably move this to the separate worker +async function broadcast( + sdk: Sdk, + payload: Broadcast["payload"] +): Promise { + const { encodedTx, signedTxs } = payload; + + const result: TxResponseMsgValue[] = []; + + for await (const signedTx of signedTxs) { + for await (const _ of encodedTx.txs) { + const response = await sdk.rpc.broadcastTx( + signedTx, + encodedTx.wrapperTxProps + ); + result.push(response); + } + } + return result; +} + +function newSdk( + cryptoMemory: WebAssembly.Memory, + payload: Init["payload"] +): Sdk { + const { rpcUrl, token } = payload; + return getSdk(cryptoMemory, rpcUrl, "", "", token); +} + +export const registerTransferHandlers = (): void => { + registerBNTransferHandler("unshield-done"); + registerBNTransferHandler("unshield"); + registerBNTransferHandler("broadcast"); +}; + +registerTransferHandlers(); +Comlink.expose(new Worker());