Claude-skill-registry asset-management

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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/asset-management" ~/.claude/skills/majiayu000-claude-skill-registry-asset-management && rm -rf "$T"
manifest: skills/data/asset-management/SKILL.md
source content

Asset Management Feature

Implement complete asset management functionality for Polkadot dApps.

Implementation Overview

Generate asset management in this order:

  1. Pure functions (lib/) - Asset operations, toast configs, error messages
  2. Custom hooks - Mutation management, fee estimation, asset ID queries
  3. Components - Forms for create/mint/transfer/destroy, asset lists, portfolio
  4. Integration - Exports and routing

Output: 14 new files, 4 modified files, ~2,100 lines

Template provides:

useFee
hook and
FeeDisplay
component (used by all features)

Critical Conventions

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

Common Mistakes

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

Layer 1: Pure Functions

1. lib/assetOperations.ts

See

references/asset-operations.md
for complete patterns.

Exports:

  • createAssetBatch(api, params, signerAddress)
    - Create + metadata + optional mint
  • mintTokens(api, params)
    - Mint tokens to recipient
  • transferTokens(api, params)
    - Transfer tokens
  • destroyAssetBatch(api, params)
    - 5-step destruction

Key: Use

toPlanck
from template,
MultiAddress.Id()
,
Binary.fromText()
,
.decodedCall
,
Utility.batch_all()

2. lib/assetToasts.ts

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.

3. lib/assetErrorMessages.ts

See

references/error-messages.md
for complete list.

Exports

ASSET_ERROR_MESSAGES
object and
getAssetErrorMessage(errorType)
function.

4. lib/assetQueryHelpers.ts (NEW file)

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.

Layer 2: Custom Hooks

5. hooks/useAssetMutation.ts

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 }
}

6. hooks/useNextAssetId.ts

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 }
}

Layer 3: Components

See

references/form-patterns.md
and
references/template-integration.md
for complete patterns.

8-11. Form Components

Create these forms using standard layout from

references/form-patterns.md
:

  • CreateAsset.tsx - Create with
    useNextAssetId()
    , fields: name, symbol, decimals, minBalance, initialSupply
  • MintTokens.tsx - Mint, fields: assetId, recipient, amount
  • TransferTokens.tsx - Transfer, fields: assetId, recipient, amount
  • DestroyAsset.tsx - Destroy with confirmation, field: assetId (type to confirm)

All forms use:

  • AccountDashboard
    at top
  • TransactionReview
    in right column
  • TransactionFormFooter
    at bottom
  • FeatureErrorBoundary
    wrapper

12-14. Display Components

AssetList.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

15. AssetDashboard.tsx

Portfolio view combining

AccountDashboard
+
AssetList
.

NO tab navigation in this component - navigation is in App.tsx sidebar (see Layer 4).

Layer 4: Integration

16-18. Exports

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'

19. App.tsx

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.

Validation

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)

Expected Capabilities

After implementation:

  • ✅ Create custom tokens with metadata
  • ✅ Mint tokens to recipients
  • ✅ Transfer tokens between accounts
  • ✅ Destroy tokens (5-step process)
  • ✅ View portfolio and balances
  • ✅ Real-time fee estimation (via template's
    useFee
    )
  • ✅ Transaction notifications (via template's TransactionContext)
  • ✅ User-friendly error messages

References

Load these as needed during implementation:

  • Asset operations:
    references/asset-operations.md
  • Form patterns:
    references/form-patterns.md
  • Error messages:
    references/error-messages.md
  • Template integration:
    references/template-integration.md

Completion Checklist

  • 14 new files generated (useFee is in template, not generated)
  • 4 files modified (3 index.ts + App.tsx)
  • Navigation added to EXISTING SIDEBAR (not as separate tabs)
  • TypeScript validation passes
  • Zero @polkadot/api imports
  • Template utilities used:
    toPlanck
    ,
    fromPlanck
    ,
    formatBalance
    ,
    useFee
    ,
    FeeDisplay
  • Shared components used:
    TransactionFormFooter
    ,
    TransactionReview
    ,
    AccountDashboard
  • All exports through barrel files