Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 49 additions & 29 deletions packages/web-wallet/src/components/CreateTokenDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useWallet } from '../contexts/WalletContext';
import { useFeatureToggle } from '../contexts/FeatureToggleContext';
import { useInvokeSnap } from '@hathor/snap-utils';
import { helpersUtils, tokensUtils, constants } from '@hathor/wallet-lib';
import { helpersUtils, tokensUtils, constants, TokenVersion } from '@hathor/wallet-lib';
import { formatAmount, amountToCents } from '../utils/hathor';
import { readOnlyWalletWrapper } from '../services/ReadOnlyWalletWrapper';
import { getAddressForMode } from '../utils/addressMode';
// TODO: Re-enable when fee token feature is ready
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { useToast } from '@/hooks/use-toast';
import { Info } from 'lucide-react';

interface CreateTokenDialogProps {
isOpen: boolean;
Expand Down Expand Up @@ -103,6 +104,7 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
} | null>(null);

const { registerToken, balances, addressMode } = useWallet();
const { isFeeTokensEnabled } = useFeatureToggle();
const invokeSnap = useInvokeSnap();
const { toast } = useToast();

Expand All @@ -114,8 +116,7 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
handleSubmit,
formState: { errors },
watch,
// TODO: Re-enable when fee token feature is ready
// setValue,
setValue,
reset,
} = useForm<CreateTokenFormData>({
resolver: zodResolver(createTokenSchema),
Expand All @@ -134,16 +135,22 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }

const isNFT = watch('isNFT');
const amount = watch('amount');
// TODO: Re-enable when fee token feature is ready
// const tokenType = watch('tokenType');
const tokenType = watch('tokenType');

// Determine if this is a fee-based token (no deposit required)
const isFeeBasedToken = isFeeTokensEnabled && tokenType === 'fee' && !isNFT;

// Calculate 1% HTR deposit (1 HTR per 100 tokens)
// For every 100 tokens, 1 HTR (100 cents) deposit is required
// Formula: (tokens / 100) * 100 cents = tokens * 1 cent
// Fee-based tokens don't require a deposit
const depositInCents = useMemo(() => {
try {
if (!amount || amount === '0') return 0n;

// Fee-based tokens don't require a deposit
if (isFeeBasedToken) return 0n;

let amountInBaseUnits: bigint;
if (isNFT) {
// NFTs: whole numbers only, 1 NFT = 0.01 HTR (1 base unit)
Expand All @@ -160,7 +167,7 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
} catch {
return 0n;
}
}, [amount, isNFT]);
}, [amount, isNFT, isFeeBasedToken]);

// Check if user has insufficient balance
const hasInsufficientBalance = useMemo(() => {
Expand All @@ -172,15 +179,14 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
setError(null);

try {
// Determine if this is a fee-based token creation
const isCreatingFeeToken = isFeeTokensEnabled && data.tokenType === 'fee' && !data.isNFT;

// Derive mint/melt settings
// For NFTs: use the checkbox values
// For regular tokens: based on token type (deposit = true, fee = false)
const createMint = data.isNFT
? (data.createMintAuthority || false)
: data.tokenType === 'deposit';
const createMelt = data.isNFT
? (data.createMeltAuthority || false)
: data.tokenType === 'deposit';
// For regular tokens: use the checkbox values
const createMint = data.createMintAuthority || false;
const createMelt = data.createMeltAuthority || false;

// Get addresses based on address mode
const changeAddress = await getAddressForMode(addressMode, readOnlyWalletWrapper);
Expand All @@ -193,7 +199,7 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
? BigInt(data.amount)
: amountToCents(data.amount);

// Prepare RPC params matching createTokenRpcSchema
// Prepare RPC params
const params = {
name: data.name,
symbol: data.symbol,
Expand All @@ -207,6 +213,7 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
allow_external_melt_authority_address: false,
data: data.isNFT && data.nftData ? [data.nftData] : null,
address: mintAddress, // Mint address where tokens are sent
...(isCreatingFeeToken && { token_version: TokenVersion.FEE }),
};

// Call RPC
Expand Down Expand Up @@ -533,8 +540,8 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
</div>
</div>

{/* TODO: Re-enable Token Type dropdown when fee token feature is ready */}
{/* {!isNFT && (
{/* Token Type dropdown - only shown when fee tokens feature is enabled */}
{isFeeTokensEnabled && !isNFT && (
<div className="flex flex-col md:flex-row md:items-center gap-2 md:gap-6">
<div className="flex items-center gap-2 md:w-[120px]">
<label className="text-sm md:text-base font-bold text-white">Token Type</label>
Expand Down Expand Up @@ -575,22 +582,35 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
</ul>
</div>
</div>
)} */}
)}

{/* Deposit Display */}
{amount && depositInCents > 0n && (
{amount && parseFloat(amount) > 0 && (
<div className="space-y-1">
<p className="text-sm text-muted-foreground">
<span className="uppercase tracking-wide font-medium">DEPOSIT:</span> {formatAmount(depositInCents, false)} HTR
</p>
<p className="text-sm text-muted-foreground">
<span className="uppercase tracking-wide font-medium">TOTAL:</span> {formatAmount(depositInCents, false)} HTR ({formatAmount(htrBalance, false)} HTR AVAILABLE)
</p>
{isFeeBasedToken ? (
<>
<p className="text-sm text-muted-foreground">
<span className="uppercase tracking-wide font-medium">DEPOSIT:</span> No deposit required
</p>
<p className="text-xs text-muted-foreground mt-1">
Fee-based tokens charge 1 HTR per transaction output instead of requiring an upfront deposit.
</p>
</>
) : depositInCents > 0n && (

@raul-oliveira raul-oliveira Feb 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid nested ternary ifs

<>
<p className="text-sm text-muted-foreground">
<span className="uppercase tracking-wide font-medium">DEPOSIT:</span> {formatAmount(depositInCents, false)} HTR
</p>
<p className="text-sm text-muted-foreground">
<span className="uppercase tracking-wide font-medium">TOTAL:</span> {formatAmount(depositInCents, false)} HTR ({formatAmount(htrBalance, false)} HTR AVAILABLE)
</p>
</>
)}
</div>
)}

{/* Insufficient Balance Warning */}
{hasInsufficientBalance && amount && parseFloat(amount) > 0 && (
{/* Insufficient Balance Warning - only for deposit-based tokens */}
{hasInsufficientBalance && !isFeeBasedToken && amount && parseFloat(amount) > 0 && (
<div className="flex items-start gap-2 p-3 bg-yellow-500/10 border border-yellow-500/50 rounded-lg">
<AlertCircle className="w-4 h-4 text-yellow-400 flex-shrink-0 mt-0.5" />
<span className="text-yellow-400 text-sm">
Expand All @@ -610,7 +630,7 @@ const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({ isOpen, onClose }
{/* Create Button */}
<button
type="submit"
disabled={isLoading || !amount || hasInsufficientBalance}
disabled={isLoading || !amount || (hasInsufficientBalance && !isFeeBasedToken)}
className="w-full px-8 py-2.5 bg-primary hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed text-white rounded-lg transition-colors font-medium flex items-center justify-center gap-2"
>
{isLoading ? (
Expand Down
2 changes: 2 additions & 0 deletions packages/web-wallet/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ export const UNLEASH_POLLING_INTERVAL = 15 * 1000; // 15s

// Feature toggle names
export const WEB_WALLET_MAINTENANCE_TOGGLE = 'web-wallet.maintenance';
export const FEE_TOKENS_RELEASE_TOGGLE = 'fee-tokens.release';

// Feature toggle defaults
// Note: Unleash Proxy only returns enabled toggles. If a toggle is disabled or
// doesn't exist, it won't be in the response.
export const FEATURE_TOGGLE_DEFAULTS: Record<string, boolean> = {
[WEB_WALLET_MAINTENANCE_TOGGLE]: false, // Default: not under maintenance
[FEE_TOKENS_RELEASE_TOGGLE]: false, // Default: fee tokens feature disabled
};
5 changes: 4 additions & 1 deletion packages/web-wallet/src/contexts/FeatureToggleContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
UNLEASH_CLIENT_KEY,
UNLEASH_POLLING_INTERVAL,
WEB_WALLET_MAINTENANCE_TOGGLE,
FEE_TOKENS_RELEASE_TOGGLE,
FEATURE_TOGGLE_DEFAULTS,
STAGE,
SKIP_FEATURE_TOGGLE,
Expand All @@ -32,6 +33,7 @@ function getBrowserId(): string {

interface FeatureToggleContextType {
isUnderMaintenance: boolean;
isFeeTokensEnabled: boolean;
isLoading: boolean;
featureToggles: Record<string, boolean>;
/** Unique browser identifier used for feature toggle targeting */
Expand Down Expand Up @@ -148,9 +150,10 @@ export function FeatureToggleProvider({ children }: FeatureToggleProviderProps)
}, []);

const isUnderMaintenance = featureToggles[WEB_WALLET_MAINTENANCE_TOGGLE] ?? FEATURE_TOGGLE_DEFAULTS[WEB_WALLET_MAINTENANCE_TOGGLE];
const isFeeTokensEnabled = featureToggles[FEE_TOKENS_RELEASE_TOGGLE] ?? FEATURE_TOGGLE_DEFAULTS[FEE_TOKENS_RELEASE_TOGGLE];

return (
<FeatureToggleContext.Provider value={{ isUnderMaintenance, isLoading, featureToggles, browserId }}>
<FeatureToggleContext.Provider value={{ isUnderMaintenance, isFeeTokensEnabled, isLoading, featureToggles, browserId }}>
{children}
</FeatureToggleContext.Provider>
);
Expand Down
Loading