Native Asset Holdings Changelog - v7.4 to v8.0
This page covers changes to the Asset, Nft, and Portfolio pallets that let an Account hold Polymesh assets directly, without a portfolio. It is part of the v7.4 → v8.0 changelog. For the Settlement pallet (instructions, affirmations, legs), see Settlement & Instructions.
Audience: asset issuers, transfer agents, and anyone building tooling against asset ownership, balances, or NFT collections.
Overview
The AssetHolder type (Account | Portfolio(PortfolioId)) already existed in v7.4, but only the Nft pallet used it — an account could already hold NFTs directly via PortfolioKind::AccountId, a variant bolted onto the portfolio-kind enum as a non-breaking addition. That path still went through PortfolioId, which always carries a did: IdentityId field regardless of kind — so holding via an account still required that DID linked to the account also be provided instead of a wallet-only model. The Asset pallet's per-account balance storage (AssetBalance, LockedBalance) also already existed in v7.4, unused by any public call.
Chain v8 doesn't remove the DID-linkage requirement — AssetHolder::Account still resolves the account to a DID via Identity::KeyRecords and errors IdentityNotFoundForAccountPortfolio if none is found — but it does give account-based holding a proper, first-class type instead of a portfolio-kind extension.
Chain v8 turns this into a proper first-class concept:
- A new
AssetHolderKindenum (Account | DefaultPortfolio | UserPortfolio(PortfolioNumber)) replaces thePortfolioKind::AccountIdvariant as the way to specify "who should receive this" without needing the caller's identity resolved yet.PortfolioKindreverts to its original two variants (Default,User) and only means an actual portfolio. - The
Asset(fungible) pallet is wired up to useAssetHolder/AssetHolderKindthe same wayNftalready did.
If your integration already handled NFTs held by accounts, the model is now consistent for fungible assets too — and PortfolioKind no longer accepts an AccountId variant, so any code passing that must switch to the new types.
Breaking changes
1. issue / redeem take AssetHolderKind, not PortfolioKind
// v7.4
issue(asset_id, amount, portfolio_kind: PortfolioKind)
redeem(asset_id, value, portfolio_kind: PortfolioKind)
// v8.0
issue(asset_id, amount, asset_holder_kind: AssetHolderKind)
redeem(asset_id, value, asset_holder_kind: AssetHolderKind)
Same call indices (7, 8), same intent, different type — AssetHolderKind is not SCALE-compatible with PortfolioKind.
2. controller_transfer takes an explicit source holder and destination kind
// v7.4
controller_transfer(asset_id, value, from_portfolio: PortfolioId)
// v8.0
controller_transfer(asset_id, value, source: AssetHolder, destination_kind: AssetHolderKind)
The v7.4 call could only pull funds from a portfolio, to the caller's own portfolio. The v8 call can pull from any AssetHolder (account or portfolio) and send to either an account or a portfolio.
3. approve — new (Allowances)
approve(asset_id, spender: AccountId, amount: Balance)
Sets a spending allowance for spender over the caller's balance, tracked in the new Allowances storage ((owner, spender, asset_id) -> Balance). Setting amount to 0 removes the allowance entry rather than storing a zero. This has no v7.4 equivalent. Two new events accompany it: Approval { owner, spender, asset_id, amount } when the allowance is set, and AllowanceSpent { owner, spender, asset_id, amount_spent, remaining_allowance } when a spender draws on it.
approve on its own doesn't move funds — the allowance is drawn down when the spender later calls Settlement::transfer_funds naming the owner's account as from (see Settlement & Instructions). Together they emulate the ERC-20 approve / transferFrom pattern: an owner pre-authorizes a spend, and a third party (typically a smart contract) executes it without needing the owner's signature on the transfer itself.
Query a spender's remaining allowance with the new AssetApi::allowance(owner, spender, asset_id) Runtime API rather than decoding Allowances storage directly — see Runtime APIs & RPC. Note that the asset_transferReport/nft_transferReport JSON-RPC methods are removed entirely in v8 — AssetApi::transfer_report/NFTApi::transfer_report are now state_call-only.
4. NFT: issue_nft / redeem_nft / controller_transfer
Same pattern as fungible assets:
// v7.4
issue_nft(asset_id, nft_metadata_attributes, portfolio_kind: PortfolioKind)
redeem_nft(asset_id, nft_id, portfolio_kind: PortfolioKind, number_of_keys)
controller_transfer(nfts, source_portfolio: PortfolioId, callers_portfolio_kind: PortfolioKind)
// v8.0
issue_nft(asset_id, nft_metadata_attributes, holdings_kind: AssetHolderKind)
redeem_nft(asset_id, nft_id, holdings_kind: AssetHolderKind, number_of_keys)
controller_transfer(nfts, source: AssetHolder, destination_kind: AssetHolderKind)
5. transfer_nft — new
transfer_nft(nfts: NFTs, to: AccountId, memo: Option<Memo>)
Direct account-to-account NFT transfer. Unlike the Asset pallet's transfer_asset (see below), there is no separate affirm/reject step — the transfer completes immediately.
6. NFT ownership storage consolidated
NFTOwner ((AssetId, NFTId) -> PortfolioId) — already marked "will be deprecated" in v7.4 — is removed in v8. Ownership is tracked entirely through NFTHolder and Owner ((AssetId, NFTId) -> AssetHolder), both of which already existed in v7.4.
7. NFTPortfolioUpdated event renamed and retyped
// v7.4
NFTPortfolioUpdated(IdentityId, NFTs, Option<PortfolioId>, Option<PortfolioId>, PortfolioUpdateReason)
// v8.0
NFTHoldingsUpdated(IdentityId, NFTs, Option<AssetHolder>, Option<AssetHolder>, HoldingsUpdateReason)
PortfolioUpdateReason is correspondingly renamed HoldingsUpdateReason.
8. Portfolio: parameter renames, and PortfolioNFT storage restructured
delete_portfolio(num) → delete_portfolio(portfolio_number); rename_portfolio(num, to_name) → rename_portfolio(portfolio_number, new_portfolio_name). Types and call indices are unchanged — only the parameter names differ, which matters if you build extrinsics with named arguments.
Separately, the PortfolioNFT storage item (tracks which NFTs a portfolio holds) changes from a StorageDoubleMap<PortfolioId, (AssetId, NFTId), bool> to a StorageNMap<(PortfolioId, AssetId, NFTId), bool>. The key set is the same; only the storage map type and resulting key encoding change. Relevant only if you decode this storage item directly rather than through a query API.
9. Errors
Asset — new: InsufficientAllowance, InsufficientTokensLocked, InvalidTransferInvalidReceiverDID, KeyNotFoundForDid, SelfOwnershipTransferNotAllowed, UnauthorizedHolderKey. Removed: InvalidTransferInvalidReceiverCDD, InvalidTransferInvalidSenderCDD (CDD is no longer checked at the transfer level, consistent with the DID-only onboarding model — see Identity & Onboarding).
Nft — new: InvalidNFTTransferInvalidReceiverDID, InvalidNFTTransferSenderDidMatchesReceiverDid, NFTIsNotLocked. Removed: InvalidNFTTransferInvalidReceiverCDD, InvalidNFTTransferInvalidSenderCDD, InvalidNFTTransferSenderIdMatchesReceiverId — same CDD-to-DID pattern as Asset, plus a rename (...SenderIdMatchesReceiverId → ...SenderDidMatchesReceiverDid).
Portfolio — removed: AccountBasedPortfoliosCannotHaveCustodians (the account-based-portfolio hack this error guarded against is gone now that accounts are first-class AssetHolders, not a special portfolio kind) and InvalidTransferSenderIdMatchesReceiverId, with no direct replacement in Portfolio itself. Similar self-transfer guards exist elsewhere in v8 (Asset::SelfOwnershipTransferNotAllowed above, Settlement::SenderSameAsReceiver in Settlement & Instructions), though these are separate errors on separate pallets, not a renamed version of this one.
What did not change
transfer_asset,receiver_affirm_asset_transfer, andreject_asset_transferon theAssetpallet — these already existed in v7.4 at the same call indices (34, 35, 36), with the same signatures. They are not new in v8.- The
AssetHolderenum itself (Account | Portfolio(PortfolioId)) is unchanged from v7.4. - The
AssetBalanceandLockedBalancestorage maps are unchanged from v7.4.
Migration checklist
- Replace any use of
PortfolioKind::AccountId— it no longer exists. UseAssetHolderKind::Accountfor calls that take it, orAssetHolder::Accountwhere a concrete holder is required. - Update
issue,redeem,issue_nft,redeem_nftcall encoding for theAssetHolderKindparameter type. - Update
controller_transfer(bothAssetandNft) for the newsource/destination_kindparameters. - If you track NFT ownership via
NFTOwnerstorage orNFTPortfolioUpdatedevents, switch toOwner/NFTHolderstorage and theNFTHoldingsUpdatedevent. - Do not assume
transfer_asset/receiver_affirm_asset_transfer/reject_asset_transferare new — they carry over unchanged from v7.4 if you already support them. - Update any extrinsic construction that names
Portfolio.delete_portfolio/rename_portfolioparameters by name. - Stop matching on the removed CDD-flavored errors (
InvalidTransferInvalidReceiverCDD/...SenderCDD,InvalidNFTTransferInvalidReceiverCDD/...SenderCDD,AccountBasedPortfoliosCannotHaveCustodians) — none exist on v8.
For the complete literal list of every added/removed/modified call, event, error, storage item, and constant on Asset, Nft, and Portfolio, see the Full Pallet API Reference. AssetApi and NFTApi's transfer_report Runtime API methods went through the same PortfolioId → AssetHolder retyping as the pallet calls above, AssetApi gains a new allowance method, and the asset_transferReport/nft_transferReport JSON-RPC wrappers are removed — see Runtime APIs & RPC.