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
26 changes: 15 additions & 11 deletions project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const dictionary = process.env.NETWORK_DICTIONARY || '';

const filters: Record<string, Record<string, string[]>> = {
asset: {
AllowanceSpent: ['handleAllowanceSpent'],
Approval: ['handleApproval'],
AssetAffirmationExemption: [],
AssetBalanceUpdated: ['handleAssetBalanceUpdated'],
AssetCreated: ['handleAssetCreated'],
Expand All @@ -20,32 +22,32 @@ const filters: Record<string, Record<string, string[]>> = {
AssetMediatorsRemoved: ['handleAssetMediatorsRemoved'],
AssetOwnershipTransferred: ['handleAssetOwnershipTransferred'],
AssetRenamed: ['handleAssetRenamed'],
AssetTypeChanged: [],
AssetTypeChanged: ['handleAssetTypeChanged'],
AssetUnfrozen: ['handleUnfrozen'],
ClassicTickerClaimed: [],
CreatedAssetTransfer: [],
CreatedAssetTransfer: ['handleCreatedAssetTransfer'],
ControllerTransfer: [],
CustomAssetTypeExists: [],
CustomAssetTypeRegistered: [],
CustomAssetTypeExists: ['handleCustomAssetTypeExists'],
CustomAssetTypeRegistered: ['handleCustomAssetTypeRegistered'],
DivisibilityChanged: ['handleDivisibilityChanged'],
DocumentAdded: ['handleDocumentAdded'],
DocumentRemoved: ['handleDocumentRemoved'],
ExtensionRemoved: [],
FundingRoundSet: ['handleFundingRoundSet'],
GlobalMetadataSpecUpdated: [],
GlobalMetadataSpecUpdated: ['handleGlobalMetadataSpecUpdated'],
IdentifiersUpdated: ['handleIdentifiersUpdated'],
IsIssuable: [],
Issued: ['handleIssued', 'handleAssetIssuedStatistics'],
LocalMetadataKeyDeleted: [],
MetadataValueDeleted: [],
LocalMetadataKeyDeleted: ['handleLocalMetadataKeyDeleted'],
MetadataValueDeleted: ['handleMetadataValueDeleted'],
PreApprovedAsset: ['handlePreApprovedAsset'],
RegisterAssetMetadataGlobalType: [],
RegisterAssetMetadataLocalType: [],
RegisterAssetMetadataGlobalType: ['handleRegisterAssetMetadataGlobalType'],
RegisterAssetMetadataLocalType: ['handleRegisterAssetMetadataLocalType'],
RemoveAssetAffirmationExemption: [],
RemovePreApprovedAsset: ['handleRemovePreApprovedAsset'],
Redeemed: ['handleRedeemed', 'handleAssetRedeemedStatistics'],
SetAssetMetadataValue: [],
SetAssetMetadataValueDetails: [],
SetAssetMetadataValue: ['handleSetAssetMetadataValue'],
SetAssetMetadataValueDetails: ['handleSetAssetMetadataValueDetails'],
TickerLinkedToAsset: ['handleTickerLinkedToAsset'],
TickerRegistered: ['handleTickerRegistered'],
TickerTransferred: ['handleTickerTransferred'],
Expand Down Expand Up @@ -235,7 +237,9 @@ const filters: Record<string, Record<string, string[]>> = {
portfolio: {
AllowIdentityToCreatePortfolios: [],
FundsMovedBetweenPortfolios: ['handleFundsMovedBetweenPortfolios'],
FungibleTokensMovedBetweenPortfolios: ['handleFungibleTokensMovedBetweenPortfolios'],
MovedBetweenPortfolios: ['handlePortfolioMovement'],
NFTsMovedBetweenPortfolios: ['handleNftsMovedBetweenPortfolios'],
PortfolioCreated: ['handlePortfolioCreated'],
PortfolioCustodianChanged: ['handlePortfolioCustodianChanged'],
PortfolioDeleted: ['handlePortfolioDeleted'],
Expand Down
180 changes: 148 additions & 32 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2357,35 +2357,6 @@ type Portfolio @entity {
createdEvent: Event!
}

enum PortfolioMovementTypeEnum {
Fungible
NonFungible
}

"""
Represents a transfer of fungible assets between a single identities portfolios.

Note: transfers between identities are known as `Settlements`
"""
type PortfolioMovement @entity {
id: ID!
from: Portfolio @index(unique: false)
fromAccount: String @index(unique: false)
to: Portfolio @index(unique: false)
toAccount: String @index(unique: false)
identity: Identity @index(unique: false)
type: PortfolioMovementTypeEnum!
asset: Asset!
"the number of fungible tokens transferred. Defined if type == Fungible"
amount: BigInt
"the IDs of non-fungible tokens transferred. Defined if type == NonFungible"
nftIds: [BigInt]
memo: String
address: String!
createdBlock: Block!
updatedBlock: Block!
}

"""
Represents a place to trade assets. This allows for additional mediation and control over the exchange of Assets

Expand Down Expand Up @@ -2913,7 +2884,71 @@ type NftHolder @entity {
id: ID! #assetId/did
identity: Identity! @index(unique: false)
asset: Asset! @index(unique: false)
nftIds: [Int]
nftIds: [BigInt]
createdBlock: Block!
updatedBlock: Block!
}

"""
Discriminates the grain a `Holding` is keyed at: a portfolio (identity model) or a v8
account-level holder.
"""
enum HolderKind {
Portfolio
Account
}

"""
One key/value pair of an NFT's on-chain metadata.
"""
type NftMetadataEntry @jsonField {
key: String!
value: String!
}

"""
One row per individual non-fungible token — the authoritative per-token record (defects G9, G10).
The `NftHolder` rollup is kept alongside for the SDK, but provenance ("who owns NFT #42?", "where
has it been?") is answered here.
"""
type Nft @entity @compositeIndexes(fields: [["asset", "nftId"]]) {
id: ID! # assetId/padId(nftId)
"the collection this token belongs to"
asset: Asset!
"BigInt everywhere — resolves G10"
nftId: BigInt!
"current location; exactly one of portfolio / account is set while the token is in circulation"
portfolio: Portfolio
account: Account
identity: Identity
metadata: [NftMetadataEntry]
mintedBlock: Block!
"null = still in circulation. Filter `burnedBlockId: { isNull: true }` — Boolean cannot be indexed"
burnedBlock: Block
updatedBlock: Block!
}

"""
One row per (asset, holder) at the finest grain the chain uses — a portfolio, or a v8
account-level holder. Identity-level holdings are derived from these (see `AssetHolder` /
`NftHolder`, kept as maintained rollups), never stored as primary truth.
"""
type Holding
@entity
@compositeIndexes(fields: [["asset", "identity"], ["portfolio", "asset"]]) {
id: ID! # assetId/portfolioId or assetId/address
asset: Asset!
holderKind: HolderKind!
"set when `holderKind` is `Portfolio`"
portfolio: Portfolio
"set when `holderKind` is `Account` (v8 account-level holder)"
account: Account
"denormalised — makes the identity rollup a groupBy, not a join. Null when the holder account has no known identity"
identity: Identity
"fungible balance held"
amount: BigInt!
"count of non-fungible tokens held; the ids live on `Nft`"
nftCount: Int!
createdBlock: Block!
updatedBlock: Block!
}
Expand Down Expand Up @@ -2948,19 +2983,24 @@ type AssetDocument @entity {
Represents a tokenized Asset on the Polymesh chain. The central data structure for the ecosystem
"""
type Asset @entity {
id: ID! # ticker
id: ID! # assetId
"the chain's own asset id — equal to `id`; on 7.x+ the on-chain AssetId, the blake2-hashed legacy ticker before that"
assetId: String! @index(unique: true)
ticker: String @index(unique: false)
name: String @index(unique: false)
isNftCollection: Boolean!
type: String @index(unique: false)
fundingRound: String @index(unique: false)
isDivisible: Boolean!
isFrozen: Boolean!
isUniquenessRequired: Boolean!
"number of holders with a non-zero balance — maintained where `Holding` rows cross zero"
holderCount: Int!
identifiers: [SecurityIdentifier]!
owner: Identity!
documents: [AssetDocument]! @derivedFrom(field: "asset")
holders: [AssetHolder]! @derivedFrom(field: "asset")
holdings: [Holding!]! @derivedFrom(field: "asset")
nfts: [Nft!]! @derivedFrom(field: "asset")
totalSupply: BigInt!
totalTransfers: BigInt!
isCompliancePaused: Boolean!
Expand Down Expand Up @@ -3006,12 +3046,88 @@ type AssetTransaction @entity {
instruction: Instruction
"`instructionMemo` will only be present for the cases where memo is provided for the instruction which got executed"
instructionMemo: String
"true when both sides belong to one Identity (a portfolio movement, or a settlement same-DID fast path); null for issuance and redemption. Not indexed — filter by plain equality"
isInternalTransfer: Boolean
"note attached to a portfolio movement or a settlement.transferFunds call"
memo: String
"the account that signed the extrinsic, for movements initiated directly by an account"
address: String
datetime: Date!
createdBlock: Block!
updatedBlock: Block!
createdEvent: Event!
}

"""
The scope a metadata key is registered in: `Local` to one asset, or `Global` across the chain.
"""
enum MetadataScope {
Local
Global
}

"""
A metadata key registered against an asset, with its current value (defect G13). Global keys
applied to an asset also land here; the chain-wide global key registry is `GlobalMetadataKey`.
"""
type AssetMetadata @entity {
id: ID! # assetId/scope/keyId
asset: Asset! @index(unique: false)
scope: MetadataScope!
"the numeric key id within its scope, as a string"
keyId: String!
"registered name of the key"
name: String
"current value, once one has been set"
value: String
"true while the value is Locked or LockedUntil"
isLocked: Boolean!
"value expiry, when the detail carried one"
expiry: Date
createdBlock: Block!
updatedBlock: Block!
}

"""
A chain-wide global metadata key registration (name and id). Asset-scoped applications of a
global key are `AssetMetadata` rows.
"""
type GlobalMetadataKey @entity {
id: ID! # global key id
name: String! @index(unique: false)
"JSON of the metadata spec (url / description / typeDef), as last registered or updated"
spec: String
createdBlock: Block!
updatedBlock: Block!
}

"""
A registered custom asset type — the `u32` id the chain assigns and the human name.
"""
type CustomAssetType @entity {
id: ID! # custom type id
name: String!
createdBlock: Block!
updatedBlock: Block!
}

"""
An ERC20-style spending allowance one account has granted another over an asset (defect G11).
v8 account-level layer — keyed by `AccountId`, outside the identity/portfolio model.
"""
type AssetAllowance @entity {
id: ID! # assetId/owner/spender
asset: Asset! @index(unique: false)
owner: Account! @index(unique: false)
spender: Account! @index(unique: false)
"remaining allowance — taken from the chain's own computed value, never derived by subtraction"
amount: BigInt!
"lifetime total spent against this allowance"
totalSpent: BigInt!
createdBlock: Block!
updatedBlock: Block!
}

"""
Represents an investment into an Asset
"""
Expand Down
33 changes: 32 additions & 1 deletion src/decode/shapes/asset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LAST_V5, V6 } from './consts';
import { LAST_V5, V6, V8 } from './consts';
import { discontinuedAt, registerShape, stable } from './registry';

/**
Expand Down Expand Up @@ -87,6 +87,37 @@ registerShape(
discontinuedAt(LAST_V5, ['did', 'assetId', 'beneficiaryDid', 'amount'])
);

// v8 account-level asset layer (defect G11). AccountId-keyed, so no `did` parameter.
registerShape('asset', 'Approval', [
{ from: V8, fields: ['owner', 'spender', 'assetId', 'amount'] },
]);
registerShape('asset', 'AllowanceSpent', [
{ from: V8, fields: ['owner', 'spender', 'assetId', 'amountSpent', 'remainingAllowance'] },
]);

// the v8 account-side transfer path — `pendingTransferId` links to an existing Instruction
registerShape('asset', 'CreatedAssetTransfer', [
{ from: V8, fields: ['assetId', 'from', 'to', 'amount', 'memo', 'pendingTransferId'] },
]);

// asset metadata (defect G13) and asset-type events. Arity has been stable across the tags
// walked in docs/reference/event-shape-verification.md; only `Ticker → AssetId` at 7.x, which
// `getAssetId` already absorbs.
registerShape('asset', 'SetAssetMetadataValue', stable(['did', 'assetId', 'value', 'detail']));
registerShape('asset', 'SetAssetMetadataValueDetails', stable(['did', 'assetId', 'detail']));
registerShape(
'asset',
'RegisterAssetMetadataLocalType',
stable(['did', 'assetId', 'name', 'localKeyId', 'spec'])
);
registerShape('asset', 'RegisterAssetMetadataGlobalType', stable(['name', 'globalKeyId', 'spec']));
registerShape('asset', 'LocalMetadataKeyDeleted', stable(['did', 'assetId', 'localKeyId']));
registerShape('asset', 'MetadataValueDeleted', stable(['did', 'assetId', 'key']));
registerShape('asset', 'GlobalMetadataSpecUpdated', stable(['name', 'spec']));
registerShape('asset', 'AssetTypeChanged', stable(['did', 'assetId', 'assetType']));
registerShape('asset', 'CustomAssetTypeExists', stable(['did', 'typeId', 'name']));
registerShape('asset', 'CustomAssetTypeRegistered', stable(['did', 'typeId', 'name']));

registerShape('asset', 'TickerRegistered', stable(['did', 'ticker', 'expiry']));
// Deprecated at 6.0.0
registerShape('asset', 'ClassicTickerClaimed', discontinuedAt(LAST_V5, ['did', 'ticker']));
Expand Down
1 change: 1 addition & 0 deletions src/decode/shapes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import './asset';
import './balances';
import './externalAgents';
import './identity';
import './portfolio';
import './settlement';
import './staking';

Expand Down
23 changes: 23 additions & 0 deletions src/decode/shapes/portfolio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LAST_V5 } from './consts';
import { discontinuedAt, registerShape } from './registry';

/**
* `portfolio` pallet parameter shapes.
*
* `FungibleTokensMovedBetweenPortfolios` and `NFTsMovedBetweenPortfolios` were declared and
* emitted only at v5.4.3, in `unchecked_move_funds`, and removed at v6.0.0 (defect A8). They are
* exclusive branches of a `match` and `MovedBetweenPortfolios` is not emitted alongside them, so
* a v5-era movement routed through `unchecked_move_funds` is otherwise absent from the index.
* Different arities (6 vs 5), so each needs its own entry.
*/
registerShape(
'portfolio',
'FungibleTokensMovedBetweenPortfolios',
discontinuedAt(LAST_V5, ['did', 'fromPortfolio', 'toPortfolio', 'ticker', 'amount', 'memo'])
);

registerShape(
'portfolio',
'NFTsMovedBetweenPortfolios',
discontinuedAt(LAST_V5, ['did', 'fromPortfolio', 'toPortfolio', 'nfts', 'memo'])
);
Loading
Loading