// Complete asset management feature for Polkadot dApps using the Assets pallet. Use when user needs fungible token/asset functionality including creating custom tokens, minting tokens to accounts, transferring tokens between accounts, destroying tokens, viewing portfolios, or managing token metadata. Generates production-ready code (~2,200 lines across 15 files) with full lifecycle support (create→mint→transfer→destroy), real-time fee estimation, transaction tracking, and user-friendly error messages. Works with template infrastructure (WalletContext, ConnectionContext, TransactionContext, balance utilities, shared components). Load when user mentions assets, tokens, fungible tokens, token creation, minting, portfolio, or asset pallet.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | asset-management |
| description | Complete asset management feature for Polkadot dApps using the Assets pallet. Use when user needs fungible token/asset functionality including creating custom tokens, minting tokens to accounts, transferring tokens between accounts, destroying tokens, viewing portfolios, or managing token metadata. Generates production-ready code (~2,200 lines across 15 files) with full lifecycle support (create→mint→transfer→destroy), real-time fee estimation, transaction tracking, and user-friendly error messages. Works with template infrastructure (WalletContext, ConnectionContext, TransactionContext, balance utilities, shared components). Load when user mentions assets, tokens, fungible tokens, token creation, minting, portfolio, or asset pallet. |
Implement complete asset management functionality for Polkadot dApps.
Generate asset management in this order:
Output: 14 new files, 4 modified files, ~2,100 lines
Template provides: useFee hook and FeeDisplay component (used by all features)
Follow template's CLAUDE.md strictly:
State: NEVER useReducer - use useState or context only
TypeScript: NEVER any or as - use unknown and narrow types
Architecture: Components presentational, logic in lib/ and hooks
Exports: ALL exports through barrel files (index.ts)
Balance: ALWAYS use template's toPlanck/fromPlanck - NEVER create custom
Components: ALWAYS use template shared components - NEVER recreate
Navigation: Add links to EXISTING SIDEBAR in App.tsx - NEVER create separate tab navigation
❌ Creating tab navigation in page content - Navigation belongs in App.tsx sidebar
❌ Custom balance utilities - Use template's toPlanck/fromPlanck
❌ Recreating FeeDisplay or TransactionFormFooter - Use template components
❌ Using @polkadot/api - Only use polkadot-api
❌ Type assertions (as) - Let types prove correctness
See references/asset-operations.md for complete patterns.
Exports:
createAssetBatch(api, params, signerAddress) - Create + metadata + optional mintmintTokens(api, params) - Mint tokens to recipienttransferTokens(api, params) - Transfer tokensdestroyAssetBatch(api, params) - 5-step destructionKey: Use toPlanck from template, MultiAddress.Id(), Binary.fromText(), .decodedCall, Utility.batch_all()
Toast configurations for all operations:
import type { ToastConfig } from './toastConfigs'
export const createAssetToasts: ToastConfig<CreateAssetParams> = {
signing: (params) => ({ description: `Creating ${params.symbol}...` }),
broadcasted: (params) => ({ description: `${params.symbol} sent to network` }),
inBlock: (params) => ({ description: `${params.symbol} in block` }),
finalized: (params) => ({ title: 'Asset Created! 🎉', description: `${params.name} ready` }),
error: (params, error) => ({ title: 'Creation Failed', description: parseError(error) }),
}
Create similar configs for mint, transfer, destroy.
See references/error-messages.md for complete list.
Exports ASSET_ERROR_MESSAGES object and getAssetErrorMessage(errorType) function.
Asset-specific query invalidation helpers:
import type { QueryClient } from '@tanstack/react-query'
export const invalidateAssetQueries = async (queryClient: QueryClient) => {
await queryClient.invalidateQueries({ queryKey: ['assets'] })
await queryClient.invalidateQueries({ queryKey: ['assetMetadata'] })
}
export const invalidateBalanceQueries = (
queryClient: QueryClient,
assetId: number,
addresses: (string | undefined)[]
) => {
addresses.forEach((address) => {
if (address) {
queryClient.invalidateQueries({ queryKey: ['assetBalance', assetId, address] })
}
})
}
Note: Template has base queryHelpers.ts - this adds asset-specific helpers.
Generic mutation hook:
export const useAssetMutation = <TParams>({
params,
operationFn,
toastConfig,
onSuccess,
transactionKey,
isValid,
}: AssetMutationConfig<TParams>) => {
const { selectedAccount } = useWalletContext()
const { executeTransaction } = useTransaction<TParams>(toastConfig)
const transaction = selectedAccount && (!isValid || isValid(params))
? operationFn(params)
: null
const mutation = useMutation({
mutationFn: async () => {
if (!selectedAccount || !transaction) throw new Error('No account or transaction')
const observable = transaction.signSubmitAndWatch(selectedAccount.polkadotSigner)
await executeTransaction(transactionKey, observable, params)
},
onSuccess,
})
return { mutation, transaction }
}
Query next available asset ID:
export function useNextAssetId() {
const { api } = useConnectionContext()
const { data, isLoading } = useQuery({
queryKey: ['nextAssetId'],
queryFn: async () => {
const result = await api.query.Assets.NextAssetId.getValue()
if (result === undefined) throw new Error('NextAssetId undefined')
return result
},
staleTime: 0,
gcTime: 0,
})
return { nextAssetId: data?.toString() ?? '', isLoading }
}
See references/form-patterns.md and references/template-integration.md for complete patterns.
Create these forms using standard layout from references/form-patterns.md:
useNextAssetId(), fields: name, symbol, decimals, minBalance, initialSupplyAll forms use:
AccountDashboard at topTransactionReview in right columnTransactionFormFooter at bottomFeatureErrorBoundary wrapperAssetList.tsx - Query and display all assets:
const { data: assets } = useQuery({
queryKey: ['assets'],
queryFn: async () => await api.query.Assets.Asset.getEntries(),
})
AssetCard.tsx - Individual asset display with action menu
AssetBalance.tsx - Display asset balance for account using formatBalance from template
Portfolio view combining AccountDashboard + AssetList.
NO tab navigation in this component - navigation is in App.tsx sidebar (see Layer 4).
components/index.ts - Add:
export { CreateAsset } from './CreateAsset'
export { MintTokens } from './MintTokens'
export { TransferTokens } from './TransferTokens'
export { DestroyAsset } from './DestroyAsset'
export { AssetList } from './AssetList'
export { AssetCard } from './AssetCard'
export { AssetBalance } from './AssetBalance'
export { AssetDashboard } from './AssetDashboard'
hooks/index.ts - Add:
export { useAssetMutation } from './useAssetMutation'
export { useNextAssetId } from './useNextAssetId'
// Note: useFee is in template, not generated here
lib/index.ts - Add:
export * from './assetOperations'
export { invalidateAssetQueries, invalidateBalanceQueries } from './assetQueryHelpers'
export { getAssetErrorMessage } from './assetErrorMessages'
CRITICAL: Add navigation links to EXISTING SIDEBAR, not as separate tabs.
Common mistake: Creating tab navigation in the main content area. Instead:
// In App.tsx sidebar navigation
<nav className="sidebar">
{/* Existing links */}
<Link to="/dashboard">Dashboard</Link>
{/* ADD asset management links HERE in sidebar */}
<Link to="/assets/create">Create Asset</Link>
<Link to="/assets/mint">Mint Tokens</Link>
<Link to="/assets/transfer">Transfer Tokens</Link>
<Link to="/assets/destroy">Destroy Asset</Link>
<Link to="/assets/portfolio">Portfolio</Link>
</nav>
// In routes
<Routes>
{/* Existing routes */}
<Route path="/" element={<Dashboard />} />
{/* ADD asset management routes */}
<Route path="/assets/create" element={<CreateAsset />} />
<Route path="/assets/mint" element={<MintTokens />} />
<Route path="/assets/transfer" element={<TransferTokens />} />
<Route path="/assets/destroy" element={<DestroyAsset />} />
<Route path="/assets/portfolio" element={<AssetDashboard />} />
</Routes>
DO NOT create separate tab navigation in the page content - use the existing sidebar.
After generation:
# REQUIRED
bash .claude/scripts/validate-typescript.sh
# Verify imports
grep -r "@polkadot/api" src/ # Should be ZERO
grep -r "parseUnits\|formatUnits" src/ # Should be ZERO (use template utilities)
After implementation:
useFee)Load these as needed during implementation:
references/asset-operations.mdreferences/form-patterns.mdreferences/error-messages.mdreferences/template-integration.mdtoPlanck, fromPlanck, formatBalance, useFee, FeeDisplayTransactionFormFooter, TransactionReview, AccountDashboard