diff --git a/src/lib/massa-react/hooks/index.ts b/src/lib/massa-react/hooks/index.ts index 300d7fad..428a8c72 100644 --- a/src/lib/massa-react/hooks/index.ts +++ b/src/lib/massa-react/hooks/index.ts @@ -4,3 +4,5 @@ export * from './useDisclaimer'; export * from './useResolveDeweb'; export * from './types'; export * from './const'; +export * from './useSend'; +export * from './useAllowance'; diff --git a/src/lib/massa-react/hooks/types.ts b/src/lib/massa-react/hooks/types.ts index 51f021ea..cb7cdbd9 100644 --- a/src/lib/massa-react/hooks/types.ts +++ b/src/lib/massa-react/hooks/types.ts @@ -4,3 +4,11 @@ export type ToasterMessage = { error: string; timeout?: string; }; + +export interface Asset { + decimals: number; + balance: bigint; + symbol: string; + address?: string; + isNative?: boolean; +} diff --git a/src/lib/massa-react/hooks/useAllowance.tsx b/src/lib/massa-react/hooks/useAllowance.tsx new file mode 100644 index 00000000..7cd3e62f --- /dev/null +++ b/src/lib/massa-react/hooks/useAllowance.tsx @@ -0,0 +1,69 @@ +import { useCallback, useMemo, useState } from 'react'; +import { MRC20, Provider } from '@massalabs/massa-web3'; +import { useHandleOperation } from './useHandleOperation'; +import { Asset } from './types'; + +export interface AllowanceParams { + spender: string; + amount: bigint; + token: Asset; +} + +export interface UseAllowanceOptions { + provider: Provider | null; +} + +export function useAllowance(options: UseAllowanceOptions) { + const { provider } = options; + const [isProcessing, setIsProcessing] = useState(false); + const { handleOperation } = useHandleOperation(); + + const increaseAllowance = useCallback( + async ({ spender, amount, token }: AllowanceParams): Promise => { + if (!provider) throw new Error('No provider'); + setIsProcessing(true); + + if (!token.address) throw new Error('Token address required'); + const mrc20 = new MRC20(provider, token.address); + try { + const op = await mrc20.increaseAllowance(spender, amount); + + await handleOperation(op, { + pending: `Increasing allowance ${amount} ${token.symbol}`, + success: `Increased allowance ${amount} ${token.symbol}`, + error: `Error increasing allowance`, + timeout: `Timeout increasing allowance`, + }); + } finally { + setIsProcessing(false); + } + }, + [provider, handleOperation], + ); + + const decreaseAllowance = useCallback( + async ({ spender, amount, token }: AllowanceParams): Promise => { + if (!provider) throw new Error('No provider'); + setIsProcessing(true); + if (!token.address) throw new Error('Token address required'); + const mrc20 = new MRC20(provider, token.address); + try { + const op = await mrc20.decreaseAllowance(spender, amount); + await handleOperation(op, { + pending: `Decreasing allowance ${amount} ${token.symbol}`, + success: `Decreased allowance ${amount} ${token.symbol}`, + error: `Error decreasing allowance`, + timeout: `Timeout decreasing allowance`, + }); + } finally { + setIsProcessing(false); + } + }, + [provider, handleOperation], + ); + + return useMemo( + () => ({ isProcessing, increaseAllowance, decreaseAllowance }), + [isProcessing, increaseAllowance, decreaseAllowance], + ); +} diff --git a/src/lib/massa-react/hooks/useSend.ts b/src/lib/massa-react/hooks/useSend.ts new file mode 100644 index 00000000..f8a46a01 --- /dev/null +++ b/src/lib/massa-react/hooks/useSend.ts @@ -0,0 +1,145 @@ +import { useCallback, useMemo, useState } from 'react'; +import { Address, MRC20, Operation, Provider } from '@massalabs/massa-web3'; +import { useHandleOperation } from './useHandleOperation'; +import toast from 'react-hot-toast'; +import { formatAmount } from '../../util'; +import { Asset } from './types'; + +export interface SendParams { + recipient: string; + amount: bigint; + asset: Asset; +} + +export interface UseSendOptions { + provider: Provider | null; +} + +export function useSend(options: UseSendOptions) { + const { provider } = options; + const [isProcessing, setIsProcessing] = useState(false); + const { handleOperation } = useHandleOperation(); + + /** + * Executes a send operation + * @param sendFn - The function to send the asset + * @param asset - The asset to send + * @param amount - The amount to send + * @param recipient - The recipient address + * @returns void + */ + const execute = useCallback( + async ( + sendFn: () => Promise, + asset: Asset, + amount: bigint, + recipient: string, + ): Promise => { + setIsProcessing(true); + + if (!provider) throw new Error('No provider'); + + try { + Address.fromString(recipient); + } catch { + toast.error('Invalid address'); + return; + } + + if (amount > asset.balance) { + toast.error('Insufficient balance'); + return; + } + + try { + const op = await sendFn(); + + await handleOperation(op, { + pending: `Sending ${ + formatAmount(amount.toString(), asset.decimals).preview + } ${asset.symbol}`, + success: `Sent ${ + formatAmount(amount.toString(), asset.decimals).preview + } ${asset.symbol}`, + error: `Error sending`, + timeout: `Timeout sending`, + }); + } finally { + setIsProcessing(false); + } + }, + [provider, handleOperation], + ); + + /** + * Sends a native Massa coin to a recipient + * @param recipient - The recipient address + * @param amount - The amount to send + * @param asset - The asset to send + * @returns void + */ + const sendMassa = useCallback( + async ({ recipient, amount, asset }: SendParams): Promise => { + if (!provider) throw new Error('No provider'); + await execute( + () => provider.transfer(recipient, amount), + asset, + amount, + recipient, + ); + }, + [provider, execute], + ); + + /** + * Sends a mrc20 token to a recipient + * @param recipient - The recipient address + * @param amount - The amount to send + * @param asset - The token to send + * @returns void + */ + const sendToken = useCallback( + async ({ recipient, amount, asset }: SendParams): Promise => { + if (!provider) throw new Error('No provider'); + if (!asset.address) throw new Error('Token address required'); + const mrc20 = new MRC20(provider, asset.address); + + await execute( + () => mrc20.transfer(recipient, amount), + asset, + amount, + recipient, + ); + }, + [provider, execute], + ); + + /** + * Sends an asset to a recipient + * The Asset can be a native Massa coin or a mrc20 token + * @param recipient - The recipient address + * @param amount - The amount to send + * @param asset - The asset to send + * @returns void + */ + const sendAsset = useCallback( + async ({ recipient, amount, asset }: SendParams): Promise => { + if (asset.isNative) { + return sendMassa({ recipient, amount, asset }); + } + + return sendToken({ recipient, amount, asset }); + }, + [sendMassa, sendToken], + ); + + return useMemo( + () => ({ + isProcessing, + sendAsset, + sendMassa, + sendToken, + }), + [isProcessing, sendAsset, sendMassa, sendToken], + ); +}