diff --git a/src/index.ts b/src/index.ts index 35882ad..8728bd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,4 @@ export * from "./payment-splitter"; export * from "./plutus-nft"; export * from "./swap"; export * from "./vesting"; +export * from "./programmable-tokens"; diff --git a/src/programmable-tokens/aiken-workspace/.gitignore b/src/programmable-tokens/aiken-workspace-standard/.gitignore similarity index 100% rename from src/programmable-tokens/aiken-workspace/.gitignore rename to src/programmable-tokens/aiken-workspace-standard/.gitignore diff --git a/src/programmable-tokens/aiken-workspace-standard/README.md b/src/programmable-tokens/aiken-workspace-standard/README.md new file mode 100644 index 0000000..9413746 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/README.md @@ -0,0 +1,313 @@ +# Programmable Tokens - Aiken Implementation + +![Aiken](https://img.shields.io/badge/Aiken-v1.0.29-blue) +![CIP-113](https://img.shields.io/badge/CIP--113-Adapted-green) +![Status](https://img.shields.io/badge/Status-R&D-yellow) + +**Smart contracts for CIP-113 programmable tokens on Cardano, written in Aiken.** + +## Overview + +This repository contains a complete Aiken implementation of CIP-113 programmable tokens — native Cardano assets enhanced with programmable transfer rules and lifecycle controls. + +**CIP-113** is the overarching standard that defines the core framework: the shared custody model, on-chain registry, and validation coordination. The actual rules that specific programmable tokens must obey (e.g., denylist checks, freeze-and-seize) are defined in **substandards** — pluggable rule sets that operate within the CIP-113 framework. This repository includes the core standard implementation along with example substandards. + +## What Are Programmable Tokens? + +Programmable tokens are **native Cardano assets** with an additional layer of validation logic that executes on every transfer, mint, or burn operation. They leverage Cardano's existing native token infrastructure and require no hard fork or ledger changes — all programmable logic is implemented using features already supported at the L1 level. However, because all programmable tokens are held at a shared script address (with ownership determined by stake credentials), existing wallets, explorers, and DEXes would require integration work to fully support them — for example, wallets need to resolve stake-credential-based ownership to display balances, and DEX contracts would need to account for the programmable logic validators. + +**Key principle**: All programmable tokens are locked in a shared smart contract address. Ownership is determined by stake credentials, allowing standard wallets to manage them while enabling unified validation across the entire token ecosystem. + +## Key Features + +- 🔐 **Permissioned Transfers** - Enforce custom validation rules on every token transfer +- 📋 **On-Chain Registry** - Decentralized directory of registered programmable tokens +- 🎯 **Composable Logic** - Plug-and-play transfer and minting validation scripts +- 🚫 **Freeze & Seize** - Optional issuer controls for regulatory compliance +- ⚡ **Constant-Time Lookups** - Sorted linked list registry enables O(1) token verification +- 🔗 **Native Asset Based** - Built on Cardano's native token infrastructure with no hard fork required +- 🛡️ **Multi-Layer Security** - NFT authenticity, ownership proofs, and authorization checks +- 🧩 **Extensible** - Support for denylists, allowlists, time-locks, and custom policies + +## Use Cases + +- **Stablecoins** - Fiat-backed tokens with sanctions screening and freeze capabilities +- **Tokenized Securities** - Compliance with securities regulations and transfer restrictions +- **Regulated Assets** - Any token requiring KYC/AML compliance or jurisdictional controls +- **Custom Policies** - Extensible framework for any programmable token logic + +## Quick Start + +### Prerequisites + +- [Aiken](https://aiken-lang.org/installation-instructions) v1.0.29 or higher +- [Cardano CLI](https://github.com/IntersectMBO/cardano-cli) (optional, for deployment) + +### Build + +```bash +cd src/programmable-tokens-onchain-aiken +aiken build +``` + +### Test + +```bash +aiken check +``` + +All tests should pass: +``` + Summary 57 checks, 0 failures +``` + +## Project Structure + +``` +. +├── validators/ # Smart contract validators +│ ├── programmable_logic_global.ak # Core transfer validation coordinator +│ ├── programmable_logic_base.ak # Token custody (delegates to global) +│ ├── registry_mint.ak # Registry sorted linked list management +│ ├── registry_spend.ak # Registry node UTxO guard +│ ├── issuance_mint.ak # Token minting/burning policy +│ ├── issuance_cbor_hex_mint.ak # Issuance script template reference NFT +│ └── protocol_params_mint.ak # Protocol parameters NFT (one-shot) +├── lib/ +│ ├── types.ak # Core data types +│ ├── utils.ak # Utility functions +│ └── linked_list.ak # Sorted linked list operations +└── documentation/ # Documentation +``` + +## Documentation + +📚 **Documentation is available in the [`documentation/`](./documentation/) directory:** + +- **[Introduction](./documentation/01-INTRODUCTION.md)** - Problem statement, concepts, and benefits +- **[Architecture](./documentation/02-ARCHITECTURE.md)** - System design, validator coordination, on-chain data structures, and validation flows +- **[Developing Substandards](./documentation/09-DEVELOPING-SUBSTANDARDS.md)** - Guide for implementing new substandards (issuance, transfer, and third-party logic) +- **[Integration Guides](./documentation/08-INTEGRATION-GUIDES.md)** - For wallet developers, indexers, and dApp developers + + +## Core Components + +The system is split into two layers: the **core standard** (CIP-113 framework) and **substandards** (pluggable token-specific rules). + +### Core Standard (CIP-113 Framework) + +These components form the shared infrastructure that all programmable tokens use: + +#### 1. Token Registry (On-Chain Directory) + +A sorted linked list of registered programmable tokens, implemented as on-chain UTxOs with NFT markers. Each registry entry contains the token policy ID, transfer validation script reference, issuer control script reference, and optional global state reference. The sorted structure enables O(1) membership and non-membership proofs via covering nodes. + +#### 2. Programmable Logic Base + Global Validator + +A shared spending validator (`programmable_logic_base`) holds all programmable tokens. It delegates all validation to the `programmable_logic_global` stake validator via the withdraw-zero pattern — the base runs per-input but the global runs once per-transaction, keeping costs constant regardless of input count. + +#### 3. Minting Policies + +- **Issuance Policy** (`issuance_mint`) — Parameterized per token type, handles minting/burning +- **Registry Policy** (`registry_mint`) — Manages the sorted linked list of registered tokens +- **Protocol Params Policy** (`protocol_params_mint`) — One-shot mint for global protocol parameters + +### Substandards (Pluggable Token Rules) + +Substandards define the actual rules that specific programmable tokens must obey. They are stake validators invoked via 0-ADA withdrawals, registered in the on-chain registry, and executed by the core framework on every transfer. Different tokens can use different substandards depending on their compliance requirements. + +Substandard implementations live in the [`substandards/`](../../substandards/) directory: + +- **[Dummy](../substandards/dummy/)** — Simple permissioned transfer requiring a specific credential +- **[Freeze and Seize](../substandards/freeze-and-seize/)** — Denylist-aware transfer logic, seizure/freeze operations, and on-chain denylist management for regulated stablecoins + +### Validator Reference + +**Core Standard (CIP-113 Framework)** + +| Validator | Type | Purpose | +|-----------|------|---------| +| `programmable_logic_base` | Spend | Custody of all programmable token UTxOs; delegates to global validator | +| `programmable_logic_global` | Stake (withdraw) | Core coordinator: registry lookups, transfer logic invocation, value preservation | +| `protocol_params_mint` | Mint | One-shot mint of protocol parameters NFT | +| `registry_mint` | Mint | Sorted linked list management for registered token policies | +| `registry_spend` | Spend | Guards registry node UTxOs | +| `issuance_mint` | Mint | Mints/burns programmable tokens (parameterized per token type) | +| `issuance_cbor_hex_mint` | Mint | One-shot mint of issuance script template reference NFT | + +See the [Architecture doc](./documentation/02-ARCHITECTURE.md) for detailed validator interactions and validation flows. For substandard validators, see the [`substandards/`](../../substandards/) directory. + +## Transaction Lifecycle + +```mermaid +graph LR + A[Deploy Protocol] --> B[Register Token] + B --> C[Issue Tokens] + C --> D[Transfer] + D --> D + C --> E[Burn] + + style A fill:#e1f5ff + style B fill:#fff4e1 + style C fill:#e8f5e9 + style D fill:#f3e5f5 + style E fill:#ffebee +``` + +1. **Deployment** - One-time setup of registry and protocol parameters +2. **Registration** - Register transfer logic and mint policy in registry +3. **Issuance** - Mint tokens with registered validation rules +4. **Transfer** - Transfer tokens with automatic validation +5. **Burn** - Burn tokens (requires issuer authorization) + +## How It Works + +```mermaid +graph TB + A[User Initiates Transfer] --> B{Lookup Token in Registry} + B -->|Found| C[Invoke Transfer Logic Script] + B -->|Not Found| D[Treat as Regular Native Token] + C --> E{Validation Passes?} + E -->|Yes| F[Complete Transfer] + E -->|No| G[Reject Transaction] + D --> F + + style A fill:#e3f2fd + style B fill:#fff9c4 + style C fill:#f3e5f5 + style E fill:#ffe0b2 + style F fill:#c8e6c9 + style G fill:#ffcdd2 +``` + +All programmable tokens are locked at a shared smart contract address. When a transfer occurs: + +1. Transaction spends token UTxO from programmable logic address +2. Global validator looks up token in on-chain registry +3. If registered, corresponding transfer logic script executes +4. Transfer succeeds only if all validation passes +5. Tokens return to programmable logic address with new stake credential + +## Example: Freeze & Seize Stablecoin + +The project includes a complete example of a regulated stablecoin with freeze and seize capabilities: + +- **On-chain Denylist** - Sorted linked list of sanctioned addresses +- **Transfer Validation** - Every transfer checks sender/recipient not denylisted +- **Constant-Time Checks** - O(1) verification using covering node proofs +- **Issuer Controls** - Authorized parties can freeze/seize tokens + +See the [freeze-and-seize substandard](../substandards/freeze-and-seize/) for the implementation. + +## Standards + +This implementation is based on the foundational [CIP-143 (Interoperable Programmable Tokens)](https://cips.cardano.org/cip/CIP-0143) architecture and has been adapted for [CIP-113](https://github.com/cardano-foundation/CIPs/pull/444), which supersedes CIP-143 as a more comprehensive standard for programmable tokens on Cardano. + +**Note**: CIP-113 is currently under active development. This implementation reflects the current understanding of the standard and may require updates as CIP-113 evolves. + +## Development Status + +**Current Status**: Research & Development + +This is high-quality research and development code with the following characteristics: + +- ✅ All core validators implemented with strong code quality +- ✅ Registry (directory) operations complete +- ✅ Token issuance and transfer flows working +- ✅ Freeze & seize functionality complete +- ✅ Denylist system operational +- ✅ Good test coverage (57 core tests passing; substandard tests in their own modules) +- ✅ Tested on Preview testnet (limited scope) +- ⏳ Comprehensive testing required +- ⏳ Professional security audit pending + +**Security features implemented:** +- ✅ NFT-based registry authenticity +- ✅ Ownership verification via stake credentials +- ✅ Multi-layer authorization checks +- ✅ One-shot minting policies for protocol components +- ✅ Immutable validation rules post-registration +- ✅ DDOS prevention mechanisms + +## Security Considerations + +⚠️ **Important**: This code has **not been professionally audited** and has only been briefly tested on Preview testnet. While code quality is high, it is **not production-ready**. Do not use with real assets or in production environments without: +- Comprehensive security audit by qualified professionals +- Extensive testing across multiple scenarios +- Thorough review by domain experts + +## Migration from Plutarch + +This is a complete Aiken rewrite of the original Plutarch implementation ([wsc-poc](https://github.com/input-output-hk/wsc-poc)) by Phil DiSarro and the IOG team. + +**What changed:** +- All validators rewritten in Aiken (from Plutarch/Haskell) with (mostly) equivalent on-chain logic: + - Registry proofs for mints and spends are combined in Aiken, disjoints in Plutarch; this is possible due to how Aiken counts and validate tokens in one pass. + - For the third-party action, we use a different input indices approaches: Aiken indices indicates inputs to be skipped, whereas Plutarch indicates relative positions. In particular, only programmable inputs must be specified in the Aiken's redeemer, whereas Plutarch requires all script-locked inputs to be acknowledged. Different checks then occurs on both side to ensure both approaches to be viable, but with different trade-offs. + - While the resulting checks are equivalent, the logic for constructing and validating assets in particular is significantly different. The third-party validations have been mostly rewritten from the ground-up to maximise the code-reuse with the transfer logic in order to reduce the overall script size. + - The order in which certain validations occur is also different, so execution may halt for different reasons on both implementations. + - Both implementations leverage some form of caching internally, but using different heuristic. For example, Plutarch remembers the last withdrawal checked when asserting token proofs, whereas Aiken builds an lightweight withdrawal checker from the initial withdrawal list which is then passed around various functions. +- Added explicit stake credential checks on minting outputs (`issuance_mint`) to prevent permanent token locking — the original did not enforce this +- Multi-UTxO seizure support (`ThirdPartyAct`) ported from Plutarch PR #99 +- Aiken's `Dict`/`Pairs` types replace Plutarch's `PMap` — keys are lexicographically sorted by default, which matches the registry's sorted-list requirement + +**Performance:** Comparable to Plutarch. The withdraw-zero pattern means the expensive global validator runs once per transaction regardless of language. Individual validator execution units are within ~10% of the Plutarch equivalents. + +**Not migrated:** The original Plutarch repo included off-chain transaction building in Haskell. This project uses a separate Java/Spring Boot backend and a Next.js/Mesh SDK frontend instead. + +## Contributing + +Contributions welcome! Please: + +1. Read the [documentation](./documentation/) to understand the architecture +2. Ensure all tests pass (`aiken check`) +3. Add tests for new functionality +4. Follow existing code style and patterns +5. Open an issue to discuss major changes + +## Testing + +Run the complete test suite: + +```bash +# Run all tests +aiken check + +# Run specific test file +aiken check -m validators/programmable_logic_global + +# Watch mode for development +aiken check --watch +``` + +## Related Components + +- **Off-chain (Java)**: [`programmable-tokens-offchain-java/`](../programmable-tokens-offchain-java/) - Transaction building and blockchain integration + +## Resources + +- 📖 [Aiken Language Documentation](https://aiken-lang.org/) +- 🎓 [CIP-143 Specification](https://cips.cardano.org/cip/CIP-0143) - Original standard +- 🔄 [CIP-113 Pull Request](https://github.com/cardano-foundation/CIPs/pull/444) - Current standard development +- 🔗 [Cardano Developer Portal](https://developers.cardano.org/) +- 💬 [Aiken Discord](https://discord.gg/Vc3x8N9nz2) + +## License + +This project is licensed under the Apache License 2.0 - see the [LICENSE](../../LICENSE) file for details. + +Copyright 2024 Cardano Foundation + +## Acknowledgments + +This implementation is migrated from the original Plutarch implementation developed by **Phil DiSarro** and the **IOG Team** (see [wsc-poc](https://github.com/input-output-hk/wsc-poc)). We are grateful for their foundational work on CIP-143. + +Special thanks to: +- **Phil DiSarro** and the **IOG Team** for the original Plutarch design and implementation +- The **Aiken team** for the excellent smart contract language and tooling +- The **CIP-143/CIP-113 authors and contributors** for standard development +- The **Cardano developer community** for continued support and collaboration + +--- + +**Built with ❤️ using [Aiken](https://aiken-lang.org/)** diff --git a/src/programmable-tokens/aiken-workspace-standard/aiken.lock b/src/programmable-tokens/aiken-workspace-standard/aiken.lock new file mode 100644 index 0000000..5570dcc --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/aiken.lock @@ -0,0 +1,27 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[[requirements]] +name = "aiken-lang/fuzz" +version = "main" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +requirements = [] +source = "github" + +[[packages]] +name = "aiken-lang/fuzz" +version = "main" +requirements = [] +source = "github" + +[etags] +"aiken-lang/fuzz@main" = [{ secs_since_epoch = 1775479957, nanos_since_epoch = 627298000 }, "9843473958e51725a9274b487d2d4aac0395ec1a2e30f090724fa737226bc127"] diff --git a/src/programmable-tokens/aiken-workspace/aiken.toml b/src/programmable-tokens/aiken-workspace-standard/aiken.toml similarity index 78% rename from src/programmable-tokens/aiken-workspace/aiken.toml rename to src/programmable-tokens/aiken-workspace-standard/aiken.toml index 6532927..0e1c3ab 100644 --- a/src/programmable-tokens/aiken-workspace/aiken.toml +++ b/src/programmable-tokens/aiken-workspace-standard/aiken.toml @@ -1,6 +1,6 @@ name = "iohk/programmable-tokens" version = "0.3.0" -compiler = "v1.1.17" +compiler = "v1.1.21" plutus = "v3" license = "Apache-2.0" description = "Aiken implementation of CIP-0143 programmable tokens (migrated from Plutarch)" @@ -15,4 +15,9 @@ name = "aiken-lang/stdlib" version = "v3.0.0" source = "github" +[[dependencies]] +name = "aiken-lang/fuzz" +version = "main" +source = "github" + [config] diff --git a/src/programmable-tokens/aiken-workspace-standard/build.sh b/src/programmable-tokens/aiken-workspace-standard/build.sh new file mode 100755 index 0000000..c81fdf7 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/build.sh @@ -0,0 +1,5 @@ +#!/usr/local/bin/bash + +set -x + +aiken build && cp plutus.json ../programmable-tokens-offchain-java/src/main/resources diff --git a/src/programmable-tokens/aiken-workspace-standard/env/default.ak b/src/programmable-tokens/aiken-workspace-standard/env/default.ak new file mode 100644 index 0000000..9c59ed0 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/env/default.ak @@ -0,0 +1,3 @@ +pub fn assert_no_ada_policy(value: a) -> a { + value +} diff --git a/src/programmable-tokens/aiken-workspace-standard/env/with_assertions.ak b/src/programmable-tokens/aiken-workspace-standard/env/with_assertions.ak new file mode 100644 index 0000000..1b5e039 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/env/with_assertions.ak @@ -0,0 +1,9 @@ +use aiken/collection/pairs +use cardano/assets.{PolicyId, ada_policy_id} + +/// Enforces that the given value has no ada. +pub fn assert_no_ada_policy(value: Pairs) -> Pairs { + trace @"assert_no_ada_policy" + expect None = pairs.get_first(value, ada_policy_id) + value +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/assets.ak b/src/programmable-tokens/aiken-workspace-standard/lib/assets.ak new file mode 100644 index 0000000..225c1b9 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/assets.ak @@ -0,0 +1,156 @@ +use aiken/builtin.{less_than_bytearray} +use aiken/collection/dict +use cardano/assets.{PolicyId, Value} +use cardano/transaction.{Output} +use env +use list +use tokens.{Tokens} + +pub type Assets = + Pairs + +pub type SumStrategy = + fn(i, fn(Output) -> Assets, fn() -> Assets) -> Assets + +/// A Convenient helper to extract assets from values. Comes at no cost since it is usually inlined by the compiler. +pub fn from_value(self: Value) -> Assets { + self |> assets.to_dict |> dict.to_pairs +} + +/// Split an dictionnary at the given key. Returning the elements before (in reverse key order), the +/// value at the key, and the elements after (in same key order). +pub fn split_at( + self: Value, + at: PolicyId, + return: fn(Assets, Tokens, Assets) -> result, +) -> result { + do_split_at(from_value(self), at, [], return) +} + +fn do_split_at( + self: Assets, + at: PolicyId, + before: Assets, + return: fn(Assets, Tokens, Assets) -> result, +) -> result { + when self is { + [] -> return(before, dict.empty, []) + [head, ..tail] -> { + let k = head.1st + if less_than_bytearray(k, at) { + // Skip while keys are smaller (strictly) than searched key + do_split_at(tail, at, [head, ..before], return) + } else if k == at { + // Done searching, get the value and return the tail after + return(before, head.2nd, tail) + } else { + // The head and tail are all after the key; no need to continue searching. + return(before, dict.empty, self) + } + } + } +} + +/// Lookup the first asset policy from an output, or fails loudly if the output holds no tokens (beyond Ada). +pub fn peek_first(self: Output) -> PolicyId { + list.head(list.tail(from_value(self.value))).1st +} + +/// A faster version of assets.merge that preserves empty maps. This allows to +/// bypass the null check on value since quantities can only ever increase. +/// Importantly, it is also *necessary* to ensure that policies are retained in +/// the map for later validations even if they result in no outputs (e.g. a burn +/// fully compensate a spend). +/// +/// It also focuses on assets and completely ignores Ada. +/// +/// The function recurses over a list or input-like objects, and let the +/// caller select whether to add an output or not. +/// +/// ## Example +/// +/// ```aiken +/// collect( +/// self.inputs, +/// [], +/// fn(input, select, discard) { +/// let output = input.output +/// if output.address.payment_credential == needle { +/// select(output) +/// } else { +/// discard() +/// } +/// }, +/// ) +/// ``` +/// +/// /!\ PRE-CONDITION /!\ +/// The given zero assets is expected to not contain any Ada. This is enforced +/// when assertions are enabled (`--env with_assertions`). +/// +/// This is generally true when the initial zero is an empty list or coming +/// from the mint value. +pub fn collect(elems: List, zero: Assets, strategy: SumStrategy) -> Assets { + do_collect(elems, strategy, env.assert_no_ada_policy(zero)) +} + +fn do_collect(elems: List, strategy: SumStrategy, sum: Assets) -> Assets { + when elems is { + [] -> sum + [head, ..tail] -> + do_collect( + tail, + strategy, + strategy( + head, + // Output is selected + fn(output) { + output.value + |> from_value + // Drop ADA, guaranteed to be present in outputs. + |> list.tail + // NOTE: left-optimised union + // The `union` consumes the left argument into the right argument. So it is + // generally better to provide the smallest argument as left value. The `sum` + // will generally grow as large as outputs and in many scenario will no be much + // larger. However, it is very often smaller initially (often empty). Hence why + // we force it as first argument here. + |> union(sum, _) + }, + // Output is discarded + fn() { sum }, + ), + ) + } +} + +/// Merge two Assets by summing token quantities. +/// Used to combine validated input prog value with validated mint prog value. +pub fn union(left: Assets, right: Assets) -> Assets { + when left is { + [] -> right + // NOTE: Preserving null assets + // It is primordial here to not discard assets if even they result in an + // empty dict. This is because the 'left' assets may have negative quantities + // coming from burns. If a burn is fully cover by a spend, we must preserve + // the key in the map to ensure that validations necessary to that policy + // occur as expected. + [Pair(k, v), ..rest] -> union(rest, do_insert(right, k, v)) + } +} + +fn do_insert(self: Assets, k1: PolicyId, v1: Tokens) -> Assets { + when self is { + [] -> [Pair(k1, v1)] + [Pair(k2, v2), ..rest] -> + if less_than_bytearray(k1, k2) { + [Pair(k1, v1), ..self] + } else { + if k1 == k2 { + [Pair(k1, tokens.union(v1, v2)), ..rest] + } else { + [Pair(k2, v2), ..do_insert(rest, k1, v1)] + } + } + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/assets.test.ak b/src/programmable-tokens/aiken-workspace-standard/lib/assets.test.ak new file mode 100644 index 0000000..e1fea5f --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/assets.test.ak @@ -0,0 +1,347 @@ +use aiken/collection/dict +use aiken/collection/list +use aiken/fuzz +use assets.{Assets} +use cardano/address +use cardano/assets.{AssetName, Value, ada_asset_name, ada_policy_id} as value +use cardano/fuzz as cardano +use cardano/transaction.{NoDatum, Output} + +// ------------------------------------------------------------ Assets generator + +/// A generator for assets. +fn any_assets(is_output: Bool) -> Fuzzer { + let policies <- fuzz.and_then(fuzz.list_between(cardano.policy_id(), 0, 3)) + let zero = + if is_output { + let quantity <- fuzz.map(fuzz.int_between(1, 3)) + dict.from_ascending_pairs( + [ + Pair( + ada_policy_id, + dict.from_ascending_pairs([Pair(ada_asset_name, quantity)]), + ), + ], + ) + } else { + fuzz.constant(dict.empty) + } + list.foldr( + policies, + zero, + fn(policy, step) { + let assets <- fuzz.and_then(step) + let lo = + if is_output { + 0 + } else { + 1 + } + let tokens <- fuzz.map(fuzz.list_between(any_token(is_output), lo, 3)) + dict.insert(assets, policy, dict.from_pairs(tokens)) + }, + ) + |> fuzz.map(dict.to_pairs) +} + +/// A generator for a single token pair. +fn any_token(is_output: Bool) -> Fuzzer> { + let asset_name <- fuzz.and_then(cardano.asset_name()) + let quantity <- + fuzz.map( + if is_output { + fuzz.int_between(1, 3) + } else { + fuzz.either(fuzz.int_between(-3, -1), fuzz.int_between(1, 3)) + }, + ) + Pair(asset_name, quantity) +} + +// 1. Must have ADA +// 2. Policies must be in ascending orders +test prop_assets_generator_is_output_ok(assets via any_assets(True)) { + // This gives us (2), without what 'from_pairs' crashes. + let value = dict.from_pairs(assets) + // This and below gives us (1) + expect Some(tokens) = dict.get(value, ada_policy_id) + expect [Pair(asset_name, quantity)] = dict.to_pairs(tokens) + and { + (asset_name == ada_asset_name)?, + (quantity > 0)?, + } +} + +// 1. Must never have ADA +// 2. Policies must be in ascending orders +test prop_assets_generator_is_mint_ok(assets via any_assets(False)) { + // This gives us (2), without what 'from_pairs' crashes. + let value = dict.from_pairs(assets) + // This and below gives us (1) + expect None = dict.get(value, ada_policy_id) +} + +test prop_assets_generator_is_mint_sometimes_empty( + assets via any_assets(False), +) fail once { + assets != [] +} + +test prop_assets_generator_is_output_never_empty( + assets via any_assets(True), +) fail { + assets == [] +} + +test prop_assets_generator_is_output_sometimes_ada_only( + assets via any_assets(True), +) fail once { + list.length(assets) > 1 +} + +// --------------------------------------------------------------------- collect + +const output_placeholder = + Output { + address: address.from_verification_key( + #"00000000000000000000000000000000000000000000000000000000", + ), + value: value.zero, + datum: NoDatum, + reference_script: None, + } + +fn all(o, keep, _discard) { + keep(o) +} + +test collect_many_assets() { + let out1 = + Output { + ..output_placeholder, + value: value.from_lovelace(42) + |> value.add("foo", "bar", 1), + } + + let out2 = + Output { + ..output_placeholder, + value: value.from_lovelace(1337) + |> value.add("foo", "bar", 1) + |> value.add("baz", "", 1), + } + + let out3 = + Output { + ..output_placeholder, + value: value.from_lovelace(14) + |> value.add("bar", "foo", 1), + } + + let expected = + value.zero + |> value.add("bar", "foo", 1) + |> value.add("baz", "", 1) + |> value.add("foo", "bar", 2) + |> assets.from_value + + assets.collect([out1, out2, out3], [], all) == expected +} + +test collect_mixed_mint_and_output() { + let mint = + value.from_asset("foo", "bar", 1) + |> assets.from_value + + let output_value = + Output { + ..output_placeholder, + value: value.from_lovelace(1337) + |> value.add("foo", "bar", 1), + } + + let expected = + value.from_asset("foo", "bar", 2) + |> assets.from_value + + assets.collect([output_value], mint, all) == expected +} + +test collect_and_filter() { + let other_address = + address.from_verification_key( + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ) + + let out1 = + Output { + ..output_placeholder, + value: value.from_lovelace(42) |> value.add("foo", "bar", 1), + } + + let out2 = + Output { + ..output_placeholder, + address: other_address, + value: value.from_lovelace(1337) + |> value.add("foo", "bar", 1), + } + + let out3 = + Output { + ..output_placeholder, + value: value.from_lovelace(1337) + |> value.add("foo", "bar", 1), + } + + let expected = + value.from_asset("foo", "bar", 2) + |> assets.from_value + + expected == assets.collect( + [out1, out2, out3], + [], + fn(o, keep, discard) { + if o.address == other_address { + discard() + } else { + keep(o) + } + }, + ) +} + +test collect_keeps_null_policies() { + let out = + Output { + ..output_placeholder, + value: value.from_lovelace(42) |> value.add("foo", "bar", 1), + } + + let mint = value.from_asset("foo", "bar", -1) |> assets.from_value + + let expected = [Pair("foo", dict.empty)] + + expected == assets.collect([out], mint, all) +} + +test prop_collect_never_contains_ada( + (zero, values) via fuzz.both( + any_assets(False), + fuzz.list_between(any_assets(True), 0, 3), + ), +) { + assets.collect( + list.map( + values, + fn(value) { + Output { ..output_placeholder, value: assets_to_value(value) } + }, + ), + zero, + all, + ) + |> dict.from_pairs + |> dict.get(ada_policy_id) + |> fn(ada_tokens) { ada_tokens == None } +} + +// ----------------------------------------------------------------------- union + +test prop_union_preserve_ordering( + (left, right) via fuzz.both(any_assets(False), any_assets(True)), +) { + // 'from_pairs' explodes if the keys are no longer in ascending order. + dict.from_pairs(assets.union(left, right)) != dict.empty +} + +test prop_union_is_monoid(left_or_right via any_assets(True)) { + assets.union(left_or_right, []) == assets.union([], left_or_right) +} + +test prop_union_is_symmetric( + (left, right) via fuzz.both(any_assets(False), any_assets(True)), +) { + assets.union(left, right) == assets.union(right, left) +} + +// ------------------------------------------------------------------ peek_first + +test prop_peek_first(assets via any_assets(True)) { + let value = assets_to_value(assets) + if value.without_lovelace(value) == value.zero { + fuzz.label(@"ada only (discarded)") + True + } else { + fuzz.label(@"with assets (useful tests)") + + let output = Output { ..output_placeholder, value } + + expect [head, ..] = value |> value.without_lovelace |> value.policies + + assets.peek_first(output) == head + } +} + +test peek_first_no_tokens() fail { + assets.peek_first( + Output { ..output_placeholder, value: value.from_lovelace(42) }, + ) != "" +} + +// -------------------------------------------------------------------- split_at + +test prop_split_at_reify( + (ix, assets) via fuzz.both(fuzz.int(), any_assets(True)), +) { + let value = assets_to_value(assets) + + let policies = value.policies(value) + expect Some(pivot) = list.at(policies, ix % list.length(policies)) + + let before, at, after <- assets.split_at(value, pivot) + let reified = + assets_to_value(before) + |> value.merge(value.from_asset_list([Pair(pivot, dict.to_pairs(at))])) + |> value.merge(assets_to_value(after)) + + if after == [] || before == [] { + fuzz.label(@"before or after is empty") + } else { + fuzz.label(@"neither is empty") + } + + value == reified +} + +test prop_split_at_after_still_ordered( + (ix, assets) via fuzz.both(fuzz.int(), any_assets(True)), +) { + let value = assets_to_value(assets) + + let policies = value.policies(value) + expect Some(pivot) = list.at(policies, ix % list.length(policies)) + + let _, _, after <- assets.split_at(value, pivot) + + // ensures key are still ordered + dict.from_pairs(after) == dict.empty || True +} + +// --------------------------------------------------------------------- helpers + +fn assets_to_value(self: Assets) -> Value { + list.foldl( + self, + value.zero, + fn(Pair(policy, tokens), acc) { + dict.foldl( + tokens, + acc, + fn(asset_name, quantity, acc) { + value.add(acc, policy, asset_name, quantity) + }, + ) + }, + ) +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/linked_list.ak b/src/programmable-tokens/aiken-workspace-standard/lib/linked_list.ak new file mode 100644 index 0000000..aeb301f --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/linked_list.ak @@ -0,0 +1,156 @@ +use aiken/collection/dict +use aiken/collection/list +use aiken/primitive/bytearray +use cardano/address.{Credential, Script, VerificationKey} +// Linked list validation logic for the token registry +// Migrated from SmartTokens.LinkedList.Common + +use cardano/assets.{PolicyId, Value, tokens} +use cardano/transaction.{Input, Output, Transaction} +use registry_node.{RegistryNode, empty_vkey, origin_node_tn} +use utils.{ + bytearray_lt, count_unique_tokens, expect_inline_datum, has_currency_symbol, +} + +/// Validate that a registry node output has the correct structure +/// QUESTION: should the `transfer_logic_script: Credential` and `third_party_transfer_logic_script: Credential` be empty for the origin node? +pub fn validate_directory_node_output( + output: Output, + node_cs: PolicyId, +) -> RegistryNode { + let value = output.value + let datum = expect_inline_datum(output) + + // Must have exactly 2 unique tokens (Ada + node token) + expect count_unique_tokens(value) == 2 + + // Must have exactly 1 node token from this policy + let node_tokens = tokens(value, node_cs) + expect dict.size(node_tokens) == 1 + + // Parse datum as RegistryNode + expect node: RegistryNode = datum + + // Node must be ordered: key < next + expect bytearray_lt(node.key, node.next) + + // Token name must match the key - convert dict to list for pattern matching + let token_pairs = dict.to_pairs(node_tokens) + expect [Pair(tn, qty)] = token_pairs + expect qty == 1 + expect tn == node.key || tn == origin_node_tn + + node +} + +/// Get all inputs and outputs at the node validator address +pub fn collect_node_ios( + tx: Transaction, + node_cs: PolicyId, +) -> (List, List) { + // Find inputs that have the node token + let node_inputs = + list.filter( + tx.inputs, + fn(input) { has_currency_symbol(input.output.value, node_cs) }, + ) + + // Find outputs that have the node token + let node_outputs = + list.filter( + tx.outputs, + fn(output) { has_currency_symbol(output.value, node_cs) }, + ) + + // All node outputs must be at the same address as the first node input + when node_inputs is { + [first, ..] -> { + let expected_address = first.output.address + expect + list.all( + node_outputs, + fn(output) { output.address == expected_address }, + ) + (node_inputs, node_outputs) + } + [] -> ([], node_outputs) + } +} + +/// Validate registry Init: no inputs, one empty origin node output, mint origin token +pub fn validate_directory_init( + node_inputs: List, + node_outputs: List, + mint: Value, + node_cs: PolicyId, +) -> Bool { + // Exactly one node output + expect [node_output] = node_outputs + let node = validate_directory_node_output(node_output, node_cs) + + and { + // No node inputs should be spent + list.is_empty(node_inputs)?, + // Node must be empty (origin node) + (node.key == #"")?, + (node.next == #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")?, + (node.transfer_logic_script == empty_vkey)?, + (node.third_party_transfer_logic_script == empty_vkey)?, + (node.global_state_cs == #"")?, + // Must mint exactly one origin token and no other tokens from this policy + assets.has_nft(mint, node_cs, origin_node_tn)?, + } +} + +/// Check if a registry node represents an insertion at a specific position +pub fn is_inserted_directory_node( + node: RegistryNode, + insert_key: ByteArray, + next_key: ByteArray, +) -> Bool { + and { + (node.key == insert_key)?, + (node.next == next_key)?, + // Validate credentials are properly formed + is_valid_credential(node.transfer_logic_script)?, + is_valid_credential(node.third_party_transfer_logic_script)?, + // Validate global_state_cs is valid (can be empty or 28 bytes) + (bytearray.length(node.global_state_cs) == 0 || bytearray.length( + node.global_state_cs, + ) == 28)?, + } +} + +/// Validate that a credential is properly formed +/// Allows empty credentials (for origin node) or valid 28-byte hashes +pub fn is_valid_credential(cred: Credential) -> Bool { + when cred is { + VerificationKey(hash) -> is_valid_credential_length(hash) + Script(hash) -> is_valid_credential_length(hash) + } +} + +fn is_valid_credential_length(hash: ByteArray) -> Bool { + let length = bytearray.length(hash) + length == 28 || length == 0 +} + +/// Check if a registry node represents the covering node after insertion. +/// Validates ALL datum fields are preserved (not just key/next). +/// This prevents an attacker from swapping transfer_logic_script, +/// third_party_transfer_logic_script, or global_state_cs during a +/// RegistryInsert operation. +/// Fix for NEW-1 — matches Plutarch's pisInsertedOnNode strict equality. +pub fn is_updated_directory_node( + node: RegistryNode, + original: RegistryNode, + insert_key: ByteArray, +) -> Bool { + and { + (node.key == original.key)?, + (node.next == insert_key)?, + (node.transfer_logic_script == original.transfer_logic_script)?, + (node.third_party_transfer_logic_script == original.third_party_transfer_logic_script)?, + (node.global_state_cs == original.global_state_cs)?, + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/linked_list.test.ak b/src/programmable-tokens/aiken-workspace-standard/lib/linked_list.test.ak new file mode 100644 index 0000000..7553c5c --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/linked_list.test.ak @@ -0,0 +1,200 @@ +// Unit tests for linked list validation logic +use cardano/address.{Script} +use linked_list.{is_inserted_directory_node, is_updated_directory_node} +use registry_node.{RegistryNode} +use utils.{bytearray_lt} + +// Helper to create a test address +// fn test_address() -> Address { +// Address { payment_credential: Script(#"aabbccdd"), stake_credential: None } +// } + +// ======================================================================== +// is_updated_directory_node — positive tests +// ======================================================================== + +// Test is_updated_directory_node: all fields match original, next updated to insert_key +test is_updated_directory_node_works() { + let original_key = #"aa" + let insert_key = #"bb" + + let original = + RegistryNode { + key: original_key, + next: #"cc", + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + let updated_node = + RegistryNode { + key: original_key, + next: insert_key, + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + is_updated_directory_node(updated_node, original, insert_key) +} + +// Test is_updated_directory_node negative — wrong next key +test is_updated_directory_node_negative() { + let original_key = #"aa" + let insert_key = #"bb" + let wrong_key = #"dd" + + let original = + RegistryNode { + key: original_key, + next: #"cc", + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + let node = + RegistryNode { + key: original_key, + next: wrong_key, + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + !is_updated_directory_node(node, original, insert_key) +} + +// Test is_inserted_directory_node +test is_inserted_directory_node_works() { + let insert_key = #"bb" + let next_key = #"cc" + let transfer_logic = + Script(#"11111111111111111111111111111111111111111111111111111111") + let third_party_transfer_logic = + Script(#"22222222222222222222222222222222222222222222222222222222") + let global_state = #"33333333333333333333333333333333333333333333333333333333" + + let node = + RegistryNode { + key: insert_key, + next: next_key, + transfer_logic_script: transfer_logic, + third_party_transfer_logic_script: third_party_transfer_logic, + global_state_cs: global_state, + } + + is_inserted_directory_node(node, insert_key, next_key) +} + +// Test directory node ordering +test directory_node_ordering() { + let key1 = #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c" + let key2 = #"1112131415161718191a1b1c0d0e0f0102030405060708090a0b0c0d" + let key3 = #"2122232425262728292a2b2c2d2e2f303132333435363738393a3b3c" + + // CS ordering + bytearray_lt(key1, key2) && bytearray_lt(key2, key3) +} + +// Test empty origin node keys +test origin_node_empty_keys() { + let empty = #"" + + // Origin node has empty key and next + let origin_node = + RegistryNode { + key: empty, + next: empty, + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + origin_node.key == #"" && origin_node.next == #"" +} + +// ======================================================================== +// NEW-1 FIX: is_updated_directory_node now REJECTS tampered datum fields +// +// Previously these tests PASSED (demonstrating the vulnerability). +// After the fix, is_updated_directory_node compares ALL five fields of the +// RegistryNode against the original, matching Plutarch's pisInsertedOnNode. +// Tampered nodes are now correctly rejected. +// ======================================================================== + +/// transfer_logic_script swap is now REJECTED. +/// Attacker cannot replace the original transfer logic with their own script. +test is_updated_directory_node_rejects_transfer_logic_swap() fail { + let original = + RegistryNode { + key: #"aa", + next: #"cc", + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + let tampered_node = + RegistryNode { + key: #"aa", + next: #"bb", + transfer_logic_script: Script(#"deadbeef"), + // ^^^ ATTACKER'S SCRIPT — original was Script(#"11") + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + is_updated_directory_node(tampered_node, original, #"bb") +} + +/// third_party_transfer_logic_script swap is now REJECTED. +/// Attacker cannot replace the seize/freeze logic with their own script. +test is_updated_directory_node_rejects_third_party_logic_swap() fail { + let original = + RegistryNode { + key: #"aa", + next: #"cc", + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + let tampered_node = + RegistryNode { + key: #"aa", + next: #"bb", + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"deadbeef"), + // ^^^ ATTACKER'S SCRIPT — original was Script(#"22") + global_state_cs: #"", + } + + is_updated_directory_node(tampered_node, original, #"bb") +} + +/// global_state_cs tampering is now REJECTED. +/// Attacker cannot swap the global state currency symbol during insert. +test is_updated_directory_node_rejects_global_state_swap() fail { + let original = + RegistryNode { + key: #"aa", + next: #"cc", + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"", + } + + let tampered_node = + RegistryNode { + key: #"aa", + next: #"bb", + transfer_logic_script: Script(#"11"), + third_party_transfer_logic_script: Script(#"22"), + global_state_cs: #"deaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead", + } + + // ^^^ TAMPERED — original was #"" + is_updated_directory_node(tampered_node, original, #"bb") +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/list.ak b/src/programmable-tokens/aiken-workspace-standard/lib/list.ak new file mode 100644 index 0000000..4dc2620 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/list.ak @@ -0,0 +1,31 @@ +use aiken/builtin.{head_list, tail_list} + +/// A proxy to the builtin; gets erased by the compiler automatically, but reads better in code. +pub fn head(self: List) -> a { + head_list(self) +} + +/// A proxy to the builtin; gets erased by the compiler automatically, but reads better in code. +pub fn tail(self: List) -> List { + tail_list(self) +} + +/// Check quickly if an element is an list; fails loudly otherwise. +pub fn has_or_fail(xs: List, x: a) -> Bool { + head(xs) == x || has_or_fail(tail(xs), x) +} + +/// Drop n element from a list while enforcing that any dropped element satisfies +/// a given predicate. Fails loudly otherwise. +pub fn skip_assert_or_fail( + self: List, + n: Int, + assert: fn(a) -> Bool, +) -> List { + if n > 0 { + expect assert(head(self)) + skip_assert_or_fail(tail(self), n - 1, assert) + } else { + self + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/list.test.ak b/src/programmable-tokens/aiken-workspace-standard/lib/list.test.ak new file mode 100644 index 0000000..56dac58 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/list.test.ak @@ -0,0 +1,63 @@ +use list.{has_or_fail, skip_assert_or_fail} + +// ----------------------------------------------------------------- has_or_fail + +test has_or_fail_matrix() { + let vkh1 = #"aabbccdd" + let vkh2 = #"ddeeff00" + let vkh3 = #"11223344" + and { + has_or_fail([vkh1], vkh1)?, + has_or_fail([vkh1, vkh2], vkh2)?, + has_or_fail([vkh1, vkh2, vkh3], vkh2)?, + } +} + +test has_or_fail_empty() fail { + let vkh1 = #"aabbccdd" + has_or_fail([], vkh1) +} + +test has_or_fail_absent() fail { + let vkh1 = #"aabbccdd" + let vkh2 = #"ddeeff00" + has_or_fail([vkh1], vkh2) +} + +// --------------------------------------------------------- skip_assert_or_fail + +fn is_non_negative(n) { + n >= 0 +} + +test skip_assert_or_fail_matrix() { + and { + (skip_assert_or_fail([0, 1, 2, 3], -1, is_non_negative) == [0, 1, 2, 3])?, + (skip_assert_or_fail([0, 1, 2, 3], 0, is_non_negative) == [0, 1, 2, 3])?, + (skip_assert_or_fail([0, 1, 2, 3], 1, is_non_negative) == [1, 2, 3])?, + (skip_assert_or_fail([0, 1, 2, 3], 2, is_non_negative) == [2, 3])?, + (skip_assert_or_fail([0, 1, 2, 3], 3, is_non_negative) == [3])?, + (skip_assert_or_fail([0, 1, 2, 3], 4, is_non_negative) == [])?, + (skip_assert_or_fail([0, 1, -2, -3], 2, is_non_negative) == [-2, -3])?, + } +} + +test skip_assert_or_fail_out_of_bound() fail { + skip_assert_or_fail([0, 1, 2, 3], 5, is_non_negative) == [] +} + +test skip_assert_or_fail_empty_list() fail { + skip_assert_or_fail([], 1, is_non_negative) == [] +} + +test skip_assert_or_fail_assert_fail_first() fail { + skip_assert_or_fail([-1, 2, 3], 1, is_non_negative) == [2, 3] +} + +test skip_assert_or_fail_assert_fail_last() fail { + skip_assert_or_fail([1, 2, -3], 3, is_non_negative) == [] +} + +test skip_assert_or_fail_assert_fail_middle() fail { + skip_assert_or_fail([1, -2, 3], 3, is_non_negative) == [] +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/pairs.ak b/src/programmable-tokens/aiken-workspace-standard/lib/pairs.ak new file mode 100644 index 0000000..a616554 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/pairs.ak @@ -0,0 +1,21 @@ +use list + +/// Search a specific key in an associative list, and fail if not found. +pub fn has_key_or_fail(self: List>, k: k) -> Bool { + list.head(self).1st == k || has_key_or_fail(list.tail(self), k) +} + +/// Drop elements from an associative list until a given key is encountered. Fails loudly otherwise. +/// Returns the value at that key, and the rest of the list. +pub fn pop_until( + self: Pairs, + until: fn(ByteArray) -> Bool, + return: fn(v, Pairs) -> result, +) -> result { + let head = list.head(self) + if until(head.1st) { + return(head.2nd, list.tail(self)) + } else { + pop_until(list.tail(self), until, return) + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/pairs.test.ak b/src/programmable-tokens/aiken-workspace-standard/lib/pairs.test.ak new file mode 100644 index 0000000..3b1c21d --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/pairs.test.ak @@ -0,0 +1,99 @@ +use aiken/builtin.{equals_bytearray} +use pairs + +// ------------------------------------------------------------- has_key_or_fail + +test has_key_or_fail_matrix() { + and { + pairs.has_key_or_fail([Pair("a", 1)], "a")?, + pairs.has_key_or_fail([Pair("a", 1), Pair("b", 2)], "a")?, + pairs.has_key_or_fail([Pair("a", 1), Pair("b", 2)], "b")?, + pairs.has_key_or_fail([Pair("b", 2), Pair("a", 1), Pair("c", 3)], "a")?, + pairs.has_key_or_fail([Pair("b", 2), Pair("a", 1), Pair("c", 3)], "b")?, + pairs.has_key_or_fail([Pair("b", 2), Pair("a", 1), Pair("c", 3)], "c")?, + } +} + +test has_key_or_fail_empty() fail { + pairs.has_key_or_fail([], "a") +} + +test has_key_or_fail_not_in_list_1() fail { + pairs.has_key_or_fail([Pair("a", 1)], "b") +} + +test has_key_or_fail_not_in_list_2() fail { + pairs.has_key_or_fail([Pair("a", 1), Pair("b", 2)], "") +} + +test has_key_or_fail_not_in_list_3() fail { + pairs.has_key_or_fail([Pair("b", 2), Pair("a", 1), Pair("c", 3)], "aa") +} + +// ------------------------------------------------------------------- pop_until + +test pop_until_first_and_only() { + let v, rest <- pairs.pop_until([Pair("a", 1)], equals_bytearray("a", _)) + and { + (v == 1)?, + (rest == [])?, + } +} + +test pop_until_first_and_rest() { + let + v, + rest, + <- pairs.pop_until([Pair("a", 1), Pair("b", 2)], equals_bytearray("a", _)) + and { + (v == 1)?, + (rest == [Pair("b", 2)])?, + } +} + +test pop_until_middle() { + let + v, + rest, + <- + pairs.pop_until( + [Pair("b", 1), Pair("a", 2), Pair("c", 3)], + equals_bytearray("a", _), + ) + and { + (v == 2)?, + (rest == [Pair("c", 3)])?, + } +} + +test pop_until_last() { + let + v, + rest, + <- + pairs.pop_until( + [Pair("b", 1), Pair("a", 2), Pair("c", 3)], + equals_bytearray("c", _), + ) + and { + (v == 3)?, + (rest == [])?, + } +} + +test pop_until_empty() fail { + let v, _rest <- pairs.pop_until([], equals_bytearray("a", _)) + v >= 0 +} + +test pop_until_not_found() fail { + let + v, + _rest, + <- + pairs.pop_until( + [Pair("b", 1), Pair("a", 2), Pair("c", 3)], + equals_bytearray("aa", _), + ) + v >= 0 +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/registry_node.ak b/src/programmable-tokens/aiken-workspace-standard/lib/registry_node.ak new file mode 100644 index 0000000..de22c41 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/registry_node.ak @@ -0,0 +1,93 @@ +use aiken/builtin.{unconstr_fields} +use assets +use cardano/address.{Credential, VerificationKey} +use cardano/assets.{PolicyId} as value +use cardano/transaction.{InlineDatum, Input} +use list + +/// Constants for token names +pub const origin_node_tn = #"" + +/// Empty Verification Key +pub const empty_vkey = VerificationKey(#"") + +/// Registry node in the linked list of registered programmable token policies +/// This represents an entry in the CIP-0143 registry +pub type RegistryNode { + /// The key (currency symbol) of the programmable token policy + key: ByteArray, + /// The next key in lexicographical order (for linked list traversal) + next: ByteArray, + /// The transfer logic script credential that validates token transfers + transfer_logic_script: Credential, + /// The third party transfer logic script credential that handles admin actions (seizure, etc.) + third_party_transfer_logic_script: Credential, + /// Optional global state NFT currency symbol for this policy + global_state_cs: ByteArray, +} + +pub fn new_registry_node_getter( + registry_node_cs: PolicyId, + ref_idx: Int, + reference_inputs: List, + get_registry_node: fn(Int) -> Data, +) -> fn(Int) -> Data { + when reference_inputs is { + [] -> get_registry_node + [head, ..tail] -> + new_registry_node_getter( + registry_node_cs, + ref_idx + 1, + tail, + fn(proof_idx) { + if ref_idx == proof_idx { + expect assets.peek_first(head.output) == registry_node_cs + expect InlineDatum(data) = head.output.datum + data + } else { + get_registry_node(proof_idx) + } + }, + ) + } +} + +/// Extract useful information from an inlined 'RegistryNode' datum. Note that +/// we need not to 'validate' the full shape of the datum here, because we do not +/// produce it. We're only consuming it, and its well-formedness is guaranteed by the +/// linked list contract when producing the datum. +/// +/// This even makes the contract more 'interoperable', as the linked-list datum +/// can change in addititive manner without invalidating this contract. +pub fn with_key_and_transfer_logic( + data: Data, + return: fn(ByteArray, Credential) -> result, +) -> result { + let fields = unconstr_fields(data) + expect key: ByteArray = list.head(fields) + expect transfer_logic_script: Credential = + list.head(list.tail(list.tail(fields))) + return(key, transfer_logic_script) +} + +pub fn with_key_and_next_key( + data: Data, + return: fn(ByteArray, ByteArray) -> result, +) -> result { + let fields = unconstr_fields(data) + expect key: ByteArray = list.head(fields) + let fields = list.tail(fields) + expect next: ByteArray = list.head(fields) + return(key, next) +} + +pub fn with_key_and_3rd_party_logic( + data: Data, + return: fn(ByteArray, Credential) -> result, +) -> result { + let fields = unconstr_fields(data) + expect key: ByteArray = list.head(fields) + expect third_party_transfer_logic_script: Credential = + list.head(list.tail(list.tail(list.tail(fields)))) + return(key, third_party_transfer_logic_script) +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/registry_node.test.ak b/src/programmable-tokens/aiken-workspace-standard/lib/registry_node.test.ak new file mode 100644 index 0000000..ffea392 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/registry_node.test.ak @@ -0,0 +1,130 @@ +use aiken/collection/list +use aiken/fuzz +use cardano/assets.{PolicyId, ada_asset_name, ada_policy_id} as value +use cardano/fuzz as cardano +use cardano/transaction.{InlineDatum, Input, Output} +use registry_node.{ + RegistryNode, new_registry_node_getter, with_key_and_3rd_party_logic, + with_key_and_next_key, with_key_and_transfer_logic, +} + +// ---------------------------------------------------- new_registry_node_getter + +const registry_policy: PolicyId = "registry" + +fn any_registry_node_reference_input() -> Fuzzer { + let output_reference <- fuzz.and_then(cardano.output_reference()) + let address <- fuzz.and_then(cardano.address()) + let registry_node <- fuzz.and_then(any_registry_node()) + let min_ada_value = + value.zero |> value.add(ada_policy_id, ada_asset_name, 1000000) + fuzz.constant( + Input { + output_reference, + output: Output { + address, + value: min_ada_value + |> value.add(registry_policy, "", 1), + datum: InlineDatum(as_data(registry_node)), + reference_script: None, + }, + }, + ) +} + +fn any_registry(min_nodes: Int, max_nodes: Int) -> Fuzzer<(Int, List)> { + let before <- fuzz.and_then(fuzz.list_between(cardano.input(), 0, 2)) + let between <- + fuzz.and_then( + fuzz.list_between( + any_registry_node_reference_input(), + min_nodes, + max_nodes, + ), + ) + let after <- fuzz.and_then(fuzz.list_between(cardano.input(), 0, 2)) + + let len_before = list.length(before) + + let len_between = list.length(between) + + let ix <- + fuzz.and_then(fuzz.int_between(len_before, len_before + len_between - 1)) + + fuzz.constant( + ( + ix, + before + |> list.concat(between) + |> list.concat(after), + ), + ) +} + +test prop_registry_lookup((ix, registry) via any_registry(1, 2)) { + let get_registry_node = + new_registry_node_getter(registry_policy, 0, registry, fn(_) { fail }) + expect _: RegistryNode = get_registry_node(ix) +} + +test prop_registry_lookup_fail_no_node( + (ix, registry) via any_registry(0, 0), +) fail { + let get_registry_node = + new_registry_node_getter(registry_policy, 0, registry, fn(_) { fail }) + get_registry_node(ix) != as_data(Void) +} + +test prop_registry_lookup_fail_wrong_policy( + (ix, registry) via any_registry(1, 1), +) fail { + let get_registry_node = + new_registry_node_getter("foo", 0, registry, fn(_) { fail }) + get_registry_node(ix) != as_data(Void) +} + +// -------------------------------------------------------------- with_key_and_* + +fn any_registry_node() -> Fuzzer { + let key <- fuzz.and_then(fuzz.bytearray_between(0, 32)) + let next <- fuzz.and_then(fuzz.bytearray_between(0, 32)) + let transfer_logic_script <- fuzz.and_then(cardano.credential()) + let third_party_transfer_logic_script <- fuzz.and_then(cardano.credential()) + let global_state_cs <- fuzz.and_then(cardano.policy_id()) + fuzz.constant( + RegistryNode { + key, + next, + transfer_logic_script, + third_party_transfer_logic_script, + global_state_cs, + }, + ) +} + +test prop_with_key_and_transfer_logic_roundtrip(node via any_registry_node()) { + let key, transfer_logic_script <- with_key_and_transfer_logic(node) + and { + (key == node.key)?, + (transfer_logic_script == node.transfer_logic_script)?, + } +} + +test prop_with_key_and_next_key_roundtrip(node via any_registry_node()) { + let key, next <- with_key_and_next_key(node) + and { + (key == node.key)?, + (next == node.next)?, + } +} + +test prop_with_key_and_3rd_party_roundtrip(node via any_registry_node()) { + let + key, + third_party_transfer_logic_script, + <- with_key_and_3rd_party_logic(node) + and { + (key == node.key)?, + (third_party_transfer_logic_script == node.third_party_transfer_logic_script)?, + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/tokens.ak b/src/programmable-tokens/aiken-workspace-standard/lib/tokens.ak new file mode 100644 index 0000000..b9687ab --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/tokens.ak @@ -0,0 +1,38 @@ +use aiken/builtin.{equals_bytearray} +use aiken/collection/dict.{Dict} +use aiken/collection/dict/strategy as dict_strategy +use cardano/assets.{AssetName} +use pairs.{pop_until} + +pub type Tokens = + Dict + +/// /!\ PRE-CONDITION /!\ +/// Given pairs are assumed to be well-formed and semantically 'correct'. That +/// is, keys contain no duplicates and are in ascending order. +/// +/// In practice, this means we suppose both values have been constructed from +/// existing well-formed Values. +pub fn contains( + superset: Pairs, + subset: Pairs, +) -> Bool { + when subset is { + [] -> True + [head_subset, ..tail_subset] -> { + let + head_superset, + tail_superset, + <- pop_until(superset, equals_bytearray(head_subset.1st, _)) + and { + head_superset >= head_subset.2nd, + contains(tail_superset, tail_subset), + } + } + } +} + +/// Merge two token maps by summing quantities per token name. +pub fn union(left: Tokens, right: Tokens) -> Dict { + dict.union_with(left, right, dict_strategy.sum_if_non_zero()) +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/tokens.test.ak b/src/programmable-tokens/aiken-workspace-standard/lib/tokens.test.ak new file mode 100644 index 0000000..6b79d4f --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/tokens.test.ak @@ -0,0 +1,167 @@ +use aiken/collection/dict +use aiken/collection/list +use aiken/collection/pairs +use aiken/fuzz +use aiken/primitive/bytearray +use cardano/assets.{AssetName} +use cardano/fuzz as cardano +use tokens.{Tokens} + +// -------------------------------------------------------------- fuzzers + +fn any_tokens() -> Fuzzer { + let tokens <- fuzz.map(fuzz.list_between(any_token(), 0, 3)) + dict.from_pairs(tokens) +} + +fn any_token() -> Fuzzer> { + let asset_name <- fuzz.and_then(cardano.asset_name()) + let quantity <- fuzz.map(fuzz.int_between(0, 3)) + Pair(asset_name, quantity) +} + +fn any_mutation() -> Fuzzer { + let ix <- fuzz.and_then(fuzz.byte()) + let quantity <- fuzz.and_then(fuzz.int_between(1, 3)) + let name <- fuzz.and_then(cardano.asset_name()) + fuzz.one_of( + [ + ReduceQuantity(ix, quantity), + IncreaseQuantity(ix, quantity), + InsertAsset(Pair(name, quantity)), + DeleteAsset(ix), + ], + ) +} + +type Mutation { + ReduceQuantity(Int, Int) + IncreaseQuantity(Int, Int) + InsertAsset(Pair) + DeleteAsset(Int) +} + +// Invariant: assume 'tokens' is not empty. +fn apply_mutation(mutation: Mutation, pairs: Pairs) { + let len = list.length(pairs) + when mutation is { + ReduceQuantity(ix, n) | IncreaseQuantity(ix, n) -> { + let prefix = list.take(pairs, ix % len) + expect [Pair(name, quantity), ..tail] = list.drop(pairs, ix % len) + let new_quantity = + if mutation == ReduceQuantity(ix, n) { + quantity - n + } else { + quantity + n + } + list.concat(prefix, [Pair(name, new_quantity), ..tail]) + } + InsertAsset(Pair(name, quantity)) -> + pairs.insert_with_by_ascending_key( + pairs, + name, + quantity, + bytearray.compare, + fn(left, right) { left + right }, + ) + DeleteAsset(ix) -> { + let prefix = list.take(pairs, ix % len) + expect [_, ..tail] = list.drop(pairs, ix % len) + list.concat(prefix, tail) + } + } +} + +// ------------------------------------------------------------- contains + +const fixture_asset_name: AssetName = #"746f6b656e" + +const fixture_superset: Pairs = [Pair(fixture_asset_name, 100)] + +const fixture_superset_with_ada: Pairs = + [Pair(fixture_asset_name, 100)] + +const fixture_subset: Pairs = [Pair(fixture_asset_name, 50)] + +test tokens_exact_match() { + tokens.contains(fixture_superset, fixture_superset) +} + +test tokens_subset() { + and { + tokens.contains(fixture_superset, fixture_subset)?, + tokens.contains(fixture_superset_with_ada, fixture_subset)?, + } +} + +test tokens_insufficient() { + !tokens.contains(fixture_subset, fixture_superset) +} + +test prop_contains_reflexive(tokens via any_tokens()) { + tokens.contains(dict.to_pairs(tokens), dict.to_pairs(tokens)) +} + +test prop_contains_monoid(tokens via any_tokens()) { + tokens.contains(dict.to_pairs(tokens), []) +} + +test prop_contains_subset_mutation( + (superset, mutation) via fuzz.both(any_tokens(), any_mutation()), +) fail { + let superset = dict.to_pairs(superset) + if superset != [] { + let subset = apply_mutation(mutation, superset) + when mutation is { + IncreaseQuantity(..) -> { + fuzz.label(@"non-empty superset, increase quantity") + tokens.contains(superset, subset) + } + InsertAsset(..) -> { + fuzz.label(@"non-empty superset, insert new asset") + tokens.contains(superset, subset) + } + ReduceQuantity(..) -> { + fuzz.label(@"non-empty superset, reduce quantity") + !tokens.contains(superset, subset) + } + DeleteAsset(..) -> { + fuzz.label(@"non-empty superset, remove asset") + !tokens.contains(superset, subset) + } + } + } else { + fuzz.label(@"empty superset, skipped") + False + } +} + +test prop_contains_superset_mutation( + (subset, mutation) via fuzz.both(any_tokens(), any_mutation()), +) fail { + let subset = dict.to_pairs(subset) + if subset != [] { + let superset = apply_mutation(mutation, subset) + when mutation is { + IncreaseQuantity(..) -> { + fuzz.label(@"non-empty subset, increase quantity") + !tokens.contains(superset, subset) + } + InsertAsset(..) -> { + fuzz.label(@"non-empty subset, insert new asset") + !tokens.contains(superset, subset) + } + ReduceQuantity(..) -> { + fuzz.label(@"non-empty subset, reduce quantity") + tokens.contains(superset, subset) + } + DeleteAsset(..) -> { + fuzz.label(@"non-empty subset, remove asset") + tokens.contains(superset, subset) + } + } + } else { + fuzz.label(@"empty subset, skipped") + False + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/types.ak b/src/programmable-tokens/aiken-workspace-standard/lib/types.ak new file mode 100644 index 0000000..2ec2012 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/types.ak @@ -0,0 +1,55 @@ +// Core data types for CIP-0143 programmable tokens +// Migrated from SmartTokens.Types.PTokenDirectory and SmartTokens.Types.ProtocolParams + +use cardano/address.{Credential} + +/// Token name for the issuance CBOR hex reference NFT +pub const issuance_cbor_hex_token_name = "IssuanceCborHex" + +/// Proof that a token is (or is not) in the registry +pub type RegistryProof { + /// Token exists in the registry at this reference input index + TokenExists { node_idx: Int } + /// Token does not exist (proof via covering node at this index) + TokenDoesNotExist { node_idx: Int } +} + +/// Redeemer for the global programmable logic stake validator +pub type ProgrammableLogicGlobalRedeemer { + /// Transfer action with proofs for each token type + TransferAct { proofs: List } + /// Third party action for admin operations (seizure, freeze, etc.) + /// Supports multiple UTxOs in a single transaction + ThirdPartyAct { + /// Index of the registry node in reference inputs + registry_node_idx: Int, + /// Starting index in outputs where processed outputs begin + outputs_start_idx: Int, + /// Length of ALL inputs in the transaction + length_inputs: Int, + } +} + +/// Action for the minting policy +pub type SmartTokenMintingAction { + /// The credential of the minting logic script that must be invoked + minting_logic_cred: Credential, +} + +/// Redeemer for registry minting policy (linked list operations) +pub type RegistryRedeemer { + /// Initialize the registry with the origin node + RegistryInit + /// Insert a new programmable token policy into the registry + RegistryInsert { key: ByteArray, hashed_param: ByteArray } +} + +/// Issuance CBOR hex reference data +/// This contains the prefix and postfix of the minting policy script +/// Used to validate programmable token registrations +pub type IssuanceCborHex { + /// Prefix bytes of the minting policy script + prefix_cbor_hex: ByteArray, + /// Postfix bytes of the minting policy script + postfix_cbor_hex: ByteArray, +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/utils.ak b/src/programmable-tokens/aiken-workspace-standard/lib/utils.ak new file mode 100644 index 0000000..812c0ec --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/utils.ak @@ -0,0 +1,95 @@ +//// Utility functions for CIP-0143 validators +//// Migrated from various Plutarch utility modules + +use aiken/builtin.{tail_list} +use aiken/collection/dict +use aiken/collection/list +use cardano/assets.{PolicyId, Value, flatten} +use cardano/transaction.{InlineDatum, Output} + +/// Get the datum from an output, expecting an inline datum +pub fn expect_inline_datum(output: Output) -> Data { + expect InlineDatum(d) = output.datum + d +} + +/// Check if a currency symbol is present in a value. Fail loudly if missing +pub fn has_currency_symbol(haystack: Value, needle: PolicyId) -> Bool { + // Fast skip first pair which is always ada + let haystack = haystack |> assets.to_dict |> dict.to_pairs |> tail_list + do_has_currency_symbol(haystack, needle) +} + +fn do_has_currency_symbol( + haystack: List>, + needle: PolicyId, +) -> Bool { + when haystack is { + [] -> False + [Pair(head, _), ..tail] -> + head == needle || do_has_currency_symbol(tail, needle) + } +} + +/// Count unique tokens in a value (number of distinct currency symbol + token name pairs) +pub fn count_unique_tokens(value: Value) -> Int { + assets.reduce(value, 0, fn(_policy, _name, _amount, acc) { acc + 1 }) +} + +/// Check if a bytearray is lexicographically less than another +pub fn bytearray_lt(a: ByteArray, b: ByteArray) -> Bool { + builtin.less_than_bytearray(a, b) +} + +/// Apply hashed parameter to prefix and postfix to compute policy ID +/// This reconstructs the minting policy script and hashes it to get the currency symbol +/// Follows Plutus V3 script format: version_header + prefix + serialized_param + postfix +pub fn apply_hashed_parameter( + prefix: ByteArray, + postfix: ByteArray, + hashed_param: ByteArray, +) -> ByteArray { + // Plutus V3 version header: most significant first encoding of version 3 + let version_header = #"03" + + // Build the script: version + prefix + serialized_param + postfix + let script_bytes = + builtin.append_bytearray( + version_header, + builtin.append_bytearray( + prefix, + builtin.append_bytearray(hashed_param, postfix), + ), + ) + + // Hash with blake2b_224 to get the policy ID (28 bytes) + builtin.blake2b_224(script_bytes) +} + +/// Check if a currency symbol is a valid programmable token registration +/// Validates that: +/// 1. Tokens with this policy ID are being minted in the transaction +/// 2. The computed policy ID (from prefix + hashed_param + postfix) matches the currency symbol +pub fn is_programmable_token_registration( + cs_to_insert: ByteArray, + prefix: ByteArray, + postfix: ByteArray, + hashed_param: ByteArray, + mint_value: Value, +) -> Bool { + // Check 1: The policy must be minting tokens in this transaction + let has_minting = + list.any( + flatten(mint_value), + fn(asset) { + let (policy, _tn, amt) = asset + policy == cs_to_insert && amt > 0 + }, + ) + + // Check 2: The computed policy ID must match the currency symbol being inserted + let computed_cs = apply_hashed_parameter(prefix, postfix, hashed_param) + let cs_matches = computed_cs == cs_to_insert + + has_minting? && cs_matches? +} diff --git a/src/programmable-tokens/aiken-workspace-standard/lib/utils.test.ak b/src/programmable-tokens/aiken-workspace-standard/lib/utils.test.ak new file mode 100644 index 0000000..f0788e5 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/lib/utils.test.ak @@ -0,0 +1,57 @@ +//// Unit tests for utility functions + +use cardano/assets.{ada_asset_name, ada_policy_id, add, from_asset, zero} +use utils.{bytearray_lt, count_unique_tokens, has_currency_symbol} + +// Test bytearray_lt with different bytearrays +test bytearray_lt_works() { + bytearray_lt(#"00", #"01") && bytearray_lt(#"aa", #"ab") && !bytearray_lt( + #"ff", + #"00", + ) && !bytearray_lt(#"aa", #"aa") +} + +// Test bytearray_lt with empty bytearrays +test bytearray_lt_empty() { + bytearray_lt(#"", #"01") && !bytearray_lt(#"01", #"") +} + +// Test count_unique_tokens with Ada only +test count_unique_tokens_ada_only() { + let value = from_asset(ada_policy_id, ada_asset_name, 1000000) + count_unique_tokens(value) == 1 +} + +// Test count_unique_tokens with multiple tokens +test count_unique_tokens_multiple() { + let cs1 = #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c" + let cs2 = #"1112131415161718191a1b1c0d0e0f0102030405060708090a0b0c0d" + + let value = + zero + |> add(ada_policy_id, ada_asset_name, 1000000) + |> add(cs1, #"746f6b656e31", 100) + |> add(cs2, #"746f6b656e32", 200) + + count_unique_tokens(value) == 3 +} + +// Test has_currency_symbol +test has_currency_symbol_present() { + let cs = #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c" + let value = + from_asset(cs, #"746f6b656e", 100) + |> add(ada_policy_id, ada_asset_name, 1000000) + has_currency_symbol(value, cs) +} + +// Test has_currency_symbol absent +test has_currency_symbol_absent() fail { + let cs1 = #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c" + let cs2 = #"1112131415161718191a1b1c0d0e0f0102030405060708090a0b0c0d" + let value = + from_asset(cs1, #"746f6b656e", 100) + |> add(ada_policy_id, ada_asset_name, 1000000) + + has_currency_symbol(value, cs2) +} diff --git a/src/programmable-tokens/aiken-workspace-standard/plutus.json b/src/programmable-tokens/aiken-workspace-standard/plutus.json new file mode 100644 index 0000000..226e062 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/plutus.json @@ -0,0 +1,557 @@ +{ + "preamble": { + "title": "iohk/programmable-tokens", + "description": "Aiken implementation of CIP-0143 programmable tokens (migrated from Plutarch)", + "version": "0.3.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.21+42babe5" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "always_fail.always_fail.spend", + "datum": { + "title": "_d", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "redeemer": { + "title": "_r", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "_nonce", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "585f010100229800aba2aba1aab9eaab9dab9a9bae00248888896600264653001300700198039804000cc01c0092225980099b8748008c020dd500144c8cc898c02c004c02cc030004c024dd50014590070c01c004c010dd5003c52689b2b20041", + "hash": "e9d8d9c7fc531f0b179d502c86bffee829613c537794dab053ae28fe" + }, + { + "title": "always_fail.always_fail.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "_nonce", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "585f010100229800aba2aba1aab9eaab9dab9a9bae00248888896600264653001300700198039804000cc01c0092225980099b8748008c020dd500144c8cc898c02c004c02cc030004c024dd50014590070c01c004c010dd5003c52689b2b20041", + "hash": "e9d8d9c7fc531f0b179d502c86bffee829613c537794dab053ae28fe" + }, + { + "title": "issuance_cbor_hex_mint.issuance_cbor_hex_mint.mint", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "always_fail_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "5903d30101002229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae00248888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e26464b300130100018999119912cc004c020c03cdd500144c8c96600266e1d20043011375400313259800980598091baa0018991919912cc004c06c00e2b300132330010013758603660306ea803c896600200314a115980099baf301c30193754603800204314a3133002002301d001405c80d22b30013371e6eb8c0680212210f49737375616e636543626f72486578008acc004cdc39bad301a301b0084800a266ebcc068c05cdd500398051980c99ba548008cc064dd480a25eb80cc065300103d87a80004bd704528202a8a50405514a080aa2c80c0dd7180c0009bae3018002301800130133754003164044602a60246ea80062c8080c014c044dd5000980998081baa0028b201c323300100137586006601e6ea8018896600200314c0103d87a80008992cc004cc896600200514a11329800992cc004c034c050dd5000c4dd6980a980c1bab301830153754003148001013180b800cdd7980b980c000cdd5801a444b30010018acc004c00801a26600a00690004528202a8992cc004cdd7980b800a610140008acc004cc018010dd6980c180d9bab3018001898019ba6301c0028a5040591598009980300224001130030078a50405880b0c0680050180ca60020033756602e603060286ea80124466030004660306e980052f5c08008889660020051330014c103d87a80004bd6f7b63044ca60026eb8c0580066eacc05c00660360069112cc004cdc8a441000038acc004cdc7a4410000389980298069980e1ba60024bd70000c4cc015300103d87a8000006406119800803c006446600e0046603c66ec0dd48029ba6004001401c80c0603200480ba2942294229410181ba633013337606ea401cdd31980999bb04c1104f49737375616e636543626f72486578004c010101004bd6f7b63025eb7bdb1808928c4c010cc04c0052f5c11330030033015002403c60260028088c048dd6180880191808180898088009ba5480022c8068c8cc004004c8cc004004dd59808180898089808980898069baa0042259800800c52f5c113233223322330020020012259800800c400e2646602c6e9ccc058dd48029980b18098009980b180a000a5eb80cc00c00cc060008c0580050141bab3011003375c601c00266006006602600460220028078896600200314bd7044cc896600266e3cdd71809001002c4cc044dd380119802002000c4cc01001000500d1bac3010001301100140386eb8c034c028dd5001c590080c024004c010dd5005452689b2b200401", + "hash": "7a52b5d9171439e64fa0df2d49ca7487d369e146f697553feba95426" + }, + { + "title": "issuance_cbor_hex_mint.issuance_cbor_hex_mint.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "always_fail_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "5903d30101002229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae00248888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e26464b300130100018999119912cc004c020c03cdd500144c8c96600266e1d20043011375400313259800980598091baa0018991919912cc004c06c00e2b300132330010013758603660306ea803c896600200314a115980099baf301c30193754603800204314a3133002002301d001405c80d22b30013371e6eb8c0680212210f49737375616e636543626f72486578008acc004cdc39bad301a301b0084800a266ebcc068c05cdd500398051980c99ba548008cc064dd480a25eb80cc065300103d87a80004bd704528202a8a50405514a080aa2c80c0dd7180c0009bae3018002301800130133754003164044602a60246ea80062c8080c014c044dd5000980998081baa0028b201c323300100137586006601e6ea8018896600200314c0103d87a80008992cc004cc896600200514a11329800992cc004c034c050dd5000c4dd6980a980c1bab301830153754003148001013180b800cdd7980b980c000cdd5801a444b30010018acc004c00801a26600a00690004528202a8992cc004cdd7980b800a610140008acc004cc018010dd6980c180d9bab3018001898019ba6301c0028a5040591598009980300224001130030078a50405880b0c0680050180ca60020033756602e603060286ea80124466030004660306e980052f5c08008889660020051330014c103d87a80004bd6f7b63044ca60026eb8c0580066eacc05c00660360069112cc004cdc8a441000038acc004cdc7a4410000389980298069980e1ba60024bd70000c4cc015300103d87a8000006406119800803c006446600e0046603c66ec0dd48029ba6004001401c80c0603200480ba2942294229410181ba633013337606ea401cdd31980999bb04c1104f49737375616e636543626f72486578004c010101004bd6f7b63025eb7bdb1808928c4c010cc04c0052f5c11330030033015002403c60260028088c048dd6180880191808180898088009ba5480022c8068c8cc004004c8cc004004dd59808180898089808980898069baa0042259800800c52f5c113233223322330020020012259800800c400e2646602c6e9ccc058dd48029980b18098009980b180a000a5eb80cc00c00cc060008c0580050141bab3011003375c601c00266006006602600460220028078896600200314bd7044cc896600266e3cdd71809001002c4cc044dd380119802002000c4cc01001000500d1bac3010001301100140386eb8c034c028dd5001c590080c024004c010dd5005452689b2b200401", + "hash": "7a52b5d9171439e64fa0df2d49ca7487d369e146f697553feba95426" + }, + { + "title": "issuance_mint.issuance_mint.mint", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1SmartTokenMintingAction" + } + }, + "parameters": [ + { + "title": "programmable_logic_base", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + }, + { + "title": "minting_logic_cred", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + } + ], + "compiledCode": "5903b30101002229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400130080024888966002600460106ea800e2664464b30013005300b375400f132598009808800c4c8c96600260100031323259800980a801401a2c8090dd7180980098079baa0028acc004c01400626464b300130150028034590121bae3013001300f37540051640348068c034dd50009808000c5900e18061baa0078b2014132598009808000c4c8c96600266ebcc044c038dd500480a4566002646600200264660020026eacc04cc050c050c050c050c050c050c040dd5004112cc004006297ae08998099808180a00099801001180a800a0242259800800c528456600266ebcc04c00405a294626600400460280028071011456600260086530010019bab3012301330133013301330133013301330133013300f375400f480010011112cc00400a200319800801cc05400a64b3001300b30113754602200315980099baf301200100d899b800024800a2004808220048080c05000900320248acc004c010c8cc0040040108966002003148002266e012002330020023014001404515980099b88480000062664464b3001300a30103754003159800980518081baa30143011375400315980099baf301430113754602860226ea800c062266e1e60026eacc008c044dd5001cdd7180a002c520004888cc88cc004004008c8c8cc004004014896600200300389919912cc004cdc8808801456600266e3c04400a20030064061133005005301e00440606eb8c05c004dd5980c000980d000a03014bd6f7b630112cc00400600713233225980099b910070028acc004cdc78038014400600c80ba26600a00a603a00880b8dd7180b0009bad30170013019001405c0048a50403d16403d16403c600260206ea8c04cc040dd500118089bac301130123012300e375400c46024602600315980099b880014800229462c806100c452820188a50403114a08062294100c1bad3010301100130103758601e003164034646600200264660020026eacc040c044c044c044c044c034dd5002912cc004006297ae08991991199119801001000912cc004006200713233016374e6602c6ea4014cc058c04c004cc058c0500052f5c0660060066030004602c00280a0dd598088019bae300e0013300300330130023011001403c44b30010018a5eb8226644b30013371e6eb8c04800801a2660226e9c008cc0100100062660080080028068dd618080009808800a01c375c601860126ea800cdc3a400516401c300800130033754011149a26cac80081", + "hash": "a5ed4b4e7813e17eec3b66f64dc5f2c6d11d62e7120d90a0f0acf450" + }, + { + "title": "issuance_mint.issuance_mint.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "programmable_logic_base", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + }, + { + "title": "minting_logic_cred", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + } + ], + "compiledCode": "5903b30101002229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400130080024888966002600460106ea800e2664464b30013005300b375400f132598009808800c4c8c96600260100031323259800980a801401a2c8090dd7180980098079baa0028acc004c01400626464b300130150028034590121bae3013001300f37540051640348068c034dd50009808000c5900e18061baa0078b2014132598009808000c4c8c96600266ebcc044c038dd500480a4566002646600200264660020026eacc04cc050c050c050c050c050c050c040dd5004112cc004006297ae08998099808180a00099801001180a800a0242259800800c528456600266ebcc04c00405a294626600400460280028071011456600260086530010019bab3012301330133013301330133013301330133013300f375400f480010011112cc00400a200319800801cc05400a64b3001300b30113754602200315980099baf301200100d899b800024800a2004808220048080c05000900320248acc004c010c8cc0040040108966002003148002266e012002330020023014001404515980099b88480000062664464b3001300a30103754003159800980518081baa30143011375400315980099baf301430113754602860226ea800c062266e1e60026eacc008c044dd5001cdd7180a002c520004888cc88cc004004008c8c8cc004004014896600200300389919912cc004cdc8808801456600266e3c04400a20030064061133005005301e00440606eb8c05c004dd5980c000980d000a03014bd6f7b630112cc00400600713233225980099b910070028acc004cdc78038014400600c80ba26600a00a603a00880b8dd7180b0009bad30170013019001405c0048a50403d16403d16403c600260206ea8c04cc040dd500118089bac301130123012300e375400c46024602600315980099b880014800229462c806100c452820188a50403114a08062294100c1bad3010301100130103758601e003164034646600200264660020026eacc040c044c044c044c044c034dd5002912cc004006297ae08991991199119801001000912cc004006200713233016374e6602c6ea4014cc058c04c004cc058c0500052f5c0660060066030004602c00280a0dd598088019bae300e0013300300330130023011001403c44b30010018a5eb8226644b30013371e6eb8c04800801a2660226e9c008cc0100100062660080080028068dd618080009808800a01c375c601860126ea800cdc3a400516401c300800130033754011149a26cac80081", + "hash": "a5ed4b4e7813e17eec3b66f64dc5f2c6d11d62e7120d90a0f0acf450" + }, + { + "title": "programmable_logic_base.programmable_logic_base.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "stake_cred", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + } + ], + "compiledCode": "588b010100229800aba2aba1aab9eaab9dab9a4888896600264646644b30013370e900118031baa0018994c004c02400660126014003225980099baf3009300b00100e8a518998010011806000a0104888cc004004dd5980618069806980698069806980698059baa300c00818039baa0018b200a30060013006300700130060013003375400d149a26cac8009", + "hash": "be1e5967da54825149611a725eddea8664c1898a98bcf54f05f6728a" + }, + { + "title": "programmable_logic_base.programmable_logic_base.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "stake_cred", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + } + ], + "compiledCode": "588b010100229800aba2aba1aab9eaab9dab9a4888896600264646644b30013370e900118031baa0018994c004c02400660126014003225980099baf3009300b00100e8a518998010011806000a0104888cc004004dd5980618069806980698069806980698059baa300c00818039baa0018b200a30060013006300700130060013003375400d149a26cac8009", + "hash": "be1e5967da54825149611a725eddea8664c1898a98bcf54f05f6728a" + }, + { + "title": "programmable_logic_global.programmable_logic_global.withdraw", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1ProgrammableLogicGlobalRedeemer" + } + }, + "parameters": [ + { + "title": "params_policy", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "590a58010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400930090024888966002600460126ea800e33001300d300a3754007370e90004dc3a4005300937540089111192cc004c01000a264b30013014001899198008009bac301400222325980080140162646644b3001300b002899192cc004c07000a0071640646eb4c068004c058dd5001c56600260140051323259800980e001400e2c80c8dd6980d000980b1baa0038b2028405060266ea80044c008c06000cc058009014180100145901118079baa0098acc004c00c00a26464653001375a602a003375a602a007375a602a0049112cc004c06401200f16405830150013014001300f375401316403480686600246024602660260032300f37540032232598009803000c4c8c966002602e0050048b2028375c602a00260226ea800e2b30013005001899192cc004c05c00a0091640506eb8c054004c044dd5001c5900f201e300f375400523012301300148888c8c966002601860266ea800626466446600e00233001329800800d20009bac30073018375401f2640044444b30010028800c4ca600200b3370000890014c08000e4b30013370e00a00315980099b8f300b300c301d375400401113259800980b180e9baa00189810980f1baa0018b2038300f301d37546018603a6ea800a2c80da2600600280d9005180f00120389180d980e180e180e180e180e180e000c8c06cc070c070c070c070006444b30010018a5189991191980080080291192cc004cdc79bae301e005375c603c00315980099b89375a603e00a6eb4c07c00633001008981180140110084528203a8998018019811801203a3021001301d001301e001406d222329800800c012006800888966002005100189919914c00401a604800b32330010010052259800800c4cc090cdd81ba9004375000697adef6c608994c004dd71811000cdd69811800cc09c0092225980099b9000800389981419bb037520106ea001c0162b30013371e01000719800804401e005233029337606ea4024dd40008014400500d44cc0a0cdd81ba900337500046600c00c00281210240c094005023200c375c603a0026eb4c078004c08000901e24444464b30013012301c375402f198009112cc00400a2003132332298008034c09c016646600200200a44b300100189981399bb037520086e9800d2f5bded8c113298009bae30250019bab302600198150012444b30013372001000713302b337606ea4020dd3003802c56600266e3c02000e26605666ec0dd48041ba69800803c00a44444600466e0001000d00e000c4cc0accdd81ba9003374c0046600c00c00281390270c0a0005026200c375c60400026eacc084004c08c00902148896600200310028994c0040124b30013375e00400314a313004001408130250024010603e6046002810a603860406eacc014c074dd500a244464664466ebd3001018000374e6530010019bac30273024375403d98009bac3027302437540373756601660486ea806e44464b30013375e605660506ea8c0acc0a0dd500080944c966002603c60506ea8006264b3001301f3029375400313232300700559800981018151baa0018992cc004c8cc004004dd618181818981898189818981898189818981898169baa024225980099b8f375c606200200714a3133002002303200140b1149a2c8150dd7181718159baa0018acc004c0240062934590292052302d302a37540031640a0605860526ea80062c8138c05cc0a0dd5181598141baa001801204c301630273754006801b30013758602c60486ea806e97adef6c609112cc004cdd7981518139baa302a3027375400602313259800980e98139baa0018acc004c074c09cdd5181598141baa00189801802459026459026180b18139baa302a302737540070014094801900111112cc00400a200713298009bae30270019bab302800198158024c0b000d222259800981098159baa00289919911980f8008acc004cdc7801003c566002601a0031323300100100a22325980099b8f00a375c60620031598009980b1bab30320010098cc00403e606c01d007981b001201e8b2060899801801981b001206030340018b205a8b205a375c6060002606060626062002603a602a6eb4c0bcc0b0dd500144c96600266e40dd71818000802c56600266e40014dd718181818800c660020153031009801401d00a45902b45902b180e980a9bad302f302c3754004815060540048140888ca6002003004801a0022225980080144006330010039815801660026054005233008002302c3756602e60506ea80062002802100320509800801c8cdd7800801cc094dd5980498109baa018400c60060068cc004c074dd500bc888ca6002003004a5eb7bdb1810011112cc00400a33001001a5eb7bdb18297adef6c60401113233225980099b900020088cc00401a00333028003004401915980099b8f0020088cc0040126eacc09800e002803a33001004a5eb7bdb18200a80390242048375c60460026050006604c00481224464646600200200844b3001001801c4c8cc896600266e4401c00a2b30013371e00e0051001803204689980280298148022046375c60440026eacc08c004c0940050230a5eb7bdb182453001002800c88888c966002603400300289801800a046337000080068021222233223298009bae30270019813981418141814000cdd698139814001cdd69813801a4444660300062b300132330010013756602260526ea8080896600266ebcc0a4c0b40040162946266004004605c002814226530010019bac301b30293754041002a5eb7bdb1810011111192cc004cdc4801a40011329800800cdd6181898171baa02580440166601e6eacc054c0b8dd5012805400d0011111112cc004cdc48022400115980099baf4c1018000374e00b133017329800800c01200480088896600200510018cc00400e6074005325980099baf303a303737546074606e6ea800408626602e660306eacc098c0dcdd500080980144009035181c801200640dc0051640c51325980099baf303730343754606e60686ea800407a264b30013375e6070606a6ea8004c0e0c0d4dd5001456600266ebcc09cc0d4dd50009813981a9baa0028cc004dd59812181a9baa002808c88a60026eacc09cc0e0dd50024052444b30015980099baf374c00c6e9800e2b30013375e6e98010dd3000c6600266ebcdd30029ba6002a50a5140e514a081ca294103946600201d303f00d98078064c0fc02e6603601400b3301b00200940391640e480d1017459033459033181b80246600200f30380069804002c012007002401c8190c0d8c0dcc0ccdd5181b002a06289919912cc004cdd7981998181baa3033303037540020351300233010005330113756603e60606ea80040322600400a8170a600200f0029801802c00500718188029818802205637009000c590260c050c030004c094010dd69812002101b18010010dd7180c000980c180c8009802980b980a1baa0018b2024300530133754600460266ea8c8cc004004dd61801980a1baa00b22325980099b8f30043005301637540020271001899801801980d001202830180012375c6024602c602e6eacc008c04cdd50009164020300900130043754013149a26cac8011", + "hash": "da609c9b93fee7a2cce156f879aa1f800ad05e86c587478026540231" + }, + { + "title": "programmable_logic_global.programmable_logic_global.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "params_policy", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "590a58010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400930090024888966002600460126ea800e33001300d300a3754007370e90004dc3a4005300937540089111192cc004c01000a264b30013014001899198008009bac301400222325980080140162646644b3001300b002899192cc004c07000a0071640646eb4c068004c058dd5001c56600260140051323259800980e001400e2c80c8dd6980d000980b1baa0038b2028405060266ea80044c008c06000cc058009014180100145901118079baa0098acc004c00c00a26464653001375a602a003375a602a007375a602a0049112cc004c06401200f16405830150013014001300f375401316403480686600246024602660260032300f37540032232598009803000c4c8c966002602e0050048b2028375c602a00260226ea800e2b30013005001899192cc004c05c00a0091640506eb8c054004c044dd5001c5900f201e300f375400523012301300148888c8c966002601860266ea800626466446600e00233001329800800d20009bac30073018375401f2640044444b30010028800c4ca600200b3370000890014c08000e4b30013370e00a00315980099b8f300b300c301d375400401113259800980b180e9baa00189810980f1baa0018b2038300f301d37546018603a6ea800a2c80da2600600280d9005180f00120389180d980e180e180e180e180e180e000c8c06cc070c070c070c070006444b30010018a5189991191980080080291192cc004cdc79bae301e005375c603c00315980099b89375a603e00a6eb4c07c00633001008981180140110084528203a8998018019811801203a3021001301d001301e001406d222329800800c012006800888966002005100189919914c00401a604800b32330010010052259800800c4cc090cdd81ba9004375000697adef6c608994c004dd71811000cdd69811800cc09c0092225980099b9000800389981419bb037520106ea001c0162b30013371e01000719800804401e005233029337606ea4024dd40008014400500d44cc0a0cdd81ba900337500046600c00c00281210240c094005023200c375c603a0026eb4c078004c08000901e24444464b30013012301c375402f198009112cc00400a2003132332298008034c09c016646600200200a44b300100189981399bb037520086e9800d2f5bded8c113298009bae30250019bab302600198150012444b30013372001000713302b337606ea4020dd3003802c56600266e3c02000e26605666ec0dd48041ba69800803c00a44444600466e0001000d00e000c4cc0accdd81ba9003374c0046600c00c00281390270c0a0005026200c375c60400026eacc084004c08c00902148896600200310028994c0040124b30013375e00400314a313004001408130250024010603e6046002810a603860406eacc014c074dd500a244464664466ebd3001018000374e6530010019bac30273024375403d98009bac3027302437540373756601660486ea806e44464b30013375e605660506ea8c0acc0a0dd500080944c966002603c60506ea8006264b3001301f3029375400313232300700559800981018151baa0018992cc004c8cc004004dd618181818981898189818981898189818981898169baa024225980099b8f375c606200200714a3133002002303200140b1149a2c8150dd7181718159baa0018acc004c0240062934590292052302d302a37540031640a0605860526ea80062c8138c05cc0a0dd5181598141baa001801204c301630273754006801b30013758602c60486ea806e97adef6c609112cc004cdd7981518139baa302a3027375400602313259800980e98139baa0018acc004c074c09cdd5181598141baa00189801802459026459026180b18139baa302a302737540070014094801900111112cc00400a200713298009bae30270019bab302800198158024c0b000d222259800981098159baa00289919911980f8008acc004cdc7801003c566002601a0031323300100100a22325980099b8f00a375c60620031598009980b1bab30320010098cc00403e606c01d007981b001201e8b2060899801801981b001206030340018b205a8b205a375c6060002606060626062002603a602a6eb4c0bcc0b0dd500144c96600266e40dd71818000802c56600266e40014dd718181818800c660020153031009801401d00a45902b45902b180e980a9bad302f302c3754004815060540048140888ca6002003004801a0022225980080144006330010039815801660026054005233008002302c3756602e60506ea80062002802100320509800801c8cdd7800801cc094dd5980498109baa018400c60060068cc004c074dd500bc888ca6002003004a5eb7bdb1810011112cc00400a33001001a5eb7bdb18297adef6c60401113233225980099b900020088cc00401a00333028003004401915980099b8f0020088cc0040126eacc09800e002803a33001004a5eb7bdb18200a80390242048375c60460026050006604c00481224464646600200200844b3001001801c4c8cc896600266e4401c00a2b30013371e00e0051001803204689980280298148022046375c60440026eacc08c004c0940050230a5eb7bdb182453001002800c88888c966002603400300289801800a046337000080068021222233223298009bae30270019813981418141814000cdd698139814001cdd69813801a4444660300062b300132330010013756602260526ea8080896600266ebcc0a4c0b40040162946266004004605c002814226530010019bac301b30293754041002a5eb7bdb1810011111192cc004cdc4801a40011329800800cdd6181898171baa02580440166601e6eacc054c0b8dd5012805400d0011111112cc004cdc48022400115980099baf4c1018000374e00b133017329800800c01200480088896600200510018cc00400e6074005325980099baf303a303737546074606e6ea800408626602e660306eacc098c0dcdd500080980144009035181c801200640dc0051640c51325980099baf303730343754606e60686ea800407a264b30013375e6070606a6ea8004c0e0c0d4dd5001456600266ebcc09cc0d4dd50009813981a9baa0028cc004dd59812181a9baa002808c88a60026eacc09cc0e0dd50024052444b30015980099baf374c00c6e9800e2b30013375e6e98010dd3000c6600266ebcdd30029ba6002a50a5140e514a081ca294103946600201d303f00d98078064c0fc02e6603601400b3301b00200940391640e480d1017459033459033181b80246600200f30380069804002c012007002401c8190c0d8c0dcc0ccdd5181b002a06289919912cc004cdd7981998181baa3033303037540020351300233010005330113756603e60606ea80040322600400a8170a600200f0029801802c00500718188029818802205637009000c590260c050c030004c094010dd69812002101b18010010dd7180c000980c180c8009802980b980a1baa0018b2024300530133754600460266ea8c8cc004004dd61801980a1baa00b22325980099b8f30043005301637540020271001899801801980d001202830180012375c6024602c602e6eacc008c04cdd50009164020300900130043754013149a26cac8011", + "hash": "da609c9b93fee7a2cce156f879aa1f800ad05e86c587478026540231" + }, + { + "title": "protocol_params_mint.protocol_params_mint.mint", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "always_fail_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "59041c0101002229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae00248888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e26464b300130100018999119912cc004c020c03cdd500144c8c96600266e1d2004301137540031332259800980618099baa00289919912cc004c06c00626464b30013011001899192cc004c07c00a0111640706eb8c074004c064dd5001456600266e1d2002001899192cc004c07c00a0111640706eb8c074004c064dd5001459017202e3017375400260340031640606eb8c060004c064004c050dd5001459012180a98091baa00115980099198008009bac30163013375401444b30010018a508acc004cdd7980b980a1baa301700101c8a51899801001180c000a024405515980099b8f375c602a00691010e50726f746f636f6c506172616d73008acc004cdc39bad301530160034800a266ebcc054c048dd500118029980a19ba548008cc050dd4807a5eb80cc051300103d87a80004bd70452820208a50404114a080822c8080c014c044dd5000980998081baa0028b201c323300100137586006601e6ea8018896600200314c0103d87a80008992cc004cc896600200514a11329800992cc004c034c050dd5000c4dd6980a980c1bab301830153754003148001013180b800cdd7980b980c000cdd5801a444b30010018acc004c00801a26600a00690004528202a8992cc004cdd7980b800a610140008acc004cc018010dd6980c180d9bab3018001898019ba6301c0028a5040591598009980300224001130030078a50405880b0c0680050180ca60020033756602e603060286ea80124466030004660306e980052f5c08008889660020051330014c103d87a80004bd6f7b63044ca60026eb8c0580066eacc05c00660360069112cc004cdc8a441000038acc004cdc7a4410000389980298069980e1ba60024bd70000c4cc015300103d87a8000006406119800803c006446600e0046603c66ec0dd48029ba6004001401c80c0603200480ba2942294229410181ba633013337606ea401cdd31980999bb04c10f4e50726f746f636f6c506172616d73004c010101004bd6f7b63025eb7bdb1808928c4c010cc04c0052f5c11330030033015002403c60260028088c048dd6180880191808180898088009ba5480022c8068c8cc004004c8cc004004dd59808180898089808980898069baa0042259800800c52f5c113233223322330020020012259800800c400e2646602c6e9ccc058dd48029980b18098009980b180a000a5eb80cc00c00cc060008c0580050141bab3011003375c601c00266006006602600460220028078896600200314bd7044cc896600266e3cdd71809001002c4cc044dd380119802002000c4cc01001000500d1bac3010001301100140386eb8c034c028dd5001c590080c024004c010dd5005452689b2b200401", + "hash": "b35a80517d8fd4bd767e7b20516fbd913b7a4961383ab29cc4a7d92c" + }, + { + "title": "protocol_params_mint.protocol_params_mint.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "always_fail_hash", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "59041c0101002229800aba4aba2aba1aba0aab9faab9eaab9dab9a9bae00248888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e26464b300130100018999119912cc004c020c03cdd500144c8c96600266e1d2004301137540031332259800980618099baa00289919912cc004c06c00626464b30013011001899192cc004c07c00a0111640706eb8c074004c064dd5001456600266e1d2002001899192cc004c07c00a0111640706eb8c074004c064dd5001459017202e3017375400260340031640606eb8c060004c064004c050dd5001459012180a98091baa00115980099198008009bac30163013375401444b30010018a508acc004cdd7980b980a1baa301700101c8a51899801001180c000a024405515980099b8f375c602a00691010e50726f746f636f6c506172616d73008acc004cdc39bad301530160034800a266ebcc054c048dd500118029980a19ba548008cc050dd4807a5eb80cc051300103d87a80004bd70452820208a50404114a080822c8080c014c044dd5000980998081baa0028b201c323300100137586006601e6ea8018896600200314c0103d87a80008992cc004cc896600200514a11329800992cc004c034c050dd5000c4dd6980a980c1bab301830153754003148001013180b800cdd7980b980c000cdd5801a444b30010018acc004c00801a26600a00690004528202a8992cc004cdd7980b800a610140008acc004cc018010dd6980c180d9bab3018001898019ba6301c0028a5040591598009980300224001130030078a50405880b0c0680050180ca60020033756602e603060286ea80124466030004660306e980052f5c08008889660020051330014c103d87a80004bd6f7b63044ca60026eb8c0580066eacc05c00660360069112cc004cdc8a441000038acc004cdc7a4410000389980298069980e1ba60024bd70000c4cc015300103d87a8000006406119800803c006446600e0046603c66ec0dd48029ba6004001401c80c0603200480ba2942294229410181ba633013337606ea401cdd31980999bb04c10f4e50726f746f636f6c506172616d73004c010101004bd6f7b63025eb7bdb1808928c4c010cc04c0052f5c11330030033015002403c60260028088c048dd6180880191808180898088009ba5480022c8068c8cc004004c8cc004004dd59808180898089808980898069baa0042259800800c52f5c113233223322330020020012259800800c400e2646602c6e9ccc058dd48029980b18098009980b180a000a5eb80cc00c00cc060008c0580050141bab3011003375c601c00266006006602600460220028078896600200314bd7044cc896600266e3cdd71809001002c4cc044dd380119802002000c4cc01001000500d1bac3010001301100140386eb8c034c028dd5001c590080c024004c010dd5005452689b2b200401", + "hash": "b35a80517d8fd4bd767e7b20516fbd913b7a4961383ab29cc4a7d92c" + }, + { + "title": "registry_mint.registry_mint.mint", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1RegistryRedeemer" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "issuance_cbor_hex_cs", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "5908ea0101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e266446644b300130060018acc004c038dd5004400a2c807a2b300130030018991919912cc004c05800e00d16404c6eb8c04c004dd71809801180980098071baa0088b20184030330012301030110019180818089808800c88c8cc00400400c896600200314a115980098019809800c528c4cc008008c05000500e20229b804800a4464b30013007001899192cc004c05400a0091640486eb8c04c004c03cdd5001c56600260080031323259800980a80140122c8090dd7180980098079baa0038b201a4034601a6ea800a6e1d200491191919800800802112cc00400600713233225980099b910070028acc004cdc78038014400600c809226600a00a60300088090dd718088009bab301200130140014048297adef6c609b8f48900918081808980898089808800c8c040c044c044c04400522222222229800999119912cc00400a26603898010180003301c374e00297ae08992cc004c8cc00400400c896600200314a315980099baf3020301d3754604000200713300200230210018a50406c80f226603a6e9c00ccc074dd380125eb822c80c8c074c068dd51807180d1baa301d002406c660046eb0c06cc060dd50079198011bab300d30193754601a60326ea800403ccc008dd61805980c1baa00f2330023756601a60326ea800403c88c8cc00400400c896600200314bd7044cc8966002600a00513301e00233004004001899802002000a034301d001301e001406c446466002002603a00644b30010018a508acc004cdc79bae3019301d0010038a51899801001180f000a030406d2259800800c520008980499801001180e000a0329192cc004c01cc05cdd5000c4c06cc060dd5000c590161805180b9baa001912cc004c040c05cdd500144c8c8c8c8ca60026eb8c0800066eb8c0800166eb8c080012604000491112cc004c094016266022604800e2660220020151640883020001301f001301e001301d00130183754005164059222300e323322330020020012259800800c00e2646644b30013372200e00515980099b8f0070028800c01901d44cc014014c08c01101d1bae301c001375a603a002603e00280e8cc02001000c52000488888ca60024464b3001300e32330010010022259800800c52000899914c004dd718110014dd598118014896600200310038991991180c9980280298160021bae3025001375a604c00260500028131222330010010021812000998010011812800a0448992cc004c054c014006264660100022b3001337206eb8c090c084dd50009bae301530213754003159800981280144c8c96600260306eb4c08c00a2b30015980099b8f001375c604c60466ea800e294626020002810a20071640851640846eb8c084004c09000a2c81122c80f8c0200122c80f0cc03400400a2c80e8dd59809180f1baa0029bac301f30200069bac301f006488966002602e603c6ea80662b30013301137586044603e6ea80588cdd7981198101baa0010278acc004c08c00a264b3001598008014528c52820428acc004c034dd7181198101baa0018acc004cdc79bae3014302037540029111effffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008acc004cdd7980998101baa0014c0105d8799f40ff008acc004cdd7980598101baa0014c0105d8799f40ff008acc004c034dd7180618101baa0018cc004dd5980618101baa01780b522100401914a080f2294101e4528203c8a50407914a080f2294101e19801981100100ac590204528203a89919912cc004c068c084dd5000c4c966002603660446ea8006264646644b3001302b0038992cc004c0b002a26466020002264b30010038992cc004c004dc6805c56600266e40dd7181718159baa00300b8acc004cdc80059bae301f302b3754007159800cc004dd5980b98159baa022810c02d01145660026034646600200200644b30010018a40011301e33002002303100140b91598009980e80112cc004cdc79bae302f302c37540026eb8c0bcc0b0dd5002456600266e3cdd7181018161baa00100c8acc004cdd7980f98161baa001301f302c375400915980099baf3017302c3754002602e60586ea8012266e3cdd7180c18161baa001375c603060586ea8012294102a452820548a5040a914a0815226603a00446464b30013371e6eb8c0c4c0b8dd5001807456600266e3cdd7181118171baa003375c6044605c6ea801a2b300130013021302e37540071598009800980c98171baa0038acc004c098dc69bae301a302e375400714a313004371a6eb8c068c0b8dd5001a0588a5040b114a08162294102c45282058259800981318169baa001898011bae3031302e375400313002375c6062605c6ea800502c1192cc004c01000629462604c0028160dc6800c52820528a5040a514a0814a2941029452820528a5040a46e1d20388a5040a0646600200201a44b30010018a5eb8226605a6601e605c00204266004004605e0028160c040c070c0a0dd518158054590292cc004c8cc004004c8cc004004dd5980a98149baa0202259800800c52f5c113233223322330020020012259800800c400e264660646e9ccc0c8dd4802998191817800998191818000a5eb80cc00c00cc0d0008c0c80050301bab302d003375c605400266006006605e004605a0028158896600200314a1159800992cc004cdc79bae302d00100a899b8848000dd6981698171817000c52820503758605800314a3133002002302d001409c8152266e3cde419b8a4881010300337146eb8c0a8c09cdd500299b8a375c605460560106eb8c06cc09cdd5002803c528204a8b2050375c60500026eb8c0a0008c0a0004c08cdd5000c590211805180b18111baa3025302237540031640806eb8c08c004c8cc004004dd6180a98109baa0182259800800c530103d87a80008992cc0066002b3001330113756602e60466ea8c05cc08cdd50008104528c5282048a50a51408513374a900019812800a5eb82266006006604e0048108c094005023180f9baa019407430040040c02cdd50031bae300d300a37540066e1d20028b2010180480098021baa0098a4d1365640081", + "hash": "7caba74f4f7d0b7f813a05552ba48bbcd3b49f6d6b4fda9c22a3fe06" + }, + { + "title": "registry_mint.registry_mint.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "issuance_cbor_hex_cs", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "5908ea0101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e266446644b300130060018acc004c038dd5004400a2c807a2b300130030018991919912cc004c05800e00d16404c6eb8c04c004dd71809801180980098071baa0088b20184030330012301030110019180818089808800c88c8cc00400400c896600200314a115980098019809800c528c4cc008008c05000500e20229b804800a4464b30013007001899192cc004c05400a0091640486eb8c04c004c03cdd5001c56600260080031323259800980a80140122c8090dd7180980098079baa0038b201a4034601a6ea800a6e1d200491191919800800802112cc00400600713233225980099b910070028acc004cdc78038014400600c809226600a00a60300088090dd718088009bab301200130140014048297adef6c609b8f48900918081808980898089808800c8c040c044c044c04400522222222229800999119912cc00400a26603898010180003301c374e00297ae08992cc004c8cc00400400c896600200314a315980099baf3020301d3754604000200713300200230210018a50406c80f226603a6e9c00ccc074dd380125eb822c80c8c074c068dd51807180d1baa301d002406c660046eb0c06cc060dd50079198011bab300d30193754601a60326ea800403ccc008dd61805980c1baa00f2330023756601a60326ea800403c88c8cc00400400c896600200314bd7044cc8966002600a00513301e00233004004001899802002000a034301d001301e001406c446466002002603a00644b30010018a508acc004cdc79bae3019301d0010038a51899801001180f000a030406d2259800800c520008980499801001180e000a0329192cc004c01cc05cdd5000c4c06cc060dd5000c590161805180b9baa001912cc004c040c05cdd500144c8c8c8c8ca60026eb8c0800066eb8c0800166eb8c080012604000491112cc004c094016266022604800e2660220020151640883020001301f001301e001301d00130183754005164059222300e323322330020020012259800800c00e2646644b30013372200e00515980099b8f0070028800c01901d44cc014014c08c01101d1bae301c001375a603a002603e00280e8cc02001000c52000488888ca60024464b3001300e32330010010022259800800c52000899914c004dd718110014dd598118014896600200310038991991180c9980280298160021bae3025001375a604c00260500028131222330010010021812000998010011812800a0448992cc004c054c014006264660100022b3001337206eb8c090c084dd50009bae301530213754003159800981280144c8c96600260306eb4c08c00a2b30015980099b8f001375c604c60466ea800e294626020002810a20071640851640846eb8c084004c09000a2c81122c80f8c0200122c80f0cc03400400a2c80e8dd59809180f1baa0029bac301f30200069bac301f006488966002602e603c6ea80662b30013301137586044603e6ea80588cdd7981198101baa0010278acc004c08c00a264b3001598008014528c52820428acc004c034dd7181198101baa0018acc004cdc79bae3014302037540029111effffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008acc004cdd7980998101baa0014c0105d8799f40ff008acc004cdd7980598101baa0014c0105d8799f40ff008acc004c034dd7180618101baa0018cc004dd5980618101baa01780b522100401914a080f2294101e4528203c8a50407914a080f2294101e19801981100100ac590204528203a89919912cc004c068c084dd5000c4c966002603660446ea8006264646644b3001302b0038992cc004c0b002a26466020002264b30010038992cc004c004dc6805c56600266e40dd7181718159baa00300b8acc004cdc80059bae301f302b3754007159800cc004dd5980b98159baa022810c02d01145660026034646600200200644b30010018a40011301e33002002303100140b91598009980e80112cc004cdc79bae302f302c37540026eb8c0bcc0b0dd5002456600266e3cdd7181018161baa00100c8acc004cdd7980f98161baa001301f302c375400915980099baf3017302c3754002602e60586ea8012266e3cdd7180c18161baa001375c603060586ea8012294102a452820548a5040a914a0815226603a00446464b30013371e6eb8c0c4c0b8dd5001807456600266e3cdd7181118171baa003375c6044605c6ea801a2b300130013021302e37540071598009800980c98171baa0038acc004c098dc69bae301a302e375400714a313004371a6eb8c068c0b8dd5001a0588a5040b114a08162294102c45282058259800981318169baa001898011bae3031302e375400313002375c6062605c6ea800502c1192cc004c01000629462604c0028160dc6800c52820528a5040a514a0814a2941029452820528a5040a46e1d20388a5040a0646600200201a44b30010018a5eb8226605a6601e605c00204266004004605e0028160c040c070c0a0dd518158054590292cc004c8cc004004c8cc004004dd5980a98149baa0202259800800c52f5c113233223322330020020012259800800c400e264660646e9ccc0c8dd4802998191817800998191818000a5eb80cc00c00cc0d0008c0c80050301bab302d003375c605400266006006605e004605a0028158896600200314a1159800992cc004cdc79bae302d00100a899b8848000dd6981698171817000c52820503758605800314a3133002002302d001409c8152266e3cde419b8a4881010300337146eb8c0a8c09cdd500299b8a375c605460560106eb8c06cc09cdd5002803c528204a8b2050375c60500026eb8c0a0008c0a0004c08cdd5000c590211805180b18111baa3025302237540031640806eb8c08c004c8cc004004dd6180a98109baa0182259800800c530103d87a80008992cc0066002b3001330113756602e60466ea8c05cc08cdd50008104528c5282048a50a51408513374a900019812800a5eb82266006006604e0048108c094005023180f9baa019407430040040c02cdd50031bae300d300a37540066e1d20028b2010180480098021baa0098a4d1365640081", + "hash": "7caba74f4f7d0b7f813a05552ba48bbcd3b49f6d6b4fda9c22a3fe06" + }, + { + "title": "registry_spend.registry_spend.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "protocol_params_cs", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "59025f010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cc0240092225980099b8748008c020dd500144cc8a6002601c005300e300f00299198008009bac3002300c375400844b30010018a60103d87a80008992cc004c8cc004004c04cdd5980298079baa3005300f375400444b30010018a508acc004cdc79bae300f301300100d8a51899801001180a000a01c404513374a900019808000a5eb8226600600660240048060c04000500e4dc3a400091112cc004c004c038dd500144c8c966002600660206ea800a2646644b30013018001899192cc004c02000626464b3001301c0028044590191bae301a0013016375400515980099b874800800626464b3001301c0028044590191bae301a0013016375400516405080a0c050dd5000980b800c590151bae301500130160013011375400516403c264646600200264660020026eacc058c05cc05cc05cc05cc04cdd5005912cc004006297ae08991991199119801001000912cc00400620071323301c374e660386ea4014cc070c064004cc070c0680052f5c066006006603c004603800280d0dd5980b8019bae30140013300300330190023017001405444b30010018a508acc004c96600266e3cdd7180b800802466002600c6eb4c05cc060c060006942945012452820243758602c00314a31330020023017001404480a0dd7180998081baa001325980099b8748010c03cdd5000c4c04cc040dd5000c5900e18091809980998079baa3005300f37546024601e6ea800a2c806860126ea80088c034c0380062c8038601200260086ea802629344d95900201", + "hash": "d38270f68cf6138cc7815cce73342378701cc6352f6f15f65235f6b5" + }, + { + "title": "registry_spend.registry_spend.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "protocol_params_cs", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "59025f010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cc0240092225980099b8748008c020dd500144cc8a6002601c005300e300f00299198008009bac3002300c375400844b30010018a60103d87a80008992cc004c8cc004004c04cdd5980298079baa3005300f375400444b30010018a508acc004cdc79bae300f301300100d8a51899801001180a000a01c404513374a900019808000a5eb8226600600660240048060c04000500e4dc3a400091112cc004c004c038dd500144c8c966002600660206ea800a2646644b30013018001899192cc004c02000626464b3001301c0028044590191bae301a0013016375400515980099b874800800626464b3001301c0028044590191bae301a0013016375400516405080a0c050dd5000980b800c590151bae301500130160013011375400516403c264646600200264660020026eacc058c05cc05cc05cc05cc04cdd5005912cc004006297ae08991991199119801001000912cc00400620071323301c374e660386ea4014cc070c064004cc070c0680052f5c066006006603c004603800280d0dd5980b8019bae30140013300300330190023017001405444b30010018a508acc004c96600266e3cdd7180b800802466002600c6eb4c05cc060c060006942945012452820243758602c00314a31330020023017001404480a0dd7180998081baa001325980099b8748010c03cdd5000c4c04cc040dd5000c5900e18091809980998079baa3005300f37546024601e6ea800a2c806860126ea80088c034c0380062c8038601200260086ea802629344d95900201", + "hash": "d38270f68cf6138cc7815cce73342378701cc6352f6f15f65235f6b5" + } + ], + "definitions": { + "ByteArray": { + "title": "ByteArray", + "dataType": "bytes" + }, + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "List": { + "dataType": "list", + "items": { + "$ref": "#/definitions/types~1RegistryProof" + } + }, + "aiken/crypto/ScriptHash": { + "title": "ScriptHash", + "dataType": "bytes" + }, + "aiken/crypto/VerificationKeyHash": { + "title": "VerificationKeyHash", + "dataType": "bytes" + }, + "cardano/address/Credential": { + "title": "Credential", + "description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).", + "anyOf": [ + { + "title": "VerificationKey", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "Script", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1ScriptHash" + } + ] + } + ] + }, + "cardano/assets/PolicyId": { + "title": "PolicyId", + "dataType": "bytes" + }, + "cardano/transaction/OutputReference": { + "title": "OutputReference", + "description": "An `OutputReference` is a unique reference to an output on-chain. The `output_index`\n corresponds to the position in the output list of the transaction (identified by its id)\n that produced that output", + "anyOf": [ + { + "title": "OutputReference", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "transaction_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "output_index", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "types/ProgrammableLogicGlobalRedeemer": { + "title": "ProgrammableLogicGlobalRedeemer", + "description": "Redeemer for the global programmable logic stake validator", + "anyOf": [ + { + "title": "TransferAct", + "description": "Transfer action with proofs for each token type", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "proofs", + "$ref": "#/definitions/List" + } + ] + }, + { + "title": "ThirdPartyAct", + "description": "Third party action for admin operations (seizure, freeze, etc.)\n Supports multiple UTxOs in a single transaction", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "title": "registry_node_idx", + "description": "Index of the registry node in reference inputs", + "$ref": "#/definitions/Int" + }, + { + "title": "outputs_start_idx", + "description": "Starting index in outputs where processed outputs begin", + "$ref": "#/definitions/Int" + }, + { + "title": "length_inputs", + "description": "Length of input_idxs list (for validation)", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "types/RegistryProof": { + "title": "RegistryProof", + "description": "Proof that a token is (or is not) in the registry", + "anyOf": [ + { + "title": "TokenExists", + "description": "Token exists in the registry at this reference input index", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "node_idx", + "$ref": "#/definitions/Int" + } + ] + }, + { + "title": "TokenDoesNotExist", + "description": "Token does not exist (proof via covering node at this index)", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "title": "node_idx", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "types/RegistryRedeemer": { + "title": "RegistryRedeemer", + "description": "Redeemer for registry minting policy (linked list operations)", + "anyOf": [ + { + "title": "RegistryInit", + "description": "Initialize the registry with the origin node", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "RegistryInsert", + "description": "Insert a new programmable token policy into the registry", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "title": "key", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "hashed_param", + "$ref": "#/definitions/ByteArray" + } + ] + } + ] + }, + "types/SmartTokenMintingAction": { + "title": "SmartTokenMintingAction", + "description": "Action for the minting policy", + "anyOf": [ + { + "title": "SmartTokenMintingAction", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "minting_logic_cred", + "description": "The credential of the minting logic script that must be invoked", + "$ref": "#/definitions/cardano~1address~1Credential" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/always_fail.ak b/src/programmable-tokens/aiken-workspace-standard/validators/always_fail.ak new file mode 100644 index 0000000..bcafd01 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/always_fail.ak @@ -0,0 +1,13 @@ +// Always-fail validator parameterized by a nonce. +// Used to permanently lock the IssuanceCborHex NFT so it can never be spent. +// The nonce parameter ensures each deployment produces a unique script hash. + +validator always_fail(_nonce: ByteArray) { + spend(_d, _r, _oref, _tx) { + fail + } + + else(_) { + fail + } +} diff --git a/src/programmable-tokens/aiken-workspace/validators/issuance_cbor_hex_mint.ak b/src/programmable-tokens/aiken-workspace-standard/validators/issuance_cbor_hex_mint.ak similarity index 55% rename from src/programmable-tokens/aiken-workspace/validators/issuance_cbor_hex_mint.ak rename to src/programmable-tokens/aiken-workspace-standard/validators/issuance_cbor_hex_mint.ak index 31d4a80..6135911 100644 --- a/src/programmable-tokens/aiken-workspace/validators/issuance_cbor_hex_mint.ak +++ b/src/programmable-tokens/aiken-workspace-standard/validators/issuance_cbor_hex_mint.ak @@ -1,45 +1,68 @@ -use aiken/collection/list -// Issuance CBOR Hex Reference NFT Minting Policy -// Simple one-shot minting policy to create a reference NFT that marks the UTxO -// containing the issuance script template bytes (prefix and postfix) -// Migrated from SmartTokens.Contracts.IssuanceCborHex - -use cardano/assets.{PolicyId, flatten} -use cardano/transaction.{OutputReference, Transaction} -use types.{issuance_cbor_hex_token_name} - -validator issuance_cbor_hex_mint(utxo_ref: OutputReference) { - mint(_redeemer: Data, own_policy: PolicyId, self: Transaction) { - trace @"Starting issuance_cbor_hex_mint validation" - // Must consume the one-shot UTxO - let consumed = - list.any(self.inputs, fn(input) { input.output_reference == utxo_ref }) - - // Get all minted tokens from this policy - let minted_tokens = flatten(self.mint) - let own_minted = - list.filter( - minted_tokens, - fn(token) { - let (cs, _tn, _qty) = token - cs == own_policy - }, - ) - - // Must mint exactly one token with the correct name and quantity - expect [(_, tn, qty)] = own_minted - - and { - // One-shot: the specified UTxO must be consumed - consumed?, - // Token name must be "IssuanceCborHex" - (tn == issuance_cbor_hex_token_name)?, - // Exactly 1 token minted - (qty == 1)?, - } - } - - else(_) { - fail - } -} +use aiken/collection/list +// Issuance CBOR Hex Reference NFT Minting Policy +// Simple one-shot minting policy to create a reference NFT that marks the UTxO +// containing the issuance script template bytes (prefix and postfix) +// Migrated from SmartTokens.Contracts.IssuanceCborHex + +use cardano/address +use cardano/assets.{PolicyId, flatten} +use cardano/transaction.{InlineDatum, OutputReference, Transaction} +use types.{IssuanceCborHex, issuance_cbor_hex_token_name} + +validator issuance_cbor_hex_mint( + utxo_ref: OutputReference, + always_fail_hash: ByteArray, +) { + mint(_redeemer: Data, own_policy: PolicyId, self: Transaction) { + trace @"Starting issuance_cbor_hex_mint validation" + // Must consume the one-shot UTxO + let consumed = + list.any(self.inputs, fn(input) { input.output_reference == utxo_ref }) + + // Get all minted tokens from this policy + let minted_tokens = flatten(self.mint) + let own_minted = + list.filter( + minted_tokens, + fn(token) { + let (cs, _tn, _qty) = token + cs == own_policy + }, + ) + + // Must mint exactly one token with the correct name and quantity + expect [(_, tn, qty)] = own_minted + + // The always-fail address where the NFT must be locked + let expected_address = address.from_script(always_fail_hash) + // Find the output with address equal to the always-fail address and containing the minted NFT + expect Some(nft_output) = + list.find( + self.outputs, + fn(output) { + assets.has_nft_strict( + output.value, + own_policy, + issuance_cbor_hex_token_name, + ) + }, + ) + expect InlineDatum(datum) = nft_output.datum + expect _issuance_datum: IssuanceCborHex = datum + // + and { + // One-shot: the specified UTxO must be consumed + consumed?, + // Token name must be "IssuanceCborHex" + (tn == issuance_cbor_hex_token_name)?, + // Exactly 1 token minted + (qty == 1)?, + // NFT must be locked at the always-fail address with no stake credential + (nft_output.address == expected_address)?, + } + } + + else(_) { + fail + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/issuance_cbor_hex_mint.test.ak b/src/programmable-tokens/aiken-workspace-standard/validators/issuance_cbor_hex_mint.test.ak new file mode 100644 index 0000000..74e545c --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/issuance_cbor_hex_mint.test.ak @@ -0,0 +1,297 @@ +//// Comprehensive tests for the issuance_cbor_hex_mint validator. +//// +//// Tests invoke the validator's mint handler directly, covering: +//// - Happy path (all conditions met) +//// - One-shot UTxO consumption +//// - Token name and quantity checks +//// - Always-fail address enforcement +//// - InlineDatum and IssuanceCborHex datum validation + +use cardano/address.{Address, Script, VerificationKey} +use cardano/assets.{PolicyId, ada_asset_name, ada_policy_id, from_asset} +use cardano/transaction.{ + InlineDatum, Input, NoDatum, Output, OutputReference, Transaction, +} +use issuance_cbor_hex_mint +use types.{IssuanceCborHex, issuance_cbor_hex_token_name} + +// --------------------------------------------------------------------------- +// Test constants +// --------------------------------------------------------------------------- + +const test_utxo_ref = + OutputReference { + transaction_id: #"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + output_index: 0, + } + +const test_always_fail_hash: ByteArray = + #"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +const test_policy_id: PolicyId = + #"cccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + +const min_ada = + assets.zero + |> assets.add(ada_policy_id, ada_asset_name, 2_000_000) + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn always_fail_address() -> Address { + Address { + payment_credential: Script(test_always_fail_hash), + stake_credential: None, + } +} + +fn valid_issuance_datum() -> Data { + as_data( + IssuanceCborHex { prefix_cbor_hex: #"aabb", postfix_cbor_hex: #"ccdd" }, + ) +} + +fn valid_nft_output() -> Output { + Output { + address: always_fail_address(), + value: min_ada + |> assets.add(test_policy_id, issuance_cbor_hex_token_name, 1), + datum: InlineDatum(valid_issuance_datum()), + reference_script: None, + } +} + +fn valid_input() -> Input { + Input { + output_reference: test_utxo_ref, + output: Output { + address: Address { + payment_credential: VerificationKey( + #"dddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + ), + stake_credential: None, + }, + value: min_ada, + datum: NoDatum, + reference_script: None, + }, + } +} + +fn valid_transaction() -> Transaction { + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, issuance_cbor_hex_token_name, 1), + outputs: [valid_nft_output()], + } +} + +fn call_validator( + utxo_ref: OutputReference, + af_hash: ByteArray, + tx: Transaction, +) -> Bool { + issuance_cbor_hex_mint.issuance_cbor_hex_mint.mint( + utxo_ref, + af_hash, + as_data(Void), + test_policy_id, + tx, + ) +} + +// ======================================================================== +// Happy path — all conditions met +// ======================================================================== + +test issuance_cbor_hex_mint_valid() { + call_validator(test_utxo_ref, test_always_fail_hash, valid_transaction()) +} + +// ======================================================================== +// One-shot UTxO not consumed +// ======================================================================== + +test fails_when_utxo_not_consumed() fail { + let wrong_utxo_ref = + OutputReference { + transaction_id: #"1111111111111111111111111111111111111111111111111111111111111111", + output_index: 99, + } + call_validator(wrong_utxo_ref, test_always_fail_hash, valid_transaction()) +} + +// ======================================================================== +// Wrong token name — the `tn == issuance_cbor_hex_token_name` check +// +// We mint "WrongName" but keep the output NFT as "IssuanceCborHex" so +// has_nft_strict and datum checks pass; only the token-name check fails. +// ======================================================================== + +test fails_with_wrong_token_name() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, "WrongName", 1), + outputs: [valid_nft_output()], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// Quantity != 1 — the `qty == 1` check +// +// We mint qty=2 but keep the output with qty=1 so has_nft_strict passes; +// only the quantity check in the and-block fails. +// ======================================================================== + +test fails_when_quantity_is_not_one() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, issuance_cbor_hex_token_name, 2), + outputs: [valid_nft_output()], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// Multiple token names minted under the same policy +// +// The `expect [(_, tn, qty)] = own_minted` pattern crashes because +// the list has 2 elements instead of 1. +// ======================================================================== + +test fails_with_multiple_token_names_minted() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, issuance_cbor_hex_token_name, 1) + |> assets.add(test_policy_id, "ExtraToken", 1), + outputs: [valid_nft_output()], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// No tokens minted from own policy +// +// The mint contains a different policy entirely, so `own_minted` is empty +// and `expect [(_, tn, qty)]` crashes. +// ======================================================================== + +test fails_when_no_tokens_minted_from_own_policy() fail { + let other_policy: PolicyId = + #"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(other_policy, "SomeToken", 1), + outputs: [valid_nft_output()], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// No output at the always-fail address +// +// The NFT goes to a completely different address. +// `expect Some(nft_output) = list.find(...)` crashes because no output +// matches the expected always-fail address. +// ======================================================================== + +test fails_when_no_output_at_always_fail_address() fail { + let wrong_address = + Address { + payment_credential: Script( + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ), + stake_credential: None, + } + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, issuance_cbor_hex_token_name, 1), + outputs: [ + Output { + address: wrong_address, + value: min_ada + |> assets.add(test_policy_id, issuance_cbor_hex_token_name, 1), + datum: InlineDatum(valid_issuance_datum()), + reference_script: None, + }, + ], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// Output at always-fail address exists but does NOT contain the NFT +// +// `has_nft_strict(nft_output.value, own_policy, ...)` fails because +// the output at the always-fail address has only ADA. +// ======================================================================== + +test fails_when_nft_missing_from_always_fail_output() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, issuance_cbor_hex_token_name, 1), + outputs: [ + // Output at always-fail address but WITHOUT the NFT + Output { + address: always_fail_address(), + value: min_ada, + datum: InlineDatum(valid_issuance_datum()), + reference_script: None, + }, + ], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// No inline datum on the always-fail output +// +// `expect InlineDatum(datum) = nft_output.datum` crashes because the +// output uses NoDatum. +// ======================================================================== + +test fails_when_no_inline_datum() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, issuance_cbor_hex_token_name, 1), + outputs: [Output { ..valid_nft_output(), datum: NoDatum }], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// Invalid datum type — datum is not IssuanceCborHex +// +// `expect _issuance_datum: IssuanceCborHex = datum` crashes because +// an Int doesn't match the IssuanceCborHex constructor. +// ======================================================================== + +test fails_with_invalid_datum_type() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, issuance_cbor_hex_token_name, 1), + outputs: [ + Output { ..valid_nft_output(), datum: InlineDatum(as_data(42)) }, + ], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} diff --git a/src/programmable-tokens/aiken-workspace/validators/issuance_mint.ak b/src/programmable-tokens/aiken-workspace-standard/validators/issuance_mint.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/validators/issuance_mint.ak rename to src/programmable-tokens/aiken-workspace-standard/validators/issuance_mint.ak diff --git a/src/programmable-tokens/aiken-workspace/validators/issuance_mint.test.ak b/src/programmable-tokens/aiken-workspace-standard/validators/issuance_mint.test.ak similarity index 95% rename from src/programmable-tokens/aiken-workspace/validators/issuance_mint.test.ak rename to src/programmable-tokens/aiken-workspace-standard/validators/issuance_mint.test.ak index c2f6ae1..ea7d724 100644 --- a/src/programmable-tokens/aiken-workspace/validators/issuance_mint.test.ak +++ b/src/programmable-tokens/aiken-workspace-standard/validators/issuance_mint.test.ak @@ -75,7 +75,10 @@ test minted_tokens_go_to_programmable_logic_base() { test one_token_name_per_policy() { let single_token = [(test_policy_id, #"746f6b656e31", 100)] let multiple_tokens = - [(test_policy_id, #"746f6b656e31", 100), (test_policy_id, #"746f6b656e32", 50)] + [ + (test_policy_id, #"746f6b656e31", 100), + (test_policy_id, #"746f6b656e32", 50), + ] list.length(single_token) == 1 && list.length(multiple_tokens) == 2 } @@ -115,7 +118,8 @@ test mint_field_structure() { // Should have exactly one entry when flattened is { - [(cs, tn, qty)] -> cs == test_policy_id && tn == #"746f6b656e31" && qty == 100 + [(cs, tn, qty)] -> + cs == test_policy_id && tn == #"746f6b656e31" && qty == 100 _ -> False } } diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/benchmarks.ak b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/benchmarks.ak new file mode 100644 index 0000000..77f06fb --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/benchmarks.ak @@ -0,0 +1,782 @@ +//// End-to-end tests / benchmarks for programmable logic base+global validators + +use aiken/collection/list +use aiken/fuzz +use aiken/primitive/bytearray +use cardano/address.{VerificationKey} +use cardano/assets.{AssetName, PolicyId} +use cardano/fuzz as cardano +use cardano/transaction.{Input, Output, OutputReference, Transaction} +use programmable_logic/fixture +use programmable_logic_base +use programmable_logic_global +use types.{ + ProgrammableLogicGlobalRedeemer, ThirdPartyAct, TokenDoesNotExist, TokenExists, + TransferAct, +} + +fn programmable_logic( + self: Transaction, + redeemer: ProgrammableLogicGlobalRedeemer, +) -> Bool { + // Include both the base and global in the benchmark + and { + programmable_logic_base.programmable_logic_base.spend( + fixture.credential_validator_logic_global, + None, + Void, + Void, + self, + ), + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + self, + ), + } +} + +/// --- Baseline 1 (3rd party) --------------------------------------------------------------- +/// +/// Replicate a Preview 3rd-party transaction based on the Plutarch version of the +/// programmable logic validators. This transaction has been used as a benchmark +/// for optimisation and improvements over the Aiken version. +/// +/// Characteristics: +/// - seize all tokens from 1 UTxO +/// - sends them to a single output +/// - no minting and burning +/// - no other tokens +/// +/// Source: https://preview.cexplorer.io/tx/d29a977a55ed54ef0f5c9d5c15f85a63e915349890f1952934809070430ef33a +const fixture_baseline_3rd_party_1: Transaction = { + let policy_programmable_token: PolicyId = + #"76658c4afd597ba7524f85bf32ac59d9e58856593a2e8399326f853a" + + let asset_name_programmable_token: AssetName = "tUSDT" + + Transaction { + ..transaction.placeholder, + inputs: [ + // Some input to pay for fuel, which has to be 1st input in this scenario, so we enforce the id + // to be lexicographically before the second. + Input { + output_reference: OutputReference { + transaction_id: #"6ef7ec1c8b5dc017042fed2eff90c3f6e4742e8a686099f95eaf73a1d403c402", + output_index: 3, + }, + output: fixture.some_input_fuel("fuel input").output, + }, + // An input holding programmable tokens being spent. + Input { + output_reference: OutputReference { + transaction_id: #"c5972f88da7442689b281d8a85c43321d0fbf32b8f41d4e9a968e0d3edda9a71", + output_index: 1, + }, + output: fixture.some_input_with_programmable_tokens( + [ + ( + policy_programmable_token, + asset_name_programmable_token, + 999999999999, + ), + ], + "programmable tokens", + ).output, + }, + ], + outputs: [ + // Some dust/noise + fixture.some_output_change("noise output"), + // All unburned seized tokens sent to a single output + fixture.some_output_with_seized_tokens( + [ + ( + policy_programmable_token, + asset_name_programmable_token, + 999999999999, + ), + ], + ), + // The first seized input, minus the (partially) seized tokens + fixture.some_output_with_programmable_tokens([]), + // Some change output + fixture.some_output_change("change output"), + ], + reference_inputs: [ + // Reference inputs with the protocol params. Identified by a specific NFT. + // Holds a ProtocolParams inline datum with configuration. + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + // Token registry node holding the programmable policy being spent. + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "current", + ), + ], + withdrawals: fixture.withdrawals_3rd_party, + } + } + +const fixture_redeemer_baseline_3rd_party_1: ProgrammableLogicGlobalRedeemer = + ThirdPartyAct { registry_node_idx: 1, outputs_start_idx: 2, length_inputs: 2 } + +test baseline_3rd_party_1() { + programmable_logic( + fixture_baseline_3rd_party_1, + fixture_redeemer_baseline_3rd_party_1, + ) +} + +/// --- Baseline 2 (3rd party) ------------------------------------------------------------- +/// +/// A slightly more interesting scenario than the baseline #1, which mostly adds "noise": +/// +/// - inputs contain other tokens +/// - seizing from multiple inputs +/// - one partial and one total seizing +/// - burning tokens +/// +const fixture_baseline_3rd_party_2: Transaction = { + let policy_programmable_token: PolicyId = + #"76658c4afd597ba7524f85bf32ac59d9e58856593a2e8399326f853a" + + let asset_name_programmable_token: AssetName = "tUSDT" + + let other_prog_tokens_1 = + (#"00000000000000000000000000000000000000000000000000000000", "foo", 14) + + let other_prog_tokens_2 = + (#"11111111111111111111111111111111111111111111111111111111", "bar", 42) + + let other_prog_tokens_3 = + (#"99999999999999999999999999999999999999999999999999999999", "baz", 1) + + Transaction { + ..transaction.placeholder, + inputs: [ + // An input holding programmable tokens being spent. + Input { + output_reference: OutputReference { + transaction_id: #"00072f88da7442689b281d8a85c43321d0fbf32b8f41d4e9a968e0d3edda9a71", + output_index: 1, + }, + output: fixture.some_input_with_programmable_tokens( + [ + other_prog_tokens_1, + ( + policy_programmable_token, + asset_name_programmable_token, + 999999999999, + ), + ], + "programmable tokens", + ).output, + }, + // Some input to pay for fuel + Input { + output_reference: OutputReference { + transaction_id: #"6ef7ec1c8b5dc017042fed2eff90c3f6e4742e8a686099f95eaf73a1d403c402", + output_index: 3, + }, + output: fixture.some_input_fuel("fuel input").output, + }, + // An input holding programmable tokens being spent. + Input { + output_reference: OutputReference { + transaction_id: #"c5972f88da7442689b281d8a85c43321d0fbf32b8f41d4e9a968e0d3edda9a71", + output_index: 1, + }, + output: fixture.some_input_with_programmable_tokens( + [ + other_prog_tokens_2, + ( + policy_programmable_token, + asset_name_programmable_token, + 999999999999, + ), other_prog_tokens_3, + ], + "programmable tokens", + ).output, + }, + ], + outputs: [ + // Some dust/noise + fixture.some_output_change("noise output"), + // All unburned seized tokens sent to a single output + fixture.some_output_with_seized_tokens( + [ + ( + policy_programmable_token, + asset_name_programmable_token, + 500000000000, + ), + ], + ), + // The first seized input, minus the (partially) seized tokens + fixture.some_output_with_programmable_tokens( + [ + other_prog_tokens_1, + ( + policy_programmable_token, + asset_name_programmable_token, + 999999999998, + ), + ], + ), + // The second seized input, minus the (integrally) seized tokens + fixture.some_output_with_programmable_tokens( + [other_prog_tokens_2, other_prog_tokens_3], + ), + // Some change output + fixture.some_output_change("change output"), + ], + mint: assets.zero + |> assets.add( + policy_programmable_token, + asset_name_programmable_token, + -500000000000, + ), + reference_inputs: [ + // Reference inputs with the protocol params. Identified by a specific NFT. + // Holds a ProtocolParams inline datum with configuration. + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + // Token registry node holding the programmable policy being spent. + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "current", + ), + ], + withdrawals: fixture.withdrawals_3rd_party, + } + } + +const fixture_redeemer_baseline_3rd_party_2: ProgrammableLogicGlobalRedeemer = + ThirdPartyAct { registry_node_idx: 1, outputs_start_idx: 2, length_inputs: 3 } + +test baseline_3rd_party_2() { + programmable_logic( + fixture_baseline_3rd_party_2, + fixture_redeemer_baseline_3rd_party_2, + ) +} + +/// --- Baseline 3 (3rd party) ------------------------------------------------------------- +/// +/// A scenario with MANY seized programmable inputs +const length_baseline_3rd_party_3: Int = 150 + +const fixture_baseline_3rd_party_3: Transaction = { + let policy_programmable_token = + #"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + let asset_name_programmable_token = "programmable" + + let inputs = + list.range(0, length_baseline_3rd_party_3 - 1) + |> list.map( + fn(output_index) { + Input { + output_reference: OutputReference { + transaction_id: #"0000000000000000000000000000000000000000000000000000000000000000", + output_index, + }, + output: fixture.some_input_with_programmable_tokens( + [ + ( + policy_programmable_token, + asset_name_programmable_token, + 1, + ), + ], + bytearray.from_int_big_endian(output_index, 0), + ).output, + } + }, + ) + + let outputs = + list.repeat( + fixture.some_output_with_programmable_tokens([]), + length_baseline_3rd_party_3, + ) + + Transaction { + ..transaction.placeholder, + inputs, + // An input holding programmable tokens being spent. + outputs: list.concat( + outputs, + [ + fixture.some_output_with_seized_tokens( + [ + ( + policy_programmable_token, + asset_name_programmable_token, + length_baseline_3rd_party_3, + ), + ], + ), + ], + ), + reference_inputs: [ + // Reference inputs with the protocol params. Identified by a specific NFT. + // Holds a ProtocolParams inline datum with configuration. + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + // Token registry node holding the programmable policy being spent. + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "current", + ), + ], + withdrawals: fixture.withdrawals_3rd_party, + } + } + +const fixture_redeemer_baseline_3rd_party_3: ProgrammableLogicGlobalRedeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: length_baseline_3rd_party_3, + } + +test baseline_3rd_party_3() { + programmable_logic( + fixture_baseline_3rd_party_3, + fixture_redeemer_baseline_3rd_party_3, + ) +} + +/// --- Plutarch baseline (transfer) --------------------------------------------------------------- +/// +/// Replicate a Preview transfer transaction based on the Plutarch version of the +/// programmable logic validator. This transaction has been used as a benchmark +/// for optimisation and improvements over the Aiken version. +/// +/// Characteristics: +/// - 2 inputs: one script-locked, one for fuel +/// - 3 outputs: two with programmable tokens, and one for change +/// - one programmable token kind (i.e. two nodes from the registry referenced) +/// - transfer logic is script-based, provided as withdrawal +/// +/// Source: https://preview.cexplorer.io/tx/d29ce2a9f79a70a91d83a40e0e1cf346ab94979b6f0ba001de8a89895aa518df +/// ------------------------------------------------------------------------------------------------ +const fixture_plutarch_baseline_transfer: Transaction = { + let policy_programmable_token: PolicyId = + #"00000000000000000000000000000000000000000000000000000f00" + + let asset_name_programmable_token: AssetName = "FOO" + + Transaction { + ..transaction.placeholder, + inputs: [ + // An input holding programmable tokens being spent. + fixture.some_input_with_programmable_tokens( + [(policy_programmable_token, asset_name_programmable_token, 42)], + "programmable tokens input", + ), + // Some input to pay for fuel + fixture.some_input_fuel("fuel input"), + ], + outputs: [ + // A first output with (still trapped) programmable tokens to some recipient + fixture.some_output_with_programmable_tokens( + [(policy_programmable_token, asset_name_programmable_token, 28)], + ), + // A second output with (still trapped) programmable tokens to some recipient + fixture.some_output_with_programmable_tokens( + [(policy_programmable_token, asset_name_programmable_token, 14)], + ), + // Some change output + fixture.some_output_change("change output"), + ], + reference_inputs: [ + // Reference inputs with the protocol params. Identified by a specific NFT. + // Holds a ProtocolParams inline datum with configuration. + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + // Token registry node holding the programmable policy being spent. + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "current", + ), + ], + withdrawals: fixture.withdrawals_transfer, + } + } + +/// Corresponding redeemer with a single proof of a token in the registry, +/// located at position #2 in reference inputs +const fixture_redeemer_plutarch_baseline_transfer: ProgrammableLogicGlobalRedeemer = + TransferAct { proofs: [TokenExists { node_idx: 1 }] } + +test plutarch_baseline_transfer() { + programmable_logic( + fixture_plutarch_baseline_transfer, + fixture_redeemer_plutarch_baseline_transfer, + ) +} + +/// --- Plutarch baseline 2 (transfer) --------------------------------------------------------------- +/// +/// Another baseline from the transfer act that is obtained by making the first one slightly more complexe, +/// although still plausible, having multiple policies and minting and collecting tokens over several +/// inputs. +/// ------------------------------------------------------------------------------------------------ +const fixture_plutarch_baseline_transfer_2: Transaction = { + let policy_programmable_token_1: PolicyId = + #"00000000000000000000000000000000000000000000000000000f00" + + let asset_name_programmable_token_1: AssetName = "FOO" + + let policy_programmable_token_2: PolicyId = + #"00000000000000000000000000000000000000000000000000000ba5" + + let asset_name_programmable_token_2: AssetName = "BAR" + + let policy_unexisting_token: PolicyId = + #"000000000000000000000000000000000000000000000000aaaaffff" + + let asset_name_unexisting_token: PolicyId = "e_e" + + Transaction { + ..transaction.placeholder, + inputs: [ + // An input holding programmable tokens being spent, as well as some unregistered token + fixture.some_input_with_programmable_tokens( + [ + (policy_programmable_token_1, asset_name_programmable_token_1, 42), + (policy_programmable_token_2, asset_name_programmable_token_2, 1), + (policy_unexisting_token, asset_name_unexisting_token, 1), + ], + "programmable tokens input", + ), + // Some input to pay for fuel + fixture.some_input_fuel("fuel input"), + ], + outputs: [ + // A first output with (still trapped) programmable tokens to some recipient + fixture.some_output_with_programmable_tokens( + [ + (policy_programmable_token_1, asset_name_programmable_token_1, 28), + (policy_programmable_token_2, asset_name_programmable_token_2, 1), + ], + ), + // A second output with (still trapped) programmable tokens to some recipient + fixture.some_output_with_programmable_tokens( + [(policy_programmable_token_1, asset_name_programmable_token_1, 113)], + ), + // Unregistered programmable tokens moved out of a programmable output + Output { + ..fixture.some_output_change("fake prog tokens"), + value: fixture.min_ada_value + |> assets.add( + policy_unexisting_token, + asset_name_unexisting_token, + 1, + ), + }, + // Some change output + fixture.some_output_change("change output"), + ], + mint: assets.zero + |> assets.add( + policy_programmable_token_1, + asset_name_programmable_token_1, + 99, + ), + reference_inputs: [ + // Reference inputs with the protocol params. Identified by a specific NFT. + // Holds a ProtocolParams inline datum with configuration. + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + // Token registry node ancestor, for inclusion validation + fixture.some_reference_input_tokens_registry_node( + "", + policy_programmable_token_2, + "ancestor", + ), + // Token registry node holding the programmable policy being spent. + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token_2, + policy_programmable_token_1, + "token_2", + ), + // Token registry node holding the programmable policy being spent. + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token_1, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "token_1", + ), + ], + withdrawals: fixture.withdrawals_transfer, + } + } + +/// Corresponding redeemer with a single proof of a token in the registry, +/// located at position #2 in reference inputs +const fixture_redeemer_plutarch_baseline_transfer_2: ProgrammableLogicGlobalRedeemer = + TransferAct { + proofs: [ + TokenExists { node_idx: 2 }, + TokenExists { node_idx: 3 }, + TokenDoesNotExist { node_idx: 3 }, + ], + } + +test plutarch_baseline_transfer_2() { + programmable_logic( + fixture_plutarch_baseline_transfer_2, + fixture_redeemer_plutarch_baseline_transfer_2, + ) +} + +/// --- Plutarch baseline 3 (transfer) --------------------------------------------------------------- +/// +/// Another baseline mimicking the 'globalTransferMixedManyCtx' from Plutarch's reference implementation +/// ------------------------------------------------------------------------------------------------ +const fixture_plutarch_baseline_transfer_3: Transaction = { + let signer_vkh = #"01010101010101010101010101010101010101010101010101010101" + + let policy_unexisting_token_1 = + #"1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a" + let policy_unexisting_token_2 = + #"1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e" + + let policy_programmable_token_1 = + #"1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b" + let policy_programmable_token_2 = + #"1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c" + let policy_programmable_token_3 = + #"1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d" + + Transaction { + ..transaction.placeholder, + extra_signatories: [signer_vkh], + inputs: [ + fixture.some_input_with_programmable_tokens_with( + VerificationKey(signer_vkh), + [ + (policy_unexisting_token_1, "np1", 4), + (policy_programmable_token_1, "0c", 5), + (policy_programmable_token_2, "1c", 7), + (policy_programmable_token_3, "2c", 9), + (policy_unexisting_token_2, "np2", 6), + ], + "programmable tokens input", + ), + ], + outputs: [ + fixture.some_output_with_programmable_tokens_with( + VerificationKey(signer_vkh), + [ + (policy_programmable_token_1, "0c", 5), + (policy_programmable_token_2, "1c", 7), + (policy_programmable_token_3, "2c", 9), + ], + ), + Output { + ..fixture.some_output_change("change"), + value: fixture.min_ada_value + |> assets.add(policy_unexisting_token_1, "np1", 4) + |> assets.add(policy_unexisting_token_2, "np2", 6), + }, + ], + reference_inputs: [ + // Reference inputs with the protocol params. Identified by a specific NFT. + // Holds a ProtocolParams inline datum with configuration. + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + #"", + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "bb00", + ), + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token_1, + policy_programmable_token_2, + "bb10", + ), + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token_2, + policy_programmable_token_3, + "bb11", + ), + fixture.some_reference_input_tokens_registry_node( + policy_programmable_token_3, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "bb12", + ), + ], + withdrawals: [Pair(fixture.credential_transfer_logic, 0)], + } + } + +const fixture_redeemer_plutarch_baseline_transfer_3: ProgrammableLogicGlobalRedeemer = + TransferAct { + proofs: [ + TokenDoesNotExist { node_idx: 1 }, + TokenExists { node_idx: 2 }, + TokenExists { node_idx: 3 }, + TokenExists { node_idx: 4 }, + TokenDoesNotExist { node_idx: 1 }, + ], + } + +test plutarch_baseline_transfer_3() { + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + fixture_redeemer_plutarch_baseline_transfer_3, + fixture.credential_validator_logic_global, + fixture_plutarch_baseline_transfer_3, + ) +} + +/// --- Bench: tokens ------------------------------------------------------------------------------ +/// +/// A benchmark evaluating the impact of increasing the number of tokens from a +/// same policy in a transfer. All tokens are located at a single TxO, and +/// transfered to a single address. +/// ------------------------------------------------------------------------------------------------ +fn sample_many_tokens( + size: Int, +) -> Fuzzer<(Transaction, ProgrammableLogicGlobalRedeemer)> { + let policy_programmable_token: PolicyId = + #"00000000000000000000000000000000000000000000000000000f00" + + let tokens <- + fuzz.map( + fuzz.list_between( + { + let asset_name <- fuzz.and_then(cardano.asset_name()) + let quantity <- fuzz.and_then(fuzz.int_at_least(1)) + fuzz.constant((policy_programmable_token, asset_name, quantity)) + }, + size + 1, + size + 1, + ), + ) + + fixture.some_transfer(tokens) +} + +bench many_tokens((self, redeemer) via sample_many_tokens) { + programmable_logic(self, redeemer) +} + +/// --- Bench: inputs ------------------------------------------------------------------------------ +/// +/// A benchmark evaluating the impact of increasing the number of inputs +/// holding programmable tokens. For the sake of benchmarking the influence of +/// inputs, every input is identical and holds tokens coming from a single +/// policy. +/// For similar reasons, everything is sent back to a single output. +/// ------------------------------------------------------------------------------------------------ +fn sample_many_inputs( + size: Int, +) -> Fuzzer<(Transaction, ProgrammableLogicGlobalRedeemer)> { + let policy_programmable_token: PolicyId = + #"00000000000000000000000000000000000000000000000000000f00" + + let asset_name_programmable_token: AssetName = "FOO" + + let inputs <- + fuzz.map( + fuzz.list_between( + fixture.any_input_with_programmable_tokens( + fixture.credential_transfer_logic, + [(policy_programmable_token, asset_name_programmable_token, 1)], + ), + size + 1, + size + 1, + ), + ) + + let (transaction, redeemer) = + fixture.some_transfer( + [(policy_programmable_token, asset_name_programmable_token, size + 1)], + ) + + (Transaction { ..transaction, inputs }, redeemer) +} + +bench many_inputs((self, redeemer) via sample_many_inputs) { + programmable_logic(self, redeemer) +} + +/// --- Bench: policies ---------------------------------------------------------------------------- +/// +/// A benchmark evaluating the impact of increasing the number of unique +/// policies. For the sake of benchmarking the influence of policies alone, we +/// only use a single token per policy, and originate all tokens from a single +/// TxO; sent to a single output. +/// ------------------------------------------------------------------------------------------------ +fn sample_many_policies( + size: Int, +) -> Fuzzer<(Transaction, ProgrammableLogicGlobalRedeemer)> { + let tokens: List<(PolicyId, AssetName, Int)> <- + fuzz.map( + fuzz.list_between( + cardano.policy_id() |> fuzz.map(fn(policy) { (policy, "FOO", 42) }), + size + 1, + size + 1, + ), + ) + + fixture.some_transfer(tokens) +} + +bench many_policies((self, redeemer) via sample_many_policies) { + programmable_logic(self, redeemer) +} + +/// --- Bench: outputs ---------------------------------------------------------------------------- +/// +/// A benchmark showing the impact of increasing the number of (programmable) +/// outputs in a transfer action. All tokens are from the same policy, and come from a single TxO. +/// ------------------------------------------------------------------------------------------------ +fn sample_many_outputs( + size: Int, +) -> Fuzzer<(Transaction, ProgrammableLogicGlobalRedeemer)> { + let policy_programmable_token: PolicyId = + #"00000000000000000000000000000000000000000000000000000f00" + + let asset_name_programmable_token: AssetName = "FOO" + + let outputs: List = + list.repeat( + fixture.some_output_with_programmable_tokens( + [(policy_programmable_token, asset_name_programmable_token, 1)], + ), + size + 1, + ) + + let (transaction, redeemer) = + fixture.some_transfer( + [(policy_programmable_token, asset_name_programmable_token, size + 1)], + ) + + fuzz.constant((Transaction { ..transaction, outputs }, redeemer)) +} + +bench many_outputs((self, redeemer) via sample_many_outputs) { + programmable_logic(self, redeemer) +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/fixture.ak b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/fixture.ak new file mode 100644 index 0000000..9675139 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/fixture.ak @@ -0,0 +1,345 @@ +//// Fixtures, fuzzers and arbitrary data useful to bench and tests the programmable logic validators end to end. + +use aiken/collection/dict +use aiken/collection/dict/strategy.{keep_left} +use aiken/collection/list +use aiken/crypto.{blake2b_256} +use aiken/fuzz +use cardano/address.{Address, Credential, Inline} +use cardano/assets.{AssetName, PolicyId, Value, ada_asset_name, ada_policy_id} +use cardano/fuzz as cardano +use cardano/transaction.{InlineDatum, Input, NoDatum, Output, Transaction} +use programmable_logic/params.{ProgrammableLogicGlobalParams} +use registry_node.{RegistryNode} +use types.{ProgrammableLogicGlobalRedeemer, TokenExists, TransferAct} + +/// Some script credential for the transfer logic. Could also be a verification +/// key hash, but the primary use-case is for script. +pub const credential_transfer_logic: Credential = + Credential.Script(#"00000000000000000000000000000000000000000000000000000b0b") + +/// Some script credential for the 3rd-party logic. Could also be a verification +/// key hash, but the primary use-case is for script. +pub const credential_3rd_party_logic: Credential = + Credential.Script(#"0000000000000000000000000000000000000000000000000000035d") + +/// Some script credential for the 3rd-party logic. Could also be a verification +/// key hash, but the primary use-case is for script. +pub const credential_seized_logic: Credential = + Credential.Script(#"000000000000000000000000000000000000000000000000002e12ed") + +/// Arbitrary script hash representing the programmable_logic_base validator. +pub const credential_validator_logic_base: Credential = + Credential.Script(#"0000000000000000000000000000000000000000000000000000ba5e") + +/// Arbitrary script hash representing the programmable_logic_global validator. +pub const credential_validator_logic_global: Credential = + Credential.Script(#"0000000000000000000000000000000000000000000000000001091c") + +/// Arbitrary script hash representing the protocol params NFT's minting script +pub const policy_protocol_params: PolicyId = + #"000000000000000000000000000000000000000000000000000005ef" + +/// Arbitrary script hash representing the linked-list registry NFT's script +pub const policy_tokens_registry: PolicyId = + #"000000000000000000000000000000000000000000000000000040de" + +/// Typical withdrawals found in a transfer transaction; used to artificially +/// invoke global logic. +pub const withdrawals_transfer: Pairs = + [ + // The artificial withdraw-0 to execute the global logic + Pair(credential_validator_logic_global, 0), + // The artificial withdraw-0 to execute the transfer logic + Pair(credential_transfer_logic, 0), + ] + +/// Typical withdrawals found in a 3rd party transaction; used to artificially +/// invoke global logic. +pub const withdrawals_3rd_party: Pairs = + [ + // The artificial withdraw-0 to execute the global logic + Pair(credential_validator_logic_global, 0), + // The artificial withdraw-0 to execute the 3rd party logic + Pair(credential_3rd_party_logic, 0), + ] + +/// Parameters for a specific instance of the registry. +pub const protocol_params: ProgrammableLogicGlobalParams = + ProgrammableLogicGlobalParams { + registry_node_cs: policy_tokens_registry, + prog_logic_cred: credential_validator_logic_base, + } + +/// Some input to pay for fuel, deterministically determined by the label. +pub fn some_input_fuel(label: ByteArray) -> Input { + Input { + output_reference: generate(cardano.output_reference(), label), + output: Output { + address: generate(cardano.address(), label), + value: min_ada_value, + datum: NoDatum, + reference_script: None, + }, + } +} + +/// An input that spends UTxO locked by the validator logic, parameterized with +/// the transfer logic and holding the given tokens (which may or may not be +/// registered tokens) +pub fn any_input_with_programmable_tokens( + stake_credential: Credential, + tokens: List<(PolicyId, AssetName, Int)>, +) -> Fuzzer { + let output_reference <- fuzz.map(cardano.output_reference()) + Input { + output_reference, + output: Output { + address: Address { + payment_credential: credential_validator_logic_base, + stake_credential: Some(Inline(stake_credential)), + }, + value: list.foldr( + tokens, + min_ada_value, + fn((policy, asset_name, quantity), assets) { + assets.add(assets, policy, asset_name, quantity) + }, + ), + datum: NoDatum, + reference_script: None, + }, + } +} + +pub fn some_input_with_programmable_tokens_with( + stake_credential: Credential, + tokens: List<(PolicyId, AssetName, Int)>, + label: ByteArray, +) -> Input { + generate(any_input_with_programmable_tokens(stake_credential, tokens), label) +} + +/// A deterministic version of 'any_input_with_programmable_tokens', which +/// determinism is given by the label. +pub fn some_input_with_programmable_tokens( + tokens: List<(PolicyId, AssetName, Int)>, + label: ByteArray, +) -> Input { + some_input_with_programmable_tokens_with( + credential_transfer_logic, + tokens, + label, + ) +} + +/// Some reference inputs holding the given protocol params as inline datum. +/// Identified by a specific NFT from the protocol params policy. +pub fn some_reference_input_protocol_params( + protocol_params: ProgrammableLogicGlobalParams, + seed: ByteArray, +) -> Input { + Input { + output_reference: generate(cardano.output_reference(), seed), + output: Output { + address: address.from_verification_key( + generate(cardano.verification_key_hash(), seed), + ), + value: min_ada_value + |> assets.add(policy_protocol_params, "ProtocolParams", 1), + datum: InlineDatum(as_data(protocol_params)), + reference_script: None, + }, + } +} + +// Some change output +pub fn some_output_change(label: ByteArray) -> Output { + Output { + address: generate(cardano.address(), label), + value: generate(cardano.lovelace(), label), + datum: NoDatum, + reference_script: None, + } +} + +pub fn some_output_with_programmable_tokens_with( + stake_credential: Credential, + tokens: List<(PolicyId, AssetName, Int)>, +) -> Output { + Output { + address: Address { + payment_credential: credential_validator_logic_base, + stake_credential: Some(Inline(stake_credential)), + }, + value: list.foldr( + tokens, + min_ada_value, + fn((policy, asset_name, quantity), assets) { + assets.add(assets, policy, asset_name, quantity) + }, + ), + datum: NoDatum, + reference_script: None, + } +} + +/// An arbitrary 'programmable' output (i.e. locked by the base logic + +/// transfer logic) containing the given tokens. +pub fn some_output_with_programmable_tokens( + tokens: List<(PolicyId, AssetName, Int)>, +) -> Output { + some_output_with_programmable_tokens_with(credential_transfer_logic, tokens) +} + +/// An arbitrary seized output (i.e. locked by the base logic + +/// 3rd party logic) containing the given tokens. +pub fn some_output_with_seized_tokens( + tokens: List<(PolicyId, AssetName, Int)>, +) -> Output { + Output { + address: Address { + payment_credential: credential_validator_logic_base, + stake_credential: Some(Inline(credential_seized_logic)), + }, + value: list.foldr( + tokens, + min_ada_value, + fn((policy, asset_name, quantity), assets) { + assets.add(assets, policy, asset_name, quantity) + }, + ), + datum: NoDatum, + reference_script: None, + } +} + +/// A token registry linked-list node configured with the 'transfer_logic' +pub fn some_reference_input_tokens_registry_node_with( + transfer_logic_script: Credential, + third_party_transfer_logic_script: Credential, + key: ByteArray, + next: ByteArray, + label: ByteArray, +) -> Input { + Input { + output_reference: generate(cardano.output_reference(), label), + output: Output { + address: address.from_verification_key( + generate(cardano.verification_key_hash(), label), + ), + value: min_ada_value + |> assets.add(policy_tokens_registry, blake2b_256(key), 1), + datum: InlineDatum( + as_data( + RegistryNode { + key, + next, + transfer_logic_script, + third_party_transfer_logic_script, + global_state_cs: "", + }, + ), + ), + reference_script: None, + }, + } +} + +pub fn some_reference_input_tokens_registry_node( + key: ByteArray, + next: ByteArray, + label: ByteArray, +) -> Input { + some_reference_input_tokens_registry_node_with( + credential_transfer_logic, + credential_3rd_party_logic, + key, + next, + label, + ) +} + +/// A baseline transaction for transferring tokens which can be used a +/// placeholder with most field pre-determined. +pub fn some_transfer( + tokens: List<(PolicyId, AssetName, Int)>, +) -> (Transaction, ProgrammableLogicGlobalRedeemer) { + // Nub token policies, while preserving their order. This way, we allow for + // declaring policies in arbitrary order, even if the proofs must be provided + // in ascending policy order. + let + (node_idxs, policies), + _, + <- + list.foldl2( + tokens, + (dict.empty, []), + 2, + fn((policy, _, _), (idxs, policies), ix, return) { + let new_idxs = dict.insert_with(idxs, policy, ix, keep_left()) + // If unchanged, it means the policy was already known. No need to increment the index + if new_idxs == idxs { + return((idxs, policies), ix) + } else { + return((new_idxs, [policy, ..policies]), ix + 1) + } + }, + ) + + // Create the linked-list registry from the end of the linked-list. Note that + // we don't have 'gaps' in our linked-list, whereas the protocol should allow + // for it just fine. + let + first_policy, + nodes, + <- + list.foldl2( + policies, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + [], + fn(key, next, nodes, return) { + let node = some_reference_input_tokens_registry_node(key, next, key) + return(key, [node, ..nodes]) + }, + ) + + let nodes = + [some_reference_input_tokens_registry_node("", first_policy, ""), ..nodes] + + ( + Transaction { + ..transaction.placeholder, + inputs: [ + some_input_with_programmable_tokens(tokens, "programmable tokens input"), + ], + outputs: [some_output_with_programmable_tokens(tokens)], + reference_inputs: [ + some_reference_input_protocol_params(protocol_params, "protocol params"), + ..nodes + ], + withdrawals: withdrawals_transfer, + }, + node_idxs + |> dict.foldr( + [], + fn(_, node_idx, proofs) { [TokenExists { node_idx }, ..proofs] }, + ) + |> TransferAct, + ) +} + +// --------------------------------------------------------------------- helpers + +/// Some value with a non-zero ADA amount. Embodies typical values found on-chain. +pub const min_ada_value: Value = + assets.zero |> assets.add(ada_policy_id, ada_asset_name, 1000000) + +/// Run a 'Fuzzer' in-place, using a label as a seed. This can be used to +/// deterministically produce a single arbitrary value from a known fuzzer. +/// +/// Different seeds give different values. +fn generate(fuzzer: Fuzzer, label: ByteArray) -> a { + expect Some((_, a)) = fuzzer(Seeded { seed: blake2b_256(label), choices: "" }) + a +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/params.ak b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/params.ak new file mode 100644 index 0000000..fb3bf0d --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/params.ak @@ -0,0 +1,47 @@ +use aiken/builtin.{head_list, tail_list, unconstr_fields} +use assets +use cardano/address.{Credential} +use cardano/assets.{PolicyId} as value +use cardano/transaction.{InlineDatum, Input, Output} + +/// Protocol parameters NFT token name +pub const protocol_params_token = "ProtocolParams" + +/// Protocol parameters stored on-chain +/// This is referenced by all programmable token transactions +pub type ProgrammableLogicGlobalParams { + /// Currency symbol of the registry node NFTs + registry_node_cs: PolicyId, + /// The programmable logic base credential (payment credential for all programmable token UTxOs) + prog_logic_cred: Credential, +} + +// Extract protocol parameters from reference inputs +pub fn with_programmable_logic_params( + reference_inputs: List, + protocol_params_cs: PolicyId, + return: fn(PolicyId, Credential) -> result, +) -> result { + let protocol_params_ref = + get_protocol_params_ref(protocol_params_cs, reference_inputs) + + expect InlineDatum(inline_datum) = protocol_params_ref.output.datum + let fields = unconstr_fields(inline_datum) + expect registry_node_cs: PolicyId = head_list(fields) + let fields = tail_list(fields) + expect prog_logic_cred: Credential = head_list(fields) + + return(registry_node_cs, prog_logic_cred) +} + +fn get_protocol_params_ref( + protocol_params_cs: PolicyId, + ref_inputs: List, +) -> Input { + let head = head_list(ref_inputs) + if assets.peek_first(head.output) == protocol_params_cs { + head + } else { + get_protocol_params_ref(protocol_params_cs, tail_list(ref_inputs)) + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/third_party.ak b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/third_party.ak new file mode 100644 index 0000000..733468f --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/third_party.ak @@ -0,0 +1,208 @@ +//// Programmable Logic Global Stake Validator +//// This is the core CIP-0143 validator that coordinates all programmable token transfers +//// Migrated from SmartTokens.Contracts.ProgrammableLogicBase (mkProgrammableLogicGlobal) + +use aiken/collection/dict +use aiken/collection/list.{foldl} as aiken_list +use assets +use cardano/address.{Credential} +use cardano/assets.{PolicyId} as value +use cardano/transaction.{Input, Output, Transaction} +use list +use pairs +use registry_node.{with_key_and_3rd_party_logic} +use tokens.{Tokens} + +pub fn validate_3rd_party( + self: Transaction, + prog_logic_cred: Credential, + registry_node: Data, + outputs_start_idx: Int, + length_inputs: Int, +) -> Bool { + // Extract registry details from datum + let + policy_id, + third_party_logic, + <- with_key_and_3rd_party_logic(registry_node) + + // The third party transfer logic script must be invoked + expect pairs.has_key_or_fail(self.withdrawals, third_party_logic) + + let + output_tokens, + outputs, + <- + drop_accum_tokens( + self.outputs, + outputs_start_idx, + prog_logic_cred, + policy_id, + dict.empty, + ) + + // Minted/Burned tokens are used as the 'zero value' for computing the delta + // between outputs and inputs: + // + // - burns (negative quantity) reduces the expected output + // - mints (positive quantity) increases it + // + // At the end, all programmable tokens must be account for. + let minted_or_burned = self.mint |> value.tokens(policy_id) + + check_seized_tokens( + prog_logic_cred, + policy_id, + self.inputs, + length_inputs, + outputs, + minted_or_burned, + output_tokens, + ) +} + +/// Drop 'n' output from the list, while accumulating tokens of a given policy present in dropped outputs +fn drop_accum_tokens( + outputs: List, + n: Int, + prog_logic_cred: Credential, + policy_id: PolicyId, + tokens: Tokens, + return: fn(Tokens, List) -> result, +) -> result { + if n <= 0 { + return(tokens, outputs) + } else { + expect [output, ..tail_outputs] = outputs + + let loop = + drop_accum_tokens( + tail_outputs, + n - 1, + prog_logic_cred, + policy_id, + _, + return, + ) + + if output.address.payment_credential == prog_logic_cred { + loop(tokens.union(tokens, output.value |> value.tokens(policy_id))) + } else { + loop(tokens) + } + } +} + +/// Analyze inputs and outputs affected by the 3rd-party action (grossly named *seized_tokens* here). +fn check_seized_tokens( + prog_logic_cred: Credential, + policy_id: PolicyId, + inputs: List, + length_inputs: Int, + outputs: List, + input_tokens: Tokens, + output_tokens: Tokens, +) -> Bool { + // This is an optimisation where the transaction builder also provides the length of the input indexes. This + // allows to not have to repeatedly pattern match on the input indices list to grab the next one; and converts + // that check into a null check on an integer. + // + // This is safe because: + // + // - if a larger length than the real one is given: + // we'll attempt to 'list.head' on an empty list and the whole validation blows up. + // + // - if a smaller length than the real one is given: + // we do ensures that there are no unprocessed programmable input once we + // reach 0 (which would indicate someone tried to bypass validations). + // + if length_inputs <= 0 { + // All inputs must have been consumed / checked + expect [] == inputs + + // Finish accumulating relevant tokens in remaining outputs. This ensure + // that tokens that have moved to new outputs that weren't paired with a + // seized input are still accounted for. + let total_output_tokens = + foldl( + outputs, + output_tokens, + fn(output, acc) { + // Ensure to only count programmable outputs, to not allow tokens to escape. + if output.address.payment_credential == prog_logic_cred { + tokens.union( + // We can safely restrict to the given policy id, to prevent arbitrarily large values + // from being too costly to merge. We check inclusion in the end, from tokens that have + // also been filtered by policy id. So other tokens are irrelevant. + // + // If anything, it only makes the inclusion stronger by restricting the superset further. + output.value |> value.tokens(policy_id), + acc, + ) + } else { + acc + } + }, + ) + + // All the inputs tokens (seized or not) must still be sent to the prog + // logic validator. This is checked by ensuring all collected tokens are + // contained within all programmable outputs, while allowing those outputs + // to contain other things. + tokens.contains( + dict.to_pairs(total_output_tokens), + dict.to_pairs(input_tokens), + )? + } else { + let Input { output: input, .. } = list.head(inputs) + + // Validate all inputs pointing at a programmable proof + if input.address.payment_credential == prog_logic_cred { + // Extract the paired output and ensures it preserves address and datum + let output = list.head(outputs) + expect output.address == input.address + expect output.datum == input.datum + + // Extract seized tokens as the delta between outputs and inputs for the + // given policy, ensuring all else is equal. + let + input_tokens_before, + input_tokens_at, + input_tokens_after, + <- assets.split_at(input.value, policy_id) + + let + output_tokens_before, + output_tokens_at, + output_tokens_after, + <- assets.split_at(output.value, policy_id) + + // Seized tokens must change, but only them. + expect and { + (input_tokens_before == output_tokens_before)?, + (input_tokens_after == output_tokens_after)?, + (input_tokens_at != output_tokens_at)?, + } + + check_seized_tokens( + prog_logic_cred, + policy_id, + list.tail(inputs), + length_inputs - 1, + list.tail(outputs), + tokens.union(input_tokens, input_tokens_at), + tokens.union(output_tokens_at, output_tokens), + ) + } else { + check_seized_tokens( + prog_logic_cred, + policy_id, + list.tail(inputs), + length_inputs - 1, + outputs, + input_tokens, + output_tokens, + ) + } + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/transfer.ak b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/transfer.ak new file mode 100644 index 0000000..5bc9cb0 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic/transfer.ak @@ -0,0 +1,216 @@ +use aiken/builtin.{equals_bytearray, less_than_bytearray} +use aiken/collection/dict.{Dict} +use assets.{Assets} +use cardano/address.{Credential, Inline, Script, VerificationKey} +use cardano/assets.{AssetName, PolicyId} as value +use cardano/transaction.{Input, Output, Transaction} +use list +use pairs +use registry_node.{with_key_and_next_key, with_key_and_transfer_logic} +use tokens +use types.{RegistryProof, TokenDoesNotExist, TokenExists} + +/// Validate a transfer action +pub fn validate_transfer( + self: Transaction, + prog_logic_cred: Credential, + get_registry_node: fn(Int) -> Data, + proofs: List, +) -> Bool { + // NOTE: The programmable logic global is alway invoked via a withdrawal, so + // we know there's AT LEAST ONE withdrawal. + let first_withdrawal = list.head(self.withdrawals).1st + let has_withdrawal = + new_withdrawal_checker( + fn(script_hash) { script_hash == first_withdrawal }, + list.tail(self.withdrawals), + ) + + // Collect all programmable tokens across inputs and minted/burned values. + let input_assets = collect_input_assets(self, has_withdrawal, prog_logic_cred) + + // Collect all outputs that carry programmable tokens. We use them to ensure that + // prog tokens cannot escape their fate. + let output_assets = collect_output_assets(self.outputs, prog_logic_cred) + + // Check transfer logic for each programmable token type and filter to only programmable tokens. + [] == verify_proofs( + proofs, + choose_registered_token_with(get_registry_node, has_withdrawal, _, _, _, _), + input_assets, + output_assets, + ) +} + +fn new_withdrawal_checker( + has_withdrawal: fn(Credential) -> Bool, + withdrawals: Pairs, +) -> fn(Credential) -> Bool { + when withdrawals is { + [] -> has_withdrawal + [Pair(head, _), ..tail] -> + new_withdrawal_checker( + fn(script_hash) { head == script_hash || has_withdrawal(script_hash) }, + tail, + ) + } +} + +fn collect_input_assets( + tx: Transaction, + has_withdrawal: fn(Credential) -> Bool, + prog_logic_cred: Credential, +) -> Assets { + let has_signatory = + list.has_or_fail(tx.extra_signatories, _) + + assets.collect( + tx.inputs, + assets.from_value(tx.mint), + fn(input, select, discard) { + let output = input.output + if output.address.payment_credential == prog_logic_cred { + expect Some(Inline(stake_cred)) = output.address.stake_credential + when stake_cred is { + VerificationKey(pkh) -> { + expect has_signatory(pkh) + } + Script(_hash) -> { + expect has_withdrawal(stake_cred) + } + } + select(output) + } else { + discard() + } + }, + ) +} + +fn collect_output_assets( + outputs: List, + prog_logic_cred: Credential, +) -> Assets { + assets.collect( + outputs, + [], + fn(output, select, discard) { + if output.address.payment_credential == prog_logic_cred { + expect Some(Inline(..)) = output.address.stake_credential + select(output) + } else { + discard() + } + }, + ) +} + +/// Get inputs from prog_logic_cred and verify ALL have proper authorization +/// This function enforces ownership by requiring that every input from prog_logic_cred +/// is authorized by its stake credential (signature or script invocation). +/// If ANY input lacks authorization, the transaction FAILS. +/// Sum values from a list of inputs +/// Check transfer logic for each currency symbol and filter to only programmable tokens +fn verify_proofs( + proofs: List, + choose_registered_token: fn( + PolicyId, + RegistryProof, + fn() -> List, + fn() -> List, + ) -> + List, + input_assets: Pairs>, + output_assets: Pairs>, +) -> List { + when input_assets is { + [] -> proofs + [Pair(policy, head_input_tokens), ..tail_input_assets] -> + // We expect one proof per policy, if not, this blows up and halts the validation. + choose_registered_token( + policy, + list.head(proofs), + fn() { + let tail_output_assets = + if head_input_tokens == dict.empty { + // Tokens have been entirely consumed (a.k.a burned) so they won't appear in outputs. + output_assets + } else { + // Existing programmable token, must be in output. + + // Outputs must be a superset of inputs, so they may contain extra + // policies not found in validated inputs. Yet, both input assets and + // output assets are in ascending policy order. + let + head_output_tokens, + tail_output_assets, + <- pairs.pop_until(output_assets, equals_bytearray(policy, _)) + expect + tokens.contains( + dict.to_pairs(head_output_tokens), + dict.to_pairs(head_input_tokens), + ) + + tail_output_assets + } + + verify_proofs( + list.tail(proofs), + choose_registered_token, + tail_input_assets, + tail_output_assets, + ) + }, + fn() { + // Not a programmable token, skip it + verify_proofs( + list.tail(proofs), + choose_registered_token, + tail_input_assets, + output_assets, + ) + }, + ) + } +} + +/// Validate a single currency symbol against its proof +/// Returns True if the token IS a programmable token (TokenExists with valid proof) +/// Returns False if the token is NOT a programmable token (TokenDoesNotExist with valid proof) +/// FAILS the transaction if any proof is invalid +fn choose_registered_token_with( + get_registry_node: fn(Int) -> Data, + has_withdrawal: fn(Credential) -> Bool, + policy: PolicyId, + proof: RegistryProof, + when_registered: fn() -> result, + when_not_registered: fn() -> result, +) -> result { + when proof is { + TokenExists { node_idx } -> { + let + key, + transfer_logic_script, + <- with_key_and_transfer_logic(get_registry_node(node_idx)) + + // Validate the node's key matches the currency symbol + expect key == policy + + // Validate the transfer logic script is invoked + expect has_withdrawal(transfer_logic_script) + + when_registered() + } + + TokenDoesNotExist { node_idx } -> { + let key, next <- with_key_and_next_key(get_registry_node(node_idx)) + + // Validate the node covers the currency symbol (node.key < policy < node.next) + expect less_than_bytearray(key, policy) + expect less_than_bytearray(policy, next) + + // This is NOT a programmable token (proof is valid) + when_not_registered() + } + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_base.ak b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_base.ak new file mode 100644 index 0000000..0a90cda --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_base.ak @@ -0,0 +1,28 @@ +//// Programmable Logic Base Validator +//// This validator locks all programmable token UTxOs +//// It forwards validation to the global programmable logic stake validator +//// Migrated from SmartTokens.Contracts.ProgrammableLogicBase + +use cardano/address.{Credential} +use cardano/transaction.{Transaction} +use pairs + +validator programmable_logic_base(stake_cred: Credential) { + spend( + _datum: Option, + _redeemer: Data, + _own_ref: Data, + self: Transaction, + ) { + trace @"Starting programmable_logic_base validation" + + // The programmable logic base validator simply checks that the global + // programmable logic stake script is invoked in the transaction via the + // withdraw-zero pattern Check that the stake credential is invoked + self.withdrawals |> pairs.has_key_or_fail(stake_cred) + } + + else(_) { + fail + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_global.ak b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_global.ak new file mode 100644 index 0000000..0a8703a --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_global.ak @@ -0,0 +1,56 @@ +//// Programmable Logic Global Stake Validator +//// This is the core CIP-0143 validator that coordinates all programmable token transfers +//// Migrated from SmartTokens.Contracts.ProgrammableLogicBase (mkProgrammableLogicGlobal) + +use cardano/address.{Credential} +use cardano/assets.{PolicyId} as value +use cardano/transaction.{Transaction} +use programmable_logic/params.{with_programmable_logic_params} +use programmable_logic/third_party.{validate_3rd_party} +use programmable_logic/transfer.{validate_transfer} +use registry_node.{new_registry_node_getter} +use types.{ProgrammableLogicGlobalRedeemer, ThirdPartyAct, TransferAct} + +validator programmable_logic_global(params_policy: PolicyId) { + withdraw( + redeemer: ProgrammableLogicGlobalRedeemer, + _account: Credential, + self: Transaction, + ) { + trace @"Starting programmable_logic_global validation" + + // Extract protocol parameters from reference inputs + let + registry_node_policy, + prog_logic_cred, + <- with_programmable_logic_params(self.reference_inputs, params_policy) + + // Pre-construct a getter for registry nodes in reference inputs, to avoid + // repeatedly doing it in loops. + let get_registry_node = + new_registry_node_getter( + registry_node_policy, + 0, + self.reference_inputs, + fn(_) { fail }, + ) + + when redeemer is { + TransferAct { proofs } -> + validate_transfer(self, prog_logic_cred, get_registry_node, proofs) + + ThirdPartyAct { registry_node_idx, outputs_start_idx, length_inputs } -> + validate_3rd_party( + self, + prog_logic_cred, + get_registry_node(registry_node_idx), + outputs_start_idx, + length_inputs, + ) + } + } + + else(_) { + fail + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_global.test.ak b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_global.test.ak new file mode 100644 index 0000000..5d40f82 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/programmable_logic_global.test.ak @@ -0,0 +1,1659 @@ +//// Integration tests for programmable logic global validator + +use aiken/collection/list +use aiken/crypto.{blake2b_256} +use cardano/address.{Address, Credential, Inline, Script, VerificationKey} +use cardano/assets.{ + PolicyId, ada_asset_name, ada_policy_id, from_asset, merge, negate, zero, +} +use cardano/transaction.{ + InlineDatum, Input, NoDatum, Output, OutputReference, Transaction, +} +use programmable_logic/fixture +use programmable_logic/params.{ProgrammableLogicGlobalParams} +use programmable_logic_global +use registry_node.{RegistryNode} +use types.{ThirdPartyAct, TokenDoesNotExist, TokenExists, TransferAct} +use utils.{bytearray_lt} + +const test_registry_node_cs: PolicyId = + #"1112131415161718191a1b1c0d0e0f0102030405060708090a0b0c0d" + +const test_prog_logic_cred: Credential = Script(#"70726f67") + +const test_transfer_logic_cred: Credential = Script(#"7472616e73666572") + +const test_third_party_transfer_logic_cred: Credential = Script(#"6973737565") + +const test_token_cs: PolicyId = + #"2122232425262728292a2b2c2d2e2f303132333435363738393a3b3c" + +// Test ProgrammableLogicGlobalParams structure +test programmable_logic_global_params_structure() { + let params = + ProgrammableLogicGlobalParams { + prog_logic_cred: test_prog_logic_cred, + registry_node_cs: test_registry_node_cs, + } + + params.prog_logic_cred == test_prog_logic_cred && params.registry_node_cs == test_registry_node_cs +} + +// Test TransferAct redeemer +test transfer_act_redeemer() { + let proof1 = TokenExists { node_idx: 0 } + let proof2 = TokenDoesNotExist { node_idx: 1 } + + let redeemer = TransferAct { proofs: [proof1, proof2] } + + when redeemer is { + TransferAct { proofs } -> list.length(proofs) == 2 + _ -> False + } +} + +// Test TokenDoesNotExist proof +test token_does_not_exist_proof() { + let proof = TokenDoesNotExist { node_idx: 3 } + + when proof is { + TokenDoesNotExist { node_idx } -> node_idx == 3 + _ -> False + } +} + +// Test registry node validation for transfer +test registry_node_validation_for_transfer() { + let node = + RegistryNode { + key: test_token_cs, + next: #"ffff", + transfer_logic_script: test_transfer_logic_cred, + third_party_transfer_logic_script: test_third_party_transfer_logic_cred, + global_state_cs: #"", + } + + // Node key should match the token CS + node.key == test_token_cs && // Transfer logic script should be present + node.transfer_logic_script == test_transfer_logic_cred +} + +// Test covering node logic for non-existent token +test covering_node_logic_for_non_existent_token() { + let covering_key = #"aa" + let token_cs = #"bb" + let next_key = #"cc" + + let covering_node = + RegistryNode { + key: covering_key, + next: next_key, + transfer_logic_script: test_transfer_logic_cred, + third_party_transfer_logic_script: test_third_party_transfer_logic_cred, + global_state_cs: #"", + } + + // Covering node should satisfy: covering_key < token_cs < next_key + bytearray_lt(covering_node.key, token_cs) && bytearray_lt( + token_cs, + covering_node.next, + ) +} + +// Test seize validation structure +test seize_validation_structure() { + let seized_cs = test_token_cs + let input_value = from_asset(seized_cs, #"746f6b656e", 100) + let expected_output_value = zero + + // After seizing, the output should have the tokens removed + input_value != expected_output_value +} + +// Test programmable tokens go to prog_logic_cred +test programmable_tokens_go_to_prog_logic_cred() { + let output = + Output { + address: Address { + payment_credential: test_prog_logic_cred, + stake_credential: None, + }, + value: from_asset(test_token_cs, #"746f6b656e", 100), + datum: NoDatum, + reference_script: None, + } + + output.address.payment_credential == test_prog_logic_cred +} + +// Test transfer logic script must be invoked +test transfer_logic_script_must_be_invoked() { + let invoked_scripts = [test_transfer_logic_cred, Script(#"6f74686572")] + + list.has(invoked_scripts, test_transfer_logic_cred) +} + +// Test third party transfer logic script must be invoked for third party actions +test third_party_transfer_logic_script_must_be_invoked_for_third_party_actions() { + let invoked_scripts = + [test_third_party_transfer_logic_cred, Script(#"6f74686572")] + + list.has(invoked_scripts, test_third_party_transfer_logic_cred) +} + +// Test signed prog inputs validation +test signed_prog_inputs_validation() { + let stake_pkh = #"aabbccdd" + let input = + Input { + output_reference: OutputReference { + transaction_id: #"00", + output_index: 0, + }, + output: Output { + address: Address { + payment_credential: test_prog_logic_cred, + stake_credential: Some(Inline(VerificationKey(stake_pkh))), + }, + value: from_asset(test_token_cs, #"746f6b656e", 100), + datum: NoDatum, + reference_script: None, + }, + } + + // Input is from prog_logic_cred with stake credential + input.output.address.payment_credential == test_prog_logic_cred && when + input.output.address.stake_credential + is { + Some(Inline(VerificationKey(pkh))) -> pkh == stake_pkh + _ -> False + } +} + +// Test seize prevents DDOS by requiring value change +test seize_prevents_ddos_by_requiring_value_change() { + let input_value = from_asset(test_token_cs, #"746f6b656e", 100) + // let output_value = from_asset(test_token_cs, #"746f6b656e", 100) + let expected_output = + merge(input_value, negate(from_asset(test_token_cs, #"746f6b656e", 50))) + + // Must actually remove tokens (DDOS prevention) + input_value != expected_output +} + +// Test only one prog_logic_cred input allowed in seize +test only_one_prog_logic_cred_input_in_seize() { + let inputs = + [ + Input { + output_reference: OutputReference { + transaction_id: #"00", + output_index: 0, + }, + output: Output { + address: Address { + payment_credential: test_prog_logic_cred, + stake_credential: None, + }, + value: zero, + datum: NoDatum, + reference_script: None, + }, + }, + ] + + list.length( + list.filter( + inputs, + fn(input) { + input.output.address.payment_credential == test_prog_logic_cred + }, + ), + ) == 1 +} + +// Test protocol params from reference inputs +test protocol_params_from_reference_inputs() { + let params = + ProgrammableLogicGlobalParams { + prog_logic_cred: test_prog_logic_cred, + registry_node_cs: test_registry_node_cs, + } + + // Protocol params should be loaded from reference input + params.registry_node_cs == test_registry_node_cs +} + +// Test invoked scripts via withdrawals +test invoked_scripts_via_withdrawals() { + let withdrawals = + [ + Pair(test_transfer_logic_cred, 0), + Pair(test_third_party_transfer_logic_cred, 0), + ] + + let invoked_scripts = + list.map( + withdrawals, + fn(wdrl) { + let Pair(cred, _amount) = wdrl + cred + }, + ) + + list.has(invoked_scripts, test_transfer_logic_cred) && list.has( + invoked_scripts, + test_third_party_transfer_logic_cred, + ) +} + +// --------------------------------------------------------------------------- +// F-01: ThirdPartyAct input_idxs uniqueness & ordering tests +// +// These tests invoke the actual programmable_logic_global.withdraw validator +// with a ThirdPartyAct redeemer to exercise the validate_third_party path. +// +// The two `fail` tests demonstrate the vulnerability: duplicate or unsorted +// input_idxs currently PASS the validator (so the `fail` test will itself +// FAIL). Once the fix enforcing strictly-increasing indices is applied, the +// validator will correctly reject them and these tests will PASS. +// --------------------------------------------------------------------------- + +/// Arbitrary credential representing a third-party transfer logic script +/// (e.g. a seize/freeze substandard) used in withdrawals. +const test_third_party_cred: Credential = + Script(#"000000000000000000000000000000000000000000000000000003d9") + +/// A programmable token policy used across the ThirdPartyAct tests. +const test_third_party_policy: PolicyId = + #"00000000000000000000000000000000000000000000000000000f00" + +/// Build a registry-node reference input whose third_party_transfer_logic_script +/// is set to a real credential (unlike the default fixture which uses Script("")). +fn third_party_registry_node_ref_input( + key: ByteArray, + third_party_cred: Credential, +) -> Input { + Input { + output_reference: OutputReference { + transaction_id: #"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + output_index: 0, + }, + output: Output { + address: Address { + payment_credential: Script( + #"00000000000000000000000000000000000000000000000000aabb", + ), + stake_credential: None, + }, + value: from_asset(ada_policy_id, ada_asset_name, 1_000_000) + |> merge( + from_asset(fixture.policy_tokens_registry, blake2b_256(key), 1), + ), + datum: InlineDatum( + as_data( + RegistryNode { + key, + next: #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + transfer_logic_script: fixture.credential_transfer_logic, + third_party_transfer_logic_script: third_party_cred, + global_state_cs: "", + }, + ), + ), + reference_script: None, + }, + } +} + +/// Run programmable_logic_global.withdraw with a ThirdPartyAct redeemer whose +/// input_idxs are given as parameter. The transaction always has two +/// prog_logic_cred inputs (indices 0 and 1) holding programmable tokens, and +/// two corresponding outputs with those tokens removed, plus a remaining +/// output at prog_logic_cred that receives the seized tokens (balance invariant). +fn validate_third_party_with_outputs( + outputs: List, + outputs_start_idx: Int, +) -> Bool { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + // Index 0 – prog_logic input A + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 42)], + "third_party_input_a", + ), + // Index 1 – prog_logic input B + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "third_party_input_b", + ), + ], + outputs, + reference_inputs: [ + // Index 0 – protocol params + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + // Index 1 – registry node with real third-party credential + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { registry_node_idx: 1, length_inputs: 2, outputs_start_idx } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +test third_party_act_prog_outputs_first() { + validate_third_party_with_outputs( + [ + // Paired outputs with programmable tokens removed (value = min_ada only) + fixture.some_output_with_programmable_tokens([]), + fixture.some_output_with_programmable_tokens([]), + // Remaining output: seized tokens stay at prog_logic_cred (balance invariant) + fixture.some_output_with_seized_tokens( + [(test_third_party_policy, "FOO", 142)], + ), + ], + 0, + ) +} + +test third_party_act_prog_outputs_skipped() { + validate_third_party_with_outputs( + [ + // Remaining output: seized tokens stay at prog_logic_cred (balance invariant) + fixture.some_output_with_seized_tokens( + [(test_third_party_policy, "FOO", 142)], + ), + // Paired outputs with programmable tokens removed (value = min_ada only) + fixture.some_output_with_programmable_tokens([]), + fixture.some_output_with_programmable_tokens([]), + ], + 1, + ) +} + +test third_party_act_missing_prog_output() fail { + validate_third_party_with_outputs( + [ + // Paired outputs with programmable tokens removed (value = min_ada only) + fixture.some_output_with_programmable_tokens([]), + // Remaining output: seized tokens stay at prog_logic_cred (balance invariant) + fixture.some_output_with_seized_tokens( + [(test_third_party_policy, "FOO", 142)], + ), + ], + 0, + ) +} + +test third_party_act_prog_outputs_out_of_order() fail { + validate_third_party_with_outputs( + [ + // Remaining output: seized tokens stay at prog_logic_cred (balance invariant) + fixture.some_output_with_seized_tokens( + [(test_third_party_policy, "FOO", 142)], + ), + // Paired outputs with programmable tokens removed (value = min_ada only) + fixture.some_output_with_programmable_tokens([]), + fixture.some_output_with_programmable_tokens([]), + ], + 0, + ) +} + +// --------------------------------------------------------------------------- +// Balance Invariant Tests (Issues #2 and #3 from functional analysis) +// +// These tests verify that seized tokens cannot escape prog_logic_cred +// and that partial seizure is supported. +// --------------------------------------------------------------------------- + +// Negative: seized tokens routed to Eve's wallet (NOT at prog_logic_cred). +// The balance invariant rejects this because total output at prog_cred (0) +// is less than total input tokens (42). +test third_party_act_seized_tokens_escape_prog_cred_must_fail() fail { + let eve_address = + Address { + payment_credential: VerificationKey( + #"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + ), + stake_credential: None, + } + + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + // Input 0: prog_logic_cred UTxO with 42 FOO tokens + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 42)], + "smuggling_input", + ), + ], + outputs: [ + // Output 0: paired output at prog_logic_cred with FOO removed + fixture.some_output_with_programmable_tokens([]), + // Output 1: Eve's wallet receives the 42 FOO — ESCAPE! + Output { + address: eve_address, + value: from_asset(ada_policy_id, ada_asset_name, 1_000_000) + |> merge(from_asset(test_third_party_policy, "FOO", 42)), + datum: NoDatum, + reference_script: None, + }, + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Positive: seized tokens routed to a new output at prog_logic_cred. +// This is the correct seizure pattern. +test third_party_act_seized_tokens_to_prog_cred_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 42)], + "seize_input", + ), + ], + outputs: [ + // Paired output: tokens removed + fixture.some_output_with_programmable_tokens([]), + // Remaining output: seized tokens stay at prog_logic_cred + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 42)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Positive: partial seizure — seize 40 of 100 tokens, leave 60 with owner. +// The paired output retains 60 FOO, and a remaining output receives the 40 seized. +test third_party_act_partial_seizure_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "partial_seize_input", + ), + ], + outputs: [ + // Paired output: owner keeps 60 FOO + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 60)], + ), + // Remaining output: authority receives 40 seized FOO + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 40)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: remaining outputs at prog_cred don't have enough seized tokens. +// Input has 100 FOO, paired output has 0, remaining only has 50 — delta of 50 is missing. +test third_party_act_delta_insufficient_remaining_fails() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "insufficient_input", + ), + ], + outputs: [ + // Paired output: all FOO removed + fixture.some_output_with_programmable_tokens([]), + // Remaining output: only 50 FOO, but 100 were seized + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 50)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// --------------------------------------------------------------------------- +// TransferAct Mint Support Tests (Issue #1 from functional analysis) +// --------------------------------------------------------------------------- + +/// A programmable token policy used for the transfer/mint tests. +const test_mint_policy: PolicyId = + #"3132333435363738393a3b3c3d3e3f404142434445464748494a4b4c" + +// Positive: partial burn via TransferAct with mint. +// Input has 100 FOO, burns 30, output has 70. The proof serves both for mint +// and input to validate the burned and spent tokens against the registry, +// adjusting the expected output down. +test transfer_act_partial_burn_with_mint_proofs_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_mint_policy, "FOO", 100)], + "burn input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", -30), + outputs: [ + fixture.some_output_with_programmable_tokens( + [(test_mint_policy, "FOO", 70)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Positive: transfer with minting — new tokens appear at prog_logic_cred. +// Input has 100 FOO, mints 50 more, output must have at least 150. +test transfer_act_with_minting_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_mint_policy, "FOO", 100)], + "mint input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", 50), + outputs: [ + fixture.some_output_with_programmable_tokens( + [(test_mint_policy, "FOO", 150)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: minting with empty mint_proofs must fail. +// If tx.mint is non-empty but mint_proofs is [], the validator must reject. +// This prevents unvalidated tokens from bypassing the directory check. +test transfer_act_mint_without_proofs_must_fail() fail { + let test_input_policy = "input policy" + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_input_policy, "FOO", 100)], + "input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", 50), + outputs: [ + fixture.some_output_with_programmable_tokens( + [(test_input_policy, "FOO", 100), (test_mint_policy, "FOO", 50)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_input_policy, + test_mint_policy, + "registry node #1", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node #2", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + // No proof for idx 2, necessary for the mint but tx.mint is non-empty — must be rejected + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Positive: transfer with ONLY minting — new tokens appear at prog_logic_cred. +// Mints 50, no other prog input, output must have at least 50. +test transfer_act_with_only_minting_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [], + mint: from_asset(test_mint_policy, "FOO", 50), + outputs: [ + fixture.some_output_with_programmable_tokens( + [(test_mint_policy, "FOO", 150)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: transfer with only minting should not succeed if lacking proofs... +test transfer_act_with_only_minting_no_proofs_fails() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [], + mint: from_asset(test_mint_policy, "FOO", 50), + outputs: [ + fixture.some_output_with_programmable_tokens( + [(test_mint_policy, "FOO", 150)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + ], + withdrawals: [Pair(fixture.credential_validator_logic_global, 0)], + } + + let redeemer = TransferAct { proofs: [] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +test transfer_act_burning_compensate_spending() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_mint_policy, "FOO", 100)], + "mint input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", -100), + outputs: [fixture.some_output_with_programmable_tokens([])], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +test transfer_act_entirely_burning_one_token() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_mint_policy, "FOO", 100), (test_mint_policy, "BAR", 100)], + "mint input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", -100), + outputs: [ + fixture.some_output_with_programmable_tokens( + [(test_mint_policy, "BAR", 100)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +test transfer_act_entirely_burning_one_token_fail_if_other_missing() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_mint_policy, "FOO", 100), (test_mint_policy, "BAR", 100)], + "mint input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", -100), + outputs: [fixture.some_output_with_programmable_tokens([])], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: unauthenticated transfer & burn where spent tokens are entirely burned (so +// mint & spend values balancing each other out) +test transfer_act_unauthorized_burning_compensate_spending_fails() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_mint_policy, "FOO", 100)], + "mint input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", -100), + outputs: [], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node", + ), + ], + withdrawals: [Pair(fixture.credential_validator_logic_global, 0)], + } + + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: unauthenticated transfer & burn where spent tokens are entirely burned (so +// mint & spend values balancing each other out) +test transfer_act_burning_compensate_spending_unproven_token_fails() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_mint_policy, "FOO", 100)], + "mint input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", -100), + outputs: [], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + let redeemer = TransferAct { proofs: [] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// --------------------------------------------------------------------------- +// ThirdPartyAct Wipe (Burn) Support Tests +// +// These tests verify that ThirdPartyAct supports burning tokens via tx.mint, +// enabling a compliance "wipe" feature: seize AND burn in one transaction. +// The mint-aware balance invariant adjusts input expectations by tx.mint. +// --------------------------------------------------------------------------- + +// Positive: full wipe — seize all 100 FOO and burn all 100. +// Input has 100 FOO, mint has -100 FOO, paired output has 0 FOO. +// Adjusted input = 100 + (-100) = 0, filtered to empty → check trivially passes. +test third_party_act_wipe_full_burn_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "wipe_full_burn_input", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", -100), + outputs: [ + // Paired output: all FOO removed + fixture.some_output_with_programmable_tokens([]), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Positive: partial burn — seize all 100 FOO, burn 60, 40 remaining at prog_cred. +// Adjusted input = 100 + (-60) = 40, output has 40 → passes. +test third_party_act_wipe_partial_burn_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "wipe_partial_burn_input", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", -60), + outputs: [ + // Paired output: all FOO removed from owner + fixture.some_output_with_programmable_tokens([]), + // Remaining output: 40 FOO stays at prog_logic_cred + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 40)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Positive: seize + partial burn — seize 80 of 100, burn 50, 30 remaining. +// Owner keeps 20, authority gets 30, 50 burned. +// Adjusted input = 100 + (-50) = 50, output = 20 + 30 = 50 → passes. +test third_party_act_wipe_seize_and_partial_burn() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "wipe_seize_partial_input", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", -50), + outputs: [ + // Paired output: owner keeps 20 FOO + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 20)], + ), + // Remaining output: authority receives 30 seized FOO + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 30)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: burn 30, but remaining output only 50 — need 70. +// Adjusted input = 100 + (-30) = 70, output = 0 + 50 = 50 < 70 → fails. +test third_party_act_wipe_insufficient_after_burn_fails() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "wipe_insufficient_input", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", -30), + outputs: [ + // Paired output: all FOO removed + fixture.some_output_with_programmable_tokens([]), + // Remaining output: only 50 FOO, but adjusted input expects 70 + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 50)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: spending and burning some tokens (exact quantities), without executing the 3rd party script. +test third_party_act_unauthorized_burn_compensate_spend() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "programmable tokens", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", -50), + outputs: [ + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 50)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [Pair(fixture.credential_validator_logic_global, 0)], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: spending and burning some tokens (exact quantities), without providing adequate proof. +test third_party_act_unproven_burn_compensate_spend() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "programmable tokens", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", -50), + outputs: [ + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 50)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 0, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// --------------------------------------------------------------------------- +// ThirdPartyAct Mint Interaction Tests +// +// Verify that minting during a ThirdPartyAct (seizure) behaves correctly for +// all combinations: same-policy mint, other-policy mint (programmable or not), +// and mixed mints. The balance invariant only tracks the seized policy_id; +// other mints are invisible to it. Each layer handles its own authorization: +// - issuance_mint → authorizes minting/burning +// - validate_third_party → tracks balance for the seized policy +// - programmable_logic_base → authorizes spending from prog_cred +// --------------------------------------------------------------------------- + +/// An unrelated policy used to test minting of tokens other than the seized policy. +/// From the validator's perspective, programmable vs non-programmable is identical: +/// get_policy_tokens only extracts the seized policy, so this is invisible to the +/// balance invariant regardless. issuance_mint handles programmable mint authorization +/// externally. +const test_unrelated_policy: PolicyId = + #"aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd" + +// Positive: mint same policy during seizure — adjusted input INCREASES. +// Seize 100 FOO, mint 50 more FOO → adjusted input = 150. +// Output must have >= 150 FOO at prog_cred. +// Not exploitable: issuance_mint forces minted tokens to prog_cred, +// so the minter is just raising the bar for themselves. +test third_party_act_same_policy_mint_during_seizure_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "same_policy_mint_input", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", 50), + outputs: [ + // Paired output: all FOO removed from owner + fixture.some_output_with_programmable_tokens([]), + // Remaining output: 100 seized + 50 minted = 150 at prog_cred + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 150)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: mint same policy during seizure but insufficient output. +// Seize 100 FOO, mint 50 → adjusted input = 150, but output only has 100. +test third_party_act_same_policy_mint_insufficient_output_fails() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "same_policy_mint_fail_input", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", 50), + outputs: [ + // Paired output: all FOO removed + fixture.some_output_with_programmable_tokens([]), + // Only 100 FOO, but adjusted input expects 150 + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Positive: mint an unrelated policy during seizure. +// Seized policy is test_third_party_policy; the unrelated mint is invisible +// to the balance invariant (get_policy_tokens ignores it). +// Programmable vs non-programmable doesn't matter here — the validator +// doesn't distinguish. issuance_mint handles programmable authorization +// externally; non-programmable tokens have their own minting policy. +test third_party_act_unrelated_policy_mint_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "unrelated_mint_input", + ), + ], + mint: from_asset(test_unrelated_policy, "BAR", 500), + outputs: [ + // Paired output: all FOO removed + fixture.some_output_with_programmable_tokens([]), + // Remaining output: seized FOO stays at prog_cred + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Positive: wipe (burn seized policy) + mint unrelated policy in the same tx. +// This is the realistic compliance scenario: admin burns seized tokens while +// the tx also mints something unrelated (e.g. a receipt NFT). +// Balance invariant sees only the seized policy burn; unrelated mint is invisible. +test third_party_act_wipe_burn_plus_unrelated_mint_succeeds() { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_third_party_policy, "FOO", 100)], + "mixed_mint_input", + ), + ], + mint: from_asset(test_third_party_policy, "FOO", -60) + |> merge(from_asset(test_unrelated_policy, "RECEIPT", 1)), + outputs: [ + // Paired output: all FOO removed from owner + fixture.some_output_with_programmable_tokens([]), + // Remaining output: 40 FOO stays at prog_cred (100 - 60 burned) + fixture.some_output_with_programmable_tokens( + [(test_third_party_policy, "FOO", 40)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + third_party_registry_node_ref_input( + test_third_party_policy, + test_third_party_cred, + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(test_third_party_cred, 0), + ], + } + + let redeemer = + ThirdPartyAct { + registry_node_idx: 1, + outputs_start_idx: 0, + length_inputs: 1, + } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} + +// Negative: minting without enough output — mints 50 FOO but output only has 100. +// Expected output is 100 (input) + 50 (mint) = 150, but output only has 100. +test transfer_act_mint_insufficient_output_fails() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [ + fixture.some_input_with_programmable_tokens( + [(test_mint_policy, "FOO", 100)], + "mint fail input", + ), + ], + mint: from_asset(test_mint_policy, "FOO", 50), + outputs: [ + // Only 100 FOO, but expected 150 + fixture.some_output_with_programmable_tokens( + [(test_mint_policy, "FOO", 100)], + ), + ], + reference_inputs: [ + fixture.some_reference_input_protocol_params( + fixture.protocol_params, + "protocol params", + ), + fixture.some_reference_input_tokens_registry_node( + test_mint_policy, + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "registry node", + ), + ], + withdrawals: [ + Pair(fixture.credential_validator_logic_global, 0), + Pair(fixture.credential_transfer_logic, 0), + ], + } + + let redeemer = TransferAct { proofs: [TokenExists { node_idx: 1 }] } + + programmable_logic_global.programmable_logic_global.withdraw( + fixture.policy_protocol_params, + redeemer, + fixture.credential_validator_logic_global, + tx, + ) +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/protocol_params_mint.ak b/src/programmable-tokens/aiken-workspace-standard/validators/protocol_params_mint.ak new file mode 100644 index 0000000..770224f --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/protocol_params_mint.ak @@ -0,0 +1,70 @@ +use aiken/collection/list +// Protocol Parameters Minting Policy +// One-shot minting policy for the protocol parameters NFT +// Migrated from SmartTokens.Contracts.ProtocolParams +// +// This validator ensures: +// 1. The specified UTXO is consumed (one-shot minting) +// 2. Exactly one token is minted with the correct token name +// 3. The token is sent to an always-fail address with an inline datum containing ProgrammableLogicGlobalParams + +use cardano/address +use cardano/assets.{PolicyId, flatten} +use cardano/transaction.{InlineDatum, OutputReference, Transaction} +use programmable_logic/params.{ + ProgrammableLogicGlobalParams, protocol_params_token, +} + +validator protocol_params_mint( + utxo_ref: OutputReference, + always_fail_hash: ByteArray, +) { + mint(_redeemer: Data, own_policy: PolicyId, self: Transaction) { + trace @"Starting protocol_params_mint validation" + // Check 1: This is a one-shot minting policy - must spend the specified UTXO + let consumed = + list.any(self.inputs, fn(input) { input.output_reference == utxo_ref }) + + // Check 2: Get all minted tokens from this policy + let minted_tokens = flatten(self.mint) + let own_minted = + list.filter( + minted_tokens, + fn(token) { + let (cs, _tn, _qty) = token + cs == own_policy + }, + ) + + // Must mint exactly one token with the correct name and quantity + expect [(_, tn, qty)] = own_minted + + // The always-fail address where the NFT must be locked + let expected_address = address.from_script(always_fail_hash) + // Find the output at the always-fail address + expect Some(nft_output) = + list.find( + self.outputs, + fn(output) { + assets.has_nft_strict(output.value, own_policy, protocol_params_token) + }, + ) + expect InlineDatum(datum) = nft_output.datum + expect _params: ProgrammableLogicGlobalParams = datum + + and { + // One-shot: the specified UTxO must be consumed + consumed?, + // Token name must be "ProtocolParams" + (tn == protocol_params_token)?, + // Exactly 1 token minted + (qty == 1)?, + // NFT must be locked at the always-fail address + (nft_output.address == expected_address)?, + } + } + + else(_) { + fail + } +} diff --git a/src/programmable-tokens/aiken-workspace-standard/validators/protocol_params_mint.test.ak b/src/programmable-tokens/aiken-workspace-standard/validators/protocol_params_mint.test.ak new file mode 100644 index 0000000..2ea1891 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-standard/validators/protocol_params_mint.test.ak @@ -0,0 +1,308 @@ +//// Comprehensive tests for the protocol_params_mint validator. +//// +//// Tests invoke the validator's mint handler directly, covering: +//// - Happy path (all conditions met) +//// - One-shot UTxO consumption +//// - Token name and quantity checks +//// - Always-fail address enforcement +//// - InlineDatum and ProgrammableLogicGlobalParams datum validation + +use cardano/address.{Address, Script, VerificationKey} +use cardano/assets.{PolicyId, ada_asset_name, ada_policy_id, from_asset} +use cardano/transaction.{ + InlineDatum, Input, NoDatum, Output, OutputReference, Transaction, +} +use programmable_logic/params.{ + ProgrammableLogicGlobalParams, protocol_params_token, +} +use protocol_params_mint + +// --------------------------------------------------------------------------- +// Test constants +// --------------------------------------------------------------------------- + +const test_utxo_ref = + OutputReference { + transaction_id: #"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + output_index: 0, + } + +const test_always_fail_hash: ByteArray = + #"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +const test_policy_id: PolicyId = + #"cccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + +const test_registry_node_cs: PolicyId = + #"dddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + +const test_prog_logic_hash: ByteArray = + #"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + +const min_ada = + assets.zero + |> assets.add(ada_policy_id, ada_asset_name, 2_000_000) + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn always_fail_address() -> Address { + Address { + payment_credential: Script(test_always_fail_hash), + stake_credential: None, + } +} + +fn valid_params_datum() -> Data { + as_data( + ProgrammableLogicGlobalParams { + registry_node_cs: test_registry_node_cs, + prog_logic_cred: Script(test_prog_logic_hash), + }, + ) +} + +fn valid_nft_output() -> Output { + Output { + address: always_fail_address(), + value: min_ada + |> assets.add(test_policy_id, protocol_params_token, 1), + datum: InlineDatum(valid_params_datum()), + reference_script: None, + } +} + +fn valid_input() -> Input { + Input { + output_reference: test_utxo_ref, + output: Output { + address: Address { + payment_credential: VerificationKey( + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ), + stake_credential: None, + }, + value: min_ada, + datum: NoDatum, + reference_script: None, + }, + } +} + +fn valid_transaction() -> Transaction { + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, protocol_params_token, 1), + outputs: [valid_nft_output()], + } +} + +fn call_validator( + utxo_ref: OutputReference, + af_hash: ByteArray, + tx: Transaction, +) -> Bool { + protocol_params_mint.protocol_params_mint.mint( + utxo_ref, + af_hash, + as_data(Void), + test_policy_id, + tx, + ) +} + +// ======================================================================== +// Happy path — all conditions met +// ======================================================================== + +test protocol_params_mint_valid() { + call_validator(test_utxo_ref, test_always_fail_hash, valid_transaction()) +} + +// ======================================================================== +// One-shot UTxO not consumed +// ======================================================================== + +test fails_when_utxo_not_consumed() fail { + let wrong_utxo_ref = + OutputReference { + transaction_id: #"1111111111111111111111111111111111111111111111111111111111111111", + output_index: 99, + } + call_validator(wrong_utxo_ref, test_always_fail_hash, valid_transaction()) +} + +// ======================================================================== +// Wrong token name — the `tn == protocol_params_token` check +// +// We mint "WrongName" but keep the output NFT as "ProtocolParams" so +// has_nft_strict and datum checks pass; only the token-name check fails. +// ======================================================================== + +test fails_with_wrong_token_name() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, "WrongName", 1), + outputs: [valid_nft_output()], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// Quantity != 1 — the `qty == 1` check +// +// We mint qty=2 but keep the output with qty=1 so has_nft_strict passes; +// only the quantity check in the and-block fails. +// ======================================================================== + +test fails_when_quantity_is_not_one() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, protocol_params_token, 2), + outputs: [valid_nft_output()], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// Multiple token names minted under the same policy +// +// The `expect [(_, tn, qty)] = own_minted` pattern crashes because +// the list has 2 elements instead of 1. +// ======================================================================== + +test fails_with_multiple_token_names_minted() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, protocol_params_token, 1) + |> assets.add(test_policy_id, "ExtraToken", 1), + outputs: [valid_nft_output()], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// No tokens minted from own policy +// +// The mint contains a different policy entirely, so `own_minted` is empty +// and `expect [(_, tn, qty)]` crashes. +// ======================================================================== + +test fails_when_no_tokens_minted_from_own_policy() fail { + let other_policy: PolicyId = + #"1111111111111111111111111111111111111111111111111111111111" + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(other_policy, "SomeToken", 1), + outputs: [valid_nft_output()], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// No output at the always-fail address +// +// The NFT goes to a completely different address. +// `expect Some(nft_output) = list.find(...)` crashes because no output +// matches the expected always-fail address. +// ======================================================================== + +test fails_when_no_output_at_always_fail_address() fail { + let wrong_address = + Address { + payment_credential: Script( + #"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ), + stake_credential: None, + } + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, protocol_params_token, 1), + outputs: [ + Output { + address: wrong_address, + value: min_ada + |> assets.add(test_policy_id, protocol_params_token, 1), + datum: InlineDatum(valid_params_datum()), + reference_script: None, + }, + ], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// Output at always-fail address exists but does NOT contain the NFT +// +// `has_nft_strict(nft_output.value, own_policy, ...)` fails because +// the output at the always-fail address has only ADA. +// ======================================================================== + +test fails_when_nft_missing_from_always_fail_output() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, protocol_params_token, 1), + outputs: [ + // Output at always-fail address but WITHOUT the NFT + Output { + address: always_fail_address(), + value: min_ada, + datum: InlineDatum(valid_params_datum()), + reference_script: None, + }, + ], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// No inline datum on the always-fail output +// +// `expect InlineDatum(datum) = nft_output.datum` crashes because the +// output uses NoDatum. +// ======================================================================== + +test fails_when_no_inline_datum() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, protocol_params_token, 1), + outputs: [Output { ..valid_nft_output(), datum: NoDatum }], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} + +// ======================================================================== +// Invalid datum type — datum is not ProgrammableLogicGlobalParams +// +// `expect _params: ProgrammableLogicGlobalParams = datum` crashes because +// an Int doesn't match the ProgrammableLogicGlobalParams constructor. +// ======================================================================== + +test fails_with_invalid_datum_type() fail { + let tx = + Transaction { + ..transaction.placeholder, + inputs: [valid_input()], + mint: from_asset(test_policy_id, protocol_params_token, 1), + outputs: [ + Output { ..valid_nft_output(), datum: InlineDatum(as_data(42)) }, + ], + } + call_validator(test_utxo_ref, test_always_fail_hash, tx) +} diff --git a/src/programmable-tokens/aiken-workspace/validators/registry_mint.ak b/src/programmable-tokens/aiken-workspace-standard/validators/registry_mint.ak similarity index 92% rename from src/programmable-tokens/aiken-workspace/validators/registry_mint.ak rename to src/programmable-tokens/aiken-workspace-standard/validators/registry_mint.ak index 757976b..459a260 100644 --- a/src/programmable-tokens/aiken-workspace/validators/registry_mint.ak +++ b/src/programmable-tokens/aiken-workspace-standard/validators/registry_mint.ak @@ -1,132 +1,131 @@ -use aiken/collection/dict -use aiken/collection/list -use aiken/primitive/bytearray -// Registry minting policy - manages the linked list of registered programmable token policies -// Migrated from SmartTokens.LinkedList.MintDirectory -// -// This validator maintains a registry of programmable token policies. -// Each entry must be a valid programmable token that passes registration checks. - -use cardano/assets.{PolicyId} -use cardano/transaction.{OutputReference, Transaction} -use linked_list.{ - collect_node_ios, is_inserted_directory_node, is_updated_directory_node, - validate_directory_init, validate_directory_node_output, -} -use types.{ - RegistryInit, RegistryInsert, RegistryRedeemer, RegistryNode, - IssuanceCborHex, -} -use utils.{bytearray_lt, - expect_inline_datum, is_programmable_token_registration} - -validator registry_mint( - utxo_ref: OutputReference, - issuance_cbor_hex_cs: PolicyId, -) { - mint(redeemer: RegistryRedeemer, policy_id: PolicyId, self: Transaction) { - trace @"Starting registry_mint validation" - let (node_inputs, node_outputs) = collect_node_ios(self, policy_id) - - when redeemer is { - RegistryInit -> { - // Ensure this is a one-shot minting policy by checking that utxo_ref is spent - let is_utxo_consumed = - list.any( - self.inputs, - fn(input) { input.output_reference == utxo_ref }, - ) - - // Initialize the registry with an empty origin node - and { - is_utxo_consumed?, - validate_directory_init( - node_inputs, - node_outputs, - self.mint, - policy_id, - )?, - } - } - RegistryInsert { key, hashed_param } -> { - // Find the issuance CBOR hex reference input - expect Some(issuance_ref_input) = - //should this be a find.any instead of find.some? - list.find( - self.reference_inputs, - fn(input) { - let tokens = - assets.tokens(input.output.value, issuance_cbor_hex_cs) - !dict.is_empty(tokens) - }, - ) - - // Extract the IssuanceCborHex datum - let issuance_datum = expect_inline_datum(issuance_ref_input.output) - expect issuance_cbor: IssuanceCborHex = issuance_datum - - // Validate that the key being inserted is a valid programmable token - // This checks: - // 1. The computed policy ID (from prefix + hashed_param + postfix) matches the key - // 2. Tokens with this policy ID are being minted in this transaction - let is_token_registered = - is_programmable_token_registration( - key, - issuance_cbor.prefix_cbor_hex, - issuance_cbor.postfix_cbor_hex, - hashed_param, - self.mint, - ) - - // Must have exactly one node input (the covering node) - expect [covering_input] = node_inputs - let covering_datum = expect_inline_datum(covering_input.output) - expect covering_node: RegistryNode = covering_datum - - // Must mint exactly one node token with the new key - let just_single_mint = assets.has_nft(self.mint, policy_id, key) - - // Parse the outputs to find the two new nodes - let output_nodes = - list.map( - node_outputs, - fn(out) { validate_directory_node_output(out, policy_id) }, - ) - - // One output must be the updated covering node (key -> insert_key) - let registry_node_updated = - list.any( - output_nodes, - fn(node) { is_updated_directory_node(node, covering_node.key, key) }, - ) - - // One output must be the new inserted node (insert_key -> next) - let registry_node_inserted = - list.any( - output_nodes, - fn(node) { - is_inserted_directory_node(node, key, covering_node.next) - }, - ) - - and { - is_token_registered?, - // Validate key is a valid currency symbol (28 bytes) - (bytearray.length(key) == 28)?, - // The covering node must cover the key to insert - bytearray_lt(covering_node.key, key)?, - bytearray_lt(key, covering_node.next)?, - just_single_mint?, - // Must have exactly 2 outputs: the updated covering node and the new inserted node - (list.length(output_nodes) == 2)?, - registry_node_updated?, - registry_node_inserted?, - } - } - } - } - - else(_) { - fail - } -} +use aiken/collection/dict +use aiken/collection/list +use aiken/primitive/bytearray +// Registry minting policy - manages the linked list of registered programmable token policies +// Migrated from SmartTokens.LinkedList.MintDirectory +// +// This validator maintains a registry of programmable token policies. +// Each entry must be a valid programmable token that passes registration checks. + +use cardano/assets.{PolicyId} +use cardano/transaction.{OutputReference, Transaction} +use linked_list.{ + collect_node_ios, is_inserted_directory_node, is_updated_directory_node, + validate_directory_init, validate_directory_node_output, +} +use registry_node.{RegistryNode} +use types.{IssuanceCborHex, RegistryInit, RegistryInsert, RegistryRedeemer} +use utils.{ + bytearray_lt, expect_inline_datum, is_programmable_token_registration, +} + +validator registry_mint( + utxo_ref: OutputReference, + issuance_cbor_hex_cs: PolicyId, +) { + mint(redeemer: RegistryRedeemer, policy_id: PolicyId, self: Transaction) { + trace @"Starting registry_mint validation" + let (node_inputs, node_outputs) = collect_node_ios(self, policy_id) + + when redeemer is { + RegistryInit -> { + // Ensure this is a one-shot minting policy by checking that utxo_ref is spent + let is_utxo_consumed = + list.any( + self.inputs, + fn(input) { input.output_reference == utxo_ref }, + ) + + // Initialize the registry with an empty origin node + and { + is_utxo_consumed?, + validate_directory_init( + node_inputs, + node_outputs, + self.mint, + policy_id, + )?, + } + } + RegistryInsert { key, hashed_param } -> { + // Find the issuance CBOR hex reference input + expect Some(issuance_ref_input) = + //should this be a find.any instead of find.some? + list.find( + self.reference_inputs, + fn(input) { + let tokens = + assets.tokens(input.output.value, issuance_cbor_hex_cs) + !dict.is_empty(tokens) + }, + ) + + // Extract the IssuanceCborHex datum + let issuance_datum = expect_inline_datum(issuance_ref_input.output) + expect issuance_cbor: IssuanceCborHex = issuance_datum + + // Validate that the key being inserted is a valid programmable token + // This checks: + // 1. The computed policy ID (from prefix + hashed_param + postfix) matches the key + // 2. Tokens with this policy ID are being minted in this transaction + let is_token_registered = + is_programmable_token_registration( + key, + issuance_cbor.prefix_cbor_hex, + issuance_cbor.postfix_cbor_hex, + hashed_param, + self.mint, + ) + + // Must have exactly one node input (the covering node) + expect [covering_input] = node_inputs + let covering_datum = expect_inline_datum(covering_input.output) + expect covering_node: RegistryNode = covering_datum + + // Must mint exactly one node token with the new key + let just_single_mint = assets.has_nft(self.mint, policy_id, key) + + // Parse the outputs to find the two new nodes + let output_nodes = + list.map( + node_outputs, + fn(out) { validate_directory_node_output(out, policy_id) }, + ) + + // One output must be the updated covering node (key -> insert_key) + let registry_node_updated = + list.any( + output_nodes, + fn(node) { is_updated_directory_node(node, covering_node, key) }, + ) + + // One output must be the new inserted node (insert_key -> next) + let registry_node_inserted = + list.any( + output_nodes, + fn(node) { + is_inserted_directory_node(node, key, covering_node.next) + }, + ) + + and { + is_token_registered?, + // Validate key is a valid currency symbol (28 bytes) + (bytearray.length(key) == 28)?, + // The covering node must cover the key to insert + bytearray_lt(covering_node.key, key)?, + bytearray_lt(key, covering_node.next)?, + just_single_mint?, + // Must have exactly 2 outputs: the updated covering node and the new inserted node + (list.length(output_nodes) == 2)?, + registry_node_updated?, + registry_node_inserted?, + } + } + } + } + + else(_) { + fail + } +} diff --git a/src/programmable-tokens/aiken-workspace/validators/registry_spend.ak b/src/programmable-tokens/aiken-workspace-standard/validators/registry_spend.ak similarity index 94% rename from src/programmable-tokens/aiken-workspace/validators/registry_spend.ak rename to src/programmable-tokens/aiken-workspace-standard/validators/registry_spend.ak index 6e31cb4..6fcad0d 100644 --- a/src/programmable-tokens/aiken-workspace/validators/registry_spend.ak +++ b/src/programmable-tokens/aiken-workspace-standard/validators/registry_spend.ak @@ -1,57 +1,57 @@ -use aiken/collection/list -// Registry spending validator - allows spending registry node UTxOs -// Migrated from SmartTokens.LinkedList.SpendDirectory (pmkDirectorySpending) -// -// This validator locks all registry node UTxOs. It allows spending them -// only when registry node NFTs are being minted, which ensures that -// the registry structure is being properly maintained. - -use cardano/assets.{PolicyId, flatten} -use cardano/transaction.{Transaction} -use types.{ProgrammableLogicGlobalParams} -use utils.{expect_inline_datum, has_currency_symbol} - -validator registry_spend(protocol_params_cs: PolicyId) { - spend( - _datum: Option, - _redeemer: Data, - _own_ref: Data, - self: Transaction, - ) { - trace @"Starting registry_spend validation" - - // Find the protocol params UTxO in reference inputs - // This contains the registry_node_cs we need to validate against - expect Some(params_ref_input) = - list.find( - self.reference_inputs, - fn(input) { - has_currency_symbol(input.output.value, protocol_params_cs) - }, - ) - - // Extract the protocol params datum - let params_datum = expect_inline_datum(params_ref_input.output) - expect params: ProgrammableLogicGlobalParams = params_datum - - let registry_node_cs = params.registry_node_cs - - // Check that registry node NFTs are being minted or burned in this transaction - // This ensures we're modifying the registry structure (inserting/removing nodes) - let minting_registry_nodes = - list.any( - flatten(self.mint), - fn(asset) { - let (policy, _tn, amt) = asset - policy == registry_node_cs && amt != 0 - }, - ) - - // Validation passes if registry NFTs are being minted/burned - minting_registry_nodes? - } - - else(_) { - fail - } -} +use aiken/collection/list +// Registry spending validator - allows spending registry node UTxOs +// Migrated from SmartTokens.LinkedList.SpendDirectory (pmkDirectorySpending) +// +// This validator locks all registry node UTxOs. It allows spending them +// only when registry node NFTs are being minted, which ensures that +// the registry structure is being properly maintained. + +use cardano/assets.{PolicyId, flatten} +use cardano/transaction.{Transaction} +use programmable_logic/params.{ProgrammableLogicGlobalParams} +use utils.{expect_inline_datum, has_currency_symbol} + +validator registry_spend(protocol_params_cs: PolicyId) { + spend( + _datum: Option, + _redeemer: Data, + _own_ref: Data, + self: Transaction, + ) { + trace @"Starting registry_spend validation" + + // Find the protocol params UTxO in reference inputs + // This contains the registry_node_cs we need to validate against + expect Some(params_ref_input) = + list.find( + self.reference_inputs, + fn(input) { + has_currency_symbol(input.output.value, protocol_params_cs) + }, + ) + + // Extract the protocol params datum + let params_datum = expect_inline_datum(params_ref_input.output) + expect params: ProgrammableLogicGlobalParams = params_datum + + let registry_node_cs = params.registry_node_cs + + // Check that registry node NFTs are being minted or burned in this transaction + // This ensures we're modifying the registry structure (inserting/removing nodes) + let minting_registry_nodes = + list.any( + flatten(self.mint), + fn(asset) { + let (policy, _tn, amt) = asset + policy == registry_node_cs && amt != 0 + }, + ) + + // Validation passes if registry NFTs are being minted/burned + minting_registry_nodes? + } + + else(_) { + fail + } +} diff --git a/src/programmable-tokens/aiken-workspace-subStandard/dummy/aiken.lock b/src/programmable-tokens/aiken-workspace-subStandard/dummy/aiken.lock new file mode 100644 index 0000000..3195130 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/dummy/aiken.lock @@ -0,0 +1,15 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "v2.2.0" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "v2.2.0" +requirements = [] +source = "github" + +[etags] diff --git a/src/programmable-tokens/aiken-workspace-subStandard/dummy/aiken.toml b/src/programmable-tokens/aiken-workspace-subStandard/dummy/aiken.toml new file mode 100644 index 0000000..aee0750 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/dummy/aiken.toml @@ -0,0 +1,18 @@ +name = "wsc-poc/aiken-policy" +version = "0.0.0" +compiler = "v1.1.19" +plutus = "v3" +license = "Apache-2.0" +description = "Aiken contracts for project 'wsc-poc/aiken-policy'" + +[repository] +user = "wsc-poc" +project = "aiken-policy" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v2.2.0" +source = "github" + +[config] diff --git a/src/programmable-tokens/aiken-workspace-subStandard/dummy/plutus.json b/src/programmable-tokens/aiken-workspace-subStandard/dummy/plutus.json new file mode 100644 index 0000000..e5ddc82 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/dummy/plutus.json @@ -0,0 +1,58 @@ +{ + "preamble": { + "title": "wsc-poc/aiken-policy", + "description": "Aiken contracts for project 'wsc-poc/aiken-policy'", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.19+e525483" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "transfer.issue.withdraw", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/Int" + } + }, + "compiledCode": "585701010029800aba2aba1aab9eaab9dab9a4888896600264646644b30013370e900218031baa00289919b87375a6012008906400980418039baa0028a504014600c600e002600c004600c00260066ea801a29344d9590011", + "hash": "a82718805c3541469346431c0cc023a76afce8d6d2c1c64d00bf1950" + }, + { + "title": "transfer.issue.else", + "redeemer": { + "schema": {} + }, + "compiledCode": "585701010029800aba2aba1aab9eaab9dab9a4888896600264646644b30013370e900218031baa00289919b87375a6012008906400980418039baa0028a504014600c600e002600c004600c00260066ea801a29344d9590011", + "hash": "a82718805c3541469346431c0cc023a76afce8d6d2c1c64d00bf1950" + }, + { + "title": "transfer.transfer.withdraw", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/Int" + } + }, + "compiledCode": "585701010029800aba2aba1aab9eaab9dab9a4888896600264646644b30013370e900218031baa00289919b87375a6012008904801980418039baa0028a504014600c600e002600c004600c00260066ea801a29344d9590011", + "hash": "93fd90884c772ced27987503f9d37c857372b99cf5cc716197ebb8bd" + }, + { + "title": "transfer.transfer.else", + "redeemer": { + "schema": {} + }, + "compiledCode": "585701010029800aba2aba1aab9eaab9dab9a4888896600264646644b30013370e900218031baa00289919b87375a6012008904801980418039baa0028a504014600c600e002600c004600c00260066ea801a29344d9590011", + "hash": "93fd90884c772ced27987503f9d37c857372b99cf5cc716197ebb8bd" + } + ], + "definitions": { + "Int": { + "dataType": "integer" + } + } +} \ No newline at end of file diff --git a/src/programmable-tokens/aiken-workspace-subStandard/dummy/validators/transfer.ak b/src/programmable-tokens/aiken-workspace-subStandard/dummy/validators/transfer.ak new file mode 100644 index 0000000..17dada4 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/dummy/validators/transfer.ak @@ -0,0 +1,24 @@ +use cardano/address.{Credential} +use cardano/transaction.{Transaction} + +validator issue { + withdraw(redeemer: Int, _account: Credential, _self: Transaction) { + trace @"withdraw" + redeemer == 100 + } + else(_) { + trace @"fallback" + False + } +} + +validator transfer { + withdraw(redeemer: Int, _account: Credential, _self: Transaction) { + trace @"withdraw" + redeemer == 200 + } + else(_) { + trace @"fallback" + False + } +} diff --git a/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/.gitignore b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/.gitignore new file mode 100644 index 0000000..ff7811b --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/.gitignore @@ -0,0 +1,6 @@ +# Aiken compilation artifacts +artifacts/ +# Aiken's project working directory +build/ +# Aiken's default documentation export +docs/ diff --git a/src/programmable-tokens/aiken-workspace/aiken.lock b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/aiken.lock similarity index 100% rename from src/programmable-tokens/aiken-workspace/aiken.lock rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/aiken.lock diff --git a/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/aiken.toml b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/aiken.toml new file mode 100644 index 0000000..cdfc71a --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/aiken.toml @@ -0,0 +1,18 @@ +name = "cip113/freeze-unfreeze" +version = "0.0.0" +compiler = "v1.1.21" +plutus = "v3" +license = "Apache-2.0" +description = "CIP113 Sub Standard for freeze/unfreeze capabilities" + +[repository] +user = "cip113" +project = "freeze-unfreeze" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[config] diff --git a/src/programmable-tokens/aiken-workspace/lib/linked_list.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/linked_list.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/lib/linked_list.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/linked_list.ak diff --git a/src/programmable-tokens/aiken-workspace/lib/linked_list_test.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/linked_list_test.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/lib/linked_list_test.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/linked_list_test.ak diff --git a/src/programmable-tokens/aiken-workspace/lib/types.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/types.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/lib/types.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/types.ak diff --git a/src/programmable-tokens/aiken-workspace/lib/types_test.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/types_test.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/lib/types_test.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/types_test.ak diff --git a/src/programmable-tokens/aiken-workspace/lib/utils.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/utils.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/lib/utils.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/utils.ak diff --git a/src/programmable-tokens/aiken-workspace/lib/utils_test.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/utils_test.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/lib/utils_test.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/lib/utils_test.ak diff --git a/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/plutus.json b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/plutus.json new file mode 100644 index 0000000..b9807e3 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/plutus.json @@ -0,0 +1,318 @@ +{ + "preamble": { + "title": "cip113/freeze-unfreeze", + "description": "CIP113 Sub Standard for freeze/unfreeze capabilities", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.21+42babe5" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "blacklist_mint.blacklist_mint.mint", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1BlacklistRedeemer" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "manager_pkh", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "590e9e0101002229800aba4aba2aba1aba0aab9faab9eaab9dab9cab9a9bae0024888888888a60022a6600692123657870656374205b6e6f64655f6f75747075745d203d206e6f64655f6f75747075747300168a99801a4924657870656374205b5061697228746e2c20717479295d203d20746f6b656e5f706169727300168a99801a493465787065637420636f766572696e675f6e6f64653a20426c61636b6c6973744e6f6465203d20636f766572696e675f646174756d00168a99801a4925657870656374205b636f766572696e675f696e7075745d203d206e6f64655f696e7075747300168a99801a4927657870656374205b6f75747075745f6e6f64655f6f75745d203d206e6f64655f6f75747075747300168a99801a4926657870656374205b6e6f64655f612c206e6f64655f625d203d20696e7075745f646174756d7300168a99801a4922657870656374206e6f64653a20426c61636b6c6973744e6f6465203d20646174756d00168a99801a491b72656465656d65723a20426c61636b6c69737452656465656d657200164888888896600264653001301200198091809800cdc3a400130120024888966002600460246ea800e264b30010058999119912cc004c01c0062b30013018375401300280620328acc004c010006264b3001001806c4c96600200300e807403a01d13259800980f801c01601e80e0dd7000a03e301c001406860306ea80262b300130030018992cc00400601b13259800800c03a01d00e80744c966002603e007005807a038375c00280f8c07000501a180c1baa009806202a405480a8660024603460360032301a301b301b0019b804800a460346036603660366036003223232330010010042259800800c00e2646644b30013372200e00515980099b8f0070028800c01901b44cc014014c08801101b1bae301b00137566038002603c00280e052f5bded8c1222598009804180c1baa0038992cc00400600513259800800c00e007003801c4cc89660020030058992cc00400600d006803401a264b30013023003804401d0201bae001408c604000280f0dd7000980f8012040301d001406c60326ea800e00280b244646600200200644b30010018a5eb822660386006603a00266004004603c00280da46034603660366036603660366036603660360032232330010010032259800800c528456600260066eb8c0740062946266004004603c00280b901b4c054dd5003c88c8cc00400400c896600200314a11598009801980e800c528c4cc008008c078005017203648888888888a600266446644b3001002899813a60101800033027374e00297ae08992cc004c8cc00400400c896600200314a315980099baf302b302837546056002007133002002302c0018a504094814a2660506e9c00ccc0a0dd380125eb822a66048921776578706563740a20202020202020206c6973742e616c6c280a202020202020202020206e6f64655f6f7574707574732c0a20202020202020202020666e286f757470757429207b206f75747075742e61646472657373203d3d2065787065637465645f61646472657373207d2c0a2020202020202020290016408c6050604a6ea8c03cc094dd51814001204c330023758604c60466ea80448cc008dd5980718121baa300e30243754002022660046eb0c030c08cdd50089198011bab300e3024375400202244646600200200644b30010018a5eb8226644b3001300500289981480119802002000c4cc01001000502418140009814800a04c2232330010010032259800800c52844c96600266e44010006266e3c01000626600600660540048118dd718121814000a04c912cc0040062900044c02ccc008008c09c005024489660020031480022601666004004604e0028122464b3001300e30223754003130263023375400315330214912465787065637420496e6c696e65446174756d286429203d206f75747075742e646174756d00164080601660446ea80064446466446600400400244b3001001801c4c8cc896600266e4401c00a2b30013371e00e0051001803204e8998028029817002204e375c604e0026eb4c0a0004c0a8005028198058020018a4000911114c004dd61814802cdd618149815002cc010012600600722230159800801c00a0028021222223259800980d806c566002660186eb0c0bcc0b0dd500d119baf3030302d375400206b159800992cc00401a051132598009819003c4c966002b30010098a518a5040c115980099b8f375c6064605e6ea8005221008acc004cdc79bae3019302f375400291011effffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008cc00400e039488100401514a08162294102c452820583300300101b814a05e303000640b86eacc04cc0b0dd500d4528c54cc0a924015076616c69646174655f626c61636b6c6973745f696e6974286e6f64655f696e707574732c206e6f64655f6f7574707574732c2073656c662e6d696e742c20706f6c6963795f696429203f2046616c73650014a0814a2a660549217d6578706563740a202020202020202020206c6973742e616e79280a20202020202020202020202073656c662e696e707574732c0a202020202020202020202020666e28696e70757429207b20696e7075742e6f75747075745f7265666572656e6365203d3d207574786f5f726566207d2c0a2020202020202020202029001640a5159800980c006c4cc89660020110268992cc004c0cc026264b30010018cc004006264b30015980099b87371a00a901c4528c54cc0bd2412e6275696c74696e2e6c656e6774685f6f665f627974656172726179286b657929203d3d203238203f2046616c73650014a081722b3001598008024528c54cc0bd2401166d616e616765725f7369676e6564203f2046616c73650014a081722b300159800cc004dd5980c18189baa01f80f40150074528c54cc0bd2401186a7573745f73696e676c655f6d696e74203f2046616c73650014a081722b30015980099b90375c606860626ea800801629462a6605e9212c6279746561727261795f6c7428636f766572696e675f6e6f64652e6b65792c206b657929203f2046616c73650014a081722b30015980099b90005375c603660626ea800a29462a6605e9212d6279746561727261795f6c74286b65792c20636f766572696e675f6e6f64652e6e65787429203f2046616c73650014a081722b300159800980e1804800c528c54cc0bd2401266c6973742e6c656e677468286f75747075745f6e6f64657329203d3d2032203f2046616c73650014a081722b3001598009980880092cc004cdc79bae3035303237540026eb8c0d4c0c8dd5001c4cdc79bae301c3032375400200d14a0817a29462a6605e92011e626c61636b6c6973745f6e6f64655f75706461746564203f2046616c73650014a081722b30013301100125980099b8f375c606a60646ea800401a266e3cdd7180e18191baa001375c603860646ea800e294102f4528c54cc0bd2411f626c61636b6c6973745f6e6f64655f696e736572746564203f2046616c73650014a08172294102e4528205c8a5040b914a08172294102e4528205c8a5040b86602801246600c00203d0294055029814c0a605281a8c02cc064c0bcdd5000c09d0301818804205e375c605e60586ea8074cc038dd6180798161baa01a23371e002053198009bae302f302c375403b3300e3758601e60586ea80688cdc7800814ccc0400188c96600200319800800c4006046809a047023811c08d0331804980b98169baa001488966002003025899912cc00400604f13259800981b00145660020170298992cc004c0dc032264b300159800803c528c54cc0c92401166d616e616765725f7369676e6564203f2046616c73650014a0818a2b30015980099b8798009bab301b30343754045021804201e4800629462a66064921186a7573745f73696e676c655f6275726e203f2046616c73650014a0818a2b3001301f300c00e8acc0056600266e3cdd7181b981a1baa0050088acc004cdc79bae3037303437540026eb8c0dcc0d0dd5001c56600266e3cdd7180f181a1baa001375c603c60686ea8016266e3cdd7180f181a1baa0030088a5040c514a0818a2b30013371e6eb8c0dcc0d0dd5001804456600266e3cdd7181b981a1baa001375c606e60686ea80162b30013371e6eb8c078c0d0dd50009bae301e3034375400713371e6eb8c078c0d0dd5002804452820628a5040c515330324901274e65697468657220696e707574206d61746368657320746865206b657920746f2072656d6f7665001640c4818a29462a660649210e636865636b73203f2046616c73650014a0818a2941031452820628a5040c46601000204102a40d0606a016819a0508198c0d000503218190009819800a060205240a44464b3001301932330010010022259800800c52000899914c004dd718188014dd598190014896600200310038991991180f99802802981d8021bae3034001375a606a002606e00281a922233001001002181980099801001181a000a0628992cc004c06cc018006264b30010018cc0040062b3001337206eb8c0ccc0c0dd50009bae301a3030375400315980080140aa264b300130350038992cc004c078dd698190014566002b30013371e0026eb8c0d4c0c8dd5001c528c4cdc7800a4410040bd10038a998182492d65787065637420746e203d3d206e6f64652e6b6579207c7c20746e203d3d206f726967696e5f6e6f64655f746e001640bd15330304910f65787065637420717479203d3d2031001640bc6eb8c0c00060568190c0cc009031454cc0b9240128657870656374206279746561727261795f6c74286e6f64652e6b65792c206e6f64652e6e65787429001640b50254055025812c09604a81a8c02c0122a6605a92012265787065637420646963742e73697a65286e6f64655f746f6b656e7329203d3d2031001640b066028002005153302c49012665787065637420636f756e745f756e697175655f746f6b656e732876616c756529203d3d2032001640ac6eacc05cc0b4dd500103015375400e6e1d2002370e900240220110088042032375c602c60266ea800e2c80806024002601a6ea804e293454cc02d2411856616c696461746f722072657475726e65642066616c7365001365640281", + "hash": "1693ffa822bd92dd0f194e0d09d5e2728663d07463c03bf6b074c7fe" + }, + { + "title": "blacklist_mint.blacklist_mint.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "manager_pkh", + "schema": { + "$ref": "#/definitions/ByteArray" + } + } + ], + "compiledCode": "590e9e0101002229800aba4aba2aba1aba0aab9faab9eaab9dab9cab9a9bae0024888888888a60022a6600692123657870656374205b6e6f64655f6f75747075745d203d206e6f64655f6f75747075747300168a99801a4924657870656374205b5061697228746e2c20717479295d203d20746f6b656e5f706169727300168a99801a493465787065637420636f766572696e675f6e6f64653a20426c61636b6c6973744e6f6465203d20636f766572696e675f646174756d00168a99801a4925657870656374205b636f766572696e675f696e7075745d203d206e6f64655f696e7075747300168a99801a4927657870656374205b6f75747075745f6e6f64655f6f75745d203d206e6f64655f6f75747075747300168a99801a4926657870656374205b6e6f64655f612c206e6f64655f625d203d20696e7075745f646174756d7300168a99801a4922657870656374206e6f64653a20426c61636b6c6973744e6f6465203d20646174756d00168a99801a491b72656465656d65723a20426c61636b6c69737452656465656d657200164888888896600264653001301200198091809800cdc3a400130120024888966002600460246ea800e264b30010058999119912cc004c01c0062b30013018375401300280620328acc004c010006264b3001001806c4c96600200300e807403a01d13259800980f801c01601e80e0dd7000a03e301c001406860306ea80262b300130030018992cc00400601b13259800800c03a01d00e80744c966002603e007005807a038375c00280f8c07000501a180c1baa009806202a405480a8660024603460360032301a301b301b0019b804800a460346036603660366036003223232330010010042259800800c00e2646644b30013372200e00515980099b8f0070028800c01901b44cc014014c08801101b1bae301b00137566038002603c00280e052f5bded8c1222598009804180c1baa0038992cc00400600513259800800c00e007003801c4cc89660020030058992cc00400600d006803401a264b30013023003804401d0201bae001408c604000280f0dd7000980f8012040301d001406c60326ea800e00280b244646600200200644b30010018a5eb822660386006603a00266004004603c00280da46034603660366036603660366036603660360032232330010010032259800800c528456600260066eb8c0740062946266004004603c00280b901b4c054dd5003c88c8cc00400400c896600200314a11598009801980e800c528c4cc008008c078005017203648888888888a600266446644b3001002899813a60101800033027374e00297ae08992cc004c8cc00400400c896600200314a315980099baf302b302837546056002007133002002302c0018a504094814a2660506e9c00ccc0a0dd380125eb822a66048921776578706563740a20202020202020206c6973742e616c6c280a202020202020202020206e6f64655f6f7574707574732c0a20202020202020202020666e286f757470757429207b206f75747075742e61646472657373203d3d2065787065637465645f61646472657373207d2c0a2020202020202020290016408c6050604a6ea8c03cc094dd51814001204c330023758604c60466ea80448cc008dd5980718121baa300e30243754002022660046eb0c030c08cdd50089198011bab300e3024375400202244646600200200644b30010018a5eb8226644b3001300500289981480119802002000c4cc01001000502418140009814800a04c2232330010010032259800800c52844c96600266e44010006266e3c01000626600600660540048118dd718121814000a04c912cc0040062900044c02ccc008008c09c005024489660020031480022601666004004604e0028122464b3001300e30223754003130263023375400315330214912465787065637420496e6c696e65446174756d286429203d206f75747075742e646174756d00164080601660446ea80064446466446600400400244b3001001801c4c8cc896600266e4401c00a2b30013371e00e0051001803204e8998028029817002204e375c604e0026eb4c0a0004c0a8005028198058020018a4000911114c004dd61814802cdd618149815002cc010012600600722230159800801c00a0028021222223259800980d806c566002660186eb0c0bcc0b0dd500d119baf3030302d375400206b159800992cc00401a051132598009819003c4c966002b30010098a518a5040c115980099b8f375c6064605e6ea8005221008acc004cdc79bae3019302f375400291011effffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008cc00400e039488100401514a08162294102c452820583300300101b814a05e303000640b86eacc04cc0b0dd500d4528c54cc0a924015076616c69646174655f626c61636b6c6973745f696e6974286e6f64655f696e707574732c206e6f64655f6f7574707574732c2073656c662e6d696e742c20706f6c6963795f696429203f2046616c73650014a0814a2a660549217d6578706563740a202020202020202020206c6973742e616e79280a20202020202020202020202073656c662e696e707574732c0a202020202020202020202020666e28696e70757429207b20696e7075742e6f75747075745f7265666572656e6365203d3d207574786f5f726566207d2c0a2020202020202020202029001640a5159800980c006c4cc89660020110268992cc004c0cc026264b30010018cc004006264b30015980099b87371a00a901c4528c54cc0bd2412e6275696c74696e2e6c656e6774685f6f665f627974656172726179286b657929203d3d203238203f2046616c73650014a081722b3001598008024528c54cc0bd2401166d616e616765725f7369676e6564203f2046616c73650014a081722b300159800cc004dd5980c18189baa01f80f40150074528c54cc0bd2401186a7573745f73696e676c655f6d696e74203f2046616c73650014a081722b30015980099b90375c606860626ea800801629462a6605e9212c6279746561727261795f6c7428636f766572696e675f6e6f64652e6b65792c206b657929203f2046616c73650014a081722b30015980099b90005375c603660626ea800a29462a6605e9212d6279746561727261795f6c74286b65792c20636f766572696e675f6e6f64652e6e65787429203f2046616c73650014a081722b300159800980e1804800c528c54cc0bd2401266c6973742e6c656e677468286f75747075745f6e6f64657329203d3d2032203f2046616c73650014a081722b3001598009980880092cc004cdc79bae3035303237540026eb8c0d4c0c8dd5001c4cdc79bae301c3032375400200d14a0817a29462a6605e92011e626c61636b6c6973745f6e6f64655f75706461746564203f2046616c73650014a081722b30013301100125980099b8f375c606a60646ea800401a266e3cdd7180e18191baa001375c603860646ea800e294102f4528c54cc0bd2411f626c61636b6c6973745f6e6f64655f696e736572746564203f2046616c73650014a08172294102e4528205c8a5040b914a08172294102e4528205c8a5040b86602801246600c00203d0294055029814c0a605281a8c02cc064c0bcdd5000c09d0301818804205e375c605e60586ea8074cc038dd6180798161baa01a23371e002053198009bae302f302c375403b3300e3758601e60586ea80688cdc7800814ccc0400188c96600200319800800c4006046809a047023811c08d0331804980b98169baa001488966002003025899912cc00400604f13259800981b00145660020170298992cc004c0dc032264b300159800803c528c54cc0c92401166d616e616765725f7369676e6564203f2046616c73650014a0818a2b30015980099b8798009bab301b30343754045021804201e4800629462a66064921186a7573745f73696e676c655f6275726e203f2046616c73650014a0818a2b3001301f300c00e8acc0056600266e3cdd7181b981a1baa0050088acc004cdc79bae3037303437540026eb8c0dcc0d0dd5001c56600266e3cdd7180f181a1baa001375c603c60686ea8016266e3cdd7180f181a1baa0030088a5040c514a0818a2b30013371e6eb8c0dcc0d0dd5001804456600266e3cdd7181b981a1baa001375c606e60686ea80162b30013371e6eb8c078c0d0dd50009bae301e3034375400713371e6eb8c078c0d0dd5002804452820628a5040c515330324901274e65697468657220696e707574206d61746368657320746865206b657920746f2072656d6f7665001640c4818a29462a660649210e636865636b73203f2046616c73650014a0818a2941031452820628a5040c46601000204102a40d0606a016819a0508198c0d000503218190009819800a060205240a44464b3001301932330010010022259800800c52000899914c004dd718188014dd598190014896600200310038991991180f99802802981d8021bae3034001375a606a002606e00281a922233001001002181980099801001181a000a0628992cc004c06cc018006264b30010018cc0040062b3001337206eb8c0ccc0c0dd50009bae301a3030375400315980080140aa264b300130350038992cc004c078dd698190014566002b30013371e0026eb8c0d4c0c8dd5001c528c4cdc7800a4410040bd10038a998182492d65787065637420746e203d3d206e6f64652e6b6579207c7c20746e203d3d206f726967696e5f6e6f64655f746e001640bd15330304910f65787065637420717479203d3d2031001640bc6eb8c0c00060568190c0cc009031454cc0b9240128657870656374206279746561727261795f6c74286e6f64652e6b65792c206e6f64652e6e65787429001640b50254055025812c09604a81a8c02c0122a6605a92012265787065637420646963742e73697a65286e6f64655f746f6b656e7329203d3d2031001640b066028002005153302c49012665787065637420636f756e745f756e697175655f746f6b656e732876616c756529203d3d2032001640ac6eacc05cc0b4dd500103015375400e6e1d2002370e900240220110088042032375c602c60266ea800e2c80806024002601a6ea804e293454cc02d2411856616c696461746f722072657475726e65642066616c7365001365640281", + "hash": "1693ffa822bd92dd0f194e0d09d5e2728663d07463c03bf6b074c7fe" + }, + { + "title": "blacklist_spend.blacklist_spend.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "blacklist_cs", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "58c1010100229800aba2aba1aab9faab9eaab9dab9cab9a9bae0024888888896600264646644b30013370e900118041baa0018994c004c0300066018601a0032259800800c52844c96600266e44024006266e3c02400626600600660200048050dd718059807000a01a4888cc004004dd59807980818081808180818069baa300f00818049baa0018b200c30090013009300a001300900130053754013149a2a660069211856616c696461746f722072657475726e65642066616c7365001365640081", + "hash": "2430d6ffbe8bd961e496fd22a08686ff9bd27643bc15f6f78c937e66" + }, + { + "title": "blacklist_spend.blacklist_spend.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "blacklist_cs", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "58c1010100229800aba2aba1aab9faab9eaab9dab9cab9a9bae0024888888896600264646644b30013370e900118041baa0018994c004c0300066018601a0032259800800c52844c96600266e44024006266e3c02400626600600660200048050dd718059807000a01a4888cc004004dd59807980818081808180818069baa300f00818049baa0018b200c30090013009300a001300900130053754013149a2a660069211856616c696461746f722072657475726e65642066616c7365001365640081", + "hash": "2430d6ffbe8bd961e496fd22a08686ff9bd27643bc15f6f78c937e66" + }, + { + "title": "example_transfer_logic.issuer_admin_contract.withdraw", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "permitted_cred", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + } + ], + "compiledCode": "590121010100229800aba2aba1aba0aab9faab9eaab9dab9cab9a4888888896600264653001300900198049805000cc0240092225980099b8748010c020dd500144c96600266e1d20003009375402113232330010013758601e6020602060206020602060206020602060186ea8010896600200314a115980099b8f375c602000200714a3133002002301100140288070dd7180698051baa01089919198008009bab300f301030103010301030103010300c375400844b30010018a508acc004cdd798061808000801c528c4cc008008c04400500a201c3374a900119806180698051baa0104bd70200e300c30093754005164018300900130043754013149a2a6600492011856616c696461746f722072657475726e65642066616c7365001365640041", + "hash": "ec10bc8537367b5f3169268e25069beee7c6767770984c90c14c3dd4" + }, + { + "title": "example_transfer_logic.issuer_admin_contract.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "permitted_cred", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + } + ], + "compiledCode": "590121010100229800aba2aba1aba0aab9faab9eaab9dab9cab9a4888888896600264653001300900198049805000cc0240092225980099b8748010c020dd500144c96600266e1d20003009375402113232330010013758601e6020602060206020602060206020602060186ea8010896600200314a115980099b8f375c602000200714a3133002002301100140288070dd7180698051baa01089919198008009bab300f301030103010301030103010300c375400844b30010018a508acc004cdd798061808000801c528c4cc008008c04400500a201c3374a900119806180698051baa0104bd70200e300c30093754005164018300900130043754013149a2a6600492011856616c696461746f722072657475726e65642066616c7365001365640041", + "hash": "ec10bc8537367b5f3169268e25069beee7c6767770984c90c14c3dd4" + }, + { + "title": "example_transfer_logic.transfer.withdraw", + "redeemer": { + "title": "proofs", + "schema": { + "$ref": "#/definitions/List" + } + }, + "parameters": [ + { + "title": "programmable_logic_base_cred", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + }, + { + "title": "blacklist_node_cs", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "59071d0101002229800aba4aba2aba1aba0aab9faab9eaab9dab9cab9a9bae00248888888888cc896600264653001300c00198061806800cdc3a4009300c0024888966002600460186ea800e264b300100580440222646466002002004446644b30010038992cc004c8cc004004dd5980c180c980c980c980c980c980c980c980c980c980a9baa0092259800800c528456600264b3001300c3016375400313375e6034602e6ea800402a2941014180a980c800c528c4cc008008c068005013202e8994c004c00400644b30010018a518acc004cdc3a4004602c6ea8c068006266004004603600314a080a10184dd61801180a9baa0094888cc008008ca600200332330010013758603a60346ea8038896600200314bd7044cc896600266ebcc080c074dd51810180e9baa300a301d375400404b13322598009806980f1baa0028992cc004c038c07cdd5000c4c966002601e60406ea8006266046604860426ea8004016266046604860426ea800401501e181198101baa001801203a3022301f375400500140706014603a6ea8c080c074dd51805180e9baa00210018800a034301e00133002002301f001407100b4004444b30010028a5eb822b30010018a5eb8226603b30013322598009805980e1baa0018992cc0056600266e212000323322330020020012259800800c00e2646644b3001337229101000028acc004cdc7a441000028800c01902144cc014014c0a00110211bae3021001375a604400260480028110c8c8cc004004dd5980718109baa0042259800800c00e2646644b30013372204200515980099b8f0210028800c01902244cc014014c0a40110221bae302200137566046002604a002811852f5bded8c0290004528c4c8cc004004c8cc004004dd5980698101baa0032259800800c52f5c113233223322330020020012259800800c400e264660526e9ccc0a4dd4802998149813000998149813800a5eb80cc00c00cc0ac008c0a40050271bab3024003375c604200266006006604c00460480028110896600200314a115980099b8f375c60466eb0c08c0040722946266004004604800280e902120368992cc0040062b3001300d301e375400313259800800c06e264b300100180e407203901c899912cc00400603d13259800800c07e03f01f80fc4c966002605200715980099b90375c6050604a6ea801c02a266e40028dd7180918129baa0078a50408902040986eb80050291813000a048375c002604a0048130c08c005021180f9baa00180d203880d406a03501a409064b30013014301e375400313022301f3754003153301d49012465787065637420496e6c696e65446174756d286429203d206f75747075742e646174756d00164070604260446044603c6ea80062a660389201ff6578706563740a20202020202020207175616e746974795f6f66286e6f64655f6f75747075742e76616c75652c20626c61636b6c6973745f6e6f64655f63732c20222229203e2030207c7c202f2f20436865636b20666f7220616e7920746f6b656e2066726f6d207468697320706f6c6963792028736f6d6520696d706c656d656e746174696f6e732075736520746f6b656e206e616d6573290a20202020202020206c6973742e616e79280a202020202020202020206173736574732e666c617474656e286e6f64655f6f75747075742e76616c7565292c0a20202020202020202020666e28617373657429207b0a2020202020202020202020206c657453202863732c205f746e2c205f71747929203d2061737365740a2020202020202020202020206373203d3d20626c61636b6c6973745f6e6f64655f63730a202020202020202020207d2c0a2020202020202020290016406c6014603a6ea8c080c074dd5000c54cc06d24013c65787065637420536f6d65287265665f696e70757429203d206c6973742e6174287265666572656e63655f696e707574732c206e6f64655f69647829001640686eb8c078008c96600266e2000520008a6103d87a8000899803802800a032375a603c60366ea8c07800698103d87a8000a60103d879800040613001003980f8014c07c005003203840702225980080145300103d87a80008acc004c014006266e95200033019301a0024bd70466002007301b00299b80001480050032028406114a080888c05cc060006264b30010018acc004c008c04cdd5000c4c96600200300f8992cc00400602101080844c966002603600713006301b007808a030375a003010406c603000280b0c050dd5000c039011403a01d00e80720323016003405060040046e1d2000375800b00880420263010300d3754007164028300c0013007375401b149a2a6600a92011856616c696461746f722072657475726e65642066616c7365001365640102a6600692012d657870656374206e6f64653a2074797065732e426c61636b6c6973744e6f6465203d20646174756d5f64617461001615330034911c70726f6f66733a204c6973743c426c61636b6c69737450726f6f663e001601", + "hash": "7bb4d341b24f9ca7a59dcb60bb55b2871bd422c06a89f958c7602b4c" + }, + { + "title": "example_transfer_logic.transfer.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "programmable_logic_base_cred", + "schema": { + "$ref": "#/definitions/cardano~1address~1Credential" + } + }, + { + "title": "blacklist_node_cs", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "59071d0101002229800aba4aba2aba1aba0aab9faab9eaab9dab9cab9a9bae00248888888888cc896600264653001300c00198061806800cdc3a4009300c0024888966002600460186ea800e264b300100580440222646466002002004446644b30010038992cc004c8cc004004dd5980c180c980c980c980c980c980c980c980c980c980a9baa0092259800800c528456600264b3001300c3016375400313375e6034602e6ea800402a2941014180a980c800c528c4cc008008c068005013202e8994c004c00400644b30010018a518acc004cdc3a4004602c6ea8c068006266004004603600314a080a10184dd61801180a9baa0094888cc008008ca600200332330010013758603a60346ea8038896600200314bd7044cc896600266ebcc080c074dd51810180e9baa300a301d375400404b13322598009806980f1baa0028992cc004c038c07cdd5000c4c966002601e60406ea8006266046604860426ea8004016266046604860426ea800401501e181198101baa001801203a3022301f375400500140706014603a6ea8c080c074dd51805180e9baa00210018800a034301e00133002002301f001407100b4004444b30010028a5eb822b30010018a5eb8226603b30013322598009805980e1baa0018992cc0056600266e212000323322330020020012259800800c00e2646644b3001337229101000028acc004cdc7a441000028800c01902144cc014014c0a00110211bae3021001375a604400260480028110c8c8cc004004dd5980718109baa0042259800800c00e2646644b30013372204200515980099b8f0210028800c01902244cc014014c0a40110221bae302200137566046002604a002811852f5bded8c0290004528c4c8cc004004c8cc004004dd5980698101baa0032259800800c52f5c113233223322330020020012259800800c400e264660526e9ccc0a4dd4802998149813000998149813800a5eb80cc00c00cc0ac008c0a40050271bab3024003375c604200266006006604c00460480028110896600200314a115980099b8f375c60466eb0c08c0040722946266004004604800280e902120368992cc0040062b3001300d301e375400313259800800c06e264b300100180e407203901c899912cc00400603d13259800800c07e03f01f80fc4c966002605200715980099b90375c6050604a6ea801c02a266e40028dd7180918129baa0078a50408902040986eb80050291813000a048375c002604a0048130c08c005021180f9baa00180d203880d406a03501a409064b30013014301e375400313022301f3754003153301d49012465787065637420496e6c696e65446174756d286429203d206f75747075742e646174756d00164070604260446044603c6ea80062a660389201ff6578706563740a20202020202020207175616e746974795f6f66286e6f64655f6f75747075742e76616c75652c20626c61636b6c6973745f6e6f64655f63732c20222229203e2030207c7c202f2f20436865636b20666f7220616e7920746f6b656e2066726f6d207468697320706f6c6963792028736f6d6520696d706c656d656e746174696f6e732075736520746f6b656e206e616d6573290a20202020202020206c6973742e616e79280a202020202020202020206173736574732e666c617474656e286e6f64655f6f75747075742e76616c7565292c0a20202020202020202020666e28617373657429207b0a2020202020202020202020206c657453202863732c205f746e2c205f71747929203d2061737365740a2020202020202020202020206373203d3d20626c61636b6c6973745f6e6f64655f63730a202020202020202020207d2c0a2020202020202020290016406c6014603a6ea8c080c074dd5000c54cc06d24013c65787065637420536f6d65287265665f696e70757429203d206c6973742e6174287265666572656e63655f696e707574732c206e6f64655f69647829001640686eb8c078008c96600266e2000520008a6103d87a8000899803802800a032375a603c60366ea8c07800698103d87a8000a60103d879800040613001003980f8014c07c005003203840702225980080145300103d87a80008acc004c014006266e95200033019301a0024bd70466002007301b00299b80001480050032028406114a080888c05cc060006264b30010018acc004c008c04cdd5000c4c96600200300f8992cc00400602101080844c966002603600713006301b007808a030375a003010406c603000280b0c050dd5000c039011403a01d00e80720323016003405060040046e1d2000375800b00880420263010300d3754007164028300c0013007375401b149a2a6600a92011856616c696461746f722072657475726e65642066616c7365001365640102a6600692012d657870656374206e6f64653a2074797065732e426c61636b6c6973744e6f6465203d20646174756d5f64617461001615330034911c70726f6f66733a204c6973743c426c61636b6c69737450726f6f663e001601", + "hash": "7bb4d341b24f9ca7a59dcb60bb55b2871bd422c06a89f958c7602b4c" + } + ], + "definitions": { + "ByteArray": { + "title": "ByteArray", + "dataType": "bytes" + }, + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "List": { + "dataType": "list", + "items": { + "$ref": "#/definitions/types~1BlacklistProof" + } + }, + "aiken/crypto/ScriptHash": { + "title": "ScriptHash", + "dataType": "bytes" + }, + "aiken/crypto/VerificationKeyHash": { + "title": "VerificationKeyHash", + "dataType": "bytes" + }, + "cardano/address/Credential": { + "title": "Credential", + "description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).", + "anyOf": [ + { + "title": "VerificationKey", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "Script", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1ScriptHash" + } + ] + } + ] + }, + "cardano/assets/PolicyId": { + "title": "PolicyId", + "dataType": "bytes" + }, + "cardano/transaction/OutputReference": { + "title": "OutputReference", + "description": "An `OutputReference` is a unique reference to an output on-chain. The `output_index`\n corresponds to the position in the output list of the transaction (identified by its id)\n that produced that output", + "anyOf": [ + { + "title": "OutputReference", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "transaction_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "output_index", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "types/BlacklistProof": { + "title": "BlacklistProof", + "description": "Proof of blacklist membership or non-membership", + "anyOf": [ + { + "title": "NonmembershipProof", + "description": "Proof of non-membership via covering node", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "node_idx", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "types/BlacklistRedeemer": { + "title": "BlacklistRedeemer", + "description": "Redeemer for blacklist minting policy (linked list operations)", + "anyOf": [ + { + "title": "BlacklistInit", + "description": "Initialize the blacklist with the origin node", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "BlacklistInsert", + "description": "Insert a credential into the blacklist", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "title": "key", + "$ref": "#/definitions/ByteArray" + } + ] + }, + { + "title": "BlacklistRemove", + "description": "Remove a credential from the blacklist", + "dataType": "constructor", + "index": 2, + "fields": [ + { + "title": "key", + "$ref": "#/definitions/ByteArray" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/src/programmable-tokens/aiken-workspace/validators/blacklist_mint.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_mint.ak similarity index 88% rename from src/programmable-tokens/aiken-workspace/validators/blacklist_mint.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_mint.ak index 68341b9..001e468 100644 --- a/src/programmable-tokens/aiken-workspace/validators/blacklist_mint.ak +++ b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_mint.ak @@ -21,15 +21,18 @@ use utils.{bytearray_lt, expect_inline_datum} validator blacklist_mint(utxo_ref: OutputReference, manager_pkh: ByteArray) { mint(redeemer: BlacklistRedeemer, policy_id: PolicyId, self: Transaction) { // Ensure this is a one-shot minting policy by checking that utxo_ref is spent - expect - list.any(self.inputs, fn(input) { input.output_reference == utxo_ref }) - let (node_inputs, node_outputs) = collect_node_ios(self, policy_id) when redeemer is { - BlacklistInit -> + BlacklistInit -> { + expect + list.any( + self.inputs, + fn(input) { input.output_reference == utxo_ref }, + ) // Initialize the blacklist with an empty origin node - validate_blacklist_init(node_inputs, node_outputs, self.mint, policy_id) + validate_blacklist_init(node_inputs, node_outputs, self.mint, policy_id)? + } BlacklistInsert { key } -> { // Authorization check: manager must sign the transaction @@ -71,16 +74,16 @@ validator blacklist_mint(utxo_ref: OutputReference, manager_pkh: ByteArray) { ) and { // Validate key is a valid public key hash (28 bytes) - builtin.length_of_bytearray(key) == 28, - manager_signed, - just_single_mint, + (builtin.length_of_bytearray(key) == 28)?, + manager_signed?, + just_single_mint?, // The covering node must cover the key to insert - bytearray_lt(covering_node.key, key), - bytearray_lt(key, covering_node.next), + bytearray_lt(covering_node.key, key)?, + bytearray_lt(key, covering_node.next)?, // Must have exactly 2 outputs: the updated covering node and the new inserted node - list.length(output_nodes) == 2, - blacklist_node_updated, - blacklist_node_inserted, + (list.length(output_nodes) == 2)?, + blacklist_node_updated?, + blacklist_node_inserted?, } } @@ -127,11 +130,11 @@ validator blacklist_mint(utxo_ref: OutputReference, manager_pkh: ByteArray) { } and { - manager_signed, - just_single_burn, + manager_signed?, + just_single_burn?, // Must have exactly two node inputs: the node to remove and the covering node list.length(node_inputs) == 2, - checks, + checks?, } } } diff --git a/src/programmable-tokens/aiken-workspace/validators/blacklist_mint.test.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_mint.test.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/validators/blacklist_mint.test.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_mint.test.ak diff --git a/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_spend.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_spend.ak new file mode 100644 index 0000000..c86f094 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_spend.ak @@ -0,0 +1,31 @@ +use aiken/collection/dict.{has_key} +use cardano/assets.{PolicyId, to_dict} +use cardano/transaction.{Transaction} + +// Blacklist spending validator - guards the blacklist node UTxOs +// Migrated from SmartTokens.LinkedList.SpendBlacklist +// +// This validator locks blacklist node UTxOs and ensures they can only be spent +// when the blacklist minting policy is also executed in the same transaction. +// This creates a paired validation: the mint policy handles the Insert/Remove/Init +// logic, while this spend validator ensures node UTxOs can't be spent without +// invoking the minting policy. + +validator blacklist_spend(blacklist_cs: PolicyId) { + spend( + _datum: Option, + _redeemer: Data, + _own_ref: Data, + self: Transaction, + ) { + // Check that the blacklist currency symbol appears in the mint field + // This ensures the minting policy is being executed, which contains + // the actual validation logic for blacklist operations + to_dict(self.mint) |> has_key(blacklist_cs) + // assets.has_policy(self.mint, blacklist_cs) + } + + else(_) { + fail + } +} diff --git a/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_spend.test.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_spend.test.ak new file mode 100644 index 0000000..2929446 --- /dev/null +++ b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/blacklist_spend.test.ak @@ -0,0 +1,35 @@ +use aiken/collection/dict.{has_key} +// Tests for blacklist spending validator +// The blacklist_spend validator ensures that blacklist node UTxOs can only be spent +// when the blacklist minting policy is also executed in the same transaction. + +use cardano/assets.{PolicyId, Value, to_dict} + +fn has_policy(mint_value: Value, policy_cs: PolicyId) -> Bool { + to_dict(mint_value) |> has_key(policy_cs) +} + +// Test that has_policy returns true when policy is in mint +test test_has_policy_returns_true() { + let policy_cs = #"aabbccdd" + let mint_value = assets.from_asset(policy_cs, "some_token", 1) + + has_policy(mint_value, policy_cs) +} + +// Test that has_policy returns false when policy is not in mint +test test_has_policy_returns_false() { + let policy_cs = #"aabbccdd" + let different_cs = #"deadbeef" + let mint_value = assets.from_asset(different_cs, "some_token", 1) + + !has_policy(mint_value, policy_cs) +} + +// Test that has_policy returns false when mint is empty +test test_has_policy_returns_false_for_empty_mint() { + let policy_cs = #"aabbccdd" + let mint_value = assets.zero + + !has_policy(mint_value, policy_cs) +} diff --git a/src/programmable-tokens/aiken-workspace/validators/example_transfer_logic.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/example_transfer_logic.ak similarity index 98% rename from src/programmable-tokens/aiken-workspace/validators/example_transfer_logic.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/example_transfer_logic.ak index 2c81460..fe16ccd 100644 --- a/src/programmable-tokens/aiken-workspace/validators/example_transfer_logic.ak +++ b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/example_transfer_logic.ak @@ -10,7 +10,7 @@ use types.{BlacklistProof, NonmembershipProof} use utils // Simple example: only allow transfers signed by a specific credential -validator example_transfer_logic(permitted_cred: Credential) { +validator issuer_admin_contract(permitted_cred: Credential) { withdraw(_redeemer: Data, _account: Credential, self: Transaction) { // This example transfer logic simply checks that a specific credential // has authorized the transfer @@ -130,7 +130,7 @@ fn is_rewarding_script( // Freeze-and-seize transfer logic validator // This validates transfers against a blacklist -validator freeze_and_seize_transfer( +validator transfer( programmable_logic_base_cred: Credential, blacklist_node_cs: PolicyId, ) { diff --git a/src/programmable-tokens/aiken-workspace/validators/example_transfer_logic.test.ak b/src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/example_transfer_logic.test.ak similarity index 100% rename from src/programmable-tokens/aiken-workspace/validators/example_transfer_logic.test.ak rename to src/programmable-tokens/aiken-workspace-subStandard/freeze-and-seize/validators/example_transfer_logic.test.ak diff --git a/src/programmable-tokens/aiken-workspace/README.md b/src/programmable-tokens/aiken-workspace/README.md deleted file mode 100644 index c8790bb..0000000 --- a/src/programmable-tokens/aiken-workspace/README.md +++ /dev/null @@ -1,292 +0,0 @@ -# Programmable Tokens - Aiken Implementation - -![Aiken](https://img.shields.io/badge/Aiken-v1.0.29-blue) -![CIP-113](https://img.shields.io/badge/CIP--113-Adapted-green) -![Status](https://img.shields.io/badge/Status-R&D-yellow) - -**Smart contracts for CIP-113 programmable tokens on Cardano, written in Aiken.** - -## Overview - -This repository contains a complete Aiken implementation of CIP-113 programmable tokens - native Cardano assets enhanced with programmable transfer rules and lifecycle controls. This implementation is based on the foundational CIP-143 architecture, adapted for CIP-113 requirements. Programmable tokens enable regulatory compliance for real-world assets like stablecoins and tokenized securities while maintaining full compatibility with the Cardano native token infrastructure. - -## What Are Programmable Tokens? - -Programmable tokens are **native Cardano assets** with an additional layer of validation logic that executes on every transfer, mint, or burn operation. They remain fully compatible with existing Cardano infrastructure (wallets, explorers, DEXes) while adding programmable constraints required for regulated assets. - -**Key principle**: All programmable tokens are locked in a shared smart contract address. Ownership is determined by stake credentials, allowing standard wallets to manage them while enabling unified validation across the entire token ecosystem. - -## Key Features - -- 🔐 **Permissioned Transfers** - Enforce custom validation rules on every token transfer -- 📋 **On-Chain Registry** - Decentralized directory of registered programmable tokens -- 🎯 **Composable Logic** - Plug-and-play transfer and minting validation scripts -- 🚫 **Freeze & Seize** - Optional issuer controls for regulatory compliance -- ⚡ **Constant-Time Lookups** - Sorted linked list registry enables O(1) token verification -- 🔗 **Native Asset Compatible** - Works with existing Cardano wallets and infrastructure -- 🛡️ **Multi-Layer Security** - NFT authenticity, ownership proofs, and authorization checks -- 🧩 **Extensible** - Support for blacklists, whitelists, time-locks, and custom policies - -## Use Cases - -- **Stablecoins** - Fiat-backed tokens with sanctions screening and freeze capabilities -- **Tokenized Securities** - Compliance with securities regulations and transfer restrictions -- **Regulated Assets** - Any token requiring KYC/AML compliance or jurisdictional controls -- **Custom Policies** - Extensible framework for any programmable token logic - -## Quick Start - -### Prerequisites - -- [Aiken](https://aiken-lang.org/installation-instructions) v1.0.29 or higher -- [Cardano CLI](https://github.com/IntersectMBO/cardano-cli) (optional, for deployment) - -### Build - -```bash -cd src/programmable-tokens-onchain-aiken -aiken build -``` - -### Test - -```bash -aiken check -``` - -All tests should pass: -``` - Summary 1 error(s), 89 passing (89) [89/89 checks passed] -``` - -### Generate Blueprints - -```bash -aiken blueprint convert > plutus.json -``` - -## Project Structure - -``` -. -├── validators/ # Smart contract validators -│ ├── programmable_logic_global.ak # Core transfer validation -│ ├── programmable_logic_base.ak # Token custody -│ ├── registry_mint.ak # Registry minting policy -│ ├── registry_spend.ak # Registry spending validator -│ ├── issuance_mint.ak # Token issuance policy -│ ├── issuance_cbor_hex_mint.ak # CBOR hex reference NFT -│ ├── protocol_params_mint.ak # Protocol parameters NFT -│ ├── example_transfer_logic.ak # Example: freeze-and-seize -│ ├── blacklist_mint.ak # Blacklist management -│ └── ... -├── lib/ -│ ├── types.ak # Core data types -│ ├── utils.ak # Utility functions -│ └── linked_list.ak # Registry list operations -└── docs/ # Documentation -``` - -## Documentation - -📚 **Complete documentation is available in the [`docs/`](./docs/) directory:** - -- **[Introduction](./docs/01-INTRODUCTION.md)** - Problem statement, concepts, and benefits -- **[Architecture](./docs/02-ARCHITECTURE.md)** - System design and components -- **[Validators](./docs/03-VALIDATORS.md)** - Smart contract reference -- **[Data Structures](./docs/04-DATA-STRUCTURES.md)** - Types, redeemers, and datums -- **[Transaction Flows](./docs/05-TRANSACTION-FLOWS.md)** - Building transactions -- **[Usage Guide](./docs/06-USAGE.md)** - Build, test, and deploy -- **[Migration Notes](./docs/07-MIGRATION-NOTES.md)** - Plutarch to Aiken migration - - -## Core Components - -### 1. Token Registry (On-Chain Directory) - -A sorted linked list of registered programmable tokens, implemented as on-chain UTxOs with NFT markers. Each registry entry contains: -- Token policy ID -- Transfer validation script reference -- Issuer control script reference -- Optional global state reference - -### 2. Programmable Logic Base - -A shared spending validator that holds all programmable tokens. All tokens share the same payment credential but have unique stake credentials for ownership. - -### 3. Validation Scripts - -Pluggable stake validators that define custom logic: -- **Transfer Logic** - Runs on every token transfer (e.g., blacklist checks) -- **Issuer Logic** - Controls minting, burning, and seizure operations - -### 4. Minting Policies - -- **Issuance Policy** - Parameterized by transfer logic, handles token minting/burning -- **Directory Policy** - Manages registry entries (one-shot for initialization) -- **Protocol Params Policy** - Stores global protocol parameters (one-shot) - -## Transaction Lifecycle - -```mermaid -graph LR - A[Deploy Protocol] --> B[Register Token] - B --> C[Issue Tokens] - C --> D[Transfer] - D --> D - C --> E[Burn] - - style A fill:#e1f5ff - style B fill:#fff4e1 - style C fill:#e8f5e9 - style D fill:#f3e5f5 - style E fill:#ffebee -``` - -1. **Deployment** - One-time setup of registry and protocol parameters -2. **Registration** - Register transfer logic and mint policy in registry -3. **Issuance** - Mint tokens with registered validation rules -4. **Transfer** - Transfer tokens with automatic validation -5. **Burn** - Burn tokens (requires issuer authorization) - -## How It Works - -```mermaid -graph TB - A[User Initiates Transfer] --> B{Lookup Token in Registry} - B -->|Found| C[Invoke Transfer Logic Script] - B -->|Not Found| D[Treat as Regular Native Token] - C --> E{Validation Passes?} - E -->|Yes| F[Complete Transfer] - E -->|No| G[Reject Transaction] - D --> F - - style A fill:#e3f2fd - style B fill:#fff9c4 - style C fill:#f3e5f5 - style E fill:#ffe0b2 - style F fill:#c8e6c9 - style G fill:#ffcdd2 -``` - -All programmable tokens are locked at a shared smart contract address. When a transfer occurs: - -1. Transaction spends token UTxO from programmable logic address -2. Global validator looks up token in on-chain registry -3. If registered, corresponding transfer logic script executes -4. Transfer succeeds only if all validation passes -5. Tokens return to programmable logic address with new stake credential - -## Example: Freeze & Seize Stablecoin - -This implementation includes a complete example of a regulated stablecoin with freeze and seize capabilities: - -- **On-chain Blacklist** - Sorted linked list of sanctioned addresses -- **Transfer Validation** - Every transfer checks sender/recipient not blacklisted -- **Constant-Time Checks** - O(1) verification using covering node proofs -- **Issuer Controls** - Authorized parties can freeze/seize tokens - -See [`validators/example_transfer_logic.ak`](./validators/example_transfer_logic.ak) for the implementation. - -## Standards - -This implementation is based on the foundational [CIP-143 (Interoperable Programmable Tokens)](https://cips.cardano.org/cip/CIP-0143) architecture and has been adapted for [CIP-113](https://github.com/cardano-foundation/CIPs/pull/444), which supersedes CIP-143 as a more comprehensive standard for programmable tokens on Cardano. - -**Note**: CIP-113 is currently under active development. This implementation reflects the current understanding of the standard and may require updates as CIP-113 evolves. - -## Development Status - -**Current Status**: Research & Development - -This is high-quality research and development code with the following characteristics: - -- ✅ All core validators implemented with strong code quality -- ✅ Registry (directory) operations complete -- ✅ Token issuance and transfer flows working -- ✅ Freeze & seize functionality complete -- ✅ Blacklist system operational -- ✅ Good test coverage (89 passing tests) -- ✅ Tested on Preview testnet (limited scope) -- ⏳ Comprehensive testing required -- ⏳ Professional security audit pending - -**Security features implemented:** -- ✅ NFT-based registry authenticity -- ✅ Ownership verification via stake credentials -- ✅ Multi-layer authorization checks -- ✅ One-shot minting policies for protocol components -- ✅ Immutable validation rules post-registration -- ✅ DDOS prevention mechanisms - -## Security Considerations - -⚠️ **Important**: This code has **not been professionally audited** and has only been briefly tested on Preview testnet. While code quality is high, it is **not production-ready**. Do not use with real assets or in production environments without: -- Comprehensive security audit by qualified professionals -- Extensive testing across multiple scenarios -- Thorough review by domain experts - -## Migration from Plutarch - -This is a complete Aiken migration of the original Plutarch implementation. Some improvements: - -- **Performance** - Comparable or slightly worse -- **Error Prevention** - Target addresses are tested for staking keys - -See [Migration Notes](./docs/07-MIGRATION-NOTES.md) for detailed comparison. - -## Contributing - -Contributions welcome! Please: - -1. Read the [documentation](./docs/) to understand the architecture -2. Ensure all tests pass (`aiken check`) -3. Add tests for new functionality -4. Follow existing code style and patterns -5. Open an issue to discuss major changes - -## Testing - -Run the complete test suite: - -```bash -# Run all tests -aiken check - -# Run specific test file -aiken check -m validators/programmable_logic_global - -# Watch mode for development -aiken check --watch -``` - -## Related Components - -- **Off-chain (Java)**: [`programmable-tokens-offchain-java/`](../programmable-tokens-offchain-java/) - Transaction building and blockchain integration - -## Resources - -- 📖 [Aiken Language Documentation](https://aiken-lang.org/) -- 🎓 [CIP-143 Specification](https://cips.cardano.org/cip/CIP-0143) - Original standard -- 🔄 [CIP-113 Pull Request](https://github.com/cardano-foundation/CIPs/pull/444) - Current standard development -- 🔗 [Cardano Developer Portal](https://developers.cardano.org/) -- 💬 [Aiken Discord](https://discord.gg/Vc3x8N9nz2) - -## License - -This project is licensed under the Apache License 2.0 - see the [LICENSE](../../LICENSE) file for details. - -Copyright 2024 Cardano Foundation - -## Acknowledgments - -This implementation is migrated from the original Plutarch implementation developed by **Phil DiSarro** and the **IOG Team** (see [wsc-poc](https://github.com/input-output-hk/wsc-poc)). We are grateful for their foundational work on CIP-143. - -Special thanks to: -- **Phil DiSarro** and the **IOG Team** for the original Plutarch design and implementation -- The **Aiken team** for the excellent smart contract language and tooling -- The **CIP-143/CIP-113 authors and contributors** for standard development -- The **Cardano developer community** for continued support and collaboration - ---- - -**Built with ❤️ using [Aiken](https://aiken-lang.org/)** diff --git a/src/programmable-tokens/aiken-workspace/plutus.json b/src/programmable-tokens/aiken-workspace/plutus.json deleted file mode 100644 index 2ad6d2e..0000000 --- a/src/programmable-tokens/aiken-workspace/plutus.json +++ /dev/null @@ -1,693 +0,0 @@ -{ - "preamble": { - "title": "iohk/programmable-tokens", - "description": "Aiken implementation of CIP-0143 programmable tokens (migrated from Plutarch)", - "version": "0.3.0", - "plutusVersion": "v3", - "compiler": { - "name": "Aiken", - "version": "v1.1.17+c3a7fba" - }, - "license": "Apache-2.0" - }, - "validators": [ - { - "title": "blacklist_mint.blacklist_mint.mint", - "redeemer": { - "title": "redeemer", - "schema": { - "$ref": "#/definitions/types~1BlacklistRedeemer" - } - }, - "parameters": [ - { - "title": "utxo_ref", - "schema": { - "$ref": "#/definitions/cardano~1transaction~1OutputReference" - } - }, - { - "title": "manager_pkh", - "schema": { - "$ref": "#/definitions/ByteArray" - } - } - ], - "compiledCode": "5907980101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e33001375c601a60146ea800e6e1d20029b874801260126ea80112222325980098038014566002601e6ea8026003164041159800980200144c8c966002602a0050038b2024375c6026002601e6ea80262b30013003002899192cc004c05400a0071640486eb8c04c004c03cdd5004c5900d201a4034264b30013300137586024601e6ea80188cdd7980998081baa0010178cc0048c04cc05000646026602860280033700900148c04cc050c050c050c0500064464646600200200844b3001001801c4c8cc896600266e4401c00a2b30013371e00e0051001803202a899802802980d802202a375c60280026eacc054004c05c0050150a5eb7bdb18244b300130093010375400513232332259800980c801c0162c80b0dd7180b0009bae301600230160013011375400516403d2232330010010032259800800c52f5c11330153003301600133002002301700140512301330143014301430143014301430143014001911919800800801912cc00400629422b30013003375c602c00314a31330020023017001404480a2601c6ea802522222222229800999119912cc00400a26603e9810180003301f374e00297ae08992cc004c8cc00400400c896600200314a315980099baf302330203754604600200713300200230240018a504078810a2660406e9c00ccc080dd380125eb822c80e0c080c074dd51807180e9baa30200024078660046eb0c078c06cdd50091198011bab300d301c3754601a60386ea8004048cc008dd61805980d9baa0122330023756601a60386ea800404888c8cc00400400c896600200314bd7044cc8966002600a00513302100233004004001899802002000a03a30200013021001407844646600200200644b30010018a508992cc004cdc8802000c4cdc7802000c4cc00c00cc08800901c1bae301c302000140792259800800c520008980519801001180f800a038912cc0040062900044c028cc008008c07c00501c48c966002601e60346ea80062603c60366ea80062c80c8c028c068dd5000c888c8cc88cc008008004896600200300389919912cc004cdc8803801456600266e3c01c00a20030064081133005005302600440806eb8c07c004dd698100009811000a0403300a0040031480012222298009bac30210059bac3021302200598020024c00c00e444602d3001003801400500424444464b3001301c00c8acc004c0a0016264b300159800803c528c528204c8acc004cdc79bae302830253754002911008acc004cdc79bae30163025375400291011effffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008cc004dd5980998129baa01c80dd220100400d14a0811a29410234528204633001302700501a8b204a8acc004c06403226644b3001302a008899198098008992cc004cdc39b8d004480e22b30010038acc00660026eacc058c0a0dd500fc07a00880322b3001337206eb8c0acc0a0dd5001002456600266e40010dd7180c98141baa0028acc004c070c0200062b30013301a00125980099b8f375c605860526ea8004dd7181618149baa003899b8f375c603460526ea8004016294102744cc06800496600266e3cdd7181618149baa001005899b8f375c603460526ea8004dd7180d18149baa0038a50409d14a0813229410264528204c8a50409914a0813229410264528204c3301200823300500101e300a301730263754605201116409c6eb8c09cc090dd500f198069bac300e30243754036466e3c00408a2646644b3001302b002899912cc004c0b402a264b3001330133758602860546ea80848cdc7800814456600266e1e60026eacc060c0a8dd5010c082008806920018acc004c078c0280322b30013371e6eb8c0b4c0a8dd5001802456600266e3cdd7181698151baa001375c605a60546ea800a2b30013371e6eb8c06cc0a8dd50009bae301b302a375400713371e6eb8c06cc0a8dd5001002452820508a5040a115980099b8f375c605a60546ea80080122b30013371e6eb8c0b4c0a8dd50009bae302d302a375400715980099b8f375c603660546ea8004dd7180d98151baa002899b8f375c603660546ea800c0122941028452820508b205040a114a0814229410284528205033006302c00a01f8b2054302a003302a0028b20503029001375c6050604a6ea807ccc03c0188c8cc0480044004c024c058c094dd5000a04440884464b3001301a32330010010022259800800c52000899914c004dd718148014dd598150014896600200310038991991180f1980280298198021bae302c001375a605a002605e0028169222330010010021815800998010011816000a0528992cc004c070c018006264660280022b3001337206eb8c0acc0a0dd50009bae301930283754003159800981600144c8c966002603e6eb4c0a800a2b30015980099b8f001375c605a60546ea800e2946266e3c00522010040a110038b20508b2050375c605000260560051640a51640986016009164094660260020051640906eacc058c094dd500108b201a2232330010010032259800800c52845660026006602a00314a3133002002301600140408099164020300900130043754013149a26cac80101", - "hash": "2cb3b825fb7cf39bb7c6d67afa597d728ad57100a4a75ec7c8c45129" - }, - { - "title": "blacklist_mint.blacklist_mint.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "utxo_ref", - "schema": { - "$ref": "#/definitions/cardano~1transaction~1OutputReference" - } - }, - { - "title": "manager_pkh", - "schema": { - "$ref": "#/definitions/ByteArray" - } - } - ], - "compiledCode": "5907980101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e33001375c601a60146ea800e6e1d20029b874801260126ea80112222325980098038014566002601e6ea8026003164041159800980200144c8c966002602a0050038b2024375c6026002601e6ea80262b30013003002899192cc004c05400a0071640486eb8c04c004c03cdd5004c5900d201a4034264b30013300137586024601e6ea80188cdd7980998081baa0010178cc0048c04cc05000646026602860280033700900148c04cc050c050c050c0500064464646600200200844b3001001801c4c8cc896600266e4401c00a2b30013371e00e0051001803202a899802802980d802202a375c60280026eacc054004c05c0050150a5eb7bdb18244b300130093010375400513232332259800980c801c0162c80b0dd7180b0009bae301600230160013011375400516403d2232330010010032259800800c52f5c11330153003301600133002002301700140512301330143014301430143014301430143014001911919800800801912cc00400629422b30013003375c602c00314a31330020023017001404480a2601c6ea802522222222229800999119912cc00400a26603e9810180003301f374e00297ae08992cc004c8cc00400400c896600200314a315980099baf302330203754604600200713300200230240018a504078810a2660406e9c00ccc080dd380125eb822c80e0c080c074dd51807180e9baa30200024078660046eb0c078c06cdd50091198011bab300d301c3754601a60386ea8004048cc008dd61805980d9baa0122330023756601a60386ea800404888c8cc00400400c896600200314bd7044cc8966002600a00513302100233004004001899802002000a03a30200013021001407844646600200200644b30010018a508992cc004cdc8802000c4cdc7802000c4cc00c00cc08800901c1bae301c302000140792259800800c520008980519801001180f800a038912cc0040062900044c028cc008008c07c00501c48c966002601e60346ea80062603c60366ea80062c80c8c028c068dd5000c888c8cc88cc008008004896600200300389919912cc004cdc8803801456600266e3c01c00a20030064081133005005302600440806eb8c07c004dd698100009811000a0403300a0040031480012222298009bac30210059bac3021302200598020024c00c00e444602d3001003801400500424444464b3001301c00c8acc004c0a0016264b300159800803c528c528204c8acc004cdc79bae302830253754002911008acc004cdc79bae30163025375400291011effffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008cc004dd5980998129baa01c80dd220100400d14a0811a29410234528204633001302700501a8b204a8acc004c06403226644b3001302a008899198098008992cc004cdc39b8d004480e22b30010038acc00660026eacc058c0a0dd500fc07a00880322b3001337206eb8c0acc0a0dd5001002456600266e40010dd7180c98141baa0028acc004c070c0200062b30013301a00125980099b8f375c605860526ea8004dd7181618149baa003899b8f375c603460526ea8004016294102744cc06800496600266e3cdd7181618149baa001005899b8f375c603460526ea8004dd7180d18149baa0038a50409d14a0813229410264528204c8a50409914a0813229410264528204c3301200823300500101e300a301730263754605201116409c6eb8c09cc090dd500f198069bac300e30243754036466e3c00408a2646644b3001302b002899912cc004c0b402a264b3001330133758602860546ea80848cdc7800814456600266e1e60026eacc060c0a8dd5010c082008806920018acc004c078c0280322b30013371e6eb8c0b4c0a8dd5001802456600266e3cdd7181698151baa001375c605a60546ea800a2b30013371e6eb8c06cc0a8dd50009bae301b302a375400713371e6eb8c06cc0a8dd5001002452820508a5040a115980099b8f375c605a60546ea80080122b30013371e6eb8c0b4c0a8dd50009bae302d302a375400715980099b8f375c603660546ea8004dd7180d98151baa002899b8f375c603660546ea800c0122941028452820508b205040a114a0814229410284528205033006302c00a01f8b2054302a003302a0028b20503029001375c6050604a6ea807ccc03c0188c8cc0480044004c024c058c094dd5000a04440884464b3001301a32330010010022259800800c52000899914c004dd718148014dd598150014896600200310038991991180f1980280298198021bae302c001375a605a002605e0028169222330010010021815800998010011816000a0528992cc004c070c018006264660280022b3001337206eb8c0acc0a0dd50009bae301930283754003159800981600144c8c966002603e6eb4c0a800a2b30015980099b8f001375c605a60546ea800e2946266e3c00522010040a110038b20508b2050375c605000260560051640a51640986016009164094660260020051640906eacc058c094dd500108b201a2232330010010032259800800c52845660026006602a00314a3133002002301600140408099164020300900130043754013149a26cac80101", - "hash": "2cb3b825fb7cf39bb7c6d67afa597d728ad57100a4a75ec7c8c45129" - }, - { - "title": "example_transfer_logic.example_transfer_logic.withdraw", - "redeemer": { - "title": "_redeemer", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "parameters": [ - { - "title": "permitted_cred", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - } - ], - "compiledCode": "58ff010100229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cc0200092225980099b8748010c01cdd500144c96600266e1d20003008375401f13232330010013758601c601e601e601e601e601e601e601e601e60166ea8010896600200314a115980099b8f375c601e00200714a3133002002301000140288068dd7180618049baa00f89919198008009bab300e300f300f300f300f300f300f300b375400844b30010018a508acc004cdd798059807800801c528c4cc008008c04000500a201a3374a900119805980618049baa00f4bd70200e300b30083754005164018300800130033754011149a26cac80081", - "hash": "414cedf7ec28ec93ca61d85849648be0423be77add5a58268a289b13" - }, - { - "title": "example_transfer_logic.example_transfer_logic.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "permitted_cred", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - } - ], - "compiledCode": "58ff010100229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cc0200092225980099b8748010c01cdd500144c96600266e1d20003008375401f13232330010013758601c601e601e601e601e601e601e601e601e60166ea8010896600200314a115980099b8f375c601e00200714a3133002002301000140288068dd7180618049baa00f89919198008009bab300e300f300f300f300f300f300f300b375400844b30010018a508acc004cdd798059807800801c528c4cc008008c04000500a201a3374a900119805980618049baa00f4bd70200e300b30083754005164018300800130033754011149a26cac80081", - "hash": "414cedf7ec28ec93ca61d85849648be0423be77add5a58268a289b13" - }, - { - "title": "example_transfer_logic.freeze_and_seize_transfer.withdraw", - "redeemer": { - "title": "proofs", - "schema": { - "$ref": "#/definitions/List$types~1BlacklistProof" - } - }, - "parameters": [ - { - "title": "programmable_logic_base_cred", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - }, - { - "title": "blacklist_node_cs", - "schema": { - "$ref": "#/definitions/cardano~1assets~1PolicyId" - } - } - ], - "compiledCode": "59045e0101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae00248888888966002646530013009300a0019b874801260120049112cc004c008c020dd5001c4cc88c8cc00400400888c9660020051325980099198008009bab30133014301430143014301430143014301430143010375401044b30010018a508acc004c966002601660226ea8006266ebcc054c048dd5000804c5282020301030140018a51899801001180a800a01e404913298009800800c896600200314a315980099b8748008c044dd5180a800c4cc008008c058006294101020269bac30023010375401091119801001194c00400664660020026eb0c060c054dd5006912cc004006297ae0899912cc004cdd7980d980c1baa301b30183754601460306ea800807e26644b3001300d30193754005132598009807180d1baa0018992cc004c03cc06cdd5000c4cc078c07cc070dd5000802c4cc078c07cc070dd5000802a034301e301b37540030024064603a60346ea800a00280c0c028c060dd5180d980c1baa300a30183754004200310014058603200266004004603400280ba01480088896600200514bd70456600200314bd7044cc06260026644b3001300b3017375400313259800acc004cdc4240006466446600400400244b3001001801c4c8cc896600266e452201000028acc004cdc7a441000028800c01901d44cc014014c08c01101d1bae301c001375a603a002603e00280e8c8c8cc004004dd59807180e1baa0042259800800c00e2646644b30013372203a00515980099b8f01d0028800c01901e44cc014014c09001101e1bae301d0013756603c002604000280f052f5bded8c0290004528c4c8cc004004c8cc004004dd59806980d9baa0032259800800c52f5c113233223322330020020012259800800c400e264660486e9ccc090dd4802998121810800998121811000a5eb80cc00c00cc098008c0900050221bab301f003375c6038002660060066042004603e00280e8896600200314a115980099b8f375c603c6eb0c0780040622946266004004603e00280c901c202e8992cc004c034c064dd5000c4c8c8cc8966002604400715980099b90375c6042603c6ea8014022266e40020dd71808180f1baa0058a50407116407c6eb8c07c004dd7180f801180f800980d1baa0018b203032598009809980c9baa0018980e980d1baa0018b2030301c301d301d3019375400316405c601460306ea8c06cc060dd5000c590161bae3019002325980099b88001480022980103d87a8000899803802800a02a375a6032602c6ea8c06400698103d87a8000a60103d879800040513001003980d0014c068005003202e405c2225980080145300103d87a80008acc004c014006266e9520003301430150024bd70466002007301600299b80001480050032020404d14a080688c048c04c006264b30013002300e37540031323259800980a80144cc018018c0540162c8090dd6980980098079baa0018b201a3011002403c6e1d2000300c300937540066eb0c0300122c8038601200260086ea802629344d95900201", - "hash": "4e85c345d37150aeddcfbccf12f33318e8ffe9735971d429aa46d852" - }, - { - "title": "example_transfer_logic.freeze_and_seize_transfer.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "programmable_logic_base_cred", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - }, - { - "title": "blacklist_node_cs", - "schema": { - "$ref": "#/definitions/cardano~1assets~1PolicyId" - } - } - ], - "compiledCode": "59045e0101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae00248888888966002646530013009300a0019b874801260120049112cc004c008c020dd5001c4cc88c8cc00400400888c9660020051325980099198008009bab30133014301430143014301430143014301430143010375401044b30010018a508acc004c966002601660226ea8006266ebcc054c048dd5000804c5282020301030140018a51899801001180a800a01e404913298009800800c896600200314a315980099b8748008c044dd5180a800c4cc008008c058006294101020269bac30023010375401091119801001194c00400664660020026eb0c060c054dd5006912cc004006297ae0899912cc004cdd7980d980c1baa301b30183754601460306ea800807e26644b3001300d30193754005132598009807180d1baa0018992cc004c03cc06cdd5000c4cc078c07cc070dd5000802c4cc078c07cc070dd5000802a034301e301b37540030024064603a60346ea800a00280c0c028c060dd5180d980c1baa300a30183754004200310014058603200266004004603400280ba01480088896600200514bd70456600200314bd7044cc06260026644b3001300b3017375400313259800acc004cdc4240006466446600400400244b3001001801c4c8cc896600266e452201000028acc004cdc7a441000028800c01901d44cc014014c08c01101d1bae301c001375a603a002603e00280e8c8c8cc004004dd59807180e1baa0042259800800c00e2646644b30013372203a00515980099b8f01d0028800c01901e44cc014014c09001101e1bae301d0013756603c002604000280f052f5bded8c0290004528c4c8cc004004c8cc004004dd59806980d9baa0032259800800c52f5c113233223322330020020012259800800c400e264660486e9ccc090dd4802998121810800998121811000a5eb80cc00c00cc098008c0900050221bab301f003375c6038002660060066042004603e00280e8896600200314a115980099b8f375c603c6eb0c0780040622946266004004603e00280c901c202e8992cc004c034c064dd5000c4c8c8cc8966002604400715980099b90375c6042603c6ea8014022266e40020dd71808180f1baa0058a50407116407c6eb8c07c004dd7180f801180f800980d1baa0018b203032598009809980c9baa0018980e980d1baa0018b2030301c301d301d3019375400316405c601460306ea8c06cc060dd5000c590161bae3019002325980099b88001480022980103d87a8000899803802800a02a375a6032602c6ea8c06400698103d87a8000a60103d879800040513001003980d0014c068005003202e405c2225980080145300103d87a80008acc004c014006266e9520003301430150024bd70466002007301600299b80001480050032020404d14a080688c048c04c006264b30013002300e37540031323259800980a80144cc018018c0540162c8090dd6980980098079baa0018b201a3011002403c6e1d2000300c300937540066eb0c0300122c8038601200260086ea802629344d95900201", - "hash": "4e85c345d37150aeddcfbccf12f33318e8ffe9735971d429aa46d852" - }, - { - "title": "issuance_cbor_hex_mint.issuance_cbor_hex_mint.mint", - "redeemer": { - "title": "_redeemer", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "parameters": [ - { - "title": "utxo_ref", - "schema": { - "$ref": "#/definitions/cardano~1transaction~1OutputReference" - } - } - ], - "compiledCode": "590180010100229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cc0200092225980099b8748000c01cdd500144c8c966002601c0031325980099198008009bac300f300c375400a44b30010018a508acc004cdd7980818069baa30100010138a518998010011808800a016403915980099b8f375c601c00291010f49737375616e636543626f7248657800899b87375a601c601e0029001452820128a504024601c6eb0c0340062c8058c8cc004004c8cc004004dd59807180798079807980798059baa0042259800800c52f5c113233223322330020020012259800800c400e264660286e9ccc050dd48029980a18088009980a1809000a5eb80cc00c00cc058008c0500050121bab300f003375c6018002660060066022004601e0028068896600200314bd7044cc896600266e3cdd71808001002c4cc03cdd380119802002000c4cc01001000500b1bac300e001300f00140306eb8c02cc020dd50014590060c020004c00cdd5004452689b2b200201", - "hash": "bee64a4bff628e9f2b12a3f9374b76e094084932f1243eff123d987b" - }, - { - "title": "issuance_cbor_hex_mint.issuance_cbor_hex_mint.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "utxo_ref", - "schema": { - "$ref": "#/definitions/cardano~1transaction~1OutputReference" - } - } - ], - "compiledCode": "590180010100229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cc0200092225980099b8748000c01cdd500144c8c966002601c0031325980099198008009bac300f300c375400a44b30010018a508acc004cdd7980818069baa30100010138a518998010011808800a016403915980099b8f375c601c00291010f49737375616e636543626f7248657800899b87375a601c601e0029001452820128a504024601c6eb0c0340062c8058c8cc004004c8cc004004dd59807180798079807980798059baa0042259800800c52f5c113233223322330020020012259800800c400e264660286e9ccc050dd48029980a18088009980a1809000a5eb80cc00c00cc058008c0500050121bab300f003375c6018002660060066022004601e0028068896600200314bd7044cc896600266e3cdd71808001002c4cc03cdd380119802002000c4cc01001000500b1bac300e001300f00140306eb8c02cc020dd50014590060c020004c00cdd5004452689b2b200201", - "hash": "bee64a4bff628e9f2b12a3f9374b76e094084932f1243eff123d987b" - }, - { - "title": "issuance_mint.issuance_mint.mint", - "redeemer": { - "title": "redeemer", - "schema": { - "$ref": "#/definitions/types~1SmartTokenMintingAction" - } - }, - "parameters": [ - { - "title": "programmable_logic_base", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - }, - { - "title": "minting_logic_cred", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - } - ], - "compiledCode": "5903b30101002229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400130080024888966002600460106ea800e2664464b30013005300b375400f132598009808800c4c8c96600260100031323259800980a801401a2c8090dd7180980098079baa0028acc004c01400626464b300130150028034590121bae3013001300f37540051640348068c034dd50009808000c5900e18061baa0078b2014132598009808000c4c8c96600266ebcc044c038dd500480a4566002646600200264660020026eacc04cc050c050c050c050c050c050c040dd5004112cc004006297ae08998099808180a00099801001180a800a0242259800800c528456600266ebcc04c00405a294626600400460280028071011456600260086530010019bab3012301330133013301330133013301330133013300f375400f480010011112cc00400a200319800801cc05400a64b3001300b30113754602200315980099baf301200100d899b800024800a2004808220048080c05000900320248acc004c010c8cc0040040108966002003148002266e012002330020023014001404515980099b88480000062664464b3001300a30103754003159800980518081baa30143011375400315980099baf301430113754602860226ea800c062266e1e60026eacc008c044dd5001cdd7180a002c520004888cc88cc004004008c8c8cc004004014896600200300389919912cc004cdc8808801456600266e3c04400a20030064061133005005301e00440606eb8c05c004dd5980c000980d000a03014bd6f7b630112cc00400600713233225980099b910070028acc004cdc78038014400600c80ba26600a00a603a00880b8dd7180b0009bad30170013019001405c0048a50403d16403d16403c600260206ea8c04cc040dd500118089bac301130123012300e375400c46024602600315980099b880014800229462c806100c452820188a50403114a08062294100c1bad3010301100130103758601e003164034646600200264660020026eacc040c044c044c044c044c034dd5002912cc004006297ae08991991199119801001000912cc004006200713233016374e6602c6ea4014cc058c04c004cc058c0500052f5c0660060066030004602c00280a0dd598088019bae300e0013300300330130023011001403c44b30010018a5eb8226644b30013371e6eb8c04800801a2660226e9c008cc0100100062660080080028068dd618080009808800a01c375c601860126ea800cdc3a400516401c300800130033754011149a26cac80081", - "hash": "a5ed4b4e7813e17eec3b66f64dc5f2c6d11d62e7120d90a0f0acf450" - }, - { - "title": "issuance_mint.issuance_mint.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "programmable_logic_base", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - }, - { - "title": "minting_logic_cred", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - } - ], - "compiledCode": "5903b30101002229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400130080024888966002600460106ea800e2664464b30013005300b375400f132598009808800c4c8c96600260100031323259800980a801401a2c8090dd7180980098079baa0028acc004c01400626464b300130150028034590121bae3013001300f37540051640348068c034dd50009808000c5900e18061baa0078b2014132598009808000c4c8c96600266ebcc044c038dd500480a4566002646600200264660020026eacc04cc050c050c050c050c050c050c040dd5004112cc004006297ae08998099808180a00099801001180a800a0242259800800c528456600266ebcc04c00405a294626600400460280028071011456600260086530010019bab3012301330133013301330133013301330133013300f375400f480010011112cc00400a200319800801cc05400a64b3001300b30113754602200315980099baf301200100d899b800024800a2004808220048080c05000900320248acc004c010c8cc0040040108966002003148002266e012002330020023014001404515980099b88480000062664464b3001300a30103754003159800980518081baa30143011375400315980099baf301430113754602860226ea800c062266e1e60026eacc008c044dd5001cdd7180a002c520004888cc88cc004004008c8c8cc004004014896600200300389919912cc004cdc8808801456600266e3c04400a20030064061133005005301e00440606eb8c05c004dd5980c000980d000a03014bd6f7b630112cc00400600713233225980099b910070028acc004cdc78038014400600c80ba26600a00a603a00880b8dd7180b0009bad30170013019001405c0048a50403d16403d16403c600260206ea8c04cc040dd500118089bac301130123012300e375400c46024602600315980099b880014800229462c806100c452820188a50403114a08062294100c1bad3010301100130103758601e003164034646600200264660020026eacc040c044c044c044c044c034dd5002912cc004006297ae08991991199119801001000912cc004006200713233016374e6602c6ea4014cc058c04c004cc058c0500052f5c0660060066030004602c00280a0dd598088019bae300e0013300300330130023011001403c44b30010018a5eb8226644b30013371e6eb8c04800801a2660226e9c008cc0100100062660080080028068dd618080009808800a01c375c601860126ea800cdc3a400516401c300800130033754011149a26cac80081", - "hash": "a5ed4b4e7813e17eec3b66f64dc5f2c6d11d62e7120d90a0f0acf450" - }, - { - "title": "programmable_logic_base.programmable_logic_base.spend", - "datum": { - "title": "_datum", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "redeemer": { - "title": "_redeemer", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "parameters": [ - { - "title": "stake_cred", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - } - ], - "compiledCode": "5897010100229800aba2aba1aab9faab9eaab9dab9a48888896600264646644b30013370e900118031baa0018994c004c028006601460160032259800800c528456600266ebcc024c03000403e2946266004004601a002804100b2444660020026eacc034c038c038c038c038c038c038c02cdd518068040c01cdd5000c59005180380098039804000980380098019baa0078a4d1365640041", - "hash": "02d99634f91ec58f17903d274b8a51e06212fa0862de5569ae4a38d4" - }, - { - "title": "programmable_logic_base.programmable_logic_base.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "stake_cred", - "schema": { - "$ref": "#/definitions/cardano~1address~1Credential" - } - } - ], - "compiledCode": "5897010100229800aba2aba1aab9faab9eaab9dab9a48888896600264646644b30013370e900118031baa0018994c004c028006601460160032259800800c528456600266ebcc024c03000403e2946266004004601a002804100b2444660020026eacc034c038c038c038c038c038c038c02cdd518068040c01cdd5000c59005180380098039804000980380098019baa0078a4d1365640041", - "hash": "02d99634f91ec58f17903d274b8a51e06212fa0862de5569ae4a38d4" - }, - { - "title": "programmable_logic_global.programmable_logic_global.withdraw", - "redeemer": { - "title": "redeemer", - "schema": { - "$ref": "#/definitions/types~1ProgrammableLogicGlobalRedeemer" - } - }, - "parameters": [ - { - "title": "protocol_params_cs", - "schema": { - "$ref": "#/definitions/cardano~1assets~1PolicyId" - } - } - ], - "compiledCode": "590c44010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cc0240092225980099b8748010c020dd5001466002601860126ea800a6e1d20009b874800a446466002002006446600600260040053008375400691111192cc004c01400a264b300130140018998021bac3013001225980080140122646644b3001300b002899192cc004c06c00a0071640606eb4c064004c054dd5001c56600260140051323259800980d801400e2c80c0dd6980c800980a9baa0038b2026404c60246ea80044c008c05c00cc05400901345901118079baa0098acc004c01000a2646464653001375a602c003375a602c009375a602c0049112cc004c0680122660146eb0c064018896600200500a8991801180e8019bad301b002406516405c301600130150013014001300f375401316403480684cc88c8cc004004dd6180198089baa0092259800800c5a264b3001330043756600a60266ea8c014c04cdd500080844c8c8cc8966002601a602c6ea800a2646644b3001301e001899802180e8008cc004c030c068dd5002cdd7180e980d1baa0059180f180f980f980f980f980f980f800c8896600266e2400520008801456600200514bd70466002007302100299b8000148005003203c406d22598009809180d9baa002899191919194c004dd71812000cdd71812002cdd718120024c0900092222598009814802c4cc03cc0a001c4cc03c00402a2c8130604800260460026044002604200260386ea800a2c80d244646600200200644b30010018a508acc004cdd79810800801c528c4cc008008c08800501c203e4888888c8cc8966002603260446ea8076330012259800800c5268992cc00400629344c96600266e40dd7181318150019bae30260018998020021981480098158014590251814800a04e30290014099222232330010010052259800800c4cc0accdd81ba9005374c00897adef6c608994c004dd71814800cdd59815000cc0b80092225980099b9000900389981799bb037520126e980200162b30013371e01200719800804c022005233030337606ea4028dd30008014400500744cc0bccdd81ba9003374c0046600c00c002815902b0c0b000502a48888c8cc004004014896600200313302b337606ea4014dd400225eb7bdb1822653001375c6052003375a6054003302e00248896600266e4002400e26605e66ec0dd48049ba80080058acc004cdc7804801c6600201300880148cc0c0cdd81ba900a37500020051001401d13302f337606ea400cdd400119803003000a05640ac302c00140a9259800800c528c528204a891111192cc004c07c00600513003001409c66e0001000e444653001001802400d0011112cc00400a200319800801cc0b400a660086058004002801902a2444444664464944c8c8cc004004c8cc8a600200532330010010022259800800c52f5bded8c113298009bae30320019bab3033001981b8012444b30013371e911000038800c4cc0e0cdd81ba9003374c0046600c00c00281a0606a002819a6eb0c0ccc0c0dd501552f5bded8c0801088896600200715980099baf4c01018000374e00510018b2060899192cc0040122d1329800803cc0e401a607200b59800acc004c0acc0d0dd5000c4c8c8cc064004566002660506eacc0a4c0dcdd51814981b9baa00201c8acc004cdc79bae303a3037375400200913301801530243037375400314a081aa294103518111814181b1baa00133014008375a6070606a6ea800626464660320023300159800998141bab3029303737546052606e6ea80080722b3001337206eb8c0e8c0dcdd500080244cdc80021bae30293037375400314a081aa2941035528528a06a3022302830363754002660280106eb4c0e0c0d4dd5000a0668994c0040066eacc0d801200a80088896600200510018994c004012607a0073322598009818800c4012330010048044c8c8008c04c004cc0f4cdd81ba9002375000297adef6c6091111192cc004c06c0060051300300140fc653001004804401e444453001005801c012005001401880e140650172072375c60700026eb4c0e4005004181d801207288022066401c606e00881a8dd71819000981a801a06698009bac3031302e375404d4bd6f7b6304896600266ebcc0ccc0c0dd5181998181baa30223030375400402d13259800981398181baa0018992cc004c0a0c0c4dd5000c4c966002605260646ea8006264b300132330010013758607060726072607260726072607260726072606a6ea80b4896600200314a115980099b8f375c607200200714a3133002002303a00140d081ba26601400a6eacc098c0d0dd51813181a1baa0068b2064375c606c60666ea80062b300132330010013756603060686ea80b0896600200314a115980099baf303430380010038a51899801001181c800a06640d91330090043756604a60666ea8c094c0ccdd5002c5903120623035303237540031640c0606860626ea80062c8178c088c0c0dd5181998181baa302230303754005100140b88030dd6180f98169baa0252259800800c5268991991199119801001000912cc004006200713298009bae30330019bad303400199801801981c0012444b3001337120046466446600400400244b3001001801c4c8cc896600266e4402400a2b30013371e0120051001803207689980280298208022076375c60740026eb4c0ec004c0f400503b191919800800807912cc00400600713233225980099b9100f0028acc004cdc78078014400600c81e226600a00a608400881e0dd7181d8009bab303c001303e00140f0297adef6c601480022934590350c0d80050341bab3031003375c605c0026600600660660046062002817a60026eb0c060c0acdd5011d2f5bded8c1225980099baf3030302d37546060605a6ea800804e264b30013024302d3754003159800981218169baa3031302e375400313300400237566040605c6ea800e2c81622c8160c07cc0b4dd5181818169baa0028800a056400c600c00c44653001001801c0090011112cc00400a200313298008024c0cc00f30010029bae302e0019bab302f00191111192cc004c03c0060051300300140cc6465300100180340150011112cc00400a200313298008024c0f400f30010029bae30380019bad3039001802a0284010607600481c9403500b2008303100240bc899199119914c004dd698158014dd6981598160014c050c068c0a0dd5000cdd61815802244446601c00426644b30013370e653001001a40010044004444b30010018a400113233225980099baf303730343754606e60686ea8c098c0d0dd500180d456600266ebcc0dcc0d0dd5000981b981a1baa30263034375400715980099baf3026303437540026e9800a2b30013375e604260686ea8004c084c0d0dd51813181a1baa0038acc006600266ebcc098c0d0dd51813181a1baa003374c0054a14a281922600f30010069803802cc0e001100645903245903245903245903245903219198008009bab302530333754604a60666ea8008896600200314bd6f7b63044ca60026eb8c0d00066eacc0d400660720049112cc004cdc8806001c56600266e3c03000e2003100540d913303a337606ea400cdd300119803003000a06c181b800a06a330113758603e60646ea80a8cdc0005001998081bac3034303137540526eb4c0d0005032002c5660026603e6eacc080c0b8dd5181018171baa0070138acc004cc03c030c0c4c0c8c0c8c0c8c0b8dd500244cdc399198008009bac3032302f375404e44b30010018a4001133225980099baf303530323754606a60646ea8c090c0c8dd500100c44c01400620028180c0cc004cc008008c0d0005031002c52820588a5040b11640b06eb8c0bcc0b0dd50011b804800860540046600a6eb0c060c098dd500f00098140009bad30270013023375403a810888c9660020031689813800a04a3300300200132330010013756600c60446ea8068896600200314bd7044cc094c088c098004cc008008c09c0050241801801a2c80d8dd7180d800980e000980b9baa0028b202a30013007301537540064464b3001300e001899192cc004c07800a00916406c6eb8c070004c060dd5001c566002601a0031323259800980f00140122c80d8dd7180e000980c1baa0038b202c4058602c6ea80088c96600266e1d20043015375400313019301637540031640506004602a6ea80048c05cc060c060006266006006602e0048088c054005013118091809800911919800800801912cc0040062942264b30013372200800313371e008003133003003301700240446eb8c044c05400501322c8038601200260086ea802629344d95900201", - "hash": "0cba7ca3756d3ef8aaad90974325d02350639c96ae65da643babd00e" - }, - { - "title": "programmable_logic_global.programmable_logic_global.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "protocol_params_cs", - "schema": { - "$ref": "#/definitions/cardano~1assets~1PolicyId" - } - } - ], - "compiledCode": "590c44010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cc0240092225980099b8748010c020dd5001466002601860126ea800a6e1d20009b874800a446466002002006446600600260040053008375400691111192cc004c01400a264b300130140018998021bac3013001225980080140122646644b3001300b002899192cc004c06c00a0071640606eb4c064004c054dd5001c56600260140051323259800980d801400e2c80c0dd6980c800980a9baa0038b2026404c60246ea80044c008c05c00cc05400901345901118079baa0098acc004c01000a2646464653001375a602c003375a602c009375a602c0049112cc004c0680122660146eb0c064018896600200500a8991801180e8019bad301b002406516405c301600130150013014001300f375401316403480684cc88c8cc004004dd6180198089baa0092259800800c5a264b3001330043756600a60266ea8c014c04cdd500080844c8c8cc8966002601a602c6ea800a2646644b3001301e001899802180e8008cc004c030c068dd5002cdd7180e980d1baa0059180f180f980f980f980f980f980f800c8896600266e2400520008801456600200514bd70466002007302100299b8000148005003203c406d22598009809180d9baa002899191919194c004dd71812000cdd71812002cdd718120024c0900092222598009814802c4cc03cc0a001c4cc03c00402a2c8130604800260460026044002604200260386ea800a2c80d244646600200200644b30010018a508acc004cdd79810800801c528c4cc008008c08800501c203e4888888c8cc8966002603260446ea8076330012259800800c5268992cc00400629344c96600266e40dd7181318150019bae30260018998020021981480098158014590251814800a04e30290014099222232330010010052259800800c4cc0accdd81ba9005374c00897adef6c608994c004dd71814800cdd59815000cc0b80092225980099b9000900389981799bb037520126e980200162b30013371e01200719800804c022005233030337606ea4028dd30008014400500744cc0bccdd81ba9003374c0046600c00c002815902b0c0b000502a48888c8cc004004014896600200313302b337606ea4014dd400225eb7bdb1822653001375c6052003375a6054003302e00248896600266e4002400e26605e66ec0dd48049ba80080058acc004cdc7804801c6600201300880148cc0c0cdd81ba900a37500020051001401d13302f337606ea400cdd400119803003000a05640ac302c00140a9259800800c528c528204a891111192cc004c07c00600513003001409c66e0001000e444653001001802400d0011112cc00400a200319800801cc0b400a660086058004002801902a2444444664464944c8c8cc004004c8cc8a600200532330010010022259800800c52f5bded8c113298009bae30320019bab3033001981b8012444b30013371e911000038800c4cc0e0cdd81ba9003374c0046600c00c00281a0606a002819a6eb0c0ccc0c0dd501552f5bded8c0801088896600200715980099baf4c01018000374e00510018b2060899192cc0040122d1329800803cc0e401a607200b59800acc004c0acc0d0dd5000c4c8c8cc064004566002660506eacc0a4c0dcdd51814981b9baa00201c8acc004cdc79bae303a3037375400200913301801530243037375400314a081aa294103518111814181b1baa00133014008375a6070606a6ea800626464660320023300159800998141bab3029303737546052606e6ea80080722b3001337206eb8c0e8c0dcdd500080244cdc80021bae30293037375400314a081aa2941035528528a06a3022302830363754002660280106eb4c0e0c0d4dd5000a0668994c0040066eacc0d801200a80088896600200510018994c004012607a0073322598009818800c4012330010048044c8c8008c04c004cc0f4cdd81ba9002375000297adef6c6091111192cc004c06c0060051300300140fc653001004804401e444453001005801c012005001401880e140650172072375c60700026eb4c0e4005004181d801207288022066401c606e00881a8dd71819000981a801a06698009bac3031302e375404d4bd6f7b6304896600266ebcc0ccc0c0dd5181998181baa30223030375400402d13259800981398181baa0018992cc004c0a0c0c4dd5000c4c966002605260646ea8006264b300132330010013758607060726072607260726072607260726072606a6ea80b4896600200314a115980099b8f375c607200200714a3133002002303a00140d081ba26601400a6eacc098c0d0dd51813181a1baa0068b2064375c606c60666ea80062b300132330010013756603060686ea80b0896600200314a115980099baf303430380010038a51899801001181c800a06640d91330090043756604a60666ea8c094c0ccdd5002c5903120623035303237540031640c0606860626ea80062c8178c088c0c0dd5181998181baa302230303754005100140b88030dd6180f98169baa0252259800800c5268991991199119801001000912cc004006200713298009bae30330019bad303400199801801981c0012444b3001337120046466446600400400244b3001001801c4c8cc896600266e4402400a2b30013371e0120051001803207689980280298208022076375c60740026eb4c0ec004c0f400503b191919800800807912cc00400600713233225980099b9100f0028acc004cdc78078014400600c81e226600a00a608400881e0dd7181d8009bab303c001303e00140f0297adef6c601480022934590350c0d80050341bab3031003375c605c0026600600660660046062002817a60026eb0c060c0acdd5011d2f5bded8c1225980099baf3030302d37546060605a6ea800804e264b30013024302d3754003159800981218169baa3031302e375400313300400237566040605c6ea800e2c81622c8160c07cc0b4dd5181818169baa0028800a056400c600c00c44653001001801c0090011112cc00400a200313298008024c0cc00f30010029bae302e0019bab302f00191111192cc004c03c0060051300300140cc6465300100180340150011112cc00400a200313298008024c0f400f30010029bae30380019bad3039001802a0284010607600481c9403500b2008303100240bc899199119914c004dd698158014dd6981598160014c050c068c0a0dd5000cdd61815802244446601c00426644b30013370e653001001a40010044004444b30010018a400113233225980099baf303730343754606e60686ea8c098c0d0dd500180d456600266ebcc0dcc0d0dd5000981b981a1baa30263034375400715980099baf3026303437540026e9800a2b30013375e604260686ea8004c084c0d0dd51813181a1baa0038acc006600266ebcc098c0d0dd51813181a1baa003374c0054a14a281922600f30010069803802cc0e001100645903245903245903245903245903219198008009bab302530333754604a60666ea8008896600200314bd6f7b63044ca60026eb8c0d00066eacc0d400660720049112cc004cdc8806001c56600266e3c03000e2003100540d913303a337606ea400cdd300119803003000a06c181b800a06a330113758603e60646ea80a8cdc0005001998081bac3034303137540526eb4c0d0005032002c5660026603e6eacc080c0b8dd5181018171baa0070138acc004cc03c030c0c4c0c8c0c8c0c8c0b8dd500244cdc399198008009bac3032302f375404e44b30010018a4001133225980099baf303530323754606a60646ea8c090c0c8dd500100c44c01400620028180c0cc004cc008008c0d0005031002c52820588a5040b11640b06eb8c0bcc0b0dd50011b804800860540046600a6eb0c060c098dd500f00098140009bad30270013023375403a810888c9660020031689813800a04a3300300200132330010013756600c60446ea8068896600200314bd7044cc094c088c098004cc008008c09c0050241801801a2c80d8dd7180d800980e000980b9baa0028b202a30013007301537540064464b3001300e001899192cc004c07800a00916406c6eb8c070004c060dd5001c566002601a0031323259800980f00140122c80d8dd7180e000980c1baa0038b202c4058602c6ea80088c96600266e1d20043015375400313019301637540031640506004602a6ea80048c05cc060c060006266006006602e0048088c054005013118091809800911919800800801912cc0040062942264b30013372200800313371e008003133003003301700240446eb8c044c05400501322c8038601200260086ea802629344d95900201", - "hash": "0cba7ca3756d3ef8aaad90974325d02350639c96ae65da643babd00e" - }, - { - "title": "protocol_params_mint.protocol_params_mint.mint", - "redeemer": { - "title": "_redeemer", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "parameters": [ - { - "title": "utxo_ref", - "schema": { - "$ref": "#/definitions/cardano~1transaction~1OutputReference" - } - } - ], - "compiledCode": "59026f010100229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400130080024888966002600460106ea800e33001375c601860126ea800e4446466446600400400244b3001001801c4c8cc896600266e4401c00a2b30013371e00e00510018032020899802802980b0022020375c601e0026eb4c040004c048005010191919800800803112cc00400600713233225980099b910090028acc004cdc78048014400600c808a26600a00a602e0088088dd718080009bab301100130130014044297adef6c601480024601a601c601c002911192cc004c018c030dd5000c4c96600266e1d2004300d37540031332259800980498079baa00289919912cc004c05c00626464b3001300e001899192cc004c06c00a0111640606eb8c064004c054dd5001456600266e1d2002001899192cc004c06c00a0111640606eb8c064004c054dd5001459013202630133754002602c0031640506eb8c050004c054004c040dd500145900e180898071baa00115980099198008009bac3012300f375400e44b30010018a508acc004cdd7980998081baa30130010168a51899801001180a000a01c404513370f3001375660226024602460246024601c6ea801a00b48810e50726f746f636f6c506172616d730040109001452820188b20183002300d37546020601a6ea80062c8058c8cc004004dd6180118069baa0052259800800c530103d87a80008992cc004cdc4240013001375660246026601e6ea800600d48810e50726f746f636f6c506172616d7300401513374a900019808800a5eb8226600600660260048068c04400500f22c8038601000260066ea802229344d9590011", - "hash": "35af7fe099c1ec2ebe9a9ab9078b0516fcd19a430fc973774c586aaf" - }, - { - "title": "protocol_params_mint.protocol_params_mint.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "utxo_ref", - "schema": { - "$ref": "#/definitions/cardano~1transaction~1OutputReference" - } - } - ], - "compiledCode": "59026f010100229800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400130080024888966002600460106ea800e33001375c601860126ea800e4446466446600400400244b3001001801c4c8cc896600266e4401c00a2b30013371e00e00510018032020899802802980b0022020375c601e0026eb4c040004c048005010191919800800803112cc00400600713233225980099b910090028acc004cdc78048014400600c808a26600a00a602e0088088dd718080009bab301100130130014044297adef6c601480024601a601c601c002911192cc004c018c030dd5000c4c96600266e1d2004300d37540031332259800980498079baa00289919912cc004c05c00626464b3001300e001899192cc004c06c00a0111640606eb8c064004c054dd5001456600266e1d2002001899192cc004c06c00a0111640606eb8c064004c054dd5001459013202630133754002602c0031640506eb8c050004c054004c040dd500145900e180898071baa00115980099198008009bac3012300f375400e44b30010018a508acc004cdd7980998081baa30130010168a51899801001180a000a01c404513370f3001375660226024602460246024601c6ea801a00b48810e50726f746f636f6c506172616d730040109001452820188b20183002300d37546020601a6ea80062c8058c8cc004004dd6180118069baa0052259800800c530103d87a80008992cc004cdc4240013001375660246026601e6ea800600d48810e50726f746f636f6c506172616d7300401513374a900019808800a5eb8226600600660260048068c04400500f22c8038601000260066ea802229344d9590011", - "hash": "35af7fe099c1ec2ebe9a9ab9078b0516fcd19a430fc973774c586aaf" - }, - { - "title": "registry_mint.registry_mint.mint", - "redeemer": { - "title": "redeemer", - "schema": { - "$ref": "#/definitions/types~1RegistryRedeemer" - } - }, - "parameters": [ - { - "title": "utxo_ref", - "schema": { - "$ref": "#/definitions/cardano~1transaction~1OutputReference" - } - }, - { - "title": "issuance_cbor_hex_cs", - "schema": { - "$ref": "#/definitions/cardano~1assets~1PolicyId" - } - } - ], - "compiledCode": "5908970101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e266446644b300130060018acc004c038dd5004400a2c807a2b300130030018991919912cc004c05800e00d16404c6eb8c04c004dd71809801180980098071baa0088b20184030330012301030110019180818089808800c88c8cc00400400c896600200314a115980098019809800c528c4cc008008c05000500e20229b804800a4464b30013007001899192cc004c05400a0091640486eb8c04c004c03cdd5001c56600260080031323259800980a80140122c8090dd7180980098079baa0038b201a4034601a6ea800a6e1d200491191919800800802112cc00400600713233225980099b910070028acc004cdc78038014400600c809226600a00a60300088090dd718088009bab301200130140014048297adef6c609b8f48900918081808980898089808800c8c040c044c044c04400522222222229800999119912cc00400a26603898010180003301c374e00297ae08992cc004c8cc00400400c896600200314a315980099baf3020301d3754604000200713300200230210018a50406c80f226603a6e9c00ccc074dd380125eb822c80c8c074c068dd51807180d1baa301d002406c660046eb0c06cc060dd50079198011bab300d30193754601a60326ea800403ccc008dd61805980c1baa00f2330023756601a60326ea800403c88c8cc00400400c896600200314bd7044cc8966002600a00513301e00233004004001899802002000a034301d001301e001406c44646600200200644b30010018a508992cc004cdc8802000c4cdc7802000c4cc00c00cc07c0090191bae3019301d001406d2259800800c520008980499801001180e000a0329192cc004c01cc05cdd5000c4c06cc060dd5000c590161805180b9baa001912cc004c040c05cdd500144c8c8c8c8ca60026eb8c0800066eb8c0800166eb8c080012604000491112cc004c094016266022604800e2660220020151640883020001301f001301e001301d00130183754005164059222300e323322330020020012259800800c00e2646644b30013372200e00515980099b8f0070028800c01901d44cc014014c08c01101d1bae301c001375a603a002603e00280e8cc02001000c52000488888ca60024464b3001300e32330010010022259800800c52000899914c004dd718110014dd598118014896600200310038991991180c9980280298160021bae3025001375a604c00260500028131222330010010021812000998010011812800a0448992cc004c054c014006264660100022b3001337206eb8c090c084dd50009bae301530213754003159800981280144c8c96600260306eb4c08c00a2b30015980099b8f001375c604c60466ea800e294626020002810a20071640851640846eb8c084004c09000a2c81122c80f8c0200122c80f0cc03400400a2c80e8dd59809180f1baa0029bac301f30200069bac301f006488966002602e603c6ea80662b30013301137586044603e6ea80588cdd7981198101baa0010278acc004c08c00a264b3001598008014528c52820428acc004c034dd7181198101baa0018acc004cdc79bae30143020375400291011effffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008acc004cdd7980998101baa0014c0105d8799f40ff008acc004cdd7980598101baa0014c0105d8799f40ff008acc004c034dd7180618101baa0018cc004dd5980618101baa01780b522100401914a080f2294101e4528203c8a50407914a080f2294101e19801981100100ac590204528203a89919912cc004c068c084dd5000c4c966002603660446ea8006264646644b3001302b0038992cc004c0b002a26466020002264b30010038acc004cdc39b8d00a480e22b3001337206eb8c0b4c0a8dd5001005456600266e40028dd7180f18151baa0028acc00660026eacc058c0a8dd5010c08201480822b3001301932330010010022259800800c520008980e998010011818000a05a8acc004cc07000496600266e3cdd7181718159baa001375c605c60566ea800e266e3cdd7180f98159baa00100b8a5040a513301c0012325980099b8f375c605e60586ea80080322b30013371e6eb8c080c0b0dd50011bae3020302c37540091598009800980f98161baa0028acc004c004c05cc0b0dd5001456600260486e34dd7180c18161baa0028a51899b87371a6eb8c060c0b0dd5001240708152294102a452820548a5040a914a08150966002604860566ea8006266e24dc69bae302f302c3754002901c44cdc49b8d375c605e60586ea8005203840a914a081422941028452820508a5040a114a08142294102845282050323300100100d2259800800c52f5c113302d3300f302e00102133002002302f00140b06020603860506ea8c0ac02a2c814966002646600200264660020026eacc054c0a4dd5010112cc004006297ae08991991199119801001000912cc004006200713233032374e660646ea4014cc0c8c0bc004cc0c8c0c00052f5c066006006606800460640028180dd598168019bae302a00133003003302f002302d00140ac44b30010018a508acc004c96600266e3cdd7181680080544cdc4240006eb4c0b4c0b8c0b800629410281bac302c0018a518998010011816800a04e40a913371e6f20cdc5245010300337146eb8c0a8c09cdd500299b8a375c605460560106eb8c06cc09cdd5002803c528204a8b2050375c60500026eb8c0a0008c0a0004c08cdd5000c590211805180b18111baa3025302237540031640806eb8c08c004c8cc004004dd6180a98109baa0182259800800c530103d87a80008992cc0066002b3001330113756602e60466ea8c05cc08cdd50008104528c5282048a50a51408513374a900019812800a5eb82266006006604e0048108c094005023180f9baa019407430040040c02cdd50031bae300d300a37540066e1d20028b2010180480098021baa0098a4d1365640081", - "hash": "4bb3e05f4a1c6693906f4283c22372b09412295c1ddcc23dff0b042d" - }, - { - "title": "registry_mint.registry_mint.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "utxo_ref", - "schema": { - "$ref": "#/definitions/cardano~1transaction~1OutputReference" - } - }, - { - "title": "issuance_cbor_hex_cs", - "schema": { - "$ref": "#/definitions/cardano~1assets~1PolicyId" - } - } - ], - "compiledCode": "5908970101002229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cdc3a400130090024888966002600460126ea800e266446644b300130060018acc004c038dd5004400a2c807a2b300130030018991919912cc004c05800e00d16404c6eb8c04c004dd71809801180980098071baa0088b20184030330012301030110019180818089808800c88c8cc00400400c896600200314a115980098019809800c528c4cc008008c05000500e20229b804800a4464b30013007001899192cc004c05400a0091640486eb8c04c004c03cdd5001c56600260080031323259800980a80140122c8090dd7180980098079baa0038b201a4034601a6ea800a6e1d200491191919800800802112cc00400600713233225980099b910070028acc004cdc78038014400600c809226600a00a60300088090dd718088009bab301200130140014048297adef6c609b8f48900918081808980898089808800c8c040c044c044c04400522222222229800999119912cc00400a26603898010180003301c374e00297ae08992cc004c8cc00400400c896600200314a315980099baf3020301d3754604000200713300200230210018a50406c80f226603a6e9c00ccc074dd380125eb822c80c8c074c068dd51807180d1baa301d002406c660046eb0c06cc060dd50079198011bab300d30193754601a60326ea800403ccc008dd61805980c1baa00f2330023756601a60326ea800403c88c8cc00400400c896600200314bd7044cc8966002600a00513301e00233004004001899802002000a034301d001301e001406c44646600200200644b30010018a508992cc004cdc8802000c4cdc7802000c4cc00c00cc07c0090191bae3019301d001406d2259800800c520008980499801001180e000a0329192cc004c01cc05cdd5000c4c06cc060dd5000c590161805180b9baa001912cc004c040c05cdd500144c8c8c8c8ca60026eb8c0800066eb8c0800166eb8c080012604000491112cc004c094016266022604800e2660220020151640883020001301f001301e001301d00130183754005164059222300e323322330020020012259800800c00e2646644b30013372200e00515980099b8f0070028800c01901d44cc014014c08c01101d1bae301c001375a603a002603e00280e8cc02001000c52000488888ca60024464b3001300e32330010010022259800800c52000899914c004dd718110014dd598118014896600200310038991991180c9980280298160021bae3025001375a604c00260500028131222330010010021812000998010011812800a0448992cc004c054c014006264660100022b3001337206eb8c090c084dd50009bae301530213754003159800981280144c8c96600260306eb4c08c00a2b30015980099b8f001375c604c60466ea800e294626020002810a20071640851640846eb8c084004c09000a2c81122c80f8c0200122c80f0cc03400400a2c80e8dd59809180f1baa0029bac301f30200069bac301f006488966002602e603c6ea80662b30013301137586044603e6ea80588cdd7981198101baa0010278acc004c08c00a264b3001598008014528c52820428acc004c034dd7181198101baa0018acc004cdc79bae30143020375400291011effffffffffffffffffffffffffffffffffffffffffffffffffffffffffff008acc004cdd7980998101baa0014c0105d8799f40ff008acc004cdd7980598101baa0014c0105d8799f40ff008acc004c034dd7180618101baa0018cc004dd5980618101baa01780b522100401914a080f2294101e4528203c8a50407914a080f2294101e19801981100100ac590204528203a89919912cc004c068c084dd5000c4c966002603660446ea8006264646644b3001302b0038992cc004c0b002a26466020002264b30010038acc004cdc39b8d00a480e22b3001337206eb8c0b4c0a8dd5001005456600266e40028dd7180f18151baa0028acc00660026eacc058c0a8dd5010c08201480822b3001301932330010010022259800800c520008980e998010011818000a05a8acc004cc07000496600266e3cdd7181718159baa001375c605c60566ea800e266e3cdd7180f98159baa00100b8a5040a513301c0012325980099b8f375c605e60586ea80080322b30013371e6eb8c080c0b0dd50011bae3020302c37540091598009800980f98161baa0028acc004c004c05cc0b0dd5001456600260486e34dd7180c18161baa0028a51899b87371a6eb8c060c0b0dd5001240708152294102a452820548a5040a914a08150966002604860566ea8006266e24dc69bae302f302c3754002901c44cdc49b8d375c605e60586ea8005203840a914a081422941028452820508a5040a114a08142294102845282050323300100100d2259800800c52f5c113302d3300f302e00102133002002302f00140b06020603860506ea8c0ac02a2c814966002646600200264660020026eacc054c0a4dd5010112cc004006297ae08991991199119801001000912cc004006200713233032374e660646ea4014cc0c8c0bc004cc0c8c0c00052f5c066006006606800460640028180dd598168019bae302a00133003003302f002302d00140ac44b30010018a508acc004c96600266e3cdd7181680080544cdc4240006eb4c0b4c0b8c0b800629410281bac302c0018a518998010011816800a04e40a913371e6f20cdc5245010300337146eb8c0a8c09cdd500299b8a375c605460560106eb8c06cc09cdd5002803c528204a8b2050375c60500026eb8c0a0008c0a0004c08cdd5000c590211805180b18111baa3025302237540031640806eb8c08c004c8cc004004dd6180a98109baa0182259800800c530103d87a80008992cc0066002b3001330113756602e60466ea8c05cc08cdd50008104528c5282048a50a51408513374a900019812800a5eb82266006006604e0048108c094005023180f9baa019407430040040c02cdd50031bae300d300a37540066e1d20028b2010180480098021baa0098a4d1365640081", - "hash": "4bb3e05f4a1c6693906f4283c22372b09412295c1ddcc23dff0b042d" - }, - { - "title": "registry_spend.registry_spend.spend", - "datum": { - "title": "_datum", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "redeemer": { - "title": "_redeemer", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "parameters": [ - { - "title": "protocol_params_cs", - "schema": { - "$ref": "#/definitions/cardano~1assets~1PolicyId" - } - } - ], - "compiledCode": "590263010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cc0240092225980099b8748008c020dd500144cc8a6002601c005300e300f00299198008009bac3002300c375400844b30010018a60103d87a80008992cc004c8cc004004dd5980298079baa3005300f375400444b30010018a508992cc004cdc8807000c4cdc7807000c4cc00c00cc05400900f1bae300f3013001404513374a900019808000a5eb8226600600660240048060c04000500e4dc3a400091112cc004c004c038dd500144c8c966002600660206ea800a2646644b30013018001899192cc004c02000626464b3001301c0028044590191bae301a0013016375400515980099b874800800626464b3001301c0028044590191bae301a0013016375400516405080a0c050dd5000980b800c590151bae301500130160013011375400516403c264646600200264660020026eacc058c05cc05cc05cc05cc04cdd5005912cc004006297ae08991991199119801001000912cc00400620071323301c374e660386ea4014cc070c064004cc070c0680052f5c066006006603c004603800280d0dd5980b8019bae30140013300300330190023017001405444b30010018a508acc004c96600266e3cdd7180b800802466002600c6eb4c05cc060c060006942945012452820243758602c00314a31330020023017001404480a0dd7180998081baa001325980099b8748010c03cdd5000c4c04cc040dd5000c5900e18091809980998079baa3005300f37546024601e6ea800a2c806860126ea80088c034c0380062c8038601200260086ea802629344d9590021", - "hash": "bcf996465f1cdccd7d94068eabc9a7c1213e7b471592fdf5d79b6ec8" - }, - { - "title": "registry_spend.registry_spend.else", - "redeemer": { - "schema": {} - }, - "parameters": [ - { - "title": "protocol_params_cs", - "schema": { - "$ref": "#/definitions/cardano~1assets~1PolicyId" - } - } - ], - "compiledCode": "590263010100229800aba2aba1aba0aab9faab9eaab9dab9a9bae0024888888896600264653001300900198049805000cc0240092225980099b8748008c020dd500144cc8a6002601c005300e300f00299198008009bac3002300c375400844b30010018a60103d87a80008992cc004c8cc004004dd5980298079baa3005300f375400444b30010018a508992cc004cdc8807000c4cdc7807000c4cc00c00cc05400900f1bae300f3013001404513374a900019808000a5eb8226600600660240048060c04000500e4dc3a400091112cc004c004c038dd500144c8c966002600660206ea800a2646644b30013018001899192cc004c02000626464b3001301c0028044590191bae301a0013016375400515980099b874800800626464b3001301c0028044590191bae301a0013016375400516405080a0c050dd5000980b800c590151bae301500130160013011375400516403c264646600200264660020026eacc058c05cc05cc05cc05cc04cdd5005912cc004006297ae08991991199119801001000912cc00400620071323301c374e660386ea4014cc070c064004cc070c0680052f5c066006006603c004603800280d0dd5980b8019bae30140013300300330190023017001405444b30010018a508acc004c96600266e3cdd7180b800802466002600c6eb4c05cc060c060006942945012452820243758602c00314a31330020023017001404480a0dd7180998081baa001325980099b8748010c03cdd5000c4c04cc040dd5000c5900e18091809980998079baa3005300f37546024601e6ea800a2c806860126ea80088c034c0380062c8038601200260086ea802629344d9590021", - "hash": "bcf996465f1cdccd7d94068eabc9a7c1213e7b471592fdf5d79b6ec8" - } - ], - "definitions": { - "ByteArray": { - "title": "ByteArray", - "dataType": "bytes" - }, - "Data": { - "title": "Data", - "description": "Any Plutus data." - }, - "Int": { - "dataType": "integer" - }, - "List$Int": { - "dataType": "list", - "items": { - "$ref": "#/definitions/Int" - } - }, - "List$types/BlacklistProof": { - "dataType": "list", - "items": { - "$ref": "#/definitions/types~1BlacklistProof" - } - }, - "List$types/RegistryProof": { - "dataType": "list", - "items": { - "$ref": "#/definitions/types~1RegistryProof" - } - }, - "aiken/crypto/ScriptHash": { - "title": "ScriptHash", - "dataType": "bytes" - }, - "aiken/crypto/VerificationKeyHash": { - "title": "VerificationKeyHash", - "dataType": "bytes" - }, - "cardano/address/Credential": { - "title": "Credential", - "description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).", - "anyOf": [ - { - "title": "VerificationKey", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" - } - ] - }, - { - "title": "Script", - "dataType": "constructor", - "index": 1, - "fields": [ - { - "$ref": "#/definitions/aiken~1crypto~1ScriptHash" - } - ] - } - ] - }, - "cardano/assets/PolicyId": { - "title": "PolicyId", - "dataType": "bytes" - }, - "cardano/transaction/OutputReference": { - "title": "OutputReference", - "description": "An `OutputReference` is a unique reference to an output on-chain. The `output_index`\n corresponds to the position in the output list of the transaction (identified by its id)\n that produced that output", - "anyOf": [ - { - "title": "OutputReference", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "title": "transaction_id", - "$ref": "#/definitions/ByteArray" - }, - { - "title": "output_index", - "$ref": "#/definitions/Int" - } - ] - } - ] - }, - "types/BlacklistProof": { - "title": "BlacklistProof", - "description": "Proof of blacklist membership or non-membership", - "anyOf": [ - { - "title": "NonmembershipProof", - "description": "Proof of non-membership via covering node", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "title": "node_idx", - "$ref": "#/definitions/Int" - } - ] - } - ] - }, - "types/BlacklistRedeemer": { - "title": "BlacklistRedeemer", - "description": "Redeemer for blacklist minting policy (linked list operations)", - "anyOf": [ - { - "title": "BlacklistInit", - "description": "Initialize the blacklist with the origin node", - "dataType": "constructor", - "index": 0, - "fields": [] - }, - { - "title": "BlacklistInsert", - "description": "Insert a credential into the blacklist", - "dataType": "constructor", - "index": 1, - "fields": [ - { - "title": "key", - "$ref": "#/definitions/ByteArray" - } - ] - }, - { - "title": "BlacklistRemove", - "description": "Remove a credential from the blacklist", - "dataType": "constructor", - "index": 2, - "fields": [ - { - "title": "key", - "$ref": "#/definitions/ByteArray" - } - ] - } - ] - }, - "types/ProgrammableLogicGlobalRedeemer": { - "title": "ProgrammableLogicGlobalRedeemer", - "description": "Redeemer for the global programmable logic stake validator", - "anyOf": [ - { - "title": "TransferAct", - "description": "Transfer action with proofs for each token type", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "title": "proofs", - "$ref": "#/definitions/List$types~1RegistryProof" - } - ] - }, - { - "title": "ThirdPartyAct", - "description": "Third party action for admin operations (seizure, freeze, etc.)\n Supports multiple UTxOs in a single transaction", - "dataType": "constructor", - "index": 1, - "fields": [ - { - "title": "registry_node_idx", - "description": "Index of the registry node in reference inputs", - "$ref": "#/definitions/Int" - }, - { - "title": "input_idxs", - "description": "List of input indices to process (supports multi-UTxO operations)", - "$ref": "#/definitions/List$Int" - }, - { - "title": "outputs_start_idx", - "description": "Starting index in outputs where processed outputs begin", - "$ref": "#/definitions/Int" - }, - { - "title": "length_input_idxs", - "description": "Length of input_idxs list (for validation)", - "$ref": "#/definitions/Int" - } - ] - } - ] - }, - "types/RegistryProof": { - "title": "RegistryProof", - "description": "Proof that a token is (or is not) in the registry", - "anyOf": [ - { - "title": "TokenExists", - "description": "Token exists in the registry at this reference input index", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "title": "node_idx", - "$ref": "#/definitions/Int" - } - ] - }, - { - "title": "TokenDoesNotExist", - "description": "Token does not exist (proof via covering node at this index)", - "dataType": "constructor", - "index": 1, - "fields": [ - { - "title": "node_idx", - "$ref": "#/definitions/Int" - } - ] - } - ] - }, - "types/RegistryRedeemer": { - "title": "RegistryRedeemer", - "description": "Redeemer for registry minting policy (linked list operations)", - "anyOf": [ - { - "title": "RegistryInit", - "description": "Initialize the registry with the origin node", - "dataType": "constructor", - "index": 0, - "fields": [] - }, - { - "title": "RegistryInsert", - "description": "Insert a new programmable token policy into the registry", - "dataType": "constructor", - "index": 1, - "fields": [ - { - "title": "key", - "$ref": "#/definitions/ByteArray" - }, - { - "title": "hashed_param", - "$ref": "#/definitions/ByteArray" - } - ] - } - ] - }, - "types/SmartTokenMintingAction": { - "title": "SmartTokenMintingAction", - "description": "Action for the minting policy", - "anyOf": [ - { - "title": "SmartTokenMintingAction", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "title": "minting_logic_cred", - "description": "The credential of the minting logic script that must be invoked", - "$ref": "#/definitions/cardano~1address~1Credential" - } - ] - } - ] - } - } -} \ No newline at end of file diff --git a/src/programmable-tokens/aiken-workspace/validators/programmable_logic_base.ak b/src/programmable-tokens/aiken-workspace/validators/programmable_logic_base.ak deleted file mode 100644 index a192711..0000000 --- a/src/programmable-tokens/aiken-workspace/validators/programmable_logic_base.ak +++ /dev/null @@ -1,36 +0,0 @@ -use aiken/collection/list -// Programmable Logic Base Validator -// This validator locks all programmable token UTxOs -// It forwards validation to the global programmable logic stake validator -// Migrated from SmartTokens.Contracts.ProgrammableLogicBase - -use cardano/address.{Credential} -use cardano/transaction.{Transaction} - -validator programmable_logic_base(stake_cred: Credential) { - spend( - _datum: Option, - _redeemer: Data, - _own_ref: Data, - self: Transaction, - ) { - trace @"Starting programmable_logic_base validation" - - // The programmable logic base validator simply checks that the - // global programmable logic stake script is invoked in the transaction - // via the withdraw-zero pattern - // Check that the stake credential is invoked - // Optimization: check first withdrawal before scanning the rest - list.any( - self.withdrawals, - fn(wdrl) { - let Pair(cred, _amount) = wdrl - cred == stake_cred - }, - ) - } - - else(_) { - fail - } -} diff --git a/src/programmable-tokens/aiken-workspace/validators/programmable_logic_base.test.ak b/src/programmable-tokens/aiken-workspace/validators/programmable_logic_base.test.ak deleted file mode 100644 index f202ffa..0000000 --- a/src/programmable-tokens/aiken-workspace/validators/programmable_logic_base.test.ak +++ /dev/null @@ -1,143 +0,0 @@ -// Integration tests for programmable logic base validator -use aiken/collection/list -use cardano/address.{Address, Credential, Inline, Script, VerificationKey} - -// Test data -const test_stake_cred: Credential = Script(#"aabbccdd") - -const test_other_cred: Credential = Script(#"11223344") - -// const test_address = -// Address { -// payment_credential: Script(#"deadbeef"), -// stake_credential: Some(Inline(test_stake_cred)), -// } - -// Test stake credential invocation via withdrawals -test stake_credential_invoked_via_withdrawals() { - let withdrawals = [Pair(test_stake_cred, 0)] - - list.any( - withdrawals, - fn(wdrl) { - let Pair(cred, _amount) = wdrl - cred == test_stake_cred - }, - ) -} - -// Test stake credential not invoked fails -test stake_credential_not_invoked_fails() { - let withdrawals = [Pair(test_other_cred, 0)] - - !list.any( - withdrawals, - fn(wdrl) { - let Pair(cred, _amount) = wdrl - cred == test_stake_cred - }, - ) -} - -// Test withdraw-zero pattern -test withdraw_zero_pattern() { - let Pair(cred, amount) = Pair(test_stake_cred, 0) - - cred == test_stake_cred && amount == 0 -} - -// Test multiple withdrawals with stake credential present -test multiple_withdrawals_with_stake_credential() { - let withdrawals = - [ - Pair(test_other_cred, 0), - Pair(test_stake_cred, 0), - Pair(Script(#"99999999"), 0), - ] - - list.any( - withdrawals, - fn(wdrl) { - let Pair(cred, _amount) = wdrl - cred == test_stake_cred - }, - ) -} - -// Test empty withdrawals list -test empty_withdrawals_list() { - let withdrawals = [] - - !list.any( - withdrawals, - fn(wdrl) { - let Pair(cred, _amount) = wdrl - cred == test_stake_cred - }, - ) -} - -// Test credential equality -test credential_equality() { - let cred1 = Script(#"aabbccdd") - let cred2 = Script(#"aabbccdd") - let cred3 = Script(#"11223344") - - cred1 == cred2 && cred1 != cred3 -} - -// Test stake credential types -test stake_credential_types() { - let script_stake = Script(#"aabbccdd") - let vkey_stake = VerificationKey(#"11223344") - - when script_stake is { - Script(_) -> True - _ -> False - } && when vkey_stake is { - VerificationKey(_) -> True - _ -> False - } -} - -// Test address with inline stake credential -test address_with_inline_stake_credential() { - let addr = - Address { - payment_credential: Script(#"deadbeef"), - stake_credential: Some(Inline(test_stake_cred)), - } - - when addr.stake_credential is { - Some(Inline(stake)) -> stake == test_stake_cred - _ -> False - } -} - -// Test programmable logic base forwards to global -test programmable_logic_base_forwards_to_global() { - // The base validator's job is simple: ensure the global stake validator runs - // This is done via the withdraw-zero pattern - - let global_stake_cred = Script(#"676c6f62616c") - let withdrawals = [Pair(global_stake_cred, 0)] - - // Global stake validator is invoked - list.any( - withdrawals, - fn(wdrl) { - let Pair(cred, _amount) = wdrl - cred == global_stake_cred - }, - ) -} - -// Test validator datum and redeemer are ignored -test validator_datum_and_redeemer_ignored() { - // The programmable_logic_base validator ignores datum and redeemer - // It only checks that the stake validator is invoked - let datum: Option = None - - // Validator doesn't care about these values - datum == None -} diff --git a/src/programmable-tokens/aiken-workspace/validators/programmable_logic_global.ak b/src/programmable-tokens/aiken-workspace/validators/programmable_logic_global.ak deleted file mode 100644 index 24b3342..0000000 --- a/src/programmable-tokens/aiken-workspace/validators/programmable_logic_global.ak +++ /dev/null @@ -1,498 +0,0 @@ -use aiken/collection/dict.{Dict} -use aiken/collection/list -use aiken/collection/pairs -// Programmable Logic Global Stake Validator -// This is the core CIP-0143 validator that coordinates all programmable token transfers -// Migrated from SmartTokens.Contracts.ProgrammableLogicBase (mkProgrammableLogicGlobal) - -use cardano/address.{Credential, Inline, Script, VerificationKey} -use cardano/assets.{PolicyId, Value, ada_policy_id, merge, zero} -use cardano/transaction.{Input, Output, Transaction} -use types.{ - ProgrammableLogicGlobalParams, ProgrammableLogicGlobalRedeemer, RegistryNode, - RegistryProof, ThirdPartyAct, TokenDoesNotExist, TokenExists, TransferAct, -} -use utils.{ - bytearray_lt, elem_at, expect_inline_datum, expect_value_contains_v3, - has_currency_symbol, is_script_invoked, is_signed_by, -} - -validator programmable_logic_global(protocol_params_cs: PolicyId) { - withdraw( - redeemer: ProgrammableLogicGlobalRedeemer, - _account: Credential, - self: Transaction, - ) { - trace @"Starting programmable_logic_global validation" - - // Extract protocol parameters from reference inputs - let protocol_params_ref <- - get_protocol_params_ref(protocol_params_cs, self.reference_inputs) - - let protocol_params_utxo = protocol_params_ref.output - let params_datum = expect_inline_datum(protocol_params_utxo) - expect params: ProgrammableLogicGlobalParams = params_datum - - let prog_logic_cred = params.prog_logic_cred - let registry_node_cs = params.registry_node_cs - - // Get all invoked scripts via withdrawals - let invoked_scripts = - list.map( - self.withdrawals, - fn(wdrl) { - let Pair(cred, _amount) = wdrl - cred - }, - ) - when redeemer is { - TransferAct { proofs } -> - validate_transfer( - self, - prog_logic_cred, - registry_node_cs, - proofs, - invoked_scripts, - ) - - ThirdPartyAct { - registry_node_idx, - input_idxs, - outputs_start_idx, - length_input_idxs, - } -> - validate_third_party( - self, - prog_logic_cred, - registry_node_cs, - registry_node_idx, - input_idxs, - outputs_start_idx, - length_input_idxs, - invoked_scripts, - ) - } - } - - else(_) { - fail - } -} - -fn get_protocol_params_ref( - protocol_params_cs: PolicyId, - ref_inputs: List, - return: fn(Input) -> result, -) -> result { - when ref_inputs is { - [] -> fail @"Protocol parameters reference input not found" - [first_ref, ..rest_refs] -> - if has_currency_symbol(first_ref.output.value, protocol_params_cs) { - return(first_ref) - } else { - get_protocol_params_ref(protocol_params_cs, rest_refs, return) - } - } -} - -/// Validate a transfer action -fn validate_transfer( - tx: Transaction, - prog_logic_cred: Credential, - registry_node_cs: PolicyId, - proofs: List, - invoked_scripts: List, -) -> Bool { - // Calculate the total value of programmable tokens being transferred - // by summing all inputs from the programmable logic credential - - // Get all inputs from prog_logic_cred where the owner (stake credential) has signed - // let prog_inputs = get_signed_prog_inputs(tx, prog_logic_cred) - - // Sum up the value from those inputs - // let total_prog_value = sum_input_values(prog_inputs) - let total_prog_value = get_signed_prog_value(tx, prog_logic_cred) - - // Check transfer logic for each programmable token type and filter to only programmable tokens - let validated_prog_value = - check_transfer_and_compute_prog_value( - total_prog_value, - registry_node_cs, - tx.reference_inputs, - proofs, - invoked_scripts, - ) - - let output_prog_value = - list.foldl( - tx.outputs, - assets.zero, - fn(output, acc) { - if output.address.payment_credential == prog_logic_cred { - expect Some(Inline(_stake_cred)) = output.address.stake_credential - // expensive - merge(acc, output.value) - } else { - acc - } - }, - ) - - // The output must contain all the validated programmable tokens - expect_value_contains_v3(output_prog_value, validated_prog_value) - True -} - -/// Validate a third party action (seizure, freeze, etc.) on multiple UTxOs -/// Migrated from Plutarch multi-seize PR #99 -fn validate_third_party( - tx: Transaction, - prog_logic_cred: Credential, - registry_node_cs: PolicyId, - registry_node_idx: Int, - input_idxs: List, - outputs_start_idx: Int, - length_input_idxs: Int, - invoked_scripts: List, -) -> Bool { - trace @"validate_third_party: processing multiple inputs" - - // Verify length parameter matches actual list length - // expect list.length(input_idxs) == length_input_idxs - // Get the registry node for this token policy - let registry_ref_input = elem_at(tx.reference_inputs, registry_node_idx) - let registry_datum = expect_inline_datum(registry_ref_input.output) - expect registry_node: RegistryNode = registry_datum - - // Get the policy ID from the registry - let policy_id = registry_node.key - - // Process each input-output pair - // For each input at input_idxs[i], there should be a corresponding output at outputs_start_idx + i - let input_size = - list.indexed_foldr( - input_idxs, - 0, - fn(idx, input_idx, acc) { - // TODO: improvements Check if the inputs are ordered - // Get the input being processed - let input = elem_at(tx.inputs, input_idx) - - // Calculate corresponding output index - let output_idx = outputs_start_idx + idx - let output = elem_at(tx.outputs, output_idx) - - let expected_output_dict = - assets.to_dict(input.output.value) |> dict.delete(policy_id) - - // // Filter the input value to only include tokens from this policy - // let filtered_value = restricted_to(input.output.value, [policy_id]) - // // Expected output value is input value with policy tokens removed - // let expected_output_value = - // merge(input.output.value, negate(filtered_value)) - // Must be from prog_logic_cred - expect input.output.address.payment_credential == prog_logic_cred - // Validate the output matches expectations: - // - Same address - // - Value with tokens removed - // - Same datum - expect output.address == input.output.address - expect assets.to_dict(output.value) == expected_output_dict - expect output.datum == input.output.datum - // Prevent DDOS: must actually remove some tokens - expect assets.to_dict(input.output.value) != expected_output_dict - acc + 1 - }, - ) - - expect input_size == length_input_idxs - // Verify that ONLY the specified inputs from prog_logic_cred are consumed - // Count how many inputs from prog_logic_cred are in the transaction - let prog_input_count = - list.count( - tx.inputs, - fn(input) { input.output.address.payment_credential == prog_logic_cred }, - ) - and { - // Validate registry node is authentic - has_currency_symbol(registry_ref_input.output.value, registry_node_cs), - // The third party transfer logic script must be invoked - list.has(invoked_scripts, registry_node.third_party_transfer_logic_script), - // matching_outputs, - // Must match the number of inputs we're processing - prog_input_count == length_input_idxs, - } -} - -fn get_signed_prog_value(tx: Transaction, prog_logic_cred: Credential) -> Value { - list.foldl( - tx.inputs, - assets.zero, - fn(input, acc) { - if input.output.address.payment_credential == prog_logic_cred { - expect Some(Inline(stake_cred)) = input.output.address.stake_credential - - when stake_cred is { - VerificationKey(pkh) -> - if is_signed_by(tx, pkh) { - merge(acc, input.output.value) - } else { - fail @"Missing required pk witness" - } - Script(_hash) -> - if is_script_invoked(tx, stake_cred) { - merge(acc, input.output.value) - } else { - fail @"Missing required script witness" - } - } - } else { - acc - } - }, - ) -} - -// fn get_signed_prog_inputs( -// tx: Transaction, -// prog_logic_cred: Credential, -// ) -> List { -// // First, collect ALL inputs from prog_logic_cred -// let prog_cred_inputs = -// list.filter( -// tx.inputs, -// fn(input) { input.output.address.payment_credential == prog_logic_cred }, -// ) - -// // Verify each input is properly authorized and return them -// // If any input is NOT authorized, the transaction fails with an error -// list.map( -// prog_cred_inputs, -// fn(input) { -// expect Some(Inline(stake_cred)) = input.output.address.stake_credential - -// when stake_cred is { -// VerificationKey(pkh) -> -// if is_signed_by(tx, pkh) { -// input -// } else { -// fail @"Missing required pk witness" -// } -// Script(_hash) -> -// if is_script_invoked(tx, stake_cred) { -// input -// } else { -// fail @"Missing required script witness" -// } -// } -// }, -// ) -// } - -// fn sum_input_values(inputs: List) -> Value { -// list.foldl(inputs, zero, fn(input, acc) { merge(acc, input.output.value) }) -// } - -// fn check_transfer_logic_and_filter( -// value: Value, -// registry_node_cs: PolicyId, -// reference_inputs: List, -// proofs: List, -// invoked_scripts: List, -// ) -> Value { -// expect [_, ..currency_symbols] = policies(value) -// // For each currency symbol, check the proof -// validate_currency_symbols( -// currency_symbols, -// proofs, -// reference_inputs, -// registry_node_cs, -// invoked_scripts, -// value, -// // Pass the original value so we can filter by CS -// zero, -// ) -// } - -/// Get inputs from prog_logic_cred and verify ALL have proper authorization -/// This function enforces ownership by requiring that every input from prog_logic_cred -/// is authorized by its stake credential (signature or script invocation). -/// If ANY input lacks authorization, the transaction FAILS. -/// Sum values from a list of inputs -/// Check transfer logic for each currency symbol and filter to only programmable tokens -fn check_transfer_and_compute_prog_value( - value: Value, - registry_node_cs: PolicyId, - reference_inputs: List, - proofs: List, - invoked_scripts: List, -) -> Value { - let - remaining_proofs, - acc_value, - <- - do_check_transfer( - value - |> assets.to_dict - |> dict.to_pairs - |> pairs.delete_first(ada_policy_id), - registry_node_cs, - reference_inputs, - invoked_scripts, - proofs, - zero, - ) - expect [] == remaining_proofs - acc_value -} - -fn do_check_transfer( - my_value: Pairs>, - registry_node_cs: PolicyId, - reference_inputs: List, - invoked_scripts: List, - proofs: List, - acc: Value, - return: fn(List, Value) -> result, -) -> result { - when my_value is { - [] -> return(proofs, acc) - [Pair(cs, tokens), ..rest] -> - when proofs is { - [] -> fail @"Not enough proofs for currency symbols" - [proof, ..rest_proofs] -> { - let validated = - validate_single_cs( - cs, - proof, - reference_inputs, - registry_node_cs, - invoked_scripts, - ) - - let new_acc = - if validated { - // Add this CS's tokens from the original value to the accumulator - dict.foldl( - tokens, - acc, - fn(name, amount, acc) { assets.add(acc, cs, name, amount) }, - ) - } else { - // Not a programmable token, skip it - acc - } - - do_check_transfer( - rest, - registry_node_cs, - reference_inputs, - invoked_scripts, - rest_proofs, - new_acc, - return, - ) - } - } - } -} - -/// Validate each currency symbol against proofs -fn validate_currency_symbols( - currency_symbols: List, - proofs: List, - reference_inputs: List, - registry_node_cs: PolicyId, - invoked_scripts: List, - original_value: Value, - acc_value: Value, -) -> Value { - // By the looks of it this multy array traversing/foldin requires the currency_symbols and proofs to be - // in the right order. Plutarch data structure makes it implicit to have policies in order. - // In aiken seems to be enforced at pairs level where keys are ordered lexicographically. - // Is this correct? - - when currency_symbols is { - [] -> acc_value - [cs, ..rest_cs] -> - when proofs is { - [] -> fail @"Not enough proofs for currency symbols" - [proof, ..rest_proofs] -> { - let validated = - validate_single_cs( - cs, - proof, - reference_inputs, - registry_node_cs, - invoked_scripts, - ) - - let new_acc = - if validated { - // Add this CS's tokens from the original value to the accumulator - dict.foldl( - assets.tokens(original_value, cs), - acc_value, - fn(name, amount, acc) { assets.add(acc, cs, name, amount) }, - ) - } else { - // Not a programmable token, skip it - acc_value - } - - validate_currency_symbols( - rest_cs, - rest_proofs, - reference_inputs, - registry_node_cs, - invoked_scripts, - original_value, - new_acc, - ) - } - } - } -} - -/// Validate a single currency symbol against its proof -fn validate_single_cs( - cs: PolicyId, - proof: RegistryProof, - reference_inputs: List, - registry_node_cs: PolicyId, - invoked_scripts: List, -) -> Bool { - when proof is { - TokenExists { node_idx } -> { - // Get the registry node - let node_input = elem_at(reference_inputs, node_idx) - let node_datum = expect_inline_datum(node_input.output) - expect node: RegistryNode = node_datum - - and { - // Validate this is a legitimate registry node - has_currency_symbol(node_input.output.value, registry_node_cs), - // Validate the node's key matches the currency symbol - node.key == cs, - // Validate the transfer logic script is invoked - list.has(invoked_scripts, node.transfer_logic_script), - } - } - - TokenDoesNotExist { node_idx } -> { - // Get the covering node - let node_input = elem_at(reference_inputs, node_idx) - let node_datum = expect_inline_datum(node_input.output) - expect node: RegistryNode = node_datum - - // This is not a programmable token - !and { - // Validate this is a legitimate registry node - has_currency_symbol(node_input.output.value, registry_node_cs), - // Validate the node covers the currency symbol - bytearray_lt(node.key, cs), - bytearray_lt(cs, node.next), - } - } - } -} diff --git a/src/programmable-tokens/aiken-workspace/validators/programmable_logic_global.test.ak b/src/programmable-tokens/aiken-workspace/validators/programmable_logic_global.test.ak deleted file mode 100644 index a7d9234..0000000 --- a/src/programmable-tokens/aiken-workspace/validators/programmable_logic_global.test.ak +++ /dev/null @@ -1,267 +0,0 @@ -// Integration tests for programmable logic global validator -use aiken/collection/list -use cardano/address.{Address, Credential, Inline, Script, VerificationKey} -use cardano/assets.{PolicyId, from_asset, merge, negate, zero} -use cardano/transaction.{Input, NoDatum, Output, OutputReference} -use types.{ - ProgrammableLogicGlobalParams, RegistryNode, TokenDoesNotExist, TokenExists, - TransferAct, -} -use utils.{bytearray_lt} - -// Test data -// const test_protocol_params_cs: PolicyId = -// #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c" - -const test_registry_node_cs: PolicyId = - #"1112131415161718191a1b1c0d0e0f0102030405060708090a0b0c0d" - -const test_prog_logic_cred: Credential = Script(#"70726f67") - -const test_transfer_logic_cred: Credential = Script(#"7472616e73666572") - -const test_third_party_transfer_logic_cred: Credential = Script(#"6973737565") - -const test_token_cs: PolicyId = - #"2122232425262728292a2b2c2d2e2f303132333435363738393a3b3c" - -// Test ProgrammableLogicGlobalParams structure -test programmable_logic_global_params_structure() { - let params = - ProgrammableLogicGlobalParams { - prog_logic_cred: test_prog_logic_cred, - registry_node_cs: test_registry_node_cs, - } - - params.prog_logic_cred == test_prog_logic_cred && params.registry_node_cs == test_registry_node_cs -} - -// Test TransferAct redeemer -test transfer_act_redeemer() { - let proof1 = TokenExists { node_idx: 0 } - let proof2 = TokenDoesNotExist { node_idx: 1 } - - let redeemer = TransferAct { proofs: [proof1, proof2] } - - when redeemer is { - TransferAct { proofs } -> list.length(proofs) == 2 - _ -> False - } -} - -// Test ThirdPartyAct redeemer -// test third_party_act_redeemer() { -// let redeemer = -// ThirdPartyAct { seize_input_idx: 0, seize_output_idx: 1, registry_node_idx: 0 } - -// when redeemer is { -// ThirdPartyAct { seize_input_idx, seize_output_idx, registry_node_idx } -> -// seize_input_idx == 0 && seize_output_idx == 1 && registry_node_idx == 0 -// _ -> False -// } -// } - -// // Test TokenExists proof -// test token_exists_proof() { -// let proof = TokenExists { node_idx: 5 } - -// when proof is { -// TokenExists { node_idx } -> node_idx == 5 -// _ -> False -// } -// } - -// Test TokenDoesNotExist proof -test token_does_not_exist_proof() { - let proof = TokenDoesNotExist { node_idx: 3 } - - when proof is { - TokenDoesNotExist { node_idx } -> node_idx == 3 - _ -> False - } -} - -// Test registry node validation for transfer -test registry_node_validation_for_transfer() { - let node = - RegistryNode { - key: test_token_cs, - next: #"ffff", - transfer_logic_script: test_transfer_logic_cred, - third_party_transfer_logic_script: test_third_party_transfer_logic_cred, - global_state_cs: #"", - } - - // Node key should match the token CS - node.key == test_token_cs && // Transfer logic script should be present - node.transfer_logic_script == test_transfer_logic_cred -} - -// Test covering node logic for non-existent token -test covering_node_logic_for_non_existent_token() { - let covering_key = #"aa" - let token_cs = #"bb" - let next_key = #"cc" - - let covering_node = - RegistryNode { - key: covering_key, - next: next_key, - transfer_logic_script: test_transfer_logic_cred, - third_party_transfer_logic_script: test_third_party_transfer_logic_cred, - global_state_cs: #"", - } - - // Covering node should satisfy: covering_key < token_cs < next_key - bytearray_lt(covering_node.key, token_cs) && bytearray_lt( - token_cs, - covering_node.next, - ) -} - -// Test seize validation structure -test seize_validation_structure() { - let seized_cs = test_token_cs - let input_value = from_asset(seized_cs, #"746f6b656e", 100) - let expected_output_value = zero - - // After seizing, the output should have the tokens removed - input_value != expected_output_value -} - -// Test programmable tokens go to prog_logic_cred -test programmable_tokens_go_to_prog_logic_cred() { - let output = - Output { - address: Address { - payment_credential: test_prog_logic_cred, - stake_credential: None, - }, - value: from_asset(test_token_cs, #"746f6b656e", 100), - datum: NoDatum, - reference_script: None, - } - - output.address.payment_credential == test_prog_logic_cred -} - -// Test transfer logic script must be invoked -test transfer_logic_script_must_be_invoked() { - let invoked_scripts = [test_transfer_logic_cred, Script(#"6f74686572")] - - list.has(invoked_scripts, test_transfer_logic_cred) -} - -// Test third party transfer logic script must be invoked for third party actions -test third_party_transfer_logic_script_must_be_invoked_for_third_party_actions() { - let invoked_scripts = - [test_third_party_transfer_logic_cred, Script(#"6f74686572")] - - list.has(invoked_scripts, test_third_party_transfer_logic_cred) -} - -// Test signed prog inputs validation -test signed_prog_inputs_validation() { - let stake_pkh = #"aabbccdd" - let input = - Input { - output_reference: OutputReference { - transaction_id: #"00", - output_index: 0, - }, - output: Output { - address: Address { - payment_credential: test_prog_logic_cred, - stake_credential: Some(Inline(VerificationKey(stake_pkh))), - }, - value: from_asset(test_token_cs, #"746f6b656e", 100), - datum: NoDatum, - reference_script: None, - }, - } - - // Input is from prog_logic_cred with stake credential - input.output.address.payment_credential == test_prog_logic_cred && when - input.output.address.stake_credential - is { - Some(Inline(VerificationKey(pkh))) -> pkh == stake_pkh - _ -> False - } -} - -// Test seize prevents DDOS by requiring value change -test seize_prevents_ddos_by_requiring_value_change() { - let input_value = from_asset(test_token_cs, #"746f6b656e", 100) - // let output_value = from_asset(test_token_cs, #"746f6b656e", 100) - let expected_output = - merge(input_value, negate(from_asset(test_token_cs, #"746f6b656e", 50))) - - // Must actually remove tokens (DDOS prevention) - input_value != expected_output -} - -// Test only one prog_logic_cred input allowed in seize -test only_one_prog_logic_cred_input_in_seize() { - let inputs = - [ - Input { - output_reference: OutputReference { - transaction_id: #"00", - output_index: 0, - }, - output: Output { - address: Address { - payment_credential: test_prog_logic_cred, - stake_credential: None, - }, - value: zero, - datum: NoDatum, - reference_script: None, - }, - }, - ] - - list.length( - list.filter( - inputs, - fn(input) { - input.output.address.payment_credential == test_prog_logic_cred - }, - ), - ) == 1 -} - -// Test protocol params from reference inputs -test protocol_params_from_reference_inputs() { - let params = - ProgrammableLogicGlobalParams { - prog_logic_cred: test_prog_logic_cred, - registry_node_cs: test_registry_node_cs, - } - - // Protocol params should be loaded from reference input - params.registry_node_cs == test_registry_node_cs -} - -// Test invoked scripts via withdrawals -test invoked_scripts_via_withdrawals() { - let withdrawals = - [ - Pair(test_transfer_logic_cred, 0), - Pair(test_third_party_transfer_logic_cred, 0), - ] - - let invoked_scripts = - list.map( - withdrawals, - fn(wdrl) { - let Pair(cred, _amount) = wdrl - cred - }, - ) - - list.has(invoked_scripts, test_transfer_logic_cred) && list.has( - invoked_scripts, - test_third_party_transfer_logic_cred, - ) -} diff --git a/src/programmable-tokens/aiken-workspace/validators/protocol_params_mint.ak b/src/programmable-tokens/aiken-workspace/validators/protocol_params_mint.ak deleted file mode 100644 index 311de88..0000000 --- a/src/programmable-tokens/aiken-workspace/validators/protocol_params_mint.ak +++ /dev/null @@ -1,45 +0,0 @@ -use aiken/collection/list -// Protocol Parameters Minting Policy -// One-shot minting policy for the protocol parameters NFT -// Migrated from SmartTokens.Contracts.ProtocolParams -// -// This validator ensures: -// 1. The specified UTXO is consumed (one-shot minting) -// 2. Exactly one token is minted with the correct token name -// 3. The token is sent to an output with an inline datum containing ProgrammableLogicGlobalParams - -use cardano/assets.{PolicyId} -use cardano/transaction.{InlineDatum, Output, OutputReference, Transaction} -use types.{ProgrammableLogicGlobalParams, protocol_params_token} - -validator protocol_params_mint(utxo_ref: OutputReference) { - mint(_redeemer: Data, policy_id: PolicyId, self: Transaction) { - // Check 1: This is a one-shot minting policy - must spend the specified UTXO - let input_spent = - list.any(self.inputs, fn(input) { input.output_reference == utxo_ref }) - - // Check 2: Validate minted tokens - exactly 1 token with correct name - let amount_minted = - self.mint |> assets.quantity_of(policy_id, protocol_params_token) - - // Check 3: Ensure an output exists with the protocol params NFT and valid inline datum - expect Some(output) = - list.find( - self.outputs, - fn(output) { - assets.quantity_of(output.value, policy_id, protocol_params_token) > 0 - }, - ) - expect InlineDatum(datum) = output.datum - expect _params: ProgrammableLogicGlobalParams = datum - - and { - input_spent?, - (amount_minted == 1)?, - } - } - - else(_) { - fail - } -} diff --git a/src/programmable-tokens/aiken-workspace/validators/registry_mint.test.ak b/src/programmable-tokens/aiken-workspace/validators/registry_mint.test.ak deleted file mode 100644 index 5dd622e..0000000 --- a/src/programmable-tokens/aiken-workspace/validators/registry_mint.test.ak +++ /dev/null @@ -1,108 +0,0 @@ -// Integration tests for registry minting policy -use aiken/builtin -use cardano/address.{Address, Script} -use cardano/assets.{ada_asset_name, ada_policy_id, from_asset} -use cardano/transaction.{Input, NoDatum, Output, OutputReference} -use types.{RegistryNode} - -// Test data -const test_utxo_ref = OutputReference { transaction_id: #"00", output_index: 0 } - -// const test_policy_id: PolicyId = -// #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c" - -const test_address = - Address { payment_credential: Script(#"aabbccdd"), stake_credential: None } - -// Test RegistryInit creates correct origin node -test registry_init_creates_origin_node() { - // This test validates the structure of an Init transaction - // In a real scenario, this would be tested against actual transaction data - - let origin_node = - RegistryNode { - key: #"", - next: #"", - transfer_logic_script: Script(#"11"), - third_party_transfer_logic_script: Script(#"22"), - global_state_cs: #"", - } - - // Origin node should have empty keys - origin_node.key == #"" && origin_node.next == #"" -} - -// Test RegistryInsert validates key length -test registry_insert_validates_key_length() { - let valid_cs = #"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c" - let invalid_cs = #"0102" - - // Valid CS is 28 bytes - builtin.length_of_bytearray(valid_cs) == 28 && // Invalid CS is not 28 bytes - builtin.length_of_bytearray(invalid_cs) != 28 -} - -// Test covering node logic -test registry_covering_node_logic() { - let covering_key = #"aa" - let insert_key = #"bb" - let next_key = #"cc" - - // Covering node should satisfy: covering_key < insert_key < next_key - builtin.less_than_bytearray(covering_key, insert_key) && builtin.less_than_bytearray( - insert_key, - next_key, - ) -} - -// Test registry insert requires two outputs -test registry_insert_two_outputs() { - // After insert, there should be: - // 1. Updated covering node (covering_key -> insert_key) - // 2. New inserted node (insert_key -> next_key) - - let covering_key = #"aa" - let insert_key = #"bb" - let next_key = #"cc" - - let updated_covering = - RegistryNode { - key: covering_key, - next: insert_key, - transfer_logic_script: Script(#"11"), - third_party_transfer_logic_script: Script(#"22"), - global_state_cs: #"", - } - - let inserted_node = - RegistryNode { - key: insert_key, - next: next_key, - transfer_logic_script: Script(#"11"), - third_party_transfer_logic_script: Script(#"22"), - global_state_cs: #"", - } - - // Validate the structure - updated_covering.key == covering_key && updated_covering.next == insert_key && inserted_node.key == insert_key && inserted_node.next == next_key -} - -// Test one-shot minting policy constraint -test registry_one_shot_constraint() { - // The utxo_ref must be spent in the transaction - // This ensures the minting policy can only be used once - - let input = - Input { - output_reference: test_utxo_ref, - output: Output { - address: test_address, - value: from_asset(ada_policy_id, ada_asset_name, 2000000), - datum: NoDatum, - reference_script: None, - }, - } - - // The input's output reference should match the policy parameter - input.output_reference == test_utxo_ref -} diff --git a/src/programmable-tokens/common.ts b/src/programmable-tokens/common.ts deleted file mode 100644 index 4af9dea..0000000 --- a/src/programmable-tokens/common.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { - byteString, - conStr, - conStr1, - integer, - PlutusScript, - scriptHash, - TxInput, -} from "@meshsdk/common"; -import { - applyParamsToScript, - resolveScriptHash, - serializePlutusScript, -} from "@meshsdk/core"; -import { scriptHashToRewardAddress } from "@meshsdk/core-cst"; - -import { ProtocolBootstrapParams } from "./types"; -import { findValidator } from "./utils"; - -export class Cip113_scripts_standard { - private networkId: number; - constructor(networkId: number) { - this.networkId = networkId; - } - async blacklist_mint(utxoReference: TxInput, managerPubkeyHash: string) { - const validator = findValidator("blacklist_mint", "mint"); - const cbor = applyParamsToScript( - validator, - [ - conStr(0, [ - byteString(utxoReference.txHash), - integer(utxoReference.outputIndex), - ]), - byteString(managerPubkeyHash), - ], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const policyId = resolveScriptHash(cbor, "V3"); - - return { cbor, plutusScript, policyId }; - } - - async issuance_mint( - mintingLogicCredential: string, - params: ProtocolBootstrapParams | string - ) { - const validator = findValidator("issuance_mint", "mint"); - let paramScriptHash: string; - if (typeof params === "string") { - paramScriptHash = params; - } else { - paramScriptHash = params?.programmableLogicBaseParams.scriptHash!; - } - if (!paramScriptHash) - throw new Error("could not resolve issuance mint parameters"); - const cbor = applyParamsToScript( - validator, - [ - conStr1([byteString(paramScriptHash)]), - conStr1([byteString(mintingLogicCredential)]), - ], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const policyId = resolveScriptHash(cbor, "V3"); - const address = serializePlutusScript( - plutusScript, - undefined, - this.networkId, - false - ).address; - return { cbor, plutusScript, policyId, address }; - } - - async issuance_cbor_hex_mint(utxoReference: TxInput) { - const validator = findValidator("issuance_cbor_hex_mint", "mint"); - const cbor = applyParamsToScript( - validator, - [ - conStr(0, [ - byteString(utxoReference.txHash), - integer(utxoReference.outputIndex), - ]), - ], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const policyId = resolveScriptHash(cbor, "V3"); - const address = serializePlutusScript( - plutusScript, - undefined, - this.networkId, - false - ).address; - return { cbor, plutusScript, policyId, address }; - } - - async programmable_logic_base(params: ProtocolBootstrapParams | string) { - const validator = findValidator("programmable_logic_base", "spend"); - let paramScriptHash: string; - if (typeof params === "string") { - paramScriptHash = params; - } else { - paramScriptHash = params?.programmableLogicGlobalPrams.scriptHash!; - } - if (!paramScriptHash) - throw new Error("could not resolve logic base parameter"); - const cbor = applyParamsToScript( - validator, - [conStr1([byteString(paramScriptHash)])], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const policyId = resolveScriptHash(cbor, "V3"); - return { - cbor, - plutusScript, - policyId, - }; - } - - async programmable_logic_global(params: ProtocolBootstrapParams | string) { - const validator = findValidator("programmable_logic_global", "withdraw"); - let paramScriptHash: string; - if (typeof params === "string") { - paramScriptHash = params; - } else { - paramScriptHash = params?.protocolParams.scriptHash!; - } - if (!paramScriptHash) - throw new Error("could not resolve logic global parameter"); - const cbor = applyParamsToScript( - validator, - [scriptHash(paramScriptHash)], - "JSON" - ); - const policyId = resolveScriptHash(cbor, "V3"); - const rewardAddress = scriptHashToRewardAddress(policyId, this.networkId); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - - return { cbor, plutusScript, rewardAddress, policyId }; - } - - async protocol_param_mint(utxoReference: TxInput) { - const validator = findValidator("protocol_params_mint", "mint"); - const cbor = applyParamsToScript( - validator, - [ - conStr(0, [ - byteString(utxoReference.txHash), - integer(utxoReference.outputIndex), - ]), - ], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const policyId = resolveScriptHash(cbor, "V3"); - const address = serializePlutusScript( - plutusScript, - undefined, - this.networkId, - false - ).address; - return { cbor, plutusScript, policyId, address }; - } - - async registry_mint( - params: ProtocolBootstrapParams | string, - utxo?: TxInput - ) { - const validator = findValidator("registry_mint", "mint"); - - let paramScriptHash: string; - let parameter; - if (typeof params === "string") { - paramScriptHash = params; - parameter = utxo; - } else { - paramScriptHash = params.directoryMintParams.issuanceScriptHash; - parameter = params.directoryMintParams.txInput; - } - - if (!parameter) - throw new Error("register mint utxo parameter could not resolve"); - if (!paramScriptHash) - throw new Error("registry mint param Script hash could not resolve"); - - const cbor = applyParamsToScript( - validator, - [ - conStr(0, [ - byteString(parameter.txHash), - integer(parameter.outputIndex), - ]), - scriptHash(paramScriptHash), - ], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const policyId = resolveScriptHash(cbor, "V3"); - return { cbor, plutusScript, policyId }; - } - - async registry_spend(params: ProtocolBootstrapParams | string) { - const validator = findValidator("registry_spend", "spend"); - let paramScriptHash: string; - if (typeof params === "string") { - paramScriptHash = params; - } else { - paramScriptHash = params.protocolParams.scriptHash; - } - if (!paramScriptHash) - throw new Error("could not resolve params for registry spend"); - const cbor = applyParamsToScript( - validator, - [scriptHash(paramScriptHash)], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const address = serializePlutusScript( - plutusScript, - "", - this.networkId, - false - ).address; - const policyId = resolveScriptHash(cbor, "V3"); - return { - cbor, - plutusScript, - address, - policyId, - }; - } - - async example_transfer_logic(permittedCredential: string) { - const validator = findValidator("example_transfer_logic", "withdraw"); - const cbor = applyParamsToScript( - validator, - [scriptHash(permittedCredential)], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const address = serializePlutusScript( - plutusScript, - permittedCredential, - this.networkId, - true - ).address; - return { cbor, plutusScript, address }; - } - - async freeze_and_seize_transfer_logic(params: ProtocolBootstrapParams | string, blacklistNodeCs: string) { - const validator = findValidator("example_transfer_logic", "withdraw"); - let paramScriptHash: string; - if (typeof params === "string") { - paramScriptHash = params; - } else { - paramScriptHash = params?.programmableLogicBaseParams.scriptHash!; - } - if (!paramScriptHash) - throw new Error("could not resolve logic base parameter"); - const cbor = applyParamsToScript( - validator, - [scriptHash(paramScriptHash), scriptHash(blacklistNodeCs)], - "JSON" - ); - const plutusScript: PlutusScript = { - code: cbor, - version: "V3", - }; - const address = serializePlutusScript( - plutusScript, - paramScriptHash, - this.networkId, - true - ).address; - return { cbor, plutusScript, address }; - } -} \ No newline at end of file diff --git a/src/programmable-tokens/offchain.ts b/src/programmable-tokens/offchain.ts deleted file mode 100644 index 92b0023..0000000 --- a/src/programmable-tokens/offchain.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { - Asset, - byteString, - conStr0, - conStr1, - integer, - list, - POLICY_ID_LENGTH, - stringToHex, - UTxO, -} from "@meshsdk/common"; -import { deserializeDatum, resolveScriptHash } from "@meshsdk/core"; -import { - buildBaseAddress, - CredentialType, - deserializeAddress, - Hash28ByteBase16, - scriptHashToRewardAddress, -} from "@meshsdk/core-cst"; - -import { MeshTxInitiator, MeshTxInitiatorInput } from "../common"; -import { Cip113_scripts_standard } from "./common"; -import { - ProtocolBootstrapParams, - RegisterTokenParams, - MintTokensParams, - TransferTokenParams, - RegistryDatum, -} from "./types"; -import { parseRegistryDatum } from "./utils"; - -export class ProgrammableTokenContract extends MeshTxInitiator { - params: ProtocolBootstrapParams | undefined; - quantity: string; - - constructor( - inputs: MeshTxInitiatorInput, - params?: ProtocolBootstrapParams, - quantity: string = "1" - ) { - super(inputs); - this.params = params; - this.quantity = quantity; - } - - registerToken = async (params: RegisterTokenParams): Promise => { - const { - assetName, - scripts: { - mintingLogic: mintingLogicPlutusScript, - transferLogic: transferLogicPlutusScript, - globalStateLogic: globalStateLogicPlutusScript, - thirdPartyLogic: thirdPartyLogicPlutusScript, - }, - transferRedeemerValue, - recipientAddress, - } = params; - - if (!this.params) { - throw new Error( - "Protocol bootstrap params are required.deploy protocolParambootstrap first if not done then pass as a param." - ); - } - - const { utxos, walletAddress, collateral } = - await this.getWalletInfoForTx(); - const standardScript = new Cip113_scripts_standard(this.networkId); - - const registrySpend = await standardScript.registry_spend(this.params); - const registryMint = await standardScript.registry_mint(this.params); - const logicBase = await standardScript.programmable_logic_base(this.params); - const mintingLogicScriptHash = resolveScriptHash( - mintingLogicPlutusScript.code, - mintingLogicPlutusScript.version - ); - const issuanceMint = await standardScript.issuance_mint( - mintingLogicScriptHash, - this.params - ); - - const bootstrapTxHash = this.params.txHash; - const protocolParamsUtxos = await this.fetcher?.fetchUTxOs( - bootstrapTxHash, - 0 - ); - - if (!protocolParamsUtxos) { - throw new Error("Could not resolve protocol params"); - } - - const issuanceUtxos = await this.fetcher?.fetchUTxOs(bootstrapTxHash, 2); - if (!issuanceUtxos) { - throw new Error("Issuance UTXO not found"); - } - - const protocolParamsUtxo = protocolParamsUtxos[0]; - const issuanceUtxo = issuanceUtxos[0]; - let thirdPartyLogicScriptHash: string = ""; - let globalStateLogicScriptHash: string = ""; - - const transferLogicScriptHash = resolveScriptHash( - transferLogicPlutusScript.code, - transferLogicPlutusScript.version - ); - const transferAddress = scriptHashToRewardAddress( - transferLogicScriptHash, - this.networkId - ); - if (thirdPartyLogicPlutusScript) { - thirdPartyLogicScriptHash = resolveScriptHash( - thirdPartyLogicPlutusScript.code, - thirdPartyLogicPlutusScript.version - ); - } - if (globalStateLogicPlutusScript) { - globalStateLogicScriptHash = resolveScriptHash( - globalStateLogicPlutusScript.code, - globalStateLogicPlutusScript.version - ); - } - const tokenPolicyId = issuanceMint.policyId; - const registryEntries = await this.fetcher?.fetchAddressUTxOs( - registrySpend.address - ); - const registryEntriesDatums = registryEntries?.flatMap((utxo: UTxO) => - deserializeDatum(utxo.output.plutusData!) - ); - - const existingEntry = registryEntriesDatums - ?.map(parseRegistryDatum) - .filter((d): d is RegistryDatum => d !== null) - .find((d) => d.key === tokenPolicyId); - - if (existingEntry) { - throw new Error(`Token policy ${tokenPolicyId} already registered`); - } - - const nodeToReplaceUtxo = registryEntries?.find((utxo) => { - const datum = deserializeDatum(utxo.output.plutusData!); - const parsedDatum = parseRegistryDatum(datum); - - if (!parsedDatum) { - console.log("Could not parse registry datum"); - return false; - } - - const after = parsedDatum.key.localeCompare(tokenPolicyId) < 0; - const before = tokenPolicyId.localeCompare(parsedDatum.next) < 0; - - return after && before; - }); - - if (!nodeToReplaceUtxo) { - throw new Error("Could not find node to replace"); - } - - const existingRegistryNodeDatum = parseRegistryDatum( - deserializeDatum(nodeToReplaceUtxo.output.plutusData!) - ); - - if (!existingRegistryNodeDatum) { - throw new Error("Could not parse current registry node"); - } - - const stakeCredential = deserializeAddress( - recipientAddress ? recipientAddress : walletAddress - ) - .asBase() - ?.getStakeCredential().hash!; - const targetAddress = buildBaseAddress( - 0, - logicBase.policyId as Hash28ByteBase16, - stakeCredential, - CredentialType.ScriptHash, - CredentialType.KeyHash - ); - - const registryMintRedeemer = conStr1([ - byteString(tokenPolicyId), - byteString(mintingLogicScriptHash), - ]); - - const issuanceRedeemer = conStr0([ - conStr1([byteString(mintingLogicScriptHash)]), - ]); - - const previousNodeDatum = conStr0([ - byteString(existingRegistryNodeDatum.key), - byteString(tokenPolicyId), - byteString(existingRegistryNodeDatum.transferScriptHash), - byteString(existingRegistryNodeDatum.thirdPartyScriptHash), - byteString(existingRegistryNodeDatum.metadata), - ]); - - const newNodeDatum = conStr0([ - byteString(tokenPolicyId), - byteString(existingRegistryNodeDatum.next), - byteString(transferLogicScriptHash), - byteString(thirdPartyLogicScriptHash), - byteString(globalStateLogicScriptHash), - ]); - - const directorySpendAssets: Asset[] = [ - { unit: "lovelace", quantity: "1500000" }, - { unit: registryMint.policyId, quantity: "1" }, - ]; - - const directoryMintAssets: Asset[] = [ - { unit: "lovelace", quantity: "1500000" }, - { unit: registryMint.policyId + tokenPolicyId, quantity: "1" }, - ]; - - const programmableTokenAssets: Asset[] = [ - { unit: "lovelace", quantity: "1500000" }, - { - unit: tokenPolicyId + stringToHex(assetName), - quantity: this.quantity, - }, - ]; - - const txHex = await this.mesh - .spendingPlutusScriptV3() - .txIn(nodeToReplaceUtxo.input.txHash, nodeToReplaceUtxo.input.outputIndex) - .txInScript(registrySpend.cbor) - .txInRedeemerValue(conStr0([]), "JSON") - .txInInlineDatumPresent() - .withdrawalPlutusScriptV3() - .withdrawal(transferAddress, "0") - .withdrawalScript(transferLogicPlutusScript.code) - .withdrawalRedeemerValue(transferRedeemerValue, "JSON") - .mintPlutusScriptV3() - .mint(this.quantity, tokenPolicyId, stringToHex(assetName)) - .mintingScript(issuanceMint.cbor) - .mintRedeemerValue(issuanceRedeemer, "JSON") - .mintPlutusScriptV3() - .mint("1", registryMint.policyId, tokenPolicyId) - .mintingScript(registryMint.cbor) - .mintRedeemerValue(registryMintRedeemer, "JSON") - - .txOut(targetAddress.toAddress().toBech32(), programmableTokenAssets) - .txOutInlineDatumValue(conStr0([]), "JSON") - .txOut(registrySpend.address, directorySpendAssets) - .txOutInlineDatumValue(previousNodeDatum, "JSON") - .txOut(registrySpend.address, directoryMintAssets) - .txOutInlineDatumValue(newNodeDatum, "JSON") - - .readOnlyTxInReference( - protocolParamsUtxo!.input.txHash, - protocolParamsUtxo!.input.outputIndex - ) - .readOnlyTxInReference( - issuanceUtxo!.input.txHash, - issuanceUtxo!.input.outputIndex - ) - .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) - .selectUtxosFrom(utxos) - .changeAddress(walletAddress) - .complete(); - - return txHex; - }; - - mintTokens = async (params: MintTokensParams): Promise => { - const { - assetName, - scripts: { - mintingLogic: mintingLogicPlutusScript, - transferLogic: transferLogicPlutusScript, - }, - transferRedeemerValue, - recipientAddress, - } = params; - - if (!this.params) { - throw new Error( - "Protocol bootstrap params are required. Call protocolParambootstrap first." - ); - } - - const { utxos, walletAddress, collateral } = - await this.getWalletInfoForTx(); - const standardScript = new Cip113_scripts_standard(this.networkId); - - const mintingLogicScriptHash = resolveScriptHash( - mintingLogicPlutusScript.code, - mintingLogicPlutusScript.version - ); - const transferLogicScriptHash = resolveScriptHash( - transferLogicPlutusScript.code, - transferLogicPlutusScript.version - ); - const transferAddress = scriptHashToRewardAddress( - transferLogicScriptHash, - this.networkId - ); - const issuanceMint = await standardScript.issuance_mint( - mintingLogicScriptHash, - this.params - ); - const senderCredential = deserializeAddress( - recipientAddress ? recipientAddress : walletAddress - ).asBase(); - if (!senderCredential) { - throw new Error("Sender credential not found"); - } - const logicBase = await standardScript.programmable_logic_base(this.params); - const logicAddress = buildBaseAddress( - 0, - logicBase.policyId as Hash28ByteBase16, - senderCredential.getPaymentCredential().hash, - CredentialType.ScriptHash, - CredentialType.KeyHash - ); - const targetAddress = logicAddress.toAddress().toBech32(); - console.log("target address", targetAddress); - - const issuanceRedeemer = conStr0([ - conStr1([byteString(mintingLogicScriptHash)]), - ]); - - const programmableTokenAssets: Asset[] = [ - { unit: "lovelace", quantity: "1500000" }, - { - unit: issuanceMint.policyId + stringToHex(assetName), - quantity: this.quantity, - }, - ]; - - const programmableTokenDatum = conStr0([]); - - const txHex = await this.mesh - .withdrawalPlutusScriptV3() - .withdrawal(transferAddress, "0") - .withdrawalScript(transferLogicPlutusScript.code) - .withdrawalRedeemerValue(transferRedeemerValue, "JSON") - - .mintPlutusScriptV3() - .mint(this.quantity, issuanceMint.policyId, stringToHex(assetName)) - .mintingScript(issuanceMint.cbor) - .mintRedeemerValue(issuanceRedeemer, "JSON") - - .txOut(targetAddress, programmableTokenAssets) - .txOutInlineDatumValue(programmableTokenDatum, "JSON") - - .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) - .selectUtxosFrom(utxos) - .changeAddress(walletAddress) - .complete(); - return txHex; - }; - - transferToken = async (params: TransferTokenParams): Promise => { - const { - unit, - quantity, - recipientAddress, - transferLogic: transferLogicPlutusScript, - transferRedeemerValue, - } = params; - - if (!this.params) { - throw new Error( - "Protocol bootstrap params are required. Call protocolParambootstrap first." - ); - } - - const { utxos, walletAddress, collateral } = - await this.getWalletInfoForTx(); - const policyId = unit.substring(0, POLICY_ID_LENGTH); - const standardScript = new Cip113_scripts_standard(this.networkId); - const logicBase = await standardScript.programmable_logic_base(this.params); - const logicGlobal = await standardScript.programmable_logic_global( - this.params - ); - const registrySpend = await standardScript.registry_spend(this.params); - const transferLogicScriptHash = resolveScriptHash( - transferLogicPlutusScript.code, - transferLogicPlutusScript.version - ); - const transferAddress = scriptHashToRewardAddress( - transferLogicScriptHash, - this.networkId - ); - const senderCredential = deserializeAddress(walletAddress) - .asBase() - ?.getStakeCredential().hash; - - const recipientCredential = deserializeAddress(recipientAddress) - .asBase() - ?.getStakeCredential().hash; - - const senderBaseAddress = buildBaseAddress( - 0, - logicBase.policyId as Hash28ByteBase16, - senderCredential!, - CredentialType.ScriptHash, - CredentialType.KeyHash - ); - const recipientBaseAddress = buildBaseAddress( - 0, - logicBase.policyId as Hash28ByteBase16, - recipientCredential!, - CredentialType.ScriptHash, - CredentialType.KeyHash - ); - const senderAddress = senderBaseAddress.toAddress().toBech32(); - const targetAddress = recipientBaseAddress.toAddress().toBech32(); - - const registryUtxos = await this.fetcher?.fetchAddressUTxOs( - registrySpend.address - ); - if (!registryUtxos) { - throw new Error("Could not find registry entry for utxos"); - } - - const tokenRegistry = registryUtxos?.find((utxo) => { - const datum = deserializeDatum(utxo.output.plutusData!); - const parsedDatum = parseRegistryDatum(datum); - return parsedDatum?.key === policyId; - }); - - if (!tokenRegistry) { - throw new Error("Could not find registry entry for token"); - } - - const protocolParamsUtxos = await this.fetcher?.fetchUTxOs( - this.params.txHash, - 0 - ); - if (!protocolParamsUtxos) { - throw new Error("Could not resolve protocol params"); - } - const protocolParamsUtxo = protocolParamsUtxos[0]; - - const senderTokenUtxos = await this.fetcher?.fetchAddressUTxOs( - senderAddress - ); - if (!senderTokenUtxos) { - throw new Error("No programmable tokens found at sender address"); - } - - let totalTokenBalance = 0; - senderTokenUtxos.forEach((utxo) => { - const tokenAsset = utxo.output.amount.find((a) => a.unit === unit); - if (tokenAsset) totalTokenBalance += Number(tokenAsset.quantity); - }); - - const transferAmount = Number(quantity); - if (totalTokenBalance < transferAmount) throw new Error("Not enough funds"); - - let selectedUtxos: UTxO[] = []; - let selectedAmount = 0; - for (const utxo of senderTokenUtxos) { - if (selectedAmount >= transferAmount) break; - const tokenAsset = utxo.output.amount.find((a) => a.unit === unit); - if (tokenAsset) { - selectedUtxos.push(utxo); - selectedAmount += Number(tokenAsset.quantity); - } - } - - const returningAmount = selectedAmount - transferAmount; - - const registryProof = conStr0([integer(1)]); - const programmableLogicGlobalRedeemer = conStr0([list([registryProof])]); - const spendingRedeemer = conStr0([]); - const tokenDatum = conStr0([]); - - const recipientAssets: Asset[] = [ - { unit: "lovelace", quantity: "1300000" }, - { unit: unit, quantity: transferAmount.toString() }, - ]; - - const returningAssets: Asset[] = [ - { unit: "lovelace", quantity: "1300000" }, - ]; - if (returningAmount > 0) { - returningAssets.push({ - unit: unit, - quantity: returningAmount.toString(), - }); - } - const txHex = await this.mesh; - - for (const utxo of selectedUtxos) { - txHex - .spendingPlutusScriptV3() - .txIn(utxo.input.txHash, utxo.input.outputIndex) - .txInScript(logicBase.cbor) - .txInRedeemerValue(spendingRedeemer, "JSON") - .txInInlineDatumPresent(); - } - - txHex - .withdrawalPlutusScriptV3() - .withdrawal(transferAddress, "0") - .withdrawalScript(transferLogicPlutusScript.code) - .withdrawalRedeemerValue(transferRedeemerValue, "JSON") - - .withdrawalPlutusScriptV3() - .withdrawal(logicGlobal.rewardAddress, "0") - .withdrawalScript(logicGlobal.cbor) - .withdrawalRedeemerValue(programmableLogicGlobalRedeemer, "JSON") - .requiredSignerHash(senderCredential!.toString()) - .txOut(walletAddress, [ - { - unit: "lovelace", - quantity: "1000000", - }, - ]); - - if (returningAmount > 0) { - txHex - .txOut(senderAddress, returningAssets) - .txOutInlineDatumValue(tokenDatum, "JSON"); - } - - txHex - .txOut(targetAddress, recipientAssets) - .txOutInlineDatumValue(tokenDatum, "JSON") - - .readOnlyTxInReference( - protocolParamsUtxo!.input.txHash, - protocolParamsUtxo!.input.outputIndex - ) - .readOnlyTxInReference( - tokenRegistry!.input.txHash, - tokenRegistry.input.outputIndex - ) - - .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) - .selectUtxosFrom(utxos) - .changeAddress(walletAddress); - - const unsignedTx = await txHex.complete(); - return unsignedTx; - }; - - blacklistToken = async (): Promise => { - throw new Error("blacklistToken is not yet implemented"); - }; - whitelistToken = async (): Promise => { - throw new Error("whitelistToken is not yet implemented"); - }; - - freezeToken = async (): Promise => { - throw new Error("freezeToken is not yet implemented"); - }; - - seizeToken = async (): Promise => { - throw new Error("seizeToken is not yet implemented"); - }; - - unfreezeToken = async (): Promise => { - throw new Error("unfreezeToken is not yet implemented"); - }; - unseizeToken = async (): Promise => { - throw new Error("unseizeToken is not yet implemented"); - }; - - burnToken = async (): Promise => { - throw new Error("burnToken is not yet implemented"); - }; - - protocolParambootstrap = async (): Promise => { - throw new Error("protocolParambootstrap is not yet implemented"); - }; -} diff --git a/src/programmable-tokens/offchain/common.ts b/src/programmable-tokens/offchain/common.ts new file mode 100644 index 0000000..bd8ccd5 --- /dev/null +++ b/src/programmable-tokens/offchain/common.ts @@ -0,0 +1,247 @@ +import { + byteString, + conStr, + conStr1, + integer, + PlutusScript, + TxInput, +} from "@meshsdk/common"; +import { + applyParamsToScript, + resolveScriptHash, + serializePlutusScript, +} from "@meshsdk/core"; +import { scriptHashToRewardAddress } from "@meshsdk/core-cst"; + +import { ProtocolBootstrapParams } from "./type"; +import { cborEncode, findValidator } from "./utils"; + +export class StandardScripts { + constructor(private readonly networkID: number) {} + + private build( + validatorName: string, + params: object[], + ): { cbor: string; plutusScript: PlutusScript } { + const cbor = applyParamsToScript( + findValidator(validatorName), + params, + "JSON", + ); + return { cbor, plutusScript: { code: cbor, version: "V3" } }; + } + + private txRef(utxo: TxInput) { + return conStr(0, [byteString(utxo.txHash), integer(utxo.outputIndex)]); + } + + private resolveParam( + params: ProtocolBootstrapParams | string, + extract: (p: ProtocolBootstrapParams) => string, + errorMsg: string, + ): string { + const hash = typeof params === "string" ? params : extract(params); + if (!hash) throw new Error(errorMsg); + return hash; + } + + private toAddress( + plutusScript: PlutusScript, + staking: string | undefined = undefined, + ) { + return serializePlutusScript(plutusScript, staking, this.networkID, false) + .address; + } + + async issuanceMint( + mintingLogicCredential: string, + params: ProtocolBootstrapParams | string, + ) { + const paramScriptHash = this.resolveParam( + params, + (p) => p.programmableLogicBaseParams.scriptHash!, + "could not resolve issuance mint parameters", + ); + const { cbor, plutusScript } = this.build( + "issuance_mint.issuance_mint.mint", + [ + conStr1([byteString(paramScriptHash)]), + conStr1([byteString(mintingLogicCredential)]), + ], + ); + return { + cbor, + plutusScript, + policyId: resolveScriptHash(cbor, "V3"), + address: this.toAddress(plutusScript), + }; + } + + async issuanceCborHexMint(utxo_reference: TxInput) { + const { cbor, plutusScript } = this.build( + "issuance_cbor_hex_mint.issuance_cbor_hex_mint.mint", + [this.txRef(utxo_reference)], + ); + return { + cbor, + plutusScript, + policyId: resolveScriptHash(cbor, "V3"), + address: this.toAddress(plutusScript), + }; + } + + async programmableLogicBase(params: ProtocolBootstrapParams | string) { + const paramScriptHash = this.resolveParam( + params, + (p) => p.programmableLogicGlobalPrams.scriptHash!, + "could not resolve logic base parameter", + ); + const { cbor, plutusScript } = this.build( + "programmable_logic_base.programmable_logic_base.spend", + [conStr1([byteString(paramScriptHash)])], + ); + return { cbor, plutusScript, policyId: resolveScriptHash(cbor, "V3") }; + } + + async programmableLogicGlobal(params: ProtocolBootstrapParams | string) { + const paramScriptHash = this.resolveParam( + params, + (p) => p.protocolParams.scriptHash!, + "could not resolve logic global parameter", + ); + const { cbor, plutusScript } = this.build( + "programmable_logic_global.programmable_logic_global.withdraw", + [byteString(paramScriptHash)], + ); + const scriptHash = resolveScriptHash(cbor, "V3"); + return { + cbor, + plutusScript, + scriptHash, + rewardAddress: scriptHashToRewardAddress(scriptHash, this.networkID), + }; + } + + async protocolParamMint(utxo_reference: TxInput) { + const { cbor, plutusScript } = this.build( + "protocol_params_mint.protocol_params_mint.mint", + [this.txRef(utxo_reference)], + ); + const scriptHash = resolveScriptHash(cbor, "V3"); + return { + cbor, + plutusScript, + scriptHash, + address: this.toAddress(plutusScript), + }; + } + + async registryMint(params: ProtocolBootstrapParams | string, utxo?: TxInput) { + const paramScriptHash = + typeof params === "string" + ? params + : params.directoryMintParams.issuanceScriptHash; + const txInput = + typeof params === "string" ? utxo! : params.directoryMintParams.txInput; + if (!txInput) + throw new Error("register mint utxo parameter could not resolve"); + if (!paramScriptHash) + throw new Error("registry mint param script hash could not resolve"); + const { cbor, plutusScript } = this.build( + "registry_mint.registry_mint.mint", + [this.txRef(txInput), byteString(paramScriptHash)], + ); + return { cbor, plutusScript, policyId: resolveScriptHash(cbor, "V3") }; + } + + async registrySpend(params: ProtocolBootstrapParams | string) { + const paramScriptHash = this.resolveParam( + params, + (p) => p.protocolParams.scriptHash, + "could not resolve params for registry spend", + ); + const { cbor, plutusScript } = this.build( + "registry_spend.registry_spend.spend", + [byteString(paramScriptHash)], + ); + return { + cbor, + plutusScript, + policyId: resolveScriptHash(cbor, "V3"), + address: this.toAddress(plutusScript, ""), + }; + } +} + +export class SubStandardScripts { + constructor(private readonly networkID: number) {} + + private buildRaw(validatorName: string) { + const _cbor = cborEncode(findValidator(validatorName, false)); + const plutusScript: PlutusScript = { code: _cbor, version: "V3" }; + const policyId = resolveScriptHash(_cbor, "V3"); + const rewardAddress = scriptHashToRewardAddress(policyId, this.networkID); + return { _cbor, plutusScript, policyId, rewardAddress }; + } + + private buildWithParams(validatorName: string, params: object[]) { + const _cbor = findValidator(validatorName, false); + const cbor = applyParamsToScript(_cbor, params, "JSON"); + const plutusScript: PlutusScript = { code: cbor, version: "V3" }; + const policyId = resolveScriptHash(cbor, "V3"); + const rewardAddress = scriptHashToRewardAddress(policyId, this.networkID); + return { cbor, plutusScript, policyId, rewardAddress }; + } + + private txRef(utxo: TxInput) { + return conStr(0, [byteString(utxo.txHash), integer(utxo.outputIndex)]); + } + + async issue() { + return this.buildRaw("transfer.issue.withdraw"); + } + + async transfer() { + return this.buildRaw("transfer.transfer.withdraw"); + } + + async blacklistSpend(blacklistNodePolicyId: string) { + const { cbor, plutusScript, policyId } = this.buildWithParams( + "blacklist_spend.blacklist_spend.spend", + [byteString(blacklistNodePolicyId)], + ); + const address = serializePlutusScript( + plutusScript, + "", + this.networkID, + false, + ).address; + return { cbor, plutusScript, address, policyId }; + } + + async blacklistMint(bootstraptxInput: TxInput, adminPubkeyHash: string) { + return this.buildWithParams("blacklist_mint.blacklist_mint.mint", [ + this.txRef(bootstraptxInput), + byteString(adminPubkeyHash), + ]); + } + + async issuerAdmin(adminPubKeyHash: string) { + return this.buildWithParams( + "example_transfer_logic.issuer_admin_contract.withdraw", + [conStr(0, [byteString(adminPubKeyHash)])], + ); + } + + async customTransfer( + programmableLogicBaseScriptHash: string, + blacklistNodePolicyId: string, + ) { + const { cbor, plutusScript, policyId, rewardAddress } = + this.buildWithParams("example_transfer_logic.transfer.withdraw", [ + conStr1([byteString(programmableLogicBaseScriptHash)]), + byteString(blacklistNodePolicyId), + ]); + return { cbor, plutusScript, policyId, rewardAddress }; + } +} diff --git a/src/programmable-tokens/offchain/index.ts b/src/programmable-tokens/offchain/index.ts new file mode 100644 index 0000000..0305f7c --- /dev/null +++ b/src/programmable-tokens/offchain/index.ts @@ -0,0 +1,3 @@ +export * from "./offchain"; +export * from "./resolvers"; +export * from "./type"; \ No newline at end of file diff --git a/src/programmable-tokens/offchain/offchain.ts b/src/programmable-tokens/offchain/offchain.ts new file mode 100644 index 0000000..cdb45a8 --- /dev/null +++ b/src/programmable-tokens/offchain/offchain.ts @@ -0,0 +1,986 @@ +import { + Asset, + byteString, + conStr0, + conStr1, + conStr2, + integer, + list, + POLICY_ID_LENGTH, + stringToHex, + UTxO, +} from "@meshsdk/common"; +import { deserializeDatum } from "@meshsdk/core"; + +import { MeshTxInitiator, MeshTxInitiatorInput } from "../../common"; +import { StandardScripts, SubStandardScripts } from "./common"; +import { + BlacklistBootstrap, + BlacklistDatum, + ProtocolBootstrapParams, + smartWalletAddress, +} from "./type"; +import { + parseBlacklistDatum, + parseRegistryDatum, + selectProgrammableTokenUtxos, +} from "./utils"; +import { resolveBlacklistScripts, resolveStakeCredential } from "./resolvers"; +import params from "./protocolParams.json"; + +export class ProgrammableTokenContract extends MeshTxInitiator { + private _blacklistBootstrap: BlacklistBootstrap | undefined; + private _params: ProtocolBootstrapParams; + + constructor( + private inputs: MeshTxInitiatorInput, + blacklistBootstrap?: BlacklistBootstrap, + ) { + super(inputs); + this._params = params; + this._blacklistBootstrap = blacklistBootstrap; + } + + get protocolParams() { + return this._params; + } + + mintToken = async ( + assetName: string, + quantity: string, + issuerAdminPkh: string, + recepientSmartAddress: smartWalletAddress, + ): Promise => { + const params = this._params; + const fetcher = this.fetcher; + const wallet = this.wallet; + if (!params || !fetcher || !wallet) + throw new Error( + "Contract parameters, fetcher, or wallet not initialized", + ); + + const { + utxos: walletUtxos, + walletAddress: changeAddress, + collateral, + } = await this.getWalletInfoForTx(); + + const standardScript = new StandardScripts(this.networkId); + const substandardScript = new SubStandardScripts(this.networkId); + + const substandardIssue = + await substandardScript.issuerAdmin(issuerAdminPkh); + const substandardIssueCbor = substandardIssue.cbor; + const substandardPolicyId = substandardIssue.policyId; + + const issuanceMint = await standardScript.issuanceMint( + substandardPolicyId, + params, + ); + + const issuanceRedeemer = conStr0([ + conStr1([byteString(substandardPolicyId)]), + ]); + + const programmableTokenAssets: Asset[] = [ + { unit: "lovelace", quantity: "1300000" }, + { + unit: issuanceMint.policyId + stringToHex(assetName), + quantity: quantity, + }, + ]; + + const programmableTokenDatum = conStr0([]); + + this.mesh.txEvaluationMultiplier = 1.3; + this.mesh + .withdrawalPlutusScriptV3() + .withdrawal(substandardIssue.rewardAddress, "0") + .withdrawalScript(substandardIssueCbor) + .withdrawalRedeemerValue(conStr0([]), "JSON") + + .mintPlutusScriptV3() + .mint(quantity, issuanceMint.policyId, stringToHex(assetName)) + .mintingScript(issuanceMint.cbor) + .mintRedeemerValue(issuanceRedeemer, "JSON") + + .txOut(recepientSmartAddress, programmableTokenAssets) + .txOutInlineDatumValue(programmableTokenDatum, "JSON") + + .requiredSignerHash(issuerAdminPkh) + .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) + .selectUtxosFrom(walletUtxos) + .setNetwork(this.networkId === 0 ? "preview" : "mainnet") + .changeAddress(changeAddress); + + return await this.mesh.complete(); + }; + + burnToken = async ( + assetName: string, + quantity: string, + txhash: string, + outputIndex: number, + issuerAdminPkh: string, + ): Promise => { + const params = this._params; + const fetcher = this.fetcher; + const wallet = this.wallet; + if (!params || !fetcher || !wallet) + throw new Error( + "Contract parameters, fetcher, or wallet not initialized", + ); + const { + utxos: walletUtxos, + walletAddress: changeAddress, + collateral, + } = await this.getWalletInfoForTx(); + + const standard = new StandardScripts(this.networkId); + const substandard = new SubStandardScripts(this.networkId); + + const programmableLogicBase = await standard.programmableLogicBase(params); + const programmableLogicGlobal = + await standard.programmableLogicGlobal(params); + const registrySpend = await standard.registrySpend(params); + const substandardIssue = await substandard.issuerAdmin(issuerAdminPkh); + const issuanceMint = await standard.issuanceMint( + substandardIssue.policyId, + params, + ); + + const utxoToBurn = (await fetcher.fetchUTxOs(txhash, outputIndex))?.[0]; + if (!utxoToBurn) throw new Error("Token UTxO not found"); + + const tokenUnit = issuanceMint.policyId + stringToHex(assetName); + const utxoTokenAmount = + utxoToBurn.output.amount.find((a) => a.unit === tokenUnit)?.quantity ?? + "0"; + if (Number(quantity) > Number(utxoTokenAmount)) + throw new Error("Not enough tokens to burn"); + + const registryUtxos = await fetcher.fetchAddressUTxOs( + registrySpend.address, + ); + const progTokenRegistry = registryUtxos.find((utxo) => { + if (!utxo.output.plutusData) return false; + const parsed = parseRegistryDatum( + deserializeDatum(utxo.output.plutusData), + ); + return parsed?.key === issuanceMint.policyId; + }); + if (!progTokenRegistry) + throw new Error("Registry entry not found, token not registered"); + + const feePayerUtxo = walletUtxos.find( + (u) => + BigInt( + u.output.amount.find((a) => a.unit === "lovelace")?.quantity ?? "0", + ) > 5_000_000n, + ); + if (!feePayerUtxo) + throw new Error("No UTXO with enough ADA for fees found"); + + const protocolParamsUtxo = ( + await fetcher.fetchUTxOs(params.txHash, 0) + )?.[0]; + if (!protocolParamsUtxo) throw new Error("Protocol params missing"); + + const totalInputs = [feePayerUtxo, utxoToBurn].length; + + const compareUtxos = (a: UTxO, b: UTxO): number => + a.input.txHash !== b.input.txHash + ? a.input.txHash.localeCompare(b.input.txHash) + : a.input.outputIndex - b.input.outputIndex; + + const sortedRefInputs = [protocolParamsUtxo, progTokenRegistry].sort( + compareUtxos, + ); + + const registryRefInputIndex = sortedRefInputs.findIndex( + (r) => + r.input.txHash === progTokenRegistry.input.txHash && + r.input.outputIndex === progTokenRegistry.input.outputIndex, + ); + if (registryRefInputIndex === -1) + throw new Error("Could not find registry in sorted reference inputs"); + + const issuanceRedeemer = conStr0([ + conStr1([byteString(substandardIssue.policyId)]), + ]); + + const programmableGlobalRedeemer = conStr1([ + integer(registryRefInputIndex), + integer(0), // outputs_start_idx + integer(totalInputs), // length_inputs + ]); + + const returningAmount = utxoToBurn.output.amount + .map((a) => + a.unit === tokenUnit + ? { + unit: a.unit, + quantity: String(BigInt(a.quantity) - BigInt(quantity)), + } + : a, + ) + .filter((a) => BigInt(a.quantity) > 0n); + + this.mesh.txEvaluationMultiplier = 1.3; + this.mesh + .txIn(feePayerUtxo.input.txHash, feePayerUtxo.input.outputIndex) + .spendingPlutusScriptV3() + .txIn(utxoToBurn.input.txHash, utxoToBurn.input.outputIndex) + .txInScript(programmableLogicBase.cbor) + .txInInlineDatumPresent() + .txInRedeemerValue(conStr0([]), "JSON") + + .withdrawalPlutusScriptV3() + .withdrawal(substandardIssue.rewardAddress, "0") + .withdrawalScript(substandardIssue.cbor) + .withdrawalRedeemerValue(conStr0([]), "JSON") + + .withdrawalPlutusScriptV3() + .withdrawal(programmableLogicGlobal.rewardAddress, "0") + .withdrawalScript(programmableLogicGlobal.cbor) + .withdrawalRedeemerValue(programmableGlobalRedeemer, "JSON") + + .mintPlutusScriptV3() + .mint(`-${quantity}`, issuanceMint.policyId, stringToHex(assetName)) + .mintingScript(issuanceMint.cbor) + .mintRedeemerValue(issuanceRedeemer, "JSON"); + + if (returningAmount.length > 0) { + this.mesh + .txOut(utxoToBurn.output.address, returningAmount) + .txOutInlineDatumValue(conStr0([]), "JSON"); + } + + for (const refInput of sortedRefInputs) { + this.mesh.readOnlyTxInReference( + refInput.input.txHash, + refInput.input.outputIndex, + ); + } + + this.mesh + .requiredSignerHash(issuerAdminPkh) + .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) + .selectUtxosFrom(walletUtxos) + .setNetwork(this.networkId === 0 ? "preview" : "mainnet") + .changeAddress(changeAddress); + + return await this.mesh.complete(); + }; + + transferToken = async ( + unit: string, + quantity: string, + senderSmartWallet: smartWalletAddress, + recipientSmartWallet: smartWalletAddress, + ): Promise => { + const params = this._params; + const fetcher = this.fetcher; + const wallet = this.wallet; + if (!params || !fetcher || !wallet) + throw new Error( + "Contract parameters, fetcher, or wallet not initialized", + ); + + const policyId = unit.substring(0, POLICY_ID_LENGTH); + const { + utxos: walletUtxos, + walletAddress: changeAddress, + collateral, + } = await this.getWalletInfoForTx(); + + const standard = new StandardScripts(this.networkId); + const substandard = new SubStandardScripts(this.networkId); + + const programmableLogicBase = await standard.programmableLogicBase(params); + const programmableLogicGlobal = + await standard.programmableLogicGlobal(params); + const registrySpend = await standard.registrySpend(params); + + if (!this._blacklistBootstrap) + throw new Error("Blacklist bootstrap not initialized"); + const blacklistNodePolicyId = + this._blacklistBootstrap.blacklistMintBootstrap.scriptHash; + const substandardTransfer = await substandard.customTransfer( + params.programmableLogicBaseParams.scriptHash, + blacklistNodePolicyId, + ); + const substandardTransferCbor = substandardTransfer.cbor; + + const progTokenRegistry = ( + await fetcher.fetchAddressUTxOs(registrySpend.address) + ).find((utxo: UTxO) => { + if (!utxo.output.plutusData) return false; + return ( + parseRegistryDatum(deserializeDatum(utxo.output.plutusData))?.key === + policyId + ); + }); + if (!progTokenRegistry) + throw new Error("Could not find registry entry for token"); + + const protocolParamsUtxo = ( + await fetcher.fetchUTxOs(params.txHash, 0) + )?.[0]; + if (!protocolParamsUtxo) + throw new Error("Could not resolve protocol params"); + + const senderProgTokenUtxos = + await fetcher.fetchAddressUTxOs(senderSmartWallet); + if (!senderProgTokenUtxos?.length) + throw new Error("No programmable tokens found at sender address"); + + const { selectedUtxos } = await selectProgrammableTokenUtxos( + senderProgTokenUtxos, + unit, + Number(quantity), + ); + if (!selectedUtxos.length) throw new Error("Not enough funds"); + + const compareUtxos = (a: UTxO, b: UTxO): number => + a.input.txHash !== b.input.txHash + ? a.input.txHash.localeCompare(b.input.txHash) + : a.input.outputIndex - b.input.outputIndex; + + const sortedInputs = [...selectedUtxos].sort(compareUtxos); + const programmableInputs: UTxO[] = []; + const uniquePolicies: string[] = []; + + for (const utxo of sortedInputs) { + programmableInputs.push(utxo); + for (const asset of utxo.output.amount) { + if (asset.unit === "lovelace") continue; + const p = asset.unit.substring(0, 56); + if (!uniquePolicies.includes(p)) uniquePolicies.push(p); + } + } + uniquePolicies.sort(); + + const blacklistProofs: UTxO[] = []; + if (blacklistNodePolicyId) { + const blacklistSpend = await substandard.blacklistSpend( + blacklistNodePolicyId, + ); + const blacklistUtxos = await fetcher.fetchAddressUTxOs( + blacklistSpend.address, + ); + + for (const utxo of programmableInputs) { + const stakingPkh = resolveStakeCredential(utxo.output.address); + if (!stakingPkh) throw new Error("UTXO missing stake credential"); + + const proofUtxo = blacklistUtxos.find((bl: UTxO) => { + if (!bl.output.plutusData) return false; + const datum = parseBlacklistDatum( + deserializeDatum(bl.output.plutusData), + ); + if (!datum) return false; + const isGreater = datum.key === "" || stakingPkh > datum.key; + const isLess = datum.next === "" || stakingPkh < datum.next; + return isGreater && isLess; + }); + + if (!proofUtxo) + throw new Error(`Blacklist proof not found for wallet ${stakingPkh}`); + blacklistProofs.push(proofUtxo); + } + } + + const registryProofs: UTxO[] = []; + const registryUtxos = await fetcher.fetchAddressUTxOs( + registrySpend.address, + ); + for (const p of uniquePolicies) { + const registryNft = params.directoryMintParams.scriptHash + p; + const proofUtxo = registryUtxos.find((u) => + u.output.amount.find((a) => a.unit === registryNft), + ); + if (!proofUtxo) + throw new Error(`Registry node not found for policy ${p}`); + registryProofs.push(proofUtxo); + } + + const uniqueBlacklistProofs = [ + ...new Map( + blacklistProofs.map((p) => [ + `${p.input.txHash}#${p.input.outputIndex}`, + p, + ]), + ).values(), + ]; + + const sortedRefInputs = [ + ...uniqueBlacklistProofs, + ...registryProofs, + protocolParamsUtxo, + ].sort(compareUtxos); + + const substandardTransferRedeemer = list( + blacklistProofs.map((p) => { + const idx = sortedRefInputs.findIndex( + (r) => + r.input.txHash === p.input.txHash && + r.input.outputIndex === p.input.outputIndex, + ); + return conStr0([integer(idx)]); + }), + ); + + const programmableGlobalRedeemer = conStr0([ + list( + registryProofs.map((p) => { + const idx = sortedRefInputs.findIndex( + (r) => + r.input.txHash === p.input.txHash && + r.input.outputIndex === p.input.outputIndex, + ); + return conStr0([integer(idx)]); + }), + ), + ]); + + const totalTokens = selectedUtxos.reduce( + (sum, utxo) => + sum + + BigInt( + utxo.output.amount.find((a) => a.unit === unit)?.quantity ?? "0", + ), + 0n, + ); + const totalLovelace = selectedUtxos.reduce( + (sum, utxo) => + sum + + BigInt( + utxo.output.amount.find((a) => a.unit === "lovelace")?.quantity ?? + "0", + ), + 0n, + ); + + if (totalTokens < BigInt(quantity)) throw new Error("Not enough funds"); + + const recipientLovelace = 1500000n; + const remainingTokens = totalTokens - BigInt(quantity); + const recipientAssets = [ + { unit: "lovelace", quantity: recipientLovelace.toString() }, + { unit: unit, quantity: quantity }, + ]; + const remainingLovelace = totalLovelace - recipientLovelace; + const returningAssets = [ + { + unit: "lovelace", + quantity: (remainingLovelace > 1_000_000n + ? remainingLovelace + : 1_500_000n + ).toString(), + }, + ...(remainingTokens > 0n + ? [{ unit: unit, quantity: remainingTokens.toString() }] + : []), + ]; + + this.mesh.verbose = true; + this.mesh.evaluator = this.inputs.mesh.evaluator; + for (const utxo of sortedInputs) { + this.mesh + .spendingPlutusScriptV3() + .txIn(utxo.input.txHash, utxo.input.outputIndex) + .txInScript(programmableLogicBase.cbor) + .txInRedeemerValue(conStr0([]), "JSON") + .txInInlineDatumPresent(); + } + + this.mesh + .withdrawalPlutusScriptV3() + .withdrawal(substandardTransfer.rewardAddress, "0") + .withdrawalScript(substandardTransferCbor) + .withdrawalRedeemerValue(substandardTransferRedeemer, "JSON") + + .withdrawalPlutusScriptV3() + .withdrawal(programmableLogicGlobal.rewardAddress, "0") + .withdrawalScript(programmableLogicGlobal.cbor) + .withdrawalRedeemerValue(programmableGlobalRedeemer, "JSON"); + + if (remainingTokens > 0n || remainingLovelace > 1_000_000n) { + this.mesh + .txOut(senderSmartWallet, returningAssets) + .txOutInlineDatumValue(conStr0([]), "JSON"); + } + + this.mesh + .txOut(recipientSmartWallet, recipientAssets) + .txOutInlineDatumValue(conStr0([]), "JSON"); + + for (const refInput of sortedRefInputs) { + this.mesh.readOnlyTxInReference( + refInput.input.txHash, + refInput.input.outputIndex, + ); + } + + this.mesh + .requiredSignerHash(resolveStakeCredential(senderSmartWallet)) + .setFee("600000") + .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) + .selectUtxosFrom(walletUtxos) + .setNetwork(this.networkId === 0 ? "preview" : "mainnet") + .changeAddress(changeAddress); + + return await this.mesh.complete(); + }; + + blacklistSmartWalletAddress = async ( + smartWalletAddress: smartWalletAddress, + ): Promise => { + const fetcher = this.fetcher; + const wallet = this.wallet; + if (!fetcher || !wallet) + throw new Error("Fetcher or wallet not initialized"); + + const { + utxos: walletUtxos, + walletAddress: changeAddress, + collateral, + } = await this.getWalletInfoForTx(); + + if (!this._blacklistBootstrap) + throw new Error("Blacklist bootstrap not initialized"); + const blacklistMintBootstrap = + this._blacklistBootstrap.blacklistMintBootstrap; + const { blacklistMint, blacklistSpend } = await resolveBlacklistScripts( + this.networkId as 0 | 1, + blacklistMintBootstrap.txInput, + blacklistMintBootstrap.adminPubKeyHash, + ); + if (!this.fetcher) { + throw new Error("Fetcher not initialized"); + } + + const blacklistUtxos = await fetcher.fetchAddressUTxOs( + blacklistSpend.address, + ); + if (!blacklistUtxos?.length) throw new Error("No blacklist UTxOs found"); + + const targetStakeHash = resolveStakeCredential(smartWalletAddress); + + let nodeToReplace: UTxO | null = null; + let preexistingNode: BlacklistDatum | null = null; + + for (const utxo of blacklistUtxos) { + if (!utxo.output.plutusData) continue; + const datum = parseBlacklistDatum( + deserializeDatum(utxo.output.plutusData), + ); + if (!datum) continue; + if (datum.key === targetStakeHash) + throw new Error("Target address is already blacklisted"); + if ( + datum.key.localeCompare(targetStakeHash) < 0 && + targetStakeHash.localeCompare(datum.next) < 0 + ) { + nodeToReplace = utxo; + preexistingNode = datum; + break; + } + } + + if (!nodeToReplace || !preexistingNode) + throw new Error("Could not find blacklist node to replace"); + + const beforeNode = conStr0([ + byteString(preexistingNode.key), + byteString(targetStakeHash), + ]); + const afterNode = conStr0([ + byteString(targetStakeHash), + byteString(preexistingNode.next), + ]); + + const mintRedeemer = conStr1([byteString(targetStakeHash)]); + const spendRedeemer = conStr0([]); + const mintedAssets: Asset[] = [ + { + unit: blacklistMint.policyId + targetStakeHash, + quantity: "1", + }, + ]; + + this.mesh.txEvaluationMultiplier = 1.3; + this.mesh + .spendingPlutusScriptV3() + .txIn(nodeToReplace.input.txHash, nodeToReplace.input.outputIndex) + .txInScript(blacklistSpend.cbor) + .txInRedeemerValue(spendRedeemer, "JSON") + .txInInlineDatumPresent() + + .mintPlutusScriptV3() + .mint("1", blacklistMint.policyId, targetStakeHash) + .mintingScript(blacklistMint.cbor) + .mintRedeemerValue(mintRedeemer, "JSON") + + .txOut(blacklistSpend.address, nodeToReplace.output.amount) + .txOutInlineDatumValue(beforeNode, "JSON") + + .txOut(blacklistSpend.address, mintedAssets) + .txOutInlineDatumValue(afterNode, "JSON") + + .requiredSignerHash( + this._blacklistBootstrap.blacklistMintBootstrap.adminPubKeyHash, + ) + .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) + .selectUtxosFrom(walletUtxos) + .setNetwork(this.networkId === 0 ? "preview" : "mainnet") + .changeAddress(changeAddress); + + return await this.mesh.complete(); + }; + + whitelistSmartWalletAddress = async ( + smartWalletAddress: smartWalletAddress, + ): Promise => { + const fetcher = this.fetcher; + const { + utxos: walletUtxos, + walletAddress: changeAddress, + collateral, + } = await this.getWalletInfoForTx(); + if (!fetcher) throw new Error("Fetcher or wallet not initialized"); + + const credentialsToRemove = resolveStakeCredential(smartWalletAddress); + + if (!this._blacklistBootstrap) + throw new Error("Blacklist bootstrap not initialized"); + const blacklistMintBootstrap = + this._blacklistBootstrap.blacklistMintBootstrap; + const { blacklistMint, blacklistSpend } = await resolveBlacklistScripts( + this.networkId as 0 | 1, + blacklistMintBootstrap.txInput, + blacklistMintBootstrap.adminPubKeyHash, + ); + if (!this.fetcher) { + throw new Error("Fetcher not initialized"); + } + + const blacklistUtxos = await fetcher.fetchAddressUTxOs( + blacklistSpend.address, + ); + if (!blacklistUtxos?.length) throw new Error("No blacklist UTxOs found"); + + let nodeToRemove: UTxO | null = null; + let nodeToRemoveDatum: { key: string; next: string } | null = null; + let nodeToUpdate: UTxO | null = null; + let nodeToUpdateDatum: { key: string; next: string } | null = null; + + for (const utxo of blacklistUtxos) { + if (!utxo.output.plutusData) continue; + const datum = parseBlacklistDatum( + deserializeDatum(utxo.output.plutusData), + ); + if (!datum) continue; + if (datum.key === credentialsToRemove) { + nodeToRemove = utxo; + nodeToRemoveDatum = datum; + } + if (datum.next === credentialsToRemove) { + nodeToUpdate = utxo; + nodeToUpdateDatum = datum; + } + if (nodeToRemove && nodeToUpdate) break; + } + + if (!nodeToRemove || !nodeToRemoveDatum) + throw new Error( + "Could not resolve relevant blacklist nodes (node to remove)", + ); + if (!nodeToUpdate || !nodeToUpdateDatum) + throw new Error( + "Could not resolve relevant blacklist nodes (node to update)", + ); + + const newNext = nodeToRemoveDatum.next; + const updatedNode = conStr0([ + byteString(nodeToUpdateDatum.key), + byteString(newNext), + ]); + + const mintRedeemer = conStr2([byteString(credentialsToRemove)]); + const spendRedeemer = conStr0([]); + + this.mesh.txEvaluationMultiplier = 1.3; + this.mesh + .spendingPlutusScriptV3() + .txIn(nodeToRemove.input.txHash, nodeToRemove.input.outputIndex) + .txInScript(blacklistSpend.cbor) + .txInInlineDatumPresent() + .txInRedeemerValue(spendRedeemer, "JSON") + + .spendingPlutusScriptV3() + .txIn(nodeToUpdate.input.txHash, nodeToUpdate.input.outputIndex) + .txInScript(blacklistSpend.cbor) + .txInInlineDatumPresent() + .txInRedeemerValue(spendRedeemer, "JSON") + + .mintPlutusScriptV3() + .mint("-1", blacklistMint.policyId, credentialsToRemove) + .mintingScript(blacklistMint.cbor) + .mintRedeemerValue(mintRedeemer, "JSON") + + .txOut(blacklistSpend.address, nodeToUpdate.output.amount) + .txOutInlineDatumValue(updatedNode, "JSON") + + .requiredSignerHash( + this._blacklistBootstrap.blacklistMintBootstrap.adminPubKeyHash, + ) + .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) + .selectUtxosFrom(walletUtxos) + .setNetwork(this.networkId === 0 ? "preview" : "mainnet") + .changeAddress(changeAddress); + + return await this.mesh.complete(); + }; + + seizeToken = async ( + unit: string, + txHash: string, + outputIndex: number, + issuerAdminPkh: string, + recipientSmartWallet: smartWalletAddress, + ): Promise => { + const params = this._params; + const fetcher = this.fetcher; + const wallet = this.wallet; + if (!params || !fetcher || !wallet) + throw new Error( + "Contract parameters, fetcher, or wallet not initialized", + ); + + const policyId = unit.substring(0, POLICY_ID_LENGTH); + const { + utxos: walletUtxos, + walletAddress: changeAddress, + collateral, + } = await this.getWalletInfoForTx(); + + const standardScript = new StandardScripts(this.networkId); + const substandardScript = new SubStandardScripts(this.networkId); + const programmableLogicBase = + await standardScript.programmableLogicBase(params); + const programmableLogicGlobal = + await standardScript.programmableLogicGlobal(params); + const registrySpend = await standardScript.registrySpend(params); + + const feePayerUtxo = walletUtxos.find( + (u) => + BigInt( + u.output.amount.find((a) => a.unit === "lovelace")?.quantity ?? "0", + ) > 10_000_000n, + ); + if (!feePayerUtxo) + throw new Error("No UTXO with enough ADA for fees found"); + + const utxosAtRef = await fetcher.fetchUTxOs(txHash, outputIndex); + const utxoToSeize = utxosAtRef?.[0]; + if (!utxoToSeize) throw new Error("Could not find utxo to seize"); + if (!utxoToSeize.output.plutusData) + throw new Error("UTXO to seize must have inline datum"); + + const totalInputs = [feePayerUtxo, utxoToSeize].length; + const tokenAsset = utxoToSeize.output.amount.find((a) => a.unit === unit); + if (!tokenAsset) + throw new Error("UTXO does not contain the specified token"); + if (Number(tokenAsset.quantity) <= 0) + throw new Error("UTXO token quantity must be greater than zero"); + + const registryUtxos = await fetcher.fetchAddressUTxOs( + registrySpend.address, + ); + const progTokenRegistry = registryUtxos.find((utxo: UTxO) => { + if (!utxo.output.plutusData) return false; + const parsedDatum = parseRegistryDatum( + deserializeDatum(utxo.output.plutusData), + ); + return parsedDatum?.key === policyId; + }); + if (!progTokenRegistry) + throw new Error("Could not find registry entry for token"); + + const protocolParamsUtxo = ( + await fetcher.fetchUTxOs(params.txHash, 0) + )?.[0]; + if (!protocolParamsUtxo) + throw new Error("Could not resolve protocol params"); + + const compareUtxos = (a: UTxO, b: UTxO): number => + a.input.txHash !== b.input.txHash + ? a.input.txHash.localeCompare(b.input.txHash) + : a.input.outputIndex - b.input.outputIndex; + + const sortedRefInputs = [protocolParamsUtxo, progTokenRegistry].sort( + compareUtxos, + ); + const registryRefInputIndex = sortedRefInputs.findIndex( + (r) => + r.input.txHash === progTokenRegistry.input.txHash && + r.input.outputIndex === progTokenRegistry.input.outputIndex, + ); + if (registryRefInputIndex === -1) + throw new Error("Could not find registry in sorted reference inputs"); + + const programmableGlobalRedeemer = conStr1([ + integer(registryRefInputIndex), + integer(1), // outputs_start_idx (skip recipient output) + integer(totalInputs), // length_inputs + ]); + + const seizedAssets: Asset[] = [ + { unit: "lovelace", quantity: "1500000" }, + { unit: unit, quantity: tokenAsset.quantity }, + ]; + const remainingAssets: Asset[] = utxoToSeize.output.amount.filter( + (a) => a.unit !== unit, + ); + if (remainingAssets.length === 0) { + remainingAssets.push({ unit: "lovelace", quantity: "1000000" }); + } + + const substandardIssueAdmin = + await substandardScript.issuerAdmin(issuerAdminPkh); + + this.mesh.txEvaluationMultiplier = 1.3; + this.mesh + .txIn(feePayerUtxo.input.txHash, feePayerUtxo.input.outputIndex) + .spendingPlutusScriptV3() + .txIn(utxoToSeize.input.txHash, utxoToSeize.input.outputIndex) + .txInScript(programmableLogicBase.cbor) + .txInRedeemerValue(conStr0([]), "JSON") + .txInInlineDatumPresent() + + .withdrawalPlutusScriptV3() + .withdrawal(substandardIssueAdmin.rewardAddress, "0") + .withdrawalScript(substandardIssueAdmin.cbor) + .withdrawalRedeemerValue(conStr0([]), "JSON") + + .withdrawalPlutusScriptV3() + .withdrawal(programmableLogicGlobal.rewardAddress, "0") + .withdrawalScript(programmableLogicGlobal.cbor) + .withdrawalRedeemerValue(programmableGlobalRedeemer, "JSON") + + .txOut(recipientSmartWallet, seizedAssets) + .txOutInlineDatumValue(conStr0([]), "JSON") + + .txOut(utxoToSeize.output.address, remainingAssets) + .txOutInlineDatumValue(conStr0([]), "JSON"); + + for (const refInput of sortedRefInputs) { + this.mesh.readOnlyTxInReference( + refInput.input.txHash, + refInput.input.outputIndex, + ); + } + + this.mesh + .requiredSignerHash(issuerAdminPkh) + .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) + .selectUtxosFrom(walletUtxos) + .setNetwork(this.networkId === 0 ? "preview" : "mainnet") + .changeAddress(changeAddress); + + return await this.mesh.complete(); + }; + + initializeBlacklist = async ( + adminPubKeyHash: string, + ): Promise<{ + txHex: string; + bootstrap: BlacklistBootstrap; + }> => { + const params = this._params; + const wallet = this.wallet; + if (!params || !wallet) + throw new Error("Contract parameters or wallet not initialized"); + + const { + utxos: walletUtxos, + walletAddress: changeAddress, + collateral, + } = await this.getWalletInfoForTx(); + + const utilityUtxos = walletUtxos.filter((utxo) => { + const lovelaceAsset = utxo.output.amount.find( + (a) => a.unit === "lovelace", + ); + if (!lovelaceAsset) return false; + const hasOnlyAda = utxo.output.amount.length === 1; + const hasEnoughAda = Number(lovelaceAsset.quantity) >= 10_000_000; + return hasOnlyAda && hasEnoughAda; + }); + + if (utilityUtxos.length === 0) { + throw new Error("No suitable UTxOs found for bootstrap"); + } + + const bootstrapInput = utilityUtxos[0]!.input; + const substandardScript = new SubStandardScripts(this.networkId); + + const blacklistMint = await substandardScript.blacklistMint( + bootstrapInput, + adminPubKeyHash, + ); + const blacklistMintPolicyId = blacklistMint.policyId; + const blacklistSpend = await substandardScript.blacklistSpend( + blacklistMintPolicyId, + ); + const blacklistSpendAddress = blacklistSpend.address; + + const blacklistInitDatum = conStr0([ + byteString(""), + byteString( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ), + ]); + + const blacklistAssets: Asset[] = [ + { + unit: blacklistMintPolicyId, + quantity: "1", + }, + ]; + + this.mesh.txEvaluationMultiplier = 1.3; + this.mesh + .txIn(bootstrapInput.txHash, bootstrapInput.outputIndex) + .mintPlutusScriptV3() + .mint("1", blacklistMintPolicyId, stringToHex("")) + .mintingScript(blacklistMint.cbor) + .mintRedeemerValue(conStr0([]), "JSON") + + .txOut(blacklistSpendAddress, blacklistAssets) + .txOutInlineDatumValue(blacklistInitDatum, "JSON") + + .txInCollateral(collateral.input.txHash, collateral.input.outputIndex) + .setNetwork(this.networkId === 0 ? "preview" : "mainnet") + .selectUtxosFrom(utilityUtxos) + .changeAddress(changeAddress); + + const txHex = await this.mesh.complete(); + + const bootstrap: BlacklistBootstrap = { + blacklistMintBootstrap: { + txInput: bootstrapInput, + adminPubKeyHash: adminPubKeyHash, + scriptHash: blacklistMintPolicyId, + }, + blacklistSpendBootstrap: { + blacklistMintScriptHash: blacklistMintPolicyId, + scriptHash: blacklistSpend.policyId, + }, + }; + + return { txHex, bootstrap }; + }; +} diff --git a/src/programmable-tokens/offchain/protocolParams.json b/src/programmable-tokens/offchain/protocolParams.json new file mode 100644 index 0000000..4eb8acb --- /dev/null +++ b/src/programmable-tokens/offchain/protocolParams.json @@ -0,0 +1,47 @@ +{ + "txHash": "259e58611145bf7276bf5e97e7924cfa704f0d658b7e53b445be976649fc59e7", + "protocolParams": { + "txInput": { + "txHash": "c39b5e0db71df7fd41d7dc44d66911d99d98bb7e76127b5335bb04dc3087f973", + "outputIndex": 0 + }, + "scriptHash": "debc6597dd54b0c372dc30c72494f49b2a0a6a6c9058dc6a67748214", + "alwaysFailScriptHash": "de8efca54bfd3b0a445f9d9f72a1c1b2524928f810ea106a6ca9aa99" + }, + "programmableLogicGlobalPrams": { + "protocolParamsScriptHash": "debc6597dd54b0c372dc30c72494f49b2a0a6a6c9058dc6a67748214", + "scriptHash": "24b338c9996217cc2a9770e00efe7d66ad459fa368d0eb804956c046" + }, + "programmableLogicBaseParams": { + "programmableLogicGlobalScriptHash": "24b338c9996217cc2a9770e00efe7d66ad459fa368d0eb804956c046", + "scriptHash": "3a7ce34145cbc597a85ac83db88c029ff9e909d92e04a7b2376b31ff" + }, + "issuanceParams": { + "txInput": { + "txHash": "c39b5e0db71df7fd41d7dc44d66911d99d98bb7e76127b5335bb04dc3087f973", + "outputIndex": 1 + }, + "scriptHash": "188a2b2cc43b93f026ccb56bfe7a1eea121e9a74e5bdb6259a0215e1", + "alwaysFailScriptHash": "de8efca54bfd3b0a445f9d9f72a1c1b2524928f810ea106a6ca9aa99" + }, + "directoryMintParams": { + "txInput": { + "txHash": "c39b5e0db71df7fd41d7dc44d66911d99d98bb7e76127b5335bb04dc3087f973", + "outputIndex": 0 + }, + "issuanceScriptHash": "188a2b2cc43b93f026ccb56bfe7a1eea121e9a74e5bdb6259a0215e1", + "scriptHash": "91b403111a7fab9ffbf5a3c7485d3f9a79d5bf65f38c8d48359be4c0" + }, + "directorySpendParams": { + "protocolParamsPolicyId": "debc6597dd54b0c372dc30c72494f49b2a0a6a6c9058dc6a67748214", + "scriptHash": "b672f3b03d19cd40c59c447aabd6c26ca296bbc78b205e5227317638" + }, + "programmableBaseRefInput": { + "txHash": "259e58611145bf7276bf5e97e7924cfa704f0d658b7e53b445be976649fc59e7", + "outputIndex": 3 + }, + "programmableGlobalRefInput": { + "txHash": "259e58611145bf7276bf5e97e7924cfa704f0d658b7e53b445be976649fc59e7", + "outputIndex": 4 + } +} \ No newline at end of file diff --git a/src/programmable-tokens/offchain/resolvers.ts b/src/programmable-tokens/offchain/resolvers.ts new file mode 100644 index 0000000..e437799 --- /dev/null +++ b/src/programmable-tokens/offchain/resolvers.ts @@ -0,0 +1,82 @@ +import { + deserializeAddress, + buildBaseAddress, + CredentialType, + Hash28ByteBase16, +} from "@meshsdk/core-cst"; +import { StandardScripts, SubStandardScripts } from "./common"; +import { smartWalletAddress, stakeCredential } from "./type"; +import params from "./protocolParams.json"; +import { TxInput } from "@meshsdk/common"; + +/** + * Resolves the programmable smart wallet address for a given base address. + */ +export const resolveSmartWalletAddress = async ( + address: string, + networkId: 0 | 1, +): Promise => { + const credential = deserializeAddress(address) + .asBase() + ?.getStakeCredential().hash; + + if (!credential) { + throw new Error("Address missing stake credential"); + } + + const standardScript = new StandardScripts(networkId); + const programmableLogicBase = + await standardScript.programmableLogicBase(params); + + const baseAddress = buildBaseAddress( + networkId, + programmableLogicBase.policyId as Hash28ByteBase16, + credential, + CredentialType.ScriptHash, + CredentialType.KeyHash, + ); + + return baseAddress.toAddress().toBech32() as smartWalletAddress; +}; + +/** + * Resolves the stake credential hash from a bech32 address. + */ +export const resolveStakeCredential = (address: string): stakeCredential => { + const cred = deserializeAddress(address).asBase()?.getStakeCredential().hash; + if (!cred) throw new Error("Address missing stake credential"); + return cred as stakeCredential; +}; + +/** + * Resolves the blacklist mint and spend scripts. + */ +export async function resolveBlacklistScripts( + networkId: 0 | 1, + blacklistMintBootstrapTxInput: TxInput, + blacklistAdminPkh: string, +) { + const substandardScript = new SubStandardScripts(networkId); + const blacklistMint = await substandardScript.blacklistMint( + blacklistMintBootstrapTxInput, + blacklistAdminPkh, + ); + const blacklistSpend = await substandardScript.blacklistSpend( + blacklistMint.policyId, + ); + return { blacklistMint, blacklistSpend }; +} + +/** + * Resolves the blacklist script address. + */ +export const resolveBlacklistAddress = async ( + blacklistMintScriptHash: string, + networkId: number, +): Promise => { + const substandard = new SubStandardScripts(networkId); + const blacklistSpend = await substandard.blacklistSpend( + blacklistMintScriptHash, + ); + return blacklistSpend.address; +}; diff --git a/src/programmable-tokens/offchain/type.ts b/src/programmable-tokens/offchain/type.ts new file mode 100644 index 0000000..bb2cbbe --- /dev/null +++ b/src/programmable-tokens/offchain/type.ts @@ -0,0 +1,121 @@ +import { PlutusScript } from "@meshsdk/common"; + +export type smartWalletAddress = string & { readonly __brand: "SmartWalletAddress" }; + +export type stakeCredential = string & { readonly __brand: "StakeCredential" }; + +export type BlacklistDatum = { + key: string; + next: string; +}; + +export type RegistryCredential = { + hash: string; + index: number; +}; + +export type RegistryDatum = { + key: string; + next: string; + transferScript: RegistryCredential; + thirdPartyScript: RegistryCredential; + metadata: string; +}; + +export type TxInput = { + txHash: string; + outputIndex: number; +}; + +export type ProtocolParams = { + txInput: TxInput; + scriptHash: string; + alwaysFailScriptHash: string; +}; + +export type ProgrammableLogicGlobalParams = { + protocolParamsScriptHash: string; + scriptHash: string; +}; + +export type ProgrammableLogicBaseParams = { + programmableLogicGlobalScriptHash: string; + scriptHash: string; +}; + +export type IssuanceParams = { + txInput: TxInput; + scriptHash: string; + alwaysFailScriptHash: string; +}; + +export type DirectoryMintParams = { + txInput: TxInput; + issuanceScriptHash: string; + scriptHash: string; +}; + +export type DirectorySpendParams = { + protocolParamsPolicyId: string; + scriptHash: string; +}; + +export type BlacklistMintBootstrap = { + txInput: TxInput; + adminPubKeyHash: string; + scriptHash: string; +}; + +export type BlacklistSpendBootstrap = { + blacklistMintScriptHash: string; + scriptHash: string; +}; + +export type BlacklistBootstrap = { + blacklistMintBootstrap: BlacklistMintBootstrap; + blacklistSpendBootstrap: BlacklistSpendBootstrap; +}; + +export type ProtocolBootstrapParams = { + protocolParams: ProtocolParams; + programmableLogicGlobalPrams: ProgrammableLogicGlobalParams; + programmableLogicBaseParams: ProgrammableLogicBaseParams; + issuanceParams: IssuanceParams; + directoryMintParams: DirectoryMintParams; + directorySpendParams: DirectorySpendParams; + programmableBaseRefInput: TxInput; + programmableGlobalRefInput: TxInput; + txHash: string; +}; + +export type TokenScripts = { + mintingLogic: PlutusScript; + transferLogic: PlutusScript; + globalStateLogic?: PlutusScript; + thirdPartyLogic?: PlutusScript; +}; + +export type RegisterTokenParams = { + assetName: string; + scripts: TokenScripts; + transferRedeemerValue: any; + recipientAddress?: string; +}; + +export type MintTokensParams = { + assetName: string; + scripts: { + mintingLogic: PlutusScript; + transferLogic: PlutusScript; + }; + transferRedeemerValue: any; + recipientAddress?: string | null; +}; + +export type TransferTokenParams = { + unit: string; + quantity: string; + recipientAddress: string; + transferLogic: PlutusScript; + transferRedeemerValue: any; +}; diff --git a/src/programmable-tokens/offchain/utils.ts b/src/programmable-tokens/offchain/utils.ts new file mode 100644 index 0000000..aa2ffd7 --- /dev/null +++ b/src/programmable-tokens/offchain/utils.ts @@ -0,0 +1,144 @@ +import cbor from "cbor"; +import subStandardPlutusScriptFreeze from "../aiken-workspace-subStandard/freeze-and-seize/plutus.json"; +import subStandardPlutusScriptDummy from "../aiken-workspace-subStandard/dummy/plutus.json"; +import standardPlutusScript from "../aiken-workspace-standard/plutus.json"; +import { + BlacklistBootstrap, + BlacklistDatum, + RegistryCredential, + RegistryDatum, +} from "./type"; +import { + deserializeAddress, +} from "@meshsdk/core-cst"; +import { + deserializeDatum, + IFetcher, + IWallet, + UTxO, +} from "@meshsdk/core"; +import { resolveBlacklistScripts } from "./resolvers"; + +export const findValidator = ( + validatorName: string, + isStandard: boolean = true, +): string => { + const sources = isStandard + ? [standardPlutusScript] + : [subStandardPlutusScriptFreeze, subStandardPlutusScriptDummy]; + + for (const script of sources) { + const match = script.validators.find( + ({ title }) => title === validatorName, + ); + if (match) return match.compiledCode; + } + + throw new Error(`Validator ${validatorName} not found`); +}; + +export const cborEncode = (cbor_param: string) => { + const _cbor = cbor.encode(Buffer.from(cbor_param, "hex")).toString("hex"); + return _cbor; +}; + +export const walletConfig = async (wallet: IWallet) => { + const changeAddress = await wallet.getChangeAddress(); + const walletUtxos = await wallet.getUtxos(); + const collateral = (await wallet.getCollateral())[0]; + if (!collateral) throw new Error("No collateral available"); + if (!walletUtxos) throw new Error("Wallet is empty"); + return { changeAddress, walletUtxos, collateral }; +}; + +function extractBytes(field: any): string { + if (!field) return ""; + if (typeof field === "string") return field; + if (field.bytes) return field.bytes; + if (field.fields && field.fields.length > 0) { + const inner = field.fields[0]; + if (typeof inner === "string") return inner; + if (inner?.bytes) return inner.bytes; + } + return ""; +} + +export function parseRegistryDatum(datum: any): RegistryDatum | null { + if (!datum?.fields || datum.fields.length < 5) { + return null; + } + + const getCredential = (field: any): RegistryCredential => { + return { + hash: extractBytes(field), + index: field.constructor ?? 0, + }; + }; + + return { + key: extractBytes(datum.fields[0]), + next: extractBytes(datum.fields[1]), + transferScript: getCredential(datum.fields[2]), + thirdPartyScript: getCredential(datum.fields[3]), + metadata: extractBytes(datum.fields[4]), + }; +} + +export function parseBlacklistDatum(datum: any): BlacklistDatum | null { + if (!datum?.fields || datum.fields.length < 2) { + return null; + } + return { + key: extractBytes(datum.fields[0]), + next: extractBytes(datum.fields[1]), + }; +} + + +export const selectProgrammableTokenUtxos = async ( + senderProgTokenUtxos: UTxO[], + unit: string, + amount: number, +) => { + let selectedUtxos: UTxO[] = []; + let selectedAmount = 0; + for (const utxo of senderProgTokenUtxos) { + if (selectedAmount >= amount) break; + const tokenAsset = utxo.output.amount.find((a) => a.unit === unit); + if (tokenAsset) { + selectedUtxos.push(utxo); + selectedAmount += Number(tokenAsset.quantity); + } + } + const returningAmount = selectedAmount - amount; + return { selectedUtxos, returningAmount }; +}; + +export const isAddressBlacklisted = async ( + address: string, + blacklistBootstrap: BlacklistBootstrap, + NetworkId: 0 | 1, + fetcher: IFetcher, +): Promise => { + const stakeCredential = deserializeAddress(address) + .asBase() + ?.getStakeCredential().hash; + + if (!stakeCredential) return false; + + const { blacklistSpend } = await resolveBlacklistScripts( + NetworkId, + blacklistBootstrap.blacklistMintBootstrap.txInput, + blacklistBootstrap.blacklistMintBootstrap.adminPubKeyHash, + ); + + const blacklistUtxos = await fetcher.fetchAddressUTxOs( + blacklistSpend.address, + ); + + return blacklistUtxos.some((utxo: UTxO) => { + if (!utxo.output.plutusData) return false; + const datum = parseBlacklistDatum(deserializeDatum(utxo.output.plutusData)); + return datum?.key === stakeCredential; + }); +}; diff --git a/src/programmable-tokens/types.ts b/src/programmable-tokens/types.ts deleted file mode 100644 index c40a737..0000000 --- a/src/programmable-tokens/types.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { PlutusScript } from "@meshsdk/common"; - -export type RegistryDatum = { - key: string; - next: any; - transferScriptHash: string; - thirdPartyScriptHash: string; - metadata: any; - }; - - export type TxInput = { - txHash: string; - outputIndex: number; - }; - - export type ProtocolParams = { - txInput: TxInput; - scriptHash: string; - }; - - export type ProgrammableLogicGlobalParams = { - protocolParamsScriptHash: string; - scriptHash: string; - }; - - export type ProgrammableLogicBaseParams = { - programmableLogicGlobalScriptHash: string; - scriptHash: string; - }; - - export type IssuanceParams = { - txInput: TxInput; - scriptHash: string; - }; - - export type DirectoryMintParams = { - txInput: TxInput; - issuanceScriptHash: string; - scriptHash: string; - }; - - export type DirectorySpendParams = { - protocolParamsPolicyId: string; - scriptHash: string; - }; - - export type ProtocolBootstrapParams = { - protocolParams: ProtocolParams; - programmableLogicGlobalPrams: ProgrammableLogicGlobalParams; - programmableLogicBaseParams: ProgrammableLogicBaseParams; - issuanceParams: IssuanceParams; - directoryMintParams: DirectoryMintParams; - directorySpendParams: DirectorySpendParams; - programmableBaseRefInput: TxInput; - programmableGlobalRefInput: TxInput; - txHash: string; - }; - - export type TokenScripts = { - mintingLogic: PlutusScript; - transferLogic: PlutusScript; - globalStateLogic?: PlutusScript; - thirdPartyLogic?: PlutusScript; - }; - - export type RegisterTokenParams = { - assetName: string; - scripts: TokenScripts; - transferRedeemerValue: any; - recipientAddress?: string; - }; - - export type MintTokensParams = { - assetName: string; - scripts: { - mintingLogic: PlutusScript; - transferLogic: PlutusScript; - }; - transferRedeemerValue: any; - recipientAddress?: string | null; - }; - - export type TransferTokenParams = { - unit: string; - quantity: string; - recipientAddress: string; - transferLogic: PlutusScript; - transferRedeemerValue: any; - }; \ No newline at end of file diff --git a/src/programmable-tokens/utils.ts b/src/programmable-tokens/utils.ts deleted file mode 100644 index 081a375..0000000 --- a/src/programmable-tokens/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import standard_plutusScript from "./aiken-workspace/plutus.json"; -import { RegistryDatum } from "./types"; - -export const findValidator = ( - validatorName: string, - purpose: string, -) => { - const validator = standard_plutusScript.validators.find( - ({ title }) => title === `${validatorName}.${validatorName}.${purpose}`, - ); - if (!validator) { - throw new Error( - `Validator ${validatorName}.${validatorName}.${purpose} not found`, - ); - } - return validator.compiledCode; - }; - -export function parseRegistryDatum(datum: any): RegistryDatum | null { - if (!datum?.fields || datum.fields.length < 5) { - return null; - } - return { - key: datum.fields[0].bytes, - next: datum.fields[1].bytes, - transferScriptHash: datum.fields[2].bytes, - thirdPartyScriptHash: datum.fields[3].bytes, - metadata: datum.fields[4].bytes, - }; -} \ No newline at end of file