SDK Changelog - v30.1.0 to v30.2.0
This guide describes changes between v30.1.0 and v30.2.0 of @polymeshassociation/polymesh-sdk.
Overview
v30.2.0 is a minor release containing new features and bug fixes. There are no breaking changes — upgrading is a drop-in replacement for v30.1.0.
This release adds registrar-gated DID registration, portfolio-level asset pre-approval, and several new settlement/asset getters. It also fixes transaction signing with Ledger hardware wallets using the Polkadot Generic Ledger app, corrects the POLYX balance calculation for chain v8 accounts with staked (held) funds, and fixes v8 off-chain settlement receipt expiry propagation and encoding. No code changes are required in consuming applications, though note the clarified meaning of locked in AccountBalance described below.
New features
registerDid — registrar-gated DID registration (v8 only)
Implements the v8-only identity.registerDid extrinsic, letting an active DID Registrar register a DID for someone else's Account:
const identity = await sdk.identities.registerDid({ targetAccount: '5G...' });
This complements the existing permissionless Identities.selfRegisterDid and the deprecated registerIdentity (cddRegisterDid), which no longer attaches a CDD claim as of v8. Calling this on a v7 chain, or targeting an Account that already has an Identity, raises a PolymeshError.
Portfolio-level asset pre-approval
A Portfolio can now pre-approve receiving a specific asset, so incoming transfers of it auto-affirm without a manual affirm step — the portfolio-level counterpart to the existing Identity-level pre-approval:
await portfolio.preApproveAsset({ asset });
await portfolio.isAssetPreApproved(asset); // true
await portfolio.removeAssetPreApproval({ asset });
Portfolio.preApprovedAssets getter
Lists all assets a Portfolio has pre-approved, paginated over the preApprovedPortfolios storage entries, mirroring Identity.preApprovedAssets.
Settlement getters: leg status and venue signer count
Instruction.getLegStatus({ legId })— returns the execution status of a specific leg in an Instruction.Venue.getSignerCount()— returns the number of signers allowed by a Venue.
unlockInstructionForExecution — complete the lock/relock cycle
Instruction.unlockForExecution() moves a LockedForExecution instruction back to Pending, and Instruction.getRelockStatus() exposes the mediator's last unlock timestamp, relock count, max relock count, and the on-chain relock cooldown window — completing the lock/relock flow that lockForExecution alone couldn't finish.
Ticker length validated against live chain config
reserveTicker, createAsset, and createNftCollection now validate the ticker length against the chain's live TickerConfig.maxTickerLength instead of a hardcoded constant, so the SDK stays correct if the chain's configured max length changes.
BaseAsset.getIssuedInFundingRound
Returns the total amount of an Asset issued in a given funding round:
const issued = await asset.getIssuedInFundingRound('Series A');
Assets.getTickerRegistrationConfig
Returns the chain-wide rules used to validate ticker registrations — maxTickerLength and registrationLength (null if registrations don't expire).
Bug fixes
Correct v8 off-chain settlement receipt expiry and encoding
expiresAtwas computed and signed into the off-chain receipt payload but dropped before reachingaffirmWithReceipts/affirmWithReceiptsWithCount— it's now forwarded into both the returnedOffChainAffirmationReceiptand the raw on-chain receipt details.- The v8 signed payload now includes the chain's
genesis_hashand length-prefixes the receipt label, matching the runtime'sChainScopedMessageencoding. - Fixed the byte order of the signed
expiresAtvalue (it was missing the little-endian flag). signatureToMeshRuntimeMultiSignaturenow uses correctly sized[u8; 64]/[u8; 65]byte arrays instead of a bareU8aFixed, which failed to resolve against a v8 chain's metadata registry.
No action is needed from consumers — off-chain receipt affirmation on v8 chains simply works correctly now.
Support signers that return a signed transaction (Ledger generic app)
Signing any transaction with a Ledger device via the Polkadot Generic Ledger app failed with:
The `signedTransaction` field may not be submitted when `withSignedTransaction` is disabled
The generic Ledger app requires the CheckMetadataHash signed extension, which means the wallet must rebuild the transaction (setting mode and metadataHash) before the device will sign it. The wallet then returns the rebuilt extrinsic in SignerResult.signedTransaction, which the Polkadot API rejects unless the caller opts in.
The SDK now passes withSignedTransaction: true when signing, so signers that return a rebuilt signedTransaction are accepted and the rebuilt extrinsic is what gets submitted to the chain.
Details:
- No impact on other signers — software wallets and local keypairs return only a signature and behave exactly as before; the option merely permits the
signedTransactionfield, it does not request it. - Call data is protected — the SDK also sets
allowCallDataAlteration: false, so a returnedsignedTransactionmay only alter signed extensions (e.g.mode,metadataHash, era, nonce, tip). Any submission where the signer changed the transaction call itself is rejected with an error. - Correct hash tracking — because a rebuilt extrinsic can have a different hash than the one the SDK composed, the SDK now tracks the hash of the transaction actually submitted to the node.
txHashon the transaction object always reflects the on-chain extrinsic.
No migration steps are needed — transactions signed with Ledger devices simply work.
Correct POLYX balance calculation for chain v8 accounts with holds
On chain v8, funds bonded for staking moved from locks on the free balance to holds tracked in reserved, and the single frozen value can legitimately exceed free (frozen funds may overlap with held funds). The SDK still calculated the spendable balance with the pre-v8 formula free - frozen, which produced negative free balances for staking accounts .
Account.getBalance (and accountBalance internally) now follows the chain's rules:
free = chain free - max(frozen - reserved, existential deposit)
clamped at zero, with the existential deposit (0.000001 POLYX) read from the chain rather than hardcoded.
Field semantics (clarified):
free— balance guaranteed to be spendable on transfers and fees. The existential deposit is always excluded, so a transaction paying up tofreewill never fail for balance reasons.locked— everything unavailable: funds on hold (e.g. bonded for staking), frozen funds not covered by holds (e.g. vesting) and the existential deposit. Always equal tototal - free. Note: previously this only reflected the chain'sfrozenvalue; it now also includesreservedfunds, so staking accounts will report a largerlockedvalue than before — this is the correct interpretation under v8 semantics.total— all funds owned by the Account:free + locked(the chain'sfree + reserved).
New fields on AccountBalance expose the underlying chain values for applications that want to present more detail:
reserved— funds placed on hold by the protocol (e.g. POLYX bonded for staking), released when e.g. unbonded and withdrawn.frozen— the minimum balance (out oftotal) that must remain in the Account due to freezes/locks (e.g. vesting). May overlap withreserved: vested POLYX that is also bonded counts towards both. On v7 chains this ismax(miscFrozen, feeFrozen).
These additions are backwards compatible for code that reads balances. Validations that consume free (transferPolyx, bondPolyx, transaction fee checks) now use the corrected value, so they no longer reject valid transactions for staking-heavy accounts.
Version compatibility matrix
| SDK version | Chain v7 | Chain v8 | Middleware V2 | Polkadot.js |
|---|---|---|---|---|
| v30.1.0 | ✅ | ✅ | ≥ v19.6.0-alpha.2 | 16.5.2 |
| v30.2.0 | ✅ | ✅ | ≥ v19.6.0-alpha.2 | 16.5.2 |