feat: add CIP-113 programmable token support#68
Open
matiwinnetou wants to merge 72 commits intodevelopfrom
Open
feat: add CIP-113 programmable token support#68matiwinnetou wants to merge 72 commits intodevelopfrom
matiwinnetou wants to merge 72 commits intodevelopfrom
Conversation
...-test/src/test/java/org/cardanofoundation/tokenmetadata/registry/it/Cip113IntegrationIT.java
Fixed
Show fixed
Hide fixed
...-test/src/test/java/org/cardanofoundation/tokenmetadata/registry/it/Cip113IntegrationIT.java
Fixed
Show fixed
Hide fixed
...-test/src/test/java/org/cardanofoundation/tokenmetadata/registry/it/Cip113IntegrationIT.java
Fixed
Show fixed
Hide fixed
575a82b to
3445684
Compare
754d81c to
90572c7
Compare
...ration-test/src/test/java/org/cardanofoundation/tokenmetadata/registry/it/OpenApiDocsIT.java
Fixed
Show fixed
Hide fixed
Add on-chain indexing and V2 API enrichment for CIP-113 programmable tokens. CIP-113 tokens are standard CIP-26/CIP-68 tokens registered in an on-chain programmable registry with transfer validation logic — effectively tokens in a "smart contract jail" for regulatory compliance, freeze/seize, or custom transfer rules. ## On-chain indexing - Cip113EventListener listens to Yaci Store AddressUtxoEvent, filters for registry node NFTs matching configured policy IDs, parses inline datums via Cip113RegistryNodeParser - Append-only cip113_registry_node table with composite PK (policy_id, slot, tx_hash) and DISTINCT ON query for latest state - Supports multiple protocol versions via comma-separated policy IDs ## V2 API — generic extensions map - New Extension marker interface with Map<String, Extension> on Subject - CIP-113 data exposed under extensions.cip113 with transfer_logic_script, third_party_transfer_logic_script, and global_state_policy_id - Extensible — future CIPs add their own key without changing Subject - Backward compatible — extensions omitted for non-programmable tokens - OpenAPI @Schema annotations with descriptions, examples, and required/nullable for all fields ## Configuration - cip113.enabled (env: CIP113_ENABLED, default: false) - cip113.registry.nft.policy-ids (env: CIP113_REGISTRY_NFT_POLICY_IDS) - Disabled by default, no impact on existing CIP-26/CIP-68 flows ## Tests - Unit: parser (6), service (5), V2 controller (3 new) - Integration: registry node mint + indexing on yaci devnet, combined CIP-68 + CIP-113 V2 response verification, Prometheus metric polling - CI enabled with CIP113_ENABLED=true for integration tests ## Version bump: 1.5.0 → 1.6.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verify ProgrammableTokenCip113 schema with its three fields, Subject extensions map, and Extension base schema are documented. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace var usage in OpenApiDocsIT with explicit Iterator/Map.Entry types. Add explicit-types-only rule to CLAUDE.md coding conventions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactor Cip113EventListenerTest, Cip113RegistryNodeParserTest, and Cip113RegistryServiceTest to use @nested grouping and AssertJ assertions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Springdoc cannot generate ProgrammableTokenCip113 as a standalone schema without @JsonSubTypes on Extension. Replace field-scanning assertion with a check that Subject.extensions is a map type with additionalProperties. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add @Schema(oneOf) to Extension interface so springdoc generates the ProgrammableTokenCip113 schema with its three fields. This makes the CIP-113 extension visible in Scalar and other OpenAPI frontends. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Annotate V2ApiController.buildExtensions with jakarta.annotation.Nullable since it returns null when no extensions are found. Add nullable-return convention to CLAUDE.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix testing strategy section to distinguish controller tests (@WebMvcTest) from end-to-end integration tests (RestTemplate in integration-test/). Add integration-test module to architecture overview. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…etails Rewrite CLAUDE.md to reflect the full multi-standard registry: - CIP-26 offchain, CIP-68 on-chain, CIP-113 programmable tokens - V2 query priority mechanism and metadata merging - Yaci Store integration and on-chain indexing - All operational endpoints (startup/liveness/readiness probes, prometheus, metrics, admin-ui) with health indicator groups - Mark /health as deprecated - Configuration table with key environment variables - Coding conventions: no var, @nullable, records over tuples Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
a89cb04 to
9120bc1
Compare
FLDT is not a CIP-113 programmable token on mainnet. Update comments to make clear the CIP-113 extension is mocked for testing purposes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove @nullable from transferLogicScript and thirdPartyTransferLogicScript in ProgrammableTokenCip113 since they are required per CIP-113 spec. Add validation in Cip113RegistryNodeParser to return Optional.empty() when either required script cannot be extracted from the datum. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ct pattern) Collections should never be null — return empty containers and let @JsonInclude(NON_EMPTY) handle serialization omission. Also harden Prometheus metric parsing in CIP-113 integration test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-113 IT
Use DocumentContext.read("$.path") instead of manual JsonNode.get()
chains for cleaner, more readable assertions. No new dependencies —
com.jayway.jsonpath is already provided by spring-boot-starter-test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… IDs CIP-113 is now enabled when CIP113_REGISTRY_NFT_POLICY_IDS is non-empty, eliminating the redundant boolean flag. The enabled field was always checked alongside the policy ID set, making it unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e tokens) ADR-015 documents the V2 API extensions model — why orthogonal CIP data needs a separate response section from the metadata merge model, and how the Extension interface + map-based field enables future CIPs. ADR-016 documents CIP-113 specifically — on-chain indexing, CBOR parsing, implicit enablement, and batch optimization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
skipsWhenDisabled and skipsWhenNoPolicyIdsConfigured became identical after removing the CIP113_ENABLED flag. Kept the descriptive name. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…onIT Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve merge conflict in OpenApiDocsIT by converting CIP-113 test section from Jackson to JsonPath, consistent with the develop refactor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Path reads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move metadata_reference_nft indices out of V3 (CIP-113) into V4__performance_improvements.sql since they are not CIP-113 specific. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bare `from conftest import` fails when pytest is invoked from the repo root. Relative imports work regardless of working directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nemo83
approved these changes
Apr 2, 2026
Contributor
nemo83
left a comment
There was a problem hiding this comment.
LGTM, just a question about a db field
api/src/main/resources/db/migration/postgresql/V3__adding_support_for_cip113.sql
Outdated
Show resolved
Hide resolved
api/src/main/resources/db/migration/postgresql/V3__adding_support_for_cip113.sql
Outdated
Show resolved
Hide resolved
# Conflicts: # README.md
- Add BatchPrefetchData record to wrap 3 bulk-fetched maps (CIP-26, CIP-68, CIP-113) issued upfront in prefetchBatch(), replacing per-subject DB queries with O(1) map lookups - Add findLatestByPolicyIds() to MetadataReferenceNftRepository using ROW_NUMBER() window function for batch CIP-68 reference NFT retrieval - Replace PostgreSQL-specific DISTINCT ON in Cip113RegistryNodeRepository with ROW_NUMBER() window function for portability (PostgreSQL, H2, MySQL) - Add batch-aware findSubject() overload to Cip68FungibleTokenService that reads from pre-fetched map instead of hitting DB per subject Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…migration Add SQL comment explaining policy_id is the 'key' field from the registry node datum. Remove idx_cip113_registry_node_policy_slot index since the PK (policy_id, slot, tx_hash) supports backward scans for the same query pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
chore: prepare 1.5.0 release
Resolved conflict in pom.xml by dropping the orphaned <revision>1.6.0</revision>
property, since origin/main removed the ${revision} mechanism and hard-coded
1.4.4 across all poms to fix release-please.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reorder cip68_tokens.json so entries with larger (newer) slot values appear first, tiebroken by subject for stable ordering. Update generate_fixtures.py to match so future regenerations preserve the order. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Align SQL column types with the explicit spec limits and add matching @column(length=...) annotations to the JPA entities (previously none). Bounds now bit-compatible with yaci-store/extensions/assets-ext, which this module is intended to eventually replace. CIP-26 metadata / logo — all bounds match the cf-tokens-cip26 validator constants enforced in MetadataValidationRules: - subject: varchar(255) → varchar(120) - policy: text → varchar(120) - name: varchar(255) → varchar(50) - ticker: varchar(32) → varchar(9) - url: varchar(255) → varchar(250) - description: text → varchar(500) CIP-68 metadata_reference_nft — on-chain columns bounded by Cardano ledger rules; datum-sourced strings bounded defensively: - policy_id: text → varchar(56) - asset_name: text → varchar(64) - name: text → varchar(255) - ticker: text → varchar(32) - url: text → varchar(250) - description, logo, datum: kept as text CIP-113 cip113_registry_node — tightened based on Aiken linked-list conventions confirmed via the cip113-programmable-tokens spec: - policy_id, next_key: text → varchar(64) (head sentinel '', real 28-byte policy = 56 hex, tail sentinel ≤ 32 bytes = 64 hex) - tx_hash: text → varchar(64) - transfer_logic_script, third_party_transfer_logic_script, global_state_policy_id: text → varchar(56) All inline comments explain the spec source of each bound. No new migration file — V0/V1/V3 are edited in place (consistent with yaci-store; re-ingest from genesis per current rollout plan). Indexes, the textsearch tsvector column, FK logo→metadata, and wallet_scam_lookup are untouched. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit tightened the CIP-113 schema columns from text to varchar(64)/varchar(56). Without matching parser-level validation, a malicious inline datum with long byte strings would pass the parser and fail the DB insert, rolling back the Cip113EventListener transaction and wedging sync on retry. This commit closes the gap by porting the hardened parser from yaci-store/extensions/assets-ext. Invariants now enforced (identical to yaci-store): 1. Root must be ConstrPlutusData with alternative 0. 2. Exactly 5 fields. 3. key: ByteString, 0..32 bytes (head sentinel, real 28-byte policy, or 32-byte tail sentinel). 4. next: ByteString, 1..32 bytes (non-empty). 5. transfer_logic_script / third_party_transfer_logic_script: Aiken Credential Constr (alt 0 VerificationKey or 1 Script) wrapping exactly 28 bytes. Absent-credential tolerance preserved — BytesPlutusData(h'') or Credential wrapping empty bytes → null. 6. global_state_cs: ByteString of exactly 0 or 28 bytes. Catch clause narrowed from catch(Exception) to catch(CborDeserializationException | RuntimeException) so Error instances propagate past the parser (caller is responsible for the batch boundary). Other changes: - ParsedRegistryNode gains isHeadSentinel(). - Cip113RegistryNodeParserTest rewritten: 36 test cases covering every invariant above, plus positive cases for head sentinel, tail sentinel, all-three-optional-absent, and both Credential variants. - Cip113EventListenerTest: TRANSFER_LOGIC and THIRD_PARTY_LOGIC constants were previously 58 hex chars (29 bytes) — latent bug masked by the old lenient parser. Fixed to 56 hex (28 bytes). savesEntityWithNullTransferLogicScript and buildRegistryNodeDatumNullThirdParty helper extended from 4 fields to 5 (the new parser requires all 5). Kept in lockstep with yaci-store/extensions/assets-ext Cip113RegistryNodeParser. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a hard 4096-hex-char size cap at Cip113RegistryNodeParser.parse entry, before HexUtil and PlutusData.deserialize are ever called. This is the only parser-layer defence against the library-layer DoS classes: - deeply nested Constr → StackOverflowError - CBOR pre-allocation bomb → OutOfMemoryError Both manifest as Error (not RuntimeException) inside the recursive CBOR decoder and would propagate past the parser's catch clause, killing the event-listener thread. A real CIP-113 RegistryNode encodes to roughly 200-400 bytes (≤ 800 hex chars), so 4096 gives ~10× safety margin. 4096 bytes into the decoder physically can't contain enough nesting to blow a reasonable JVM stack or trigger the pre-allocation bomb. Also adds rejectsHexExceedingMaxSize test proving the size cap fires before HexUtil (uses valid hex characters to rule out hex-decode rejection). Brings MalformedInputs nested class to 5 tests, total parser test count to 37. Kept in lockstep with yaci-store/extensions/assets-ext. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Javadoc enforces heading sequence starting at <h2> (since the implicit <h1> is the class name). Changed the three section headers (Absent credential encoding / Byzantine safety / Non-enforced invariant) to <h2> to keep in lockstep with yaci-store's Cip113RegistryNodeParser and to avoid the same javadoc task failure if strict doclint is enabled here. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Fixes #67
Adds CIP-113 programmable token support to the token metadata registry. CIP-113 defines an on-chain registry where tokens are registered with transfer validation logic — enabling regulatory compliance, freeze/seize capabilities, and custom transfer rules for Cardano native assets.
V2 Subject Response Changes
Token type field (additive)
A new
typefield classifies tokens asNATIVE(standard) orPROGRAMMABLE(has on-chain transfer logic):{ "subject": { "subject": "577f0b...0014df10464c4454", "type": "PROGRAMMABLE", "metadata": { "name": { "value": "FLDT", "source": "CIP_68" }, "description": { "value": "A regulated stablecoin", "source": "CIP_68" } }, "extensions": { "cip113": { "transfer_logic_script": "aaa513b0fcc01d635f8535d49f38acc33d4d6b62ee8732ca6e126102", "third_party_transfer_logic_script": "def513b0fcc01d635f8535d49f38acc33d4d6b62ee8732ca6e126103", "global_state_policy_id": null } } }, "queryPriority": ["CIP_68", "CIP_26"] }For non-programmable tokens,
typeisNATIVEandextensionsis omitted — fully backward compatible.Implementation
On-chain indexing
Cip113EventListener— filters for registry node NFTs, parses inline CBOR datumsCip113RegistryNodeParser— parses CIP-113 RegistryNode PlutusData;third_party_transfer_logic_scriptandglobal_state_policy_idare nullable (per CIP-113 spec, not all substandards require them)Cip113RegistryNodeentity — JPA entity with composite PK, append-only logCip113RegistryService— single and batch lookups for CIP-113 dataCustomUtxoStorage— persists CIP-113 registry node UTxOs alongside CIP-68Extensions model (ADR-015)
extensions: Map<String, Extension>field on V2SubjectresponseExtensionmarker interface —ProgrammableTokenCip113is the first implementationTokenTypeenum (NATIVE/PROGRAMMABLE) derived from extensions presenceCIP-113 fields
transfer_logic_scriptthird_party_transfer_logic_scriptglobal_state_policy_idConfiguration
CIP-113 is implicitly enabled when
CIP113_REGISTRY_NFT_POLICY_IDScontains at least one policy ID.Preview network
.env.previewadded for testing against Cardano preview testnet where CIP-113 tokens are deployed. Registry Mint policy ID:49fb5fddb1b04ca8d2e2edd6f17a4ef00c726d69e4cd98c4d0c89d85.ADRs
Tests
Unit tests (139 total):
Cip113RegistryServiceTest— 10 tests (find, batch, disabled, containsRegistryNode)Cip113RegistryNodeParserTest— 7 tests (full datum, sentinel, no global state, null third party, invalid inputs)Cip113EventListenerTest— 5 tests (save, skip disabled/non-matching/no-datum/invalid)MetadataApiV2ControllerTest— 14 tests (type=NATIVE/PROGRAMMABLE, extensions, batch, priority)Integration tests:
Cip113IntegrationIT— end-to-end on yaci devnet: mints registry nodes, verifies metrics, tests combined CIP-68+CIP-113 subject response with type=PROGRAMMABLEOpenApiDocsIT— verifies type field and extensions in OpenAPI schemaTest plan
mvn clean verify— all 139 unit tests pass.env.previewfor CIP-113 token testingNotes
CIP-113 is under active development (CIP PR #444) and has not been finalized. This implementation may need to evolve as the specification matures.