diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 44d913a..555555f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,9 +1,9 @@
name: CI
on:
push:
- branches: [main]
+ branches: [main, develop]
pull_request:
- branches: [main]
+ branches: [main, develop]
jobs:
test:
@@ -19,11 +19,15 @@ jobs:
- uses: actions/checkout@v4
- uses: graalvm/setup-graalvm@v1
with:
- java-version: '25'
+ java-version: '25.0.3'
distribution: 'graalvm'
- uses: oven-sh/setup-bun@v2
- - name: Install pytest
- run: python3 -m pip install --break-system-packages pytest
+ - uses: actions/setup-go@v5
+ with:
+ go-version: 'stable'
+ - uses: dtolnay/rust-toolchain@stable
+ - name: Install Python test deps
+ run: python3 -m pip install --break-system-packages pytest pyyaml
- name: Build native library
run: ./gradlew :core:nativeCompile
- name: Run JVM tests
@@ -38,3 +42,22 @@ jobs:
run: ./gradlew :wrappers:rust:test
- name: Run JS tests
run: ./gradlew :wrappers:js:test
+
+ # Windows: verify the native library (libccl.dll) builds and core logic passes.
+ # Wrapper test coverage on Windows (Go cgo, C Makefile, etc.) is tracked separately
+ # in TODO.md.
+ windows:
+ runs-on: windows-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - uses: actions/checkout@v4
+ - uses: graalvm/setup-graalvm@v1
+ with:
+ java-version: '25.0.3'
+ distribution: 'graalvm'
+ - name: Build native library (libccl.dll)
+ run: ./gradlew :core:nativeCompile
+ - name: Run JVM tests
+ run: ./gradlew :core:test
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
new file mode 100644
index 0000000..bc710bb
--- /dev/null
+++ b/.github/workflows/integration-tests.yml
@@ -0,0 +1,76 @@
+name: Integration Tests (DevKit)
+
+# Runs the wrappers' integration tests against a local Yaci DevKit Cardano devnet.
+# These exercise real build -> sign -> submit round trips. The devnet's admin API is
+# exposed on :10000. Heavier than the unit CI, so it runs on PRs to main/develop and on demand.
+on:
+ pull_request:
+ branches: [main, develop]
+ workflow_dispatch:
+
+jobs:
+ integration-tests:
+ name: Integration Tests (Cardano Dev-Net)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: graalvm/setup-graalvm@v1
+ with:
+ java-version: '25.0.3'
+ distribution: 'graalvm'
+ - uses: oven-sh/setup-bun@v2
+ - uses: actions/setup-go@v5
+ with:
+ go-version: 'stable'
+ - uses: dtolnay/rust-toolchain@stable
+ # Node.js is needed for the npm-based yaci-devkit CLI.
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ - name: Install Python test deps
+ run: python3 -m pip install --break-system-packages pytest pyyaml
+
+ - name: Install Yaci DevKit
+ run: npm install -g @bloxbean/yaci-devkit
+
+ # Start the devnet in the background; it warms up while the native lib builds.
+ - name: Start Yaci DevKit
+ run: nohup yaci-devkit up --enable-yaci-store > devkit.log 2>&1 &
+
+ - name: Build native library
+ run: ./gradlew :core:nativeCompile
+
+ - name: Wait for DevKit admin API (:10000)
+ run: |
+ echo "Waiting for Yaci DevKit admin API..."
+ for i in $(seq 1 60); do
+ if curl -sf http://localhost:10000/local-cluster/api/admin/devnet >/dev/null 2>&1; then
+ echo "DevKit is ready (attempt $i)."
+ exit 0
+ fi
+ echo "Attempt $i/60 - not ready yet, waiting 5s..."
+ sleep 5
+ done
+ echo "DevKit did not become ready in time." >&2
+ exit 1
+
+ - name: Python integration tests
+ run: ./gradlew :wrappers:python:integrationTest
+
+ # The Go and Rust test tasks already include their integration tests, which
+ # skip themselves when DevKit is down and run when it is up.
+ - name: Go integration tests
+ run: ./gradlew :wrappers:go:test
+
+ - name: Rust integration tests
+ run: ./gradlew :wrappers:rust:test
+
+ - name: JS integration tests
+ run: ./gradlew :wrappers:js:integrationTest
+
+ - name: Upload DevKit log on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: devkit-log
+ path: devkit.log
diff --git a/.github/workflows/portable-linux-lib.yml b/.github/workflows/portable-linux-lib.yml
new file mode 100644
index 0000000..34660d5
--- /dev/null
+++ b/.github/workflows/portable-linux-lib.yml
@@ -0,0 +1,116 @@
+name: Portable Linux lib (glibc baseline)
+
+# Guards the distro-portability of the shipped libccl.so. native-image can't emit a static library
+# (oracle/graal#3053), so portability is achieved by building the .so against an OLD glibc baseline:
+# inside manylinux_2_28 the resulting lib references no symbol newer than GLIBC_2.17, so it runs on
+# RHEL/CentOS 7+, Amazon Linux 2, Ubuntu 18.04+, Debian 9+ and every newer distro. See
+# docs/adr/0008-linux-glibc-baseline-portability.md. This builds it, asserts the glibc floor, proves it
+# actually runs on glibc 2.17 (centos:7) — catching any regression that pulls in a newer symbol.
+
+on:
+ pull_request:
+ push:
+ branches: [develop, main]
+ workflow_dispatch:
+
+jobs:
+ glibc-baseline:
+ name: Build libccl.so on glibc 2.28 baseline (manylinux_2_28)
+ runs-on: ubuntu-latest
+ container:
+ image: quay.io/pypa/manylinux_2_28_x86_64
+ env:
+ GRAALVM_URL: https://download.oracle.com/graalvm/25/archive/graalvm-jdk-25.0.3_linux-x64_bin.tar.gz
+ # The guarantee: the .so must require no glibc symbol newer than the builder's glibc (2.28).
+ # (In practice native-image tops out at 2.17 — the centos:7 job below enforces that strictly.)
+ BASELINE: "2.28"
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Show build-container glibc
+ run: ldd --version | head -1
+
+ - name: Ensure build prerequisites (gcc toolset, zlib headers)
+ run: |
+ set -eux
+ gcc --version | head -1 || dnf install -y gcc
+ dnf install -y zlib-devel || true
+
+ - name: Install Oracle GraalVM 25.0.3
+ run: |
+ set -eux
+ mkdir -p /opt/graalvm
+ curl -fsSL -o /tmp/graalvm.tar.gz "$GRAALVM_URL"
+ tar xzf /tmp/graalvm.tar.gz -C /opt/graalvm --strip-components=1
+ echo "JAVA_HOME=/opt/graalvm" >> "$GITHUB_ENV"
+ echo "/opt/graalvm/bin" >> "$GITHUB_PATH"
+
+ - name: Toolchain versions
+ run: |
+ set -eux
+ "$JAVA_HOME/bin/java" -version
+ "$JAVA_HOME/bin/native-image" --version
+
+ - name: Build native library
+ run: ./gradlew :core:nativeCompile --no-daemon --stacktrace
+
+ - name: Assert glibc floor (<= 2.28)
+ run: |
+ set -eux
+ SO=core/build/native/nativeCompile/libccl.so
+ test -f "$SO"
+ objdump -T "$SO" | grep -oE 'GLIBC_[0-9]+\.[0-9]+(\.[0-9]+)?' | sort -uV > /tmp/syms.txt
+ echo "---- GLIBC symbol versions required ----"; cat /tmp/syms.txt
+ MAX=$(sed 's/GLIBC_//' /tmp/syms.txt | sort -V | tail -1)
+ echo "---- Max required: GLIBC_$MAX (guarantee: <= $BASELINE) ----"
+ HIGHER=$(printf '%s\n%s\n' "$MAX" "$BASELINE" | sort -V | tail -1)
+ if [ "$HIGHER" != "$BASELINE" ]; then
+ echo "FAIL: libccl.so requires GLIBC_$MAX, newer than baseline GLIBC_$BASELINE"
+ exit 1
+ fi
+ echo "PASS: libccl.so runs on any system with glibc >= $MAX"
+
+ - name: Compile the smoke harness against libccl.so
+ run: |
+ set -eux
+ ND=core/build/native/nativeCompile
+ gcc native-test/src/smoke.c -I"$ND" -L"$ND" -lccl -Wl,-rpath,'$ORIGIN' -o "$ND/smoke"
+
+ - name: Stage runnable artifact (lib + harness together)
+ run: |
+ set -eux
+ mkdir -p dist
+ cp core/build/native/nativeCompile/libccl.so dist/
+ cp core/build/native/nativeCompile/smoke dist/
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: libccl-glibc-baseline
+ path: dist/
+
+ run-on-old-distro:
+ name: Run libccl.so on glibc 2.17 (centos:7)
+ needs: glibc-baseline
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: libccl-glibc-baseline
+ path: dist
+
+ # Run the prebuilt binary inside centos:7 (glibc 2.17) WITHOUT installing anything. This is a
+ # strict run-on-2.17 guard: if some change pushes the required glibc above 2.17, the loader
+ # here fails. We don't use a `container:` for the job because GitHub's node-based actions can't
+ # run inside glibc 2.17; `docker run` keeps the actions on the modern runner.
+ - name: Load + run on centos:7
+ run: |
+ set -eux
+ chmod +x dist/smoke
+ docker run --rm -v "$PWD/dist:/work" -w /work centos:7 bash -c '
+ set -eux
+ ldd --version | head -1
+ echo "---- libccl.so shared-lib deps on this host ----"
+ ldd ./libccl.so
+ echo "---- running harness ----"
+ ./smoke
+ '
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f9b34da..3df8f9d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,37 +4,86 @@ on:
tags: ['v*']
jobs:
- build:
+ # macOS + Windows: stable ABIs, no glibc-portability concern. Built on the native runner.
+ build-native:
strategy:
matrix:
include:
- os: macos-14
platform: macos-aarch64
ext: dylib
- - os: ubuntu-latest
- platform: linux-x86_64
- ext: so
+ - os: windows-latest
+ platform: windows-x86_64
+ ext: dll
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: graalvm/setup-graalvm@v1
with:
- java-version: '25'
+ java-version: '25.0.3'
distribution: 'graalvm'
- name: Build native library
run: ./gradlew :core:nativeCompile
- name: Package
+ shell: bash
run: |
cd core/build/native/nativeCompile
+ # On Windows native-image also emits an import library (libccl.lib) needed
+ # to link against the DLL; include it when present.
+ EXTRA=""
+ [ -f libccl.lib ] && EXTRA="libccl.lib"
tar czf ccl-bridge-${{ github.ref_name }}-${{ matrix.platform }}.tar.gz \
- libccl.${{ matrix.ext }} libccl.h graal_isolate.h
+ libccl.${{ matrix.ext }} libccl.h graal_isolate.h $EXTRA
- uses: actions/upload-artifact@v4
with:
name: ccl-bridge-${{ matrix.platform }}
path: core/build/native/nativeCompile/ccl-bridge-*.tar.gz
+ # Linux: built in an OLD-glibc container (manylinux_2_28) so the shipped libccl.so runs on
+ # glibc >= 2.17 (RHEL/CentOS 7+, Amazon Linux 2, Ubuntu 18.04+, Debian 9+) instead of only the
+ # very newest distros. Portability is verified continuously by portable-linux-lib.yml; here we
+ # re-assert the floor before shipping. See docs/adr/0008-linux-glibc-baseline-portability.md.
+ build-linux:
+ name: build (linux-x86_64, glibc 2.17 baseline)
+ runs-on: ubuntu-latest
+ container:
+ image: quay.io/pypa/manylinux_2_28_x86_64
+ env:
+ GRAALVM_URL: https://download.oracle.com/graalvm/25/archive/graalvm-jdk-25.0.3_linux-x64_bin.tar.gz
+ steps:
+ - uses: actions/checkout@v4
+ - name: Ensure build prerequisites
+ run: dnf install -y zlib-devel || true
+ - name: Install Oracle GraalVM 25.0.3
+ run: |
+ set -eux
+ mkdir -p /opt/graalvm
+ curl -fsSL -o /tmp/graalvm.tar.gz "$GRAALVM_URL"
+ tar xzf /tmp/graalvm.tar.gz -C /opt/graalvm --strip-components=1
+ echo "JAVA_HOME=/opt/graalvm" >> "$GITHUB_ENV"
+ echo "/opt/graalvm/bin" >> "$GITHUB_PATH"
+ - name: Build native library
+ run: ./gradlew :core:nativeCompile --no-daemon --stacktrace
+ - name: Assert glibc floor (<= 2.28)
+ run: |
+ set -eux
+ SO=core/build/native/nativeCompile/libccl.so
+ MAX=$(objdump -T "$SO" | grep -oE 'GLIBC_[0-9]+\.[0-9]+(\.[0-9]+)?' | sed 's/GLIBC_//' | sort -V | tail -1)
+ echo "libccl.so max required: GLIBC_$MAX"
+ HIGHER=$(printf '%s\n2.28\n' "$MAX" | sort -V | tail -1)
+ [ "$HIGHER" = "2.28" ] || { echo "FAIL: requires GLIBC_$MAX > 2.28"; exit 1; }
+ - name: Package
+ run: |
+ cd core/build/native/nativeCompile
+ tar czf ccl-bridge-${{ github.ref_name }}-linux-x86_64.tar.gz \
+ libccl.so libccl.h graal_isolate.h
+ - uses: actions/upload-artifact@v4
+ with:
+ name: ccl-bridge-linux-x86_64
+ path: core/build/native/nativeCompile/ccl-bridge-*.tar.gz
+
release:
- needs: build
+ needs: [build-native, build-linux]
runs-on: ubuntu-latest
permissions:
contents: write
diff --git a/CLAUDE.md b/CLAUDE.md
index 5a2656c..68f2e29 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,4 +4,4 @@
- **Cardano Client Lib (CCL)**: https://github.com/bloxbean/cardano-client-lib
- **Local CCL source**: `/Users/satya/work/bloxbean/reference/cardano-client-lib`
-- **Target CCL version**: 0.7.1
+- **Target CCL version**: 0.8.0-pre4
diff --git a/README.md b/README.md
index b538d25..303880f 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ ccl-bridge/
**For core developers** (building from source):
- **[GraalVM 25+](https://www.graalvm.org/)** (includes `native-image`)
```bash
- sdk install java 25.0.2-graal # via SDKMAN
+ sdk install java 25.0.3-graal # Oracle GraalVM, via SDKMAN
```
**Language runtimes (install whichever you need):**
@@ -138,6 +138,11 @@ curl -L https://github.com/bloxbean/ccl-bridge/releases/latest/download/ccl-brid
curl -L https://github.com/bloxbean/ccl-bridge/releases/latest/download/ccl-bridge-v0.1.0-linux-x86_64.tar.gz | tar xz -C /usr/local/lib/
```
+> The Linux `libccl.so` is built against an old **glibc 2.17** baseline (in a `manylinux_2_28`
+> container), so it runs on any glibc ≥ 2.17 — RHEL/CentOS 7+, Amazon Linux 2, Ubuntu 18.04+,
+> Debian 9+, and all newer distros. (It does **not** run on musl-only systems such as Alpine; a
+> musl variant is a possible future addition.) See [ADR-0008](docs/adr/0008-linux-glibc-baseline-portability.md) for the why.
+
Then set the library path:
```bash
@@ -396,8 +401,8 @@ bridge.close();
## Upstream
-- **Cardano Client Lib**: [bloxbean/cardano-client-lib](https://github.com/bloxbean/cardano-client-lib) v0.7.1
-- **GraalVM**: 25.0.2 (`native-image --shared`)
+- **Cardano Client Lib**: [bloxbean/cardano-client-lib](https://github.com/bloxbean/cardano-client-lib) v0.8.0-pre4
+- **GraalVM**: Oracle GraalVM 25.0.3 (`native-image --shared`)
## License
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..efebb95
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,184 @@
+# CCL Bridge — TODO
+
+WISHLIST (Satya):
+- YAML support for TX building (TxPlan)
+- UTxO capture on the client side, callback maybe an issue (e.g. BloxBean) - UTxO selection
+- UTxO selection on the client
+- Protocol Parameters should be fetched via provider (cost calculation)
+- Script Supplier?
+
+A living, categorized backlog of work for CCL Bridge. CCL Bridge compiles
+[Cardano Client Lib](https://github.com/bloxbean/cardano-client-lib) into a GraalVM
+native shared library and exposes it to **Python, Go, Rust, and JavaScript (Bun)**.
+The project is functionally mature (v0.1.0-preview1) but had no roadmap — this file
+is the starting point. **It is meant to be extended**: add, re-prioritize, or check
+off items freely as the project evolves.
+
+**Priority legend:**
+- `P0` — blocks real-world adoption / advertised but missing
+- `P1` — important; needed for a solid 1.0
+- `P2` — nice-to-have / future polish
+
+**Supported languages today:** Python, Go, Rust, JavaScript (Bun only).
+C is test-only (smoke tests in `native-test/`); C headers ship for raw FFI consumers,
+but there is no standalone "C wrapper" product.
+
+> **Coverage note:** All four wrappers expose all 34 `@CEntryPoint` functions. Python
+> is currently the most complete and best-tested wrapper (the de-facto reference);
+> Go and Rust trail on test breadth; **JavaScript is the laggard** on QuickTx features.
+
+---
+
+## 1. Development — Wrapper Parity & Features
+
+- [x] `P0` ~~Audit & confirm JS QuickTx/ScriptTx/compose parity vs Python.~~ **Done (verified against source):** JS is feature-complete — `mintPlutusAssets`, `collectFromScript`, `readFrom`, the full `ScriptTxBuilder`, and `compose()`/`ComposeTxBuilder` all exist in `wrappers/js/src/index.js`. No feature gap. The real gap is test coverage — see §3.
+- [ ] `P1` Designate Python as the documented "reference wrapper" and write a parity checklist so all four wrappers stay in lockstep as the API grows.
+- [ ] `P2` Split the monolithic Go `wrappers/go/ccl/ccl.go` (~2k LOC) and Rust `wrappers/rust/src/lib.rs` into focused modules for maintainability.
+- [ ] `P2` Cross-wrapper error-handling review for consistent `CclError` semantics (codes, messages, idiomatic types).
+- [ ] `P2` Give the Go wrapper a clear build-time message when `CGO_ENABLED=0` (a `//go:build cgo` guard + a stub that explains cgo is required), instead of a cryptic linker error.
+- [x] `P2` ~~Expose **stake-key signing**~~ **Done** — added `ccl_account_sign_tx_multi(…, keys)`, which signs with any subset of `payment` / `stake` / `drep` / `committee_cold` / `committee_hot` (CCL's `Account.signWith*Key`), wired through all four wrappers (`sign_tx_with_keys` / `SignTxWithKeys` / `signTxWithKeys`). Fixes the `MissingVKeyWitnessesUTXOW` rejection for stake/vote/DRep certs; the original `ccl_account_sign_tx` (payment only) is unchanged.
+
+## 2. Development — Build, CI & Distribution
+
+- [x] `P0` ~~Fix the Go wrapper's thread affinity on Linux x86_64.~~ **Done** — all FFI calls now run on a single dedicated OS thread that owns the isolate for the `Bridge`'s lifetime (`runtime.LockOSThread` + a channel-served executor goroutine in `wrappers/go/ccl/ccl.go`). This keeps the executing OS thread and the GraalVM `IsolateThread` in sync, eliminating the Linux "yellow zone" `StackOverflowError`. Linux Go CI is blocking again and green.
+- [x] `P0` ~~Add a **Windows** native build (`libccl.dll`) to CI and the release pipeline.~~ **Done** — CI has a `windows-latest` job that builds `libccl.dll` (`:core:nativeCompile`) and runs the JVM tests; `release.yml` produces a `windows-x86_64` artifact (DLL + `libccl.lib` import library + headers). Verified green on CI.
+- [ ] `P1` Add **Windows wrapper test coverage** to CI (Python/Rust/JS/Go). The Windows job currently only builds the DLL + runs JVM tests; the wrapper test tasks assume a bash/`python3` shell and Unix `*_LIBRARY_PATH` semantics, and Go cgo + the C `native-test` Makefile need a Windows C toolchain. Each needs Windows-specific wiring.
+- [ ] `P0` Bundle or auto-fetch the native lib per wrapper (wheel platform tags / Rust `build.rs` / npm `postinstall`) so users no longer hand-set `CCL_LIB_PATH` / `DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH`.
+- [~] `P1` **Investigate static linking** — *decided; see [ADR-0008](docs/adr/0008-linux-glibc-baseline-portability.md).* **Finding:** `native-image` **cannot** emit a static library (`.a`) — oracle/graal#3053 is still open on GraalVM 25 — and musl's run-anywhere property applies only to static *executables*, not shared libraries. So a fully-static, no-`.so` distribution that keeps the in-process FFI is not possible without re-architecting to a static musl executable behind IPC (rejected as too invasive). **Decision + done: distro/glibc independence via a glibc-baseline build.** Building the Linux `.so` in `manylinux_2_28` yields a lib that requires only **`GLIBC_2.17`** — verified green in CI, and proven to load + run a real key-derivation on `centos:7` (glibc 2.17). Rolled out: `portable-linux-lib.yml` guards it on every PR/develop (objdump floor + centos:7 run), and `release.yml` ships the Linux artifact from the same container. Runs on RHEL/CentOS 7+, Amazon Linux 2, Ubuntu 18.04+, Debian 9+. **Not** Alpine/musl (possible future variant). _Follow-up `P2`: linux-arm64 baseline build; musl variant for Alpine._
+- [ ] `P1` Add **linux-arm64** and **macos-x86_64** to the build/release matrix (currently only `ubuntu-latest` x86_64 + `macos-14` ARM64).
+- [ ] `P1` Add **musl / Alpine Linux** native builds. The current glibc-linked `.so` fails on musl-based images (common in Go/Docker). Ship a musl variant (and document the glibc baseline for the standard build).
+- [ ] `P1` Publish wrappers to registries: PyPI (`ccl`), crates.io (`ccl`), npm (`@bloxbean/ccl`), and tag the Go module for the proxy.
+- [x] `P1` Pin CI to Oracle GraalVM `25.0.3` exactly (CI currently floats `java-version: '25'`) for reproducible builds.
+- [ ] `P2` Fill in wrapper manifest metadata (`[project.urls]`, `repository`, `homepage`, `documentation`) in `pyproject.toml` / `Cargo.toml` / `package.json` / `go.mod`.
+- [ ] `P2` Automate version bumping from a single source of truth (the version is duplicated across `gradle.properties` and each wrapper manifest).
+- [ ] `P2` **Runtime lib↔wrapper version check.** A native lib a version behind its wrapper fails confusingly; have each wrapper call `ccl_version` on init and error clearly on mismatch.
+- [ ] `P2` **Sign release artifacts** (cosign/sigstore) for supply-chain trust when pulling a prebuilt native lib. The release already emits `SHA256SUMS`; add signatures + verification docs.
+
+## 2b. Plutus script evaluation — pluggable evaluators
+
+The bridge builds Plutus script transactions offline by accepting the redeemers' **execution
+units** (mem + CPU steps) as a fourth caller-supplied input to `ccl_quicktx_build` — exactly like
+UTXOs and protocol parameters. Internally it wires CCL's `StaticTransactionEvaluator`, so the
+bridge never runs the script; the caller computes the units with whatever evaluator they prefer.
+This is shipped and tested (`QuickTxApiTest.plutusMint*`).
+
+- [ ] `P1` **Evaluator abstraction + examples (pick-and-choose).** Give users a clear, per-language
+ story for *obtaining* the exec units to pass in, with helper/service classes and runnable
+ examples for each supported evaluator:
+ - **HTTP / Blockfrost** `…/utils/txs/evaluate` (online)
+ - **Ogmios** `EvaluateTx` (online)
+ - **Aiken** UPLC evaluator (offline; e.g. `aiken-java-binding` server-side, or a wrapper-native
+ binding)
+ - **Scalus** UPLC evaluator (offline, JVM/Scala)
+ The bridge stays evaluator-agnostic (it only consumes `[{mem, steps}]`); these are thin,
+ swappable client-side helpers + docs showing the two-pass flow (build → evaluate → rebuild with
+ units). Cover Python, Go, Rust, JS.
+- [ ] `P2` **Self-contained offline evaluation spike — `aiken-java-binding` inside the GraalVM
+ native image.** If the Aiken Rust UPLC evaluator can be loaded via JNI from within `libccl`
+ (the blockers: the binding extracts its `.so` from the classpath jar at runtime — absent in a
+ native image — plus JNI config and per-platform Rust binaries), the bridge could run scripts
+ itself and callers would supply *nothing* extra. Prove feasibility before committing.
+
+## 2c. Chain-data provider helpers — make the API easy in all 4 languages
+
+`ccl_quicktx_build` is offline by design: the caller supplies **UTXOs**, **protocol parameters**,
+and (for Plutus) **execution units**. Today every wrapper is a pure pass-through — it marshals
+those three inputs and calls the native lib, but does **nothing** to obtain them. The user has to
+make their own HTTP calls to a backend first. That is the single biggest friction point for a
+first-time user, in every language.
+
+The fix keeps the **native lib provider-free** (offline stays offline) and adds the convenience
+*entirely in wrapper code*, using each language's own HTTP client — so the offline contract is
+untouched and the helpers are optional and swappable. This is the sibling of §2b: §2b obtains the
+*exec units*; this obtains the *UTXOs + protocol parameters*. Together they cover all three inputs.
+
+- [ ] `P1` **Optional per-wrapper chain-data provider helpers (UTXOs + protocol params).** A thin,
+ optional helper in Python/Go/Rust/JS that fetches the data `build()` needs and returns it in the
+ exact shape the wrapper already accepts, e.g.:
+ ```
+ provider = BlockfrostProvider(project_id) # or Koios / Ogmios / Yaci DevKit
+ utxos = provider.utxos(sender_addr) # all UTXOs at the address
+ pp = provider.protocol_params()
+ result = quicktx.build(yaml, utxos, pp) # unchanged offline core call
+ ```
+ Notes:
+ - **No UTXO *selection* needed** — the bridge already selects internally (it hands all sender
+ UTXOs to `QuickTxBuilder`/`StaticUtxoSupplier`). The helper only needs "UTXOs at address X".
+ - Define a small provider interface per language (`utxos(addr)`, `protocol_params()`), ship at
+ least one concrete impl (Blockfrost-style + Yaci DevKit, which the integration tests already
+ hit), and document a `buildWithProvider(yaml, provider, sender)` convenience that composes
+ fetch → build.
+ - Compose cleanly with §2b's exec-unit evaluators so a Plutus build is `fetch → evaluate → build`.
+- [ ] `P2` **Reconcile the WISHLIST vs Non-Goals tension.** Satya's wishlist wants provider-fetched
+ protocol params + client-side UTXO capture; Non-Goals excludes "HTTP provider modules". The
+ resolution is the split above: *optional wrapper-side helpers are in scope; baking a provider
+ into the native `libccl` is not.* The Non-Goals note now says this explicitly.
+
+## 3. Testing
+
+- [x] `P1` ~~Add JS integration tests for the script/Plutus paths.~~ **Done (and the item's premise was superseded by the TxPlan refactor):** the old fluent `ScriptTxBuilder` / `collectFromScript` / `mintPlutusAssets` / `readFrom` API was deleted — script/Plutus paths are now TxPlan YAML fixtures, covered at the build level in `test/intents.e2e.test.js`: all 20 top-level intents (incl. `reference_input`, `compose`, `native_script`) plus the three `plutus/` fixtures — **mint**, **spend**, and **lock** — each asserting non-empty CBOR + 64-char hash + positive fee, that mint/spend **require** caller-supplied exec units (build throws without them), and that `plutus.dataHash` reproduces the lock fixture's datum hash. Node-level (DevKit): a Plutus-mint **build → sign → submit → assert the minted asset landed on-chain** round-trip in `test/quicktx.integration.test.js`, mirroring Go's `TestIntegrationPlutusMint`.
+- [ ] `P1` **Fix JS cost-model key ordering for Plutus builds.** Surfaced while adding the JS Plutus-mint submit test: passing the cost models fetched from a Blockfrost-style provider (`/epochs/parameters` returns them as a map keyed by zero-padded indices `"000".."165"`) into a Plutus `build()` yields a tx the node rejects with `PPViewHashesDontMatch` — JS's JSON parse reorders the non-padded integer-like keys (`"100".."165"`) ahead of the padded ones, scrambling the cost-model order vs the ledger's canonical order and corrupting the script-integrity hash. (Go's `json.Marshal` sorts keys lexicographically, which for zero-padded keys equals numeric order, so the Go path is unaffected.) The integration test works around it by dropping the fetched cost models so the lib uses its built-in standard set; the real fix is to preserve canonical cost-model order in the JS wrapper before handing params to the native lib. Likely affects the §2c provider helpers, which will fetch and pass these params.
+- [ ] `P1` Raise Go and Rust test breadth toward Python's (~100 cases vs ~61); port Python's per-module unit tests.
+- [ ] `P1` Add a cross-wrapper parity test matrix asserting every `@CEntryPoint` is exercised in every language.
+- [ ] `P2` Run the Yaci DevKit integration tests in CI (containerized DevKit) instead of skip-if-not-running.
+- [ ] `P2` Expand the C smoke tests and add an FFI memory-leak / valgrind check across the native boundary.
+- [ ] `P2` Add benchmarks for FFI call overhead and JSON (de)serialization cost.
+
+## 4. User Documentation
+
+- [x] `P1` Per-wrapper `README.md` (install, load the lib, first call) for python / go / rust / js. **Done** — added `wrappers/{python,go,rust,js}/README.md`.
+- [x] `P1` Add per-wrapper `examples/` with runnable offline samples. **Done** — each wrapper has account / primitives / transaction examples (offline build+sign, no DevKit). All four verified running locally (Python, Go, Rust, JS/Bun). _Follow-up: richer samples (NFT mint, staking, governance)._
+- [ ] `P2` Generated API reference per language (Sphinx / rustdoc / godoc / JSDoc or TypeDoc).
+- [ ] `P2` Add project-meta docs: `CONTRIBUTING.md`, `CHANGELOG.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, and GitHub issue/PR templates.
+- [ ] `P2` Expand the 7-line `devkit.md` into a proper Yaci DevKit integration-testing guide.
+- [ ] `P2` Add an **end-to-end "build → sign → submit" example** per language. The bridge is offline-only, so users get stuck at broadcasting; show submitting the signed CBOR with the language's own HTTP client (e.g. Go `net/http`).
+- [ ] `P2` Add CI status + DevKit-integration badges to the README so the working round trips are visible at a glance.
+
+## 5. Website
+
+- [ ] `P1` Stand up a **GitHub Pages documentation site** (MkDocs Material or Docusaurus) hosting the README content, per-language guides, and `docs/quicktx.md`.
+- [ ] `P2` Auto-deploy the site from CI on release and wire in the generated per-language API references.
+
+## 6. Upstream CCL — New Modules to Evaluate
+
+Surfaced by scanning upstream CCL. The bridge now targets **0.8.0-pre4**, so all of these are
+available as a current dependency — no further upgrade needed.
+
+### CIP modules (already a bridge dependency)
+
+- [ ] `P2` **CIP-30 data signing** — wrap `DataSignature` / `CIP30DataSigner` (COSE_Sign1 `signData` create + verify). Offline. Complements existing CIP-8 message signing with the wallet/dApp data-signature format.
+- [ ] `P2` **CIP-27 royalty metadata** — wrap royalty metadata construction/parsing for NFTs. Offline; complements the bridge's existing CIP-25 support.
+
+### Now available on CCL 0.8.0-pre4
+
+- [x] `P1` ~~**Upgrade CCL 0.7.2 → 0.8.0**~~ **Done** — the bridge is on `0.8.0-pre4` (the TxPlan refactor). The QuickTx wrapper was rewritten to TxPlan YAML; the 0.8.0 unified `Tx`/`ScriptTx` + `DepositMode` are exercised by the intent E2E suite. Re-pin to the stable `0.8.0` when it releases.
+- [ ] `P2` **`plutus-aiken` blueprint handling** — expose runtime CIP-57 blueprint parsing and apply-params-to-script (parameterized validators). Offline. (The compile-time `@MetadataType` annotation processor is build-time Java codegen and is **not** FFI-able, so it is out of scope for the wrappers.)
+- [ ] `P2` **`txflow` multi-step orchestration** — evaluate exposing the offline flow-composition parts. Caveat: confirmation tracking is online/stateful and fits the bridge's stateless-FFI model awkwardly; wrap only the pure-composition surface, if any.
+- [ ] `P2` **CIP-102 royalty datum (v2)** — inline royalty datum on UTXOs; extends CIP-27. Offline datum (de)serialization.
+- [ ] `P2` **`crypto-ext` VRF/KES** — niche (block-producer / consensus simulation, experimental). Offline. Only if devnet simulation becomes a goal.
+
+## 7. Maintenance — Existing Wrappers (audit, likely already covered)
+
+- [ ] `P2` Audit governance key derivation parity (`DRepKey`, `CommitteeColdKey`, `CommitteeHotKey`, gov-action IDs) — the bridge already exposes these; confirm nothing new in CCL is missing.
+- [ ] `P2` Audit QuickTx deposit handling against CCL's `DepositMode` (AUTO / CHANGE_OUTPUT / ANY_OUTPUT / NEW_UTXO_SELECTION) when on 0.8.0.
+
+---
+
+## Non-Goals (intentional, for now)
+
+- **Verified data structures** (`verified-structures`: Merkle Patricia Forestry,
+ Jellyfish Merkle Tree, RocksDB/RDBMS backends) — out of scope. They require
+ persistent, stateful storage backends, which is incompatible with the bridge's
+ stateless, side-effect-free FFI model. The pure-compute proof core could be
+ reconsidered only if there is explicit demand for Merkle-proof APIs.
+
+- **Node.js support** — *wanted but blocked.* Node FFI libraries (ffi-napi, koffi) crash
+ with the GraalVM native-image library due to stack-boundary detection issues on macOS
+ ARM64. Bun (built-in FFI) is the supported JS runtime. Tracked as a `P2` investigation
+ item, not a committed deliverable.
+- **Backend / HTTP provider modules *in the native `libccl`*** (Blockfrost, Koios, Ogmios) —
+ deliberately excluded; the native lib stays offline and side-effect-free. **This does not
+ exclude optional, wrapper-side provider helpers** that fetch UTXOs / protocol params / exec
+ units using each language's own HTTP client and feed them into the offline `build()` — those
+ are explicitly in scope and tracked in §2b (exec units) and §2c (UTXOs + protocol params).
+ The line is: convenience in wrapper code = yes; a provider baked into `libccl` = no.
diff --git a/core/build.gradle b/core/build.gradle
index c9bc807..1a5280a 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -5,7 +5,7 @@ plugins {
base.archivesName = 'ccl-bridge-core'
-def cclVersion = '0.7.1'
+def cclVersion = '0.8.0-pre4'
dependencies {
// CCL core modules (offline-only, no backend HTTP)
@@ -23,7 +23,7 @@ dependencies {
implementation "com.bloxbean.cardano:cardano-client-cip68:${cclVersion}"
// GraalVM SDK for @CEntryPoint, CCharPointer, etc.
- compileOnly "org.graalvm.sdk:nativeimage:25.0.0"
+ compileOnly "org.graalvm.sdk:nativeimage:25.0.3"
// SLF4J NOP - avoids log framework init issues in native image
implementation 'org.slf4j:slf4j-nop:2.0.11'
@@ -47,6 +47,9 @@ graalvmNative {
'--initialize-at-build-time=net.i2p.crypto.eddsa',
'--initialize-at-build-time=com.fasterxml.jackson',
'--initialize-at-build-time=co.nstant.in.cbor',
+ // snakeyaml is pulled in by CCL's TxPlan YAML support; its option enums
+ // must be initialized at build time for native-image.
+ '--initialize-at-build-time=org.yaml.snakeyaml',
'--enable-url-protocols=http,https',
)
}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/CclBridge.java b/core/src/main/java/com/bloxbean/cardano/bridge/CclBridge.java
index 6499df0..f47ae09 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/CclBridge.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/CclBridge.java
@@ -8,18 +8,87 @@
import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.type.CCharPointer;
+/**
+ * Core lifecycle and result-retrieval entry points of the CCL Bridge native library.
+ *
+ *
What this library is
+ * The CCL Bridge compiles Cardano
+ * Client Lib into a native shared library ({@code libccl.so} / {@code .dylib} / {@code .dll})
+ * with GraalVM {@code native-image}, exposing CCL's offline Cardano operations through a
+ * flat C ABI. Every public function in this package is a GraalVM {@link CEntryPoint}, i.e. an
+ * exported C symbol callable from any language with an FFI (Python, Go, Rust, JavaScript, C).
+ *
+ * Calling convention
+ * All entry points follow the same contract, because only C-compatible primitives can cross the
+ * boundary (no Java objects):
+ *
+ * First parameter is always an {@link IsolateThread} — the handle returned by
+ * {@code graal_create_isolate} (or {@code graal_attach_thread}). It identifies the GraalVM
+ * isolate (managed heap) and the thread the call runs on.
+ * Inputs are C primitives ({@code int}) and null-terminated UTF-8 C strings
+ * ({@link CCharPointer}); structured input is passed as a JSON string.
+ * Return value is an {@code int} status code from {@link ErrorCodes}
+ * ({@code 0} = success, negative = error).
+ * The actual result (when there is one) is not returned directly. It is
+ * stored in thread-local state and retrieved with a follow-up call to
+ * {@link #getResult(IsolateThread) ccl_get_result}. Results are JSON strings (or a bare
+ * value such as a hex string, depending on the function).
+ * On error , a human-readable message is stored thread-local and retrieved with
+ * {@link #getLastError(IsolateThread) ccl_get_last_error}.
+ * Returned strings are allocated in unmanaged (malloc'd) memory and must be
+ * released by the caller with {@link #freeString(IsolateThread, CCharPointer) ccl_free_string}.
+ *
+ *
+ * Typical sequence (per logical operation)
+ * {@code
+ * int rc = ccl_account_create(thread, networkId); // 1. do the work; returns status
+ * if (rc == 0) {
+ * char* json = ccl_get_result(thread); // 2. fetch JSON result (thread-local)
+ * // ... use json ...
+ * ccl_free_string(thread, json); // 3. release the malloc'd string
+ * } else {
+ * char* err = ccl_get_last_error(thread); // or fetch the error message
+ * ccl_free_string(thread, err);
+ * }
+ * }
+ *
+ * Because the result/error are thread-local, the work call and its {@code ccl_get_result} /
+ * {@code ccl_get_last_error} retrieval must run on the same isolate thread.
+ *
+ * @see ErrorCodes status codes returned by every entry point
+ */
public final class CclBridge {
private static final String VERSION = "0.1.0";
private CclBridge() {}
+ /**
+ * Returns the CCL Bridge library version.
+ *
+ *
Exported as {@code ccl_version}. On success the version string (e.g. {@code "0.1.0"}) is
+ * placed in the thread-local result; retrieve it with
+ * {@link #getResult(IsolateThread) ccl_get_result}.
+ *
+ * @param thread the current isolate thread
+ * @return {@link ErrorCodes#CCL_SUCCESS}
+ */
@CEntryPoint(name = "ccl_version")
public static int version(IsolateThread thread) {
ResultState.set(VERSION);
return ErrorCodes.CCL_SUCCESS;
}
+ /**
+ * Returns the result string produced by the most recent successful call on this thread.
+ *
+ *
Exported as {@code ccl_get_result}. The returned pointer is malloc'd and owned by the
+ * caller, who must release it with {@link #freeString(IsolateThread, CCharPointer) ccl_free_string}.
+ * If no result is set, an empty string is returned (never {@code NULL}).
+ *
+ * @param thread the current isolate thread
+ * @return a newly allocated, null-terminated UTF-8 C string holding the result (often JSON)
+ */
@CEntryPoint(name = "ccl_get_result")
public static CCharPointer getResult(IsolateThread thread) {
String result = ResultState.get();
@@ -29,6 +98,17 @@ public static CCharPointer getResult(IsolateThread thread) {
return NativeString.toCString(result);
}
+ /**
+ * Returns the error message produced by the most recent failed call on this thread.
+ *
+ *
Exported as {@code ccl_get_last_error}. Call this after an entry point returns a negative
+ * {@link ErrorCodes status code}. The returned pointer is malloc'd and owned by the caller, who
+ * must release it with {@link #freeString(IsolateThread, CCharPointer) ccl_free_string}. If no
+ * error is set, an empty string is returned (never {@code NULL}).
+ *
+ * @param thread the current isolate thread
+ * @return a newly allocated, null-terminated UTF-8 C string holding the error message
+ */
@CEntryPoint(name = "ccl_get_last_error")
public static CCharPointer getLastError(IsolateThread thread) {
String error = ErrorState.get();
@@ -38,6 +118,17 @@ public static CCharPointer getLastError(IsolateThread thread) {
return NativeString.toCString(error);
}
+ /**
+ * Frees a string previously returned by this library.
+ *
+ *
Exported as {@code ccl_free_string}. Every non-{@code NULL} pointer returned by
+ * {@code ccl_get_result} / {@code ccl_get_last_error} is allocated in unmanaged memory and
+ * must be passed here exactly once to avoid a memory leak. Passing {@code NULL} is a
+ * safe no-op.
+ *
+ * @param thread the current isolate thread
+ * @param ptr the string pointer to release (may be {@code NULL})
+ */
@CEntryPoint(name = "ccl_free_string")
public static void freeString(IsolateThread thread, CCharPointer ptr) {
if (ptr.isNonNull()) {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/ErrorCodes.java b/core/src/main/java/com/bloxbean/cardano/bridge/ErrorCodes.java
index a309b76..fbd20cf 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/ErrorCodes.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/ErrorCodes.java
@@ -1,16 +1,36 @@
package com.bloxbean.cardano.bridge;
+/**
+ * Status codes returned by every CCL Bridge entry point.
+ *
+ *
{@link #CCL_SUCCESS} ({@code 0}) indicates success; all error codes are negative. On a
+ * negative return, a human-readable message is available via {@code ccl_get_last_error}. The
+ * specific code is a coarse category — the message carries the detail.
+ *
+ * @see CclBridge#getLastError calling convention and result/error retrieval
+ */
public final class ErrorCodes {
+ /** Operation succeeded; a result (if any) is available via {@code ccl_get_result}. */
public static final int CCL_SUCCESS = 0;
+ /** Unspecified failure not covered by a more specific code. */
public static final int CCL_ERROR_GENERAL = -1;
+ /** A required argument was missing, malformed, or out of range. */
public static final int CCL_ERROR_INVALID_ARGUMENT = -2;
+ /** CBOR/JSON (de)serialization failed. */
public static final int CCL_ERROR_SERIALIZATION = -3;
+ /** A cryptographic operation failed (hashing, signing, verification, key derivation). */
public static final int CCL_ERROR_CRYPTO = -4;
+ /** The supplied network id is not one of mainnet/testnet/preprod/preview. */
public static final int CCL_ERROR_INVALID_NETWORK = -5;
+ /** The supplied mnemonic phrase is invalid. */
public static final int CCL_ERROR_INVALID_MNEMONIC = -6;
+ /** The supplied address is invalid or could not be parsed. */
public static final int CCL_ERROR_INVALID_ADDRESS = -7;
+ /** Inputs do not cover the transaction's outputs, fees, and deposits. */
public static final int CCL_ERROR_INSUFFICIENT_FUNDS = -8;
+ /** A transaction could not be parsed, signed, or otherwise processed. */
public static final int CCL_ERROR_INVALID_TRANSACTION = -9;
+ /** Building a transaction from a QuickTx spec failed. */
public static final int CCL_ERROR_TX_BUILD = -10;
private ErrorCodes() {}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/AccountApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/AccountApi.java
index f0961f8..e8b8321 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/AccountApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/AccountApi.java
@@ -13,10 +13,33 @@
import java.util.LinkedHashMap;
import java.util.Map;
+/**
+ * Account entry points: deterministic (HD) account creation, key derivation, and signing.
+ *
+ *
An "account" is a CIP-1852 HD account derived from a BIP-39 mnemonic. Methods that take a
+ * {@code networkId} use {@code 0}=mainnet, {@code 1}=testnet, {@code 2}=preprod, {@code 3}=preview.
+ * The {@code accountIndex}/{@code addressIndex} select the HD derivation path
+ * ({@code m/1852'/1815'/account'/role/address}).
+ *
+ *
See {@link com.bloxbean.cardano.bridge.CclBridge} for the calling convention (status code +
+ * thread-local result retrieved via {@code ccl_get_result}). Every entry point here is a static
+ * GraalVM {@code @CEntryPoint}.
+ */
public final class AccountApi {
private AccountApi() {}
+ /**
+ * Creates a brand-new account with a freshly generated 24-word mnemonic.
+ *
+ *
Exported as {@code ccl_account_create}. On success the result is a JSON object:
+ *
{@code {"mnemonic","base_address","enterprise_address","stake_address","change_address"}}
+ *
+ * @param thread the current isolate thread
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_account_create")
public static int create(IsolateThread thread, int networkId) {
try {
@@ -36,6 +59,20 @@ public static int create(IsolateThread thread, int networkId) {
}
}
+ /**
+ * Restores an account from an existing mnemonic at the given derivation indices.
+ *
+ * Exported as {@code ccl_account_from_mnemonic}. On success the result is the same JSON
+ * object as {@code ccl_account_create}.
+ *
+ * @param thread the current isolate thread
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param accountIndex HD account index (typically 0)
+ * @param addressIndex HD address index (typically 0)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_INVALID_MNEMONIC} / {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_account_from_mnemonic")
public static int fromMnemonic(IsolateThread thread, int networkId,
CCharPointer mnemonicPtr,
@@ -68,6 +105,21 @@ public static int fromMnemonic(IsolateThread thread, int networkId,
}
}
+ /**
+ * Derives the account's extended private key.
+ *
+ *
Exported as {@code ccl_account_get_private_key}. On success the result is the hex-encoded
+ * 64-byte extended BIP32-Ed25519 payment private key (128 hex chars). Note: a raw 32-byte
+ * Ed25519 key (e.g. for {@code ccl_crypto_sign}) is the first 32 bytes / 64 hex chars.
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param accountIndex HD account index
+ * @param addressIndex HD address index
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_CRYPTO}
+ */
@CEntryPoint(name = "ccl_account_get_private_key")
public static int getPrivateKey(IsolateThread thread, CCharPointer mnemonicPtr,
int networkId, int accountIndex, int addressIndex) {
@@ -94,6 +146,20 @@ public static int getPrivateKey(IsolateThread thread, CCharPointer mnemonicPtr,
}
}
+ /**
+ * Derives the account's public key.
+ *
+ *
Exported as {@code ccl_account_get_public_key}. On success the result is the hex-encoded
+ * 32-byte Ed25519 payment public key (64 hex chars).
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param accountIndex HD account index
+ * @param addressIndex HD address index
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_CRYPTO}
+ */
@CEntryPoint(name = "ccl_account_get_public_key")
public static int getPublicKey(IsolateThread thread, CCharPointer mnemonicPtr,
int networkId, int accountIndex, int addressIndex) {
@@ -120,6 +186,23 @@ public static int getPublicKey(IsolateThread thread, CCharPointer mnemonicPtr,
}
}
+ /**
+ * Signs a transaction with the account's payment key.
+ *
+ *
Exported as {@code ccl_account_sign_tx}. On success the result is the signed transaction as
+ * CBOR hex. Only the payment key is added; transactions whose certificates must be authorized by
+ * the stake key (e.g. vote-power / stake delegation) also need the stake key, which this entry
+ * point does not yet add.
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param accountIndex HD account index
+ * @param addressIndex HD address index
+ * @param txCborHexPtr the unsigned (or partially signed) transaction as CBOR hex
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_INVALID_TRANSACTION}
+ */
@CEntryPoint(name = "ccl_account_sign_tx")
public static int signTx(IsolateThread thread, CCharPointer mnemonicPtr,
int networkId, int accountIndex, int addressIndex,
@@ -154,6 +237,87 @@ public static int signTx(IsolateThread thread, CCharPointer mnemonicPtr,
}
}
+ /**
+ * Signs a transaction with one or more of the account's keys, selected by role.
+ *
+ *
Exported as {@code ccl_account_sign_tx_multi}. {@code keys} is a comma-separated list of
+ * roles to add witnesses for, applied in order: {@code payment}, {@code stake}, {@code drep},
+ * {@code committee_cold}, {@code committee_hot} (empty/null defaults to {@code payment}). Use
+ * this when a transaction's certificates need more than the payment key — stake
+ * registration/deregistration/delegation, reward withdrawal, and DRep/vote operations must also
+ * be witnessed by the stake or DRep key, or the node rejects them with
+ * {@code MissingVKeyWitnessesUTXOW}.
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param accountIndex HD account index
+ * @param addressIndex HD address index
+ * @param txCborHexPtr the unsigned (or partially signed) transaction as CBOR hex
+ * @param keysPtr comma-separated signing roles (UTF-8 C string), e.g. {@code "payment,stake"}
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or an error code
+ */
+ @CEntryPoint(name = "ccl_account_sign_tx_multi")
+ public static int signTxMulti(IsolateThread thread, CCharPointer mnemonicPtr,
+ int networkId, int accountIndex, int addressIndex,
+ CCharPointer txCborHexPtr, CCharPointer keysPtr) {
+ try {
+ Network network = NetworkMapper.toNetwork(networkId);
+ if (network == null) {
+ ErrorState.set("Invalid network id: " + networkId);
+ return ErrorCodes.CCL_ERROR_INVALID_NETWORK;
+ }
+ String mnemonic = NativeString.toJavaString(mnemonicPtr);
+ if (mnemonic == null || mnemonic.isEmpty()) {
+ ErrorState.set("Mnemonic is required");
+ return ErrorCodes.CCL_ERROR_INVALID_ARGUMENT;
+ }
+ String txCborHex = NativeString.toJavaString(txCborHexPtr);
+ if (txCborHex == null || txCborHex.isEmpty()) {
+ ErrorState.set("Transaction CBOR hex is required");
+ return ErrorCodes.CCL_ERROR_INVALID_ARGUMENT;
+ }
+ String keys = NativeString.toJavaString(keysPtr);
+ if (keys == null || keys.isBlank()) {
+ keys = "payment";
+ }
+
+ Account account = Account.createFromMnemonic(network, mnemonic, accountIndex, addressIndex);
+ Transaction tx = Transaction.deserialize(HexUtil.decodeHexString(txCborHex));
+ for (String role : keys.split(",")) {
+ switch (role.trim().toLowerCase()) {
+ case "payment": tx = account.sign(tx); break;
+ case "stake": tx = account.signWithStakeKey(tx); break;
+ case "drep": tx = account.signWithDRepKey(tx); break;
+ case "committee_cold": tx = account.signWithCommitteeColdKey(tx); break;
+ case "committee_hot": tx = account.signWithCommitteeHotKey(tx); break;
+ case "": break;
+ default:
+ ErrorState.set("Unknown signing key role: " + role);
+ return ErrorCodes.CCL_ERROR_INVALID_ARGUMENT;
+ }
+ }
+ ResultState.set(tx.serializeToHex());
+ return ErrorCodes.CCL_SUCCESS;
+ } catch (Exception e) {
+ ErrorState.set(e.getMessage());
+ return ErrorCodes.CCL_ERROR_INVALID_TRANSACTION;
+ }
+ }
+
+ /**
+ * Derives the account's governance DRep ID.
+ *
+ *
Exported as {@code ccl_account_get_drep_id}. On success the result is the bech32 DRep ID
+ * (e.g. {@code drep1...}). Uses address index 0.
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param accountIndex HD account index
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_account_get_drep_id")
public static int getDrepId(IsolateThread thread, CCharPointer mnemonicPtr,
int networkId, int accountIndex) {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/AddressApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/AddressApi.java
index 46c8b2d..c46b211 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/AddressApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/AddressApi.java
@@ -13,10 +13,29 @@
import java.util.LinkedHashMap;
import java.util.Map;
+/**
+ * Address entry points: parse, validate, and convert Cardano addresses.
+ *
+ *
Operates on bech32 addresses ({@code addr1...}, {@code addr_test1...}, {@code stake1...}) and
+ * their raw byte (hex) form. See {@link com.bloxbean.cardano.bridge.CclBridge} for the calling
+ * convention. Every entry point here is a static GraalVM {@code @CEntryPoint}.
+ */
public final class AddressApi {
private AddressApi() {}
+ /**
+ * Parses an address into its components.
+ *
+ *
Exported as {@code ccl_address_info}. On success the result is a JSON object:
+ *
{@code {"type","network_id","payment_credential_hash"?,"delegation_credential_hash"?,
+ * "is_pubkey_payment","is_script_payment"}}
+ * The credential-hash fields are present only when applicable to the address type.
+ *
+ * @param thread the current isolate thread
+ * @param bech32Ptr the bech32 address (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_ADDRESS}
+ */
@CEntryPoint(name = "ccl_address_info")
public static int info(IsolateThread thread, CCharPointer bech32Ptr) {
try {
@@ -54,6 +73,16 @@ public static int info(IsolateThread thread, CCharPointer bech32Ptr) {
}
}
+ /**
+ * Converts a bech32 address to its raw bytes.
+ *
+ * Exported as {@code ccl_address_to_bytes}. On success the result is the hex-encoded address
+ * bytes.
+ *
+ * @param thread the current isolate thread
+ * @param bech32Ptr the bech32 address (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_ADDRESS}
+ */
@CEntryPoint(name = "ccl_address_to_bytes")
public static int toBytes(IsolateThread thread, CCharPointer bech32Ptr) {
try {
@@ -72,6 +101,15 @@ public static int toBytes(IsolateThread thread, CCharPointer bech32Ptr) {
}
}
+ /**
+ * Converts raw address bytes back to a bech32 address.
+ *
+ *
Exported as {@code ccl_address_from_bytes}. On success the result is the bech32 address.
+ *
+ * @param thread the current isolate thread
+ * @param hexBytesPtr the hex-encoded address bytes (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_ADDRESS}
+ */
@CEntryPoint(name = "ccl_address_from_bytes")
public static int fromBytes(IsolateThread thread, CCharPointer hexBytesPtr) {
try {
@@ -91,6 +129,17 @@ public static int fromBytes(IsolateThread thread, CCharPointer hexBytesPtr) {
}
}
+ /**
+ * Validates an address by attempting to parse it.
+ *
+ *
Exported as {@code ccl_address_validate}. This reports the result via the status code only
+ * (no result string): {@link ErrorCodes#CCL_SUCCESS} if valid,
+ * {@link ErrorCodes#CCL_ERROR_INVALID_ADDRESS} if not.
+ *
+ * @param thread the current isolate thread
+ * @param bech32Ptr the bech32 address to validate (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS} (valid) or {@link ErrorCodes#CCL_ERROR_INVALID_ADDRESS}
+ */
@CEntryPoint(name = "ccl_address_validate")
public static int validate(IsolateThread thread, CCharPointer bech32Ptr) {
try {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/CryptoApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/CryptoApi.java
index 70fcc3d..4338034 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/CryptoApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/CryptoApi.java
@@ -13,10 +13,27 @@
import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.type.CCharPointer;
+/**
+ * Cryptographic primitives: Blake2b hashing, BIP-39 mnemonics, and Ed25519 sign/verify.
+ *
+ *
Hashing and signing take/return hex-encoded bytes. See
+ * {@link com.bloxbean.cardano.bridge.CclBridge} for the calling convention. Every entry point here
+ * is a static GraalVM {@code @CEntryPoint}.
+ */
public final class CryptoApi {
private CryptoApi() {}
+ /**
+ * Computes a Blake2b-256 hash.
+ *
+ *
Exported as {@code ccl_crypto_blake2b_256}. Hex in, hex out; the result is a 32-byte digest
+ * (64 hex chars).
+ *
+ * @param thread the current isolate thread
+ * @param dataHexPtr the input bytes as hex (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_CRYPTO}
+ */
@CEntryPoint(name = "ccl_crypto_blake2b_256")
public static int blake2b256(IsolateThread thread, CCharPointer dataHexPtr) {
try {
@@ -36,6 +53,16 @@ public static int blake2b256(IsolateThread thread, CCharPointer dataHexPtr) {
}
}
+ /**
+ * Computes a Blake2b-224 hash (the size used for Cardano credential/key hashes).
+ *
+ *
Exported as {@code ccl_crypto_blake2b_224}. Hex in, hex out; the result is a 28-byte digest
+ * (56 hex chars).
+ *
+ * @param thread the current isolate thread
+ * @param dataHexPtr the input bytes as hex (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_CRYPTO}
+ */
@CEntryPoint(name = "ccl_crypto_blake2b_224")
public static int blake2b224(IsolateThread thread, CCharPointer dataHexPtr) {
try {
@@ -55,6 +82,17 @@ public static int blake2b224(IsolateThread thread, CCharPointer dataHexPtr) {
}
}
+ /**
+ * Generates a new BIP-39 mnemonic.
+ *
+ *
Exported as {@code ccl_crypto_generate_mnemonic}. On success the result is the
+ * space-separated mnemonic phrase.
+ *
+ * @param thread the current isolate thread
+ * @param wordCount number of words: 12, 15, 18, 21, or 24
+ * @return {@link ErrorCodes#CCL_SUCCESS}, {@link ErrorCodes#CCL_ERROR_INVALID_ARGUMENT}
+ * (bad word count), or {@link ErrorCodes#CCL_ERROR_CRYPTO}
+ */
@CEntryPoint(name = "ccl_crypto_generate_mnemonic")
public static int generateMnemonic(IsolateThread thread, int wordCount) {
try {
@@ -72,6 +110,7 @@ public static int generateMnemonic(IsolateThread thread, int wordCount) {
String mnemonic = MnemonicUtil.generateNew(words);
ResultState.set(mnemonic);
+
return ErrorCodes.CCL_SUCCESS;
} catch (Exception e) {
ErrorState.set(e.getMessage());
@@ -79,6 +118,16 @@ public static int generateMnemonic(IsolateThread thread, int wordCount) {
}
}
+ /**
+ * Validates a BIP-39 mnemonic (word list and checksum).
+ *
+ *
Exported as {@code ccl_crypto_validate_mnemonic}. Reported via the status code only (no
+ * result string).
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the mnemonic phrase to validate (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS} (valid) or {@link ErrorCodes#CCL_ERROR_INVALID_MNEMONIC}
+ */
@CEntryPoint(name = "ccl_crypto_validate_mnemonic")
public static int validateMnemonic(IsolateThread thread, CCharPointer mnemonicPtr) {
try {
@@ -96,6 +145,18 @@ public static int validateMnemonic(IsolateThread thread, CCharPointer mnemonicPt
}
}
+ /**
+ * Produces an Ed25519 signature.
+ *
+ *
Exported as {@code ccl_crypto_sign}. On success the result is the hex-encoded 64-byte
+ * signature. {@code skHex} must be a raw 32-byte Ed25519 secret key (64 hex chars) — note an
+ * account's extended private key is 64 bytes, so use its first 32 bytes here.
+ *
+ * @param thread the current isolate thread
+ * @param messageHexPtr the message bytes as hex (UTF-8 C string)
+ * @param skHexPtr the 32-byte Ed25519 secret key as hex (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_CRYPTO}
+ */
@CEntryPoint(name = "ccl_crypto_sign")
public static int sign(IsolateThread thread, CCharPointer messageHexPtr, CCharPointer skHexPtr) {
try {
@@ -122,6 +183,19 @@ public static int sign(IsolateThread thread, CCharPointer messageHexPtr, CCharPo
}
}
+ /**
+ * Verifies an Ed25519 signature.
+ *
+ *
Exported as {@code ccl_crypto_verify}. Reported via the status code only:
+ * {@link ErrorCodes#CCL_SUCCESS} if the signature is valid,
+ * {@link ErrorCodes#CCL_ERROR_CRYPTO} if it is not.
+ *
+ * @param thread the current isolate thread
+ * @param signatureHexPtr the 64-byte signature as hex (UTF-8 C string)
+ * @param messageHexPtr the message bytes as hex (UTF-8 C string)
+ * @param pkHexPtr the 32-byte Ed25519 public key as hex (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS} (valid) or {@link ErrorCodes#CCL_ERROR_CRYPTO}
+ */
@CEntryPoint(name = "ccl_crypto_verify")
public static int verify(IsolateThread thread, CCharPointer signatureHexPtr,
CCharPointer messageHexPtr, CCharPointer pkHexPtr) {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/GovernanceApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/GovernanceApi.java
index 09b025f..3b54df6 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/GovernanceApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/GovernanceApi.java
@@ -15,10 +15,32 @@
import java.util.LinkedHashMap;
import java.util.Map;
+/**
+ * Governance key-derivation entry points (Conway era): DRep and constitutional-committee keys.
+ *
+ *
All keys are derived from a mnemonic via CIP-1852/CIP-105 paths. {@code networkId} uses
+ * {@code 0}=mainnet, {@code 1}=testnet, {@code 2}=preprod, {@code 3}=preview; address index is 0.
+ * See {@link com.bloxbean.cardano.bridge.CclBridge} for the calling convention. Every entry point
+ * here is a static GraalVM {@code @CEntryPoint}.
+ */
public final class GovernanceApi {
private GovernanceApi() {}
+ /**
+ * Derives the DRep key pair from a mnemonic.
+ *
+ *
Exported as {@code ccl_gov_drep_key_from_mnemonic}. On success the result is a JSON object:
+ *
{@code {"drep_id","verification_key","verification_key_hash",
+ * "bech32_verification_key","bech32_verification_key_hash"}}
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param accountIndex HD account index
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_gov_drep_key_from_mnemonic")
public static int drepKeyFromMnemonic(IsolateThread thread, CCharPointer mnemonicPtr,
int networkId, int accountIndex) {
@@ -53,6 +75,21 @@ public static int drepKeyFromMnemonic(IsolateThread thread, CCharPointer mnemoni
}
}
+ /**
+ * Derives the constitutional-committee cold key pair from a mnemonic.
+ *
+ * Exported as {@code ccl_gov_committee_cold_key_from_mnemonic}. On success the result is a
+ * JSON object:
+ *
{@code {"id","verification_key","verification_key_hash",
+ * "bech32_verification_key","bech32_verification_key_hash"}}
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param accountIndex HD account index
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_gov_committee_cold_key_from_mnemonic")
public static int committeeColdKeyFromMnemonic(IsolateThread thread, CCharPointer mnemonicPtr,
int networkId, int accountIndex) {
@@ -87,6 +124,20 @@ public static int committeeColdKeyFromMnemonic(IsolateThread thread, CCharPointe
}
}
+ /**
+ * Derives the constitutional-committee hot key pair from a mnemonic.
+ *
+ * Exported as {@code ccl_gov_committee_hot_key_from_mnemonic}. On success the result is the
+ * same JSON shape as {@code ccl_gov_committee_cold_key_from_mnemonic}
+ * ({@code id}, {@code verification_key}, {@code verification_key_hash}, and the bech32 forms).
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param accountIndex HD account index
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_gov_committee_hot_key_from_mnemonic")
public static int committeeHotKeyFromMnemonic(IsolateThread thread, CCharPointer mnemonicPtr,
int networkId, int accountIndex) {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/PlutusApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/PlutusApi.java
index 344590c..7d247d0 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/PlutusApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/PlutusApi.java
@@ -10,10 +10,27 @@
import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.type.CCharPointer;
+/**
+ * Plutus data (datum/redeemer) entry points: hashing and CBOR↔JSON conversion.
+ *
+ *
The JSON form is CCL's PlutusData JSON schema (constructor/fields/bytes/int/list/map). See
+ * {@link com.bloxbean.cardano.bridge.CclBridge} for the calling convention. Every entry point here
+ * is a static GraalVM {@code @CEntryPoint}.
+ */
public final class PlutusApi {
private PlutusApi() {}
+ /**
+ * Computes the datum hash of a PlutusData value.
+ *
+ *
Exported as {@code ccl_plutus_data_hash}. On success the result is the datum hash as hex
+ * (the value you place in a transaction output's datum-hash field).
+ *
+ * @param thread the current isolate thread
+ * @param datumCborHexPtr the PlutusData as CBOR hex
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_SERIALIZATION}
+ */
@CEntryPoint(name = "ccl_plutus_data_hash")
public static int datumHash(IsolateThread thread, CCharPointer datumCborHexPtr) {
try {
@@ -27,6 +44,7 @@ public static int datumHash(IsolateThread thread, CCharPointer datumCborHexPtr)
PlutusData plutusData = PlutusData.deserialize(datumBytes);
String hash = plutusData.getDatumHash();
ResultState.set(hash);
+
return ErrorCodes.CCL_SUCCESS;
} catch (Exception e) {
ErrorState.set(e.getMessage());
@@ -34,6 +52,15 @@ public static int datumHash(IsolateThread thread, CCharPointer datumCborHexPtr)
}
}
+ /**
+ * Converts PlutusData CBOR to its JSON representation.
+ *
+ *
Exported as {@code ccl_plutus_data_to_json}. On success the result is the PlutusData as JSON.
+ *
+ * @param thread the current isolate thread
+ * @param cborHexPtr the PlutusData as CBOR hex
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_SERIALIZATION}
+ */
@CEntryPoint(name = "ccl_plutus_data_to_json")
public static int toJson(IsolateThread thread, CCharPointer cborHexPtr) {
try {
@@ -54,6 +81,16 @@ public static int toJson(IsolateThread thread, CCharPointer cborHexPtr) {
}
}
+ /**
+ * Builds PlutusData CBOR from its JSON representation.
+ *
+ *
Exported as {@code ccl_plutus_data_from_json}. The inverse of
+ * {@code ccl_plutus_data_to_json}: on success the result is the PlutusData as CBOR hex.
+ *
+ * @param thread the current isolate thread
+ * @param jsonPtr the PlutusData as JSON (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_SERIALIZATION}
+ */
@CEntryPoint(name = "ccl_plutus_data_from_json")
public static int fromJson(IsolateThread thread, CCharPointer jsonPtr) {
try {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/QuickTxApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/QuickTxApi.java
index 2fc8463..33c9491 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/QuickTxApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/QuickTxApi.java
@@ -9,22 +9,70 @@
import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.type.CCharPointer;
+/**
+ * QuickTx entry point: build an unsigned transaction from a CCL TxPlan (YAML), fully offline.
+ *
+ *
The transaction is defined by a TxPlan YAML document
+ * (CCL's native transaction format). The caller also supplies the chain data the build needs —
+ * available UTXOs and protocol parameters — as JSON. Nothing is fetched and nothing is submitted;
+ * the result is the unsigned transaction CBOR.
+ *
+ *
See {@link com.bloxbean.cardano.bridge.CclBridge} for the calling convention. The single entry
+ * point here is a static GraalVM {@code @CEntryPoint}.
+ */
public final class QuickTxApi {
private static final QuickTxService service = new QuickTxService();
private QuickTxApi() {}
+ /**
+ * Builds an unsigned transaction from a TxPlan YAML document and caller-supplied chain data.
+ *
+ *
Exported as {@code ccl_quicktx_build}. {@code yaml} is the TxPlan transaction definition;
+ * {@code utxos_json} is a JSON array of the sender's UTXOs and {@code protocol_params_json} a
+ * JSON protocol-parameters object (both standard CCL data models). On success the result is a
+ * JSON object:
+ *
{@code {"tx_cbor","tx_hash","fee"}}
+ * where {@code tx_cbor} is the unsigned transaction; sign it with {@code ccl_account_sign_tx} /
+ * {@code ccl_tx_sign_with_secret_key} and submit it yourself.
+ *
+ * For Plutus script transactions, pass the redeemers' execution units in
+ * {@code exec_units_json} (a JSON array of {@code {"mem","steps"}}, one per redeemer in
+ * transaction order). The caller computes these with any UPLC evaluator (Ogmios, Blockfrost,
+ * Aiken, Scalus) and passes them in, just like UTXOs and protocol parameters. Pass {@code null}
+ * (or an empty array) for non-script transactions. A script transaction with no execution units
+ * fails with {@link ErrorCodes#CCL_ERROR_TX_BUILD}.
+ *
+ * @param thread the current isolate thread
+ * @param yamlPtr the TxPlan YAML (UTF-8 C string)
+ * @param utxosJsonPtr JSON array of UTXOs (UTF-8 C string)
+ * @param protocolParamsJsonPtr JSON protocol parameters (UTF-8 C string)
+ * @param execUnitsJsonPtr JSON array of redeemer execution units, or null (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}; on failure
+ * {@link ErrorCodes#CCL_ERROR_INVALID_ARGUMENT},
+ * {@link ErrorCodes#CCL_ERROR_INSUFFICIENT_FUNDS}, or
+ * {@link ErrorCodes#CCL_ERROR_TX_BUILD}
+ */
@CEntryPoint(name = "ccl_quicktx_build")
- public static int build(IsolateThread thread, CCharPointer specJsonPtr) {
+ public static int build(IsolateThread thread, CCharPointer yamlPtr,
+ CCharPointer utxosJsonPtr, CCharPointer protocolParamsJsonPtr,
+ CCharPointer execUnitsJsonPtr) {
try {
- String specJson = NativeString.toJavaString(specJsonPtr);
- if (specJson == null || specJson.isEmpty()) {
- ErrorState.set("Transaction spec JSON is required");
+ String yaml = NativeString.toJavaString(yamlPtr);
+ if (yaml == null || yaml.isEmpty()) {
+ ErrorState.set("TxPlan YAML is required");
return ErrorCodes.CCL_ERROR_INVALID_ARGUMENT;
}
+ String utxosJson = NativeString.toJavaString(utxosJsonPtr);
+ String protocolParamsJson = NativeString.toJavaString(protocolParamsJsonPtr);
+ if (protocolParamsJson == null || protocolParamsJson.isEmpty()) {
+ ErrorState.set("Protocol parameters JSON is required");
+ return ErrorCodes.CCL_ERROR_INVALID_ARGUMENT;
+ }
+ String execUnitsJson = NativeString.toJavaString(execUnitsJsonPtr);
- String resultJson = service.buildTransaction(specJson);
+ String resultJson = service.buildTransaction(yaml, utxosJson, protocolParamsJson, execUnitsJson);
ResultState.set(resultJson);
return ErrorCodes.CCL_SUCCESS;
} catch (IllegalArgumentException e) {
@@ -36,7 +84,14 @@ public static int build(IsolateThread thread, CCharPointer specJsonPtr) {
ErrorState.set(msg);
return ErrorCodes.CCL_ERROR_INSUFFICIENT_FUNDS;
}
- ErrorState.set(msg != null ? msg : e.getClass().getName());
+ // Include the root cause — wrapped exceptions (e.g. YAML deserialization) otherwise
+ // hide the actual problem behind a generic message.
+ StringBuilder detail = new StringBuilder(msg != null ? msg : e.getClass().getName());
+ for (Throwable c = e.getCause(); c != null && c != c.getCause(); c = c.getCause()) {
+ detail.append(" | ").append(c.getClass().getSimpleName())
+ .append(": ").append(c.getMessage());
+ }
+ ErrorState.set(detail.toString());
return ErrorCodes.CCL_ERROR_TX_BUILD;
}
}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/ScriptApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/ScriptApi.java
index 6f59ce6..6147532 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/ScriptApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/ScriptApi.java
@@ -14,10 +14,28 @@
import java.util.LinkedHashMap;
import java.util.Map;
+/**
+ * Script entry points: parse native scripts and compute script hashes.
+ *
+ *
See {@link com.bloxbean.cardano.bridge.CclBridge} for the calling convention. Every entry
+ * point here is a static GraalVM {@code @CEntryPoint}.
+ */
public final class ScriptApi {
private ScriptApi() {}
+ /**
+ * Parses a native script from its JSON form.
+ *
+ *
Exported as {@code ccl_script_native_from_json}. Accepts the standard native-script JSON
+ * (e.g. {@code {"type":"sig","keyHash":"..."}}, {@code all}/{@code any}/{@code atLeast},
+ * {@code before}/{@code after}). On success the result is a JSON object:
+ *
{@code {"policy_id","script_hash","cbor_hex"}}
+ *
+ * @param thread the current isolate thread
+ * @param jsonPtr the native script as JSON (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_SERIALIZATION}
+ */
@CEntryPoint(name = "ccl_script_native_from_json")
public static int nativeScriptFromJson(IsolateThread thread, CCharPointer jsonPtr) {
try {
@@ -42,6 +60,18 @@ public static int nativeScriptFromJson(IsolateThread thread, CCharPointer jsonPt
}
}
+ /**
+ * Computes a script hash (policy id) from a script's CBOR.
+ *
+ * Exported as {@code ccl_script_hash}. The hash is Blake2b-224 of {@code (typeByte || scriptBytes)},
+ * where {@code scriptType} is the language tag: {@code 0}=native, {@code 1}=Plutus V1,
+ * {@code 2}=Plutus V2, {@code 3}=Plutus V3. On success the result is the 28-byte hash as hex.
+ *
+ * @param thread the current isolate thread
+ * @param scriptCborHexPtr the script as CBOR hex
+ * @param scriptType language tag (0=native, 1=PlutusV1, 2=PlutusV2, 3=PlutusV3)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_SERIALIZATION}
+ */
@CEntryPoint(name = "ccl_script_hash")
public static int scriptHash(IsolateThread thread, CCharPointer scriptCborHexPtr, int scriptType) {
try {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/TransactionApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/TransactionApi.java
index 0360a40..0840661 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/TransactionApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/TransactionApi.java
@@ -14,10 +14,30 @@
import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.type.CCharPointer;
+/**
+ * Transaction entry points: hash, sign (with a raw secret key), and CBOR↔JSON conversion.
+ *
+ *
Transactions are passed as CBOR hex (the on-chain serialization). The JSON form is a readable
+ * representation for inspection, not the on-chain format. See
+ * {@link com.bloxbean.cardano.bridge.CclBridge} for the calling convention. Every entry point here
+ * is a static GraalVM {@code @CEntryPoint}.
+ */
public final class TransactionApi {
private TransactionApi() {}
+ /**
+ * Signs a transaction with a raw Ed25519 secret key.
+ *
+ *
Exported as {@code ccl_tx_sign_with_secret_key}. On success the result is the signed
+ * transaction as CBOR hex. Use this when you hold a key directly rather than a mnemonic
+ * (cf. {@code ccl_account_sign_tx}).
+ *
+ * @param thread the current isolate thread
+ * @param txCborHexPtr the transaction as CBOR hex
+ * @param skCborHexPtr the secret key as CBOR hex
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_TRANSACTION}
+ */
@CEntryPoint(name = "ccl_tx_sign_with_secret_key")
public static int signWithSecretKey(IsolateThread thread, CCharPointer txCborHexPtr,
CCharPointer skCborHexPtr) {
@@ -45,6 +65,16 @@ public static int signWithSecretKey(IsolateThread thread, CCharPointer txCborHex
}
}
+ /**
+ * Computes the transaction hash (Blake2b-256 of the transaction body).
+ *
+ *
Exported as {@code ccl_tx_hash}. On success the result is the 32-byte hash as hex (64 hex
+ * chars) — i.e. the transaction id.
+ *
+ * @param thread the current isolate thread
+ * @param txCborHexPtr the transaction as CBOR hex
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_TRANSACTION}
+ */
@CEntryPoint(name = "ccl_tx_hash")
public static int hash(IsolateThread thread, CCharPointer txCborHexPtr) {
try {
@@ -65,6 +95,16 @@ public static int hash(IsolateThread thread, CCharPointer txCborHexPtr) {
}
}
+ /**
+ * Converts a transaction's CBOR to a readable JSON representation.
+ *
+ *
Exported as {@code ccl_tx_to_json}. On success the result is the transaction as JSON (for
+ * inspection — this is not the on-chain format). Equivalent to {@code ccl_tx_deserialize}.
+ *
+ * @param thread the current isolate thread
+ * @param txCborHexPtr the transaction as CBOR hex
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_SERIALIZATION}
+ */
@CEntryPoint(name = "ccl_tx_to_json")
public static int toJson(IsolateThread thread, CCharPointer txCborHexPtr) {
try {
@@ -84,6 +124,16 @@ public static int toJson(IsolateThread thread, CCharPointer txCborHexPtr) {
}
}
+ /**
+ * Builds a transaction's CBOR from its JSON representation.
+ *
+ *
Exported as {@code ccl_tx_from_json}. The inverse of {@code ccl_tx_to_json}: on success the
+ * result is the transaction as CBOR hex.
+ *
+ * @param thread the current isolate thread
+ * @param txJsonPtr the transaction as JSON (UTF-8 C string)
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_SERIALIZATION}
+ */
@CEntryPoint(name = "ccl_tx_from_json")
public static int fromJson(IsolateThread thread, CCharPointer txJsonPtr) {
try {
@@ -102,6 +152,16 @@ public static int fromJson(IsolateThread thread, CCharPointer txJsonPtr) {
}
}
+ /**
+ * Deserializes a transaction's CBOR into its JSON representation.
+ *
+ *
Exported as {@code ccl_tx_deserialize}. On success the result is the transaction as JSON
+ * (for inspection). Same output as {@code ccl_tx_to_json}.
+ *
+ * @param thread the current isolate thread
+ * @param txCborHexPtr the transaction as CBOR hex
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_SERIALIZATION}
+ */
@CEntryPoint(name = "ccl_tx_deserialize")
public static int deserialize(IsolateThread thread, CCharPointer txCborHexPtr) {
try {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/WalletApi.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/WalletApi.java
index 4bb6e41..89047c0 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/WalletApi.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/WalletApi.java
@@ -13,12 +13,31 @@
import java.util.List;
import java.util.Map;
+/**
+ * HD wallet entry points: create/restore a wallet and derive its addresses.
+ *
+ *
Unlike {@link AccountApi} (a single account), a {@code Wallet} exposes a sequence of addresses.
+ * {@code networkId} uses {@code 0}=mainnet, {@code 1}=testnet, {@code 2}=preprod, {@code 3}=preview.
+ * See {@link com.bloxbean.cardano.bridge.CclBridge} for the calling convention. Every entry point
+ * here is a static GraalVM {@code @CEntryPoint}.
+ */
public final class WalletApi {
private static final int DEFAULT_ADDRESS_COUNT = 5;
private WalletApi() {}
+ /**
+ * Creates a new HD wallet with a freshly generated mnemonic.
+ *
+ *
Exported as {@code ccl_wallet_create}. On success the result is a JSON object:
+ *
{@code {"mnemonic","stake_address","addresses":[ ...first 5 base addresses... ]}}
+ *
+ * @param thread the current isolate thread
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_wallet_create")
public static int create(IsolateThread thread, int networkId) {
try {
@@ -38,6 +57,18 @@ public static int create(IsolateThread thread, int networkId) {
}
}
+ /**
+ * Restores an HD wallet from an existing mnemonic.
+ *
+ * Exported as {@code ccl_wallet_from_mnemonic}. On success the result is the same JSON object
+ * as {@code ccl_wallet_create}.
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_wallet_from_mnemonic")
public static int fromMnemonic(IsolateThread thread, CCharPointer mnemonicPtr, int networkId) {
try {
@@ -63,6 +94,19 @@ public static int fromMnemonic(IsolateThread thread, CCharPointer mnemonicPtr, i
}
}
+ /**
+ * Derives a single base address at the given index from a wallet's mnemonic.
+ *
+ *
Exported as {@code ccl_wallet_get_address}. On success the result is the bech32 base address
+ * at {@code index}.
+ *
+ * @param thread the current isolate thread
+ * @param mnemonicPtr the BIP-39 mnemonic phrase (UTF-8 C string)
+ * @param networkId 0=mainnet, 1=testnet, 2=preprod, 3=preview
+ * @param index the address index to derive
+ * @return {@link ErrorCodes#CCL_SUCCESS}, or {@link ErrorCodes#CCL_ERROR_INVALID_NETWORK} /
+ * {@link ErrorCodes#CCL_ERROR_GENERAL}
+ */
@CEntryPoint(name = "ccl_wallet_get_address")
public static int getAddress(IsolateThread thread, CCharPointer mnemonicPtr,
int networkId, int index) {
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/ProviderConfig.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/ProviderConfig.java
deleted file mode 100644
index a18997d..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/ProviderConfig.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class ProviderConfig {
-
- @JsonProperty("name")
- private String name;
-
- @JsonProperty("url")
- private String url;
-
- @JsonProperty("api_key")
- private String apiKey;
-
- @JsonProperty("enable_cost_evaluation")
- private Boolean enableCostEvaluation;
-
- public String getName() { return name; }
- public void setName(String name) { this.name = name; }
-
- public String getUrl() { return url; }
- public void setUrl(String url) { this.url = url; }
-
- public String getApiKey() { return apiKey; }
- public void setApiKey(String apiKey) { this.apiKey = apiKey; }
-
- public Boolean getEnableCostEvaluation() { return enableCostEvaluation; }
- public void setEnableCostEvaluation(Boolean enableCostEvaluation) { this.enableCostEvaluation = enableCostEvaluation; }
-
- public void validate() {
- if (name == null || name.isEmpty())
- throw new IllegalArgumentException("provider 'name' is required");
- if (url == null || url.isEmpty())
- throw new IllegalArgumentException("provider 'url' is required");
- if (!"yaci".equals(name))
- throw new IllegalArgumentException("Unsupported provider: " + name + ". Supported: yaci");
- }
-}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/QuickTxService.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/QuickTxService.java
index ffd0d31..d26c4e7 100644
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/QuickTxService.java
+++ b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/QuickTxService.java
@@ -2,145 +2,104 @@
import com.bloxbean.cardano.bridge.util.JsonHelper;
import com.bloxbean.cardano.client.api.ProtocolParamsSupplier;
-import com.bloxbean.cardano.client.api.TransactionEvaluator;
import com.bloxbean.cardano.client.api.UtxoSupplier;
+import com.bloxbean.cardano.client.api.impl.StaticTransactionEvaluator;
+import com.bloxbean.cardano.client.api.model.ProtocolParams;
+import com.bloxbean.cardano.client.api.model.Utxo;
import com.bloxbean.cardano.client.common.cbor.CborSerializationUtil;
import com.bloxbean.cardano.client.crypto.Blake2bUtil;
-import com.bloxbean.cardano.client.quicktx.AbstractTx;
+import com.bloxbean.cardano.client.plutus.spec.ExUnits;
import com.bloxbean.cardano.client.quicktx.QuickTxBuilder;
+import com.bloxbean.cardano.client.quicktx.serialization.TxPlan;
+import com.bloxbean.cardano.client.quicktx.serialization.YamlSerializer;
import com.bloxbean.cardano.client.transaction.spec.Transaction;
import com.bloxbean.cardano.client.util.HexUtil;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
/**
- * Service that builds unsigned Cardano transactions from a JSON spec
- * using QuickTxBuilder for automatic coin selection, fee calculation,
- * and change balancing.
+ * Builds unsigned Cardano transactions from a CCL {@link TxPlan} (YAML), fully offline.
+ *
+ *
The transaction is defined by a TxPlan YAML document; the caller supplies the chain data
+ * (UTXOs and protocol parameters) as JSON. No backend/provider is used and the transaction is never
+ * submitted — the result is the unsigned CBOR plus its hash and fee.
+ *
+ *
Plutus script transactions are supported when the caller supplies the redeemers' execution
+ * units (memory + CPU steps). Computing those units requires running the script in a UPLC
+ * evaluator, which the caller does out-of-band (Ogmios, Blockfrost, Aiken, Scalus, …) and passes
+ * in — exactly as it supplies UTXOs and protocol parameters. With units supplied, a
+ * {@link StaticTransactionEvaluator} stamps them onto the redeemers, fully offline. A script
+ * transaction built without execution units fails (no offline evaluator runs the script).
*/
public class QuickTxService {
/**
- * Build an unsigned transaction from a JSON spec string.
+ * Build an unsigned transaction from a TxPlan YAML document and caller-supplied chain data.
*
- * @param specJson JSON transaction specification
- * @return JSON string with tx_cbor, tx_hash, fee
+ * @param yaml the TxPlan YAML defining the transaction(s)
+ * @param utxosJson JSON array of UTXOs available to the sender (CCL {@code Utxo} model)
+ * @param protocolParamsJson JSON protocol parameters (CCL {@code ProtocolParams} model)
+ * @param execUnitsJson JSON array of redeemer execution units ({@code [{"mem","steps"}]},
+ * one per redeemer in transaction order); null/empty for non-script txs
+ * @return JSON string with {@code tx_cbor}, {@code tx_hash}, {@code fee}
*/
- public String buildTransaction(String specJson) throws Exception {
- // Parse spec
- TxSpec spec = JsonHelper.fromJson(specJson, TxSpec.class);
- spec.validate();
+ public String buildTransaction(String yaml, String utxosJson, String protocolParamsJson,
+ String execUnitsJson) throws Exception {
+ TxPlan plan = TxPlan.from(yaml);
- // Create suppliers — provider mode uses HTTP, static mode uses inline data
- UtxoSupplier utxoSupplier;
- ProtocolParamsSupplier ppSupplier;
+ List utxos = parseUtxos(utxosJson);
+ ProtocolParams protocolParams = JsonHelper.fromJson(protocolParamsJson, ProtocolParams.class);
- if (spec.getProvider() != null) {
- String providerUrl = spec.getProvider().getUrl();
- utxoSupplier = new YaciUtxoSupplier(providerUrl);
- ppSupplier = spec.getProtocolParams() != null
- ? () -> spec.getProtocolParams()
- : new YaciProtocolParamsSupplier(providerUrl);
- } else {
- utxoSupplier = new StaticUtxoSupplier(spec.getUtxos());
- ppSupplier = () -> spec.getProtocolParams();
- }
-
- // Create evaluator for script cost evaluation if provider is configured
- TransactionEvaluator evaluator = null;
- if (spec.getProvider() != null) {
- boolean enableEval = spec.getProvider().getEnableCostEvaluation() == null
- || spec.getProvider().getEnableCostEvaluation();
- if (enableEval) {
- evaluator = new YaciTransactionEvaluator(spec.getProvider().getUrl());
- }
- }
+ UtxoSupplier utxoSupplier = new StaticUtxoSupplier(utxos);
+ ProtocolParamsSupplier ppSupplier = () -> protocolParams;
- // Build with QuickTxBuilder
+ // No TransactionProcessor (offline; never submits). compose(plan) applies the plan's
+ // context (fee payer, validity, deposit mode, required signers, …) to the TxContext.
QuickTxBuilder builder = new QuickTxBuilder(utxoSupplier, ppSupplier, null);
-
- // Detect compose vs single mode
- QuickTxBuilder.TxContext txContext;
- if (spec.getTransactions() != null && !spec.getTransactions().isEmpty()) {
- // Compose mode — each item can be Tx or ScriptTx
- AbstractTx>[] txs = spec.getTransactions().stream()
- .map(item -> {
- if ("script_tx".equals(item.getTxType())) {
- return (AbstractTx>) ScriptTxSpecMapper.toScriptTx(item);
- } else {
- return (AbstractTx>) TxSpecMapper.toTx(item);
- }
- })
- .toArray(AbstractTx[]::new);
- txContext = builder.compose(txs);
- } else {
- // Single mode
- if ("script_tx".equals(spec.getTxType())) {
- txContext = builder.compose(ScriptTxSpecMapper.toScriptTx(spec));
- } else {
- txContext = builder.compose(TxSpecMapper.toTx(spec));
- }
- }
-
- // Set additional signers count for fee estimation
- int signerCount;
- if (spec.getSignerCount() != null) {
- signerCount = spec.getSignerCount();
- } else if (spec.getTransactions() != null && !spec.getTransactions().isEmpty()) {
- signerCount = spec.getTransactions().size();
- } else {
- signerCount = 1;
+ QuickTxBuilder.TxContext txContext = builder.compose(plan);
+
+ // Plutus script cost: when the caller supplies execution units, a static evaluator stamps
+ // them onto the redeemers (offline). The caller computes them however it likes (Ogmios,
+ // Blockfrost, Aiken, Scalus); the bridge does not run the script.
+ List execUnits = parseExUnits(execUnitsJson);
+ if (!execUnits.isEmpty()) {
+ txContext.withTxEvaluator(new StaticTransactionEvaluator(execUnits));
}
- txContext.additionalSignersCount(signerCount);
- // Set validity interval
- if (spec.getValidity() != null) {
- if (spec.getValidity().getValidFrom() != null) {
- txContext.validFrom(spec.getValidity().getValidFrom());
- }
- if (spec.getValidity().getValidTo() != null) {
- txContext.validTo(spec.getValidity().getValidTo());
- }
- }
+ // Budget witnesses for fee estimation of the (still unsigned) transaction.
+ txContext.additionalSignersCount(Math.max(1, plan.getTxs().size()));
- // Set merge outputs
- if (spec.getMergeOutputs() != null) {
- txContext.mergeOutputs(spec.getMergeOutputs());
- }
-
- // Set fee payer
- // For script_tx mode, feePayer is required since ScriptTx.from() is package-private.
- // TxContext.feePayer() sets both the feePayer and calls ScriptTx.from() internally.
- if (spec.getFeePayer() != null && !spec.getFeePayer().isEmpty()) {
- txContext.feePayer(spec.getFeePayer());
- } else if ("script_tx".equals(spec.getTxType())
- && spec.getFrom() != null && !spec.getFrom().isEmpty()) {
- txContext.feePayer(spec.getFrom());
- }
-
- // Set transaction evaluator for script cost evaluation
- if (evaluator != null) {
- txContext.withTxEvaluator(evaluator);
- }
-
- // Build the transaction
Transaction transaction = txContext.build();
- // Serialize and compute hash
String txCborHex = transaction.serializeToHex();
byte[] txBodyBytes = CborSerializationUtil.serialize(transaction.getBody().serialize());
String txHash = HexUtil.encodeHexString(Blake2bUtil.blake2bHash256(txBodyBytes));
-
- // Get fee from the built transaction
String fee = transaction.getBody().getFee().toString();
- // Build result
Map result = new LinkedHashMap<>();
result.put("tx_cbor", txCborHex);
result.put("tx_hash", txHash);
result.put("fee", fee);
+ return YamlSerializer.serialize(result);
+ }
- return JsonHelper.toJson(result);
+ private static List parseUtxos(String utxosJson) throws Exception {
+ if (utxosJson == null || utxosJson.isBlank()) {
+ return Collections.emptyList();
+ }
+ Utxo[] utxos = JsonHelper.fromJson(utxosJson, Utxo[].class);
+ return utxos != null ? Arrays.asList(utxos) : Collections.emptyList();
+ }
+
+ private static List parseExUnits(String execUnitsJson) throws Exception {
+ if (execUnitsJson == null || execUnitsJson.isBlank()) {
+ return Collections.emptyList();
+ }
+ ExUnits[] exUnits = JsonHelper.fromJson(execUnitsJson, ExUnits[].class);
+ return exUnits != null ? Arrays.asList(exUnits) : Collections.emptyList();
}
}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/ScriptTxSpecMapper.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/ScriptTxSpecMapper.java
deleted file mode 100644
index be1cbb3..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/ScriptTxSpecMapper.java
+++ /dev/null
@@ -1,736 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.bloxbean.cardano.client.address.Address;
-import com.bloxbean.cardano.client.address.Credential;
-import com.bloxbean.cardano.client.api.model.Utxo;
-import com.bloxbean.cardano.client.metadata.cbor.CBORMetadata;
-import com.bloxbean.cardano.client.metadata.cbor.CBORMetadataList;
-import com.bloxbean.cardano.client.metadata.cbor.CBORMetadataMap;
-import com.bloxbean.cardano.client.plutus.spec.PlutusData;
-import com.bloxbean.cardano.client.plutus.spec.PlutusScript;
-import com.bloxbean.cardano.client.plutus.spec.PlutusV1Script;
-import com.bloxbean.cardano.client.plutus.spec.PlutusV2Script;
-import com.bloxbean.cardano.client.plutus.spec.PlutusV3Script;
-import com.bloxbean.cardano.client.quicktx.ScriptTx;
-import com.bloxbean.cardano.client.spec.UnitInterval;
-import com.bloxbean.cardano.client.transaction.spec.Asset;
-import com.bloxbean.cardano.client.transaction.spec.ProtocolVersion;
-import com.bloxbean.cardano.client.transaction.spec.Withdrawal;
-import com.bloxbean.cardano.client.transaction.spec.governance.Anchor;
-import com.bloxbean.cardano.client.transaction.spec.governance.Constitution;
-import com.bloxbean.cardano.client.transaction.spec.governance.DRep;
-import com.bloxbean.cardano.client.transaction.spec.governance.Vote;
-import com.bloxbean.cardano.client.transaction.spec.governance.Voter;
-import com.bloxbean.cardano.client.transaction.spec.governance.VoterType;
-import com.bloxbean.cardano.client.transaction.spec.governance.actions.*;
-import com.bloxbean.cardano.client.spec.Script;
-import com.bloxbean.cardano.client.transaction.spec.script.NativeScript;
-import com.bloxbean.cardano.client.util.HexUtil;
-
-import java.math.BigInteger;
-import java.util.*;
-
-/**
- * Maps a TxSpec/TxItemSpec (parsed from JSON) into a CCL ScriptTx object.
- */
-public class ScriptTxSpecMapper {
-
- public static ScriptTx toScriptTx(TxSpec spec) {
- ScriptTx tx = new ScriptTx();
- // Note: ScriptTx.from() is package-private, so 'from' is set by TxContext via feePayer
- if (spec.getChangeAddress() != null && !spec.getChangeAddress().isEmpty()) {
- applyChangeAddress(tx, spec.getChangeAddress(), spec.getChangeDatumCborHex(), spec.getChangeDatumHash());
- }
- applyOperations(tx, spec.getOperations());
- return tx;
- }
-
- public static ScriptTx toScriptTx(TxItemSpec item) {
- ScriptTx tx = new ScriptTx();
- // Note: ScriptTx.from() is package-private, so 'from' is set by TxContext via feePayer
- if (item.getChangeAddress() != null && !item.getChangeAddress().isEmpty()) {
- applyChangeAddress(tx, item.getChangeAddress(), item.getChangeDatumCborHex(), item.getChangeDatumHash());
- }
- applyOperations(tx, item.getOperations());
- return tx;
- }
-
- private static void applyChangeAddress(ScriptTx tx, String changeAddress, String datumCborHex, String datumHash) {
- if (datumCborHex != null && !datumCborHex.isEmpty()) {
- PlutusData datum = parseRedeemer(datumCborHex);
- tx.withChangeAddress(changeAddress, datum);
- } else if (datumHash != null && !datumHash.isEmpty()) {
- tx.withChangeAddress(changeAddress, datumHash);
- } else {
- tx.withChangeAddress(changeAddress);
- }
- }
-
- private static void applyOperations(ScriptTx tx, List operations) {
- for (TxOperation op : operations) {
- switch (op.getType()) {
- case "pay_to_address":
- applyPayToAddress(tx, op);
- break;
- case "pay_to_contract":
- applyPayToContract(tx, op);
- break;
- case "attach_metadata":
- applyAttachMetadata(tx, op);
- break;
- case "collect_from":
- applyCollectFrom(tx, op);
- break;
- case "read_from":
- applyReadFrom(tx, op);
- break;
- case "mint_plutus_assets":
- applyMintPlutusAssets(tx, op);
- break;
- case "attach_spending_validator":
- applyAttachValidator(tx, op, "spending");
- break;
- case "attach_certificate_validator":
- applyAttachValidator(tx, op, "certificate");
- break;
- case "attach_reward_validator":
- applyAttachValidator(tx, op, "reward");
- break;
- case "attach_proposing_validator":
- applyAttachValidator(tx, op, "proposing");
- break;
- case "attach_voting_validator":
- applyAttachValidator(tx, op, "voting");
- break;
- // Staking (register_stake_address not available in ScriptTx)
- case "register_stake_address":
- throw new IllegalArgumentException(
- "register_stake_address is not supported in script_tx mode. Use regular tx mode.");
- case "deregister_stake_address":
- applyDeregisterStakeAddress(tx, op);
- break;
- case "delegate_to":
- applyDelegateTo(tx, op);
- break;
- case "withdraw":
- applyWithdraw(tx, op);
- break;
- // DRep
- case "register_drep":
- applyRegisterDRep(tx, op);
- break;
- case "unregister_drep":
- applyUnregisterDRep(tx, op);
- break;
- case "update_drep":
- applyUpdateDRep(tx, op);
- break;
- // Voting
- case "delegate_voting_power_to":
- applyDelegateVotingPowerTo(tx, op);
- break;
- case "create_vote":
- applyCreateVote(tx, op);
- break;
- // Governance
- case "create_proposal":
- applyCreateProposal(tx, op);
- break;
- // Treasury donation (Gap 4)
- case "donate_to_treasury":
- applyDonateToTreasury(tx, op);
- break;
- case "mint_assets":
- throw new IllegalArgumentException(
- "Use 'mint_plutus_assets' instead of 'mint_assets' in script_tx mode");
- default:
- throw new IllegalArgumentException("Unknown operation type: " + op.getType());
- }
- }
- }
-
- private static void applyPayToAddress(ScriptTx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("pay_to_address requires 'address'");
- }
- if (op.getAmounts() == null || op.getAmounts().isEmpty()) {
- throw new IllegalArgumentException("pay_to_address requires 'amounts'");
- }
- if (op.getScriptRefCborHex() != null && !op.getScriptRefCborHex().isEmpty()) {
- Script refScript = parseScriptRef(op.getScriptRefCborHex(), op.getScriptRefType());
- tx.payToAddress(op.getAddress(), op.getAmounts(), refScript);
- } else {
- tx.payToAddress(op.getAddress(), op.getAmounts());
- }
- }
-
- private static void applyPayToContract(ScriptTx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("pay_to_contract requires 'address'");
- }
- if (op.getAmounts() == null || op.getAmounts().isEmpty()) {
- throw new IllegalArgumentException("pay_to_contract requires 'amounts'");
- }
-
- Script refScript = null;
- if (op.getScriptRefCborHex() != null && !op.getScriptRefCborHex().isEmpty()) {
- refScript = parseScriptRef(op.getScriptRefCborHex(), op.getScriptRefType());
- }
-
- if (op.getDatumCborHex() != null && !op.getDatumCborHex().isEmpty()) {
- PlutusData datum = parseRedeemer(op.getDatumCborHex());
- if (refScript != null) {
- tx.payToContract(op.getAddress(), op.getAmounts(), datum, refScript);
- } else {
- tx.payToContract(op.getAddress(), op.getAmounts(), datum);
- }
- } else if (op.getDatumHash() != null && !op.getDatumHash().isEmpty()) {
- tx.payToContract(op.getAddress(), op.getAmounts(), op.getDatumHash());
- } else {
- throw new IllegalArgumentException("pay_to_contract requires 'datum_cbor_hex' or 'datum_hash'");
- }
- }
-
- @SuppressWarnings("unchecked")
- private static void applyAttachMetadata(ScriptTx tx, TxOperation op) {
- if (op.getLabel() == null) {
- throw new IllegalArgumentException("attach_metadata requires 'label'");
- }
- if (op.getMetadata() == null) {
- throw new IllegalArgumentException("attach_metadata requires 'metadata'");
- }
-
- CBORMetadata cborMetadata = new CBORMetadata();
- BigInteger label = BigInteger.valueOf(op.getLabel());
- Object metaValue = op.getMetadata();
-
- if (metaValue instanceof String) {
- cborMetadata.put(label, (String) metaValue);
- } else if (metaValue instanceof Number) {
- cborMetadata.put(label, BigInteger.valueOf(((Number) metaValue).longValue()));
- } else if (metaValue instanceof List) {
- CBORMetadataList list = buildMetadataList((List) metaValue);
- cborMetadata.put(label, list);
- } else if (metaValue instanceof Map) {
- CBORMetadataMap map = buildMetadataMap((Map) metaValue);
- cborMetadata.put(label, map);
- } else {
- throw new IllegalArgumentException("Unsupported metadata value type: " +
- (metaValue != null ? metaValue.getClass().getName() : "null"));
- }
-
- tx.attachMetadata(cborMetadata);
- }
-
- @SuppressWarnings("unchecked")
- private static CBORMetadataList buildMetadataList(List items) {
- CBORMetadataList list = new CBORMetadataList();
- for (Object item : items) {
- if (item instanceof String) {
- list.add((String) item);
- } else if (item instanceof Number) {
- list.add(BigInteger.valueOf(((Number) item).longValue()));
- } else if (item instanceof List) {
- list.add(buildMetadataList((List) item));
- } else if (item instanceof Map) {
- list.add(buildMetadataMap((Map) item));
- }
- }
- return list;
- }
-
- @SuppressWarnings("unchecked")
- private static CBORMetadataMap buildMetadataMap(Map map) {
- CBORMetadataMap metaMap = new CBORMetadataMap();
- for (Map.Entry entry : map.entrySet()) {
- String key = entry.getKey();
- Object value = entry.getValue();
- if (value instanceof String) {
- metaMap.put(key, (String) value);
- } else if (value instanceof Number) {
- metaMap.put(key, BigInteger.valueOf(((Number) value).longValue()));
- } else if (value instanceof List) {
- metaMap.put(key, buildMetadataList((List) value));
- } else if (value instanceof Map) {
- metaMap.put(key, buildMetadataMap((Map) value));
- }
- }
- return metaMap;
- }
-
- private static void applyCollectFrom(ScriptTx tx, TxOperation op) {
- if (op.getCollectUtxos() == null || op.getCollectUtxos().isEmpty()) {
- throw new IllegalArgumentException("collect_from requires 'collect_utxos'");
- }
-
- if (op.getRedeemerCborHex() != null && !op.getRedeemerCborHex().isEmpty()) {
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
- PlutusData datum = null;
- if (op.getDatumCborHex() != null && !op.getDatumCborHex().isEmpty()) {
- datum = parseRedeemer(op.getDatumCborHex());
- }
- if (datum != null) {
- tx.collectFrom(op.getCollectUtxos(), redeemer, datum);
- } else {
- tx.collectFrom(op.getCollectUtxos(), redeemer);
- }
- } else {
- tx.collectFrom(op.getCollectUtxos());
- }
- }
-
- private static void applyReadFrom(ScriptTx tx, TxOperation op) {
- if (op.getReferenceInputs() == null || op.getReferenceInputs().isEmpty()) {
- throw new IllegalArgumentException("read_from requires 'reference_inputs'");
- }
- for (TxOperation.ReferenceInput ref : op.getReferenceInputs()) {
- if (ref.getTxHash() == null || ref.getTxHash().isEmpty()) {
- throw new IllegalArgumentException("read_from reference_input requires 'tx_hash'");
- }
- tx.readFrom(ref.getTxHash(), ref.getOutputIndex());
- }
- }
-
- private static void applyMintPlutusAssets(ScriptTx tx, TxOperation op) {
- if (op.getScriptCborHex() == null || op.getScriptCborHex().isEmpty()) {
- throw new IllegalArgumentException("mint_plutus_assets requires 'script_cbor_hex'");
- }
- if (op.getScriptType() == null || op.getScriptType().isEmpty()) {
- throw new IllegalArgumentException("mint_plutus_assets requires 'script_type'");
- }
- if (op.getAssets() == null || op.getAssets().isEmpty()) {
- throw new IllegalArgumentException("mint_plutus_assets requires 'assets'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("mint_plutus_assets requires 'redeemer_cbor_hex'");
- }
-
- PlutusScript script = parsePlutusScript(op.getScriptCborHex(), op.getScriptType());
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
-
- List assets = new ArrayList<>();
- for (TxOperation.MintAsset ma : op.getAssets()) {
- assets.add(new Asset(ma.getName(), new BigInteger(ma.getQuantity())));
- }
-
- PlutusData outputDatum = null;
- if (op.getOutputDatumCborHex() != null && !op.getOutputDatumCborHex().isEmpty()) {
- outputDatum = parseRedeemer(op.getOutputDatumCborHex());
- }
-
- if (op.getReceiver() != null && !op.getReceiver().isEmpty()) {
- if (outputDatum != null) {
- tx.mintAsset(script, assets, redeemer, op.getReceiver(), outputDatum);
- } else {
- tx.mintAsset(script, assets, redeemer, op.getReceiver());
- }
- } else {
- tx.mintAsset(script, assets, redeemer);
- }
- }
-
- private static void applyAttachValidator(ScriptTx tx, TxOperation op, String validatorType) {
- if (op.getScriptCborHex() == null || op.getScriptCborHex().isEmpty()) {
- throw new IllegalArgumentException("attach_" + validatorType + "_validator requires 'script_cbor_hex'");
- }
- if (op.getScriptType() == null || op.getScriptType().isEmpty()) {
- throw new IllegalArgumentException("attach_" + validatorType + "_validator requires 'script_type'");
- }
-
- PlutusScript script = parsePlutusScript(op.getScriptCborHex(), op.getScriptType());
-
- switch (validatorType) {
- case "spending":
- tx.attachSpendingValidator(script);
- break;
- case "certificate":
- tx.attachCertificateValidator(script);
- break;
- case "reward":
- tx.attachRewardValidator(script);
- break;
- case "proposing":
- tx.attachProposingValidator(script);
- break;
- case "voting":
- tx.attachVotingValidator(script);
- break;
- default:
- throw new IllegalArgumentException("Unknown validator type: " + validatorType);
- }
- }
-
- // --- Staking ---
- // Note: In ScriptTx, deregisterStakeAddress, delegateTo, and withdraw always require a redeemer
-
- private static void applyDeregisterStakeAddress(ScriptTx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("deregister_stake_address requires 'address'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("deregister_stake_address in script_tx mode requires 'redeemer_cbor_hex'");
- }
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
-
- if (op.getRefundAddress() != null && !op.getRefundAddress().isEmpty()) {
- tx.deregisterStakeAddress(op.getAddress(), redeemer, op.getRefundAddress());
- } else {
- tx.deregisterStakeAddress(op.getAddress(), redeemer);
- }
- }
-
- private static void applyDelegateTo(ScriptTx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("delegate_to requires 'address'");
- }
- if (op.getPoolId() == null || op.getPoolId().isEmpty()) {
- throw new IllegalArgumentException("delegate_to requires 'pool_id'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("delegate_to in script_tx mode requires 'redeemer_cbor_hex'");
- }
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
- tx.delegateTo(op.getAddress(), op.getPoolId(), redeemer);
- }
-
- private static void applyWithdraw(ScriptTx tx, TxOperation op) {
- if (op.getRewardAddress() == null) {
- throw new IllegalArgumentException("withdraw requires 'reward_address'");
- }
- if (op.getAmount() == null) {
- throw new IllegalArgumentException("withdraw requires 'amount'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("withdraw in script_tx mode requires 'redeemer_cbor_hex'");
- }
- BigInteger amount = new BigInteger(op.getAmount());
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
-
- if (op.getReceiver() != null && !op.getReceiver().isEmpty()) {
- tx.withdraw(op.getRewardAddress(), amount, redeemer, op.getReceiver());
- } else {
- tx.withdraw(op.getRewardAddress(), amount, redeemer);
- }
- }
-
- // --- DRep ---
-
- private static void applyRegisterDRep(ScriptTx tx, TxOperation op) {
- if (op.getCredentialHash() == null) {
- throw new IllegalArgumentException("register_drep requires 'credential_hash'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("register_drep in script_tx mode requires 'redeemer_cbor_hex'");
- }
- Credential credential = parseCredential(op.getCredentialHash(), op.getCredentialType());
- Anchor anchor = parseAnchor(op.getAnchorUrl(), op.getAnchorDataHash());
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
-
- if (anchor != null) {
- tx.registerDRep(credential, anchor, redeemer);
- } else {
- tx.registerDRep(credential, redeemer);
- }
- }
-
- private static void applyUnregisterDRep(ScriptTx tx, TxOperation op) {
- if (op.getCredentialHash() == null) {
- throw new IllegalArgumentException("unregister_drep requires 'credential_hash'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("unregister_drep in script_tx mode requires 'redeemer_cbor_hex'");
- }
- Credential credential = parseCredential(op.getCredentialHash(), op.getCredentialType());
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
- String refundAddress = op.getRefundAddress() != null ? op.getRefundAddress() : null;
- BigInteger refundAmount = op.getRefundAmount() != null && !op.getRefundAmount().isEmpty()
- ? new BigInteger(op.getRefundAmount()) : null;
-
- tx.unRegisterDRep(credential, refundAddress, refundAmount, redeemer);
- }
-
- private static void applyUpdateDRep(ScriptTx tx, TxOperation op) {
- if (op.getCredentialHash() == null) {
- throw new IllegalArgumentException("update_drep requires 'credential_hash'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("update_drep in script_tx mode requires 'redeemer_cbor_hex'");
- }
- Credential credential = parseCredential(op.getCredentialHash(), op.getCredentialType());
- Anchor anchor = parseAnchor(op.getAnchorUrl(), op.getAnchorDataHash());
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
-
- tx.updateDRep(credential, anchor, redeemer);
- }
-
- // --- Voting ---
-
- private static void applyDelegateVotingPowerTo(ScriptTx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("delegate_voting_power_to requires 'address'");
- }
- if (op.getDrepType() == null) {
- throw new IllegalArgumentException("delegate_voting_power_to requires 'drep_type'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("delegate_voting_power_to in script_tx mode requires 'redeemer_cbor_hex'");
- }
- DRep drep = parseDRep(op.getDrepType(), op.getDrepHash());
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
- Address address = new Address(op.getAddress());
-
- tx.delegateVotingPowerTo(address, drep, redeemer);
- }
-
- private static void applyCreateVote(ScriptTx tx, TxOperation op) {
- if (op.getVoterType() == null) {
- throw new IllegalArgumentException("create_vote requires 'voter_type'");
- }
- if (op.getVoterHash() == null) {
- throw new IllegalArgumentException("create_vote requires 'voter_hash'");
- }
- if (op.getGovActionTxHash() == null) {
- throw new IllegalArgumentException("create_vote requires 'gov_action_tx_hash'");
- }
- if (op.getGovActionIndex() == null) {
- throw new IllegalArgumentException("create_vote requires 'gov_action_index'");
- }
- if (op.getVote() == null) {
- throw new IllegalArgumentException("create_vote requires 'vote'");
- }
-
- Voter voter = parseVoter(op.getVoterType(), op.getVoterHash());
- GovActionId govActionId = new GovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- Vote vote = parseVote(op.getVote());
- Anchor anchor = parseAnchor(op.getAnchorUrl(), op.getAnchorDataHash());
- PlutusData redeemer = op.getRedeemerCborHex() != null ? parseRedeemer(op.getRedeemerCborHex()) : null;
-
- if (redeemer == null) {
- throw new IllegalArgumentException("create_vote in script_tx mode requires 'redeemer_cbor_hex'");
- }
- tx.createVote(voter, govActionId, vote, anchor, redeemer);
- }
-
- // --- Governance proposals ---
-
- private static void applyCreateProposal(ScriptTx tx, TxOperation op) {
- if (op.getGovActionType() == null) {
- throw new IllegalArgumentException("create_proposal requires 'gov_action_type'");
- }
- if (op.getReturnAddress() == null) {
- throw new IllegalArgumentException("create_proposal requires 'return_address'");
- }
- if (op.getRedeemerCborHex() == null || op.getRedeemerCborHex().isEmpty()) {
- throw new IllegalArgumentException("create_proposal in script_tx mode requires 'redeemer_cbor_hex'");
- }
-
- Anchor anchor = parseAnchor(op.getAnchorUrl(), op.getAnchorDataHash());
- PlutusData redeemer = parseRedeemer(op.getRedeemerCborHex());
-
- switch (op.getGovActionType().toLowerCase()) {
- case "info_action": {
- InfoAction action = InfoAction.builder().build();
- tx.createProposal(action, op.getReturnAddress(), anchor, redeemer);
- break;
- }
- case "treasury_withdrawals": {
- TreasuryWithdrawalsAction action = TreasuryWithdrawalsAction.builder().build();
- if (op.getWithdrawals() != null) {
- for (Map w : op.getWithdrawals()) {
- String rewardAddr = w.get("reward_address");
- String amt = w.get("amount");
- if (rewardAddr == null || amt == null) {
- throw new IllegalArgumentException(
- "Each withdrawal requires 'reward_address' and 'amount'");
- }
- action.addWithdrawal(new Withdrawal(rewardAddr, new BigInteger(amt)));
- }
- }
- tx.createProposal(action, op.getReturnAddress(), anchor, redeemer);
- break;
- }
- case "no_confidence": {
- NoConfidence.NoConfidenceBuilder builder = NoConfidence.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor, redeemer);
- break;
- }
- case "update_committee": {
- UpdateCommittee.UpdateCommitteeBuilder builder = UpdateCommittee.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- if (op.getMembersToRemove() != null) {
- Set removals = new LinkedHashSet<>();
- for (Map m : op.getMembersToRemove()) {
- removals.add(parseCredential(m.get("hash"), m.get("type")));
- }
- builder.membersForRemoval(removals);
- }
- if (op.getNewMembers() != null) {
- Map newMembersMap = new LinkedHashMap<>();
- for (Map m : op.getNewMembers()) {
- Credential cred = parseCredential((String) m.get("hash"), (String) m.get("type"));
- int epoch = ((Number) m.get("epoch")).intValue();
- newMembersMap.put(cred, epoch);
- }
- builder.newMembersAndTerms(newMembersMap);
- }
- if (op.getQuorumNumerator() != null && op.getQuorumDenominator() != null) {
- builder.quorumThreshold(new UnitInterval(
- new BigInteger(op.getQuorumNumerator()),
- new BigInteger(op.getQuorumDenominator())));
- }
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor, redeemer);
- break;
- }
- case "new_constitution": {
- NewConstitution.NewConstitutionBuilder builder = NewConstitution.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- Anchor constitutionAnchor = parseAnchor(op.getConstitutionAnchorUrl(), op.getConstitutionAnchorDataHash());
- Constitution.ConstitutionBuilder cBuilder = Constitution.builder();
- if (constitutionAnchor != null) cBuilder.anchor(constitutionAnchor);
- if (op.getConstitutionScriptHash() != null) cBuilder.scripthash(op.getConstitutionScriptHash());
- builder.constitution(cBuilder.build());
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor, redeemer);
- break;
- }
- case "hard_fork_initiation": {
- HardForkInitiationAction.HardForkInitiationActionBuilder builder = HardForkInitiationAction.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- if (op.getProtocolVersionMajor() == null || op.getProtocolVersionMinor() == null) {
- throw new IllegalArgumentException(
- "hard_fork_initiation requires 'protocol_version_major' and 'protocol_version_minor'");
- }
- builder.protocolVersion(new ProtocolVersion(op.getProtocolVersionMajor(), op.getProtocolVersionMinor()));
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor, redeemer);
- break;
- }
- case "parameter_change": {
- ParameterChangeAction.ParameterChangeActionBuilder builder = ParameterChangeAction.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- if (op.getPolicyHash() != null && !op.getPolicyHash().isEmpty()) {
- builder.policyHash(HexUtil.decodeHexString(op.getPolicyHash()));
- }
- if (op.getProtocolParamUpdateJson() != null && !op.getProtocolParamUpdateJson().isEmpty()) {
- throw new IllegalArgumentException(
- "parameter_change protocol_param_update_json is not yet supported. " +
- "Use the raw transaction builder for parameter change proposals.");
- }
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor, redeemer);
- break;
- }
- default:
- throw new IllegalArgumentException(
- "Unsupported gov_action_type: " + op.getGovActionType());
- }
- }
-
- // --- Treasury donation (Gap 4) ---
-
- private static void applyDonateToTreasury(ScriptTx tx, TxOperation op) {
- if (op.getTreasuryValue() == null) {
- throw new IllegalArgumentException("donate_to_treasury requires 'treasury_value'");
- }
- if (op.getDonationAmount() == null) {
- throw new IllegalArgumentException("donate_to_treasury requires 'donation_amount'");
- }
- tx.donateToTreasury(new BigInteger(op.getTreasuryValue()), new BigInteger(op.getDonationAmount()));
- }
-
- // --- Helper methods ---
-
- private static GovActionId parsePrevGovActionId(String txHash, Integer index) {
- if (txHash == null || txHash.isEmpty()) return null;
- return new GovActionId(txHash, index != null ? index : 0);
- }
-
- private static Script parseScriptRef(String cborHex, String scriptRefType) {
- if (scriptRefType == null || scriptRefType.isEmpty()) {
- throw new IllegalArgumentException("script_ref_type is required when script_ref_cbor_hex is provided. " +
- "Supported: plutus_v1, plutus_v2, plutus_v3");
- }
- return switch (scriptRefType.toLowerCase()) {
- case "plutus_v1" -> PlutusV1Script.builder().cborHex(cborHex).build();
- case "plutus_v2" -> PlutusV2Script.builder().cborHex(cborHex).build();
- case "plutus_v3" -> PlutusV3Script.builder().cborHex(cborHex).build();
- default -> throw new IllegalArgumentException("Unsupported script_ref_type: " + scriptRefType
- + ". Supported: plutus_v1, plutus_v2, plutus_v3");
- };
- }
-
- static PlutusScript parsePlutusScript(String cborHex, String type) {
- return switch (type.toLowerCase()) {
- case "plutus_v1" -> PlutusV1Script.builder().cborHex(cborHex).build();
- case "plutus_v2" -> PlutusV2Script.builder().cborHex(cborHex).build();
- case "plutus_v3" -> PlutusV3Script.builder().cborHex(cborHex).build();
- default -> throw new IllegalArgumentException("Unsupported script_type: " + type
- + ". Supported: plutus_v1, plutus_v2, plutus_v3");
- };
- }
-
- static PlutusData parseRedeemer(String cborHex) {
- try {
- return PlutusData.deserialize(HexUtil.decodeHexString(cborHex));
- } catch (Exception e) {
- throw new IllegalArgumentException("Invalid CBOR hex: " + e.getMessage(), e);
- }
- }
-
- private static Credential parseCredential(String hash, String type) {
- if ("script".equalsIgnoreCase(type)) {
- return Credential.fromScript(hash);
- }
- return Credential.fromKey(hash);
- }
-
- private static Anchor parseAnchor(String url, String dataHash) {
- if (url == null || dataHash == null) return null;
- return Anchor.builder()
- .anchorUrl(url)
- .anchorDataHash(HexUtil.decodeHexString(dataHash))
- .build();
- }
-
- private static DRep parseDRep(String drepType, String drepHash) {
- return switch (drepType.toLowerCase()) {
- case "abstain" -> DRep.abstain();
- case "no_confidence" -> DRep.noConfidence();
- case "script_hash" -> DRep.scriptHash(drepHash);
- default -> DRep.addrKeyHash(drepHash);
- };
- }
-
- private static Vote parseVote(String voteStr) {
- return switch (voteStr.toLowerCase()) {
- case "yes" -> Vote.YES;
- case "no" -> Vote.NO;
- case "abstain" -> Vote.ABSTAIN;
- default -> throw new IllegalArgumentException("Invalid vote value: " + voteStr
- + ". Must be 'yes', 'no', or 'abstain'");
- };
- }
-
- private static Voter parseVoter(String voterType, String voterHash) {
- Credential credential = Credential.fromKey(voterHash);
- VoterType type = switch (voterType.toLowerCase()) {
- case "drep_key_hash" -> VoterType.DREP_KEY_HASH;
- case "drep_script_hash" -> {
- credential = Credential.fromScript(voterHash);
- yield VoterType.DREP_SCRIPT_HASH;
- }
- case "staking_pool_key_hash" -> VoterType.STAKING_POOL_KEY_HASH;
- case "constitutional_committee_hot_key_hash" ->
- VoterType.CONSTITUTIONAL_COMMITTEE_HOT_KEY_HASH;
- case "constitutional_committee_hot_script_hash" -> {
- credential = Credential.fromScript(voterHash);
- yield VoterType.CONSTITUTIONAL_COMMITTEE_HOT_SCRIPT_HASH;
- }
- default -> throw new IllegalArgumentException("Invalid voter_type: " + voterType);
- };
- return new Voter(type, credential);
- }
-}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxItemSpec.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxItemSpec.java
deleted file mode 100644
index 13c78d1..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxItemSpec.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.List;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class TxItemSpec {
-
- @JsonProperty("from")
- private String from;
-
- @JsonProperty("change_address")
- private String changeAddress;
-
- @JsonProperty("operations")
- private List operations;
-
- @JsonProperty("tx_type")
- private String txType;
-
- @JsonProperty("change_datum_cbor_hex")
- private String changeDatumCborHex;
-
- @JsonProperty("change_datum_hash")
- private String changeDatumHash;
-
- public String getFrom() { return from; }
- public void setFrom(String from) { this.from = from; }
-
- public String getChangeAddress() { return changeAddress; }
- public void setChangeAddress(String changeAddress) { this.changeAddress = changeAddress; }
-
- public List getOperations() { return operations; }
- public void setOperations(List operations) { this.operations = operations; }
-
- public String getTxType() { return txType; }
- public void setTxType(String txType) { this.txType = txType; }
-
- public String getChangeDatumCborHex() { return changeDatumCborHex; }
- public void setChangeDatumCborHex(String changeDatumCborHex) { this.changeDatumCborHex = changeDatumCborHex; }
-
- public String getChangeDatumHash() { return changeDatumHash; }
- public void setChangeDatumHash(String changeDatumHash) { this.changeDatumHash = changeDatumHash; }
-
- public void validate(int index) {
- if (!"script_tx".equals(txType) && (from == null || from.isEmpty())) {
- throw new IllegalArgumentException("transactions[" + index + "]: 'from' address is required");
- }
- if (operations == null || operations.isEmpty()) {
- throw new IllegalArgumentException("transactions[" + index + "]: at least one operation is required");
- }
- }
-}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxOperation.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxOperation.java
deleted file mode 100644
index acddf38..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxOperation.java
+++ /dev/null
@@ -1,430 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.bloxbean.cardano.client.api.model.Amount;
-import com.bloxbean.cardano.client.api.model.Utxo;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.List;
-import java.util.Map;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class TxOperation {
-
- @JsonProperty("type")
- private String type;
-
- // pay_to_address / pay_to_contract
- @JsonProperty("address")
- private String address;
-
- @JsonProperty("amounts")
- private List amounts;
-
- // pay_to_contract - inline datum CBOR hex
- @JsonProperty("datum_cbor_hex")
- private String datumCborHex;
-
- // pay_to_contract - datum hash hex
- @JsonProperty("datum_hash")
- private String datumHash;
-
- // mint_assets
- @JsonProperty("script_json")
- private String scriptJson;
-
- @JsonProperty("assets")
- private List assets;
-
- @JsonProperty("receiver")
- private String receiver;
-
- // attach_metadata
- @JsonProperty("label")
- private Integer label;
-
- @JsonProperty("metadata")
- private Object metadata;
-
- // collect_from
- @JsonProperty("collect_utxos")
- private List collectUtxos;
-
- // Staking
- @JsonProperty("pool_id")
- private String poolId;
-
- @JsonProperty("reward_address")
- private String rewardAddress;
-
- @JsonProperty("amount")
- private String amount;
-
- @JsonProperty("refund_address")
- private String refundAddress;
-
- // DRep
- @JsonProperty("credential_hash")
- private String credentialHash;
-
- @JsonProperty("credential_type")
- private String credentialType;
-
- @JsonProperty("anchor_url")
- private String anchorUrl;
-
- @JsonProperty("anchor_data_hash")
- private String anchorDataHash;
-
- // Voting
- @JsonProperty("drep_type")
- private String drepType;
-
- @JsonProperty("drep_hash")
- private String drepHash;
-
- @JsonProperty("voter_type")
- private String voterType;
-
- @JsonProperty("voter_hash")
- private String voterHash;
-
- @JsonProperty("gov_action_tx_hash")
- private String govActionTxHash;
-
- @JsonProperty("gov_action_index")
- private Integer govActionIndex;
-
- @JsonProperty("vote")
- private String vote;
-
- // Governance proposals
- @JsonProperty("gov_action_type")
- private String govActionType;
-
- @JsonProperty("return_address")
- private String returnAddress;
-
- @JsonProperty("withdrawals")
- private List> withdrawals;
-
- // Reference script on outputs (Gap 1)
- @JsonProperty("script_ref_cbor_hex")
- private String scriptRefCborHex;
-
- @JsonProperty("script_ref_type")
- private String scriptRefType;
-
- // Pool operations (Gap 3)
- @JsonProperty("operator")
- private String operator;
-
- @JsonProperty("vrf_key_hash")
- private String vrfKeyHash;
-
- @JsonProperty("pledge")
- private String pledge;
-
- @JsonProperty("cost")
- private String cost;
-
- @JsonProperty("margin_numerator")
- private String marginNumerator;
-
- @JsonProperty("margin_denominator")
- private String marginDenominator;
-
- @JsonProperty("pool_owners")
- private List poolOwners;
-
- @JsonProperty("relays")
- private List> relays;
-
- @JsonProperty("pool_metadata_url")
- private String poolMetadataUrl;
-
- @JsonProperty("pool_metadata_hash")
- private String poolMetadataHash;
-
- @JsonProperty("epoch")
- private Integer epoch;
-
- // Treasury donation (Gap 4)
- @JsonProperty("treasury_value")
- private String treasuryValue;
-
- @JsonProperty("donation_amount")
- private String donationAmount;
-
- // Governance additional fields (Gap 2)
- @JsonProperty("members_to_remove")
- private List> membersToRemove;
-
- @JsonProperty("new_members")
- private List> newMembers;
-
- @JsonProperty("quorum_numerator")
- private String quorumNumerator;
-
- @JsonProperty("quorum_denominator")
- private String quorumDenominator;
-
- @JsonProperty("constitution_anchor_url")
- private String constitutionAnchorUrl;
-
- @JsonProperty("constitution_anchor_data_hash")
- private String constitutionAnchorDataHash;
-
- @JsonProperty("constitution_script_hash")
- private String constitutionScriptHash;
-
- @JsonProperty("protocol_version_major")
- private Integer protocolVersionMajor;
-
- @JsonProperty("protocol_version_minor")
- private Integer protocolVersionMinor;
-
- @JsonProperty("protocol_param_update_json")
- private String protocolParamUpdateJson;
-
- @JsonProperty("policy_hash")
- private String policyHash;
-
- // DRep unregister refund amount (Gap 6)
- @JsonProperty("refund_amount")
- private String refundAmount;
-
- // ScriptTx fields
- @JsonProperty("redeemer_cbor_hex")
- private String redeemerCborHex;
-
- @JsonProperty("script_cbor_hex")
- private String scriptCborHex;
-
- @JsonProperty("script_type")
- private String scriptType;
-
- @JsonProperty("reference_inputs")
- private List referenceInputs;
-
- @JsonProperty("output_datum_cbor_hex")
- private String outputDatumCborHex;
-
- public String getType() { return type; }
- public void setType(String type) { this.type = type; }
-
- public String getAddress() { return address; }
- public void setAddress(String address) { this.address = address; }
-
- public List getAmounts() { return amounts; }
- public void setAmounts(List amounts) { this.amounts = amounts; }
-
- public String getDatumCborHex() { return datumCborHex; }
- public void setDatumCborHex(String datumCborHex) { this.datumCborHex = datumCborHex; }
-
- public String getDatumHash() { return datumHash; }
- public void setDatumHash(String datumHash) { this.datumHash = datumHash; }
-
- public String getScriptJson() { return scriptJson; }
- public void setScriptJson(String scriptJson) { this.scriptJson = scriptJson; }
-
- public List getAssets() { return assets; }
- public void setAssets(List assets) { this.assets = assets; }
-
- public String getReceiver() { return receiver; }
- public void setReceiver(String receiver) { this.receiver = receiver; }
-
- public Integer getLabel() { return label; }
- public void setLabel(Integer label) { this.label = label; }
-
- public Object getMetadata() { return metadata; }
- public void setMetadata(Object metadata) { this.metadata = metadata; }
-
- public List getCollectUtxos() { return collectUtxos; }
- public void setCollectUtxos(List collectUtxos) { this.collectUtxos = collectUtxos; }
-
- public String getPoolId() { return poolId; }
- public void setPoolId(String poolId) { this.poolId = poolId; }
-
- public String getRewardAddress() { return rewardAddress; }
- public void setRewardAddress(String rewardAddress) { this.rewardAddress = rewardAddress; }
-
- public String getAmount() { return amount; }
- public void setAmount(String amount) { this.amount = amount; }
-
- public String getRefundAddress() { return refundAddress; }
- public void setRefundAddress(String refundAddress) { this.refundAddress = refundAddress; }
-
- public String getCredentialHash() { return credentialHash; }
- public void setCredentialHash(String credentialHash) { this.credentialHash = credentialHash; }
-
- public String getCredentialType() { return credentialType; }
- public void setCredentialType(String credentialType) { this.credentialType = credentialType; }
-
- public String getAnchorUrl() { return anchorUrl; }
- public void setAnchorUrl(String anchorUrl) { this.anchorUrl = anchorUrl; }
-
- public String getAnchorDataHash() { return anchorDataHash; }
- public void setAnchorDataHash(String anchorDataHash) { this.anchorDataHash = anchorDataHash; }
-
- public String getDrepType() { return drepType; }
- public void setDrepType(String drepType) { this.drepType = drepType; }
-
- public String getDrepHash() { return drepHash; }
- public void setDrepHash(String drepHash) { this.drepHash = drepHash; }
-
- public String getVoterType() { return voterType; }
- public void setVoterType(String voterType) { this.voterType = voterType; }
-
- public String getVoterHash() { return voterHash; }
- public void setVoterHash(String voterHash) { this.voterHash = voterHash; }
-
- public String getGovActionTxHash() { return govActionTxHash; }
- public void setGovActionTxHash(String govActionTxHash) { this.govActionTxHash = govActionTxHash; }
-
- public Integer getGovActionIndex() { return govActionIndex; }
- public void setGovActionIndex(Integer govActionIndex) { this.govActionIndex = govActionIndex; }
-
- public String getVote() { return vote; }
- public void setVote(String vote) { this.vote = vote; }
-
- public String getGovActionType() { return govActionType; }
- public void setGovActionType(String govActionType) { this.govActionType = govActionType; }
-
- public String getReturnAddress() { return returnAddress; }
- public void setReturnAddress(String returnAddress) { this.returnAddress = returnAddress; }
-
- public List> getWithdrawals() { return withdrawals; }
- public void setWithdrawals(List> withdrawals) { this.withdrawals = withdrawals; }
-
- public String getRedeemerCborHex() { return redeemerCborHex; }
- public void setRedeemerCborHex(String redeemerCborHex) { this.redeemerCborHex = redeemerCborHex; }
-
- public String getScriptCborHex() { return scriptCborHex; }
- public void setScriptCborHex(String scriptCborHex) { this.scriptCborHex = scriptCborHex; }
-
- public String getScriptType() { return scriptType; }
- public void setScriptType(String scriptType) { this.scriptType = scriptType; }
-
- public List getReferenceInputs() { return referenceInputs; }
- public void setReferenceInputs(List referenceInputs) { this.referenceInputs = referenceInputs; }
-
- public String getOutputDatumCborHex() { return outputDatumCborHex; }
- public void setOutputDatumCborHex(String outputDatumCborHex) { this.outputDatumCborHex = outputDatumCborHex; }
-
- // Gap 1: Reference script
- public String getScriptRefCborHex() { return scriptRefCborHex; }
- public void setScriptRefCborHex(String scriptRefCborHex) { this.scriptRefCborHex = scriptRefCborHex; }
-
- public String getScriptRefType() { return scriptRefType; }
- public void setScriptRefType(String scriptRefType) { this.scriptRefType = scriptRefType; }
-
- // Gap 3: Pool operations
- public String getOperator() { return operator; }
- public void setOperator(String operator) { this.operator = operator; }
-
- public String getVrfKeyHash() { return vrfKeyHash; }
- public void setVrfKeyHash(String vrfKeyHash) { this.vrfKeyHash = vrfKeyHash; }
-
- public String getPledge() { return pledge; }
- public void setPledge(String pledge) { this.pledge = pledge; }
-
- public String getCost() { return cost; }
- public void setCost(String cost) { this.cost = cost; }
-
- public String getMarginNumerator() { return marginNumerator; }
- public void setMarginNumerator(String marginNumerator) { this.marginNumerator = marginNumerator; }
-
- public String getMarginDenominator() { return marginDenominator; }
- public void setMarginDenominator(String marginDenominator) { this.marginDenominator = marginDenominator; }
-
- public List getPoolOwners() { return poolOwners; }
- public void setPoolOwners(List poolOwners) { this.poolOwners = poolOwners; }
-
- public List> getRelays() { return relays; }
- public void setRelays(List> relays) { this.relays = relays; }
-
- public String getPoolMetadataUrl() { return poolMetadataUrl; }
- public void setPoolMetadataUrl(String poolMetadataUrl) { this.poolMetadataUrl = poolMetadataUrl; }
-
- public String getPoolMetadataHash() { return poolMetadataHash; }
- public void setPoolMetadataHash(String poolMetadataHash) { this.poolMetadataHash = poolMetadataHash; }
-
- public Integer getEpoch() { return epoch; }
- public void setEpoch(Integer epoch) { this.epoch = epoch; }
-
- // Gap 4: Treasury donation
- public String getTreasuryValue() { return treasuryValue; }
- public void setTreasuryValue(String treasuryValue) { this.treasuryValue = treasuryValue; }
-
- public String getDonationAmount() { return donationAmount; }
- public void setDonationAmount(String donationAmount) { this.donationAmount = donationAmount; }
-
- // Gap 2: Additional governance
- public List> getMembersToRemove() { return membersToRemove; }
- public void setMembersToRemove(List> membersToRemove) { this.membersToRemove = membersToRemove; }
-
- public List> getNewMembers() { return newMembers; }
- public void setNewMembers(List> newMembers) { this.newMembers = newMembers; }
-
- public String getQuorumNumerator() { return quorumNumerator; }
- public void setQuorumNumerator(String quorumNumerator) { this.quorumNumerator = quorumNumerator; }
-
- public String getQuorumDenominator() { return quorumDenominator; }
- public void setQuorumDenominator(String quorumDenominator) { this.quorumDenominator = quorumDenominator; }
-
- public String getConstitutionAnchorUrl() { return constitutionAnchorUrl; }
- public void setConstitutionAnchorUrl(String constitutionAnchorUrl) { this.constitutionAnchorUrl = constitutionAnchorUrl; }
-
- public String getConstitutionAnchorDataHash() { return constitutionAnchorDataHash; }
- public void setConstitutionAnchorDataHash(String constitutionAnchorDataHash) { this.constitutionAnchorDataHash = constitutionAnchorDataHash; }
-
- public String getConstitutionScriptHash() { return constitutionScriptHash; }
- public void setConstitutionScriptHash(String constitutionScriptHash) { this.constitutionScriptHash = constitutionScriptHash; }
-
- public Integer getProtocolVersionMajor() { return protocolVersionMajor; }
- public void setProtocolVersionMajor(Integer protocolVersionMajor) { this.protocolVersionMajor = protocolVersionMajor; }
-
- public Integer getProtocolVersionMinor() { return protocolVersionMinor; }
- public void setProtocolVersionMinor(Integer protocolVersionMinor) { this.protocolVersionMinor = protocolVersionMinor; }
-
- public String getProtocolParamUpdateJson() { return protocolParamUpdateJson; }
- public void setProtocolParamUpdateJson(String protocolParamUpdateJson) { this.protocolParamUpdateJson = protocolParamUpdateJson; }
-
- public String getPolicyHash() { return policyHash; }
- public void setPolicyHash(String policyHash) { this.policyHash = policyHash; }
-
- // Gap 6: DRep refund amount
- public String getRefundAmount() { return refundAmount; }
- public void setRefundAmount(String refundAmount) { this.refundAmount = refundAmount; }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- public static class ReferenceInput {
- @JsonProperty("tx_hash")
- private String txHash;
-
- @JsonProperty("output_index")
- private int outputIndex;
-
- public String getTxHash() { return txHash; }
- public void setTxHash(String txHash) { this.txHash = txHash; }
-
- public int getOutputIndex() { return outputIndex; }
- public void setOutputIndex(int outputIndex) { this.outputIndex = outputIndex; }
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- public static class MintAsset {
- @JsonProperty("name")
- private String name;
-
- @JsonProperty("quantity")
- private String quantity;
-
- public String getName() { return name; }
- public void setName(String name) { this.name = name; }
-
- public String getQuantity() { return quantity; }
- public void setQuantity(String quantity) { this.quantity = quantity; }
- }
-}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxSpec.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxSpec.java
deleted file mode 100644
index e3bc572..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxSpec.java
+++ /dev/null
@@ -1,144 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.bloxbean.cardano.client.api.model.Amount;
-import com.bloxbean.cardano.client.api.model.ProtocolParams;
-import com.bloxbean.cardano.client.api.model.Utxo;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.List;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class TxSpec {
-
- @JsonProperty("operations")
- private List operations;
-
- @JsonProperty("from")
- private String from;
-
- @JsonProperty("change_address")
- private String changeAddress;
-
- @JsonProperty("fee_payer")
- private String feePayer;
-
- @JsonProperty("utxos")
- private List utxos;
-
- @JsonProperty("protocol_params")
- private ProtocolParams protocolParams;
-
- @JsonProperty("validity")
- private Validity validity;
-
- @JsonProperty("merge_outputs")
- private Boolean mergeOutputs;
-
- @JsonProperty("signer_count")
- private Integer signerCount;
-
- @JsonProperty("transactions")
- private List transactions;
-
- @JsonProperty("provider")
- private ProviderConfig provider;
-
- @JsonProperty("tx_type")
- private String txType;
-
- @JsonProperty("change_datum_cbor_hex")
- private String changeDatumCborHex;
-
- @JsonProperty("change_datum_hash")
- private String changeDatumHash;
-
- public List getOperations() { return operations; }
- public void setOperations(List operations) { this.operations = operations; }
-
- public String getFrom() { return from; }
- public void setFrom(String from) { this.from = from; }
-
- public String getChangeAddress() { return changeAddress; }
- public void setChangeAddress(String changeAddress) { this.changeAddress = changeAddress; }
-
- public String getFeePayer() { return feePayer; }
- public void setFeePayer(String feePayer) { this.feePayer = feePayer; }
-
- public List getUtxos() { return utxos; }
- public void setUtxos(List utxos) { this.utxos = utxos; }
-
- public ProtocolParams getProtocolParams() { return protocolParams; }
- public void setProtocolParams(ProtocolParams protocolParams) { this.protocolParams = protocolParams; }
-
- public Validity getValidity() { return validity; }
- public void setValidity(Validity validity) { this.validity = validity; }
-
- public Boolean getMergeOutputs() { return mergeOutputs; }
- public void setMergeOutputs(Boolean mergeOutputs) { this.mergeOutputs = mergeOutputs; }
-
- public Integer getSignerCount() { return signerCount; }
- public void setSignerCount(Integer signerCount) { this.signerCount = signerCount; }
-
- public List getTransactions() { return transactions; }
- public void setTransactions(List transactions) { this.transactions = transactions; }
-
- public ProviderConfig getProvider() { return provider; }
- public void setProvider(ProviderConfig provider) { this.provider = provider; }
-
- public String getTxType() { return txType; }
- public void setTxType(String txType) { this.txType = txType; }
-
- public String getChangeDatumCborHex() { return changeDatumCborHex; }
- public void setChangeDatumCborHex(String changeDatumCborHex) { this.changeDatumCborHex = changeDatumCborHex; }
-
- public String getChangeDatumHash() { return changeDatumHash; }
- public void setChangeDatumHash(String changeDatumHash) { this.changeDatumHash = changeDatumHash; }
-
- public void validate() {
- if (transactions != null && !transactions.isEmpty()) {
- // Compose mode
- if (feePayer == null || feePayer.isEmpty()) {
- throw new IllegalArgumentException("'fee_payer' is required when composing multiple transactions");
- }
- for (int i = 0; i < transactions.size(); i++) {
- transactions.get(i).validate(i);
- }
- } else {
- // Single mode
- if (operations == null || operations.isEmpty()) {
- throw new IllegalArgumentException("At least one operation is required");
- }
- if (!"script_tx".equals(txType) && (from == null || from.isEmpty())) {
- throw new IllegalArgumentException("'from' address is required");
- }
- }
-
- // Common validations — provider mode supplies utxos and protocol_params via HTTP
- if (provider != null) {
- provider.validate();
- } else {
- if (utxos == null || utxos.isEmpty()) {
- throw new IllegalArgumentException("'utxos' are required");
- }
- if (protocolParams == null) {
- throw new IllegalArgumentException("'protocol_params' are required");
- }
- }
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- public static class Validity {
- @JsonProperty("valid_from")
- private Long validFrom;
-
- @JsonProperty("valid_to")
- private Long validTo;
-
- public Long getValidFrom() { return validFrom; }
- public void setValidFrom(Long validFrom) { this.validFrom = validFrom; }
-
- public Long getValidTo() { return validTo; }
- public void setValidTo(Long validTo) { this.validTo = validTo; }
- }
-}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxSpecMapper.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxSpecMapper.java
deleted file mode 100644
index 41f151a..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/TxSpecMapper.java
+++ /dev/null
@@ -1,728 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.bloxbean.cardano.client.address.Credential;
-import com.bloxbean.cardano.client.metadata.cbor.CBORMetadata;
-import com.bloxbean.cardano.client.metadata.cbor.CBORMetadataList;
-import com.bloxbean.cardano.client.metadata.cbor.CBORMetadataMap;
-import com.bloxbean.cardano.client.plutus.spec.PlutusData;
-import com.bloxbean.cardano.client.plutus.spec.PlutusV1Script;
-import com.bloxbean.cardano.client.plutus.spec.PlutusV2Script;
-import com.bloxbean.cardano.client.plutus.spec.PlutusV3Script;
-import com.bloxbean.cardano.client.quicktx.Tx;
-import com.bloxbean.cardano.client.spec.UnitInterval;
-import com.bloxbean.cardano.client.transaction.spec.Asset;
-import com.bloxbean.cardano.client.transaction.spec.ProtocolVersion;
-import com.bloxbean.cardano.client.transaction.spec.Withdrawal;
-import com.bloxbean.cardano.client.transaction.spec.cert.PoolRegistration;
-import com.bloxbean.cardano.client.transaction.spec.cert.SingleHostAddr;
-import com.bloxbean.cardano.client.transaction.spec.cert.SingleHostName;
-import com.bloxbean.cardano.client.transaction.spec.cert.MultiHostName;
-import com.bloxbean.cardano.client.transaction.spec.cert.Relay;
-import com.bloxbean.cardano.client.transaction.spec.governance.Anchor;
-import com.bloxbean.cardano.client.transaction.spec.governance.Constitution;
-import com.bloxbean.cardano.client.transaction.spec.governance.DRep;
-import com.bloxbean.cardano.client.transaction.spec.governance.Vote;
-import com.bloxbean.cardano.client.transaction.spec.governance.Voter;
-import com.bloxbean.cardano.client.transaction.spec.governance.VoterType;
-import com.bloxbean.cardano.client.transaction.spec.governance.actions.*;
-import com.bloxbean.cardano.client.spec.Script;
-import com.bloxbean.cardano.client.transaction.spec.script.NativeScript;
-import com.bloxbean.cardano.client.util.HexUtil;
-
-import java.math.BigInteger;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.UnknownHostException;
-import java.util.*;
-
-/**
- * Maps a TxSpec (parsed from JSON) into a CCL Tx object.
- */
-public class TxSpecMapper {
-
- public static Tx toTx(TxSpec spec) {
- Tx tx = new Tx();
- tx.from(spec.getFrom());
- if (spec.getChangeAddress() != null && !spec.getChangeAddress().isEmpty()) {
- tx.withChangeAddress(spec.getChangeAddress());
- }
- applyOperations(tx, spec.getOperations());
- return tx;
- }
-
- public static Tx toTx(TxItemSpec item) {
- Tx tx = new Tx();
- tx.from(item.getFrom());
- if (item.getChangeAddress() != null && !item.getChangeAddress().isEmpty()) {
- tx.withChangeAddress(item.getChangeAddress());
- }
- applyOperations(tx, item.getOperations());
- return tx;
- }
-
- private static void applyOperations(Tx tx, List operations) {
- for (TxOperation op : operations) {
- switch (op.getType()) {
- case "pay_to_address":
- applyPayToAddress(tx, op);
- break;
- case "pay_to_contract":
- applyPayToContract(tx, op);
- break;
- case "mint_assets":
- applyMintAssets(tx, op);
- break;
- case "attach_metadata":
- applyAttachMetadata(tx, op);
- break;
- case "collect_from":
- applyCollectFrom(tx, op);
- break;
- // Staking
- case "register_stake_address":
- applyRegisterStakeAddress(tx, op);
- break;
- case "deregister_stake_address":
- applyDeregisterStakeAddress(tx, op);
- break;
- case "delegate_to":
- applyDelegateTo(tx, op);
- break;
- case "withdraw":
- applyWithdraw(tx, op);
- break;
- // DRep
- case "register_drep":
- applyRegisterDRep(tx, op);
- break;
- case "unregister_drep":
- applyUnregisterDRep(tx, op);
- break;
- case "update_drep":
- applyUpdateDRep(tx, op);
- break;
- // Voting
- case "delegate_voting_power_to":
- applyDelegateVotingPowerTo(tx, op);
- break;
- case "create_vote":
- applyCreateVote(tx, op);
- break;
- // Governance
- case "create_proposal":
- applyCreateProposal(tx, op);
- break;
- // Pool operations (Gap 3)
- case "register_pool":
- applyRegisterPool(tx, op);
- break;
- case "update_pool":
- applyUpdatePool(tx, op);
- break;
- case "retire_pool":
- applyRetirePool(tx, op);
- break;
- // Treasury donation (Gap 4)
- case "donate_to_treasury":
- applyDonateToTreasury(tx, op);
- break;
- // Attach native script standalone (Gap 5)
- case "attach_native_script":
- applyAttachNativeScript(tx, op);
- break;
- default:
- throw new IllegalArgumentException("Unknown operation type: " + op.getType());
- }
- }
- }
-
- private static void applyPayToAddress(Tx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("pay_to_address requires 'address'");
- }
- if (op.getAmounts() == null || op.getAmounts().isEmpty()) {
- throw new IllegalArgumentException("pay_to_address requires 'amounts'");
- }
- if (op.getScriptRefCborHex() != null && !op.getScriptRefCborHex().isEmpty()) {
- Script refScript = parseScriptRef(op.getScriptRefCborHex(), op.getScriptRefType());
- tx.payToAddress(op.getAddress(), op.getAmounts(), refScript);
- } else {
- tx.payToAddress(op.getAddress(), op.getAmounts());
- }
- }
-
- private static void applyPayToContract(Tx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("pay_to_contract requires 'address'");
- }
- if (op.getAmounts() == null || op.getAmounts().isEmpty()) {
- throw new IllegalArgumentException("pay_to_contract requires 'amounts'");
- }
-
- Script refScript = null;
- if (op.getScriptRefCborHex() != null && !op.getScriptRefCborHex().isEmpty()) {
- refScript = parseScriptRef(op.getScriptRefCborHex(), op.getScriptRefType());
- }
-
- if (op.getDatumCborHex() != null && !op.getDatumCborHex().isEmpty()) {
- PlutusData datum;
- try {
- datum = PlutusData.deserialize(HexUtil.decodeHexString(op.getDatumCborHex()));
- } catch (Exception e) {
- throw new IllegalArgumentException("Invalid datum CBOR: " + e.getMessage(), e);
- }
- if (refScript != null) {
- tx.payToContract(op.getAddress(), op.getAmounts(), datum, refScript);
- } else {
- tx.payToContract(op.getAddress(), op.getAmounts(), datum);
- }
- } else if (op.getDatumHash() != null && !op.getDatumHash().isEmpty()) {
- tx.payToContract(op.getAddress(), op.getAmounts(), op.getDatumHash());
- } else {
- throw new IllegalArgumentException("pay_to_contract requires 'datum_cbor_hex' or 'datum_hash'");
- }
- }
-
- private static void applyMintAssets(Tx tx, TxOperation op) {
- if (op.getScriptJson() == null) {
- throw new IllegalArgumentException("mint_assets requires 'script_json'");
- }
- if (op.getAssets() == null || op.getAssets().isEmpty()) {
- throw new IllegalArgumentException("mint_assets requires 'assets'");
- }
- if (op.getReceiver() == null || op.getReceiver().isEmpty()) {
- throw new IllegalArgumentException("mint_assets requires 'receiver' address");
- }
-
- NativeScript script;
- try {
- script = NativeScript.deserializeJson(op.getScriptJson());
- } catch (Exception e) {
- throw new IllegalArgumentException("Invalid native script JSON: " + e.getMessage(), e);
- }
-
- List assets = new ArrayList<>();
- for (TxOperation.MintAsset ma : op.getAssets()) {
- assets.add(new Asset(ma.getName(), new BigInteger(ma.getQuantity())));
- }
-
- tx.mintAssets(script, assets, op.getReceiver());
- }
-
- @SuppressWarnings("unchecked")
- private static void applyAttachMetadata(Tx tx, TxOperation op) {
- if (op.getLabel() == null) {
- throw new IllegalArgumentException("attach_metadata requires 'label'");
- }
- if (op.getMetadata() == null) {
- throw new IllegalArgumentException("attach_metadata requires 'metadata'");
- }
-
- CBORMetadata cborMetadata = new CBORMetadata();
- BigInteger label = BigInteger.valueOf(op.getLabel());
- Object metaValue = op.getMetadata();
-
- if (metaValue instanceof String) {
- cborMetadata.put(label, (String) metaValue);
- } else if (metaValue instanceof Number) {
- cborMetadata.put(label, BigInteger.valueOf(((Number) metaValue).longValue()));
- } else if (metaValue instanceof List) {
- CBORMetadataList list = buildMetadataList((List) metaValue);
- cborMetadata.put(label, list);
- } else if (metaValue instanceof Map) {
- CBORMetadataMap map = buildMetadataMap((Map) metaValue);
- cborMetadata.put(label, map);
- } else {
- throw new IllegalArgumentException("Unsupported metadata value type: " +
- (metaValue != null ? metaValue.getClass().getName() : "null"));
- }
-
- tx.attachMetadata(cborMetadata);
- }
-
- @SuppressWarnings("unchecked")
- private static CBORMetadataList buildMetadataList(List items) {
- CBORMetadataList list = new CBORMetadataList();
- for (Object item : items) {
- if (item instanceof String) {
- list.add((String) item);
- } else if (item instanceof Number) {
- list.add(BigInteger.valueOf(((Number) item).longValue()));
- } else if (item instanceof List) {
- list.add(buildMetadataList((List) item));
- } else if (item instanceof Map) {
- list.add(buildMetadataMap((Map) item));
- }
- }
- return list;
- }
-
- @SuppressWarnings("unchecked")
- private static CBORMetadataMap buildMetadataMap(Map map) {
- CBORMetadataMap metaMap = new CBORMetadataMap();
- for (Map.Entry entry : map.entrySet()) {
- String key = entry.getKey();
- Object value = entry.getValue();
- if (value instanceof String) {
- metaMap.put(key, (String) value);
- } else if (value instanceof Number) {
- metaMap.put(key, BigInteger.valueOf(((Number) value).longValue()));
- } else if (value instanceof List) {
- metaMap.put(key, buildMetadataList((List) value));
- } else if (value instanceof Map) {
- metaMap.put(key, buildMetadataMap((Map) value));
- }
- }
- return metaMap;
- }
-
- private static void applyCollectFrom(Tx tx, TxOperation op) {
- if (op.getCollectUtxos() == null || op.getCollectUtxos().isEmpty()) {
- throw new IllegalArgumentException("collect_from requires 'collect_utxos'");
- }
- tx.collectFrom(op.getCollectUtxos());
- }
-
- // --- Staking ---
-
- private static void applyRegisterStakeAddress(Tx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("register_stake_address requires 'address'");
- }
- tx.registerStakeAddress(op.getAddress());
- }
-
- private static void applyDeregisterStakeAddress(Tx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("deregister_stake_address requires 'address'");
- }
- if (op.getRefundAddress() != null && !op.getRefundAddress().isEmpty()) {
- tx.deregisterStakeAddress(op.getAddress(), op.getRefundAddress());
- } else {
- tx.deregisterStakeAddress(op.getAddress());
- }
- }
-
- private static void applyDelegateTo(Tx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("delegate_to requires 'address'");
- }
- if (op.getPoolId() == null || op.getPoolId().isEmpty()) {
- throw new IllegalArgumentException("delegate_to requires 'pool_id'");
- }
- tx.delegateTo(op.getAddress(), op.getPoolId());
- }
-
- private static void applyWithdraw(Tx tx, TxOperation op) {
- if (op.getRewardAddress() == null) {
- throw new IllegalArgumentException("withdraw requires 'reward_address'");
- }
- if (op.getAmount() == null) {
- throw new IllegalArgumentException("withdraw requires 'amount'");
- }
- BigInteger amount = new BigInteger(op.getAmount());
- if (op.getReceiver() != null && !op.getReceiver().isEmpty()) {
- tx.withdraw(op.getRewardAddress(), amount, op.getReceiver());
- } else {
- tx.withdraw(op.getRewardAddress(), amount);
- }
- }
-
- // --- DRep ---
-
- private static void applyRegisterDRep(Tx tx, TxOperation op) {
- if (op.getCredentialHash() == null) {
- throw new IllegalArgumentException("register_drep requires 'credential_hash'");
- }
- Credential credential = parseCredential(op.getCredentialHash(), op.getCredentialType());
- Anchor anchor = parseAnchor(op.getAnchorUrl(), op.getAnchorDataHash());
- if (anchor != null) {
- tx.registerDRep(credential, anchor);
- } else {
- tx.registerDRep(credential);
- }
- }
-
- private static void applyUnregisterDRep(Tx tx, TxOperation op) {
- if (op.getCredentialHash() == null) {
- throw new IllegalArgumentException("unregister_drep requires 'credential_hash'");
- }
- Credential credential = parseCredential(op.getCredentialHash(), op.getCredentialType());
- if (op.getRefundAmount() != null && !op.getRefundAmount().isEmpty()
- && op.getRefundAddress() != null && !op.getRefundAddress().isEmpty()) {
- tx.unregisterDRep(credential, op.getRefundAddress(), new BigInteger(op.getRefundAmount()));
- } else if (op.getRefundAddress() != null && !op.getRefundAddress().isEmpty()) {
- tx.unregisterDRep(credential, op.getRefundAddress());
- } else {
- tx.unregisterDRep(credential);
- }
- }
-
- private static void applyUpdateDRep(Tx tx, TxOperation op) {
- if (op.getCredentialHash() == null) {
- throw new IllegalArgumentException("update_drep requires 'credential_hash'");
- }
- Credential credential = parseCredential(op.getCredentialHash(), op.getCredentialType());
- Anchor anchor = parseAnchor(op.getAnchorUrl(), op.getAnchorDataHash());
- if (anchor != null) {
- tx.updateDRep(credential, anchor);
- } else {
- tx.updateDRep(credential);
- }
- }
-
- // --- Voting ---
-
- private static void applyDelegateVotingPowerTo(Tx tx, TxOperation op) {
- if (op.getAddress() == null) {
- throw new IllegalArgumentException("delegate_voting_power_to requires 'address'");
- }
- if (op.getDrepType() == null) {
- throw new IllegalArgumentException("delegate_voting_power_to requires 'drep_type'");
- }
- DRep drep = parseDRep(op.getDrepType(), op.getDrepHash());
- tx.delegateVotingPowerTo(op.getAddress(), drep);
- }
-
- private static void applyCreateVote(Tx tx, TxOperation op) {
- if (op.getVoterType() == null) {
- throw new IllegalArgumentException("create_vote requires 'voter_type'");
- }
- if (op.getVoterHash() == null) {
- throw new IllegalArgumentException("create_vote requires 'voter_hash'");
- }
- if (op.getGovActionTxHash() == null) {
- throw new IllegalArgumentException("create_vote requires 'gov_action_tx_hash'");
- }
- if (op.getGovActionIndex() == null) {
- throw new IllegalArgumentException("create_vote requires 'gov_action_index'");
- }
- if (op.getVote() == null) {
- throw new IllegalArgumentException("create_vote requires 'vote'");
- }
-
- Voter voter = parseVoter(op.getVoterType(), op.getVoterHash());
- GovActionId govActionId = new GovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- Vote vote = parseVote(op.getVote());
- Anchor anchor = parseAnchor(op.getAnchorUrl(), op.getAnchorDataHash());
-
- if (anchor != null) {
- tx.createVote(voter, govActionId, vote, anchor);
- } else {
- tx.createVote(voter, govActionId, vote);
- }
- }
-
- // --- Governance proposals ---
-
- private static void applyCreateProposal(Tx tx, TxOperation op) {
- if (op.getGovActionType() == null) {
- throw new IllegalArgumentException("create_proposal requires 'gov_action_type'");
- }
- if (op.getReturnAddress() == null) {
- throw new IllegalArgumentException("create_proposal requires 'return_address'");
- }
-
- Anchor anchor = parseAnchor(op.getAnchorUrl(), op.getAnchorDataHash());
-
- switch (op.getGovActionType().toLowerCase()) {
- case "info_action": {
- InfoAction action = InfoAction.builder().build();
- tx.createProposal(action, op.getReturnAddress(), anchor);
- break;
- }
- case "treasury_withdrawals": {
- TreasuryWithdrawalsAction action = TreasuryWithdrawalsAction.builder().build();
- if (op.getWithdrawals() != null) {
- for (Map w : op.getWithdrawals()) {
- String rewardAddr = w.get("reward_address");
- String amt = w.get("amount");
- if (rewardAddr == null || amt == null) {
- throw new IllegalArgumentException(
- "Each withdrawal requires 'reward_address' and 'amount'");
- }
- action.addWithdrawal(new Withdrawal(rewardAddr, new BigInteger(amt)));
- }
- }
- tx.createProposal(action, op.getReturnAddress(), anchor);
- break;
- }
- case "no_confidence": {
- NoConfidence.NoConfidenceBuilder builder = NoConfidence.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor);
- break;
- }
- case "update_committee": {
- UpdateCommittee.UpdateCommitteeBuilder builder = UpdateCommittee.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- if (op.getMembersToRemove() != null) {
- Set removals = new LinkedHashSet<>();
- for (Map m : op.getMembersToRemove()) {
- removals.add(parseCredential(m.get("hash"), m.get("type")));
- }
- builder.membersForRemoval(removals);
- }
- if (op.getNewMembers() != null) {
- Map newMembersMap = new LinkedHashMap<>();
- for (Map m : op.getNewMembers()) {
- Credential cred = parseCredential((String) m.get("hash"), (String) m.get("type"));
- int epoch = ((Number) m.get("epoch")).intValue();
- newMembersMap.put(cred, epoch);
- }
- builder.newMembersAndTerms(newMembersMap);
- }
- if (op.getQuorumNumerator() != null && op.getQuorumDenominator() != null) {
- builder.quorumThreshold(new UnitInterval(
- new BigInteger(op.getQuorumNumerator()),
- new BigInteger(op.getQuorumDenominator())));
- }
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor);
- break;
- }
- case "new_constitution": {
- NewConstitution.NewConstitutionBuilder builder = NewConstitution.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- Anchor constitutionAnchor = parseAnchor(op.getConstitutionAnchorUrl(), op.getConstitutionAnchorDataHash());
- Constitution.ConstitutionBuilder cBuilder = Constitution.builder();
- if (constitutionAnchor != null) cBuilder.anchor(constitutionAnchor);
- if (op.getConstitutionScriptHash() != null) cBuilder.scripthash(op.getConstitutionScriptHash());
- builder.constitution(cBuilder.build());
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor);
- break;
- }
- case "hard_fork_initiation": {
- HardForkInitiationAction.HardForkInitiationActionBuilder builder = HardForkInitiationAction.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- if (op.getProtocolVersionMajor() == null || op.getProtocolVersionMinor() == null) {
- throw new IllegalArgumentException(
- "hard_fork_initiation requires 'protocol_version_major' and 'protocol_version_minor'");
- }
- builder.protocolVersion(new ProtocolVersion(op.getProtocolVersionMajor(), op.getProtocolVersionMinor()));
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor);
- break;
- }
- case "parameter_change": {
- ParameterChangeAction.ParameterChangeActionBuilder builder = ParameterChangeAction.builder();
- GovActionId prevId = parsePrevGovActionId(op.getGovActionTxHash(), op.getGovActionIndex());
- if (prevId != null) builder.prevGovActionId(prevId);
- if (op.getPolicyHash() != null && !op.getPolicyHash().isEmpty()) {
- builder.policyHash(HexUtil.decodeHexString(op.getPolicyHash()));
- }
- // ProtocolParamUpdate is complex — pass as CBOR hex if provided
- if (op.getProtocolParamUpdateJson() != null && !op.getProtocolParamUpdateJson().isEmpty()) {
- throw new IllegalArgumentException(
- "parameter_change protocol_param_update_json is not yet supported. " +
- "Use the raw transaction builder for parameter change proposals.");
- }
- tx.createProposal(builder.build(), op.getReturnAddress(), anchor);
- break;
- }
- default:
- throw new IllegalArgumentException(
- "Unsupported gov_action_type: " + op.getGovActionType());
- }
- }
-
- // --- Pool operations (Gap 3) ---
-
- private static void applyRegisterPool(Tx tx, TxOperation op) {
- tx.registerPool(buildPoolRegistration(op));
- }
-
- private static void applyUpdatePool(Tx tx, TxOperation op) {
- tx.updatePool(buildPoolRegistration(op));
- }
-
- private static PoolRegistration buildPoolRegistration(TxOperation op) {
- if (op.getOperator() == null) throw new IllegalArgumentException("Pool operation requires 'operator'");
- if (op.getVrfKeyHash() == null) throw new IllegalArgumentException("Pool operation requires 'vrf_key_hash'");
- if (op.getPledge() == null) throw new IllegalArgumentException("Pool operation requires 'pledge'");
- if (op.getCost() == null) throw new IllegalArgumentException("Pool operation requires 'cost'");
- if (op.getMarginNumerator() == null || op.getMarginDenominator() == null) {
- throw new IllegalArgumentException("Pool operation requires 'margin_numerator' and 'margin_denominator'");
- }
- if (op.getRewardAddress() == null) throw new IllegalArgumentException("Pool operation requires 'reward_address'");
- if (op.getPoolOwners() == null || op.getPoolOwners().isEmpty()) {
- throw new IllegalArgumentException("Pool operation requires 'pool_owners'");
- }
-
- PoolRegistration.PoolRegistrationBuilder builder = PoolRegistration.builder()
- .operator(HexUtil.decodeHexString(op.getOperator()))
- .vrfKeyHash(HexUtil.decodeHexString(op.getVrfKeyHash()))
- .pledge(new BigInteger(op.getPledge()))
- .cost(new BigInteger(op.getCost()))
- .margin(new UnitInterval(new BigInteger(op.getMarginNumerator()), new BigInteger(op.getMarginDenominator())))
- .rewardAccount(op.getRewardAddress())
- .poolOwners(new LinkedHashSet<>(op.getPoolOwners()));
-
- if (op.getRelays() != null) {
- builder.relays(parseRelays(op.getRelays()));
- }
- if (op.getPoolMetadataUrl() != null) {
- builder.poolMetadataUrl(op.getPoolMetadataUrl());
- }
- if (op.getPoolMetadataHash() != null) {
- builder.poolMetadataHash(op.getPoolMetadataHash());
- }
-
- return builder.build();
- }
-
- @SuppressWarnings("unchecked")
- private static List parseRelays(List> relayList) {
- List relays = new ArrayList<>();
- for (Map r : relayList) {
- String type = (String) r.get("type");
- if (type == null) throw new IllegalArgumentException("Relay requires 'type'");
- switch (type.toLowerCase()) {
- case "single_host_addr": {
- SingleHostAddr.SingleHostAddrBuilder b = SingleHostAddr.builder();
- if (r.get("port") != null) b.port(((Number) r.get("port")).intValue());
- if (r.get("ipv4") != null) {
- try {
- b.ipv4((Inet4Address) Inet4Address.getByName((String) r.get("ipv4")));
- } catch (UnknownHostException e) {
- throw new IllegalArgumentException("Invalid ipv4: " + r.get("ipv4"), e);
- }
- }
- if (r.get("ipv6") != null) {
- try {
- b.ipv6((Inet6Address) Inet6Address.getByName((String) r.get("ipv6")));
- } catch (UnknownHostException e) {
- throw new IllegalArgumentException("Invalid ipv6: " + r.get("ipv6"), e);
- }
- }
- relays.add(b.build());
- break;
- }
- case "single_host_name": {
- SingleHostName.SingleHostNameBuilder b = SingleHostName.builder();
- if (r.get("port") != null) b.port(((Number) r.get("port")).intValue());
- b.dnsName((String) r.get("dns_name"));
- relays.add(b.build());
- break;
- }
- case "multi_host_name": {
- relays.add(MultiHostName.builder().dnsName((String) r.get("dns_name")).build());
- break;
- }
- default:
- throw new IllegalArgumentException("Unknown relay type: " + type);
- }
- }
- return relays;
- }
-
- private static void applyRetirePool(Tx tx, TxOperation op) {
- if (op.getPoolId() == null) throw new IllegalArgumentException("retire_pool requires 'pool_id'");
- if (op.getEpoch() == null) throw new IllegalArgumentException("retire_pool requires 'epoch'");
- tx.retirePool(op.getPoolId(), op.getEpoch());
- }
-
- // --- Treasury donation (Gap 4) ---
-
- private static void applyDonateToTreasury(Tx tx, TxOperation op) {
- if (op.getTreasuryValue() == null) {
- throw new IllegalArgumentException("donate_to_treasury requires 'treasury_value'");
- }
- if (op.getDonationAmount() == null) {
- throw new IllegalArgumentException("donate_to_treasury requires 'donation_amount'");
- }
- tx.donateToTreasury(new BigInteger(op.getTreasuryValue()), new BigInteger(op.getDonationAmount()));
- }
-
- // --- Attach native script standalone (Gap 5) ---
-
- private static void applyAttachNativeScript(Tx tx, TxOperation op) {
- if (op.getScriptJson() == null) {
- throw new IllegalArgumentException("attach_native_script requires 'script_json'");
- }
- NativeScript script;
- try {
- script = NativeScript.deserializeJson(op.getScriptJson());
- } catch (Exception e) {
- throw new IllegalArgumentException("Invalid native script JSON: " + e.getMessage(), e);
- }
- tx.attachNativeScript(script);
- }
-
- // --- Helper methods ---
-
- private static GovActionId parsePrevGovActionId(String txHash, Integer index) {
- if (txHash == null || txHash.isEmpty()) return null;
- return new GovActionId(txHash, index != null ? index : 0);
- }
-
- private static Script parseScriptRef(String cborHex, String scriptRefType) {
- if (scriptRefType == null || scriptRefType.isEmpty()) {
- throw new IllegalArgumentException("script_ref_type is required when script_ref_cbor_hex is provided. " +
- "Supported: plutus_v1, plutus_v2, plutus_v3");
- }
- return switch (scriptRefType.toLowerCase()) {
- case "plutus_v1" -> PlutusV1Script.builder().cborHex(cborHex).build();
- case "plutus_v2" -> PlutusV2Script.builder().cborHex(cborHex).build();
- case "plutus_v3" -> PlutusV3Script.builder().cborHex(cborHex).build();
- default -> throw new IllegalArgumentException("Unsupported script_ref_type: " + scriptRefType
- + ". Supported: plutus_v1, plutus_v2, plutus_v3");
- };
- }
-
- private static Credential parseCredential(String hash, String type) {
- if ("script".equalsIgnoreCase(type)) {
- return Credential.fromScript(hash);
- }
- return Credential.fromKey(hash);
- }
-
- private static Anchor parseAnchor(String url, String dataHash) {
- if (url == null || dataHash == null) return null;
- return Anchor.builder()
- .anchorUrl(url)
- .anchorDataHash(HexUtil.decodeHexString(dataHash))
- .build();
- }
-
- private static DRep parseDRep(String drepType, String drepHash) {
- return switch (drepType.toLowerCase()) {
- case "abstain" -> DRep.abstain();
- case "no_confidence" -> DRep.noConfidence();
- case "script_hash" -> DRep.scriptHash(drepHash);
- default -> DRep.addrKeyHash(drepHash); // key_hash
- };
- }
-
- private static Vote parseVote(String voteStr) {
- return switch (voteStr.toLowerCase()) {
- case "yes" -> Vote.YES;
- case "no" -> Vote.NO;
- case "abstain" -> Vote.ABSTAIN;
- default -> throw new IllegalArgumentException("Invalid vote value: " + voteStr
- + ". Must be 'yes', 'no', or 'abstain'");
- };
- }
-
- private static Voter parseVoter(String voterType, String voterHash) {
- Credential credential = Credential.fromKey(voterHash);
- VoterType type = switch (voterType.toLowerCase()) {
- case "drep_key_hash" -> VoterType.DREP_KEY_HASH;
- case "drep_script_hash" -> {
- credential = Credential.fromScript(voterHash);
- yield VoterType.DREP_SCRIPT_HASH;
- }
- case "staking_pool_key_hash" -> VoterType.STAKING_POOL_KEY_HASH;
- case "constitutional_committee_hot_key_hash" ->
- VoterType.CONSTITUTIONAL_COMMITTEE_HOT_KEY_HASH;
- case "constitutional_committee_hot_script_hash" -> {
- credential = Credential.fromScript(voterHash);
- yield VoterType.CONSTITUTIONAL_COMMITTEE_HOT_SCRIPT_HASH;
- }
- default -> throw new IllegalArgumentException("Invalid voter_type: " + voterType);
- };
- return new Voter(type, credential);
- }
-}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciProtocolParamsSupplier.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciProtocolParamsSupplier.java
deleted file mode 100644
index c426807..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciProtocolParamsSupplier.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.bloxbean.cardano.client.api.ProtocolParamsSupplier;
-import com.bloxbean.cardano.client.api.model.ProtocolParams;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class YaciProtocolParamsSupplier implements ProtocolParamsSupplier {
-
- private final HttpClient httpClient;
- private final String baseUrl;
- private final ObjectMapper mapper;
-
- public YaciProtocolParamsSupplier(String baseUrl) {
- this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
- this.httpClient = HttpClient.newHttpClient();
- this.mapper = new ObjectMapper();
- }
-
- @Override
- public ProtocolParams getProtocolParams() {
- String url = baseUrl + "/epochs/parameters";
-
- try {
- HttpRequest req = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .GET()
- .build();
- HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
-
- if (resp.statusCode() != 200) {
- throw new RuntimeException("Failed to fetch protocol params: HTTP " + resp.statusCode());
- }
-
- return mapper.readValue(resp.body(), ProtocolParams.class);
- } catch (RuntimeException e) {
- throw e;
- } catch (Exception e) {
- throw new RuntimeException("Failed to fetch protocol params from provider: " + e.getMessage(), e);
- }
- }
-}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciTransactionEvaluator.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciTransactionEvaluator.java
deleted file mode 100644
index 9b827d1..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciTransactionEvaluator.java
+++ /dev/null
@@ -1,134 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.bloxbean.cardano.client.api.TransactionEvaluator;
-import com.bloxbean.cardano.client.api.exception.ApiException;
-import com.bloxbean.cardano.client.api.model.EvaluationResult;
-import com.bloxbean.cardano.client.api.model.Result;
-import com.bloxbean.cardano.client.api.model.Utxo;
-import com.bloxbean.cardano.client.plutus.spec.ExUnits;
-import com.bloxbean.cardano.client.plutus.spec.RedeemerTag;
-import com.bloxbean.cardano.client.util.HexUtil;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import java.math.BigInteger;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * TransactionEvaluator that uses Yaci Store / Blockfrost-compatible
- * evaluate endpoint for script cost evaluation.
- *
- * POST {baseUrl}/utils/txs/evaluate
- * Content-Type: application/cbor
- * Body: hex-encoded transaction CBOR
- *
- * Response: Ogmios JSON format with EvaluationResult map.
- */
-public class YaciTransactionEvaluator implements TransactionEvaluator {
-
- private final String baseUrl;
- private final HttpClient httpClient;
- private final ObjectMapper objectMapper;
-
- public YaciTransactionEvaluator(String baseUrl) {
- this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
- this.httpClient = HttpClient.newHttpClient();
- this.objectMapper = new ObjectMapper();
- }
-
- @Override
- public Result> evaluateTx(byte[] cbor, Set inputUtxos) throws ApiException {
- return evaluateTx(cbor);
- }
-
- @Override
- public Result> evaluateTx(byte[] cbor) throws ApiException {
- try {
- String txCborHex = HexUtil.encodeHexString(cbor);
- String url = baseUrl + "/utils/txs/evaluate";
-
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .header("Content-Type", "application/cbor")
- .POST(HttpRequest.BodyPublishers.ofString(txCborHex))
- .build();
-
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() != 200) {
- return Result.>error("Evaluation failed: " + response.body())
- .code(response.statusCode());
- }
-
- List results = parseResponse(response.body());
- return Result.>success(response.body())
- .code(200)
- .withValue(results);
-
- } catch (Exception e) {
- throw new ApiException("Failed to evaluate transaction: " + e.getMessage(), e);
- }
- }
-
- private List parseResponse(String responseBody) throws Exception {
- List results = new ArrayList<>();
- JsonNode root = objectMapper.readTree(responseBody);
-
- // Ogmios format: { "type": "jsonwsp/response", "result": { "EvaluationResult": { "spend:0": {...}, ... } } }
- JsonNode evalResult = root.path("result").path("EvaluationResult");
- if (evalResult.isMissingNode()) {
- // Try alternative format: direct result
- evalResult = root.path("result");
- }
- if (evalResult.isMissingNode() || !evalResult.isObject()) {
- return results;
- }
-
- Iterator> fields = evalResult.fields();
- while (fields.hasNext()) {
- Map.Entry entry = fields.next();
- String key = entry.getKey(); // e.g., "spend:0", "mint:0"
- JsonNode value = entry.getValue();
-
- String[] parts = key.split(":");
- if (parts.length != 2) continue;
-
- RedeemerTag tag = parseRedeemerTag(parts[0]);
- int index = Integer.parseInt(parts[1]);
-
- long memory = value.path("memory").asLong();
- long steps = value.path("steps").asLong();
-
- results.add(EvaluationResult.builder()
- .redeemerTag(tag)
- .index(index)
- .exUnits(ExUnits.builder()
- .mem(BigInteger.valueOf(memory))
- .steps(BigInteger.valueOf(steps))
- .build())
- .build());
- }
-
- return results;
- }
-
- private RedeemerTag parseRedeemerTag(String tag) {
- return switch (tag.toLowerCase()) {
- case "spend" -> RedeemerTag.Spend;
- case "mint" -> RedeemerTag.Mint;
- case "cert", "publish", "certificate" -> RedeemerTag.Cert;
- case "reward", "withdrawal", "withdraw" -> RedeemerTag.Reward;
- case "vote", "voting" -> RedeemerTag.Voting;
- case "propose", "proposing" -> RedeemerTag.Proposing;
- default -> throw new IllegalArgumentException("Unknown redeemer tag: " + tag);
- };
- }
-}
diff --git a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciUtxoSupplier.java b/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciUtxoSupplier.java
deleted file mode 100644
index 9194a91..0000000
--- a/core/src/main/java/com/bloxbean/cardano/bridge/api/quicktx/YaciUtxoSupplier.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.bloxbean.cardano.bridge.api.quicktx;
-
-import com.bloxbean.cardano.client.api.UtxoSupplier;
-import com.bloxbean.cardano.client.api.common.OrderEnum;
-import com.bloxbean.cardano.client.api.model.Utxo;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
-public class YaciUtxoSupplier implements UtxoSupplier {
-
- private static final TypeReference> UTXO_LIST_TYPE = new TypeReference<>() {};
-
- private final HttpClient httpClient;
- private final String baseUrl;
- private final ObjectMapper mapper;
-
- public YaciUtxoSupplier(String baseUrl) {
- this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
- this.httpClient = HttpClient.newHttpClient();
- this.mapper = new ObjectMapper();
- }
-
- @Override
- public List getPage(String address, Integer nrOfItems, Integer page, OrderEnum order) {
- int pageNum = (page != null) ? page : 0;
- int count = (nrOfItems != null && nrOfItems > 0) ? nrOfItems : DEFAULT_NR_OF_ITEMS_TO_FETCH;
- String orderStr = (order == OrderEnum.desc) ? "desc" : "asc";
-
- String url = baseUrl + "/addresses/" + address + "/utxos"
- + "?page=" + pageNum + "&count=" + count + "&order=" + orderStr;
-
- try {
- HttpRequest req = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .GET()
- .build();
- HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
-
- if (resp.statusCode() != 200) {
- return Collections.emptyList();
- }
-
- return mapper.readValue(resp.body(), UTXO_LIST_TYPE);
- } catch (Exception e) {
- throw new RuntimeException("Failed to fetch UTXOs from provider: " + e.getMessage(), e);
- }
- }
-
- @Override
- public Optional getTxOutput(String txHash, int outputIndex) {
- // Not needed for coin selection; return empty
- return Optional.empty();
- }
-}
diff --git a/core/src/main/resources/META-INF/native-image/com.bloxbean.cardano/ccl-bridge/native-image.properties b/core/src/main/resources/META-INF/native-image/com.bloxbean.cardano/ccl-bridge/native-image.properties
index eec555e..42f02f8 100644
--- a/core/src/main/resources/META-INF/native-image/com.bloxbean.cardano/ccl-bridge/native-image.properties
+++ b/core/src/main/resources/META-INF/native-image/com.bloxbean.cardano/ccl-bridge/native-image.properties
@@ -1,4 +1,9 @@
+# -march=compatibility: emit only the baseline instruction set common to all CPUs of the target
+# architecture, instead of optimizing for the build machine's CPU (the default). Without it a lib
+# built on a modern CI runner can SIGILL on older / datacenter CPUs that lack newer instruction
+# sets (AVX2/AVX-512). This is the CPU-portability counterpart to the glibc-baseline build.
Args = --no-fallback \
+ -march=compatibility \
-H:+ReportExceptionStackTraces \
--initialize-at-build-time=org.slf4j \
--initialize-at-build-time=com.bloxbean.cardano.client.crypto \
diff --git a/core/src/main/resources/META-INF/native-image/com.bloxbean.cardano/ccl-bridge/reflect-config.json b/core/src/main/resources/META-INF/native-image/com.bloxbean.cardano/ccl-bridge/reflect-config.json
index 853d77b..35bea32 100644
--- a/core/src/main/resources/META-INF/native-image/com.bloxbean.cardano/ccl-bridge/reflect-config.json
+++ b/core/src/main/resources/META-INF/native-image/com.bloxbean.cardano/ccl-bridge/reflect-config.json
@@ -448,5 +448,533 @@
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.CollectFromIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.DRepDeregistrationIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.DRepRegistrationIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.DRepUpdateIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.DepositHelper",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.DonationIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.GovernanceProposalIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.MetadataIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.MintingIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.NativeScriptAttachmentIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.PaymentIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.PoolRegistrationIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.PoolRetirementIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.ReferenceInputIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.ScriptCollectFromIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.ScriptMintingIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.ScriptValidatorAttachmentIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.StakeDelegationIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.StakeDeregistrationIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.StakeRegistrationIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.StakeWithdrawalIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.TxInputIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.TxIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.TxScriptAttachmentIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.UtxoRef",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.VotingDelegationIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.intent.VotingIntent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.MetadataDeserializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.MetadataSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.PlaceholderMetadata",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.PlutusDataYamlUtil",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.TransactionDocument",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.TxPlan",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.VariableResolver",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.YamlSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.TransactionDocument$TxEntry",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.TransactionDocument$TxContent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.TransactionDocument$ScriptTxContent",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.TransactionDocument$TxContext",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.quicktx.serialization.TransactionDocument$SignerRef",
+ "allDeclaredConstructors": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.RedeemerTag",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.PlutusScript",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.Language",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.ExUnitPrices",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.CostMdls",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.CostModel",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.blueprint.model.PlutusVersion",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.BigIntDataJsonDeserializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.BigIntDataJsonSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.BytesDataJsonDeserializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.BytesDataJsonSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.ConstrDataJsonDeserializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.ConstrDataJsonSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.ListDataJsonDeserializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.ListDataJsonSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.MapDataJsonDeserializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.MapDataJsonSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.PlutusDataJsonConverter",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.plutus.spec.serializers.PlutusDataJsonKeys",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.util.serializers.ByteArrayToHexSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.util.serializers.HexToByteArrayDeserializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.util.serializers.InetAddressDeserializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.util.serializers.InetAddressSerializer",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.AuthCommitteeHotCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.CertificateType",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.GenesisKeyDelegation",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.MirPot",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.MoveInstataneous",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.PoolRetirement",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.RegCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.RegDRepCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.Relay",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.ResignCommitteeColdCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakeCredType",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakeCredential",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakeDelegation",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakeDeregistration",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakePoolId",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakeRegDelegCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakeRegistration",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakeVoteDelegCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.StakeVoteRegDelegCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.UnregCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.UnregDRepCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.UpdateDRepCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.VoteDelegCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.transaction.spec.cert.VoteRegDelegCert",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.client.spec.UnitInterval",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
}
-]
+]
\ No newline at end of file
diff --git a/core/src/test/java/com/bloxbean/cardano/bridge/api/QuickTxApiTest.java b/core/src/test/java/com/bloxbean/cardano/bridge/api/QuickTxApiTest.java
index b296429..8229419 100644
--- a/core/src/test/java/com/bloxbean/cardano/bridge/api/QuickTxApiTest.java
+++ b/core/src/test/java/com/bloxbean/cardano/bridge/api/QuickTxApiTest.java
@@ -3,8 +3,19 @@
import com.bloxbean.cardano.bridge.api.quicktx.QuickTxService;
import com.bloxbean.cardano.client.account.Account;
import com.bloxbean.cardano.client.common.model.Networks;
+import com.bloxbean.cardano.client.plutus.spec.BigIntPlutusData;
+import com.bloxbean.cardano.client.plutus.spec.PlutusData;
+import com.bloxbean.cardano.client.plutus.spec.PlutusScript;
+import com.bloxbean.cardano.client.plutus.spec.PlutusV2Script;
+import com.bloxbean.cardano.client.quicktx.Tx;
+import com.bloxbean.cardano.client.quicktx.serialization.TxPlan;
+import com.bloxbean.cardano.client.quicktx.serialization.YamlSerializer;
+import com.bloxbean.cardano.client.transaction.spec.Asset;
+import com.bloxbean.cardano.client.transaction.spec.Transaction;
+import com.bloxbean.cardano.client.util.HexUtil;
+
+import java.math.BigInteger;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -14,1686 +25,195 @@
import static org.junit.jupiter.api.Assertions.*;
+/**
+ * Builds unsigned transactions from TxPlan YAML, fully offline, with static UTXOs + protocol params.
+ */
class QuickTxApiTest {
private static final String TEST_MNEMONIC =
"test walk nut penalty hip pave soap entry language right filter choice";
+ private static final String FAKE_TX_HASH = "a".repeat(64);
- private final ObjectMapper mapper = new ObjectMapper();
private final QuickTxService service = new QuickTxService();
- private String protocolParamsJson;
- // Generate deterministic addresses from test mnemonic
+ private String protocolParamsJson;
private String sender;
- private String sender2;
private String receiver1;
private String receiver2;
@BeforeEach
void setUp() throws IOException {
- InputStream is = getClass().getClassLoader().getResourceAsStream("protocol-params.json");
- protocolParamsJson = new String(is.readAllBytes(), StandardCharsets.UTF_8);
-
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- sender = senderAccount.baseAddress();
- sender2 = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 1).baseAddress();
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream("protocol-params.json")) {
+ protocolParamsJson = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ sender = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0).baseAddress();
receiver1 = new Account(Networks.testnet()).baseAddress();
receiver2 = new Account(Networks.testnet()).baseAddress();
}
- @Test
- void testSimpleAdaPayment() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]}
- ]
- """.formatted(receiver1));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
-
- assertNotNull(json.get("tx_cbor").asText());
- assertFalse(json.get("tx_cbor").asText().isEmpty());
- assertNotNull(json.get("tx_hash").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- assertTrue(Long.parseLong(json.get("fee").asText()) > 0);
- }
-
- @Test
- void testMultiplePayments() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]},
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "3000000"}]}
- ]
- """.formatted(receiver1, receiver2));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
-
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testWithMetadata() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]},
- {"type": "attach_metadata", "label": 674,
- "metadata": {"msg": ["Hello from CCL Bridge"]}}
- ]
- """.formatted(receiver1));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
-
- assertNotNull(json.get("tx_cbor").asText());
- // Fee should be slightly higher with metadata
- assertTrue(Long.parseLong(json.get("fee").asText()) > 0);
- }
-
- @Test
- void testWithChangeAddress() throws Exception {
- // change_address = sender is the typical case; change goes back to sender
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]}
- ],
- "from": "%s",
- "change_address": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, sender, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- }
-
- @Test
- void testWithValidityInterval() throws Exception {
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "validity": {"valid_from": 1000, "valid_to": 50000},
- "signer_count": 1
- }
- """.formatted(receiver1, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- }
-
- @Test
- void testInsufficientFunds() {
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "200000000"}]}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "1000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, sender, sender, protocolParamsJson);
-
- assertThrows(Exception.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testMissingOperations() {
- String spec = """
- {
- "operations": [],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaa", "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "1000000"}]}
- ],
- "protocol_params": %s
- }
- """.formatted(sender, sender, protocolParamsJson);
-
- assertThrows(IllegalArgumentException.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testMissingFrom() {
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]}
- ],
- "utxos": [
- {"tx_hash": "aaaa", "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "1000000"}]}
- ],
- "protocol_params": %s
- }
- """.formatted(receiver1, sender, protocolParamsJson);
-
- assertThrows(IllegalArgumentException.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testMissingUtxos() {
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]}
- ],
- "from": "%s",
- "protocol_params": %s
- }
- """.formatted(receiver1, sender, protocolParamsJson);
-
- assertThrows(IllegalArgumentException.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testProviderModeSkipsUtxoValidation() {
- // Provider mode should not require utxos or protocol_params in the spec
- // (it will fail at HTTP fetch, not at validation)
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]}
- ],
- "from": "%s",
- "provider": {"name": "yaci", "url": "http://localhost:9999/api/v1"}
- }
- """.formatted(receiver1, sender);
-
- // Should fail with connection error, NOT IllegalArgumentException
- Exception ex = assertThrows(Exception.class, () -> service.buildTransaction(spec));
- assertFalse(ex instanceof IllegalArgumentException,
- "Should not fail validation — expected HTTP error, got: " + ex.getMessage());
- }
-
- @Test
- void testProviderModeInvalidProviderName() {
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]}
- ],
- "from": "%s",
- "provider": {"name": "unknown", "url": "http://localhost:9999/api/v1"}
- }
- """.formatted(receiver1, sender);
-
- IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
- () -> service.buildTransaction(spec));
- assertTrue(ex.getMessage().contains("Unsupported provider"));
- }
-
- @Test
- void testProviderModeMissingName() {
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]}
- ],
- "from": "%s",
- "provider": {"url": "http://localhost:9999/api/v1"}
- }
- """.formatted(receiver1, sender);
-
- IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
- () -> service.buildTransaction(spec));
- assertTrue(ex.getMessage().contains("'name' is required"));
- }
-
- @Test
- void testMissingProtocolParams() {
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaa", "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "1000000"}]}
- ]
- }
- """.formatted(receiver1, sender, sender);
-
- assertThrows(IllegalArgumentException.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testMultiAssetPayment() throws Exception {
- String policyId = "a".repeat(56);
- String unit = policyId + "546f6b656e"; // "Token" hex
- String spec = """
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [
- {"unit": "lovelace", "quantity": "2000000"},
- {"unit": "%s", "quantity": "100"}
- ]}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [
- {"unit": "lovelace", "quantity": "100000000"},
- {"unit": "%s", "quantity": "500"}
- ]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, unit, sender, sender, unit, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- Compose (multi-Tx) tests ---
-
- @Test
- void testComposeTwoSenders() throws Exception {
- String spec = """
- {
- "transactions": [
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]}
- ]
- },
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "3000000"}]}
- ]
- }
- ],
- "fee_payer": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]},
- {"tx_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s
- }
- """.formatted(sender, receiver1, sender2, receiver2,
- sender, sender, sender2, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
-
- assertNotNull(json.get("tx_cbor").asText());
- assertFalse(json.get("tx_cbor").asText().isEmpty());
- assertEquals(64, json.get("tx_hash").asText().length());
- assertTrue(Long.parseLong(json.get("fee").asText()) > 0);
- }
-
- @Test
- void testComposeMissingFeePayer() {
- String spec = """
- {
- "transactions": [
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]}
- ]
- },
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "3000000"}]}
- ]
- }
- ],
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s
- }
- """.formatted(sender, receiver1, sender2, receiver2,
- sender, protocolParamsJson);
-
- assertThrows(IllegalArgumentException.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testComposeMissingFromInItem() {
- String spec = """
- {
- "transactions": [
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]}
- ]
- },
- {
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "3000000"}]}
- ]
- }
- ],
- "fee_payer": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s
- }
- """.formatted(sender, receiver1, receiver2,
- sender, sender, protocolParamsJson);
-
- IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
- () -> service.buildTransaction(spec));
- assertTrue(ex.getMessage().contains("transactions[1]"));
- }
-
- @Test
- void testComposeWithMetadata() throws Exception {
- String spec = """
- {
- "transactions": [
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]},
- {"type": "attach_metadata", "label": 674,
- "metadata": {"msg": ["Compose test"]}}
- ]
- },
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "3000000"}]}
- ]
- }
- ],
- "fee_payer": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]},
- {"tx_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s
- }
- """.formatted(sender, receiver1, sender2, receiver2,
- sender, sender, sender2, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertTrue(Long.parseLong(json.get("fee").asText()) > 0);
- }
-
- @Test
- void testComposeSignerCountDefault() throws Exception {
- // When signer_count is omitted in compose mode, it defaults to number of transactions
- String spec = """
- {
- "transactions": [
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]}
- ]
- },
- {
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "3000000"}]}
- ]
- }
- ],
- "fee_payer": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]},
- {"tx_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s
- }
- """.formatted(sender, receiver1, sender2, receiver2,
- sender, sender, sender2, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- // Should build successfully with default signer count = 2
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- Staking tests ---
-
- @Test
- void testRegisterStakeAddress() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "register_stake_address", "address": "%s"}
- ]
- """.formatted(sender));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertFalse(json.get("tx_cbor").asText().isEmpty());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testDeregisterStakeAddress() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "deregister_stake_address", "address": "%s"}
- ]
- """.formatted(sender));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testDeregisterStakeAddressWithRefund() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "deregister_stake_address", "address": "%s",
- "refund_address": "%s"}
- ]
- """.formatted(sender, receiver1));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testDelegateTo() throws Exception {
- // pool1... bech32 format; using a fake but valid-looking pool ID
- String poolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy";
- String spec = buildSpec("""
- "operations": [
- {"type": "delegate_to", "address": "%s",
- "pool_id": "%s"}
- ]
- """.formatted(sender, poolId));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testWithdraw() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
-
- String spec = buildSpec("""
- "operations": [
- {"type": "withdraw", "reward_address": "%s",
- "amount": "5000000"}
- ]
- """.formatted(stakeAddr));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testWithdrawWithReceiver() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
-
- String spec = buildSpec("""
- "operations": [
- {"type": "withdraw", "reward_address": "%s",
- "amount": "5000000", "receiver": "%s"}
- ]
- """.formatted(stakeAddr, receiver1));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- DRep tests ---
-
- @Test
- void testRegisterDRep() throws Exception {
- String credentialHash = "ab".repeat(28);
- String spec = buildSpec("""
- "operations": [
- {"type": "register_drep", "credential_hash": "%s",
- "credential_type": "key"}
- ]
- """.formatted(credentialHash));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testRegisterDRepWithAnchor() throws Exception {
- String credentialHash = "ab".repeat(28);
- String dataHash = "cd".repeat(32);
- String spec = buildSpec("""
- "operations": [
- {"type": "register_drep", "credential_hash": "%s",
- "credential_type": "key",
- "anchor_url": "https://example.com/drep.json",
- "anchor_data_hash": "%s"}
- ]
- """.formatted(credentialHash, dataHash));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testUnregisterDRep() throws Exception {
- String credentialHash = "ab".repeat(28);
- String spec = buildSpec("""
- "operations": [
- {"type": "unregister_drep", "credential_hash": "%s",
- "credential_type": "key"}
- ]
- """.formatted(credentialHash));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testUpdateDRep() throws Exception {
- String credentialHash = "ab".repeat(28);
- String dataHash = "cd".repeat(32);
- String spec = buildSpec("""
- "operations": [
- {"type": "update_drep", "credential_hash": "%s",
- "credential_type": "key",
- "anchor_url": "https://example.com/drep-v2.json",
- "anchor_data_hash": "%s"}
- ]
- """.formatted(credentialHash, dataHash));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- Voting tests ---
-
- @Test
- void testDelegateVotingPowerToKeyHash() throws Exception {
- String drepHash = "ab".repeat(28);
- String spec = buildSpec("""
- "operations": [
- {"type": "delegate_voting_power_to", "address": "%s",
- "drep_type": "key_hash", "drep_hash": "%s"}
- ]
- """.formatted(sender, drepHash));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testDelegateVotingPowerToAbstain() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "delegate_voting_power_to", "address": "%s",
- "drep_type": "abstain"}
- ]
- """.formatted(sender));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testCreateVote() throws Exception {
- String voterHash = "ab".repeat(28);
- String govTxHash = "cd".repeat(32);
- String spec = buildSpec("""
- "operations": [
- {"type": "create_vote", "voter_type": "drep_key_hash",
- "voter_hash": "%s",
- "gov_action_tx_hash": "%s",
- "gov_action_index": 0, "vote": "yes"}
- ]
- """.formatted(voterHash, govTxHash));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testCreateVoteWithAnchor() throws Exception {
- String voterHash = "ab".repeat(28);
- String govTxHash = "cd".repeat(32);
- String anchorDataHash = "ef".repeat(32);
- String spec = buildSpec("""
- "operations": [
- {"type": "create_vote", "voter_type": "drep_key_hash",
- "voter_hash": "%s",
- "gov_action_tx_hash": "%s",
- "gov_action_index": 0, "vote": "no",
- "anchor_url": "https://example.com/rationale.json",
- "anchor_data_hash": "%s"}
- ]
- """.formatted(voterHash, govTxHash, anchorDataHash));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- Governance proposal tests ---
-
- @Test
- void testCreateInfoActionProposal() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String anchorDataHash = "ab".repeat(32);
-
- // gov_action_deposit = 1000 ADA, so we need a large UTXO
- String spec = """
- {
- "operations": [
- {"type": "create_proposal", "gov_action_type": "info_action",
- "return_address": "%s",
- "anchor_url": "https://example.com/proposal.json",
- "anchor_data_hash": "%s"}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(stakeAddr, anchorDataHash, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testCreateTreasuryWithdrawalsProposal() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String anchorDataHash = "ab".repeat(32);
-
- // gov_action_deposit = 1000 ADA, so we need a large UTXO
- String spec = """
- {
- "operations": [
- {"type": "create_proposal", "gov_action_type": "treasury_withdrawals",
- "return_address": "%s",
- "anchor_url": "https://example.com/proposal.json",
- "anchor_data_hash": "%s",
- "withdrawals": [
- {"reward_address": "%s", "amount": "1000000"}
- ]}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(stakeAddr, anchorDataHash, stakeAddr, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testCreateVoteInvalidVoteValue() {
- String voterHash = "ab".repeat(28);
- String govTxHash = "cd".repeat(32);
- String spec = buildSpec("""
- "operations": [
- {"type": "create_vote", "voter_type": "drep_key_hash",
- "voter_hash": "%s",
- "gov_action_tx_hash": "%s",
- "gov_action_index": 0, "vote": "maybe"}
- ]
- """.formatted(voterHash, govTxHash));
-
- assertThrows(Exception.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testUnsupportedGovActionType() {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String spec = buildSpec("""
- "operations": [
- {"type": "create_proposal", "gov_action_type": "unsupported_action",
- "return_address": "%s",
- "anchor_url": "https://example.com/proposal.json",
- "anchor_data_hash": "abcdef"}
- ]
- """.formatted(stakeAddr));
-
- assertThrows(Exception.class, () -> service.buildTransaction(spec));
- }
-
- // --- Gap 1: Reference Script on Outputs ---
-
- @Test
- void testPayToAddressWithRefScript() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}],
- "script_ref_cbor_hex": "%s",
- "script_ref_type": "plutus_v3"}
- ]
- """.formatted(receiver1, ALWAYS_SUCCEEDS_SCRIPT_CBOR));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertFalse(json.get("tx_cbor").asText().isEmpty());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testPayToContractWithRefScript() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "pay_to_contract", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}],
- "datum_cbor_hex": "%s",
- "script_ref_cbor_hex": "%s",
- "script_ref_type": "plutus_v3"}
- ]
- """.formatted(receiver1, SIMPLE_DATUM, ALWAYS_SUCCEEDS_SCRIPT_CBOR));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testPayToAddressRefScriptMissingType() {
- String spec = buildSpec("""
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}],
- "script_ref_cbor_hex": "%s"}
- ]
- """.formatted(receiver1, ALWAYS_SUCCEEDS_SCRIPT_CBOR));
-
- assertThrows(Exception.class, () -> service.buildTransaction(spec));
- }
-
- // --- Gap 2: Additional Governance Actions ---
-
- @Test
- void testCreateNoConfidenceProposal() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String anchorDataHash = "ab".repeat(32);
-
- String spec = """
- {
- "operations": [
- {"type": "create_proposal", "gov_action_type": "no_confidence",
- "return_address": "%s",
- "anchor_url": "https://example.com/no-confidence.json",
- "anchor_data_hash": "%s"}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(stakeAddr, anchorDataHash, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testCreateNoConfidenceWithPrevAction() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String anchorDataHash = "ab".repeat(32);
- String prevTxHash = "cc".repeat(32);
-
- String spec = """
- {
- "operations": [
- {"type": "create_proposal", "gov_action_type": "no_confidence",
- "return_address": "%s",
- "anchor_url": "https://example.com/no-confidence.json",
- "anchor_data_hash": "%s",
- "gov_action_tx_hash": "%s",
- "gov_action_index": 0}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(stakeAddr, anchorDataHash, prevTxHash, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testCreateUpdateCommitteeProposal() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String anchorDataHash = "ab".repeat(32);
- String memberHash = "cd".repeat(28);
-
- String spec = """
- {
- "operations": [
- {"type": "create_proposal", "gov_action_type": "update_committee",
- "return_address": "%s",
- "anchor_url": "https://example.com/committee.json",
- "anchor_data_hash": "%s",
- "members_to_remove": [{"hash": "%s", "type": "key"}],
- "new_members": [{"hash": "%s", "type": "key", "epoch": 500}],
- "quorum_numerator": "2",
- "quorum_denominator": "3"}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(stakeAddr, anchorDataHash, memberHash, memberHash, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testCreateNewConstitutionProposal() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String anchorDataHash = "ab".repeat(32);
- String constitutionDataHash = "ef".repeat(32);
-
- String spec = """
- {
- "operations": [
- {"type": "create_proposal", "gov_action_type": "new_constitution",
- "return_address": "%s",
- "anchor_url": "https://example.com/proposal.json",
- "anchor_data_hash": "%s",
- "constitution_anchor_url": "https://example.com/constitution.json",
- "constitution_anchor_data_hash": "%s"}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(stakeAddr, anchorDataHash, constitutionDataHash, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testCreateHardForkInitiationProposal() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String anchorDataHash = "ab".repeat(32);
-
- String spec = """
- {
- "operations": [
- {"type": "create_proposal", "gov_action_type": "hard_fork_initiation",
- "return_address": "%s",
- "anchor_url": "https://example.com/hardfork.json",
- "anchor_data_hash": "%s",
- "protocol_version_major": 10,
- "protocol_version_minor": 0}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(stakeAddr, anchorDataHash, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- Gap 3: Pool Operations ---
-
- @Test
- void testRegisterPool() throws Exception {
- String operatorHash = "ab".repeat(28);
- String vrfKeyHash = "cd".repeat(32);
- String ownerHash = "ab".repeat(28);
- // rewardAccount must be hex-encoded reward address bytes (not bech32)
- // e0 prefix = reward address type on testnet, then 28 bytes key hash
- String rewardAccountHex = "e0" + "ab".repeat(28);
-
- String spec = """
- {
- "operations": [
- {"type": "register_pool",
- "operator": "%s",
- "vrf_key_hash": "%s",
- "pledge": "100000000",
- "cost": "340000000",
- "margin_numerator": "1",
- "margin_denominator": "100",
- "reward_address": "%s",
- "pool_owners": ["%s"],
- "relays": [{"type": "single_host_addr", "port": 6000, "ipv4": "127.0.0.1"}],
- "pool_metadata_url": "https://example.com/pool.json",
- "pool_metadata_hash": "%s"}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "1000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(operatorHash, vrfKeyHash, rewardAccountHex, ownerHash,
- "ef".repeat(32), sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testRetirePool() throws Exception {
- String poolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy";
- String spec = buildSpec("""
- "operations": [
- {"type": "retire_pool", "pool_id": "%s", "epoch": 500}
- ]
- """.formatted(poolId));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- Gap 4: Treasury Donation ---
-
- @Test
- void testDonateToTreasury() throws Exception {
- String spec = buildSpec("""
- "operations": [
- {"type": "donate_to_treasury",
- "treasury_value": "10000000000",
- "donation_amount": "5000000"}
- ]
- """);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- Gap 5: Attach Native Script ---
-
- @Test
- void testAttachNativeScript() throws Exception {
- String keyHash = "ab".repeat(28);
- String spec = buildSpec("""
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]},
- {"type": "attach_native_script", "script_json": "{\\"type\\": \\"sig\\", \\"keyHash\\": \\"%s\\"}"}
- ]
- """.formatted(receiver1, keyHash));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- Gap 6: unregisterDRep with refundAmount ---
-
- @Test
- void testUnregisterDRepWithRefundAmount() throws Exception {
- String credentialHash = "ab".repeat(28);
- String spec = buildSpec("""
- "operations": [
- {"type": "unregister_drep", "credential_hash": "%s",
- "credential_type": "key",
- "refund_address": "%s",
- "refund_amount": "500000000"}
- ]
- """.formatted(credentialHash, receiver1));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- ScriptTx variants of new features ---
-
- @Test
- void testScriptTxPayToAddressWithRefScript() throws Exception {
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}],
- "script_ref_cbor_hex": "%s",
- "script_ref_type": "plutus_v3"}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, ALWAYS_SUCCEEDS_SCRIPT_CBOR, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testScriptTxDonateToTreasury() throws Exception {
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {"type": "donate_to_treasury",
- "treasury_value": "10000000000",
- "donation_amount": "5000000"},
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testScriptTxNoConfidenceProposal() throws Exception {
- Account senderAccount = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
- String stakeAddr = senderAccount.stakeAddress();
- String anchorDataHash = "ab".repeat(32);
-
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {"type": "create_proposal", "gov_action_type": "no_confidence",
- "return_address": "%s",
- "anchor_url": "https://example.com/no-confidence.json",
- "anchor_data_hash": "%s",
- "redeemer_cbor_hex": "%s"}
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(stakeAddr, anchorDataHash, SIMPLE_REDEEMER, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- // --- ScriptTx tests ---
-
- // Always-succeeds PlutusV3 script CBOR (from cardano-client-lib tests)
- private static final String ALWAYS_SUCCEEDS_SCRIPT_CBOR = "46450101002499";
- // Simple redeemer: ConstrPlutusData(0, []) = d87980 in CBOR
- private static final String SIMPLE_REDEEMER = "d87980";
- // Simple datum same as redeemer
- private static final String SIMPLE_DATUM = "d87980";
-
- @Test
- void testScriptTxCollectFromWithRedeemer() throws Exception {
- String scriptAddr = receiver1; // Use any address for test
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "collect_from",
- "collect_utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "10000000"}]}
- ],
- "redeemer_cbor_hex": "%s",
- "datum_cbor_hex": "%s"
- },
- {
- "type": "attach_spending_validator",
- "script_cbor_hex": "%s",
- "script_type": "plutus_v3"
- },
- {
- "type": "pay_to_address",
- "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]
- }
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(scriptAddr, SIMPLE_REDEEMER, SIMPLE_DATUM,
- ALWAYS_SUCCEEDS_SCRIPT_CBOR, receiver2, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertFalse(json.get("tx_cbor").asText().isEmpty());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testScriptTxReadFrom() throws Exception {
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "read_from",
- "reference_inputs": [
- {"tx_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "output_index": 0}
- ]
- },
- {
- "type": "pay_to_address",
- "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]
- }
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testScriptTxMintPlutusAssets() throws Exception {
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "mint_plutus_assets",
- "script_cbor_hex": "%s",
- "script_type": "plutus_v3",
- "assets": [{"name": "TestToken", "quantity": "100"}],
- "redeemer_cbor_hex": "%s",
- "receiver": "%s"
- },
- {
- "type": "pay_to_address",
- "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "2000000"}]
- }
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(ALWAYS_SUCCEEDS_SCRIPT_CBOR, SIMPLE_REDEEMER, receiver1,
- receiver1, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testScriptTxAttachValidators() throws Exception {
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "attach_spending_validator",
- "script_cbor_hex": "%s",
- "script_type": "plutus_v3"
- },
- {
- "type": "pay_to_address",
- "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]
- }
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(ALWAYS_SUCCEEDS_SCRIPT_CBOR, receiver1, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testScriptTxDelegateWithRedeemer() throws Exception {
- String poolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy";
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "delegate_to",
- "address": "%s",
- "pool_id": "%s",
- "redeemer_cbor_hex": "%s"
- }
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(sender, poolId, SIMPLE_REDEEMER, sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testComposeScriptTxWithTx() throws Exception {
- String spec = """
- {
- "transactions": [
- {
- "tx_type": "tx",
- "from": "%s",
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]}
- ]
- },
- {
- "tx_type": "script_tx",
- "from": "%s",
- "operations": [
- {
- "type": "read_from",
- "reference_inputs": [
- {"tx_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "output_index": 0}
- ]
- },
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "3000000"}]}
- ]
- }
- ],
- "fee_payer": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s
- }
- """.formatted(sender, receiver1, sender, receiver2,
- sender, sender, protocolParamsJson);
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- @Test
- void testScriptTxMissingScriptType() {
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "attach_spending_validator",
- "script_cbor_hex": "%s"
- }
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(ALWAYS_SUCCEEDS_SCRIPT_CBOR, sender, sender, protocolParamsJson);
-
- assertThrows(Exception.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testScriptTxInvalidRedeemer() {
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "collect_from",
- "collect_utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "10000000"}]}
- ],
- "redeemer_cbor_hex": "invalidhex"
- }
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, sender, sender, protocolParamsJson);
-
- assertThrows(Exception.class, () -> service.buildTransaction(spec));
- }
-
- @Test
- void testScriptTxMintAssetsNotAllowedInScriptTxMode() {
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "mint_assets",
- "script_json": "{}",
- "assets": [{"name": "Token", "quantity": "100"}],
- "receiver": "%s"
- }
- ],
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, sender, sender, protocolParamsJson);
-
- IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
- () -> service.buildTransaction(spec));
- assertTrue(ex.getMessage().contains("mint_plutus_assets"));
- }
-
- @Test
- void testScriptTxFromOptional() throws Exception {
- // ScriptTx mode allows 'from' to be omitted
- String spec = """
- {
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "pay_to_address",
- "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]
- }
- ],
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(receiver1, sender, protocolParamsJson);
-
- // Should not throw IllegalArgumentException for missing 'from'
- // but may fail during build since no from means no coin selection source
- // Just verify validation passes
- try {
- service.buildTransaction(spec);
- } catch (IllegalArgumentException e) {
- fail("Should not fail validation for missing 'from' in script_tx mode: " + e.getMessage());
- } catch (Exception e) {
- // Build failures are ok - we're testing validation
- }
- }
-
- @Test
- void testBackwardCompatibilityDefaultTxType() throws Exception {
- // Existing specs without tx_type should default to "tx" and work as before
- String spec = buildSpec("""
- "operations": [
- {"type": "pay_to_address", "address": "%s",
- "amounts": [{"unit": "lovelace", "quantity": "5000000"}]}
- ]
- """.formatted(receiver1));
-
- String result = service.buildTransaction(spec);
- JsonNode json = mapper.readTree(result);
- assertNotNull(json.get("tx_cbor").asText());
- assertEquals(64, json.get("tx_hash").asText().length());
- }
-
- /**
- * Helper to build a standard spec JSON with common defaults.
- */
- private String buildSpec(String operationsFragment) {
+ /** A single 100-ADA UTXO at {@code sender}, as a JSON array of the CCL Utxo model. */
+ private String utxos() {
return """
- {
- %s,
- "from": "%s",
- "utxos": [
- {"tx_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "output_index": 0, "address": "%s",
- "amount": [{"unit": "lovelace", "quantity": "100000000"}]}
- ],
- "protocol_params": %s,
- "signer_count": 1
- }
- """.formatted(operationsFragment, sender, sender, protocolParamsJson);
+ [{"tx_hash":"%s","output_index":0,"address":"%s",
+ "amount":[{"unit":"lovelace","quantity":"100000000"}]}]
+ """.formatted(FAKE_TX_HASH, sender);
+ }
+
+ private JsonNode build(String yaml) throws Exception {
+ // The build result is YAML now; parse it with CCL's YAML mapper.
+ return YamlSerializer.getYamlMapper()
+ .readTree(service.buildTransaction(yaml, utxos(), protocolParamsJson, null));
+ }
+
+ private static void assertBuilt(JsonNode result) {
+ assertFalse(result.get("tx_cbor").asText().isEmpty());
+ assertEquals(64, result.get("tx_hash").asText().length());
+ assertTrue(Long.parseLong(result.get("fee").asText()) > 0);
+ }
+
+ @Test
+ void simplePayment() throws Exception {
+ String yaml = """
+ version: 1.0
+ transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+ """.formatted(sender, receiver1);
+ assertBuilt(build(yaml));
+ }
+
+ @Test
+ void multiplePayments() throws Exception {
+ String yaml = """
+ version: 1.0
+ transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
+ """.formatted(sender, receiver1, receiver2);
+ assertBuilt(build(yaml));
+ }
+
+ @Test
+ void paymentWithMetadata() throws Exception {
+ // The metadata intent's value is a scalar string the deserializer auto-detects; JSON
+ // (starting with '{') is parsed via MetadataBuilder.metadataFromJson.
+ String yaml = """
+ version: 1.0
+ transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "2000000"
+ - type: metadata
+ metadata: '{"674": {"msg": "Hello from CCL Bridge"}}'
+ """.formatted(sender, receiver1);
+ JsonNode result = build(yaml);
+ assertBuilt(result);
+
+ // The metadata must actually be attached: the tx body carries an auxiliary data hash.
+ Transaction tx = Transaction.deserialize(HexUtil.decodeHexString(result.get("tx_cbor").asText()));
+ assertNotNull(tx.getBody().getAuxiliaryDataHash(), "metadata should set the auxiliary data hash");
+ }
+
+ @Test
+ void variableSubstitution() throws Exception {
+ String yaml = """
+ version: 1.0
+ variables:
+ to: %s
+ amount: "4000000"
+ transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: ${to}
+ amounts:
+ - unit: lovelace
+ quantity: ${amount}
+ """.formatted(receiver1, sender);
+ assertBuilt(build(yaml));
+ }
+
+ @Test
+ void insufficientFundsFails() {
+ String yaml = """
+ version: 1.0
+ transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "200000000"
+ """.formatted(sender, receiver1);
+ assertThrows(Exception.class, () -> service.buildTransaction(yaml, utxos(), protocolParamsJson, null));
+ }
+
+ // An always-succeeds Plutus V2 minting policy. The script is never executed offline — the
+ // StaticTransactionEvaluator stamps the caller-supplied execution units onto the redeemer —
+ // so any valid Plutus CBOR is enough to build the transaction.
+ private static final String ALWAYS_SUCCEEDS_V2 = "4e4d01000033222220051200120011";
+
+ /** Build a Plutus mint as a TxPlan YAML (generated by CCL so the script-intent shape is exact). */
+ private String mintScriptYaml() {
+ PlutusScript mintScript = PlutusV2Script.builder().cborHex(ALWAYS_SUCCEEDS_V2).build();
+ Asset asset = new Asset("TestToken", BigInteger.ONE);
+ PlutusData redeemer = BigIntPlutusData.of(0);
+ Tx tx = new Tx()
+ .mintAsset(mintScript, asset, redeemer, sender)
+ .from(sender);
+ return TxPlan.from(tx).feePayer(sender).toYaml();
+ }
+
+ @Test
+ void plutusMintWithSuppliedExecUnits() throws Exception {
+ String yaml = mintScriptYaml();
+ // One redeemer (the mint) → one ExUnits, supplied by the caller (as it would from Ogmios,
+ // Blockfrost, Aiken, Scalus, …).
+ String execUnits = "[{\"mem\": 2000000, \"steps\": 500000000}]";
+
+ JsonNode result = YamlSerializer.getYamlMapper()
+ .readTree(service.buildTransaction(yaml, utxos(), protocolParamsJson, execUnits));
+ assertBuilt(result);
+
+ // The built tx must carry the redeemer with exactly the supplied execution units.
+ Transaction tx = Transaction.deserialize(HexUtil.decodeHexString(result.get("tx_cbor").asText()));
+ assertNotNull(tx.getWitnessSet().getRedeemers());
+ assertEquals(1, tx.getWitnessSet().getRedeemers().size());
+ var exUnits = tx.getWitnessSet().getRedeemers().get(0).getExUnits();
+ assertEquals(BigInteger.valueOf(2000000), exUnits.getMem());
+ assertEquals(BigInteger.valueOf(500000000), exUnits.getSteps());
+ }
+
+ @Test
+ void plutusMintWithoutExecUnitsFails() {
+ String yaml = mintScriptYaml();
+ // No execution units → no offline evaluator runs the script → the build fails.
+ assertThrows(Exception.class,
+ () -> service.buildTransaction(yaml, utxos(), protocolParamsJson, null));
}
}
diff --git a/core/src/test/java/com/bloxbean/cardano/bridge/api/QuickTxIntentsTest.java b/core/src/test/java/com/bloxbean/cardano/bridge/api/QuickTxIntentsTest.java
new file mode 100644
index 0000000..f3d1f19
--- /dev/null
+++ b/core/src/test/java/com/bloxbean/cardano/bridge/api/QuickTxIntentsTest.java
@@ -0,0 +1,398 @@
+package com.bloxbean.cardano.bridge.api;
+
+import com.bloxbean.cardano.bridge.api.quicktx.QuickTxService;
+import com.bloxbean.cardano.client.account.Account;
+import com.bloxbean.cardano.client.address.Address;
+import com.bloxbean.cardano.client.address.AddressProvider;
+import com.bloxbean.cardano.client.address.Credential;
+import com.bloxbean.cardano.client.api.model.Amount;
+import com.bloxbean.cardano.client.api.model.Utxo;
+import com.bloxbean.cardano.client.common.model.Networks;
+import com.bloxbean.cardano.client.metadata.Metadata;
+import com.bloxbean.cardano.client.metadata.MetadataBuilder;
+import com.bloxbean.cardano.client.quicktx.AbstractTx;
+import com.bloxbean.cardano.client.plutus.spec.BigIntPlutusData;
+import com.bloxbean.cardano.client.plutus.spec.PlutusData;
+import com.bloxbean.cardano.client.plutus.spec.PlutusScript;
+import com.bloxbean.cardano.client.plutus.spec.PlutusV2Script;
+import com.bloxbean.cardano.client.quicktx.Tx;
+import com.bloxbean.cardano.client.quicktx.serialization.TxPlan;
+import com.bloxbean.cardano.client.transaction.spec.governance.Anchor;
+import com.bloxbean.cardano.client.transaction.spec.governance.DRep;
+import com.bloxbean.cardano.client.transaction.spec.governance.Vote;
+import com.bloxbean.cardano.client.transaction.spec.governance.Voter;
+import com.bloxbean.cardano.client.transaction.spec.governance.VoterType;
+import com.bloxbean.cardano.client.transaction.spec.governance.actions.GovActionId;
+import com.bloxbean.cardano.client.transaction.spec.governance.actions.InfoAction;
+import com.bloxbean.cardano.client.spec.UnitInterval;
+import com.bloxbean.cardano.client.transaction.spec.Asset;
+import com.bloxbean.cardano.client.transaction.spec.Policy;
+import com.bloxbean.cardano.client.transaction.spec.cert.PoolRegistration;
+import com.bloxbean.cardano.client.transaction.spec.cert.SingleHostAddr;
+import com.bloxbean.cardano.client.transaction.spec.script.ScriptAll;
+import com.bloxbean.cardano.client.util.HexUtil;
+import com.bloxbean.cardano.client.util.PolicyUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Verifies the bridge builds each non-payment TxPlan intent (staking, governance, DRep, voting,
+ * proposals, …) offline. Each operation is built programmatically with CCL, serialized to TxPlan
+ * YAML via {@link TxPlan#toYaml()} (so the exact intent shape is authoritative), then built through
+ * the bridge with caller-supplied UTXOs + protocol parameters.
+ */
+class QuickTxIntentsTest {
+
+ private static final String TEST_MNEMONIC =
+ "test walk nut penalty hip pave soap entry language right filter choice";
+ private static final String FAKE_TX_HASH = "a".repeat(64);
+ private static final String POOL_ID = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy";
+ private static final String GOV_ACTION_TX = "12745f09b138d4d0a11a560b4591ebb830cf12336347606d2edbbf1893d395c6";
+
+ private final QuickTxService service = new QuickTxService();
+
+ private String protocolParamsJson;
+ private Account account;
+ private String sender;
+ private String sender2;
+ private String stakeAddress;
+ private Credential drepCredential;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream("protocol-params.json")) {
+ protocolParamsJson = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ account = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 0);
+ sender = account.baseAddress();
+ sender2 = Account.createFromMnemonic(Networks.testnet(), TEST_MNEMONIC, 0, 1).baseAddress();
+ stakeAddress = account.stakeAddress();
+ drepCredential = account.drepCredential();
+ }
+
+ private static final String REF_TX_HASH = "c".repeat(64);
+
+ /**
+ * UTXOs at {@code sender}: a 2000-ADA one (covers deposits — gov action = 1000 ADA) plus a small
+ * one that the reference-input test can read.
+ */
+ private String utxos() {
+ return """
+ [{"tx_hash":"%s","output_index":0,"address":"%s",
+ "amount":[{"unit":"lovelace","quantity":"2000000000"}]},
+ {"tx_hash":"%s","output_index":0,"address":"%s",
+ "amount":[{"unit":"lovelace","quantity":"5000000"}]},
+ {"tx_hash":"%s","output_index":1,"address":"%s",
+ "amount":[{"unit":"lovelace","quantity":"2000000000"}]}]
+ """.formatted(FAKE_TX_HASH, sender, REF_TX_HASH, sender, FAKE_TX_HASH, sender2);
+ }
+
+ /**
+ * Build a Tx through the bridge (TxPlan YAML -> offline build) and assert it produced CBOR. Also
+ * writes the generated TxPlan YAML to {@code build/intent-yamls/.yaml} so the wrapper
+ * end-to-end tests (Go) can drive the exact same intent through the native library.
+ */
+ private void assertBuilds(String name, Tx tx) throws Exception {
+ String yaml = TxPlan.from(tx).feePayer(sender).toYaml();
+
+ java.nio.file.Path dir = java.nio.file.Path.of("build/intent-yamls");
+ java.nio.file.Files.createDirectories(dir);
+ java.nio.file.Files.writeString(dir.resolve(name + ".yaml"), yaml);
+
+ String resultYaml = service.buildTransaction(yaml, utxos(), protocolParamsJson, null);
+ var result = com.bloxbean.cardano.client.quicktx.serialization.YamlSerializer
+ .getYamlMapper().readTree(resultYaml);
+ assertFalse(result.get("tx_cbor").asText().isEmpty(), "tx_cbor should not be empty");
+ assertEquals(64, result.get("tx_hash").asText().length());
+ assertTrue(Long.parseLong(result.get("fee").asText()) > 0);
+ }
+
+ private Anchor anchor() {
+ return new Anchor("https://example.com/meta.json",
+ com.bloxbean.cardano.client.util.HexUtil.decodeHexString(FAKE_TX_HASH));
+ }
+
+ // --- Staking ---
+
+ @Test
+ void stakeRegistration() throws Exception {
+ assertBuilds("stake_registration", new Tx().registerStakeAddress(stakeAddress).from(sender));
+ }
+
+ @Test
+ void stakeDelegation() throws Exception {
+ // Delegation only (no registration): the integration test registers the stake address and a
+ // pool first, then delegates to that pool.
+ assertBuilds("stake_delegation", new Tx().delegateTo(stakeAddress, POOL_ID).from(sender));
+ }
+
+ @Test
+ void stakeDeregistration() throws Exception {
+ assertBuilds("stake_deregistration", new Tx().deregisterStakeAddress(stakeAddress, sender).from(sender));
+ }
+
+ @Test
+ void stakeWithdrawal() throws Exception {
+ assertBuilds("stake_withdrawal", new Tx().withdraw(stakeAddress, BigInteger.ZERO).from(sender));
+ }
+
+ // --- Metadata ---
+
+ @Test
+ void metadata() throws Exception {
+ Metadata md = MetadataBuilder.metadataFromJson("{\"674\":{\"msg\":\"Hello from CCL Bridge\"}}");
+ assertBuilds("metadata", new Tx()
+ .payToAddress(account.enterpriseAddress(), Amount.ada(2))
+ .attachMetadata(md)
+ .from(sender));
+ }
+
+ // --- Compose (multiple senders into one transaction) ---
+
+ @Test
+ void compose() throws Exception {
+ String receiver = account.enterpriseAddress();
+ Tx tx1 = new Tx().payToAddress(receiver, Amount.ada(5)).from(sender);
+ Tx tx2 = new Tx().payToAddress(receiver, Amount.ada(3)).from(sender2);
+ String yaml = TxPlan.from(List.>of(tx1, tx2)).feePayer(sender).toYaml();
+
+ java.nio.file.Path dir = java.nio.file.Path.of("build/intent-yamls");
+ java.nio.file.Files.createDirectories(dir);
+ java.nio.file.Files.writeString(dir.resolve("compose.yaml"), yaml);
+
+ var result = com.bloxbean.cardano.client.quicktx.serialization.YamlSerializer.getYamlMapper()
+ .readTree(service.buildTransaction(yaml, utxos(), protocolParamsJson, null));
+ assertFalse(result.get("tx_cbor").asText().isEmpty());
+ assertEquals(64, result.get("tx_hash").asText().length());
+ assertTrue(Long.parseLong(result.get("fee").asText()) > 0);
+ }
+
+ // --- Treasury ---
+
+ @Test
+ void donation() throws Exception {
+ // currentTreasuryValue is 0 to match a freshly-reset devnet (the Conway donation cert
+ // asserts the stated treasury equals the chain's actual value at submit time).
+ assertBuilds("donation", new Tx()
+ .donateToTreasury(BigInteger.ZERO, BigInteger.valueOf(1_000_000L))
+ .from(sender));
+ }
+
+ // --- DRep ---
+
+ @Test
+ void drepRegistration() throws Exception {
+ assertBuilds("drep_registration", new Tx().registerDRep(drepCredential, anchor()).from(sender));
+ }
+
+ @Test
+ void drepDeregistration() throws Exception {
+ assertBuilds("drep_deregistration", new Tx().unregisterDRep(drepCredential).from(sender));
+ }
+
+ @Test
+ void drepUpdate() throws Exception {
+ assertBuilds("drep_update", new Tx().updateDRep(drepCredential, anchor()).from(sender));
+ }
+
+ // --- Voting & proposals ---
+
+ @Test
+ void voting() throws Exception {
+ Voter voter = new Voter(VoterType.DREP_KEY_HASH, drepCredential);
+ assertBuilds("voting", new Tx()
+ .createVote(voter, new GovActionId(GOV_ACTION_TX, 0), Vote.YES, anchor())
+ .from(sender));
+ }
+
+ @Test
+ void votingDelegation() throws Exception {
+ assertBuilds("voting_delegation", new Tx()
+ .delegateVotingPowerTo(new Address(stakeAddress), DRep.abstain())
+ .from(sender));
+ }
+
+ @Test
+ void governanceProposalInfoAction() throws Exception {
+ assertBuilds("governance_proposal", new Tx()
+ .createProposal(new InfoAction(), stakeAddress, anchor())
+ .from(sender));
+ }
+
+ // --- Stake pools ---
+
+ private PoolRegistration samplePool() {
+ // Key the pool to the account's stake key (operator + owner + reward account) so the pool
+ // registration can be witnessed by signing with the account's stake key — no separate pool
+ // cold key is needed. The reward account must be a registered stake address (the integration
+ // test registers it first).
+ byte[] stakeAddrBytes = new Address(account.stakeAddress()).getBytes();
+ byte[] stakeKeyHash = java.util.Arrays.copyOfRange(stakeAddrBytes, 1, stakeAddrBytes.length);
+ return PoolRegistration.builder()
+ .operator(stakeKeyHash)
+ .vrfKeyHash(HexUtil.decodeHexString("b95af7a0a58928fbd0e73b03ce81dedd42d4a776685b443cf2016c18438a3b9b"))
+ .pledge(BigInteger.valueOf(100_000_000L))
+ .cost(BigInteger.valueOf(340_000_000L))
+ .margin(new UnitInterval(BigInteger.valueOf(1), BigInteger.valueOf(100)))
+ .rewardAccount(HexUtil.encodeHexString(stakeAddrBytes))
+ .poolOwners(Set.of(HexUtil.encodeHexString(stakeKeyHash)))
+ .relays(List.of(SingleHostAddr.builder().port(3001).build()))
+ .build();
+ }
+
+ @Test
+ void poolRegistration() throws Exception {
+ assertBuilds("pool_registration", new Tx().registerPool(samplePool()).from(sender));
+ }
+
+ @Test
+ void poolUpdate() throws Exception {
+ assertBuilds("pool_update", new Tx().updatePool(samplePool()).from(sender));
+ }
+
+ @Test
+ void poolRetirement() throws Exception {
+ assertBuilds("pool_retirement", new Tx().retirePool(POOL_ID, 500).from(sender));
+ }
+
+ // --- Native scripts, explicit & reference inputs ---
+
+ @Test
+ void nativeMinting() throws Exception {
+ // An empty ScriptAll requires no signatures (vacuously true), so the minted policy needs no
+ // policy-key witness — the fee payer alone can submit it.
+ ScriptAll noKeyPolicy = new ScriptAll();
+ assertBuilds("minting", new Tx()
+ .mintAssets(noKeyPolicy, new Asset("TestNFT", BigInteger.ONE), account.enterpriseAddress())
+ .from(sender));
+ }
+
+ @Test
+ void nativeScriptAttachment() throws Exception {
+ Policy policy = PolicyUtil.createMultiSigScriptAllPolicy("test-policy", 1);
+ assertBuilds("native_script", new Tx()
+ .attachNativeScript(policy.getPolicyScript())
+ .payToAddress(account.enterpriseAddress(), Amount.ada(5))
+ .from(sender));
+ }
+
+ @Test
+ void collectFromRegular() throws Exception {
+ Utxo senderUtxo = Utxo.builder()
+ .txHash(FAKE_TX_HASH).outputIndex(0).address(sender)
+ .amount(List.of(Amount.ada(2000)))
+ .build();
+ assertBuilds("collect_from", new Tx()
+ .collectFrom(List.of(senderUtxo))
+ .payToAddress(account.enterpriseAddress(), Amount.ada(5))
+ .from(sender));
+ }
+
+ @Test
+ void referenceInput() throws Exception {
+ assertBuilds("reference_input", new Tx()
+ .readFrom(REF_TX_HASH, 0)
+ .payToAddress(account.enterpriseAddress(), Amount.ada(5))
+ .from(sender));
+ }
+
+ // --- Plutus scripts (mint + spend) ---
+
+ private static final String ALWAYS_SUCCEEDS_V2 = "4e4d01000033222220051200120011";
+
+ @Test
+ void scriptMinting() throws Exception {
+ PlutusScript script = PlutusV2Script.builder().cborHex(ALWAYS_SUCCEEDS_V2).build();
+ PlutusData redeemer = BigIntPlutusData.of(0);
+ Tx tx = new Tx()
+ .mintAsset(script, new Asset("TestToken", BigInteger.ONE), redeemer, account.enterpriseAddress())
+ .from(sender);
+
+ String yaml = TxPlan.from(tx).feePayer(sender).toYaml();
+ java.nio.file.Path dir = java.nio.file.Path.of("build/intent-yamls");
+ java.nio.file.Files.createDirectories(dir);
+ java.nio.file.Files.writeString(dir.resolve("script_minting.yaml"), yaml);
+
+ String execUnits = "[{\"mem\": 2000000, \"steps\": 500000000}]";
+ var result = com.bloxbean.cardano.client.quicktx.serialization.YamlSerializer.getYamlMapper()
+ .readTree(service.buildTransaction(yaml, utxos(), protocolParamsJson, execUnits));
+ assertFalse(result.get("tx_cbor").asText().isEmpty());
+ assertEquals(64, result.get("tx_hash").asText().length());
+ assertTrue(Long.parseLong(result.get("fee").asText()) > 0);
+ }
+
+ @Test
+ void plutusLock() throws Exception {
+ // Pays a UTXO to the always-succeeds script address carrying the datum hash, so a later
+ // script spend has something to collect. The integration test submits this first.
+ PlutusScript script = PlutusV2Script.builder().cborHex(ALWAYS_SUCCEEDS_V2).build();
+ String scriptAddr = AddressProvider.getEntAddress(script, Networks.testnet()).toBech32();
+ PlutusData datum = BigIntPlutusData.of(42);
+
+ Tx tx = new Tx()
+ .payToContract(scriptAddr, Amount.ada(10), datum.getDatumHash())
+ .from(sender);
+
+ String yaml = TxPlan.from(tx).feePayer(sender).toYaml();
+ java.nio.file.Path dir = java.nio.file.Path.of("build/intent-yamls");
+ java.nio.file.Files.createDirectories(dir);
+ java.nio.file.Files.writeString(dir.resolve("plutus_lock.yaml"), yaml);
+
+ var result = com.bloxbean.cardano.client.quicktx.serialization.YamlSerializer.getYamlMapper()
+ .readTree(service.buildTransaction(yaml, utxos(), protocolParamsJson, null));
+ assertFalse(result.get("tx_cbor").asText().isEmpty());
+ assertEquals(64, result.get("tx_hash").asText().length());
+ }
+
+ @Test
+ void scriptCollectFrom() throws Exception {
+ PlutusScript script = PlutusV2Script.builder().cborHex(ALWAYS_SUCCEEDS_V2).build();
+ String scriptAddr = AddressProvider.getEntAddress(script, Networks.testnet()).toBech32();
+ PlutusData datum = BigIntPlutusData.of(42);
+ PlutusData redeemer = BigIntPlutusData.of(0);
+ String scriptTxHash = "b".repeat(64);
+
+ Utxo scriptUtxo = Utxo.builder()
+ .txHash(scriptTxHash).outputIndex(0).address(scriptAddr)
+ .amount(List.of(Amount.ada(10)))
+ .dataHash(datum.getDatumHash())
+ .build();
+
+ Tx tx = new Tx()
+ .collectFrom(scriptUtxo, redeemer, datum)
+ .payToAddress(account.enterpriseAddress(), Amount.ada(5))
+ .attachSpendingValidator(script)
+ .from(sender);
+
+ String yaml = TxPlan.from(tx).feePayer(sender).toYaml();
+ java.nio.file.Path dir = java.nio.file.Path.of("build/intent-yamls");
+ java.nio.file.Files.createDirectories(dir);
+ java.nio.file.Files.writeString(dir.resolve("script_collect_from.yaml"), yaml);
+
+ // The script UTXO (to spend) + a sender UTXO (fees + collateral). Plutus spends need
+ // caller-supplied execution units (one redeemer here).
+ String utxosJson = """
+ [{"tx_hash":"%s","output_index":0,"address":"%s",
+ "amount":[{"unit":"lovelace","quantity":"10000000"}],"data_hash":"%s"},
+ {"tx_hash":"%s","output_index":0,"address":"%s",
+ "amount":[{"unit":"lovelace","quantity":"2000000000"}]}]
+ """.formatted(scriptTxHash, scriptAddr, datum.getDatumHash(), FAKE_TX_HASH, sender);
+ String execUnits = "[{\"mem\": 2000000, \"steps\": 500000000}]";
+
+ String resultYaml = service.buildTransaction(yaml, utxosJson, protocolParamsJson, execUnits);
+ var result = com.bloxbean.cardano.client.quicktx.serialization.YamlSerializer
+ .getYamlMapper().readTree(resultYaml);
+ assertFalse(result.get("tx_cbor").asText().isEmpty());
+ assertEquals(64, result.get("tx_hash").asText().length());
+ assertTrue(Long.parseLong(result.get("fee").asText()) > 0);
+ }
+}
diff --git a/docs/adr/0001-native-shared-library-ffi.md b/docs/adr/0001-native-shared-library-ffi.md
new file mode 100644
index 0000000..b406662
--- /dev/null
+++ b/docs/adr/0001-native-shared-library-ffi.md
@@ -0,0 +1,39 @@
+# ADR-0001: Native shared library via GraalVM native-image + C FFI
+
+- **Status:** Accepted
+- **Date:** 2026-02-11
+- **Deciders:** bloxbean maintainers
+
+## Context
+
+Cardano Client Lib (CCL) is a mature, actively-maintained JVM library for Cardano transaction building,
+crypto, and serialization. Other ecosystems (Python, Go, Rust, JavaScript) rely on their own native
+implementations, which vary in completeness and — as has happened in the Cardano ecosystem — can stop
+being maintained abruptly, leaving downstream users stranded. We wanted a **maintained alternative**
+that reuses CCL's exact, well-tested behavior from other languages, with native startup and no JVM at
+runtime — without having to build and keep four independent reimplementations in lockstep ourselves.
+
+## Decision
+
+Compile CCL into a single **native shared library** (`libccl`) using **GraalVM native-image**, exposing
+a stable **C ABI** via `@CEntryPoint` exports, and bind to it from each language through that language's
+FFI. No JVM is shipped or required at runtime. Data crosses the boundary as C strings (JSON/YAML/hex).
+
+## Consequences
+
+- One core codebase reused everywhere; CCL semantics are identical across all languages.
+- Native startup, small footprint, no JVM dependency.
+- native-image constraints become ours: reflection must be registered (`reflect-config.json`),
+ some libraries need build-time initialization, builds are slower.
+- The C ABI is a lowest-common-denominator interface (strings in/out, manual memory release).
+- Portability of the produced `.so`/`.dylib`/`.dll` becomes a real concern — see [ADR-0008](0008-linux-glibc-baseline-portability.md).
+
+## Alternatives considered
+
+- **Rely solely on existing per-language native libraries** — they exist and work, but carry the risk
+ of being abruptly abandoned; CCL Bridge is the maintained fallback. (Building and maintaining our
+ *own* four independent reimplementations would also be a large, duplicated effort with correctness
+ drift across languages.)
+- **JNI / embedded JVM** — ships and runs a JVM; heavy footprint and startup.
+- **REST sidecar service** — network hop, stateful, operational burden; contradicts an offline,
+ in-process model ([ADR-0002](0002-offline-stateless-no-provider.md)).
diff --git a/docs/adr/0002-offline-stateless-no-provider.md b/docs/adr/0002-offline-stateless-no-provider.md
new file mode 100644
index 0000000..cf6bc49
--- /dev/null
+++ b/docs/adr/0002-offline-stateless-no-provider.md
@@ -0,0 +1,35 @@
+# ADR-0002: Offline, stateless bridge — caller-supplied chain data, no HTTP provider in libccl
+
+- **Status:** Accepted
+- **Date:** 2026-02-11
+- **Deciders:** bloxbean maintainers
+
+## Context
+
+Transaction building needs chain data — UTxOs and protocol parameters (and, for scripts, execution
+units — [ADR-0007](0007-caller-supplied-plutus-exec-units.md)). CCL can fetch these via providers
+(Blockfrost/Koios/Ogmios). Baking HTTP providers into the native library would pull networking, retry
+state, configuration, and secret handling into what is otherwise a side-effect-free FFI boundary — and
+every host language already has excellent HTTP clients.
+
+## Decision
+
+`libccl` is **offline, stateless, and side-effect-free**: it makes no network calls and never submits.
+The **caller supplies all chain data** as explicit inputs (UTxOs, protocol parameters; exec units for
+Plutus). HTTP provider modules are **out of scope for the native lib**. Optional convenience helpers
+that *fetch* this data may live in the **wrappers** ([ADR-0003](0003-four-language-wrappers-uniform-ffi.md)),
+using each language's own HTTP client — never inside `libccl`.
+
+## Consequences
+
+- Deterministic, easily testable; no secrets or keys to manage inside the library.
+- Submission/broadcast is the caller's responsibility, with their own client.
+- Callers must obtain UTxOs / params / exec units themselves — friction, mitigated by planned
+ wrapper-side helpers (TODO §2b exec-unit evaluators, §2c chain-data providers).
+- No lazy fetching; integration tests pass static data in.
+
+## Alternatives considered
+
+- **Built-in HTTP provider in libccl** — rejected: state, networking, and secrets inside an FFI lib.
+- **Provider as a separate native module** — possible future, but wrapper-side helpers are preferred
+ to keep the core pure.
diff --git a/docs/adr/0003-four-language-wrappers-uniform-ffi.md b/docs/adr/0003-four-language-wrappers-uniform-ffi.md
new file mode 100644
index 0000000..2dc1d52
--- /dev/null
+++ b/docs/adr/0003-four-language-wrappers-uniform-ffi.md
@@ -0,0 +1,40 @@
+# ADR-0003: One FFI, four language wrappers — a uniform, thin contract with explicit inputs
+
+- **Status:** Accepted
+- **Date:** 2026-02-11
+- **Deciders:** bloxbean maintainers
+
+## Context
+
+The native library ([ADR-0001](0001-native-shared-library-ffi.md)) exposes a C ABI. We want first-class
+support in **Python, Go, Rust, and JavaScript**, with consistent behavior and minimal maintenance.
+Early wrappers carried large per-language *fluent builders* (~10k LOC across the four) whose only job
+was emitting the bridge's transaction format; these drifted and duplicated logic.
+
+## Decision
+
+Support exactly **four language wrappers** — Python (ctypes), Go (cgo), Rust, JavaScript (Bun FFI —
+[ADR-0004](0004-bun-only-javascript-runtime.md)) — all binding the **same** C ABI. Keep wrappers
+**thin**: they marshal inputs to the FFI and parse results; all logic lives once in the core. Chain
+data (UTxOs, protocol params, Plutus exec units) is passed **explicitly** to the build call rather than
+fetched ([ADR-0002](0002-offline-stateless-no-provider.md)).
+
+We accept that explicit inputs are slightly more work for users today, and **plan an optional
+convenience layer** (provider/evaluator helpers) per wrapper to make it easier — without moving logic
+out of the core or breaking the offline contract (TODO §2b/§2c).
+
+## Consequences
+
+- Consistent semantics across languages; a new feature = one core change + thin wrapper plumbing.
+- The thin contract became concrete with the TxPlan migration
+ ([ADR-0006](0006-txplan-yaml-transaction-format.md)), which deleted the fluent builders.
+- Four FFI integrations to maintain, each with quirks (e.g. Go threading —
+ [ADR-0010](0010-go-isolate-thread-affinity.md)).
+- Until the convenience layer ships, users fetch chain data themselves.
+
+## Alternatives considered
+
+- **Fat per-language builders** — drift and duplication; removed in ADR-0006.
+- **Fewer languages / one blessed language** — less ecosystem reach.
+- **Fetching inputs inside the wrappers by default** — kept optional, to preserve the explicit,
+ offline contract and let users bring their own data source.
diff --git a/docs/adr/0004-bun-only-javascript-runtime.md b/docs/adr/0004-bun-only-javascript-runtime.md
new file mode 100644
index 0000000..e9d1aed
--- /dev/null
+++ b/docs/adr/0004-bun-only-javascript-runtime.md
@@ -0,0 +1,29 @@
+# ADR-0004: Bun is the only supported JavaScript runtime
+
+- **Status:** Accepted
+- **Date:** 2026-02-11
+- **Deciders:** bloxbean maintainers
+
+## Context
+
+The JavaScript wrapper needs FFI into `libccl`. Node.js FFI libraries (`ffi-napi`, `koffi`) crash
+against the GraalVM native-image library due to stack-boundary detection issues (notably on macOS
+ARM64). Bun ships a built-in, stable FFI (`bun:ffi`).
+
+## Decision
+
+Support **Bun** as the only JavaScript runtime for the JS wrapper. **Node.js is not supported** — it is
+tracked as a wanted-but-blocked investigation, not a committed deliverable.
+
+## Consequences
+
+- A working, stable JS FFI path via `bun:ffi`.
+- Node.js users are not served directly; Bun is required to use the JS wrapper.
+- Revisit if/when Node FFI stabilizes against GraalVM native-image.
+
+## Alternatives considered
+
+- **`ffi-napi` / `koffi` on Node** — crashes against the native-image library (stack boundaries).
+- **A WebAssembly build** — different toolchain and a large, separate effort.
+- **A local helper service for JS** — rejected; contradicts the in-process, offline model
+ ([ADR-0002](0002-offline-stateless-no-provider.md)).
diff --git a/docs/adr/0005-oracle-graalvm-25.md b/docs/adr/0005-oracle-graalvm-25.md
new file mode 100644
index 0000000..736202a
--- /dev/null
+++ b/docs/adr/0005-oracle-graalvm-25.md
@@ -0,0 +1,26 @@
+# ADR-0005: Standardize on Oracle GraalVM 25.0.3
+
+- **Status:** Accepted
+- **Date:** 2026-06-10
+- **Deciders:** bloxbean maintainers
+
+## Context
+
+CI floated `java-version: '25'` (a moving target), while local builds and docs referenced varying
+setups. native-image behavior, available flags, and the produced binary can shift across GraalVM
+versions, so an unpinned toolchain undermines reproducibility.
+
+## Decision
+
+Pin the **entire project** — local builds, CI, and release — to **Oracle GraalVM 25.0.3** exactly
+(`distribution: 'graalvm'`, `java-version: '25.0.3'`).
+
+## Consequences
+
+- Reproducible builds; consistent native-image behavior across machines and CI.
+- Adopting a newer GraalVM patch/feature release is a deliberate, single-point bump.
+
+## Alternatives considered
+
+- **Floating `'25'`** — non-reproducible; silent behavior changes on runner image updates.
+- **GraalVM Community Edition** — we standardized on Oracle GraalVM (the `graalvm` distribution).
diff --git a/docs/adr/0006-txplan-yaml-transaction-format.md b/docs/adr/0006-txplan-yaml-transaction-format.md
new file mode 100644
index 0000000..4a4e68f
--- /dev/null
+++ b/docs/adr/0006-txplan-yaml-transaction-format.md
@@ -0,0 +1,38 @@
+# ADR-0006: TxPlan (YAML) as the transaction-building format, replacing the bespoke JSON spec
+
+- **Status:** Accepted
+- **Date:** 2026-06-11
+- **Deciders:** bloxbean maintainers
+
+## Context
+
+The bridge originally defined transactions with a **bespoke JSON operations spec**, parsed by
+hand-written mappers (~1,500 LOC) into CCL `Tx`/`ScriptTx`, plus large per-language fluent builders
+(~10k LOC) whose only job was to emit that JSON. CCL `0.8.0-pre4` ships **TxPlan** — a first-class YAML
+transaction format that deserializes into CCL's own `AbstractTx` objects and builds offline to CBOR.
+
+The bridge is new and pre-1.0 with, as far as we know, **no production consumers yet**, so we were free
+to replace the transaction format outright rather than evolve it — and doing so now, before adoption, is
+the point at which it costs nothing.
+
+## Decision
+
+Adopt CCL **TxPlan (YAML)** as the transaction-building input. `ccl_quicktx_build` takes a TxPlan YAML
+document plus caller-supplied chain data ([ADR-0002](0002-offline-stateless-no-provider.md)) and returns
+the result as **YAML** (`tx_cbor`, `tx_hash`, `fee`). Delete the bespoke spec, its mappers, the provider
+path, and all per-language fluent builders; wrappers become thin pass-throughs
+([ADR-0003](0003-four-language-wrappers-uniform-ffi.md)).
+
+## Consequences
+
+- ~−11,300 net LOC; one authoritative format (CCL's own) instead of a custom one to maintain.
+- Wrappers reduce to `build(yaml, utxos, protocolParams, execUnits?)`.
+- Couples us to CCL's TxPlan schema and to a **preview** release (`0.8.0-pre4`) — re-pin when `0.8.0`
+ is stable.
+- The input/output format changed completely, but with no known consumers this was a clean swap, not a
+ migration — and adopting TxPlan pre-1.0 is what spares us a genuinely breaking change later.
+
+## Alternatives considered
+
+- **Keep/extend the bespoke spec** — perpetual maintenance and divergence from CCL.
+- **JSON result output** — chose YAML in *and* out for consistency with the TxPlan format.
diff --git a/docs/adr/0007-caller-supplied-plutus-exec-units.md b/docs/adr/0007-caller-supplied-plutus-exec-units.md
new file mode 100644
index 0000000..1b2b53e
--- /dev/null
+++ b/docs/adr/0007-caller-supplied-plutus-exec-units.md
@@ -0,0 +1,33 @@
+# ADR-0007: Plutus execution units are caller-supplied; the bridge stays evaluator-agnostic
+
+- **Status:** Accepted
+- **Date:** 2026-06-11
+- **Deciders:** bloxbean maintainers
+
+## Context
+
+Building Plutus script transactions requires **execution units** (memory + CPU steps) per redeemer,
+normally produced by a UPLC evaluator. CCL `0.8.0-pre4` has no offline UPLC evaluator usable inside a
+GraalVM native image; running scripts in-library would mean bundling an evaluator (e.g.
+`aiken-java-binding`), which is not feasible/initializable in a native image today.
+
+## Decision
+
+Treat exec units like UTxOs and protocol params — a **caller-supplied input** (`exec_units_json`, one
+`{mem, steps}` per redeemer in transaction order). The bridge wires CCL's `StaticTransactionEvaluator`
+to stamp them onto the transaction and **never runs the script**. Callers compute units with whatever
+evaluator they prefer (Blockfrost / Ogmios / Aiken / Scalus). A script build with no units fails with a
+clear error.
+
+## Consequences
+
+- Offline Plutus building works today, consistent with the offline contract
+ ([ADR-0002](0002-offline-stateless-no-provider.md)).
+- The bridge stays evaluator-agnostic; users pick and choose.
+- Users need an external evaluator to *obtain* the units — planned wrapper helpers (TODO §2b).
+- Self-contained in-library evaluation is deferred (spike: `aiken-java-binding` inside the native image).
+
+## Alternatives considered
+
+- **Bundle a UPLC evaluator inside libccl** — not feasible in a native image today; revisit later.
+- **Refuse Plutus entirely** — too limiting; scripts are core to Cardano use.
diff --git a/docs/adr/0008-linux-glibc-baseline-portability.md b/docs/adr/0008-linux-glibc-baseline-portability.md
new file mode 100644
index 0000000..a17da30
--- /dev/null
+++ b/docs/adr/0008-linux-glibc-baseline-portability.md
@@ -0,0 +1,49 @@
+# ADR-0008: Linux native-lib portability — glibc-baseline build + `-march=compatibility` (not static)
+
+- **Status:** Accepted
+- **Date:** 2026-06-25
+- **Deciders:** bloxbean maintainers (with Satya's review)
+
+## Context
+
+The shipped Linux `libccl.so` was built on `ubuntu-latest` (glibc ~2.39), so it failed to load on older
+distros (`version 'GLIBC_2.3x' not found`). We explored shipping a **fully static, no-`.so`** library to
+be distro-independent. A spike established two hard facts:
+
+1. GraalVM native-image **cannot emit a static library** (`.a`) — `oracle/graal#3053`, still open — and
+ musl's run-anywhere property applies only to static **executables**, not shared libraries. A truly
+ static, no-`.so` distribution would require re-architecting to an IPC subprocess model (rejected as
+ too invasive).
+2. native-image defaults to the **build machine's CPU** instruction set, which can `SIGILL` on older /
+ datacenter CPUs lacking newer instructions (AVX2/AVX-512).
+
+## Decision
+
+Keep the in-process FFI **shared library** and achieve portability on two axes:
+
+1. **glibc baseline** — build the Linux `.so` inside `manylinux_2_28`. The result requires only
+ `GLIBC_2.17`, so it runs on **glibc ≥ 2.17** (RHEL/CentOS 7+, Amazon Linux 2, Ubuntu 18.04+,
+ Debian 9+, and all newer).
+2. **CPU baseline** — set `-march=compatibility` in `native-image.properties` so the binary uses only
+ instructions common to all CPUs of the architecture.
+
+Verified continuously by `portable-linux-lib.yml` (objdump glibc-floor assertion + a real run on
+`centos:7`); `release.yml` ships the Linux artifact from the same container. macOS/Windows are
+unaffected (stable ABIs).
+
+## Consequences
+
+- One portable `.so` across virtually every non-musl Linux of the last decade — no wrapper or
+ architecture changes ([ADR-0001](0001-native-shared-library-ffi.md), [ADR-0003](0003-four-language-wrappers-uniform-ffi.md)).
+- CPU-portable; no `SIGILL` on older datacenter VMs.
+- **Does not run on Alpine / musl-only systems** — documented limitation; a musl variant is deferred
+ and technically unproven for shared libraries.
+- Linux release builds run inside a container (extra CI plumbing).
+
+## Alternatives considered
+
+- **Static library** — impossible (`oracle/graal#3053`).
+- **IPC static musl executable** — meets "no dynamic linking" literally, but a large re-architecture
+ with per-call overhead; rejected.
+- **musl shared library** — unproven for `--shared`; bloxbean also dropped musl builds for Yaci Store.
+- **Build on `ubuntu-latest`** — the status quo that fails on older distros.
diff --git a/docs/adr/0010-go-isolate-thread-affinity.md b/docs/adr/0010-go-isolate-thread-affinity.md
new file mode 100644
index 0000000..b4b9e16
--- /dev/null
+++ b/docs/adr/0010-go-isolate-thread-affinity.md
@@ -0,0 +1,30 @@
+# ADR-0010: Go wrapper isolate thread-affinity — all FFI on one dedicated OS thread
+
+- **Status:** Accepted
+- **Date:** 2026-06-10
+- **Deciders:** bloxbean maintainers
+
+## Context
+
+A GraalVM isolate's `IsolateThread` is bound to the OS thread that created/attached it. Go goroutines
+migrate freely across OS threads, so an FFI call could execute on a different OS thread than the one
+that owns the isolate — producing a GraalVM "yellow zone" `StackOverflowError` (observed on Linux
+x86_64, which forced the Go CI to be non-blocking).
+
+## Decision
+
+In the Go wrapper, pin **all FFI calls to a single dedicated OS thread** for the `Bridge`'s lifetime: a
+`runtime.LockOSThread`'d executor goroutine serializes every native call onto the thread that owns the
+isolate (calls are submitted over a channel and run there).
+
+## Consequences
+
+- Eliminates the thread-migration crash; Linux Go CI is blocking and green again.
+- Native calls are **serialized per `Bridge`** — correctness at the FFI boundary over raw concurrency.
+- One dedicated OS thread per `Bridge` (acceptable for this workload).
+
+## Alternatives considered
+
+- **Attach/detach the isolate on every call** — per-call overhead and easy to get wrong.
+- **Only raise the native-image stack size** — masks the symptom without fixing the root cause.
+- **Document "don't call from multiple goroutines"** — fragile; pushes a sharp edge onto users.
diff --git a/docs/adr/README.md b/docs/adr/README.md
new file mode 100644
index 0000000..a08731d
--- /dev/null
+++ b/docs/adr/README.md
@@ -0,0 +1,34 @@
+# Architecture Decision Records (ADRs)
+
+This directory records the **significant architectural decisions** for CCL Bridge — the *why*
+behind choices that aren't obvious from the code, so a future maintainer doesn't unknowingly
+undo them.
+
+## Convention
+
+- One decision per file, named `NNNN-kebab-title.md` (zero-padded, e.g. `0001-...`).
+- ADRs are **immutable once Accepted**. To change a decision, write a *new* ADR and mark the old
+ one `Superseded by ADR-XXXX` — don't rewrite history.
+- Numbered in the order they are **recorded**. Several here are *retrospective* — they document
+ decisions taken earlier in the project; the **Date** field reflects when the decision was
+ actually made, not when it was written down.
+- Start from [`template.md`](template.md).
+
+## Status legend
+
+`Proposed` → under discussion · `Accepted` → in effect · `Superseded` → replaced by a later ADR ·
+`Deprecated` → no longer relevant.
+
+## Index
+
+| ADR | Title | Status | Decided |
+|-----|-------|--------|---------|
+| [0001](0001-native-shared-library-ffi.md) | Native shared library via GraalVM native-image + C FFI | Accepted | 2026-02-11 |
+| [0002](0002-offline-stateless-no-provider.md) | Offline, stateless bridge — caller-supplied chain data, no HTTP provider in libccl | Accepted | 2026-02-11 |
+| [0003](0003-four-language-wrappers-uniform-ffi.md) | One FFI, four language wrappers — uniform thin contract with explicit inputs | Accepted | 2026-02-11 |
+| [0004](0004-bun-only-javascript-runtime.md) | Bun is the only supported JavaScript runtime | Accepted | 2026-02-11 |
+| [0005](0005-oracle-graalvm-25.md) | Standardize on Oracle GraalVM 25.0.3 | Accepted | 2026-06-10 |
+| [0006](0006-txplan-yaml-transaction-format.md) | TxPlan (YAML) transaction format, replacing the bespoke JSON spec | Accepted | 2026-06-11 |
+| [0007](0007-caller-supplied-plutus-exec-units.md) | Plutus execution units are caller-supplied; evaluator-agnostic | Accepted | 2026-06-11 |
+| [0008](0008-linux-glibc-baseline-portability.md) | Linux portability — glibc-baseline build + `-march=compatibility` (not static) | Accepted | 2026-06-25 |
+| [0010](0010-go-isolate-thread-affinity.md) | Go wrapper isolate thread-affinity — all FFI on one dedicated OS thread | Accepted | 2026-06-10 |
diff --git a/docs/adr/template.md b/docs/adr/template.md
new file mode 100644
index 0000000..e10eafd
--- /dev/null
+++ b/docs/adr/template.md
@@ -0,0 +1,26 @@
+# ADR-NNNN:
+
+- **Status:** Proposed | Accepted | Superseded by [ADR-XXXX](xxxx-....md) | Deprecated
+- **Date:** YYYY-MM-DD
+- **Deciders:**
+
+## Context
+
+What is the problem and what forces are at play? Constraints, requirements, and the
+relevant facts (e.g. tool capabilities, prior decisions this builds on). State things
+that were true at decision time; link related ADRs.
+
+## Decision
+
+The decision, in active voice: "We will …". Be specific about scope and what is
+explicitly *out* of scope.
+
+## Consequences
+
+What becomes easier and what becomes harder as a result. Follow-ups, risks accepted,
+and anything that would invalidate or revisit this decision.
+
+## Alternatives considered
+
+Each option weighed, and why it was not chosen. This is the part future readers come
+back for, so capture the ones that looked plausible.
diff --git a/docs/quicktx.md b/docs/quicktx.md
index 6e43877..bae7ab0 100644
--- a/docs/quicktx.md
+++ b/docs/quicktx.md
@@ -1,980 +1,293 @@
-# QuickTx — Transaction Builder
+# QuickTx — TxPlan (YAML) Transaction Builder
-QuickTx is a JSON-driven offline transaction builder exposed through a single C function: `ccl_quicktx_build`. You pass a JSON specification describing what the transaction should do — payments, staking, governance, Plutus scripts — and get back an unsigned transaction in CBOR hex.
+QuickTx builds unsigned Cardano transactions **fully offline** from a CCL
+[**TxPlan**](https://github.com/bloxbean/cardano-client-lib) — a YAML document describing what the
+transaction should do. You pass the TxPlan YAML plus the chain data the build needs (UTXOs and
+protocol parameters), and get back an unsigned transaction in CBOR hex.
+
+The whole interface is YAML: **TxPlan YAML in → YAML result out**.
## Overview
-- **Single function**: `ccl_quicktx_build(thread, spec_json)` → returns `0` on success
-- **Result**: JSON with `tx_cbor` (unsigned transaction), `tx_hash`, and `fee`
-- **Offline by default**: supply UTXOs and protocol params inline — no HTTP calls from the native library
-- **Provider mode**: optionally point to a Yaci DevKit (or compatible) API to fetch UTXOs automatically
-- **Two transaction types**: `tx` (regular) and `script_tx` (Plutus script transactions)
-- **Compose mode**: combine multiple sub-transactions (even from different senders) into one
+- **Single function**: `ccl_quicktx_build(thread, yaml, utxos_json, protocol_params_json, exec_units_json)` → returns `0` on success.
+- **Result**: a YAML document with `tx_cbor` (unsigned transaction), `tx_hash`, and `fee`.
+- **Fully offline**: the caller supplies UTXOs and protocol parameters — the native library makes no
+ HTTP calls and never submits. (There is no provider mode; fetching chain data is the caller's job.)
+- **Client-side chain data**: UTXOs and protocol parameters are passed as JSON (the standard CCL
+ `Utxo` / `ProtocolParams` models).
+
+### Entry point
+
+```c
+int ccl_quicktx_build(
+ graal_isolatethread_t* thread,
+ const char* yaml, // TxPlan YAML
+ const char* utxos_json, // JSON array of UTXOs
+ const char* protocol_params_json, // JSON protocol parameters
+ const char* exec_units_json // JSON [{mem, steps}] per redeemer, or null (Plutus only)
+);
+```
-### Return Codes
+### Return codes
| Code | Meaning |
|------|---------|
-| `0` | Success — retrieve result via `ccl_get_result(thread)` |
-| `-2` | Invalid argument or validation failure |
+| `0` | Success — retrieve the result via `ccl_get_result(thread)` |
+| `-2` | Invalid argument (e.g. missing YAML or protocol parameters) |
| `-8` | Insufficient funds (UTXOs can't cover outputs + fees) |
-| `-10` | Transaction build failure |
+| `-10`| Transaction build failure (e.g. malformed TxPlan) |
-### Success Result
+### Success result (YAML)
-```json
-{
- "tx_cbor": "84a400...",
- "tx_hash": "abcd1234...",
- "fee": "173333"
-}
+```yaml
+tx_cbor: 84a400...
+tx_hash: abcd1234...
+fee: "173333"
```
---
-## TxSpec — Top-Level JSON Structure
-
-| Field | Type | Required | Description |
-|-------|------|----------|-------------|
-| `operations` | array | Yes (single mode) | List of operations to perform |
-| `from` | string | Yes (`tx` mode) | Sender bech32 address |
-| `change_address` | string | No | Change output address (defaults to `from`) |
-| `fee_payer` | string | Yes (compose mode) | Address that pays the fee |
-| `utxos` | array | Yes (inline mode) | Pre-supplied UTXOs for coin selection |
-| `protocol_params` | object | Yes (inline mode) | Protocol parameters (Blockfrost/Koios/DevKit format) |
-| `validity` | object | No | Slot-based validity interval |
-| `signer_count` | integer | No | Number of signers for fee estimation (default: 1) |
-| `merge_outputs` | boolean | No | Merge outputs to the same address |
-| `transactions` | array | Yes (compose mode) | Array of sub-transactions |
-| `provider` | object | No | Provider config for HTTP-based UTXO fetching |
-| `tx_type` | string | No | `"tx"` (default) or `"script_tx"` |
-| `change_datum_cbor_hex` | string | No | Inline datum CBOR hex for change output (script_tx) |
-| `change_datum_hash` | string | No | Datum hash for change output (script_tx) |
-
-### Validity
-
-```json
-"validity": {
- "valid_from": 1000000,
- "valid_to": 2000000
-}
-```
-
-### UTXO Format
-
-UTXOs use the standard Blockfrost/Koios/DevKit format:
-
-```json
-{
- "tx_hash": "aaaa...64hex",
- "output_index": 0,
- "address": "addr_test1...",
- "amount": [
- { "unit": "lovelace", "quantity": "100000000" },
- { "unit": "policy_hex+asset_name_hex", "quantity": "500" }
- ]
-}
-```
+## TxPlan — YAML structure
+
+A TxPlan is a YAML document with an optional `version`, optional `variables` for substitution, and a
+`transaction` list. Each list entry has a `tx` block with the sender (`from`) and a list of `intents`.
+
+```yaml
+version: 1.0
+
+# Optional: ${name} placeholders are substituted before the plan is parsed.
+variables:
+ to: addr_test1...
+ amount: "5000000"
+
+transaction:
+ - tx:
+ from: addr_test1... # sender / default fee payer
+ intents:
+ - type: payment
+ address: ${to}
+ amounts:
+ - unit: lovelace
+ quantity: ${amount}
+```
+
+Multiple entries in `transaction` are **composed** into a single transaction (each `tx` may have a
+different `from`). The `tx` block also accepts context such as a fee payer and a validity interval;
+those fields follow CCL's TxPlan serialization (see the reference link above).
+
+### Intents
+
+Each intent has a `type` discriminator. The full set supported by CCL's TxPlan:
+
+| `type` | Purpose |
+|--------|---------|
+| `payment` | Pay ADA / native tokens to an address |
+| `minting` | Mint/burn with a native script |
+| `metadata` | Attach transaction metadata |
+| `donation` | Treasury donation |
+| `stake_registration` | Register a stake address |
+| `stake_deregistration` | Deregister a stake address |
+| `stake_delegation` | Delegate to a stake pool |
+| `stake_withdrawal` | Withdraw staking rewards |
+| `drep_registration` / `drep_deregistration` / `drep_update` | DRep lifecycle |
+| `voting` | Cast a governance vote |
+| `voting_delegation` | Delegate voting power to a DRep |
+| `governance_proposal` | Submit a governance action |
+| `pool_registration` / `pool_update` / `pool_retirement` | Stake-pool lifecycle |
+| `collect_from` | Explicitly select input UTXOs |
+| `reference_input` | Add read-only reference inputs |
+| `native_script` | Attach a native script |
+| `script_collect_from` / `script_minting` / `validator` | Plutus script operations |
+
+> The exact YAML fields for each intent come from CCL's TxPlan serialization. This bridge passes the
+> YAML through unchanged, so the authoritative field reference is the CCL `quicktx` module
+> (`intent/*Intent.java` and the `TxMetadataSerializationTest` / TxPlan tests at `v0.8.0-pre4`).
+> Verified `payment` and `metadata` shapes are shown below.
+
+> **Plutus script transactions** build offline when you pass the redeemers' **execution units** in
+> `exec_units_json` — a JSON array of `[{mem, steps}]`, one per redeemer in transaction order. The
+> bridge wires CCL's `StaticTransactionEvaluator` to stamp them on; it never runs the script. You
+> compute the units with any UPLC evaluator (Ogmios, Blockfrost, Aiken, Scalus) and pass them in,
+> exactly as you pass UTXOs and protocol parameters. A script transaction built with no execution
+> units fails with `-10` (no offline evaluator runs the script).
---
-## Operations Reference
-
-### Payments
-
-#### `pay_to_address`
-
-Send ADA or tokens to an address.
-
-```json
-{
- "type": "pay_to_address",
- "address": "addr_test1...",
- "amounts": [
- { "unit": "lovelace", "quantity": "5000000" },
- { "unit": "aabb...policy...asset_hex", "quantity": "100" }
- ]
-}
-```
-
-Optional fields: `script_ref_cbor_hex`, `script_ref_type` (attach a reference script to the output).
-
-#### `pay_to_contract`
-
-Send to a script address with a datum.
-
-```json
-{
- "type": "pay_to_contract",
- "address": "addr_test1wz...",
- "amounts": [{ "unit": "lovelace", "quantity": "10000000" }],
- "datum_cbor_hex": "d87980"
-}
-```
-
-Use either `datum_cbor_hex` (inline datum) or `datum_hash`. Optional: `script_ref_cbor_hex`, `script_ref_type`.
-
-### Minting
-
-#### `mint_assets` (native scripts)
-
-Mint tokens using a native script (regular `tx` mode only).
-
-```json
-{
- "type": "mint_assets",
- "script_json": "{\"type\":\"all\",\"scripts\":[{\"type\":\"sig\",\"keyHash\":\"ab12...\"}]}",
- "assets": [
- { "name": "MyToken", "quantity": "1000" }
- ],
- "receiver": "addr_test1..."
-}
-```
-
-#### `mint_plutus_assets` (Plutus scripts)
-
-Mint tokens using a Plutus script (`script_tx` mode only).
-
-```json
-{
- "type": "mint_plutus_assets",
- "script_cbor_hex": "46450101002499",
- "script_type": "plutus_v3",
- "assets": [
- { "name": "TestToken", "quantity": "100" }
- ],
- "redeemer_cbor_hex": "d87980",
- "receiver": "addr_test1..."
-}
-```
-
-Optional: `output_datum_cbor_hex` (attach datum to mint output).
+## Chain data (caller-supplied)
-### Metadata
+### UTXO format
-#### `attach_metadata`
-
-Attach transaction metadata.
+`utxos_json` is a JSON array in the standard Blockfrost/Koios/DevKit shape:
```json
-{
- "type": "attach_metadata",
- "label": 674,
- "metadata": {
- "msg": ["Hello from CCL Bridge"]
+[
+ {
+ "tx_hash": "aaaa...64hex",
+ "output_index": 0,
+ "address": "addr_test1...",
+ "amount": [
+ { "unit": "lovelace", "quantity": "100000000" },
+ { "unit": "policy_hex+asset_name_hex", "quantity": "500" }
+ ]
}
-}
-```
-
-The `metadata` field accepts strings, numbers, lists, and maps.
-
-### UTXO Collection
-
-#### `collect_from`
-
-Explicitly select input UTXOs (bypasses coin selection). In `script_tx` mode, include `redeemer_cbor_hex` and `datum_cbor_hex` for script-locked UTXOs.
-
-```json
-{
- "type": "collect_from",
- "collect_utxos": [
- {
- "tx_hash": "aaaa...",
- "output_index": 0,
- "address": "addr_test1...",
- "amount": [{ "unit": "lovelace", "quantity": "10000000" }]
- }
- ],
- "redeemer_cbor_hex": "d87980",
- "datum_cbor_hex": "d87980"
-}
-```
-
-#### `read_from` (script_tx only)
-
-Add reference inputs (read-only, not consumed).
-
-```json
-{
- "type": "read_from",
- "reference_inputs": [
- { "tx_hash": "abcd...", "output_index": 0 }
- ]
-}
-```
-
-### Staking
-
-#### `register_stake_address`
-
-Register a stake address (regular `tx` mode only — not available in `script_tx`).
-
-```json
-{ "type": "register_stake_address", "address": "stake_test1..." }
-```
-
-#### `deregister_stake_address`
-
-```json
-{
- "type": "deregister_stake_address",
- "address": "stake_test1...",
- "refund_address": "addr_test1..."
-}
+]
```
-In `script_tx` mode, also requires `redeemer_cbor_hex`.
-
-#### `delegate_to`
+### Protocol parameters
-Delegate to a stake pool.
-
-```json
-{
- "type": "delegate_to",
- "address": "stake_test1...",
- "pool_id": "pool1..."
-}
-```
-
-In `script_tx` mode, also requires `redeemer_cbor_hex`.
-
-#### `withdraw`
-
-Withdraw staking rewards.
-
-```json
-{
- "type": "withdraw",
- "reward_address": "stake_test1...",
- "amount": "5000000",
- "receiver": "addr_test1..."
-}
-```
-
-In `script_tx` mode, also requires `redeemer_cbor_hex`.
-
-### DRep Operations
-
-#### `register_drep`
-
-```json
-{
- "type": "register_drep",
- "credential_hash": "ab12...56hex",
- "credential_type": "key",
- "anchor_url": "https://example.com/drep.json",
- "anchor_data_hash": "cd34...64hex"
-}
-```
-
-`credential_type`: `"key"` (default) or `"script"`. In `script_tx` mode, also requires `redeemer_cbor_hex`.
-
-#### `unregister_drep`
-
-```json
-{
- "type": "unregister_drep",
- "credential_hash": "ab12...56hex",
- "refund_address": "addr_test1...",
- "refund_amount": "500000000"
-}
-```
-
-#### `update_drep`
-
-```json
-{
- "type": "update_drep",
- "credential_hash": "ab12...56hex",
- "anchor_url": "https://example.com/drep-updated.json",
- "anchor_data_hash": "ef56...64hex"
-}
-```
-
-### Voting
-
-#### `delegate_voting_power_to`
-
-```json
-{
- "type": "delegate_voting_power_to",
- "address": "stake_test1...",
- "drep_type": "key_hash",
- "drep_hash": "ab12...56hex"
-}
-```
-
-`drep_type` values: `"key_hash"` (default), `"script_hash"`, `"abstain"`, `"no_confidence"`.
-
-#### `create_vote`
-
-```json
-{
- "type": "create_vote",
- "voter_type": "drep_key_hash",
- "voter_hash": "ab12...56hex",
- "gov_action_tx_hash": "aaaa...64hex",
- "gov_action_index": 0,
- "vote": "yes",
- "anchor_url": "https://example.com/rationale.json",
- "anchor_data_hash": "cd34...64hex"
-}
-```
-
-`voter_type` values: `"drep_key_hash"`, `"drep_script_hash"`, `"staking_pool_key_hash"`, `"constitutional_committee_hot_key_hash"`, `"constitutional_committee_hot_script_hash"`.
-
-`vote` values: `"yes"`, `"no"`, `"abstain"`.
-
-### Governance Proposals
-
-#### `create_proposal`
-
-All proposals require `gov_action_type`, `return_address`, and optionally `anchor_url` / `anchor_data_hash`. Action-specific fields vary by type:
-
-**`info_action`** — No additional fields.
-
-```json
-{
- "type": "create_proposal",
- "gov_action_type": "info_action",
- "return_address": "stake_test1...",
- "anchor_url": "https://example.com/proposal.json",
- "anchor_data_hash": "ab12...64hex"
-}
-```
-
-**`treasury_withdrawals`**
-
-```json
-{
- "type": "create_proposal",
- "gov_action_type": "treasury_withdrawals",
- "return_address": "stake_test1...",
- "withdrawals": [
- { "reward_address": "stake_test1...", "amount": "50000000" }
- ]
-}
-```
-
-**`no_confidence`**
-
-```json
-{
- "type": "create_proposal",
- "gov_action_type": "no_confidence",
- "return_address": "stake_test1...",
- "gov_action_tx_hash": "prev_action_tx_hash",
- "gov_action_index": 0
-}
-```
-
-**`update_committee`**
-
-```json
-{
- "type": "create_proposal",
- "gov_action_type": "update_committee",
- "return_address": "stake_test1...",
- "members_to_remove": [{ "hash": "ab12...", "type": "key" }],
- "new_members": [{ "hash": "cd34...", "type": "key", "epoch": 500 }],
- "quorum_numerator": "2",
- "quorum_denominator": "3"
-}
-```
-
-**`new_constitution`**
-
-```json
-{
- "type": "create_proposal",
- "gov_action_type": "new_constitution",
- "return_address": "stake_test1...",
- "constitution_anchor_url": "https://example.com/constitution.json",
- "constitution_anchor_data_hash": "ef56...64hex",
- "constitution_script_hash": "ab12...56hex"
-}
-```
-
-**`hard_fork_initiation`**
-
-```json
-{
- "type": "create_proposal",
- "gov_action_type": "hard_fork_initiation",
- "return_address": "stake_test1...",
- "protocol_version_major": 10,
- "protocol_version_minor": 0
-}
-```
-
-**`parameter_change`**
-
-```json
-{
- "type": "create_proposal",
- "gov_action_type": "parameter_change",
- "return_address": "stake_test1...",
- "policy_hash": "ab12...56hex"
-}
-```
-
-### Pool Operations
-
-#### `register_pool`
-
-```json
-{
- "type": "register_pool",
- "operator": "ab12...56hex",
- "vrf_key_hash": "cd34...64hex",
- "pledge": "100000000",
- "cost": "340000000",
- "margin_numerator": "1",
- "margin_denominator": "100",
- "reward_address": "e0ab...58hex",
- "pool_owners": ["ab12...56hex"],
- "relays": [
- { "type": "single_host_addr", "port": 6000, "ipv4": "127.0.0.1" },
- { "type": "single_host_name", "port": 6000, "dns_name": "relay.example.com" },
- { "type": "multi_host_name", "dns_name": "pool.example.com" }
- ],
- "pool_metadata_url": "https://example.com/pool.json",
- "pool_metadata_hash": "ef56...64hex"
-}
-```
-
-#### `update_pool`
-
-Same fields as `register_pool`.
-
-#### `retire_pool`
-
-```json
-{
- "type": "retire_pool",
- "pool_id": "pool1...",
- "epoch": 500
-}
-```
-
-### Treasury
-
-#### `donate_to_treasury`
-
-```json
-{
- "type": "donate_to_treasury",
- "treasury_value": "1000000000",
- "donation_amount": "50000000"
-}
-```
-
-### Script Validators (script_tx only)
-
-Attach validators to the transaction witness set. Required when spending from script addresses, minting with Plutus, or performing script-based staking/governance.
-
-```json
-{ "type": "attach_spending_validator", "script_cbor_hex": "46450101002499", "script_type": "plutus_v3" }
-{ "type": "attach_certificate_validator", "script_cbor_hex": "...", "script_type": "plutus_v3" }
-{ "type": "attach_reward_validator", "script_cbor_hex": "...", "script_type": "plutus_v3" }
-{ "type": "attach_proposing_validator", "script_cbor_hex": "...", "script_type": "plutus_v3" }
-{ "type": "attach_voting_validator", "script_cbor_hex": "...", "script_type": "plutus_v3" }
-```
-
-`script_type` values: `"plutus_v1"`, `"plutus_v2"`, `"plutus_v3"`.
-
-### Native Scripts
-
-#### `attach_native_script`
-
-Attach a native script to the witness set (regular `tx` mode).
-
-```json
-{
- "type": "attach_native_script",
- "script_json": "{\"type\":\"all\",\"scripts\":[{\"type\":\"sig\",\"keyHash\":\"ab12...\"}]}"
-}
-```
-
----
-
-## UTXO Sourcing Modes
-
-There are three ways to supply UTXOs and protocol parameters to QuickTx.
-
-### 1. Inline Mode
-
-The caller fetches UTXOs and protocol params from any source (Blockfrost, Koios, Ogmios, a database, etc.) and passes them directly in the JSON spec.
-
-```
-App fetches UTXOs + protocol params
- ↓
-Builds JSON spec with inline "utxos" and "protocol_params"
- ↓
-Calls ccl_quicktx_build
- ↓
-Gets unsigned tx CBOR
-```
-
-**Example:**
-```json
-{
- "operations": [{ "type": "pay_to_address", "address": "...", "amounts": [...] }],
- "from": "addr_test1...",
- "utxos": [{ "tx_hash": "...", "output_index": 0, "address": "...", "amount": [...] }],
- "protocol_params": { "min_fee_a": 44, "min_fee_b": 155381, "..." : "..." },
- "signer_count": 1
-}
-```
-
-Best for: full control over UTXO selection, custom backends, offline workflows, caching.
-
-### 2. Provider Mode
-
-Instead of inline data, set the `provider` field. The native library fetches UTXOs and protocol params via HTTP internally.
-
-```
-App builds JSON spec with "provider" config (no "utxos"/"protocol_params")
- ↓
-Calls ccl_quicktx_build
- ↓
-Library fetches UTXOs + params from provider via HTTP
- ↓
-Builds transaction
- ↓
-Returns unsigned tx CBOR
-```
-
-**Provider config:**
-```json
-{
- "provider": {
- "name": "yaci",
- "url": "http://localhost:8080/api/v1",
- "api_key": "optional-api-key",
- "enable_cost_evaluation": true
- }
-}
-```
-
-| Field | Type | Required | Description |
-|-------|------|----------|-------------|
-| `name` | string | Yes | Provider name (currently only `"yaci"`) |
-| `url` | string | Yes | Base URL of the API |
-| `api_key` | string | No | API key for authentication |
-| `enable_cost_evaluation` | boolean | No | Enable Plutus script cost evaluation (default: true) |
-
-The provider fetches UTXOs via `GET {url}/addresses/{address}/utxos` and protocol params via `GET {url}/epochs/parameters`. When `enable_cost_evaluation` is true and `script_tx` is used, it evaluates Plutus costs via `POST {url}/utils/txs/evaluate`.
-
-Best for: simple setups, quick prototyping, when a compatible provider is available.
-
-### 3. Wrapper-Level Provider
-
-Some wrappers (Python, JS) support a wrapper-level provider config. The wrapper handles HTTP calls (not the native library), then passes data inline.
-
-```
-App configures provider on wrapper
- ↓
-Calls build via wrapper API
- ↓
-Wrapper fetches UTXOs + params via HTTP
- ↓
-Constructs inline spec with fetched data
- ↓
-Calls ccl_quicktx_build
- ↓
-Returns unsigned tx CBOR
-```
-
-Best for: wrapper users who want provider convenience with wrapper-native HTTP (better error handling, auth headers, retries).
-
-### Comparison
-
-| | Inline | Provider | Wrapper Provider |
-|---|--------|----------|-----------------|
-| **Who fetches UTXOs?** | Your app | Native library | Wrapper library |
-| **HTTP calls from native lib?** | No | Yes | No |
-| **Provider dependency** | None | Yaci-compatible API | Wrapper-specific |
-| **Flexibility** | Maximum | Limited to supported providers | Medium |
-| **Simplicity** | More setup | Simplest | Simple |
-| **Offline capable?** | Yes | No | No |
-
----
-
-## Compose Mode
-
-Compose mode builds a single transaction from multiple sub-transactions, potentially from different senders. Set the `transactions` array instead of top-level `operations`.
-
-```json
-{
- "transactions": [
- {
- "from": "addr_test1_sender1...",
- "operations": [
- { "type": "pay_to_address", "address": "addr_test1_receiver1...",
- "amounts": [{ "unit": "lovelace", "quantity": "5000000" }] }
- ]
- },
- {
- "from": "addr_test1_sender2...",
- "operations": [
- { "type": "pay_to_address", "address": "addr_test1_receiver2...",
- "amounts": [{ "unit": "lovelace", "quantity": "3000000" }] }
- ]
- }
- ],
- "fee_payer": "addr_test1_sender1...",
- "utxos": [
- { "tx_hash": "aaa...", "output_index": 0, "address": "addr_test1_sender1...",
- "amount": [{ "unit": "lovelace", "quantity": "100000000" }] },
- { "tx_hash": "bbb...", "output_index": 0, "address": "addr_test1_sender2...",
- "amount": [{ "unit": "lovelace", "quantity": "100000000" }] }
- ],
- "protocol_params": { "..." : "..." }
-}
-```
-
-Each sub-transaction item supports:
-
-| Field | Type | Required |
-|-------|------|----------|
-| `from` | string | Yes (except `script_tx`) |
-| `change_address` | string | No |
-| `operations` | array | Yes |
-| `tx_type` | string | No (default `"tx"`) |
-| `change_datum_cbor_hex` | string | No |
-| `change_datum_hash` | string | No |
-
-You can mix `tx` and `script_tx` items in the same compose transaction. The `signer_count` defaults to the number of sub-transactions if not set.
-
----
-
-## Script_tx Mode
-
-Set `"tx_type": "script_tx"` to build Plutus script transactions. Key differences from regular `tx`:
-
-- `from` is optional (use `fee_payer` to specify who pays fees)
-- Staking, DRep, voting, and governance operations require a `redeemer_cbor_hex` field
-- `register_stake_address` is **not available** — use regular `tx` mode for that
-- `mint_assets` is **not available** — use `mint_plutus_assets` instead
-- Additional operations: `read_from`, `mint_plutus_assets`, `attach_*_validator`
-
-### Example: Collect from Script and Spend
-
-```json
-{
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "collect_from",
- "collect_utxos": [
- { "tx_hash": "aaa...", "output_index": 0, "address": "addr_test1wz...",
- "amount": [{ "unit": "lovelace", "quantity": "10000000" }] }
- ],
- "redeemer_cbor_hex": "d87980",
- "datum_cbor_hex": "d87980"
- },
- {
- "type": "attach_spending_validator",
- "script_cbor_hex": "46450101002499",
- "script_type": "plutus_v3"
- },
- {
- "type": "pay_to_address",
- "address": "addr_test1...",
- "amounts": [{ "unit": "lovelace", "quantity": "5000000" }]
- }
- ],
- "from": "addr_test1...",
- "utxos": [
- { "tx_hash": "bbb...", "output_index": 0, "address": "addr_test1...",
- "amount": [{ "unit": "lovelace", "quantity": "100000000" }] }
- ],
- "protocol_params": { "..." : "..." },
- "signer_count": 1
-}
-```
+`protocol_params_json` is a JSON object in the standard protocol-parameters shape (`min_fee_a`,
+`min_fee_b`, `key_deposit`, `pool_deposit`, `coins_per_utxo_size`, the Conway governance deposits,
+etc.). The CCL `ProtocolParams` model deserializes it directly.
---
## Examples
-### 1. Simple ADA Payment (Inline UTXOs)
-
-```json
-{
- "operations": [
- {
- "type": "pay_to_address",
- "address": "addr_test1qz...",
- "amounts": [{ "unit": "lovelace", "quantity": "5000000" }]
- }
- ],
- "from": "addr_test1qp...",
- "utxos": [
- {
- "tx_hash": "aaaa...64hex",
- "output_index": 0,
- "address": "addr_test1qp...",
- "amount": [{ "unit": "lovelace", "quantity": "100000000" }]
- }
- ],
- "protocol_params": { "min_fee_a": 44, "min_fee_b": 155381, "..." : "..." },
- "signer_count": 1
-}
-```
-
-### 2. Multi-Asset Payment
-
-```json
-{
- "operations": [
- {
- "type": "pay_to_address",
- "address": "addr_test1qz...",
- "amounts": [
- { "unit": "lovelace", "quantity": "2000000" },
- { "unit": "aabb...policyId...assetNameHex", "quantity": "100" }
- ]
- }
- ],
- "from": "addr_test1qp...",
- "utxos": [
- {
- "tx_hash": "aaaa...64hex",
- "output_index": 0,
- "address": "addr_test1qp...",
- "amount": [
- { "unit": "lovelace", "quantity": "100000000" },
- { "unit": "aabb...policyId...assetNameHex", "quantity": "500" }
- ]
- }
- ],
- "protocol_params": { "..." : "..." },
- "signer_count": 1
-}
-```
-
-### 3. Payment with Metadata
-
-```json
-{
- "operations": [
- {
- "type": "pay_to_address",
- "address": "addr_test1qz...",
- "amounts": [{ "unit": "lovelace", "quantity": "2000000" }]
- },
- {
- "type": "attach_metadata",
- "label": 674,
- "metadata": { "msg": ["Hello from CCL Bridge"] }
- }
- ],
- "from": "addr_test1qp...",
- "utxos": [{ "..." : "..." }],
- "protocol_params": { "..." : "..." },
- "signer_count": 1
-}
+### 1. Simple ADA payment
+
+```yaml
+version: 1.0
+transaction:
+ - tx:
+ from: addr_test1qp...
+ intents:
+ - type: payment
+ address: addr_test1qz...
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+```
+
+### 2. Multiple payments (one sender)
+
+```yaml
+version: 1.0
+transaction:
+ - tx:
+ from: addr_test1qp...
+ intents:
+ - type: payment
+ address: addr_test1_receiver1...
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+ - type: payment
+ address: addr_test1_receiver2...
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
+```
+
+### 3. Variable substitution
+
+```yaml
+version: 1.0
+variables:
+ to: addr_test1qz...
+ amount: "4000000"
+transaction:
+ - tx:
+ from: addr_test1qp...
+ intents:
+ - type: payment
+ address: ${to}
+ amounts:
+ - unit: lovelace
+ quantity: ${amount}
+```
+
+### 4. Payment with metadata
+
+The `metadata` intent's value is a **scalar string** that the deserializer auto-detects — pass it as
+a JSON string (it may also be CBOR hex). Labels are the top-level keys:
+
+```yaml
+version: 1.0
+transaction:
+ - tx:
+ from: addr_test1qp...
+ intents:
+ - type: payment
+ address: addr_test1qz...
+ amounts:
+ - unit: lovelace
+ quantity: "2000000"
+ - type: metadata
+ metadata: '{"674": {"msg": "Hello from CCL Bridge"}}'
+```
+
+### 5. Plutus mint (with caller-supplied execution units)
+
+A script intent goes under `scripts:` (the validator) with the operation in `intents:`. Pass the
+redeemer's execution units alongside — the bridge does not run the script.
+
+```yaml
+version: 1.0
+transaction:
+ - tx:
+ from: addr_test1qp...
+ intents:
+ - type: script_minting
+ policyId: 793f8c8cffba081b2a56462fc219cc8fe652d6a338b62c7b134876e7
+ assets:
+ - name: TestToken
+ value: 1
+ receiver: addr_test1qp...
+ redeemer:
+ int: 0
+ scripts:
+ - type: validator
+ role: mint
+ cbor_hex: 4e4d01000033222220051200120011
+ version: v2
+```
+
+…built with `exec_units_json = [{"mem": 2000000, "steps": 500000000}]` (one entry for the single
+mint redeemer).
+
+### 6. Compose (multiple senders into one transaction)
+
+The `transaction` list can hold more than one `tx`, each with its own `from`; they are composed into
+a single transaction. The `context.fee_payer` pays the fee. Supply UTXOs for every sender.
+
+```yaml
+version: 1.0
+context:
+ fee_payer: addr_test1_sender1...
+transaction:
+ - tx:
+ from: addr_test1_sender1...
+ intents:
+ - type: payment
+ address: addr_test1_receiver...
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+ - tx:
+ from: addr_test1_sender2...
+ intents:
+ - type: payment
+ address: addr_test1_receiver...
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
```
-### 4. Staking Delegation
-
-```json
-{
- "operations": [
- {
- "type": "register_stake_address",
- "address": "stake_test1ur..."
- },
- {
- "type": "delegate_to",
- "address": "stake_test1ur...",
- "pool_id": "pool1abc..."
- }
- ],
- "from": "addr_test1qp...",
- "utxos": [{ "..." : "..." }],
- "protocol_params": { "..." : "..." },
- "signer_count": 1
-}
-```
-
-### 5. Governance — Register DRep and Create Vote
-
-**Register DRep:**
-```json
-{
- "operations": [
- {
- "type": "register_drep",
- "credential_hash": "ab12...56hex",
- "anchor_url": "https://example.com/drep.json",
- "anchor_data_hash": "cd34...64hex"
- }
- ],
- "from": "addr_test1qp...",
- "utxos": [{ "..." : "..." }],
- "protocol_params": { "..." : "..." },
- "signer_count": 1
-}
-```
-
-**Create Vote:**
-```json
-{
- "operations": [
- {
- "type": "create_vote",
- "voter_type": "drep_key_hash",
- "voter_hash": "ab12...56hex",
- "gov_action_tx_hash": "aaaa...64hex",
- "gov_action_index": 0,
- "vote": "yes",
- "anchor_url": "https://example.com/rationale.json",
- "anchor_data_hash": "ef56...64hex"
- }
- ],
- "from": "addr_test1qp...",
- "utxos": [{ "..." : "..." }],
- "protocol_params": { "..." : "..." },
- "signer_count": 1
-}
-```
-
-**Create Info Action Proposal:**
-```json
-{
- "operations": [
- {
- "type": "create_proposal",
- "gov_action_type": "info_action",
- "return_address": "stake_test1ur...",
- "anchor_url": "https://example.com/proposal.json",
- "anchor_data_hash": "ab12...64hex"
- }
- ],
- "from": "addr_test1qp...",
- "utxos": [{ "..." : "..." }],
- "protocol_params": { "..." : "..." },
- "signer_count": 1
-}
-```
-
-### 6. ScriptTx — Collect from Script with Plutus Validator
-
-```json
-{
- "tx_type": "script_tx",
- "operations": [
- {
- "type": "collect_from",
- "collect_utxos": [
- {
- "tx_hash": "aaa...64hex",
- "output_index": 0,
- "address": "addr_test1wz...",
- "amount": [{ "unit": "lovelace", "quantity": "10000000" }]
- }
- ],
- "redeemer_cbor_hex": "d87980",
- "datum_cbor_hex": "d87980"
- },
- {
- "type": "attach_spending_validator",
- "script_cbor_hex": "46450101002499",
- "script_type": "plutus_v3"
- },
- {
- "type": "pay_to_address",
- "address": "addr_test1qz...",
- "amounts": [{ "unit": "lovelace", "quantity": "5000000" }]
- }
- ],
- "from": "addr_test1qp...",
- "utxos": [
- {
- "tx_hash": "bbb...64hex",
- "output_index": 0,
- "address": "addr_test1qp...",
- "amount": [{ "unit": "lovelace", "quantity": "100000000" }]
- }
- ],
- "protocol_params": { "..." : "..." },
- "signer_count": 1
-}
-```
-
-### 7. Compose Mode — Two Senders
-
-```json
-{
- "transactions": [
- {
- "from": "addr_test1_sender1...",
- "operations": [
- {
- "type": "pay_to_address",
- "address": "addr_test1_receiver1...",
- "amounts": [{ "unit": "lovelace", "quantity": "5000000" }]
- }
- ]
- },
- {
- "from": "addr_test1_sender2...",
- "operations": [
- {
- "type": "pay_to_address",
- "address": "addr_test1_receiver2...",
- "amounts": [{ "unit": "lovelace", "quantity": "3000000" }]
- }
- ]
- }
- ],
- "fee_payer": "addr_test1_sender1...",
- "utxos": [
- {
- "tx_hash": "aaa...", "output_index": 0,
- "address": "addr_test1_sender1...",
- "amount": [{ "unit": "lovelace", "quantity": "100000000" }]
- },
- {
- "tx_hash": "bbb...", "output_index": 0,
- "address": "addr_test1_sender2...",
- "amount": [{ "unit": "lovelace", "quantity": "100000000" }]
- }
- ],
- "protocol_params": { "..." : "..." }
-}
-```
-
-### 8. Provider Mode — Using Yaci DevKit
-
-```json
-{
- "operations": [
- {
- "type": "pay_to_address",
- "address": "addr_test1qz...",
- "amounts": [{ "unit": "lovelace", "quantity": "2000000" }]
- }
- ],
- "from": "addr_test1qp...",
- "provider": {
- "name": "yaci",
- "url": "http://localhost:10000/local-cluster/api"
- }
-}
-```
-
-No `utxos` or `protocol_params` needed — the library fetches them from the provider.
-
---
-## Signing the Transaction
+## Using it from the wrappers
+
+Each wrapper exposes a thin `build(yaml, utxos, protocolParams)` that marshals the chain data to JSON,
+calls `ccl_quicktx_build`, and parses the YAML result. The result is an object/dict/struct with
+`tx_cbor`, `tx_hash`, and `fee`. `ccl_quicktx_build` returns an **unsigned** transaction — sign
+`tx_cbor` with the account sign API, then submit it yourself.
-`ccl_quicktx_build` returns an **unsigned** transaction. Sign it with `ccl_account_sign_tx`, which takes the mnemonic, network ID, account/address indices, and the unsigned CBOR hex.
+> **Signing stake/governance transactions.** `sign_tx` adds only the **payment** key. Certificates
+> in stake registration/deregistration/delegation, reward withdrawal, and DRep/vote operations must
+> also be witnessed by the **stake** (or **DRep**) key, or the node rejects the tx with
+> `MissingVKeyWitnessesUTXOW`. Use `sign_tx_with_keys(..., keys)` (Go `SignTxWithKeys`, JS
+> `signTxWithKeys`) with the roles you need, e.g. `["payment", "stake"]` or `["payment", "drep"]`
+> (roles: `payment`, `stake`, `drep`, `committee_cold`, `committee_hot`).
### Python
@@ -982,19 +295,8 @@ No `utxos` or `protocol_params` needed — the library fetches them from the pro
from ccl import CclLib
lib = CclLib()
-
-# Build
-result = lib.quicktx.new_tx() \
- .pay_to_address(receiver_addr, Amount.ada(5)) \
- .from_address(sender_addr) \
- .with_utxos(utxos) \
- .with_protocol_params(pp) \
- .build()
-
-# Sign
-signed_tx = lib.account.sign_tx(
- mnemonic, result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
+result = lib.quicktx.build(txplan_yaml, utxos, protocol_params) # -> {"tx_cbor","tx_hash","fee"}
+signed = lib.account.sign_tx(mnemonic, result["tx_cbor"], CclLib.TESTNET, 0, 0)
```
### JavaScript (Bun)
@@ -1003,19 +305,8 @@ signed_tx = lib.account.sign_tx(
import { CclBridge, TESTNET } from '@bloxbean/ccl';
const bridge = new CclBridge();
-
-// Build
-const result = bridge.quicktx
- .newTx()
- .payToAddress(receiverAddr, Amount.ada(5))
- .from(senderAddr)
- .withUtxos(utxos)
- .withProtocolParams(pp)
- .build();
-
-// Sign
-const signedTx = bridge.account.signTx(
- mnemonic, TESTNET, 0, 0, result.tx_cbor);
+const result = bridge.quicktx.build(txplanYaml, utxos, protocolParams);
+const signed = bridge.account.signTx(mnemonic, TESTNET, 0, 0, result.tx_cbor);
```
### Go
@@ -1024,17 +315,8 @@ const signedTx = bridge.account.signTx(
bridge, _ := ccl.New()
defer bridge.Close()
-// Build
-result, _ := bridge.QuickTx.NewTx().
- PayToAddress(receiverAddr, ccl.Amount{Unit: "lovelace", Quantity: "5000000"}).
- From(senderAddr).
- WithUtxos(utxos).
- WithProtocolParams(pp).
- Build()
-
-// Sign
-signedTx, _ := bridge.Account.SignTx(
- mnemonic, ccl.Testnet, 0, 0, result.TxCbor)
+result, _ := bridge.QuickTx.Build(txplanYaml, utxos, protocolParams)
+signed, _ := bridge.Account.SignTx(mnemonic, ccl.Testnet, 0, 0, result.TxCbor)
```
### Rust
@@ -1042,17 +324,10 @@ signedTx, _ := bridge.Account.SignTx(
```rust
let bridge = ccl::Bridge::new().unwrap();
-// Build
-let result = bridge.quicktx().new_tx()
- .pay_to_address(&receiver, &[Amount::ada(5.0)], None, None)
- .from(&sender)
- .with_utxos(utxos)
- .with_protocol_params(pp)
- .build()
- .unwrap();
-
-// Sign
-let signed_tx = bridge.account()
+let result = bridge.quicktx().build(&txplan_yaml, &utxos, &protocol_params).unwrap();
+let signed = bridge.account()
.sign_tx(&mnemonic, ccl::network::TESTNET, 0, 0, &result.tx_cbor)
.unwrap();
```
+
+See each wrapper's `examples/transaction.*` for a complete build-and-sign program.
diff --git a/native-test/src/smoke.c b/native-test/src/smoke.c
new file mode 100644
index 0000000..056fe49
--- /dev/null
+++ b/native-test/src/smoke.c
@@ -0,0 +1,49 @@
+/*
+ * Minimal functional smoke for the static-linking spike.
+ *
+ * Proves libccl.so doesn't just *load* on an old-glibc distro but actually *runs*: it creates a
+ * GraalVM isolate (the runtime initialises), reads the version, and derives a testnet account
+ * (exercises the real crypto path). Compiled in the manylinux_2_28 builder against the headers,
+ * then executed inside an old container (centos:7, glibc 2.17). Returns non-zero on any failure.
+ */
+#include
+#include
+#include "libccl.h"
+
+int main(void) {
+ graal_isolatethread_t *thread = NULL;
+ graal_isolate_t *isolate = NULL;
+
+ if (graal_create_isolate(NULL, &isolate, &thread) != 0) {
+ fprintf(stderr, "FAIL: graal_create_isolate\n");
+ return 1;
+ }
+
+ if (ccl_version(thread) != 0) {
+ fprintf(stderr, "FAIL: ccl_version rc\n");
+ return 1;
+ }
+ char *version = ccl_get_result(thread);
+ if (version == NULL || strlen(version) == 0) {
+ fprintf(stderr, "FAIL: empty version\n");
+ return 1;
+ }
+ printf("libccl version: %s\n", version);
+ ccl_free_string(thread, version);
+
+ /* A real operation (key derivation), not just a load check. */
+ if (ccl_account_create(thread, 1) != 0) {
+ fprintf(stderr, "FAIL: ccl_account_create rc\n");
+ return 1;
+ }
+ char *account = ccl_get_result(thread);
+ if (account == NULL || strstr(account, "addr_test1") == NULL) {
+ fprintf(stderr, "FAIL: testnet account missing addr_test1\n");
+ return 1;
+ }
+ printf("account ok (testnet address derived)\n");
+ ccl_free_string(thread, account);
+
+ printf("SMOKE OK\n");
+ return 0;
+}
diff --git a/test-fixtures/quicktx-intents/collect_from.yaml b/test-fixtures/quicktx-intents/collect_from.yaml
new file mode 100644
index 0000000..7711636
--- /dev/null
+++ b/test-fixtures/quicktx-intents/collect_from.yaml
@@ -0,0 +1,18 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ inputs:
+ - type: collect_from
+ utxo_refs:
+ - tx_hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ output_index: 0
+ intents:
+ - type: payment
+ address: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ amounts:
+ - unit: lovelace
+ quantity: 5000000
diff --git a/test-fixtures/quicktx-intents/compose.yaml b/test-fixtures/quicktx-intents/compose.yaml
new file mode 100644
index 0000000..00c344d
--- /dev/null
+++ b/test-fixtures/quicktx-intents/compose.yaml
@@ -0,0 +1,22 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: payment
+ address: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ amounts:
+ - unit: lovelace
+ quantity: 5000000
+- tx:
+ from: addr_test1qz7svwszky8gcmhrfza7a89z9u0dfzd3l7h23sqlc5yml7ejcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqcqrvr0
+ change_address: addr_test1qz7svwszky8gcmhrfza7a89z9u0dfzd3l7h23sqlc5yml7ejcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqcqrvr0
+ intents:
+ - type: payment
+ address: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ amounts:
+ - unit: lovelace
+ quantity: 3000000
diff --git a/test-fixtures/quicktx-intents/donation.yaml b/test-fixtures/quicktx-intents/donation.yaml
new file mode 100644
index 0000000..1c5da7d
--- /dev/null
+++ b/test-fixtures/quicktx-intents/donation.yaml
@@ -0,0 +1,11 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: donation
+ current_treasury_value: 0
+ donation_amount: 1000000
diff --git a/test-fixtures/quicktx-intents/drep_deregistration.yaml b/test-fixtures/quicktx-intents/drep_deregistration.yaml
new file mode 100644
index 0000000..93343a3
--- /dev/null
+++ b/test-fixtures/quicktx-intents/drep_deregistration.yaml
@@ -0,0 +1,11 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: drep_deregistration
+ drep_credential_hex: a5b45515a3ff8cb7c02ce351834da324eb6dfc41b5779cb5e6b832aa
+ drep_credential_type: key_hash
diff --git a/test-fixtures/quicktx-intents/drep_registration.yaml b/test-fixtures/quicktx-intents/drep_registration.yaml
new file mode 100644
index 0000000..85fa731
--- /dev/null
+++ b/test-fixtures/quicktx-intents/drep_registration.yaml
@@ -0,0 +1,13 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: drep_registration
+ drep_credential_hex: a5b45515a3ff8cb7c02ce351834da324eb6dfc41b5779cb5e6b832aa
+ drep_credential_type: key_hash
+ anchor_url: https://example.com/meta.json
+ anchor_hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
diff --git a/test-fixtures/quicktx-intents/drep_update.yaml b/test-fixtures/quicktx-intents/drep_update.yaml
new file mode 100644
index 0000000..7d78f91
--- /dev/null
+++ b/test-fixtures/quicktx-intents/drep_update.yaml
@@ -0,0 +1,13 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: drep_update
+ drep_credential_hex: a5b45515a3ff8cb7c02ce351834da324eb6dfc41b5779cb5e6b832aa
+ drep_credential_type: key_hash
+ anchor_url: https://example.com/meta.json
+ anchor_hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
diff --git a/test-fixtures/quicktx-intents/governance_proposal.yaml b/test-fixtures/quicktx-intents/governance_proposal.yaml
new file mode 100644
index 0000000..14f3a31
--- /dev/null
+++ b/test-fixtures/quicktx-intents/governance_proposal.yaml
@@ -0,0 +1,13 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: governance_proposal
+ gov_action_hex: 8106
+ return_address: stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl
+ anchor_url: https://example.com/meta.json
+ anchor_hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
diff --git a/test-fixtures/quicktx-intents/metadata.yaml b/test-fixtures/quicktx-intents/metadata.yaml
new file mode 100644
index 0000000..55276b4
--- /dev/null
+++ b/test-fixtures/quicktx-intents/metadata.yaml
@@ -0,0 +1,17 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: payment
+ address: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ amounts:
+ - unit: lovelace
+ quantity: 2000000
+ - type: metadata
+ metadata: |
+ "674":
+ msg: "Hello from CCL Bridge"
diff --git a/test-fixtures/quicktx-intents/minting.yaml b/test-fixtures/quicktx-intents/minting.yaml
new file mode 100644
index 0000000..57c0f8b
--- /dev/null
+++ b/test-fixtures/quicktx-intents/minting.yaml
@@ -0,0 +1,15 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: minting
+ assets:
+ - name: TestNFT
+ value: 1
+ receiver: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ script_hex: 820180
+ script_type: 0
diff --git a/test-fixtures/quicktx-intents/native_script.yaml b/test-fixtures/quicktx-intents/native_script.yaml
new file mode 100644
index 0000000..22304d2
--- /dev/null
+++ b/test-fixtures/quicktx-intents/native_script.yaml
@@ -0,0 +1,16 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: payment
+ address: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ amounts:
+ - unit: lovelace
+ quantity: 5000000
+ scripts:
+ - type: native_script
+ script_hex: 8201818200581ca1010fafd65c325fe38e57d249485080f34c33f9ba8a118cc961eab5
diff --git a/test-fixtures/quicktx-intents/plutus/plutus_lock.yaml b/test-fixtures/quicktx-intents/plutus/plutus_lock.yaml
new file mode 100644
index 0000000..73c96b8
--- /dev/null
+++ b/test-fixtures/quicktx-intents/plutus/plutus_lock.yaml
@@ -0,0 +1,14 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: payment
+ address: addr_test1wpunlryvl7aqsxe22erzlsseej87v5kk5vutvtrmzdy8dect48z0w
+ amounts:
+ - unit: lovelace
+ quantity: 10000000
+ datum_hash: 9e1199a988ba72ffd6e9c269cadb3b53b5f360ff99f112d9b2ee30c4d74ad88b
diff --git a/test-fixtures/quicktx-intents/plutus/script_collect_from.yaml b/test-fixtures/quicktx-intents/plutus/script_collect_from.yaml
new file mode 100644
index 0000000..39e2400
--- /dev/null
+++ b/test-fixtures/quicktx-intents/plutus/script_collect_from.yaml
@@ -0,0 +1,27 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ inputs:
+ - type: script_collect_from
+ utxo_refs:
+ - tx_hash: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+ output_index: 0
+ redeemer:
+ int: 0
+ datum:
+ int: 42
+ intents:
+ - type: payment
+ address: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ amounts:
+ - unit: lovelace
+ quantity: 5000000
+ scripts:
+ - type: validator
+ role: spend
+ cbor_hex: 4e4d01000033222220051200120011
+ version: v2
diff --git a/test-fixtures/quicktx-intents/plutus/script_minting.yaml b/test-fixtures/quicktx-intents/plutus/script_minting.yaml
new file mode 100644
index 0000000..715ef23
--- /dev/null
+++ b/test-fixtures/quicktx-intents/plutus/script_minting.yaml
@@ -0,0 +1,21 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - policyId: 793f8c8cffba081b2a56462fc219cc8fe652d6a338b62c7b134876e7
+ type: script_minting
+ assets:
+ - name: TestToken
+ value: 1
+ receiver: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ redeemer:
+ int: 0
+ scripts:
+ - type: validator
+ role: mint
+ cbor_hex: 4e4d01000033222220051200120011
+ version: v2
diff --git a/test-fixtures/quicktx-intents/pool_registration.yaml b/test-fixtures/quicktx-intents/pool_registration.yaml
new file mode 100644
index 0000000..f393926
--- /dev/null
+++ b/test-fixtures/quicktx-intents/pool_registration.yaml
@@ -0,0 +1,26 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: pool_registration
+ update: false
+ pool_registration:
+ type: POOL_REGISTRATION
+ operator: 32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc
+ vrfKeyHash: b95af7a0a58928fbd0e73b03ce81dedd42d4a776685b443cf2016c18438a3b9b
+ pledge: 100000000
+ cost: 340000000
+ margin:
+ numerator: 1
+ denominator: 100
+ rewardAccount: e032c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc
+ poolOwners:
+ - 32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc
+ relays:
+ - relay_type: single_host_addr
+ port: 3001
+ is_update: false
diff --git a/test-fixtures/quicktx-intents/pool_retirement.yaml b/test-fixtures/quicktx-intents/pool_retirement.yaml
new file mode 100644
index 0000000..72c6c29
--- /dev/null
+++ b/test-fixtures/quicktx-intents/pool_retirement.yaml
@@ -0,0 +1,11 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: pool_retirement
+ pool_id: pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy
+ retirement_epoch: 500
diff --git a/test-fixtures/quicktx-intents/pool_update.yaml b/test-fixtures/quicktx-intents/pool_update.yaml
new file mode 100644
index 0000000..9326c52
--- /dev/null
+++ b/test-fixtures/quicktx-intents/pool_update.yaml
@@ -0,0 +1,26 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: pool_update
+ update: true
+ pool_registration:
+ type: POOL_REGISTRATION
+ operator: 32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc
+ vrfKeyHash: b95af7a0a58928fbd0e73b03ce81dedd42d4a776685b443cf2016c18438a3b9b
+ pledge: 100000000
+ cost: 340000000
+ margin:
+ numerator: 1
+ denominator: 100
+ rewardAccount: e032c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc
+ poolOwners:
+ - 32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc
+ relays:
+ - relay_type: single_host_addr
+ port: 3001
+ is_update: true
diff --git a/test-fixtures/quicktx-intents/reference_input.yaml b/test-fixtures/quicktx-intents/reference_input.yaml
new file mode 100644
index 0000000..093c992
--- /dev/null
+++ b/test-fixtures/quicktx-intents/reference_input.yaml
@@ -0,0 +1,18 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ inputs:
+ - type: reference_input
+ refs:
+ - tx_hash: cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
+ output_index: 0
+ intents:
+ - type: payment
+ address: addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz
+ amounts:
+ - unit: lovelace
+ quantity: 5000000
diff --git a/test-fixtures/quicktx-intents/stake_delegation.yaml b/test-fixtures/quicktx-intents/stake_delegation.yaml
new file mode 100644
index 0000000..faa5c20
--- /dev/null
+++ b/test-fixtures/quicktx-intents/stake_delegation.yaml
@@ -0,0 +1,11 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: stake_delegation
+ stake_address: stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl
+ pool_id: pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy
diff --git a/test-fixtures/quicktx-intents/stake_deregistration.yaml b/test-fixtures/quicktx-intents/stake_deregistration.yaml
new file mode 100644
index 0000000..f81bc1d
--- /dev/null
+++ b/test-fixtures/quicktx-intents/stake_deregistration.yaml
@@ -0,0 +1,11 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: stake_deregistration
+ stake_address: stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl
+ refund_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
diff --git a/test-fixtures/quicktx-intents/stake_registration.yaml b/test-fixtures/quicktx-intents/stake_registration.yaml
new file mode 100644
index 0000000..437be13
--- /dev/null
+++ b/test-fixtures/quicktx-intents/stake_registration.yaml
@@ -0,0 +1,10 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: stake_registration
+ stake_address: stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl
diff --git a/test-fixtures/quicktx-intents/stake_withdrawal.yaml b/test-fixtures/quicktx-intents/stake_withdrawal.yaml
new file mode 100644
index 0000000..44e8c27
--- /dev/null
+++ b/test-fixtures/quicktx-intents/stake_withdrawal.yaml
@@ -0,0 +1,11 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: stake_withdrawal
+ reward_address: stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl
+ amount: 0
diff --git a/test-fixtures/quicktx-intents/voting.yaml b/test-fixtures/quicktx-intents/voting.yaml
new file mode 100644
index 0000000..6a0d27c
--- /dev/null
+++ b/test-fixtures/quicktx-intents/voting.yaml
@@ -0,0 +1,15 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: voting
+ voter_hex: 8202581ca5b45515a3ff8cb7c02ce351834da324eb6dfc41b5779cb5e6b832aa
+ gov_action_tx_hash: 12745f09b138d4d0a11a560b4591ebb830cf12336347606d2edbbf1893d395c6
+ gov_action_index: 0
+ vote: "YES"
+ anchor_url: https://example.com/meta.json
+ anchor_hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
diff --git a/test-fixtures/quicktx-intents/voting_delegation.yaml b/test-fixtures/quicktx-intents/voting_delegation.yaml
new file mode 100644
index 0000000..68dd4de
--- /dev/null
+++ b/test-fixtures/quicktx-intents/voting_delegation.yaml
@@ -0,0 +1,12 @@
+version: 1.0
+context:
+ fee_payer: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+transaction:
+- tx:
+ from: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ change_address: addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp
+ intents:
+ - type: voting_delegation
+ address: stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl
+ drep_hex: 8102
+ drep_type: abstain
diff --git a/wrappers/go/README.md b/wrappers/go/README.md
new file mode 100644
index 0000000..c74d798
--- /dev/null
+++ b/wrappers/go/README.md
@@ -0,0 +1,95 @@
+# CCL Bridge — Go
+
+Go bindings for [Cardano Client Lib](https://github.com/bloxbean/cardano-client-lib)
+via the CCL Bridge native library, using `cgo`.
+
+> Part of the [CCL Bridge](../../README.md) project. See the
+> [top-level README](../../README.md) for the full API reference and
+> [`docs/quicktx.md`](../../docs/quicktx.md) for transaction building.
+
+## Requirements
+
+- Go 1.21+ with `cgo` enabled (a C toolchain on `PATH`).
+- The native library `libccl.{dylib,so,dll}` for your platform.
+
+> **Threading:** all FFI calls run on a single dedicated OS thread that the `Bridge`
+> pins for its lifetime, so a `Bridge` is safe to share across goroutines and is immune
+> to Go's goroutine/OS-thread migration (which otherwise crashes the GraalVM isolate on
+> Linux x86_64). Calls are serialized; create multiple `Bridge` instances if you need
+> concurrent isolate work.
+
+## Getting the native library
+
+The `cgo` directives in `ccl/ccl.go` already point the compiler/linker at
+`core/build/native/nativeCompile` (relative to the package), so you only need to build
+or download the library there. From the repo root:
+
+```bash
+./gradlew :core:nativeCompile # build from source (needs Oracle GraalVM 25.0.3)
+# or:
+make download-lib # download a pre-built binary
+```
+
+At **runtime** the OS loader also needs to find the library, via `DYLD_LIBRARY_PATH`
+(macOS) / `LD_LIBRARY_PATH` (Linux).
+
+## Running the examples
+
+From `wrappers/go`:
+
+```bash
+LIB_DIR=../../core/build/native/nativeCompile
+
+DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+ go run ./examples/account
+```
+
+The [`examples/`](examples/) directory contains:
+
+| Program | What it shows |
+|---------|---------------|
+| [`account`](examples/account/main.go) | Create an account, restore from mnemonic, derive keys and a DRep ID |
+| [`primitives`](examples/primitives/main.go) | Mnemonics, Blake2b hashing, Ed25519 signing, address parsing/validation |
+| [`transaction`](examples/transaction/main.go) | Build an unsigned payment **offline** (QuickTx) and sign it — no node/DevKit needed |
+
+## Quick start
+
+```go
+package main
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/bloxbean/ccl-bridge/wrappers/go/ccl"
+)
+
+func main() {
+ bridge, err := ccl.New() // loads libccl, starts a GraalVM isolate
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer bridge.Close() // tears down the isolate
+
+ account, err := bridge.Account.Create(ccl.Testnet)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println(account.BaseAddress) // addr_test1...
+ fmt.Println(account.Mnemonic) // 24-word phrase
+}
+```
+
+## API namespaces
+
+A `*Bridge` exposes these namespaces (all offline operations):
+`bridge.Account`, `bridge.Address`, `bridge.Crypto`, `bridge.Tx`, `bridge.Plutus`,
+`bridge.Script`, `bridge.Gov`, `bridge.Wallet`, `bridge.QuickTx`.
+
+Network IDs: `ccl.Mainnet` (0), `ccl.Testnet` (1), `ccl.Preprod` (2), `ccl.Preview` (3).
+Errors are returned as a `*ccl.CclError`.
+
+Transactions are built from a [TxPlan](https://github.com/bloxbean/cardano-client-lib)
+**YAML** document via `bridge.QuickTx.Build(yaml, utxos, protocolParams)`, fully offline —
+you supply the UTXOs and protocol parameters. See
+[`examples/transaction`](examples/transaction/main.go).
diff --git a/wrappers/go/ccl/ccl.go b/wrappers/go/ccl/ccl.go
index 9675d0c..aa4b9df 100644
--- a/wrappers/go/ccl/ccl.go
+++ b/wrappers/go/ccl/ccl.go
@@ -11,8 +11,12 @@ import "C"
import (
"encoding/json"
"fmt"
- "math"
+ "runtime"
+ "strings"
+ "sync"
"unsafe"
+
+ goyaml "gopkg.in/yaml.v3"
)
// Network IDs
@@ -25,15 +29,15 @@ const (
// Error codes
const (
- Success = 0
- ErrGeneral = -1
- ErrInvalidArgument = -2
- ErrSerialization = -3
- ErrCrypto = -4
- ErrInvalidNetwork = -5
- ErrInvalidMnemonic = -6
- ErrInvalidAddress = -7
- ErrInsufficientFunds = -8
+ Success = 0
+ ErrGeneral = -1
+ ErrInvalidArgument = -2
+ ErrSerialization = -3
+ ErrCrypto = -4
+ ErrInvalidNetwork = -5
+ ErrInvalidMnemonic = -6
+ ErrInvalidAddress = -7
+ ErrInsufficientFunds = -8
ErrInvalidTransaction = -9
)
@@ -75,19 +79,30 @@ type WalletInfo struct {
// GovKeyInfo contains governance key derivation result.
type GovKeyInfo struct {
- DrepID string `json:"drep_id,omitempty"`
- ID string `json:"id,omitempty"`
- VerificationKey string `json:"verification_key"`
- VerificationKeyHash string `json:"verification_key_hash"`
- Bech32VerificationKey string `json:"bech32_verification_key"`
+ DrepID string `json:"drep_id,omitempty"`
+ ID string `json:"id,omitempty"`
+ VerificationKey string `json:"verification_key"`
+ VerificationKeyHash string `json:"verification_key_hash"`
+ Bech32VerificationKey string `json:"bech32_verification_key"`
Bech32VerificationKeyHash string `json:"bech32_verification_key_hash"`
}
// Bridge wraps the CCL native library.
+//
+// All FFI calls are funneled to a single, dedicated OS thread (see loop) for the
+// lifetime of the Bridge. A GraalVM IsolateThread is bound to the OS thread that
+// created it, but the Go runtime can migrate a goroutine across OS threads between
+// (or within) cgo calls. Calling the isolate from a different OS thread than the one
+// that created it makes GraalVM read the wrong thread's stack — which on Linux
+// x86_64 crashes with a "yellow zone" StackOverflowError. Pinning every call to one
+// locked OS thread keeps the isolate thread and the executing thread in sync.
type Bridge struct {
isolate *C.graal_isolate_t
thread *C.graal_isolatethread_t
+ mu sync.Mutex
+ calls chan func()
+
Account *AccountApi
Address *AddressApi
Crypto *CryptoApi
@@ -101,10 +116,12 @@ type Bridge struct {
// New creates a new Bridge instance with a GraalVM isolate.
func New() (*Bridge, error) {
- b := &Bridge{}
- rc := C.graal_create_isolate(nil, &b.isolate, &b.thread)
- if rc != 0 {
- return nil, fmt.Errorf("failed to create GraalVM isolate: %d", rc)
+ b := &Bridge{calls: make(chan func())}
+
+ ready := make(chan error, 1)
+ go b.loop(ready)
+ if err := <-ready; err != nil {
+ return nil, err
}
b.Account = &AccountApi{bridge: b}
@@ -120,12 +137,78 @@ func New() (*Bridge, error) {
return b, nil
}
-// Close tears down the GraalVM isolate.
+// loop owns the isolate's OS thread: it locks the thread, creates the isolate on it,
+// then executes every queued FFI closure on that same thread until Close.
+func (b *Bridge) loop(ready chan<- error) {
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ if rc := C.graal_create_isolate(nil, &b.isolate, &b.thread); rc != 0 {
+ ready <- fmt.Errorf("failed to create GraalVM isolate: %d", int(rc))
+ return
+ }
+ ready <- nil
+
+ for fn := range b.calls {
+ fn()
+ }
+}
+
+// run executes fn on the isolate's dedicated OS thread and blocks until it finishes.
+func (b *Bridge) run(fn func()) {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ done := make(chan struct{})
+ b.calls <- func() {
+ fn()
+ close(done)
+ }
+ <-done
+}
+
+// invoke runs a result-returning FFI call on the isolate thread and reads the
+// per-thread result/error there, where the Java thread-local state lives.
+func (b *Bridge) invoke(call func() C.int) (string, error) {
+ var s string
+ var err error
+ b.run(func() {
+ if rc := call(); rc != Success {
+ err = &CclError{Code: int(rc), Message: b.getError()}
+ } else {
+ s = b.getResult()
+ }
+ })
+ return s, err
+}
+
+// invokeRC runs an FFI call on the isolate thread and returns its raw status code,
+// for calls (e.g. validate/verify) where the caller interprets the code directly.
+func (b *Bridge) invokeRC(call func() C.int) C.int {
+ var rc C.int
+ b.run(func() { rc = call() })
+ return rc
+}
+
+// Close tears down the GraalVM isolate and stops its OS thread.
func (b *Bridge) Close() error {
- if b.thread != nil {
- C.graal_tear_down_isolate(b.thread)
- b.thread = nil
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ if b.calls == nil {
+ return nil
+ }
+ done := make(chan struct{})
+ b.calls <- func() {
+ if b.thread != nil {
+ C.graal_tear_down_isolate(b.thread)
+ b.thread = nil
+ }
+ close(done)
}
+ <-done
+ close(b.calls)
+ b.calls = nil
return nil
}
@@ -149,21 +232,13 @@ func (b *Bridge) getError() string {
return result
}
-func (b *Bridge) check(rc C.int) (string, error) {
- if rc != Success {
- return "", &CclError{Code: int(rc), Message: b.getError()}
- }
- return b.getResult(), nil
-}
-
func cstr(s string) *C.char {
return C.CString(s)
}
// Version returns the library version string.
func (b *Bridge) Version() (string, error) {
- rc := C.ccl_version(b.thread)
- return b.check(rc)
+ return b.invoke(func() C.int { return C.ccl_version(b.thread) })
}
// --- AccountApi ---
@@ -173,8 +248,7 @@ type AccountApi struct {
}
func (a *AccountApi) Create(networkID int) (*AccountInfo, error) {
- rc := C.ccl_account_create(a.bridge.thread, C.int(networkID))
- result, err := a.bridge.check(rc)
+ result, err := a.bridge.invoke(func() C.int { return C.ccl_account_create(a.bridge.thread, C.int(networkID)) })
if err != nil {
return nil, err
}
@@ -189,8 +263,9 @@ func (a *AccountApi) FromMnemonic(mnemonic string, networkID, accountIndex, addr
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_account_from_mnemonic(a.bridge.thread, C.int(networkID), cs, C.int(accountIndex), C.int(addressIndex))
- result, err := a.bridge.check(rc)
+ result, err := a.bridge.invoke(func() C.int {
+ return C.ccl_account_from_mnemonic(a.bridge.thread, C.int(networkID), cs, C.int(accountIndex), C.int(addressIndex))
+ })
if err != nil {
return nil, err
}
@@ -205,24 +280,27 @@ func (a *AccountApi) GetPublicKey(mnemonic string, networkID, accountIndex, addr
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_account_get_public_key(a.bridge.thread, cs, C.int(networkID), C.int(accountIndex), C.int(addressIndex))
- return a.bridge.check(rc)
+ return a.bridge.invoke(func() C.int {
+ return C.ccl_account_get_public_key(a.bridge.thread, cs, C.int(networkID), C.int(accountIndex), C.int(addressIndex))
+ })
}
func (a *AccountApi) GetPrivateKey(mnemonic string, networkID, accountIndex, addressIndex int) (string, error) {
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_account_get_private_key(a.bridge.thread, cs, C.int(networkID), C.int(accountIndex), C.int(addressIndex))
- return a.bridge.check(rc)
+ return a.bridge.invoke(func() C.int {
+ return C.ccl_account_get_private_key(a.bridge.thread, cs, C.int(networkID), C.int(accountIndex), C.int(addressIndex))
+ })
}
func (a *AccountApi) GetDRepID(mnemonic string, networkID, accountIndex int) (string, error) {
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_account_get_drep_id(a.bridge.thread, cs, C.int(networkID), C.int(accountIndex))
- return a.bridge.check(rc)
+ return a.bridge.invoke(func() C.int {
+ return C.ccl_account_get_drep_id(a.bridge.thread, cs, C.int(networkID), C.int(accountIndex))
+ })
}
func (a *AccountApi) SignTx(mnemonic string, networkID, accountIndex, addressIndex int, txCborHex string) (string, error) {
@@ -231,8 +309,26 @@ func (a *AccountApi) SignTx(mnemonic string, networkID, accountIndex, addressInd
csTx := cstr(txCborHex)
defer C.free(unsafe.Pointer(csTx))
- rc := C.ccl_account_sign_tx(a.bridge.thread, csMnemonic, C.int(networkID), C.int(accountIndex), C.int(addressIndex), csTx)
- return a.bridge.check(rc)
+ return a.bridge.invoke(func() C.int {
+ return C.ccl_account_sign_tx(a.bridge.thread, csMnemonic, C.int(networkID), C.int(accountIndex), C.int(addressIndex), csTx)
+ })
+}
+
+// SignTxWithKeys signs a transaction with one or more of the account's keys, selected by role
+// (any of: payment, stake, drep, committee_cold, committee_hot, applied in order). Use this for
+// transactions whose certificates also need the stake or DRep key — stake registration/delegation/
+// withdrawal and DRep/vote operations — which the payment key alone cannot witness.
+func (a *AccountApi) SignTxWithKeys(mnemonic string, networkID, accountIndex, addressIndex int, txCborHex string, keys ...string) (string, error) {
+ csMnemonic := cstr(mnemonic)
+ defer C.free(unsafe.Pointer(csMnemonic))
+ csTx := cstr(txCborHex)
+ defer C.free(unsafe.Pointer(csTx))
+ csKeys := cstr(strings.Join(keys, ","))
+ defer C.free(unsafe.Pointer(csKeys))
+
+ return a.bridge.invoke(func() C.int {
+ return C.ccl_account_sign_tx_multi(a.bridge.thread, csMnemonic, C.int(networkID), C.int(accountIndex), C.int(addressIndex), csTx, csKeys)
+ })
}
// --- AddressApi ---
@@ -245,8 +341,7 @@ func (a *AddressApi) Info(bech32 string) (*AddressInfo, error) {
cs := cstr(bech32)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_address_info(a.bridge.thread, cs)
- result, err := a.bridge.check(rc)
+ result, err := a.bridge.invoke(func() C.int { return C.ccl_address_info(a.bridge.thread, cs) })
if err != nil {
return nil, err
}
@@ -261,24 +356,21 @@ func (a *AddressApi) Validate(bech32 string) bool {
cs := cstr(bech32)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_address_validate(a.bridge.thread, cs)
- return rc == Success
+ return a.bridge.invokeRC(func() C.int { return C.ccl_address_validate(a.bridge.thread, cs) }) == Success
}
func (a *AddressApi) ToBytes(bech32 string) (string, error) {
cs := cstr(bech32)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_address_to_bytes(a.bridge.thread, cs)
- return a.bridge.check(rc)
+ return a.bridge.invoke(func() C.int { return C.ccl_address_to_bytes(a.bridge.thread, cs) })
}
func (a *AddressApi) FromBytes(hexBytes string) (string, error) {
cs := cstr(hexBytes)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_address_from_bytes(a.bridge.thread, cs)
- return a.bridge.check(rc)
+ return a.bridge.invoke(func() C.int { return C.ccl_address_from_bytes(a.bridge.thread, cs) })
}
// --- CryptoApi ---
@@ -291,29 +383,25 @@ func (c *CryptoApi) Blake2b256(dataHex string) (string, error) {
cs := cstr(dataHex)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_crypto_blake2b_256(c.bridge.thread, cs)
- return c.bridge.check(rc)
+ return c.bridge.invoke(func() C.int { return C.ccl_crypto_blake2b_256(c.bridge.thread, cs) })
}
func (c *CryptoApi) Blake2b224(dataHex string) (string, error) {
cs := cstr(dataHex)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_crypto_blake2b_224(c.bridge.thread, cs)
- return c.bridge.check(rc)
+ return c.bridge.invoke(func() C.int { return C.ccl_crypto_blake2b_224(c.bridge.thread, cs) })
}
func (c *CryptoApi) GenerateMnemonic(wordCount int) (string, error) {
- rc := C.ccl_crypto_generate_mnemonic(c.bridge.thread, C.int(wordCount))
- return c.bridge.check(rc)
+ return c.bridge.invoke(func() C.int { return C.ccl_crypto_generate_mnemonic(c.bridge.thread, C.int(wordCount)) })
}
func (c *CryptoApi) ValidateMnemonic(mnemonic string) bool {
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_crypto_validate_mnemonic(c.bridge.thread, cs)
- return rc == Success
+ return c.bridge.invokeRC(func() C.int { return C.ccl_crypto_validate_mnemonic(c.bridge.thread, cs) }) == Success
}
func (c *CryptoApi) Sign(messageHex, skHex string) (string, error) {
@@ -322,8 +410,7 @@ func (c *CryptoApi) Sign(messageHex, skHex string) (string, error) {
csSk := cstr(skHex)
defer C.free(unsafe.Pointer(csSk))
- rc := C.ccl_crypto_sign(c.bridge.thread, csMsg, csSk)
- return c.bridge.check(rc)
+ return c.bridge.invoke(func() C.int { return C.ccl_crypto_sign(c.bridge.thread, csMsg, csSk) })
}
func (c *CryptoApi) Verify(signatureHex, messageHex, pkHex string) bool {
@@ -334,8 +421,7 @@ func (c *CryptoApi) Verify(signatureHex, messageHex, pkHex string) bool {
csPk := cstr(pkHex)
defer C.free(unsafe.Pointer(csPk))
- rc := C.ccl_crypto_verify(c.bridge.thread, csSig, csMsg, csPk)
- return rc == Success
+ return c.bridge.invokeRC(func() C.int { return C.ccl_crypto_verify(c.bridge.thread, csSig, csMsg, csPk) }) == Success
}
// --- TxApi ---
@@ -348,8 +434,7 @@ func (t *TxApi) Hash(txCborHex string) (string, error) {
cs := cstr(txCborHex)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_tx_hash(t.bridge.thread, cs)
- return t.bridge.check(rc)
+ return t.bridge.invoke(func() C.int { return C.ccl_tx_hash(t.bridge.thread, cs) })
}
func (t *TxApi) SignWithSecretKey(txCborHex, skCborHex string) (string, error) {
@@ -358,32 +443,28 @@ func (t *TxApi) SignWithSecretKey(txCborHex, skCborHex string) (string, error) {
csSk := cstr(skCborHex)
defer C.free(unsafe.Pointer(csSk))
- rc := C.ccl_tx_sign_with_secret_key(t.bridge.thread, csTx, csSk)
- return t.bridge.check(rc)
+ return t.bridge.invoke(func() C.int { return C.ccl_tx_sign_with_secret_key(t.bridge.thread, csTx, csSk) })
}
func (t *TxApi) ToJson(txCborHex string) (string, error) {
cs := cstr(txCborHex)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_tx_to_json(t.bridge.thread, cs)
- return t.bridge.check(rc)
+ return t.bridge.invoke(func() C.int { return C.ccl_tx_to_json(t.bridge.thread, cs) })
}
func (t *TxApi) FromJson(txJson string) (string, error) {
cs := cstr(txJson)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_tx_from_json(t.bridge.thread, cs)
- return t.bridge.check(rc)
+ return t.bridge.invoke(func() C.int { return C.ccl_tx_from_json(t.bridge.thread, cs) })
}
func (t *TxApi) Deserialize(txCborHex string) (string, error) {
cs := cstr(txCborHex)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_tx_deserialize(t.bridge.thread, cs)
- return t.bridge.check(rc)
+ return t.bridge.invoke(func() C.int { return C.ccl_tx_deserialize(t.bridge.thread, cs) })
}
// --- PlutusApi ---
@@ -396,24 +477,21 @@ func (p *PlutusApi) DataHash(datumCborHex string) (string, error) {
cs := cstr(datumCborHex)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_plutus_data_hash(p.bridge.thread, cs)
- return p.bridge.check(rc)
+ return p.bridge.invoke(func() C.int { return C.ccl_plutus_data_hash(p.bridge.thread, cs) })
}
func (p *PlutusApi) DataToJson(cborHex string) (string, error) {
cs := cstr(cborHex)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_plutus_data_to_json(p.bridge.thread, cs)
- return p.bridge.check(rc)
+ return p.bridge.invoke(func() C.int { return C.ccl_plutus_data_to_json(p.bridge.thread, cs) })
}
func (p *PlutusApi) DataFromJson(jsonStr string) (string, error) {
cs := cstr(jsonStr)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_plutus_data_from_json(p.bridge.thread, cs)
- return p.bridge.check(rc)
+ return p.bridge.invoke(func() C.int { return C.ccl_plutus_data_from_json(p.bridge.thread, cs) })
}
// --- ScriptApi ---
@@ -426,16 +504,14 @@ func (s *ScriptApi) NativeFromJson(jsonStr string) (string, error) {
cs := cstr(jsonStr)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_script_native_from_json(s.bridge.thread, cs)
- return s.bridge.check(rc)
+ return s.bridge.invoke(func() C.int { return C.ccl_script_native_from_json(s.bridge.thread, cs) })
}
func (s *ScriptApi) Hash(scriptCborHex string, scriptType int) (string, error) {
cs := cstr(scriptCborHex)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_script_hash(s.bridge.thread, cs, C.int(scriptType))
- return s.bridge.check(rc)
+ return s.bridge.invoke(func() C.int { return C.ccl_script_hash(s.bridge.thread, cs, C.int(scriptType)) })
}
// --- GovApi ---
@@ -448,8 +524,9 @@ func (g *GovApi) DrepKeyFromMnemonic(mnemonic string, networkID, accountIndex in
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_gov_drep_key_from_mnemonic(g.bridge.thread, cs, C.int(networkID), C.int(accountIndex))
- result, err := g.bridge.check(rc)
+ result, err := g.bridge.invoke(func() C.int {
+ return C.ccl_gov_drep_key_from_mnemonic(g.bridge.thread, cs, C.int(networkID), C.int(accountIndex))
+ })
if err != nil {
return nil, err
}
@@ -464,8 +541,9 @@ func (g *GovApi) CommitteeColdKeyFromMnemonic(mnemonic string, networkID, accoun
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_gov_committee_cold_key_from_mnemonic(g.bridge.thread, cs, C.int(networkID), C.int(accountIndex))
- result, err := g.bridge.check(rc)
+ result, err := g.bridge.invoke(func() C.int {
+ return C.ccl_gov_committee_cold_key_from_mnemonic(g.bridge.thread, cs, C.int(networkID), C.int(accountIndex))
+ })
if err != nil {
return nil, err
}
@@ -480,8 +558,9 @@ func (g *GovApi) CommitteeHotKeyFromMnemonic(mnemonic string, networkID, account
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_gov_committee_hot_key_from_mnemonic(g.bridge.thread, cs, C.int(networkID), C.int(accountIndex))
- result, err := g.bridge.check(rc)
+ result, err := g.bridge.invoke(func() C.int {
+ return C.ccl_gov_committee_hot_key_from_mnemonic(g.bridge.thread, cs, C.int(networkID), C.int(accountIndex))
+ })
if err != nil {
return nil, err
}
@@ -499,8 +578,7 @@ type WalletApi struct {
}
func (w *WalletApi) Create(networkID int) (*WalletInfo, error) {
- rc := C.ccl_wallet_create(w.bridge.thread, C.int(networkID))
- result, err := w.bridge.check(rc)
+ result, err := w.bridge.invoke(func() C.int { return C.ccl_wallet_create(w.bridge.thread, C.int(networkID)) })
if err != nil {
return nil, err
}
@@ -515,8 +593,7 @@ func (w *WalletApi) FromMnemonic(mnemonic string, networkID int) (*WalletInfo, e
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_wallet_from_mnemonic(w.bridge.thread, cs, C.int(networkID))
- result, err := w.bridge.check(rc)
+ result, err := w.bridge.invoke(func() C.int { return C.ccl_wallet_from_mnemonic(w.bridge.thread, cs, C.int(networkID)) })
if err != nil {
return nil, err
}
@@ -531,1693 +608,70 @@ func (w *WalletApi) GetAddress(mnemonic string, networkID, index int) (string, e
cs := cstr(mnemonic)
defer C.free(unsafe.Pointer(cs))
- rc := C.ccl_wallet_get_address(w.bridge.thread, cs, C.int(networkID), C.int(index))
- return w.bridge.check(rc)
+ return w.bridge.invoke(func() C.int { return C.ccl_wallet_get_address(w.bridge.thread, cs, C.int(networkID), C.int(index)) })
}
// --- QuickTx API ---
-// TxResult is the result from building a transaction.
+// TxResult is the result of building a transaction: the unsigned CBOR, its hash, and the fee.
type TxResult struct {
- TxCbor string `json:"tx_cbor"`
- TxHash string `json:"tx_hash"`
- Fee string `json:"fee"`
-}
-
-// Amount represents a token amount in a transaction.
-type Amount struct {
- Unit string `json:"unit"`
- Quantity string `json:"quantity"`
-}
-
-// Lovelace creates a lovelace Amount.
-func Lovelace(quantity int64) Amount {
- return Amount{Unit: "lovelace", Quantity: fmt.Sprintf("%d", quantity)}
-}
-
-// Ada creates a lovelace Amount from ADA (1 ADA = 1,000,000 lovelace).
-func Ada(ada float64) Amount {
- return Amount{Unit: "lovelace", Quantity: fmt.Sprintf("%d", int64(math.Floor(ada*1_000_000)))}
-}
-
-// Asset creates a native asset Amount.
-func Asset(unit string, quantity int64) Amount {
- return Amount{Unit: unit, Quantity: fmt.Sprintf("%d", quantity)}
-}
-
-// MintAsset represents an asset to mint.
-type MintAsset struct {
- Name string `json:"name"`
- Quantity string `json:"quantity"`
-}
-
-// AnchorOption holds optional anchor fields.
-type AnchorOption struct {
- AnchorURL string
- AnchorDataHash string
-}
-
-// ProviderConfig configures Java-side lazy provider fetching.
-type ProviderConfig struct {
- Name string `json:"name"`
- URL string `json:"url"`
- APIKey string `json:"api_key,omitempty"`
- EnableCostEvaluation *bool `json:"enable_cost_evaluation,omitempty"`
-}
-
-// ReferenceInput represents a reference input (read-only) for script transactions.
-type ReferenceInput struct {
- TxHash string `json:"tx_hash"`
- OutputIndex int `json:"output_index"`
-}
-
-// Composable is implemented by Tx and ScriptTx for use with Compose().
-type Composable interface {
- ToSpec() map[string]interface{}
-}
-
-// ProposalWithdrawal represents a treasury withdrawal in a proposal.
-type ProposalWithdrawal struct {
- RewardAddress string `json:"reward_address"`
- Amount string `json:"amount"`
-}
-
-// PoolOptions holds optional fields for pool registration/update.
-type PoolOptions struct {
- Relays []interface{}
- PoolMetadataURL string
- PoolMetadataHash string
+ TxCbor string `yaml:"tx_cbor"`
+ TxHash string `yaml:"tx_hash"`
+ Fee string `yaml:"fee"`
}
-// ScriptRefOption holds optional script reference fields.
-type ScriptRefOption struct {
- ScriptRefCborHex string
- ScriptRefType string
-}
-
-// UnregisterDRepOption holds optional fields for DRep unregistration.
-type UnregisterDRepOption struct {
- RefundAddress string
- RefundAmount string
-}
-
-// QuickTxApi provides transaction building via QuickTx.
+// QuickTxApi builds unsigned transactions from a CCL TxPlan (YAML), fully offline.
type QuickTxApi struct {
bridge *Bridge
}
-// NewTx creates a new TxBuilder for building a single transaction.
-func (q *QuickTxApi) NewTx() *TxBuilder {
- return &TxBuilder{bridge: q.bridge, signerCount: 1}
-}
-
-// Tx creates a new Tx for use with Compose().
-func (q *QuickTxApi) Tx() *Tx {
- return &Tx{}
-}
-
-// Compose creates a ComposeTxBuilder from multiple Composable objects (Tx or ScriptTx).
-func (q *QuickTxApi) Compose(txs ...Composable) *ComposeTxBuilder {
- return &ComposeTxBuilder{bridge: q.bridge, txs: txs}
-}
-
-// NewScriptTx creates a new ScriptTxBuilder for building a single script transaction.
-func (q *QuickTxApi) NewScriptTx() *ScriptTxBuilder {
- return &ScriptTxBuilder{bridge: q.bridge, signerCount: 1}
-}
-
-// ScriptTx creates a new ScriptTx for use with Compose().
-func (q *QuickTxApi) ScriptTx() *ScriptTx {
- return &ScriptTx{}
-}
-
-// --- TxBuilder ---
-
-// TxBuilder builds a single transaction spec.
-type TxBuilder struct {
- bridge *Bridge
- operations []map[string]interface{}
- from string
- changeAddress string
- feePayer string
- utxos interface{}
- protocolParams interface{}
- validity map[string]interface{}
- mergeOutputs *bool
- signerCount int
-}
-
-func (tb *TxBuilder) PayToAddress(address string, amounts ...Amount) *TxBuilder {
- amountList := make([]Amount, len(amounts))
- copy(amountList, amounts)
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "pay_to_address",
- "address": address,
- "amounts": amountList,
- })
- return tb
-}
-
-// PayToAddressWithScriptRef is like PayToAddress but attaches a reference script to the output.
-func (tb *TxBuilder) PayToAddressWithScriptRef(address string, amounts []Amount, scriptRef ScriptRefOption) *TxBuilder {
- op := map[string]interface{}{
- "type": "pay_to_address",
- "address": address,
- "amounts": amounts,
- }
- if scriptRef.ScriptRefCborHex != "" {
- op["script_ref_cbor_hex"] = scriptRef.ScriptRefCborHex
- }
- if scriptRef.ScriptRefType != "" {
- op["script_ref_type"] = scriptRef.ScriptRefType
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-func (tb *TxBuilder) PayToContract(address string, amounts []Amount, datumCborHex, datumHash string, scriptRef ...ScriptRefOption) *TxBuilder {
- op := map[string]interface{}{
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts,
- }
- if datumCborHex != "" {
- op["datum_cbor_hex"] = datumCborHex
- }
- if datumHash != "" {
- op["datum_hash"] = datumHash
- }
- if len(scriptRef) > 0 {
- if scriptRef[0].ScriptRefCborHex != "" {
- op["script_ref_cbor_hex"] = scriptRef[0].ScriptRefCborHex
- }
- if scriptRef[0].ScriptRefType != "" {
- op["script_ref_type"] = scriptRef[0].ScriptRefType
- }
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-func (tb *TxBuilder) MintAssets(scriptJSON string, assets []MintAsset, receiver string) *TxBuilder {
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "mint_assets",
- "script_json": scriptJSON,
- "assets": assets,
- "receiver": receiver,
- })
- return tb
-}
-
-func (tb *TxBuilder) AttachMetadata(label int, metadata interface{}) *TxBuilder {
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- })
- return tb
-}
-
-func (tb *TxBuilder) CollectFrom(utxos []map[string]interface{}) *TxBuilder {
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "collect_from",
- "collect_utxos": utxos,
- })
- return tb
-}
-
-// Staking
-
-func (tb *TxBuilder) RegisterStakeAddress(address string) *TxBuilder {
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "register_stake_address", "address": address,
- })
- return tb
-}
-
-func (tb *TxBuilder) DeregisterStakeAddress(address string, refundAddress ...string) *TxBuilder {
- op := map[string]interface{}{"type": "deregister_stake_address", "address": address}
- if len(refundAddress) > 0 && refundAddress[0] != "" {
- op["refund_address"] = refundAddress[0]
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-func (tb *TxBuilder) DelegateTo(address, poolID string) *TxBuilder {
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "delegate_to", "address": address, "pool_id": poolID,
- })
- return tb
-}
-
-func (tb *TxBuilder) Withdraw(rewardAddress string, amount int64, receiver ...string) *TxBuilder {
- op := map[string]interface{}{
- "type": "withdraw", "reward_address": rewardAddress, "amount": fmt.Sprintf("%d", amount),
- }
- if len(receiver) > 0 && receiver[0] != "" {
- op["receiver"] = receiver[0]
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-// DRep
-
-func (tb *TxBuilder) RegisterDRep(credHash, credType string, anchor ...AnchorOption) *TxBuilder {
- op := map[string]interface{}{
- "type": "register_drep", "credential_hash": credHash, "credential_type": credType,
- }
- if len(anchor) > 0 {
- if anchor[0].AnchorURL != "" {
- op["anchor_url"] = anchor[0].AnchorURL
- }
- if anchor[0].AnchorDataHash != "" {
- op["anchor_data_hash"] = anchor[0].AnchorDataHash
- }
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-func (tb *TxBuilder) UnregisterDRep(credHash, credType string, opts ...UnregisterDRepOption) *TxBuilder {
- op := map[string]interface{}{
- "type": "unregister_drep", "credential_hash": credHash, "credential_type": credType,
- }
- if len(opts) > 0 {
- if opts[0].RefundAddress != "" {
- op["refund_address"] = opts[0].RefundAddress
- }
- if opts[0].RefundAmount != "" {
- op["refund_amount"] = opts[0].RefundAmount
- }
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-func (tb *TxBuilder) UpdateDRep(credHash, credType string, anchor ...AnchorOption) *TxBuilder {
- op := map[string]interface{}{
- "type": "update_drep", "credential_hash": credHash, "credential_type": credType,
- }
- if len(anchor) > 0 {
- if anchor[0].AnchorURL != "" {
- op["anchor_url"] = anchor[0].AnchorURL
- }
- if anchor[0].AnchorDataHash != "" {
- op["anchor_data_hash"] = anchor[0].AnchorDataHash
- }
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-// Voting
-
-func (tb *TxBuilder) DelegateVotingPowerTo(address, drepType string, drepHash ...string) *TxBuilder {
- op := map[string]interface{}{
- "type": "delegate_voting_power_to", "address": address, "drep_type": drepType,
- }
- if len(drepHash) > 0 && drepHash[0] != "" {
- op["drep_hash"] = drepHash[0]
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-func (tb *TxBuilder) CreateVote(voterType, voterHash, govActionTxHash string, govActionIndex int, vote string, anchor ...AnchorOption) *TxBuilder {
- op := map[string]interface{}{
- "type": "create_vote", "voter_type": voterType, "voter_hash": voterHash,
- "gov_action_tx_hash": govActionTxHash, "gov_action_index": govActionIndex, "vote": vote,
- }
- if len(anchor) > 0 {
- if anchor[0].AnchorURL != "" {
- op["anchor_url"] = anchor[0].AnchorURL
- }
- if anchor[0].AnchorDataHash != "" {
- op["anchor_data_hash"] = anchor[0].AnchorDataHash
- }
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-// Governance
-
-func (tb *TxBuilder) CreateProposal(govActionType, returnAddress, anchorURL, anchorDataHash string, withdrawals ...[]ProposalWithdrawal) *TxBuilder {
- op := map[string]interface{}{
- "type": "create_proposal", "gov_action_type": govActionType,
- "return_address": returnAddress, "anchor_url": anchorURL, "anchor_data_hash": anchorDataHash,
+// Build builds an unsigned transaction from a TxPlan YAML document using the caller-supplied
+// UTXOs and protocol parameters. utxos and protocolParams are marshalled to JSON (the standard
+// CCL Utxo / ProtocolParams models). The transaction is built offline and never submitted —
+// sign the returned TxCbor and submit it yourself.
+//
+// For Plutus script transactions, pass the redeemers' execution units as the optional execUnits
+// argument: a slice of {mem, steps} (one per redeemer, in transaction order). Compute them with any
+// evaluator (Ogmios, Blockfrost, Aiken, Scalus); the bridge does not run the script.
+func (q *QuickTxApi) Build(yaml string, utxos interface{}, protocolParams interface{}, execUnits ...interface{}) (*TxResult, error) {
+ utxosJSON, err := json.Marshal(utxos)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal utxos: %w", err)
}
- if len(withdrawals) > 0 && len(withdrawals[0]) > 0 {
- op["withdrawals"] = withdrawals[0]
+ ppJSON, err := json.Marshal(protocolParams)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal protocol params: %w", err)
}
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-// Pool Operations
-func (tb *TxBuilder) RegisterPool(operator, vrfKeyHash, pledge, cost, marginNumerator, marginDenominator, rewardAddress string, poolOwners []string, opts ...PoolOptions) *TxBuilder {
- op := map[string]interface{}{
- "type": "register_pool", "operator": operator, "vrf_key_hash": vrfKeyHash,
- "pledge": pledge, "cost": cost, "margin_numerator": marginNumerator,
- "margin_denominator": marginDenominator, "reward_address": rewardAddress,
- "pool_owners": poolOwners,
- }
- if len(opts) > 0 {
- if len(opts[0].Relays) > 0 {
- op["relays"] = opts[0].Relays
- }
- if opts[0].PoolMetadataURL != "" {
- op["pool_metadata_url"] = opts[0].PoolMetadataURL
- }
- if opts[0].PoolMetadataHash != "" {
- op["pool_metadata_hash"] = opts[0].PoolMetadataHash
- }
- }
- tb.operations = append(tb.operations, op)
- return tb
-}
+ yamlCs := cstr(yaml)
+ defer C.free(unsafe.Pointer(yamlCs))
+ utxosCs := cstr(string(utxosJSON))
+ defer C.free(unsafe.Pointer(utxosCs))
+ ppCs := cstr(string(ppJSON))
+ defer C.free(unsafe.Pointer(ppCs))
-func (tb *TxBuilder) UpdatePool(operator, vrfKeyHash, pledge, cost, marginNumerator, marginDenominator, rewardAddress string, poolOwners []string, opts ...PoolOptions) *TxBuilder {
- op := map[string]interface{}{
- "type": "update_pool", "operator": operator, "vrf_key_hash": vrfKeyHash,
- "pledge": pledge, "cost": cost, "margin_numerator": marginNumerator,
- "margin_denominator": marginDenominator, "reward_address": rewardAddress,
- "pool_owners": poolOwners,
- }
- if len(opts) > 0 {
- if len(opts[0].Relays) > 0 {
- op["relays"] = opts[0].Relays
- }
- if opts[0].PoolMetadataURL != "" {
- op["pool_metadata_url"] = opts[0].PoolMetadataURL
- }
- if opts[0].PoolMetadataHash != "" {
- op["pool_metadata_hash"] = opts[0].PoolMetadataHash
+ // nil *C.char marshals to a NULL pointer (no execution units).
+ var execCs *C.char
+ if len(execUnits) > 0 && execUnits[0] != nil {
+ execJSON, err := json.Marshal(execUnits[0])
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal exec units: %w", err)
}
+ execCs = cstr(string(execJSON))
+ defer C.free(unsafe.Pointer(execCs))
}
- tb.operations = append(tb.operations, op)
- return tb
-}
-
-func (tb *TxBuilder) RetirePool(poolID string, epoch int) *TxBuilder {
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "retire_pool", "pool_id": poolID, "epoch": epoch,
- })
- return tb
-}
-
-// Treasury
-func (tb *TxBuilder) DonateToTreasury(treasuryValue, donationAmount string) *TxBuilder {
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "donate_to_treasury", "treasury_value": treasuryValue, "donation_amount": donationAmount,
+ result, err := q.bridge.invoke(func() C.int {
+ return C.ccl_quicktx_build(q.bridge.thread, yamlCs, utxosCs, ppCs, execCs)
})
- return tb
-}
-
-// Native Script
-
-func (tb *TxBuilder) AttachNativeScript(scriptJSON string) *TxBuilder {
- tb.operations = append(tb.operations, map[string]interface{}{
- "type": "attach_native_script", "script_json": scriptJSON,
- })
- return tb
-}
-
-// Config
-
-func (tb *TxBuilder) From(address string) *TxBuilder {
- tb.from = address
- return tb
-}
-
-func (tb *TxBuilder) ChangeAddress(address string) *TxBuilder {
- tb.changeAddress = address
- return tb
-}
-
-func (tb *TxBuilder) FeePayer(address string) *TxBuilder {
- tb.feePayer = address
- return tb
-}
-
-func (tb *TxBuilder) WithUtxos(utxos interface{}) *TxBuilder {
- tb.utxos = utxos
- return tb
-}
-
-func (tb *TxBuilder) WithProtocolParams(params interface{}) *TxBuilder {
- tb.protocolParams = params
- return tb
-}
-
-func (tb *TxBuilder) ValidFrom(slot int64) *TxBuilder {
- if tb.validity == nil {
- tb.validity = make(map[string]interface{})
- }
- tb.validity["valid_from"] = slot
- return tb
-}
-
-func (tb *TxBuilder) ValidTo(slot int64) *TxBuilder {
- if tb.validity == nil {
- tb.validity = make(map[string]interface{})
- }
- tb.validity["valid_to"] = slot
- return tb
-}
-
-func (tb *TxBuilder) MergeOutputs(merge bool) *TxBuilder {
- tb.mergeOutputs = &merge
- return tb
-}
-
-func (tb *TxBuilder) SignerCount(count int) *TxBuilder {
- tb.signerCount = count
- return tb
-}
-
-func (tb *TxBuilder) buildSpec(providerConfig *ProviderConfig) map[string]interface{} {
- spec := map[string]interface{}{
- "operations": tb.operations,
- "from": tb.from,
- "signer_count": tb.signerCount,
- }
- if providerConfig != nil {
- spec["provider"] = providerConfig
- } else {
- spec["utxos"] = tb.utxos
- }
- if tb.protocolParams != nil {
- spec["protocol_params"] = tb.protocolParams
- }
- if tb.changeAddress != "" {
- spec["change_address"] = tb.changeAddress
- }
- if tb.feePayer != "" {
- spec["fee_payer"] = tb.feePayer
- }
- if len(tb.validity) > 0 {
- spec["validity"] = tb.validity
- }
- if tb.mergeOutputs != nil {
- spec["merge_outputs"] = *tb.mergeOutputs
- }
- return spec
-}
-
-// Build builds the transaction. Returns TxResult with tx_cbor, tx_hash, fee.
-func (tb *TxBuilder) Build() (*TxResult, error) {
- return tb.doBuild(nil)
-}
-
-// BuildWithProvider builds with a Java-side provider config for lazy UTXO fetching.
-func (tb *TxBuilder) BuildWithProvider(config ProviderConfig) (*TxResult, error) {
- return tb.doBuild(&config)
-}
-
-func (tb *TxBuilder) doBuild(providerConfig *ProviderConfig) (*TxResult, error) {
- spec := tb.buildSpec(providerConfig)
- specJSON, err := json.Marshal(spec)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal spec: %w", err)
- }
-
- cs := cstr(string(specJSON))
- defer C.free(unsafe.Pointer(cs))
-
- rc := C.ccl_quicktx_build(tb.bridge.thread, cs)
- result, err := tb.bridge.check(rc)
if err != nil {
return nil, err
}
+ // The build result is a YAML document.
var txResult TxResult
- if err := json.Unmarshal([]byte(result), &txResult); err != nil {
+ if err := goyaml.Unmarshal([]byte(result), &txResult); err != nil {
return nil, fmt.Errorf("failed to parse tx result: %w", err)
}
return &txResult, nil
}
-
-// --- Tx (for Compose) ---
-
-// Tx is a lightweight operation collector for use with Compose.
-type Tx struct {
- operations []map[string]interface{}
- from string
- changeAddress string
-}
-
-func (tx *Tx) PayToAddress(address string, amounts ...Amount) *Tx {
- amountList := make([]Amount, len(amounts))
- copy(amountList, amounts)
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "pay_to_address",
- "address": address,
- "amounts": amountList,
- })
- return tx
-}
-
-// PayToAddressWithScriptRef is like PayToAddress but attaches a reference script to the output.
-func (tx *Tx) PayToAddressWithScriptRef(address string, amounts []Amount, scriptRef ScriptRefOption) *Tx {
- op := map[string]interface{}{
- "type": "pay_to_address",
- "address": address,
- "amounts": amounts,
- }
- if scriptRef.ScriptRefCborHex != "" {
- op["script_ref_cbor_hex"] = scriptRef.ScriptRefCborHex
- }
- if scriptRef.ScriptRefType != "" {
- op["script_ref_type"] = scriptRef.ScriptRefType
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) PayToContract(address string, amounts []Amount, datumCborHex, datumHash string, scriptRef ...ScriptRefOption) *Tx {
- op := map[string]interface{}{
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts,
- }
- if datumCborHex != "" {
- op["datum_cbor_hex"] = datumCborHex
- }
- if datumHash != "" {
- op["datum_hash"] = datumHash
- }
- if len(scriptRef) > 0 {
- if scriptRef[0].ScriptRefCborHex != "" {
- op["script_ref_cbor_hex"] = scriptRef[0].ScriptRefCborHex
- }
- if scriptRef[0].ScriptRefType != "" {
- op["script_ref_type"] = scriptRef[0].ScriptRefType
- }
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) MintAssets(scriptJSON string, assets []MintAsset, receiver string) *Tx {
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "mint_assets",
- "script_json": scriptJSON,
- "assets": assets,
- "receiver": receiver,
- })
- return tx
-}
-
-func (tx *Tx) AttachMetadata(label int, metadata interface{}) *Tx {
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- })
- return tx
-}
-
-func (tx *Tx) CollectFrom(utxos []map[string]interface{}) *Tx {
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "collect_from",
- "collect_utxos": utxos,
- })
- return tx
-}
-
-func (tx *Tx) RegisterStakeAddress(address string) *Tx {
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "register_stake_address", "address": address,
- })
- return tx
-}
-
-func (tx *Tx) DeregisterStakeAddress(address string, refundAddress ...string) *Tx {
- op := map[string]interface{}{"type": "deregister_stake_address", "address": address}
- if len(refundAddress) > 0 && refundAddress[0] != "" {
- op["refund_address"] = refundAddress[0]
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) DelegateTo(address, poolID string) *Tx {
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "delegate_to", "address": address, "pool_id": poolID,
- })
- return tx
-}
-
-func (tx *Tx) Withdraw(rewardAddress string, amount int64, receiver ...string) *Tx {
- op := map[string]interface{}{
- "type": "withdraw", "reward_address": rewardAddress, "amount": fmt.Sprintf("%d", amount),
- }
- if len(receiver) > 0 && receiver[0] != "" {
- op["receiver"] = receiver[0]
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) RegisterDRep(credHash, credType string, anchor ...AnchorOption) *Tx {
- op := map[string]interface{}{
- "type": "register_drep", "credential_hash": credHash, "credential_type": credType,
- }
- if len(anchor) > 0 {
- if anchor[0].AnchorURL != "" {
- op["anchor_url"] = anchor[0].AnchorURL
- }
- if anchor[0].AnchorDataHash != "" {
- op["anchor_data_hash"] = anchor[0].AnchorDataHash
- }
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) UnregisterDRep(credHash, credType string, opts ...UnregisterDRepOption) *Tx {
- op := map[string]interface{}{
- "type": "unregister_drep", "credential_hash": credHash, "credential_type": credType,
- }
- if len(opts) > 0 {
- if opts[0].RefundAddress != "" {
- op["refund_address"] = opts[0].RefundAddress
- }
- if opts[0].RefundAmount != "" {
- op["refund_amount"] = opts[0].RefundAmount
- }
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) UpdateDRep(credHash, credType string, anchor ...AnchorOption) *Tx {
- op := map[string]interface{}{
- "type": "update_drep", "credential_hash": credHash, "credential_type": credType,
- }
- if len(anchor) > 0 {
- if anchor[0].AnchorURL != "" {
- op["anchor_url"] = anchor[0].AnchorURL
- }
- if anchor[0].AnchorDataHash != "" {
- op["anchor_data_hash"] = anchor[0].AnchorDataHash
- }
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) DelegateVotingPowerTo(address, drepType string, drepHash ...string) *Tx {
- op := map[string]interface{}{
- "type": "delegate_voting_power_to", "address": address, "drep_type": drepType,
- }
- if len(drepHash) > 0 && drepHash[0] != "" {
- op["drep_hash"] = drepHash[0]
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) CreateVote(voterType, voterHash, govActionTxHash string, govActionIndex int, vote string, anchor ...AnchorOption) *Tx {
- op := map[string]interface{}{
- "type": "create_vote", "voter_type": voterType, "voter_hash": voterHash,
- "gov_action_tx_hash": govActionTxHash, "gov_action_index": govActionIndex, "vote": vote,
- }
- if len(anchor) > 0 {
- if anchor[0].AnchorURL != "" {
- op["anchor_url"] = anchor[0].AnchorURL
- }
- if anchor[0].AnchorDataHash != "" {
- op["anchor_data_hash"] = anchor[0].AnchorDataHash
- }
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) CreateProposal(govActionType, returnAddress, anchorURL, anchorDataHash string, withdrawals ...[]ProposalWithdrawal) *Tx {
- op := map[string]interface{}{
- "type": "create_proposal", "gov_action_type": govActionType,
- "return_address": returnAddress, "anchor_url": anchorURL, "anchor_data_hash": anchorDataHash,
- }
- if len(withdrawals) > 0 && len(withdrawals[0]) > 0 {
- op["withdrawals"] = withdrawals[0]
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-// Pool Operations
-
-func (tx *Tx) RegisterPool(operator, vrfKeyHash, pledge, cost, marginNumerator, marginDenominator, rewardAddress string, poolOwners []string, opts ...PoolOptions) *Tx {
- op := map[string]interface{}{
- "type": "register_pool", "operator": operator, "vrf_key_hash": vrfKeyHash,
- "pledge": pledge, "cost": cost, "margin_numerator": marginNumerator,
- "margin_denominator": marginDenominator, "reward_address": rewardAddress,
- "pool_owners": poolOwners,
- }
- if len(opts) > 0 {
- if len(opts[0].Relays) > 0 {
- op["relays"] = opts[0].Relays
- }
- if opts[0].PoolMetadataURL != "" {
- op["pool_metadata_url"] = opts[0].PoolMetadataURL
- }
- if opts[0].PoolMetadataHash != "" {
- op["pool_metadata_hash"] = opts[0].PoolMetadataHash
- }
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) UpdatePool(operator, vrfKeyHash, pledge, cost, marginNumerator, marginDenominator, rewardAddress string, poolOwners []string, opts ...PoolOptions) *Tx {
- op := map[string]interface{}{
- "type": "update_pool", "operator": operator, "vrf_key_hash": vrfKeyHash,
- "pledge": pledge, "cost": cost, "margin_numerator": marginNumerator,
- "margin_denominator": marginDenominator, "reward_address": rewardAddress,
- "pool_owners": poolOwners,
- }
- if len(opts) > 0 {
- if len(opts[0].Relays) > 0 {
- op["relays"] = opts[0].Relays
- }
- if opts[0].PoolMetadataURL != "" {
- op["pool_metadata_url"] = opts[0].PoolMetadataURL
- }
- if opts[0].PoolMetadataHash != "" {
- op["pool_metadata_hash"] = opts[0].PoolMetadataHash
- }
- }
- tx.operations = append(tx.operations, op)
- return tx
-}
-
-func (tx *Tx) RetirePool(poolID string, epoch int) *Tx {
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "retire_pool", "pool_id": poolID, "epoch": epoch,
- })
- return tx
-}
-
-// Treasury
-
-func (tx *Tx) DonateToTreasury(treasuryValue, donationAmount string) *Tx {
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "donate_to_treasury", "treasury_value": treasuryValue, "donation_amount": donationAmount,
- })
- return tx
-}
-
-// Native Script
-
-func (tx *Tx) AttachNativeScript(scriptJSON string) *Tx {
- tx.operations = append(tx.operations, map[string]interface{}{
- "type": "attach_native_script", "script_json": scriptJSON,
- })
- return tx
-}
-
-func (tx *Tx) From(address string) *Tx {
- tx.from = address
- return tx
-}
-
-func (tx *Tx) ChangeAddress(address string) *Tx {
- tx.changeAddress = address
- return tx
-}
-
-func (tx *Tx) ToSpec() map[string]interface{} {
- spec := map[string]interface{}{
- "from": tx.from,
- "operations": tx.operations,
- }
- if tx.changeAddress != "" {
- spec["change_address"] = tx.changeAddress
- }
- return spec
-}
-
-// --- ComposeTxBuilder ---
-
-// ComposeTxBuilder composes multiple Composable objects into a single transaction.
-type ComposeTxBuilder struct {
- bridge *Bridge
- txs []Composable
- feePayer string
- utxos interface{}
- protocolParams interface{}
- validity map[string]interface{}
- mergeOutputs *bool
- signerCount *int
-}
-
-func (cb *ComposeTxBuilder) FeePayer(address string) *ComposeTxBuilder {
- cb.feePayer = address
- return cb
-}
-
-func (cb *ComposeTxBuilder) WithUtxos(utxos interface{}) *ComposeTxBuilder {
- cb.utxos = utxos
- return cb
-}
-
-func (cb *ComposeTxBuilder) WithProtocolParams(params interface{}) *ComposeTxBuilder {
- cb.protocolParams = params
- return cb
-}
-
-func (cb *ComposeTxBuilder) ValidFrom(slot int64) *ComposeTxBuilder {
- if cb.validity == nil {
- cb.validity = make(map[string]interface{})
- }
- cb.validity["valid_from"] = slot
- return cb
-}
-
-func (cb *ComposeTxBuilder) ValidTo(slot int64) *ComposeTxBuilder {
- if cb.validity == nil {
- cb.validity = make(map[string]interface{})
- }
- cb.validity["valid_to"] = slot
- return cb
-}
-
-func (cb *ComposeTxBuilder) MergeOutputs(merge bool) *ComposeTxBuilder {
- cb.mergeOutputs = &merge
- return cb
-}
-
-func (cb *ComposeTxBuilder) SignerCount(count int) *ComposeTxBuilder {
- cb.signerCount = &count
- return cb
-}
-
-// Build builds the composed transaction.
-func (cb *ComposeTxBuilder) Build() (*TxResult, error) {
- return cb.doBuild(nil)
-}
-
-// BuildWithProvider builds with a Java-side provider config.
-func (cb *ComposeTxBuilder) BuildWithProvider(config ProviderConfig) (*TxResult, error) {
- return cb.doBuild(&config)
-}
-
-func (cb *ComposeTxBuilder) doBuild(providerConfig *ProviderConfig) (*TxResult, error) {
- txSpecs := make([]map[string]interface{}, len(cb.txs))
- for i, tx := range cb.txs {
- txSpecs[i] = tx.ToSpec()
- }
-
- spec := map[string]interface{}{
- "transactions": txSpecs,
- "fee_payer": cb.feePayer,
- }
-
- if providerConfig != nil {
- spec["provider"] = providerConfig
- } else {
- spec["utxos"] = cb.utxos
- }
- if cb.protocolParams != nil {
- spec["protocol_params"] = cb.protocolParams
- }
- if cb.signerCount != nil {
- spec["signer_count"] = *cb.signerCount
- }
- if len(cb.validity) > 0 {
- spec["validity"] = cb.validity
- }
- if cb.mergeOutputs != nil {
- spec["merge_outputs"] = *cb.mergeOutputs
- }
-
- specJSON, err := json.Marshal(spec)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal compose spec: %w", err)
- }
-
- cs := cstr(string(specJSON))
- defer C.free(unsafe.Pointer(cs))
-
- rc := C.ccl_quicktx_build(cb.bridge.thread, cs)
- result, err := cb.bridge.check(rc)
- if err != nil {
- return nil, err
- }
-
- var txResult TxResult
- if err := json.Unmarshal([]byte(result), &txResult); err != nil {
- return nil, fmt.Errorf("failed to parse tx result: %w", err)
- }
- return &txResult, nil
-}
-
-// --- ScriptTxBuilder ---
-
-// ScriptTxBuilder builds a single script transaction spec.
-type ScriptTxBuilder struct {
- bridge *Bridge
- operations []map[string]interface{}
- from string
- changeAddress string
- feePayer string
- utxos interface{}
- protocolParams interface{}
- validity map[string]interface{}
- mergeOutputs *bool
- signerCount int
- changeDatumCbor string
- changeDatumHash string
-}
-
-func (sb *ScriptTxBuilder) PayToAddress(address string, amounts ...Amount) *ScriptTxBuilder {
- amountList := make([]Amount, len(amounts))
- copy(amountList, amounts)
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "pay_to_address",
- "address": address,
- "amounts": amountList,
- })
- return sb
-}
-
-// PayToAddressWithScriptRef is like PayToAddress but attaches a reference script to the output.
-func (sb *ScriptTxBuilder) PayToAddressWithScriptRef(address string, amounts []Amount, scriptRef ScriptRefOption) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "pay_to_address",
- "address": address,
- "amounts": amounts,
- }
- if scriptRef.ScriptRefCborHex != "" {
- op["script_ref_cbor_hex"] = scriptRef.ScriptRefCborHex
- }
- if scriptRef.ScriptRefType != "" {
- op["script_ref_type"] = scriptRef.ScriptRefType
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) PayToContract(address string, amounts []Amount, datumCborHex, datumHash string, scriptRef ...ScriptRefOption) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts,
- }
- if datumCborHex != "" {
- op["datum_cbor_hex"] = datumCborHex
- }
- if datumHash != "" {
- op["datum_hash"] = datumHash
- }
- if len(scriptRef) > 0 {
- if scriptRef[0].ScriptRefCborHex != "" {
- op["script_ref_cbor_hex"] = scriptRef[0].ScriptRefCborHex
- }
- if scriptRef[0].ScriptRefType != "" {
- op["script_ref_type"] = scriptRef[0].ScriptRefType
- }
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) AttachMetadata(label int, metadata interface{}) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- })
- return sb
-}
-
-func (sb *ScriptTxBuilder) CollectFrom(utxos []map[string]interface{}) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "collect_from",
- "collect_utxos": utxos,
- })
- return sb
-}
-
-func (sb *ScriptTxBuilder) CollectFromScript(utxos []map[string]interface{}, redeemerCborHex, datumCborHex string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "collect_from",
- "collect_utxos": utxos,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if datumCborHex != "" {
- op["datum_cbor_hex"] = datumCborHex
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) ReadFrom(referenceInputs []ReferenceInput) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "read_from",
- "reference_inputs": referenceInputs,
- })
- return sb
-}
-
-func (sb *ScriptTxBuilder) MintPlutusAssets(scriptCborHex, scriptType string, assets []MintAsset, redeemerCborHex, receiver, outputDatumCborHex string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "mint_plutus_assets",
- "script_cbor_hex": scriptCborHex,
- "script_type": scriptType,
- "assets": assets,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if receiver != "" {
- op["receiver"] = receiver
- }
- if outputDatumCborHex != "" {
- op["output_datum_cbor_hex"] = outputDatumCborHex
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) AttachSpendingValidator(scriptCborHex, scriptType string) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "attach_spending_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return sb
-}
-
-func (sb *ScriptTxBuilder) AttachCertificateValidator(scriptCborHex, scriptType string) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "attach_certificate_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return sb
-}
-
-func (sb *ScriptTxBuilder) AttachRewardValidator(scriptCborHex, scriptType string) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "attach_reward_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return sb
-}
-
-func (sb *ScriptTxBuilder) AttachProposingValidator(scriptCborHex, scriptType string) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "attach_proposing_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return sb
-}
-
-func (sb *ScriptTxBuilder) AttachVotingValidator(scriptCborHex, scriptType string) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "attach_voting_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return sb
-}
-
-// Redeemer-enhanced staking/governance
-
-func (sb *ScriptTxBuilder) DeregisterStakeAddress(address, redeemerCborHex, refundAddress string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "deregister_stake_address", "address": address, "redeemer_cbor_hex": redeemerCborHex,
- }
- if refundAddress != "" {
- op["refund_address"] = refundAddress
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) DelegateTo(address, poolID, redeemerCborHex string) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "delegate_to", "address": address, "pool_id": poolID, "redeemer_cbor_hex": redeemerCborHex,
- })
- return sb
-}
-
-func (sb *ScriptTxBuilder) Withdraw(rewardAddress, amount, redeemerCborHex, receiver string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "withdraw", "reward_address": rewardAddress, "amount": amount, "redeemer_cbor_hex": redeemerCborHex,
- }
- if receiver != "" {
- op["receiver"] = receiver
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) RegisterDRep(credHash, credType, redeemerCborHex, anchorURL, anchorDataHash string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "register_drep", "credential_hash": credHash, "credential_type": credType,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if anchorURL != "" {
- op["anchor_url"] = anchorURL
- }
- if anchorDataHash != "" {
- op["anchor_data_hash"] = anchorDataHash
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) UnregisterDRep(credHash, credType, redeemerCborHex string, opts ...UnregisterDRepOption) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "unregister_drep", "credential_hash": credHash, "credential_type": credType,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if len(opts) > 0 {
- if opts[0].RefundAddress != "" {
- op["refund_address"] = opts[0].RefundAddress
- }
- if opts[0].RefundAmount != "" {
- op["refund_amount"] = opts[0].RefundAmount
- }
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) UpdateDRep(credHash, credType, redeemerCborHex, anchorURL, anchorDataHash string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "update_drep", "credential_hash": credHash, "credential_type": credType,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if anchorURL != "" {
- op["anchor_url"] = anchorURL
- }
- if anchorDataHash != "" {
- op["anchor_data_hash"] = anchorDataHash
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) DelegateVotingPowerTo(address, drepType, drepHash, redeemerCborHex string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "delegate_voting_power_to", "address": address, "drep_type": drepType,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if drepHash != "" {
- op["drep_hash"] = drepHash
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) CreateVote(voterType, voterHash, govActionTxHash string, govActionIndex int, vote, redeemerCborHex, anchorURL, anchorDataHash string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "create_vote", "voter_type": voterType, "voter_hash": voterHash,
- "gov_action_tx_hash": govActionTxHash, "gov_action_index": govActionIndex, "vote": vote,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if anchorURL != "" {
- op["anchor_url"] = anchorURL
- }
- if anchorDataHash != "" {
- op["anchor_data_hash"] = anchorDataHash
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-func (sb *ScriptTxBuilder) CreateProposal(govActionType, returnAddress, anchorURL, anchorDataHash, redeemerCborHex string, withdrawals []map[string]string) *ScriptTxBuilder {
- op := map[string]interface{}{
- "type": "create_proposal", "gov_action_type": govActionType,
- "return_address": returnAddress, "anchor_url": anchorURL, "anchor_data_hash": anchorDataHash,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if len(withdrawals) > 0 {
- op["withdrawals"] = withdrawals
- }
- sb.operations = append(sb.operations, op)
- return sb
-}
-
-// Treasury
-
-func (sb *ScriptTxBuilder) DonateToTreasury(treasuryValue, donationAmount, redeemerCborHex string) *ScriptTxBuilder {
- sb.operations = append(sb.operations, map[string]interface{}{
- "type": "donate_to_treasury", "treasury_value": treasuryValue,
- "donation_amount": donationAmount, "redeemer_cbor_hex": redeemerCborHex,
- })
- return sb
-}
-
-// Config
-
-func (sb *ScriptTxBuilder) From(address string) *ScriptTxBuilder {
- sb.from = address
- return sb
-}
-
-func (sb *ScriptTxBuilder) ChangeAddress(address string) *ScriptTxBuilder {
- sb.changeAddress = address
- return sb
-}
-
-func (sb *ScriptTxBuilder) ChangeDatum(datumCborHex string) *ScriptTxBuilder {
- sb.changeDatumCbor = datumCborHex
- return sb
-}
-
-func (sb *ScriptTxBuilder) ChangeDatumHash(hash string) *ScriptTxBuilder {
- sb.changeDatumHash = hash
- return sb
-}
-
-func (sb *ScriptTxBuilder) FeePayer(address string) *ScriptTxBuilder {
- sb.feePayer = address
- return sb
-}
-
-func (sb *ScriptTxBuilder) WithUtxos(utxos interface{}) *ScriptTxBuilder {
- sb.utxos = utxos
- return sb
-}
-
-func (sb *ScriptTxBuilder) WithProtocolParams(params interface{}) *ScriptTxBuilder {
- sb.protocolParams = params
- return sb
-}
-
-func (sb *ScriptTxBuilder) ValidFrom(slot int64) *ScriptTxBuilder {
- if sb.validity == nil {
- sb.validity = make(map[string]interface{})
- }
- sb.validity["valid_from"] = slot
- return sb
-}
-
-func (sb *ScriptTxBuilder) ValidTo(slot int64) *ScriptTxBuilder {
- if sb.validity == nil {
- sb.validity = make(map[string]interface{})
- }
- sb.validity["valid_to"] = slot
- return sb
-}
-
-func (sb *ScriptTxBuilder) MergeOutputs(merge bool) *ScriptTxBuilder {
- sb.mergeOutputs = &merge
- return sb
-}
-
-func (sb *ScriptTxBuilder) SignerCount(count int) *ScriptTxBuilder {
- sb.signerCount = count
- return sb
-}
-
-func (sb *ScriptTxBuilder) buildSpec(providerConfig *ProviderConfig) map[string]interface{} {
- spec := map[string]interface{}{
- "tx_type": "script_tx",
- "operations": sb.operations,
- "from": sb.from,
- "signer_count": sb.signerCount,
- }
- if providerConfig != nil {
- spec["provider"] = providerConfig
- } else {
- spec["utxos"] = sb.utxos
- }
- if sb.protocolParams != nil {
- spec["protocol_params"] = sb.protocolParams
- }
- if sb.changeAddress != "" {
- spec["change_address"] = sb.changeAddress
- }
- if sb.feePayer != "" {
- spec["fee_payer"] = sb.feePayer
- }
- if len(sb.validity) > 0 {
- spec["validity"] = sb.validity
- }
- if sb.mergeOutputs != nil {
- spec["merge_outputs"] = *sb.mergeOutputs
- }
- if sb.changeDatumCbor != "" {
- spec["change_datum_cbor_hex"] = sb.changeDatumCbor
- }
- if sb.changeDatumHash != "" {
- spec["change_datum_hash"] = sb.changeDatumHash
- }
- return spec
-}
-
-// Build builds the script transaction. Returns TxResult with tx_cbor, tx_hash, fee.
-func (sb *ScriptTxBuilder) Build() (*TxResult, error) {
- return sb.doBuild(nil)
-}
-
-// BuildWithProvider builds with a Java-side provider config for lazy UTXO fetching.
-func (sb *ScriptTxBuilder) BuildWithProvider(config ProviderConfig) (*TxResult, error) {
- return sb.doBuild(&config)
-}
-
-func (sb *ScriptTxBuilder) doBuild(providerConfig *ProviderConfig) (*TxResult, error) {
- spec := sb.buildSpec(providerConfig)
- specJSON, err := json.Marshal(spec)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal spec: %w", err)
- }
-
- cs := cstr(string(specJSON))
- defer C.free(unsafe.Pointer(cs))
-
- rc := C.ccl_quicktx_build(sb.bridge.thread, cs)
- result, err := sb.bridge.check(rc)
- if err != nil {
- return nil, err
- }
-
- var txResult TxResult
- if err := json.Unmarshal([]byte(result), &txResult); err != nil {
- return nil, fmt.Errorf("failed to parse tx result: %w", err)
- }
- return &txResult, nil
-}
-
-// --- ScriptTx (for Compose) ---
-
-// ScriptTx is a lightweight operation collector for script transactions in Compose.
-type ScriptTx struct {
- operations []map[string]interface{}
- from string
- changeAddress string
- changeDatumCbor string
- changeDatumHash string
-}
-
-func (st *ScriptTx) PayToAddress(address string, amounts ...Amount) *ScriptTx {
- amountList := make([]Amount, len(amounts))
- copy(amountList, amounts)
- st.operations = append(st.operations, map[string]interface{}{
- "type": "pay_to_address",
- "address": address,
- "amounts": amountList,
- })
- return st
-}
-
-// PayToAddressWithScriptRef is like PayToAddress but attaches a reference script to the output.
-func (st *ScriptTx) PayToAddressWithScriptRef(address string, amounts []Amount, scriptRef ScriptRefOption) *ScriptTx {
- op := map[string]interface{}{
- "type": "pay_to_address",
- "address": address,
- "amounts": amounts,
- }
- if scriptRef.ScriptRefCborHex != "" {
- op["script_ref_cbor_hex"] = scriptRef.ScriptRefCborHex
- }
- if scriptRef.ScriptRefType != "" {
- op["script_ref_type"] = scriptRef.ScriptRefType
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) PayToContract(address string, amounts []Amount, datumCborHex, datumHash string, scriptRef ...ScriptRefOption) *ScriptTx {
- op := map[string]interface{}{
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts,
- }
- if datumCborHex != "" {
- op["datum_cbor_hex"] = datumCborHex
- }
- if datumHash != "" {
- op["datum_hash"] = datumHash
- }
- if len(scriptRef) > 0 {
- if scriptRef[0].ScriptRefCborHex != "" {
- op["script_ref_cbor_hex"] = scriptRef[0].ScriptRefCborHex
- }
- if scriptRef[0].ScriptRefType != "" {
- op["script_ref_type"] = scriptRef[0].ScriptRefType
- }
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) AttachMetadata(label int, metadata interface{}) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- })
- return st
-}
-
-func (st *ScriptTx) CollectFrom(utxos []map[string]interface{}) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "collect_from",
- "collect_utxos": utxos,
- })
- return st
-}
-
-func (st *ScriptTx) CollectFromScript(utxos []map[string]interface{}, redeemerCborHex, datumCborHex string) *ScriptTx {
- op := map[string]interface{}{
- "type": "collect_from",
- "collect_utxos": utxos,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if datumCborHex != "" {
- op["datum_cbor_hex"] = datumCborHex
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) ReadFrom(referenceInputs []ReferenceInput) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "read_from",
- "reference_inputs": referenceInputs,
- })
- return st
-}
-
-func (st *ScriptTx) MintPlutusAssets(scriptCborHex, scriptType string, assets []MintAsset, redeemerCborHex, receiver, outputDatumCborHex string) *ScriptTx {
- op := map[string]interface{}{
- "type": "mint_plutus_assets",
- "script_cbor_hex": scriptCborHex,
- "script_type": scriptType,
- "assets": assets,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if receiver != "" {
- op["receiver"] = receiver
- }
- if outputDatumCborHex != "" {
- op["output_datum_cbor_hex"] = outputDatumCborHex
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) AttachSpendingValidator(scriptCborHex, scriptType string) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "attach_spending_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return st
-}
-
-func (st *ScriptTx) AttachCertificateValidator(scriptCborHex, scriptType string) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "attach_certificate_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return st
-}
-
-func (st *ScriptTx) AttachRewardValidator(scriptCborHex, scriptType string) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "attach_reward_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return st
-}
-
-func (st *ScriptTx) AttachProposingValidator(scriptCborHex, scriptType string) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "attach_proposing_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return st
-}
-
-func (st *ScriptTx) AttachVotingValidator(scriptCborHex, scriptType string) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "attach_voting_validator", "script_cbor_hex": scriptCborHex, "script_type": scriptType,
- })
- return st
-}
-
-// Redeemer-enhanced staking/governance
-
-func (st *ScriptTx) DeregisterStakeAddress(address, redeemerCborHex, refundAddress string) *ScriptTx {
- op := map[string]interface{}{
- "type": "deregister_stake_address", "address": address, "redeemer_cbor_hex": redeemerCborHex,
- }
- if refundAddress != "" {
- op["refund_address"] = refundAddress
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) DelegateTo(address, poolID, redeemerCborHex string) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "delegate_to", "address": address, "pool_id": poolID, "redeemer_cbor_hex": redeemerCborHex,
- })
- return st
-}
-
-func (st *ScriptTx) Withdraw(rewardAddress, amount, redeemerCborHex, receiver string) *ScriptTx {
- op := map[string]interface{}{
- "type": "withdraw", "reward_address": rewardAddress, "amount": amount, "redeemer_cbor_hex": redeemerCborHex,
- }
- if receiver != "" {
- op["receiver"] = receiver
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) RegisterDRep(credHash, credType, redeemerCborHex, anchorURL, anchorDataHash string) *ScriptTx {
- op := map[string]interface{}{
- "type": "register_drep", "credential_hash": credHash, "credential_type": credType,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if anchorURL != "" {
- op["anchor_url"] = anchorURL
- }
- if anchorDataHash != "" {
- op["anchor_data_hash"] = anchorDataHash
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) UnregisterDRep(credHash, credType, redeemerCborHex string, opts ...UnregisterDRepOption) *ScriptTx {
- op := map[string]interface{}{
- "type": "unregister_drep", "credential_hash": credHash, "credential_type": credType,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if len(opts) > 0 {
- if opts[0].RefundAddress != "" {
- op["refund_address"] = opts[0].RefundAddress
- }
- if opts[0].RefundAmount != "" {
- op["refund_amount"] = opts[0].RefundAmount
- }
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) UpdateDRep(credHash, credType, redeemerCborHex, anchorURL, anchorDataHash string) *ScriptTx {
- op := map[string]interface{}{
- "type": "update_drep", "credential_hash": credHash, "credential_type": credType,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if anchorURL != "" {
- op["anchor_url"] = anchorURL
- }
- if anchorDataHash != "" {
- op["anchor_data_hash"] = anchorDataHash
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) DelegateVotingPowerTo(address, drepType, drepHash, redeemerCborHex string) *ScriptTx {
- op := map[string]interface{}{
- "type": "delegate_voting_power_to", "address": address, "drep_type": drepType,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if drepHash != "" {
- op["drep_hash"] = drepHash
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) CreateVote(voterType, voterHash, govActionTxHash string, govActionIndex int, vote, redeemerCborHex, anchorURL, anchorDataHash string) *ScriptTx {
- op := map[string]interface{}{
- "type": "create_vote", "voter_type": voterType, "voter_hash": voterHash,
- "gov_action_tx_hash": govActionTxHash, "gov_action_index": govActionIndex, "vote": vote,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if anchorURL != "" {
- op["anchor_url"] = anchorURL
- }
- if anchorDataHash != "" {
- op["anchor_data_hash"] = anchorDataHash
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-func (st *ScriptTx) CreateProposal(govActionType, returnAddress, anchorURL, anchorDataHash, redeemerCborHex string, withdrawals []map[string]string) *ScriptTx {
- op := map[string]interface{}{
- "type": "create_proposal", "gov_action_type": govActionType,
- "return_address": returnAddress, "anchor_url": anchorURL, "anchor_data_hash": anchorDataHash,
- "redeemer_cbor_hex": redeemerCborHex,
- }
- if len(withdrawals) > 0 {
- op["withdrawals"] = withdrawals
- }
- st.operations = append(st.operations, op)
- return st
-}
-
-// Treasury
-
-func (st *ScriptTx) DonateToTreasury(treasuryValue, donationAmount, redeemerCborHex string) *ScriptTx {
- st.operations = append(st.operations, map[string]interface{}{
- "type": "donate_to_treasury", "treasury_value": treasuryValue,
- "donation_amount": donationAmount, "redeemer_cbor_hex": redeemerCborHex,
- })
- return st
-}
-
-func (st *ScriptTx) From(address string) *ScriptTx {
- st.from = address
- return st
-}
-
-func (st *ScriptTx) ChangeAddress(address string) *ScriptTx {
- st.changeAddress = address
- return st
-}
-
-func (st *ScriptTx) ChangeDatum(datumCborHex string) *ScriptTx {
- st.changeDatumCbor = datumCborHex
- return st
-}
-
-func (st *ScriptTx) ChangeDatumHash(hash string) *ScriptTx {
- st.changeDatumHash = hash
- return st
-}
-
-func (st *ScriptTx) ToSpec() map[string]interface{} {
- spec := map[string]interface{}{
- "tx_type": "script_tx",
- "from": st.from,
- "operations": st.operations,
- }
- if st.changeAddress != "" {
- spec["change_address"] = st.changeAddress
- }
- if st.changeDatumCbor != "" {
- spec["change_datum_cbor_hex"] = st.changeDatumCbor
- }
- if st.changeDatumHash != "" {
- spec["change_datum_hash"] = st.changeDatumHash
- }
- return spec
-}
diff --git a/wrappers/go/ccl/ccl_test.go b/wrappers/go/ccl/ccl_test.go
index 3d76415..18dee72 100644
--- a/wrappers/go/ccl/ccl_test.go
+++ b/wrappers/go/ccl/ccl_test.go
@@ -469,51 +469,51 @@ var fakeTxHash = strings.Repeat("a", 64)
func testProtocolParams() map[string]interface{} {
return map[string]interface{}{
- "min_fee_a": 44,
- "min_fee_b": 155381,
- "max_block_size": 65536,
- "max_tx_size": 16384,
- "max_block_header_size": 1100,
- "key_deposit": "2000000",
- "pool_deposit": "500000000",
- "e_max": 18,
- "n_opt": 500,
- "a0": 0.3,
- "rho": 0.003,
- "tau": 0.2,
- "min_utxo": "34482",
- "min_pool_cost": "340000000",
- "price_mem": 0.0577,
- "price_step": 0.0000721,
- "max_tx_ex_mem": "10000000",
- "max_tx_ex_steps": "10000000000",
- "max_block_ex_mem": "50000000",
- "max_block_ex_steps": "40000000000",
- "max_val_size": "5000",
- "collateral_percent": 150,
- "max_collateral_inputs": 3,
- "coins_per_utxo_size": "4310",
- "coins_per_utxo_word": "34482",
- "pvt_motion_no_confidence": 0.51,
- "pvt_committee_normal": 0.51,
- "pvt_committee_no_confidence": 0.51,
- "pvt_hard_fork_initiation": 0.51,
- "dvt_motion_no_confidence": 0.51,
- "dvt_committee_normal": 0.51,
- "dvt_committee_no_confidence": 0.51,
- "dvt_update_to_constitution": 0.51,
- "dvt_hard_fork_initiation": 0.51,
- "dvt_ppnetwork_group": 0.51,
- "dvt_ppeconomic_group": 0.51,
- "dvt_pptechnical_group": 0.51,
- "dvt_ppgov_group": 0.51,
- "dvt_treasury_withdrawal": 0.51,
- "committee_min_size": 0,
- "committee_max_term_length": 200,
- "gov_action_lifetime": 10,
- "gov_action_deposit": 1000000000,
- "drep_deposit": 2000000,
- "drep_activity": 20,
+ "min_fee_a": 44,
+ "min_fee_b": 155381,
+ "max_block_size": 65536,
+ "max_tx_size": 16384,
+ "max_block_header_size": 1100,
+ "key_deposit": "2000000",
+ "pool_deposit": "500000000",
+ "e_max": 18,
+ "n_opt": 500,
+ "a0": 0.3,
+ "rho": 0.003,
+ "tau": 0.2,
+ "min_utxo": "34482",
+ "min_pool_cost": "340000000",
+ "price_mem": 0.0577,
+ "price_step": 0.0000721,
+ "max_tx_ex_mem": "10000000",
+ "max_tx_ex_steps": "10000000000",
+ "max_block_ex_mem": "50000000",
+ "max_block_ex_steps": "40000000000",
+ "max_val_size": "5000",
+ "collateral_percent": 150,
+ "max_collateral_inputs": 3,
+ "coins_per_utxo_size": "4310",
+ "coins_per_utxo_word": "34482",
+ "pvt_motion_no_confidence": 0.51,
+ "pvt_committee_normal": 0.51,
+ "pvt_committee_no_confidence": 0.51,
+ "pvt_hard_fork_initiation": 0.51,
+ "dvt_motion_no_confidence": 0.51,
+ "dvt_committee_normal": 0.51,
+ "dvt_committee_no_confidence": 0.51,
+ "dvt_update_to_constitution": 0.51,
+ "dvt_hard_fork_initiation": 0.51,
+ "dvt_ppnetwork_group": 0.51,
+ "dvt_ppeconomic_group": 0.51,
+ "dvt_pptechnical_group": 0.51,
+ "dvt_ppgov_group": 0.51,
+ "dvt_treasury_withdrawal": 0.51,
+ "committee_min_size": 0,
+ "committee_max_term_length": 200,
+ "gov_action_lifetime": 10,
+ "gov_action_deposit": 1000000000,
+ "drep_deposit": 2000000,
+ "drep_activity": 20,
"min_fee_ref_script_cost_per_byte": 44,
}
}
@@ -546,644 +546,98 @@ func assertTxResult(t *testing.T, result *TxResult) {
}
}
-func TestQuickTxSimpleADAPayment(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- receiver, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver: %v", err)
- }
-
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(5)).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxMultipleReceivers(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- receiver1, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver1: %v", err)
- }
- receiver2, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver2: %v", err)
- }
-
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver1.BaseAddress, Ada(5)).
- PayToAddress(receiver2.BaseAddress, Ada(3)).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxPayToContract(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- receiver, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver: %v", err)
- }
-
- result, err := bridge.QuickTx.NewTx().
- PayToContract(receiver.BaseAddress, []Amount{Ada(5)}, "182a", "").
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxMintAssets(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- addrInfo, err := bridge.Address.Info(sender.BaseAddress)
- if err != nil {
- t.Fatalf("address info: %v", err)
- }
-
- scriptJSON := fmt.Sprintf(`{"type":"sig","keyHash":"%s"}`, addrInfo.PaymentCredentialHash)
- assets := []MintAsset{{Name: "TestToken", Quantity: "1000"}}
-
- result, err := bridge.QuickTx.NewTx().
- MintAssets(scriptJSON, assets, sender.BaseAddress).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxAttachMetadata(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- receiver, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver: %v", err)
- }
-
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(2)).
- AttachMetadata(674, map[string]interface{}{"msg": []string{"Hello from Go"}}).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxCollectFrom(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- receiver, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver: %v", err)
- }
-
- utxos := makeUtxos(sender.BaseAddress, 100_000_000)
- result, err := bridge.QuickTx.NewTx().
- CollectFrom(utxos).
- PayToAddress(receiver.BaseAddress, Ada(2)).
- From(sender.BaseAddress).
- WithUtxos(utxos).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxRegisterStakeAddress(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- result, err := bridge.QuickTx.NewTx().
- RegisterStakeAddress(sender.BaseAddress).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxDeregisterStakeAddress(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- result, err := bridge.QuickTx.NewTx().
- DeregisterStakeAddress(sender.BaseAddress).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxDelegateTo(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- poolID := "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
- result, err := bridge.QuickTx.NewTx().
- DelegateTo(sender.BaseAddress, poolID).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxWithdraw(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- restored, err := bridge.Account.FromMnemonic(sender.Mnemonic, Testnet, 0, 0)
- if err != nil {
- t.Fatalf("restore: %v", err)
- }
-
- result, err := bridge.QuickTx.NewTx().
- Withdraw(restored.StakeAddress, 5000000).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxRegisterDRep(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- credHash := strings.Repeat("ab", 28)
- result, err := bridge.QuickTx.NewTx().
- RegisterDRep(credHash, "key").
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxUnregisterDRep(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- credHash := strings.Repeat("ab", 28)
- result, err := bridge.QuickTx.NewTx().
- UnregisterDRep(credHash, "key").
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- // The UnregisterDRep now takes optional UnregisterDRepOption - calling without opts should work
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxUpdateDRep(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- credHash := strings.Repeat("ab", 28)
- dataHash := strings.Repeat("cd", 32)
- result, err := bridge.QuickTx.NewTx().
- UpdateDRep(credHash, "key", AnchorOption{
- AnchorURL: "https://example.com/drep-v2.json",
- AnchorDataHash: dataHash,
- }).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
+// quickTxYaml is a single-payment TxPlan YAML document.
+func quickTxYaml(from, to, quantity string) string {
+ return fmt.Sprintf(`
+version: 1.0
+transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "%s"
+`, from, to, quantity)
}
-func TestQuickTxDelegateVotingPowerTo(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
+func TestQuickTxSimplePayment(t *testing.T) {
+ sender, _ := bridge.Account.Create(Testnet)
+ receiver, _ := bridge.Account.Create(Testnet)
- drepHash := strings.Repeat("ab", 28)
- result, err := bridge.QuickTx.NewTx().
- DelegateVotingPowerTo(sender.BaseAddress, "key_hash", drepHash).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
+ yaml := quickTxYaml(sender.BaseAddress, receiver.BaseAddress, "5000000")
+ result, err := bridge.QuickTx.Build(yaml, makeUtxos(sender.BaseAddress, 100_000_000), testProtocolParams())
if err != nil {
t.Fatalf("Build() failed: %v", err)
}
assertTxResult(t, result)
}
-func TestQuickTxCreateVote(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- voterHash := strings.Repeat("ab", 28)
- govTxHash := strings.Repeat("cd", 32)
- result, err := bridge.QuickTx.NewTx().
- CreateVote("drep_key_hash", voterHash, govTxHash, 0, "yes").
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
+func TestQuickTxMultiplePayments(t *testing.T) {
+ sender, _ := bridge.Account.Create(Testnet)
+ r1, _ := bridge.Account.Create(Testnet)
+ r2, _ := bridge.Account.Create(Testnet)
+
+ yaml := fmt.Sprintf(`
+version: 1.0
+transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
+`, sender.BaseAddress, r1.BaseAddress, r2.BaseAddress)
+
+ result, err := bridge.QuickTx.Build(yaml, makeUtxos(sender.BaseAddress, 100_000_000), testProtocolParams())
if err != nil {
t.Fatalf("Build() failed: %v", err)
}
assertTxResult(t, result)
}
-func TestQuickTxCreateInfoProposal(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- restored, err := bridge.Account.FromMnemonic(sender.Mnemonic, Testnet, 0, 0)
- if err != nil {
- t.Fatalf("restore: %v", err)
- }
-
- anchorDataHash := strings.Repeat("ab", 32)
- result, err := bridge.QuickTx.NewTx().
- CreateProposal("info_action", restored.StakeAddress,
- "https://example.com/proposal.json", anchorDataHash).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 2_000_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxCreateTreasuryWithdrawalsProposal(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- restored, err := bridge.Account.FromMnemonic(sender.Mnemonic, Testnet, 0, 0)
- if err != nil {
- t.Fatalf("restore: %v", err)
- }
-
- anchorDataHash := strings.Repeat("ab", 32)
- withdrawals := []ProposalWithdrawal{
- {RewardAddress: restored.StakeAddress, Amount: "1000000"},
- }
- result, err := bridge.QuickTx.NewTx().
- CreateProposal("treasury_withdrawals", restored.StakeAddress,
- "https://example.com/proposal.json", anchorDataHash, withdrawals).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 2_000_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxCompose(t *testing.T) {
- sender1, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender1: %v", err)
- }
- sender2, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender2: %v", err)
- }
- receiver1, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver1: %v", err)
- }
- receiver2, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver2: %v", err)
- }
-
- tx1 := bridge.QuickTx.Tx().
- PayToAddress(receiver1.BaseAddress, Ada(5)).
- From(sender1.BaseAddress)
-
- tx2 := bridge.QuickTx.Tx().
- PayToAddress(receiver2.BaseAddress, Ada(3)).
- From(sender2.BaseAddress)
-
- utxos := []map[string]interface{}{
- {
- "tx_hash": fakeTxHash,
- "output_index": 0,
- "address": sender1.BaseAddress,
- "amount": []map[string]interface{}{
- {"unit": "lovelace", "quantity": "100000000"},
- },
- },
- {
- "tx_hash": strings.Repeat("b", 64),
- "output_index": 0,
- "address": sender2.BaseAddress,
- "amount": []map[string]interface{}{
- {"unit": "lovelace", "quantity": "100000000"},
- },
- },
- }
-
- result, err := bridge.QuickTx.Compose(tx1, tx2).
- FeePayer(sender1.BaseAddress).
- WithUtxos(utxos).
- WithProtocolParams(testProtocolParams()).
- SignerCount(2).
- Build()
+func TestQuickTxVariableSubstitution(t *testing.T) {
+ sender, _ := bridge.Account.Create(Testnet)
+ receiver, _ := bridge.Account.Create(Testnet)
+
+ yaml := fmt.Sprintf(`
+version: 1.0
+variables:
+ to: %s
+ amount: "4000000"
+transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: ${to}
+ amounts:
+ - unit: lovelace
+ quantity: ${amount}
+`, receiver.BaseAddress, sender.BaseAddress)
+
+ result, err := bridge.QuickTx.Build(yaml, makeUtxos(sender.BaseAddress, 100_000_000), testProtocolParams())
if err != nil {
t.Fatalf("Build() failed: %v", err)
}
assertTxResult(t, result)
}
-func TestQuickTxProviderConfig(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- receiver, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver: %v", err)
- }
-
- // This test verifies the spec is built correctly with provider config.
- // It will fail at the FFI level because there's no actual provider,
- // but we verify the builder doesn't error on spec construction.
- _, err = bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(5)).
- From(sender.BaseAddress).
- WithProtocolParams(testProtocolParams()).
- BuildWithProvider(ProviderConfig{
- Name: "yaci_devkit",
- URL: "http://localhost:3000",
- })
- // Provider config will attempt Java-side HTTP; expect an error
- if err == nil {
- t.Log("BuildWithProvider succeeded (provider was reachable)")
- }
-}
-
func TestQuickTxInsufficientFunds(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- receiver, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver: %v", err)
- }
+ sender, _ := bridge.Account.Create(Testnet)
+ receiver, _ := bridge.Account.Create(Testnet)
- _, err = bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(200)).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 1_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
+ yaml := quickTxYaml(sender.BaseAddress, receiver.BaseAddress, "200000000")
+ _, err := bridge.QuickTx.Build(yaml, makeUtxos(sender.BaseAddress, 1_000_000), testProtocolParams())
if err == nil {
- t.Fatal("expected error for insufficient funds")
- }
- cclErr, ok := err.(*CclError)
- if !ok {
- t.Fatalf("expected CclError, got %T: %v", err, err)
+ t.Fatal("expected insufficient funds error")
}
- if cclErr.Code >= 0 {
- t.Errorf("expected negative error code, got %d", cclErr.Code)
- }
-}
-
-func TestQuickTxRegisterPool(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- operatorHash := strings.Repeat("ab", 28) // 28-byte (56 hex chars) pool operator key hash
- vrfKeyHash := strings.Repeat("cd", 32) // 32-byte (64 hex chars) VRF key hash
- rewardAccountHex := "e0" + strings.Repeat("ab", 28) // testnet reward address (hex)
- result, err := bridge.QuickTx.NewTx().
- RegisterPool(operatorHash, vrfKeyHash, "500000000", "340000000", "1", "5",
- rewardAccountHex, []string{operatorHash}).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 1_000_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxUpdatePool(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- operatorHash := strings.Repeat("ab", 28) // 28-byte (56 hex chars) pool operator key hash
- vrfKeyHash := strings.Repeat("cd", 32) // 32-byte (64 hex chars) VRF key hash
- metadataHash := strings.Repeat("ef", 32) // 32-byte (64 hex chars) metadata hash
- rewardAccountHex := "e0" + strings.Repeat("ab", 28) // testnet reward address (hex)
- result, err := bridge.QuickTx.NewTx().
- UpdatePool(operatorHash, vrfKeyHash, "600000000", "340000000", "1", "10",
- rewardAccountHex, []string{operatorHash}, PoolOptions{
- PoolMetadataURL: "https://example.com/pool.json",
- PoolMetadataHash: metadataHash,
- }).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 1_000_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxRetirePool(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- poolID := "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
- result, err := bridge.QuickTx.NewTx().
- RetirePool(poolID, 100).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxDonateToTreasury(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- result, err := bridge.QuickTx.NewTx().
- DonateToTreasury("5000000", "1000000").
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxAttachNativeScript(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- addrInfo, err := bridge.Address.Info(sender.BaseAddress)
- if err != nil {
- t.Fatalf("address info: %v", err)
- }
-
- scriptJSON := fmt.Sprintf(`{"type":"sig","keyHash":"%s"}`, addrInfo.PaymentCredentialHash)
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(sender.BaseAddress, Ada(2)).
- AttachNativeScript(scriptJSON).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxPayWithScriptRef(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
- receiver, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver: %v", err)
- }
-
- // Always-succeeds PlutusV3 script CBOR
- scriptRefCbor := "46450101002499"
-
- result, err := bridge.QuickTx.NewTx().
- PayToAddressWithScriptRef(receiver.BaseAddress, []Amount{Ada(5)}, ScriptRefOption{
- ScriptRefCborHex: scriptRefCbor,
- ScriptRefType: "plutus_v3",
- }).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
-}
-
-func TestQuickTxUnregisterDRepWithRefundAmount(t *testing.T) {
- sender, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create sender: %v", err)
- }
-
- credHash := strings.Repeat("ab", 28)
- result, err := bridge.QuickTx.NewTx().
- UnregisterDRep(credHash, "key", UnregisterDRepOption{RefundAmount: "2000000"}).
- From(sender.BaseAddress).
- WithUtxos(makeUtxos(sender.BaseAddress, 100_000_000)).
- WithProtocolParams(testProtocolParams()).
- Build()
- if err != nil {
- t.Fatalf("Build() failed: %v", err)
- }
- assertTxResult(t, result)
}
diff --git a/wrappers/go/ccl/intents_integration_test.go b/wrappers/go/ccl/intents_integration_test.go
new file mode 100644
index 0000000..b3e185a
--- /dev/null
+++ b/wrappers/go/ccl/intents_integration_test.go
@@ -0,0 +1,461 @@
+package ccl
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+)
+
+// End-to-end submit tests: build each intent's TxPlan offline, sign it with the right key roles,
+// submit it to a Yaci DevKit devnet, and assert the node accepted it (the tx is retrievable
+// on-chain). This proves the bridge produces node-acceptable transactions — not just buildable CBOR.
+//
+// They use the fixed test account the fixtures are derived from (intentMnemonic / intentSender),
+// funded fresh on the devnet per test for isolation. They skip when DevKit is not running, so they
+// are exercised only by the CI "Integration Tests (DevKit)" job, not locally.
+
+func readIntentFixture(t *testing.T, rel string) string {
+ t.Helper()
+ b, err := os.ReadFile("../../../test-fixtures/quicktx-intents/" + rel)
+ if err != nil {
+ t.Fatalf("read fixture %s: %v", rel, err)
+ }
+ return string(b)
+}
+
+// buildSignSubmit resets the devnet, funds the fixed account, builds the fixture with its real
+// UTXOs, signs with the given key roles, submits, and verifies the tx landed on-chain.
+func buildSignSubmit(t *testing.T, fixture string, execUnits []map[string]interface{}, keys ...string) string {
+ t.Helper()
+ devkitReset()
+ waitForBlock()
+ if err := devkitTopup(intentSender, 6000); err != nil {
+ t.Fatalf("topup: %v", err)
+ }
+ waitForBlock()
+
+ utxos, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ return signSubmit(t, readIntentFixture(t, fixture), utxos, devnetPP(t), execUnits, keys...)
+}
+
+// devnetPP fetches the devnet protocol parameters and fills in the Conway deposits DevKit returns as
+// null (the node validates the actual values on submit).
+func devnetPP(t *testing.T) map[string]interface{} {
+ t.Helper()
+ pp, err := devkitGetProtocolParams()
+ if err != nil {
+ t.Fatalf("get protocol params: %v", err)
+ }
+ pp["drep_deposit"] = "500000000"
+ pp["gov_action_deposit"] = "1000000000"
+ pp["pool_deposit"] = "500000000"
+ return pp
+}
+
+// signSubmit builds the YAML with the given UTXOs + params, signs with the key roles, and submits.
+// The devnet's /tx/submit returns 200/202 only after the node has validated and accepted the tx (a
+// rejected tx gets a 400 with the ledger error) — that acceptance is the proof.
+func signSubmit(t *testing.T, yaml string, utxos []map[string]interface{}, pp map[string]interface{}, execUnits []map[string]interface{}, keys ...string) string {
+ t.Helper()
+ var result *TxResult
+ var err error
+ if execUnits != nil {
+ result, err = bridge.QuickTx.Build(yaml, utxos, pp, execUnits)
+ } else {
+ result, err = bridge.QuickTx.Build(yaml, utxos, pp)
+ }
+ if err != nil {
+ t.Fatalf("build: %v", err)
+ }
+ signed, err := bridge.Account.SignTxWithKeys(intentMnemonic, Testnet, 0, 0, result.TxCbor, keys...)
+ if err != nil {
+ t.Fatalf("sign: %v", err)
+ }
+ txHash, err := devkitSubmitTx(signed)
+ if err != nil {
+ t.Fatalf("submit: %v", err)
+ }
+ return txHash
+}
+
+// mintReceiver is the address the mint fixtures pay the minted asset to (account.enterpriseAddress).
+const mintReceiver = "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz"
+
+// assertMintedAssetAt confirms a mint actually landed on-chain: the receiver holds a non-lovelace
+// asset. ("Submit accepted" alone doesn't prove the intended effect; this does.)
+func assertMintedAssetAt(t *testing.T, address string) {
+ t.Helper()
+ waitForBlock()
+ utxos, err := devkitGetUtxos(address)
+ if err != nil {
+ t.Fatalf("get receiver utxos: %v", err)
+ }
+ for _, u := range utxos {
+ amounts, _ := u["amount"].([]interface{})
+ for _, a := range amounts {
+ if am, ok := a.(map[string]interface{}); ok {
+ if unit, _ := am["unit"].(string); unit != "" && unit != "lovelace" {
+ return // a minted asset is present
+ }
+ }
+ }
+ }
+ t.Fatalf("expected a minted asset at %s, found none", address)
+}
+
+// assertUtxoConsumed confirms the given UTXO is no longer present at an address (it was spent).
+func assertUtxoConsumed(t *testing.T, address, txHash string) {
+ t.Helper()
+ waitForBlock()
+ utxos, err := devkitGetUtxos(address)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ for _, u := range utxos {
+ if h, _ := u["tx_hash"].(string); h == txHash {
+ t.Fatalf("UTXO %s at %s was not consumed", txHash, address)
+ }
+ }
+}
+
+func TestIntegrationStakeRegistration(t *testing.T) {
+ skipIfNoDevKit(t)
+ buildSignSubmit(t, "stake_registration.yaml", nil, "payment", "stake")
+}
+
+func TestIntegrationDRepRegistration(t *testing.T) {
+ skipIfNoDevKit(t)
+ buildSignSubmit(t, "drep_registration.yaml", nil, "payment", "drep")
+}
+
+// Negative test: a DRep registration certificate must be witnessed by the DRep key, so signing with
+// the payment key alone must be rejected by the node (MissingVKeyWitnessesUTXOW). This proves the
+// extra witness sign_tx_with_keys adds is genuinely required — not cosmetic — and complements the
+// positive TestIntegrationDRepRegistration (payment+drep) above.
+func TestIntegrationDRepKeyRequired(t *testing.T) {
+ skipIfNoDevKit(t)
+ devkitReset()
+ waitForBlock()
+ if err := devkitTopup(intentSender, 6000); err != nil {
+ t.Fatalf("topup: %v", err)
+ }
+ waitForBlock()
+ pp := devnetPP(t)
+
+ u, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ built, err := bridge.QuickTx.Build(readIntentFixture(t, "drep_registration.yaml"), u, pp)
+ if err != nil {
+ t.Fatalf("build: %v", err)
+ }
+
+ // Sign with the payment key ONLY (ccl_account_sign_tx), omitting the DRep-key witness.
+ signedPaymentOnly, err := bridge.Account.SignTx(intentMnemonic, Testnet, 0, 0, built.TxCbor)
+ if err != nil {
+ t.Fatalf("sign: %v", err)
+ }
+ if _, err := devkitSubmitTx(signedPaymentOnly); err == nil {
+ t.Fatal("the node accepted a DRep registration signed with the payment key only; " +
+ "expected rejection (MissingVKeyWitnessesUTXOW)")
+ }
+}
+
+func TestIntegrationDonation(t *testing.T) {
+ skipIfNoDevKit(t)
+ buildSignSubmit(t, "donation.yaml", nil, "payment")
+}
+
+func TestIntegrationInfoProposal(t *testing.T) {
+ skipIfNoDevKit(t)
+ // A Conway proposal's deposit-return account must be a registered stake address, so register it
+ // first, then submit the proposal in the next block.
+ devkitReset()
+ waitForBlock()
+ if err := devkitTopup(intentSender, 6000); err != nil {
+ t.Fatalf("topup: %v", err)
+ }
+ waitForBlock()
+ pp := devnetPP(t)
+
+ utxos, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, "stake_registration.yaml"), utxos, pp, nil, "payment", "stake")
+ waitForBlock()
+
+ utxos2, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos (post-registration): %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, "governance_proposal.yaml"), utxos2, pp, nil, "payment")
+}
+
+func TestIntegrationMetadata(t *testing.T) {
+ skipIfNoDevKit(t)
+ buildSignSubmit(t, "metadata.yaml", nil, "payment")
+}
+
+func TestIntegrationNativeMint(t *testing.T) {
+ skipIfNoDevKit(t)
+ // The fixture mints under an empty-ScriptAll policy that needs no signature, so the fee payer
+ // alone can submit it.
+ buildSignSubmit(t, "minting.yaml", nil, "payment")
+ assertMintedAssetAt(t, mintReceiver)
+}
+
+func TestIntegrationPlutusMint(t *testing.T) {
+ skipIfNoDevKit(t)
+ buildSignSubmit(t, "plutus/script_minting.yaml",
+ []map[string]interface{}{{"mem": 2000000, "steps": 500000000}}, "payment")
+ assertMintedAssetAt(t, mintReceiver)
+}
+
+// setupThenSubmit resets+funds the devnet, submits a prerequisite fixture (e.g. registering a stake
+// address or DRep), then submits the target fixture in the next block. Used for intents whose
+// certificate depends on prior on-chain state.
+func setupThenSubmit(t *testing.T, setupFixture string, setupKeys []string, fixture string, keys []string) {
+ t.Helper()
+ devkitReset()
+ waitForBlock()
+ if err := devkitTopup(intentSender, 6000); err != nil {
+ t.Fatalf("topup: %v", err)
+ }
+ waitForBlock()
+ pp := devnetPP(t)
+
+ u, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, setupFixture), u, pp, nil, setupKeys...)
+ waitForBlock()
+
+ u2, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos (post-setup): %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, fixture), u2, pp, nil, keys...)
+}
+
+func TestIntegrationVotingDelegation(t *testing.T) {
+ skipIfNoDevKit(t)
+ // Delegating voting power requires the stake address to be registered; vote target is abstain.
+ setupThenSubmit(t,
+ "stake_registration.yaml", []string{"payment", "stake"},
+ "voting_delegation.yaml", []string{"payment", "stake"})
+}
+
+func TestIntegrationDRepUpdate(t *testing.T) {
+ skipIfNoDevKit(t)
+ setupThenSubmit(t,
+ "drep_registration.yaml", []string{"payment", "drep"},
+ "drep_update.yaml", []string{"payment", "drep"})
+}
+
+func TestIntegrationDRepDeregistration(t *testing.T) {
+ skipIfNoDevKit(t)
+ setupThenSubmit(t,
+ "drep_registration.yaml", []string{"payment", "drep"},
+ "drep_deregistration.yaml", []string{"payment", "drep"})
+}
+
+func TestIntegrationStakeWithdrawal(t *testing.T) {
+ skipIfNoDevKit(t)
+ // Conway requires a stake address to be vote-delegated to a DRep before it can withdraw, so the
+ // sequence is: register stake -> delegate voting power -> withdraw the (zero) reward balance.
+ devkitReset()
+ waitForBlock()
+ if err := devkitTopup(intentSender, 6000); err != nil {
+ t.Fatalf("topup: %v", err)
+ }
+ waitForBlock()
+ pp := devnetPP(t)
+
+ u, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, "stake_registration.yaml"), u, pp, nil, "payment", "stake")
+ waitForBlock()
+
+ u2, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos (post-registration): %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, "voting_delegation.yaml"), u2, pp, nil, "payment", "stake")
+ waitForBlock()
+
+ u3, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos (post-vote-delegation): %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, "stake_withdrawal.yaml"), u3, pp, nil, "payment", "stake")
+}
+
+// govActionPlaceholder is the gov_action_tx_hash baked into voting.yaml; the voting test repoints it
+// at the real proposal it submits.
+const govActionPlaceholder = "12745f09b138d4d0a11a560b4591ebb830cf12336347606d2edbbf1893d395c6"
+
+func TestIntegrationVoting(t *testing.T) {
+ skipIfNoDevKit(t)
+ // A vote needs a registered DRep (the voter), a registered stake address (the proposal's return
+ // account), a live gov action to vote on, and the vote referencing it.
+ devkitReset()
+ waitForBlock()
+ if err := devkitTopup(intentSender, 6000); err != nil {
+ t.Fatalf("topup: %v", err)
+ }
+ waitForBlock()
+ pp := devnetPP(t)
+
+ u, _ := devkitGetUtxos(intentSender)
+ signSubmit(t, readIntentFixture(t, "drep_registration.yaml"), u, pp, nil, "payment", "drep")
+ waitForBlock()
+ u2, _ := devkitGetUtxos(intentSender)
+ signSubmit(t, readIntentFixture(t, "stake_registration.yaml"), u2, pp, nil, "payment", "stake")
+ waitForBlock()
+
+ // Submit an info proposal. Its tx hash (from the build result, not the garbled submit response)
+ // is the gov action id we vote on.
+ u3, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ proposal, err := bridge.QuickTx.Build(readIntentFixture(t, "governance_proposal.yaml"), u3, pp)
+ if err != nil {
+ t.Fatalf("build proposal: %v", err)
+ }
+ actionTxHash := proposal.TxHash
+ signedProposal, err := bridge.Account.SignTxWithKeys(intentMnemonic, Testnet, 0, 0, proposal.TxCbor, "payment")
+ if err != nil {
+ t.Fatalf("sign proposal: %v", err)
+ }
+ if _, err := devkitSubmitTx(signedProposal); err != nil {
+ t.Fatalf("submit proposal: %v", err)
+ }
+ waitForBlock()
+
+ // Vote on the proposal we just submitted.
+ u4, _ := devkitGetUtxos(intentSender)
+ voteYaml := strings.ReplaceAll(readIntentFixture(t, "voting.yaml"), govActionPlaceholder, actionTxHash)
+ signSubmit(t, voteYaml, u4, pp, nil, "payment", "drep")
+}
+
+// poolPlaceholder is the pool id baked into stake_delegation.yaml; the delegation test repoints it
+// at the pool it registers. accountPoolID is that pool's id — the one keyed to the account's stake
+// key in pool_registration.yaml (captured from QuickTxIntentsTest).
+const (
+ poolPlaceholder = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+ accountPoolID = "pool1xtrj35uxrctye2egew8sqezgzwwg796ql7uw02572gedcpgmwck"
+)
+
+func TestIntegrationStakeDelegation(t *testing.T) {
+ skipIfNoDevKit(t)
+ // Register the stake address, register a pool keyed to the account, then delegate to that pool.
+ // (DevKit exposes no pool-list endpoint, so we delegate to a pool we create rather than discover.)
+ devkitReset()
+ waitForBlock()
+ if err := devkitTopup(intentSender, 6000); err != nil {
+ t.Fatalf("topup: %v", err)
+ }
+ waitForBlock()
+ pp := devnetPP(t)
+
+ u, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, "stake_registration.yaml"), u, pp, nil, "payment", "stake")
+ waitForBlock()
+
+ u2, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos (post-registration): %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, "pool_registration.yaml"), u2, pp, nil, "payment", "stake")
+ waitForBlock()
+
+ u3, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos (post-pool-registration): %v", err)
+ }
+ delegYaml := strings.ReplaceAll(readIntentFixture(t, "stake_delegation.yaml"), poolPlaceholder, accountPoolID)
+ signSubmit(t, delegYaml, u3, pp, nil, "payment", "stake")
+}
+
+func TestIntegrationPoolRegistration(t *testing.T) {
+ skipIfNoDevKit(t)
+ // The fixture keys the pool to the account's stake key (operator, owner, reward account), so
+ // signing with the stake key witnesses it. The reward account must be a registered stake
+ // address, so register it first.
+ setupThenSubmit(t,
+ "stake_registration.yaml", []string{"payment", "stake"},
+ "pool_registration.yaml", []string{"payment", "stake"})
+}
+
+// Plutus spend: lock a UTXO at the script address (with the datum hash), then spend it. The spend
+// fixture references a placeholder UTXO; we repoint it at the real on-chain locked UTXO.
+func TestIntegrationPlutusSpend(t *testing.T) {
+ skipIfNoDevKit(t)
+ devkitReset()
+ waitForBlock()
+ if err := devkitTopup(intentSender, 6000); err != nil {
+ t.Fatalf("topup: %v", err)
+ }
+ waitForBlock()
+ pp := devnetPP(t)
+
+ // Step 1: lock 10 ADA at the script address with the datum hash.
+ utxos, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get utxos: %v", err)
+ }
+ signSubmit(t, readIntentFixture(t, "plutus/plutus_lock.yaml"), utxos, pp, nil, "payment")
+ waitForBlock()
+
+ // Step 2: find the locked UTXO at the script address.
+ scriptUtxos, err := devkitGetUtxos(scriptAddr)
+ if err != nil || len(scriptUtxos) == 0 {
+ t.Fatalf("no locked UTXO at script address: %v", err)
+ }
+ locked := scriptUtxos[0]
+ lockHash, _ := locked["tx_hash"].(string)
+ lockIdx := 0
+ if idx, ok := locked["output_index"].(float64); ok {
+ lockIdx = int(idx)
+ }
+
+ // Step 3: repoint the spend fixture's utxo_ref at the real locked UTXO.
+ spendYaml := readIntentFixture(t, "plutus/script_collect_from.yaml")
+ spendYaml = strings.ReplaceAll(spendYaml, scriptTxHash, lockHash)
+ if lockIdx != 0 {
+ spendYaml = strings.Replace(spendYaml, "output_index: 0", fmt.Sprintf("output_index: %d", lockIdx), 1)
+ }
+
+ // Step 4: spend it — supply the locked UTXO (with its datum hash) + a fee/collateral UTXO.
+ feeUtxos, err := devkitGetUtxos(intentSender)
+ if err != nil {
+ t.Fatalf("get fee utxos: %v", err)
+ }
+ spendUtxos := []map[string]interface{}{{
+ "tx_hash": lockHash,
+ "output_index": lockIdx,
+ "address": scriptAddr,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "10000000"}},
+ "data_hash": scriptDatumHsh,
+ }}
+ spendUtxos = append(spendUtxos, feeUtxos...)
+
+ signSubmit(t, spendYaml, spendUtxos, pp,
+ []map[string]interface{}{{"mem": 2000000, "steps": 500000000}}, "payment")
+
+ // Confirm the spend actually consumed the locked script UTXO.
+ assertUtxoConsumed(t, scriptAddr, lockHash)
+}
diff --git a/wrappers/go/ccl/intents_test.go b/wrappers/go/ccl/intents_test.go
new file mode 100644
index 0000000..315424b
--- /dev/null
+++ b/wrappers/go/ccl/intents_test.go
@@ -0,0 +1,75 @@
+package ccl
+
+import (
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+// The fee_payer/from address baked into the testdata/intents/*.yaml fixtures (derived from the
+// core test mnemonic). The fixtures are generated by the JVM QuickTxIntentsTest via
+// TxPlan.from(tx).toYaml(), so they carry CCL's exact intent YAML shapes; here we drive each one
+// through the native library end-to-end to prove the bridge builds it offline (and that the
+// native-image reflection config covers the governance/staking/DRep classes).
+const intentSender = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp"
+
+// Second sender used by the compose fixture (multiple senders into one transaction).
+const intentSender2 = "addr_test1qz7svwszky8gcmhrfza7a89z9u0dfzd3l7h23sqlc5yml7ejcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqcqrvr0"
+
+func TestQuickTxIntentsE2E(t *testing.T) {
+ files, err := filepath.Glob("../../../test-fixtures/quicktx-intents/*.yaml")
+ if err != nil {
+ t.Fatalf("glob fixtures: %v", err)
+ }
+ if len(files) == 0 {
+ t.Fatal("no intent fixtures found in testdata/intents/")
+ }
+
+ // A 2000-ADA UTXO at the sender (covers the largest deposit — gov action = 1000 ADA) plus a
+ // small one that the reference-input fixture reads.
+ utxos := []map[string]interface{}{
+ {
+ "tx_hash": strings.Repeat("a", 64),
+ "output_index": 0,
+ "address": intentSender,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "2000000000"}},
+ },
+ {
+ "tx_hash": strings.Repeat("c", 64),
+ "output_index": 0,
+ "address": intentSender,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "5000000"}},
+ },
+ {
+ "tx_hash": strings.Repeat("a", 64),
+ "output_index": 1,
+ "address": intentSender2,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "2000000000"}},
+ },
+ }
+
+ for _, f := range files {
+ name := strings.TrimSuffix(filepath.Base(f), ".yaml")
+ t.Run(name, func(t *testing.T) {
+ yamlBytes, err := os.ReadFile(f)
+ if err != nil {
+ t.Fatalf("read %s: %v", f, err)
+ }
+ result, err := bridge.QuickTx.Build(string(yamlBytes), utxos, testProtocolParams())
+ if err != nil {
+ t.Fatalf("build %s: %v", name, err)
+ }
+ if len(result.TxCbor) == 0 {
+ t.Error("tx_cbor should not be empty")
+ }
+ if len(result.TxHash) != 64 {
+ t.Errorf("expected 64-char tx_hash, got %d", len(result.TxHash))
+ }
+ if fee, err := strconv.Atoi(result.Fee); err != nil || fee <= 0 {
+ t.Errorf("expected positive fee, got %q", result.Fee)
+ }
+ })
+ }
+}
diff --git a/wrappers/go/ccl/quicktx_integration_test.go b/wrappers/go/ccl/quicktx_integration_test.go
index e0dbf0c..b618976 100644
--- a/wrappers/go/ccl/quicktx_integration_test.go
+++ b/wrappers/go/ccl/quicktx_integration_test.go
@@ -242,10 +242,7 @@ func TestIntegrationSimpleADATransfer(t *testing.T) {
waitForBlock()
sender := fundSender(t, 150)
- receiver, err := bridge.Account.Create(Testnet)
- if err != nil {
- t.Fatalf("create receiver: %v", err)
- }
+ receiver, _ := bridge.Account.Create(Testnet)
utxos, err := devkitGetUtxos(sender.BaseAddress)
if err != nil {
@@ -256,25 +253,17 @@ func TestIntegrationSimpleADATransfer(t *testing.T) {
t.Fatalf("get pp: %v", err)
}
- // Build
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(5)).
- From(sender.BaseAddress).
- WithUtxos(utxos).
- WithProtocolParams(pp).
- Build()
+ yaml := quickTxYaml(sender.BaseAddress, receiver.BaseAddress, "5000000")
+ result, err := bridge.QuickTx.Build(yaml, utxos, pp)
if err != nil {
t.Fatalf("build: %v", err)
}
assertTxResult(t, result)
- // Sign
signedTx, err := bridge.Account.SignTx(sender.Mnemonic, Testnet, 0, 0, result.TxCbor)
if err != nil {
t.Fatalf("sign: %v", err)
}
-
- // Submit
txHash, err := devkitSubmitTx(signedTx)
if err != nil {
t.Fatalf("submit: %v", err)
@@ -283,97 +272,71 @@ func TestIntegrationSimpleADATransfer(t *testing.T) {
t.Fatal("empty tx hash from submit")
}
- // Verify
waitForBlock()
receiverUtxos, err := devkitGetUtxos(receiver.BaseAddress)
if err != nil {
t.Fatalf("get receiver utxos: %v", err)
}
- total := totalLovelace(receiverUtxos)
- if total != 5_000_000 {
+ if total := totalLovelace(receiverUtxos); total != 5_000_000 {
t.Errorf("expected 5 ADA (5000000), got %d lovelace", total)
}
}
func TestIntegrationMultipleReceivers(t *testing.T) {
skipIfNoDevKit(t)
+ devkitReset()
+ waitForBlock()
sender := fundSender(t, 150)
r1, _ := bridge.Account.Create(Testnet)
r2, _ := bridge.Account.Create(Testnet)
- utxos, _ := devkitGetUtxos(sender.BaseAddress)
- pp, _ := devkitGetProtocolParams()
-
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(r1.BaseAddress, Ada(3)).
- PayToAddress(r2.BaseAddress, Ada(2)).
- From(sender.BaseAddress).
- WithUtxos(utxos).
- WithProtocolParams(pp).
- Build()
+ utxos, err := devkitGetUtxos(sender.BaseAddress)
if err != nil {
- t.Fatalf("build: %v", err)
+ t.Fatalf("get utxos: %v", err)
}
-
- signedTx, _ := bridge.Account.SignTx(sender.Mnemonic, Testnet, 0, 0, result.TxCbor)
- _, err = devkitSubmitTx(signedTx)
+ pp, err := devkitGetProtocolParams()
if err != nil {
- t.Fatalf("submit: %v", err)
- }
-
- waitForBlock()
-
- r1Utxos, _ := devkitGetUtxos(r1.BaseAddress)
- r2Utxos, _ := devkitGetUtxos(r2.BaseAddress)
-
- if total := totalLovelace(r1Utxos); total != 3_000_000 {
- t.Errorf("r1: expected 3 ADA, got %d lovelace", total)
- }
- if total := totalLovelace(r2Utxos); total != 2_000_000 {
- t.Errorf("r2: expected 2 ADA, got %d lovelace", total)
+ t.Fatalf("get pp: %v", err)
}
-}
-
-func TestIntegrationWithMetadata(t *testing.T) {
- skipIfNoDevKit(t)
- sender := fundSender(t, 150)
- receiver, _ := bridge.Account.Create(Testnet)
-
- utxos, _ := devkitGetUtxos(sender.BaseAddress)
- pp, _ := devkitGetProtocolParams()
-
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(2)).
- AttachMetadata(674, map[string]interface{}{"msg": []string{"Hello from Go integration"}}).
- From(sender.BaseAddress).
- WithUtxos(utxos).
- WithProtocolParams(pp).
- Build()
+ yaml := fmt.Sprintf(`
+version: 1.0
+transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "2000000"
+`, sender.BaseAddress, r1.BaseAddress, r2.BaseAddress)
+
+ result, err := bridge.QuickTx.Build(yaml, utxos, pp)
if err != nil {
t.Fatalf("build: %v", err)
}
-
signedTx, _ := bridge.Account.SignTx(sender.Mnemonic, Testnet, 0, 0, result.TxCbor)
- _, err = devkitSubmitTx(signedTx)
- if err != nil {
+ if _, err := devkitSubmitTx(signedTx); err != nil {
t.Fatalf("submit: %v", err)
}
waitForBlock()
-
- txInfo, err := devkitGetTx(result.TxHash)
- if err != nil {
- t.Fatalf("get tx: %v", err)
- }
- if txInfo == nil {
- t.Fatal("tx not found on-chain")
+ if total := totalLovelace(mustUtxos(t, r1.BaseAddress)); total != 3_000_000 {
+ t.Errorf("expected 3 ADA for r1, got %d", total)
}
}
func TestIntegrationInsufficientFunds(t *testing.T) {
skipIfNoDevKit(t)
+ devkitReset()
+ waitForBlock()
sender := fundSender(t, 2)
receiver, _ := bridge.Account.Create(Testnet)
@@ -381,177 +344,17 @@ func TestIntegrationInsufficientFunds(t *testing.T) {
utxos, _ := devkitGetUtxos(sender.BaseAddress)
pp, _ := devkitGetProtocolParams()
- _, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(100)).
- From(sender.BaseAddress).
- WithUtxos(utxos).
- WithProtocolParams(pp).
- Build()
- if err == nil {
- t.Fatal("expected error for insufficient funds")
+ yaml := quickTxYaml(sender.BaseAddress, receiver.BaseAddress, "100000000")
+ if _, err := bridge.QuickTx.Build(yaml, utxos, pp); err == nil {
+ t.Fatal("expected insufficient funds error")
}
}
-func TestIntegrationFullRoundTrip(t *testing.T) {
- skipIfNoDevKit(t)
-
- sender := fundSender(t, 150)
- receiver, _ := bridge.Account.Create(Testnet)
-
- utxos, _ := devkitGetUtxos(sender.BaseAddress)
- pp, _ := devkitGetProtocolParams()
-
- // Build
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(10)).
- From(sender.BaseAddress).
- WithUtxos(utxos).
- WithProtocolParams(pp).
- Build()
- if err != nil {
- t.Fatalf("build: %v", err)
- }
- assertTxResult(t, result)
-
- // Sign
- signedTx, err := bridge.Account.SignTx(sender.Mnemonic, Testnet, 0, 0, result.TxCbor)
- if err != nil {
- t.Fatalf("sign: %v", err)
- }
-
- // Submit
- _, err = devkitSubmitTx(signedTx)
- if err != nil {
- t.Fatalf("submit: %v", err)
- }
-
- waitForBlock()
-
- // Confirm on-chain
- txInfo, err := devkitGetTx(result.TxHash)
- if err != nil {
- t.Fatalf("get tx: %v", err)
- }
- if txInfo == nil {
- t.Fatal("tx not found on-chain")
- }
-
- // Check receiver balance
- receiverUtxos, _ := devkitGetUtxos(receiver.BaseAddress)
- total := totalLovelace(receiverUtxos)
- if total != 10_000_000 {
- t.Errorf("expected 10 ADA (10000000), got %d lovelace", total)
- }
-}
-
-// --- Provider Config (Java-side lazy UTXO fetching) tests ---
-
-func TestIntegrationProviderConfigSimpleTransfer(t *testing.T) {
- skipIfNoDevKit(t)
-
- sender := fundSender(t, 150)
- receiver, _ := bridge.Account.Create(Testnet)
-
- // Build using ProviderConfig — Java fetches UTXOs and PP lazily via HTTP
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(5)).
- From(sender.BaseAddress).
- BuildWithProvider(ProviderConfig{
- Name: "yaci",
- URL: devkitProviderURL,
- })
- if err != nil {
- t.Fatalf("build with provider: %v", err)
- }
- assertTxResult(t, result)
-
- // Sign and submit
- signedTx, _ := bridge.Account.SignTx(sender.Mnemonic, Testnet, 0, 0, result.TxCbor)
- txHash, err := devkitSubmitTx(signedTx)
- if err != nil {
- t.Fatalf("submit: %v", err)
- }
- if txHash == "" {
- t.Fatal("empty tx hash")
- }
-
- waitForBlock()
- receiverUtxos, _ := devkitGetUtxos(receiver.BaseAddress)
- total := totalLovelace(receiverUtxos)
- if total != 5_000_000 {
- t.Errorf("expected 5 ADA (5000000), got %d lovelace", total)
- }
-}
-
-func TestIntegrationProviderConfigMultipleReceivers(t *testing.T) {
- skipIfNoDevKit(t)
-
- sender := fundSender(t, 150)
- r1, _ := bridge.Account.Create(Testnet)
- r2, _ := bridge.Account.Create(Testnet)
-
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(r1.BaseAddress, Ada(3)).
- PayToAddress(r2.BaseAddress, Ada(2)).
- From(sender.BaseAddress).
- BuildWithProvider(ProviderConfig{
- Name: "yaci",
- URL: devkitProviderURL,
- })
- if err != nil {
- t.Fatalf("build with provider: %v", err)
- }
-
- signedTx, _ := bridge.Account.SignTx(sender.Mnemonic, Testnet, 0, 0, result.TxCbor)
- _, err = devkitSubmitTx(signedTx)
- if err != nil {
- t.Fatalf("submit: %v", err)
- }
-
- waitForBlock()
-
- r1Utxos, _ := devkitGetUtxos(r1.BaseAddress)
- r2Utxos, _ := devkitGetUtxos(r2.BaseAddress)
-
- if total := totalLovelace(r1Utxos); total != 3_000_000 {
- t.Errorf("r1: expected 3 ADA, got %d lovelace", total)
- }
- if total := totalLovelace(r2Utxos); total != 2_000_000 {
- t.Errorf("r2: expected 2 ADA, got %d lovelace", total)
- }
-}
-
-func TestIntegrationProviderConfigWithMetadata(t *testing.T) {
- skipIfNoDevKit(t)
-
- sender := fundSender(t, 150)
- receiver, _ := bridge.Account.Create(Testnet)
-
- result, err := bridge.QuickTx.NewTx().
- PayToAddress(receiver.BaseAddress, Ada(2)).
- AttachMetadata(674, map[string]interface{}{"msg": []string{"Hello from Go providerConfig"}}).
- From(sender.BaseAddress).
- BuildWithProvider(ProviderConfig{
- Name: "yaci",
- URL: devkitProviderURL,
- })
- if err != nil {
- t.Fatalf("build with provider: %v", err)
- }
-
- signedTx, _ := bridge.Account.SignTx(sender.Mnemonic, Testnet, 0, 0, result.TxCbor)
- _, err = devkitSubmitTx(signedTx)
- if err != nil {
- t.Fatalf("submit: %v", err)
- }
-
- waitForBlock()
-
- txInfo, err := devkitGetTx(result.TxHash)
+func mustUtxos(t *testing.T, address string) []map[string]interface{} {
+ t.Helper()
+ utxos, err := devkitGetUtxos(address)
if err != nil {
- t.Fatalf("get tx: %v", err)
- }
- if txInfo == nil {
- t.Fatal("tx not found on-chain")
+ t.Fatalf("get utxos: %v", err)
}
+ return utxos
}
diff --git a/wrappers/go/ccl/script_spend_test.go b/wrappers/go/ccl/script_spend_test.go
new file mode 100644
index 0000000..d615773
--- /dev/null
+++ b/wrappers/go/ccl/script_spend_test.go
@@ -0,0 +1,88 @@
+package ccl
+
+import (
+ "os"
+ "strings"
+ "testing"
+)
+
+// End-to-end Plutus script spend (script_collect_from) through the native lib via Go. Unlike the
+// generic intent table, this one needs a UTXO at the script address (with the datum hash) plus a
+// fee/collateral UTXO, and caller-supplied execution units for the spend redeemer. The fixture is
+// generated by QuickTxIntentsTest.scriptCollectFrom; the script address + datum hash below are the
+// deterministic values for the always-succeeds V2 validator and datum int(42).
+const (
+ scriptAddr = "addr_test1wpunlryvl7aqsxe22erzlsseej87v5kk5vutvtrmzdy8dect48z0w"
+ scriptDatumHsh = "9e1199a988ba72ffd6e9c269cadb3b53b5f360ff99f112d9b2ee30c4d74ad88b"
+ scriptTxHash = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+)
+
+func TestQuickTxScriptSpendE2E(t *testing.T) {
+ yamlBytes, err := os.ReadFile("../../../test-fixtures/quicktx-intents/plutus/script_collect_from.yaml")
+ if err != nil {
+ t.Fatalf("read fixture: %v", err)
+ }
+ yaml := string(yamlBytes)
+
+ // The script UTXO to spend (with its datum hash) + a fee/collateral UTXO at the sender.
+ utxos := []map[string]interface{}{
+ {
+ "tx_hash": scriptTxHash,
+ "output_index": 0,
+ "address": scriptAddr,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "10000000"}},
+ "data_hash": scriptDatumHsh,
+ },
+ {
+ "tx_hash": strings.Repeat("a", 64),
+ "output_index": 0,
+ "address": intentSender,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "2000000000"}},
+ },
+ }
+ execUnits := []map[string]interface{}{{"mem": 2000000, "steps": 500000000}}
+
+ // With execution units: builds.
+ result, err := bridge.QuickTx.Build(yaml, utxos, testProtocolParams(), execUnits)
+ if err != nil {
+ t.Fatalf("script spend build failed: %v", err)
+ }
+ if len(result.TxCbor) == 0 || len(result.TxHash) != 64 {
+ t.Errorf("bad result: cbor=%d hash=%d", len(result.TxCbor), len(result.TxHash))
+ }
+
+ // Without execution units: the script can't be costed, so the build must fail.
+ if _, err := bridge.QuickTx.Build(yaml, utxos, testProtocolParams()); err == nil {
+ t.Error("expected script spend to fail without execution units")
+ }
+}
+
+// End-to-end Plutus script mint (script_minting). Only needs a fee/collateral UTXO at the sender
+// plus the redeemer's execution units.
+func TestQuickTxScriptMintE2E(t *testing.T) {
+ yamlBytes, err := os.ReadFile("../../../test-fixtures/quicktx-intents/plutus/script_minting.yaml")
+ if err != nil {
+ t.Fatalf("read fixture: %v", err)
+ }
+ yaml := string(yamlBytes)
+
+ utxos := []map[string]interface{}{{
+ "tx_hash": strings.Repeat("a", 64),
+ "output_index": 0,
+ "address": intentSender,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "2000000000"}},
+ }}
+ execUnits := []map[string]interface{}{{"mem": 2000000, "steps": 500000000}}
+
+ result, err := bridge.QuickTx.Build(yaml, utxos, testProtocolParams(), execUnits)
+ if err != nil {
+ t.Fatalf("script mint build failed: %v", err)
+ }
+ if len(result.TxCbor) == 0 || len(result.TxHash) != 64 {
+ t.Errorf("bad result: cbor=%d hash=%d", len(result.TxCbor), len(result.TxHash))
+ }
+
+ if _, err := bridge.QuickTx.Build(yaml, utxos, testProtocolParams()); err == nil {
+ t.Error("expected script mint to fail without execution units")
+ }
+}
diff --git a/wrappers/go/ccl/sign_test.go b/wrappers/go/ccl/sign_test.go
new file mode 100644
index 0000000..35ff0ae
--- /dev/null
+++ b/wrappers/go/ccl/sign_test.go
@@ -0,0 +1,53 @@
+package ccl
+
+import (
+ "os"
+ "strings"
+ "testing"
+)
+
+// The mnemonic the intent fixtures are derived from (account index 0/0 == intentSender).
+const intentMnemonic = "test walk nut penalty hip pave soap entry language right filter choice"
+
+// A stake registration must be witnessed by the stake key in addition to the payment key, or the
+// node rejects it with MissingVKeyWitnessesUTXOW. This verifies SignTxWithKeys adds that second
+// witness (the signed CBOR is longer by one vkey witness) where SignTx (payment only) does not.
+func TestSignTxWithStakeKey(t *testing.T) {
+ yamlBytes, err := os.ReadFile("../../../test-fixtures/quicktx-intents/stake_registration.yaml")
+ if err != nil {
+ t.Fatalf("read fixture: %v", err)
+ }
+ utxos := []map[string]interface{}{{
+ "tx_hash": strings.Repeat("a", 64),
+ "output_index": 0,
+ "address": intentSender,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "2000000000"}},
+ }}
+
+ built, err := bridge.QuickTx.Build(string(yamlBytes), utxos, testProtocolParams())
+ if err != nil {
+ t.Fatalf("build stake registration: %v", err)
+ }
+
+ signedPayment, err := bridge.Account.SignTx(intentMnemonic, Testnet, 0, 0, built.TxCbor)
+ if err != nil {
+ t.Fatalf("sign (payment): %v", err)
+ }
+ signedStake, err := bridge.Account.SignTxWithKeys(intentMnemonic, Testnet, 0, 0, built.TxCbor, "payment", "stake")
+ if err != nil {
+ t.Fatalf("sign (payment,stake): %v", err)
+ }
+
+ if len(signedStake) <= len(signedPayment) {
+ t.Errorf("payment+stake signing should add a witness: payment=%d, payment+stake=%d",
+ len(signedPayment), len(signedStake))
+ }
+}
+
+// An unknown key role is rejected.
+func TestSignTxWithKeysRejectsUnknownRole(t *testing.T) {
+ if _, err := bridge.Account.SignTxWithKeys(intentMnemonic, Testnet, 0, 0,
+ "84a300d9010281825820"+strings.Repeat("0", 100), "bogus"); err == nil {
+ t.Error("expected an error for an unknown signing role")
+ }
+}
diff --git a/wrappers/go/examples/account/main.go b/wrappers/go/examples/account/main.go
new file mode 100644
index 0000000..19a6438
--- /dev/null
+++ b/wrappers/go/examples/account/main.go
@@ -0,0 +1,51 @@
+// Account creation and key derivation (offline).
+//
+// Run from wrappers/go:
+//
+// LIB_DIR=../../core/build/native/nativeCompile
+// DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR go run ./examples/account
+package main
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/bloxbean/ccl-bridge/wrappers/go/ccl"
+)
+
+func main() {
+ bridge, err := ccl.New()
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer bridge.Close()
+
+ // 1. Create a brand-new testnet account (random mnemonic).
+ account, err := bridge.Account.Create(ccl.Testnet)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println("Created account")
+ fmt.Println(" base address:", account.BaseAddress)
+ fmt.Println(" mnemonic :", account.Mnemonic)
+
+ // 2. Restore the same account from its mnemonic — the address must match.
+ restored, err := bridge.Account.FromMnemonic(account.Mnemonic, ccl.Testnet, 0, 0)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if restored.BaseAddress != account.BaseAddress {
+ log.Fatal("restored address does not match")
+ }
+ fmt.Println("Restored from mnemonic — address matches:", restored.BaseAddress)
+
+ // 3. Derive keys.
+ priv, _ := bridge.Account.GetPrivateKey(account.Mnemonic, ccl.Testnet, 0, 0)
+ pub, _ := bridge.Account.GetPublicKey(account.Mnemonic, ccl.Testnet, 0, 0)
+ fmt.Println(" private key (extended, hex):", priv)
+ fmt.Println(" public key (hex) :", pub)
+
+ // 4. Derive the governance DRep ID.
+ drepID, _ := bridge.Account.GetDRepID(account.Mnemonic, ccl.Testnet, 0)
+ fmt.Println(" DRep ID:", drepID)
+}
diff --git a/wrappers/go/examples/primitives/main.go b/wrappers/go/examples/primitives/main.go
new file mode 100644
index 0000000..e899592
--- /dev/null
+++ b/wrappers/go/examples/primitives/main.go
@@ -0,0 +1,55 @@
+// Crypto and address primitives (offline).
+//
+// Run from wrappers/go:
+//
+// LIB_DIR=../../core/build/native/nativeCompile
+// DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR go run ./examples/primitives
+package main
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/bloxbean/ccl-bridge/wrappers/go/ccl"
+)
+
+func main() {
+ bridge, err := ccl.New()
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer bridge.Close()
+
+ // --- Mnemonics ---
+ mnemonic, _ := bridge.Crypto.GenerateMnemonic(24)
+ fmt.Println("Generated 24-word mnemonic:", mnemonic)
+ fmt.Println(" valid?", bridge.Crypto.ValidateMnemonic(mnemonic))
+ fmt.Println(" 'not a real mnemonic' valid?", bridge.Crypto.ValidateMnemonic("not a real mnemonic"))
+
+ // --- Blake2b hashing (hex in -> hex out). "Hello" == 48656c6c6f ---
+ h256, _ := bridge.Crypto.Blake2b256("48656c6c6f")
+ h224, _ := bridge.Crypto.Blake2b224("48656c6c6f")
+ fmt.Println("Blake2b-256('Hello'):", h256)
+ fmt.Println("Blake2b-224('Hello'):", h224)
+
+ // --- Ed25519 signing ---
+ // GetPrivateKey returns the 64-byte extended key; Sign expects a 32-byte
+ // Ed25519 key, so take the first 32 bytes (64 hex chars).
+ acct, _ := bridge.Account.Create(ccl.Testnet)
+ privExt, _ := bridge.Account.GetPrivateKey(acct.Mnemonic, ccl.Testnet, 0, 0)
+ pub, _ := bridge.Account.GetPublicKey(acct.Mnemonic, ccl.Testnet, 0, 0)
+ messageHex := "68656c6c6f" // "hello"
+ sig, _ := bridge.Crypto.Sign(messageHex, privExt[:64])
+ fmt.Println("Ed25519 signature:", sig)
+ // A tampered signature is correctly rejected.
+ fmt.Println(" verify(fake signature) ->", bridge.Crypto.Verify("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", messageHex, pub))
+
+ // --- Address parsing & validation ---
+ addr := acct.BaseAddress
+ fmt.Println("Address valid?", bridge.Address.Validate(addr))
+ info, _ := bridge.Address.Info(addr)
+ fmt.Printf("Address info : %+v\n", info)
+ raw, _ := bridge.Address.ToBytes(addr)
+ back, _ := bridge.Address.FromBytes(raw)
+ fmt.Println("Address -> bytes -> address round-trips:", back == addr)
+}
diff --git a/wrappers/go/examples/transaction/main.go b/wrappers/go/examples/transaction/main.go
new file mode 100644
index 0000000..cdaa11d
--- /dev/null
+++ b/wrappers/go/examples/transaction/main.go
@@ -0,0 +1,80 @@
+// Build and sign a payment transaction fully offline from a TxPlan (YAML).
+//
+// The transaction is defined as a TxPlan YAML document; we supply the UTXOs and protocol
+// parameters ourselves (no node / no provider). The bridge builds the unsigned CBOR, which we
+// then sign locally. Submitting it is a separate, online step.
+//
+// Run from wrappers/go:
+//
+// LIB_DIR=../../core/build/native/nativeCompile
+// DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR go run ./examples/transaction
+package main
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/bloxbean/ccl-bridge/wrappers/go/ccl"
+)
+
+// Minimal protocol parameters (CCL test-resource values), the CCL ProtocolParams model as a map.
+var protocolParams = map[string]interface{}{
+ "min_fee_a": 44, "min_fee_b": 155381, "max_tx_size": 16384,
+ "key_deposit": "2000000", "pool_deposit": "500000000",
+ "coins_per_utxo_size": "4310", "max_val_size": "5000",
+ "max_tx_ex_mem": "10000000", "max_tx_ex_steps": "10000000000",
+ "price_mem": 0.0577, "price_step": 0.0000721, "collateral_percent": 150,
+ "max_collateral_inputs": 3,
+}
+
+func main() {
+ bridge, err := ccl.New()
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer bridge.Close()
+
+ sender, _ := bridge.Account.Create(ccl.Testnet)
+ receiver, _ := bridge.Account.Create(ccl.Testnet)
+
+ // A static UTXO the sender controls (100 ADA), instead of querying a node.
+ utxos := []map[string]interface{}{{
+ "tx_hash": strings.Repeat("a", 64),
+ "output_index": 0,
+ "address": sender.BaseAddress,
+ "amount": []map[string]interface{}{{"unit": "lovelace", "quantity": "100000000"}},
+ }}
+
+ // Define the transaction as a TxPlan YAML document: pay 5 ADA to the receiver.
+ yaml := fmt.Sprintf(`
+version: 1.0
+transaction:
+ - tx:
+ from: %s
+ intents:
+ - type: payment
+ address: %s
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+`, sender.BaseAddress, receiver.BaseAddress)
+
+ // Build the unsigned transaction offline.
+ result, err := bridge.QuickTx.Build(yaml, utxos, protocolParams)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println("Built unsigned transaction from TxPlan YAML")
+ fmt.Println(" tx hash:", result.TxHash)
+ fmt.Println(" fee :", result.Fee)
+ fmt.Println(" cbor :", result.TxCbor[:80], "...")
+
+ // Sign it with the sender's mnemonic.
+ signed, err := bridge.Account.SignTx(sender.Mnemonic, ccl.Testnet, 0, 0, result.TxCbor)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println("Signed transaction cbor:", signed[:80], "...")
+ fmt.Println("\nNext step (not shown): submit `signed` to a Cardano node over HTTP.")
+}
diff --git a/wrappers/go/go.mod b/wrappers/go/go.mod
index a818740..0f1a462 100644
--- a/wrappers/go/go.mod
+++ b/wrappers/go/go.mod
@@ -1,3 +1,5 @@
module github.com/bloxbean/ccl-bridge/wrappers/go
go 1.21
+
+require gopkg.in/yaml.v3 v3.0.1
diff --git a/wrappers/go/go.sum b/wrappers/go/go.sum
new file mode 100644
index 0000000..a62c313
--- /dev/null
+++ b/wrappers/go/go.sum
@@ -0,0 +1,4 @@
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/wrappers/js/README.md b/wrappers/js/README.md
new file mode 100644
index 0000000..e304871
--- /dev/null
+++ b/wrappers/js/README.md
@@ -0,0 +1,83 @@
+# CCL Bridge — JavaScript (Bun)
+
+JavaScript bindings for [Cardano Client Lib](https://github.com/bloxbean/cardano-client-lib)
+via the CCL Bridge native library, using Bun's built-in FFI.
+
+> Part of the [CCL Bridge](../../README.md) project. See the
+> [top-level README](../../README.md) for the full API reference and
+> [`docs/quicktx.md`](../../docs/quicktx.md) for transaction building.
+
+## Requirements
+
+- [Bun](https://bun.sh/) 1.0+.
+- The native library `libccl.{dylib,so,dll}` for your platform.
+
+> **Node.js is not supported.** Node's FFI libraries (ffi-napi, koffi) crash against the
+> GraalVM native library due to stack-boundary detection. Use Bun, whose built-in FFI
+> works correctly. See the project [`TODO.md`](../../TODO.md) Non-Goals.
+
+## Getting the native library
+
+The bindings load the library at runtime via the `CCL_LIB_PATH` environment variable.
+Build or download it first. From the repo root:
+
+```bash
+./gradlew :core:nativeCompile # build from source (needs Oracle GraalVM 25.0.3)
+# or:
+make download-lib # download a pre-built binary
+```
+
+At **runtime** the OS loader also needs it via `DYLD_LIBRARY_PATH` (macOS) /
+`LD_LIBRARY_PATH` (Linux).
+
+## Running the examples
+
+From `wrappers/js`:
+
+```bash
+LIB_DIR=../../core/build/native/nativeCompile
+
+CCL_LIB_PATH=$LIB_DIR DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+ bun examples/account.js
+```
+
+The [`examples/`](examples/) directory contains:
+
+| File | What it shows |
+|------|---------------|
+| [`account.js`](examples/account.js) | Create an account, restore from mnemonic, derive keys and a DRep ID |
+| [`primitives.js`](examples/primitives.js) | Mnemonics, Blake2b hashing, Ed25519 signing, address parsing/validation |
+| [`transaction.js`](examples/transaction.js) | Build an unsigned payment **offline** (QuickTx) and sign it — no node/DevKit needed |
+
+## Quick start
+
+```javascript
+import { CclBridge, TESTNET } from './src/index.js';
+
+const bridge = new CclBridge(); // loads libccl, starts a GraalVM isolate
+try {
+ const account = bridge.account.create(TESTNET);
+ console.log(account.base_address); // addr_test1...
+ console.log(account.mnemonic); // 24-word phrase
+} finally {
+ bridge.close(); // tears down the isolate
+}
+```
+
+## API namespaces
+
+A `CclBridge` instance exposes these namespaces (all offline operations):
+`bridge.account`, `bridge.address`, `bridge.crypto`, `bridge.tx`, `bridge.plutus`,
+`bridge.script`, `bridge.gov`, `bridge.wallet`, `bridge.quicktx`.
+
+Network IDs are exported constants: `MAINNET` (0), `TESTNET` (1), `PREPROD` (2),
+`PREVIEW` (3). Errors throw `CclError`.
+
+Transactions are defined as a [TxPlan](https://github.com/bloxbean/cardano-client-lib)
+**YAML** document and built fully offline — you supply the UTXOs and protocol parameters:
+
+```js
+const result = bridge.quicktx.build(yaml, utxos, protocolParams); // { tx_cbor, tx_hash, fee }
+```
+
+See [`examples/transaction.js`](examples/transaction.js).
diff --git a/wrappers/js/build.gradle b/wrappers/js/build.gradle
index 527332f..5cc21f8 100644
--- a/wrappers/js/build.gradle
+++ b/wrappers/js/build.gradle
@@ -17,5 +17,19 @@ task test(type: Exec) {
environment 'DYLD_LIBRARY_PATH', nativeDir
environment 'LD_LIBRARY_PATH', nativeDir
- commandLine 'bash', '-c', 'bun test test/ccl.test.js'
+ commandLine 'bash', '-c', 'bun install && bun test test/ccl.test.js test/intents.e2e.test.js'
+}
+
+// Full suite including the DevKit integration tests. Requires a running Yaci DevKit
+// on :10000 (the integration tests skip themselves if it is not available).
+task integrationTest(type: Exec) {
+ dependsOn copyNativeLib
+ workingDir projectDir
+
+ def nativeDir = layout.buildDirectory.dir('native').get().asFile.absolutePath
+ environment 'CCL_LIB_PATH', nativeDir
+ environment 'DYLD_LIBRARY_PATH', nativeDir
+ environment 'LD_LIBRARY_PATH', nativeDir
+
+ commandLine 'bash', '-c', 'bun install && bun test test/'
}
diff --git a/wrappers/js/bun.lock b/wrappers/js/bun.lock
new file mode 100644
index 0000000..977061d
--- /dev/null
+++ b/wrappers/js/bun.lock
@@ -0,0 +1,15 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "@bloxbean/ccl",
+ "dependencies": {
+ "yaml": "^2.3.0",
+ },
+ },
+ },
+ "packages": {
+ "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
+ }
+}
diff --git a/wrappers/js/examples/account.js b/wrappers/js/examples/account.js
new file mode 100644
index 0000000..6d9f893
--- /dev/null
+++ b/wrappers/js/examples/account.js
@@ -0,0 +1,32 @@
+// Account creation and key derivation (offline).
+//
+// Run from wrappers/js:
+//
+// LIB_DIR=../../core/build/native/nativeCompile
+// CCL_LIB_PATH=$LIB_DIR DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+// bun examples/account.js
+import { CclBridge, TESTNET } from '../src/index.js';
+
+const bridge = new CclBridge();
+try {
+ // 1. Create a brand-new testnet account (random mnemonic).
+ const account = bridge.account.create(TESTNET);
+ const { mnemonic, base_address } = account;
+ console.log('Created account');
+ console.log(' base address:', base_address);
+ console.log(' mnemonic :', mnemonic);
+
+ // 2. Restore the same account from its mnemonic — the address must match.
+ const restored = bridge.account.fromMnemonic(mnemonic, TESTNET, 0, 0);
+ if (restored.base_address !== base_address) throw new Error('address mismatch');
+ console.log('Restored from mnemonic — address matches:', restored.base_address);
+
+ // 3. Derive keys.
+ console.log(' private key (extended, hex):', bridge.account.getPrivateKey(mnemonic, TESTNET));
+ console.log(' public key (hex) :', bridge.account.getPublicKey(mnemonic, TESTNET));
+
+ // 4. Derive the governance DRep ID.
+ console.log(' DRep ID:', bridge.account.getDrepId(mnemonic, TESTNET));
+} finally {
+ bridge.close();
+}
diff --git a/wrappers/js/examples/primitives.js b/wrappers/js/examples/primitives.js
new file mode 100644
index 0000000..147e294
--- /dev/null
+++ b/wrappers/js/examples/primitives.js
@@ -0,0 +1,41 @@
+// Crypto and address primitives (offline).
+//
+// Run from wrappers/js:
+//
+// LIB_DIR=../../core/build/native/nativeCompile
+// CCL_LIB_PATH=$LIB_DIR DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+// bun examples/primitives.js
+import { CclBridge, TESTNET } from '../src/index.js';
+
+const bridge = new CclBridge();
+try {
+ // --- Mnemonics ---
+ const mnemonic = bridge.crypto.generateMnemonic(24);
+ console.log('Generated 24-word mnemonic:', mnemonic);
+ console.log(' valid?', bridge.crypto.validateMnemonic(mnemonic));
+ console.log(" 'not a real mnemonic' valid?", bridge.crypto.validateMnemonic('not a real mnemonic'));
+
+ // --- Blake2b hashing (hex in -> hex out). "Hello" == 48656c6c6f ---
+ console.log("Blake2b-256('Hello'):", bridge.crypto.blake2b256('48656c6c6f'));
+ console.log("Blake2b-224('Hello'):", bridge.crypto.blake2b224('48656c6c6f'));
+
+ // --- Ed25519 signing ---
+ // getPrivateKey returns the 64-byte extended key; sign expects a 32-byte
+ // Ed25519 key, so take the first 32 bytes (64 hex chars).
+ const acct = bridge.account.create(TESTNET);
+ const sk = bridge.account.getPrivateKey(acct.mnemonic, TESTNET).slice(0, 64);
+ const pk = bridge.account.getPublicKey(acct.mnemonic, TESTNET);
+ const messageHex = '68656c6c6f'; // "hello"
+ console.log('Ed25519 signature:', bridge.crypto.sign(messageHex, sk));
+ // A tampered signature is correctly rejected.
+ console.log(' verify(fake signature) ->', bridge.crypto.verify('00'.repeat(64), messageHex, pk));
+
+ // --- Address parsing & validation ---
+ const addr = acct.base_address;
+ console.log('Address valid?', bridge.address.validate(addr));
+ console.log('Address info :', bridge.address.info(addr));
+ const raw = bridge.address.toBytes(addr);
+ console.log('Address -> bytes -> address round-trips:', bridge.address.fromBytes(raw) === addr);
+} finally {
+ bridge.close();
+}
diff --git a/wrappers/js/examples/transaction.js b/wrappers/js/examples/transaction.js
new file mode 100644
index 0000000..5645eb1
--- /dev/null
+++ b/wrappers/js/examples/transaction.js
@@ -0,0 +1,64 @@
+// Build and sign a payment transaction fully offline from a TxPlan (YAML).
+//
+// The transaction is defined as a TxPlan YAML document; we supply the UTXOs and protocol
+// parameters ourselves (no node / no provider), build the unsigned CBOR, then sign it locally.
+// Submitting it is a separate, online step.
+//
+// Run from wrappers/js:
+//
+// LIB_DIR=../../core/build/native/nativeCompile
+// CCL_LIB_PATH=$LIB_DIR DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+// bun examples/transaction.js
+import { CclBridge, TESTNET } from '../src/index.js';
+
+// Minimal protocol parameters (CCL test-resource values).
+const protocolParams = {
+ min_fee_a: 44, min_fee_b: 155381, max_tx_size: 16384,
+ key_deposit: '2000000', pool_deposit: '500000000',
+ coins_per_utxo_size: '4310', max_val_size: '5000',
+ max_tx_ex_mem: '10000000', max_tx_ex_steps: '10000000000',
+ price_mem: 0.0577, price_step: 0.0000721, collateral_percent: 150,
+ max_collateral_inputs: 3,
+};
+
+const bridge = new CclBridge();
+try {
+ const sender = bridge.account.create(TESTNET);
+ const receiver = bridge.account.create(TESTNET);
+
+ // A static UTXO the sender controls (100 ADA), instead of querying a node.
+ const utxos = [{
+ tx_hash: 'a'.repeat(64),
+ output_index: 0,
+ address: sender.base_address,
+ amount: [{ unit: 'lovelace', quantity: '100000000' }],
+ }];
+
+ // Define the transaction as a TxPlan YAML document: pay 5 ADA to the receiver.
+ const yaml = `
+version: 1.0
+transaction:
+ - tx:
+ from: ${sender.base_address}
+ intents:
+ - type: payment
+ address: ${receiver.base_address}
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+`;
+
+ // Build the unsigned transaction offline.
+ const result = bridge.quicktx.build(yaml, utxos, protocolParams);
+ console.log('Built unsigned transaction from TxPlan YAML');
+ console.log(' tx hash:', result.tx_hash);
+ console.log(' fee :', result.fee);
+ console.log(' cbor :', result.tx_cbor.slice(0, 80), '...');
+
+ // Sign it with the sender's mnemonic.
+ const signed = bridge.account.signTx(sender.mnemonic, TESTNET, 0, 0, result.tx_cbor);
+ console.log('Signed transaction cbor:', signed.slice(0, 80), '...');
+ console.log('\nNext step (not shown): submit `signed` to a Cardano node over HTTP.');
+} finally {
+ bridge.close();
+}
diff --git a/wrappers/js/package.json b/wrappers/js/package.json
index 037e23f..f581689 100644
--- a/wrappers/js/package.json
+++ b/wrappers/js/package.json
@@ -7,6 +7,9 @@
"scripts": {
"test": "bun test test/ccl.test.js"
},
+ "dependencies": {
+ "yaml": "^2.3.0"
+ },
"engines": {
"bun": ">=1.0.0"
}
diff --git a/wrappers/js/src/index.d.ts b/wrappers/js/src/index.d.ts
index e853c3d..ee13cea 100644
--- a/wrappers/js/src/index.d.ts
+++ b/wrappers/js/src/index.d.ts
@@ -45,6 +45,7 @@ export declare class CclBridge {
accountGetPublicKey(mnemonic: string, networkId?: number, accountIndex?: number, addressIndex?: number): string;
accountGetDrepId(mnemonic: string, networkId?: number, accountIndex?: number): string;
accountSignTx(mnemonic: string, networkId: number, accountIndex: number, addressIndex: number, txCborHex: string): string;
+ accountSignTxWithKeys(mnemonic: string, networkId: number, accountIndex: number, addressIndex: number, txCborHex: string, keys: string[] | string): string;
// Address
addressInfo(bech32: string): AddressInfo;
@@ -86,249 +87,22 @@ export interface QuickTxResult {
fee: string;
}
-export interface AmountSpec {
- unit: string;
- quantity: string;
-}
-
-export declare class Amount {
- static lovelace(quantity: number): AmountSpec;
- static ada(adaAmount: number): AmountSpec;
- static asset(unit: string, quantity: number): AmountSpec;
-}
-
-export interface ReferenceInput {
- txHash: string;
- outputIndex: number;
-}
-
-export interface MintAsset {
- name: string;
- quantity: string;
-}
-
-export interface ProviderConfig {
- name: string;
- url: string;
- apiKey?: string;
- enableCostEvaluation?: boolean;
-}
-
-export interface ProposalOptions {
- withdrawals?: Array<{ reward_address: string; amount: string }>;
- govActionTxHash?: string;
- govActionIndex?: number;
- membersToRemove?: string[];
- newMembers?: Record;
- quorumNumerator?: number;
- quorumDenominator?: number;
- constitutionAnchorUrl?: string;
- constitutionAnchorDataHash?: string;
- constitutionScriptHash?: string;
- protocolVersionMajor?: number;
- protocolVersionMinor?: number;
- policyHash?: string;
-}
-
-export interface PoolOptions {
- relays?: Array<{ type: string; ipv4?: string; ipv6?: string; port?: number; dns_name?: string }>;
- poolMetadataUrl?: string;
- poolMetadataHash?: string;
-}
-
-export interface Relay {
- type: string;
- ipv4?: string;
- ipv6?: string;
- port?: number;
- dns_name?: string;
-}
-
-export declare class TxBuilder {
- payToAddress(address: string, ...amounts: (AmountSpec | { scriptRefCborHex?: string; scriptRefType?: string })[]): TxBuilder;
- payToContract(address: string, amounts: AmountSpec | AmountSpec[], options?: { datumCborHex?: string; datumHash?: string; scriptRefCborHex?: string; scriptRefType?: string }): TxBuilder;
- mintAssets(scriptJson: string | object, assets: Array<{ name: string; quantity: string }>, receiver: string): TxBuilder;
- attachMetadata(label: number, metadata: any): TxBuilder;
- collectFrom(utxos: any[]): TxBuilder;
- // Staking
- registerStakeAddress(address: string): TxBuilder;
- deregisterStakeAddress(address: string, refundAddress?: string | null): TxBuilder;
- delegateTo(address: string, poolId: string): TxBuilder;
- withdraw(rewardAddress: string, amount: string | number, receiver?: string | null): TxBuilder;
- // DRep
- registerDRep(credentialHash: string, credentialType?: string, options?: { anchorUrl?: string; anchorDataHash?: string }): TxBuilder;
- unregisterDRep(credentialHash: string, credentialType?: string, options?: { refundAddress?: string; refundAmount?: string | number }): TxBuilder;
- updateDRep(credentialHash: string, credentialType?: string, options?: { anchorUrl?: string; anchorDataHash?: string }): TxBuilder;
- // Voting
- delegateVotingPowerTo(address: string, drepType: string, drepHash?: string | null): TxBuilder;
- createVote(voterType: string, voterHash: string, govActionTxHash: string, govActionIndex: number, vote: string, options?: { anchorUrl?: string; anchorDataHash?: string }): TxBuilder;
- // Governance
- createProposal(govActionType: string, returnAddress: string, anchorUrl: string, anchorDataHash: string, options?: ProposalOptions): TxBuilder;
- // Pool operations
- registerPool(operator: string, vrfKeyHash: string, pledge: string | number, cost: string | number, marginNumerator: string | number, marginDenominator: string | number, rewardAddress: string, poolOwners: string[], options?: PoolOptions): TxBuilder;
- updatePool(operator: string, vrfKeyHash: string, pledge: string | number, cost: string | number, marginNumerator: string | number, marginDenominator: string | number, rewardAddress: string, poolOwners: string[], options?: PoolOptions): TxBuilder;
- retirePool(poolId: string, epoch: number): TxBuilder;
- // Treasury donation
- donateToTreasury(treasuryValue: string | number, donationAmount: string | number): TxBuilder;
- // Native script
- attachNativeScript(scriptJson: string | object): TxBuilder;
- from(address: string): TxBuilder;
- changeAddress(address: string): TxBuilder;
- feePayer(address: string): TxBuilder;
- withUtxos(utxos: any[]): TxBuilder;
- withProtocolParams(params: any): TxBuilder;
- validFrom(slot: number): TxBuilder;
- validTo(slot: number): TxBuilder;
- mergeOutputs(merge: boolean): TxBuilder;
- signerCount(count: number): TxBuilder;
- build(providerConfig?: ProviderConfig | null): QuickTxResult;
- buildWithProvider(provider: Provider): Promise;
-}
-
-export declare class Tx {
- payToAddress(address: string, ...amounts: (AmountSpec | { scriptRefCborHex?: string; scriptRefType?: string })[]): Tx;
- payToContract(address: string, amounts: AmountSpec | AmountSpec[], options?: { datumCborHex?: string; datumHash?: string; scriptRefCborHex?: string; scriptRefType?: string }): Tx;
- mintAssets(scriptJson: string | object, assets: Array<{ name: string; quantity: string }>, receiver: string): Tx;
- attachMetadata(label: number, metadata: any): Tx;
- collectFrom(utxos: any[]): Tx;
- // Staking
- registerStakeAddress(address: string): Tx;
- deregisterStakeAddress(address: string, refundAddress?: string | null): Tx;
- delegateTo(address: string, poolId: string): Tx;
- withdraw(rewardAddress: string, amount: string | number, receiver?: string | null): Tx;
- // DRep
- registerDRep(credentialHash: string, credentialType?: string, options?: { anchorUrl?: string; anchorDataHash?: string }): Tx;
- unregisterDRep(credentialHash: string, credentialType?: string, options?: { refundAddress?: string; refundAmount?: string | number }): Tx;
- updateDRep(credentialHash: string, credentialType?: string, options?: { anchorUrl?: string; anchorDataHash?: string }): Tx;
- // Voting
- delegateVotingPowerTo(address: string, drepType: string, drepHash?: string | null): Tx;
- createVote(voterType: string, voterHash: string, govActionTxHash: string, govActionIndex: number, vote: string, options?: { anchorUrl?: string; anchorDataHash?: string }): Tx;
- // Governance
- createProposal(govActionType: string, returnAddress: string, anchorUrl: string, anchorDataHash: string, options?: ProposalOptions): Tx;
- // Pool operations
- registerPool(operator: string, vrfKeyHash: string, pledge: string | number, cost: string | number, marginNumerator: string | number, marginDenominator: string | number, rewardAddress: string, poolOwners: string[], options?: PoolOptions): Tx;
- updatePool(operator: string, vrfKeyHash: string, pledge: string | number, cost: string | number, marginNumerator: string | number, marginDenominator: string | number, rewardAddress: string, poolOwners: string[], options?: PoolOptions): Tx;
- retirePool(poolId: string, epoch: number): Tx;
- // Treasury donation
- donateToTreasury(treasuryValue: string | number, donationAmount: string | number): Tx;
- // Native script
- attachNativeScript(scriptJson: string | object): Tx;
- from(address: string): Tx;
- changeAddress(address: string): Tx;
-}
-
-export declare class ComposeTxBuilder {
- feePayer(address: string): ComposeTxBuilder;
- withUtxos(utxos: any[]): ComposeTxBuilder;
- withProtocolParams(params: any): ComposeTxBuilder;
- validFrom(slot: number): ComposeTxBuilder;
- validTo(slot: number): ComposeTxBuilder;
- mergeOutputs(merge: boolean): ComposeTxBuilder;
- signerCount(count: number): ComposeTxBuilder;
- build(providerConfig?: ProviderConfig | null): QuickTxResult;
- buildWithProvider(provider: Provider): Promise;
-}
-
-export declare class ScriptTxBuilder {
- payToAddress(address: string, ...amounts: (AmountSpec | { scriptRefCborHex?: string; scriptRefType?: string })[]): ScriptTxBuilder;
- payToContract(address: string, amounts: AmountSpec | AmountSpec[], options?: { datumCborHex?: string; datumHash?: string; scriptRefCborHex?: string; scriptRefType?: string }): ScriptTxBuilder;
- attachMetadata(label: number, metadata: any): ScriptTxBuilder;
- collectFrom(utxos: any[]): ScriptTxBuilder;
- collectFromScript(utxos: any[], redeemerCborHex: string, datumCborHex?: string | null): ScriptTxBuilder;
- readFrom(referenceInputs: ReferenceInput[]): ScriptTxBuilder;
- mintPlutusAssets(scriptCborHex: string, scriptType: string, assets: MintAsset[], redeemerCborHex: string, receiver?: string | null, outputDatumCborHex?: string | null): ScriptTxBuilder;
- attachSpendingValidator(scriptCborHex: string, scriptType: string): ScriptTxBuilder;
- attachCertificateValidator(scriptCborHex: string, scriptType: string): ScriptTxBuilder;
- attachRewardValidator(scriptCborHex: string, scriptType: string): ScriptTxBuilder;
- attachProposingValidator(scriptCborHex: string, scriptType: string): ScriptTxBuilder;
- attachVotingValidator(scriptCborHex: string, scriptType: string): ScriptTxBuilder;
- // Staking (with redeemer)
- deregisterStakeAddress(address: string, redeemerCborHex: string, refundAddress?: string | null): ScriptTxBuilder;
- delegateTo(address: string, poolId: string, redeemerCborHex: string): ScriptTxBuilder;
- withdraw(rewardAddress: string, amount: string | number, redeemerCborHex: string, receiver?: string | null): ScriptTxBuilder;
- // DRep (with redeemer)
- registerDRep(credentialHash: string, credentialType: string, redeemerCborHex: string, options?: { anchorUrl?: string; anchorDataHash?: string }): ScriptTxBuilder;
- unregisterDRep(credentialHash: string, credentialType: string, redeemerCborHex: string, options?: { refundAddress?: string; refundAmount?: string | number }): ScriptTxBuilder;
- updateDRep(credentialHash: string, credentialType: string, redeemerCborHex: string, options?: { anchorUrl?: string; anchorDataHash?: string }): ScriptTxBuilder;
- // Voting (with redeemer)
- delegateVotingPowerTo(address: string, drepType: string, drepHash: string, redeemerCborHex: string): ScriptTxBuilder;
- createVote(voterType: string, voterHash: string, govActionTxHash: string, govActionIndex: number, vote: string, redeemerCborHex: string, options?: { anchorUrl?: string; anchorDataHash?: string }): ScriptTxBuilder;
- // Governance (with redeemer)
- createProposal(govActionType: string, returnAddress: string, anchorUrl: string, anchorDataHash: string, redeemerCborHex: string, options?: ProposalOptions): ScriptTxBuilder;
- // Treasury donation (with redeemer)
- donateToTreasury(treasuryValue: string | number, donationAmount: string | number, redeemerCborHex: string): ScriptTxBuilder;
- from(address: string): ScriptTxBuilder;
- changeAddress(address: string): ScriptTxBuilder;
- changeDatum(datumCborHex: string): ScriptTxBuilder;
- changeDatumHash(hash: string): ScriptTxBuilder;
- feePayer(address: string): ScriptTxBuilder;
- withUtxos(utxos: any[]): ScriptTxBuilder;
- withProtocolParams(params: any): ScriptTxBuilder;
- validFrom(slot: number): ScriptTxBuilder;
- validTo(slot: number): ScriptTxBuilder;
- mergeOutputs(merge: boolean): ScriptTxBuilder;
- signerCount(count: number): ScriptTxBuilder;
- build(providerConfig?: ProviderConfig | null): QuickTxResult;
- buildWithProvider(provider: Provider): Promise;
-}
-
-export declare class ScriptTx {
- payToAddress(address: string, ...amounts: (AmountSpec | { scriptRefCborHex?: string; scriptRefType?: string })[]): ScriptTx;
- payToContract(address: string, amounts: AmountSpec | AmountSpec[], options?: { datumCborHex?: string; datumHash?: string; scriptRefCborHex?: string; scriptRefType?: string }): ScriptTx;
- attachMetadata(label: number, metadata: any): ScriptTx;
- collectFrom(utxos: any[]): ScriptTx;
- collectFromScript(utxos: any[], redeemerCborHex: string, datumCborHex?: string | null): ScriptTx;
- readFrom(referenceInputs: ReferenceInput[]): ScriptTx;
- mintPlutusAssets(scriptCborHex: string, scriptType: string, assets: MintAsset[], redeemerCborHex: string, receiver?: string | null, outputDatumCborHex?: string | null): ScriptTx;
- attachSpendingValidator(scriptCborHex: string, scriptType: string): ScriptTx;
- attachCertificateValidator(scriptCborHex: string, scriptType: string): ScriptTx;
- attachRewardValidator(scriptCborHex: string, scriptType: string): ScriptTx;
- attachProposingValidator(scriptCborHex: string, scriptType: string): ScriptTx;
- attachVotingValidator(scriptCborHex: string, scriptType: string): ScriptTx;
- // Staking (with redeemer)
- deregisterStakeAddress(address: string, redeemerCborHex: string, refundAddress?: string | null): ScriptTx;
- delegateTo(address: string, poolId: string, redeemerCborHex: string): ScriptTx;
- withdraw(rewardAddress: string, amount: string | number, redeemerCborHex: string, receiver?: string | null): ScriptTx;
- // DRep (with redeemer)
- registerDRep(credentialHash: string, credentialType: string, redeemerCborHex: string, options?: { anchorUrl?: string; anchorDataHash?: string }): ScriptTx;
- unregisterDRep(credentialHash: string, credentialType: string, redeemerCborHex: string, options?: { refundAddress?: string; refundAmount?: string | number }): ScriptTx;
- updateDRep(credentialHash: string, credentialType: string, redeemerCborHex: string, options?: { anchorUrl?: string; anchorDataHash?: string }): ScriptTx;
- // Voting (with redeemer)
- delegateVotingPowerTo(address: string, drepType: string, drepHash: string, redeemerCborHex: string): ScriptTx;
- createVote(voterType: string, voterHash: string, govActionTxHash: string, govActionIndex: number, vote: string, redeemerCborHex: string, options?: { anchorUrl?: string; anchorDataHash?: string }): ScriptTx;
- // Governance (with redeemer)
- createProposal(govActionType: string, returnAddress: string, anchorUrl: string, anchorDataHash: string, redeemerCborHex: string, options?: ProposalOptions): ScriptTx;
- // Treasury donation (with redeemer)
- donateToTreasury(treasuryValue: string | number, donationAmount: string | number, redeemerCborHex: string): ScriptTx;
- from(address: string): ScriptTx;
- changeAddress(address: string): ScriptTx;
- changeDatum(datumCborHex: string): ScriptTx;
- changeDatumHash(hash: string): ScriptTx;
-}
-
export declare class QuickTxApi {
- newTx(): TxBuilder;
- tx(): Tx;
- newScriptTx(): ScriptTxBuilder;
- scriptTx(): ScriptTx;
- compose(...txs: (Tx | ScriptTx)[]): ComposeTxBuilder;
-}
-
-export declare class Provider {
- getUtxos(address: string): Promise;
- getProtocolParams(): Promise;
- submitTx(txCborHex: string): Promise;
-}
-
-export declare class YaciDevKitProvider extends Provider {
- constructor(baseUrl?: string);
- getUtxos(address: string): Promise;
- getProtocolParams(): Promise;
- submitTx(txCborHex: string): Promise;
- topup(address: string, adaAmount?: number): Promise;
- reset(): Promise;
- waitForBlock(ms?: number): Promise;
- isAvailable(): Promise;
+ /**
+ * Build an unsigned transaction from a CCL TxPlan (YAML), fully offline.
+ * @param txplanYaml the TxPlan YAML document defining the transaction(s)
+ * @param utxos UTXOs available to the sender (CCL Utxo model)
+ * @param protocolParams protocol parameters (CCL ProtocolParams model)
+ * @param execUnits optional redeemer execution units (one per redeemer, in transaction order)
+ * for Plutus script transactions; compute them with any evaluator (Ogmios, Blockfrost, Aiken,
+ * Scalus) — the bridge does not run the script
+ */
+ build(
+ txplanYaml: string,
+ utxos: object[],
+ protocolParams: object,
+ execUnits?: Array<{ mem: number | string; steps: number | string }>,
+ ): QuickTxResult;
}
export declare const MAINNET: number;
diff --git a/wrappers/js/src/index.js b/wrappers/js/src/index.js
index 38dbb9c..9aaf02c 100644
--- a/wrappers/js/src/index.js
+++ b/wrappers/js/src/index.js
@@ -1,8 +1,7 @@
import { dlopen, FFIType, ptr, CString } from 'bun:ffi';
import path from 'path';
import os from 'os';
-
-export { Provider, YaciDevKitProvider } from './provider.js';
+import { parse as parseYaml } from 'yaml';
// Error codes
export const CCL_SUCCESS = 0;
@@ -72,6 +71,7 @@ export class CclBridge {
ccl_account_get_public_key: { args: [FFIType.ptr, FFIType.cstring, FFIType.i32, FFIType.i32, FFIType.i32], returns: FFIType.i32 },
ccl_account_get_drep_id: { args: [FFIType.ptr, FFIType.cstring, FFIType.i32, FFIType.i32], returns: FFIType.i32 },
ccl_account_sign_tx: { args: [FFIType.ptr, FFIType.cstring, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.cstring], returns: FFIType.i32 },
+ ccl_account_sign_tx_multi: { args: [FFIType.ptr, FFIType.cstring, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.cstring, FFIType.cstring], returns: FFIType.i32 },
// Address
ccl_address_info: { args: [FFIType.ptr, FFIType.cstring], returns: FFIType.i32 },
@@ -114,7 +114,7 @@ export class CclBridge {
ccl_wallet_get_address: { args: [FFIType.ptr, FFIType.cstring, FFIType.i32, FFIType.i32], returns: FFIType.i32 },
// QuickTx
- ccl_quicktx_build: { args: [FFIType.ptr, FFIType.cstring], returns: FFIType.i32 },
+ ccl_quicktx_build: { args: [FFIType.ptr, FFIType.cstring, FFIType.cstring, FFIType.cstring, FFIType.cstring], returns: FFIType.i32 },
});
this._lib = lib.symbols;
@@ -210,6 +210,15 @@ class AccountApi {
return this._b._check(
this._b._lib.ccl_account_sign_tx(this._b._thread, cstr(mnemonic), networkId, accountIndex, addressIndex, cstr(txCborHex)));
}
+
+ // Sign with one or more of the account's keys, selected by role (any of: payment, stake, drep,
+ // committee_cold, committee_hot, applied in order). Use for transactions whose certificates also
+ // need the stake or DRep key — stake registration/delegation/withdrawal and DRep/vote operations.
+ signTxWithKeys(mnemonic, networkId, accountIndex, addressIndex, txCborHex, keys) {
+ const keysStr = Array.isArray(keys) ? keys.join(",") : keys;
+ return this._b._check(
+ this._b._lib.ccl_account_sign_tx_multi(this._b._thread, cstr(mnemonic), networkId, accountIndex, addressIndex, cstr(txCborHex), cstr(keysStr)));
+ }
}
class AddressApi {
@@ -350,1317 +359,31 @@ class WalletApi {
}
class QuickTxApi {
- constructor(bridge) { this._b = bridge; }
-
- newTx() {
- return new TxBuilder(this._b);
- }
-
- tx() {
- return new Tx();
- }
-
- newScriptTx() {
- return new ScriptTxBuilder(this._b);
- }
-
- scriptTx() {
- return new ScriptTx();
- }
-
- compose(...txs) {
- return new ComposeTxBuilder(this._b, txs);
- }
-}
-
-export class Amount {
- static lovelace(quantity) {
- return { unit: 'lovelace', quantity: String(Math.floor(quantity)) };
- }
-
- static ada(adaAmount) {
- return { unit: 'lovelace', quantity: String(Math.floor(adaAmount * 1_000_000)) };
- }
-
- static asset(unit, quantity) {
- return { unit, quantity: String(Math.floor(quantity)) };
- }
-}
-
-export class TxBuilder {
constructor(bridge) {
this._b = bridge;
- this._operations = [];
- this._from = null;
- this._changeAddress = null;
- this._feePayer = null;
- this._utxos = null;
- this._protocolParams = null;
- this._validity = {};
- this._mergeOutputs = null;
- this._signerCount = 1;
- }
-
- payToAddress(address, ...args) {
- let amounts = args;
- let options = {};
- if (args.length > 0) {
- const last = args[args.length - 1];
- if (last && typeof last === 'object' && !last.unit) {
- options = args[args.length - 1];
- amounts = args.slice(0, -1);
- }
- }
- const op = {
- type: 'pay_to_address',
- address,
- amounts: [...amounts],
- };
- if (options.scriptRefCborHex) op.script_ref_cbor_hex = options.scriptRefCborHex;
- if (options.scriptRefType) op.script_ref_type = options.scriptRefType;
- this._operations.push(op);
- return this;
- }
-
- payToContract(address, amounts, { datumCborHex, datumHash, scriptRefCborHex, scriptRefType } = {}) {
- const op = {
- type: 'pay_to_contract',
- address,
- amounts: Array.isArray(amounts) ? amounts : [amounts],
- };
- if (datumCborHex) op.datum_cbor_hex = datumCborHex;
- if (datumHash) op.datum_hash = datumHash;
- if (scriptRefCborHex) op.script_ref_cbor_hex = scriptRefCborHex;
- if (scriptRefType) op.script_ref_type = scriptRefType;
- this._operations.push(op);
- return this;
- }
-
- mintAssets(scriptJson, assets, receiver) {
- this._operations.push({
- type: 'mint_assets',
- script_json: typeof scriptJson === 'string' ? scriptJson : JSON.stringify(scriptJson),
- assets,
- receiver,
- });
- return this;
- }
-
- attachMetadata(label, metadata) {
- this._operations.push({
- type: 'attach_metadata',
- label,
- metadata,
- });
- return this;
- }
-
- collectFrom(utxos) {
- this._operations.push({
- type: 'collect_from',
- collect_utxos: utxos,
- });
- return this;
- }
-
- // Staking
- registerStakeAddress(address) {
- this._operations.push({ type: 'register_stake_address', address });
- return this;
- }
-
- deregisterStakeAddress(address, refundAddress = null) {
- const op = { type: 'deregister_stake_address', address };
- if (refundAddress) op.refund_address = refundAddress;
- this._operations.push(op);
- return this;
- }
-
- delegateTo(address, poolId) {
- this._operations.push({ type: 'delegate_to', address, pool_id: poolId });
- return this;
- }
-
- withdraw(rewardAddress, amount, receiver = null) {
- const op = { type: 'withdraw', reward_address: rewardAddress, amount: String(amount) };
- if (receiver) op.receiver = receiver;
- this._operations.push(op);
- return this;
- }
-
- // DRep
- registerDRep(credentialHash, credentialType = 'key', { anchorUrl, anchorDataHash } = {}) {
- const op = { type: 'register_drep', credential_hash: credentialHash, credential_type: credentialType };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- unregisterDRep(credentialHash, credentialType = 'key', { refundAddress, refundAmount } = {}) {
- const op = { type: 'unregister_drep', credential_hash: credentialHash, credential_type: credentialType };
- if (refundAddress) op.refund_address = refundAddress;
- if (refundAmount != null) op.refund_amount = String(refundAmount);
- this._operations.push(op);
- return this;
- }
-
- updateDRep(credentialHash, credentialType = 'key', { anchorUrl, anchorDataHash } = {}) {
- const op = { type: 'update_drep', credential_hash: credentialHash, credential_type: credentialType };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- // Voting
- delegateVotingPowerTo(address, drepType, drepHash = null) {
- const op = { type: 'delegate_voting_power_to', address, drep_type: drepType };
- if (drepHash) op.drep_hash = drepHash;
- this._operations.push(op);
- return this;
- }
-
- createVote(voterType, voterHash, govActionTxHash, govActionIndex, vote, { anchorUrl, anchorDataHash } = {}) {
- const op = {
- type: 'create_vote', voter_type: voterType, voter_hash: voterHash,
- gov_action_tx_hash: govActionTxHash, gov_action_index: govActionIndex, vote,
- };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- // Governance
- createProposal(govActionType, returnAddress, anchorUrl, anchorDataHash, options = {}) {
- const op = {
- type: 'create_proposal', gov_action_type: govActionType,
- return_address: returnAddress, anchor_url: anchorUrl, anchor_data_hash: anchorDataHash,
- };
- if (options.withdrawals) op.withdrawals = options.withdrawals;
- if (options.govActionTxHash) op.gov_action_tx_hash = options.govActionTxHash;
- if (options.govActionIndex != null) op.gov_action_index = options.govActionIndex;
- if (options.membersToRemove) op.members_to_remove = options.membersToRemove;
- if (options.newMembers) op.new_members = options.newMembers;
- if (options.quorumNumerator != null) op.quorum_numerator = String(options.quorumNumerator);
- if (options.quorumDenominator != null) op.quorum_denominator = String(options.quorumDenominator);
- if (options.constitutionAnchorUrl) op.constitution_anchor_url = options.constitutionAnchorUrl;
- if (options.constitutionAnchorDataHash) op.constitution_anchor_data_hash = options.constitutionAnchorDataHash;
- if (options.constitutionScriptHash) op.constitution_script_hash = options.constitutionScriptHash;
- if (options.protocolVersionMajor != null) op.protocol_version_major = options.protocolVersionMajor;
- if (options.protocolVersionMinor != null) op.protocol_version_minor = options.protocolVersionMinor;
- if (options.policyHash) op.policy_hash = options.policyHash;
- this._operations.push(op);
- return this;
- }
-
- // Pool operations
- registerPool(operator, vrfKeyHash, pledge, cost, marginNumerator, marginDenominator,
- rewardAddress, poolOwners, options = {}) {
- const op = {
- type: 'register_pool', operator, vrf_key_hash: vrfKeyHash,
- pledge: String(pledge), cost: String(cost),
- margin_numerator: String(marginNumerator), margin_denominator: String(marginDenominator),
- reward_address: rewardAddress, pool_owners: poolOwners,
- };
- if (options.relays) op.relays = options.relays;
- if (options.poolMetadataUrl) op.pool_metadata_url = options.poolMetadataUrl;
- if (options.poolMetadataHash) op.pool_metadata_hash = options.poolMetadataHash;
- this._operations.push(op);
- return this;
- }
-
- updatePool(operator, vrfKeyHash, pledge, cost, marginNumerator, marginDenominator,
- rewardAddress, poolOwners, options = {}) {
- const op = {
- type: 'update_pool', operator, vrf_key_hash: vrfKeyHash,
- pledge: String(pledge), cost: String(cost),
- margin_numerator: String(marginNumerator), margin_denominator: String(marginDenominator),
- reward_address: rewardAddress, pool_owners: poolOwners,
- };
- if (options.relays) op.relays = options.relays;
- if (options.poolMetadataUrl) op.pool_metadata_url = options.poolMetadataUrl;
- if (options.poolMetadataHash) op.pool_metadata_hash = options.poolMetadataHash;
- this._operations.push(op);
- return this;
- }
-
- retirePool(poolId, epoch) {
- this._operations.push({ type: 'retire_pool', pool_id: poolId, epoch });
- return this;
- }
-
- // Treasury donation
- donateToTreasury(treasuryValue, donationAmount) {
- this._operations.push({
- type: 'donate_to_treasury',
- treasury_value: String(treasuryValue),
- donation_amount: String(donationAmount),
- });
- return this;
- }
-
- // Native script attachment
- attachNativeScript(scriptJson) {
- this._operations.push({
- type: 'attach_native_script',
- script_json: typeof scriptJson === 'string' ? scriptJson : JSON.stringify(scriptJson),
- });
- return this;
- }
-
- from(address) {
- this._from = address;
- return this;
- }
-
- changeAddress(address) {
- this._changeAddress = address;
- return this;
- }
-
- feePayer(address) {
- this._feePayer = address;
- return this;
- }
-
- withUtxos(utxos) {
- this._utxos = utxos;
- return this;
- }
-
- withProtocolParams(params) {
- this._protocolParams = params;
- return this;
- }
-
- validFrom(slot) {
- this._validity.valid_from = slot;
- return this;
- }
-
- validTo(slot) {
- this._validity.valid_to = slot;
- return this;
- }
-
- mergeOutputs(merge) {
- this._mergeOutputs = merge;
- return this;
- }
-
- signerCount(count) {
- this._signerCount = count;
- return this;
- }
-
- build(providerConfig = null) {
- const spec = {
- operations: this._operations,
- from: this._from,
- signer_count: this._signerCount,
- };
-
- if (providerConfig) {
- spec.provider = { name: providerConfig.name, url: providerConfig.url };
- if (providerConfig.apiKey) spec.provider.api_key = providerConfig.apiKey;
- if (providerConfig.enableCostEvaluation !== undefined) spec.provider.enable_cost_evaluation = providerConfig.enableCostEvaluation;
- if (this._protocolParams !== null) spec.protocol_params = this._protocolParams;
- } else {
- spec.utxos = this._utxos;
- spec.protocol_params = this._protocolParams;
- }
-
- if (this._changeAddress) spec.change_address = this._changeAddress;
- if (this._feePayer) spec.fee_payer = this._feePayer;
- if (Object.keys(this._validity).length > 0) spec.validity = this._validity;
- if (this._mergeOutputs !== null) spec.merge_outputs = this._mergeOutputs;
-
- const specJson = JSON.stringify(spec);
- const rc = this._b._lib.ccl_quicktx_build(this._b._thread, cstr(specJson));
- return JSON.parse(this._b._check(rc));
- }
-
- async buildWithProvider(provider) {
- if (this._utxos === null && this._from) {
- this._utxos = await provider.getUtxos(this._from);
- }
- if (this._protocolParams === null) {
- this._protocolParams = await provider.getProtocolParams();
- }
- return this.build();
- }
-}
-
-export class Tx {
- constructor() {
- this._operations = [];
- this._from = null;
- this._changeAddress = null;
- }
-
- payToAddress(address, ...args) {
- let amounts = args;
- let options = {};
- if (args.length > 0) {
- const last = args[args.length - 1];
- if (last && typeof last === 'object' && !last.unit) {
- options = args[args.length - 1];
- amounts = args.slice(0, -1);
- }
- }
- const op = {
- type: 'pay_to_address',
- address,
- amounts: [...amounts],
- };
- if (options.scriptRefCborHex) op.script_ref_cbor_hex = options.scriptRefCborHex;
- if (options.scriptRefType) op.script_ref_type = options.scriptRefType;
- this._operations.push(op);
- return this;
- }
-
- payToContract(address, amounts, { datumCborHex, datumHash, scriptRefCborHex, scriptRefType } = {}) {
- const op = {
- type: 'pay_to_contract',
- address,
- amounts: Array.isArray(amounts) ? amounts : [amounts],
- };
- if (datumCborHex) op.datum_cbor_hex = datumCborHex;
- if (datumHash) op.datum_hash = datumHash;
- if (scriptRefCborHex) op.script_ref_cbor_hex = scriptRefCborHex;
- if (scriptRefType) op.script_ref_type = scriptRefType;
- this._operations.push(op);
- return this;
- }
-
- mintAssets(scriptJson, assets, receiver) {
- this._operations.push({
- type: 'mint_assets',
- script_json: typeof scriptJson === 'string' ? scriptJson : JSON.stringify(scriptJson),
- assets,
- receiver,
- });
- return this;
- }
-
- attachMetadata(label, metadata) {
- this._operations.push({
- type: 'attach_metadata',
- label,
- metadata,
- });
- return this;
- }
-
- collectFrom(utxos) {
- this._operations.push({
- type: 'collect_from',
- collect_utxos: utxos,
- });
- return this;
- }
-
- // Staking
- registerStakeAddress(address) {
- this._operations.push({ type: 'register_stake_address', address });
- return this;
- }
-
- deregisterStakeAddress(address, refundAddress = null) {
- const op = { type: 'deregister_stake_address', address };
- if (refundAddress) op.refund_address = refundAddress;
- this._operations.push(op);
- return this;
- }
-
- delegateTo(address, poolId) {
- this._operations.push({ type: 'delegate_to', address, pool_id: poolId });
- return this;
- }
-
- withdraw(rewardAddress, amount, receiver = null) {
- const op = { type: 'withdraw', reward_address: rewardAddress, amount: String(amount) };
- if (receiver) op.receiver = receiver;
- this._operations.push(op);
- return this;
- }
-
- // DRep
- registerDRep(credentialHash, credentialType = 'key', { anchorUrl, anchorDataHash } = {}) {
- const op = { type: 'register_drep', credential_hash: credentialHash, credential_type: credentialType };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- unregisterDRep(credentialHash, credentialType = 'key', { refundAddress, refundAmount } = {}) {
- const op = { type: 'unregister_drep', credential_hash: credentialHash, credential_type: credentialType };
- if (refundAddress) op.refund_address = refundAddress;
- if (refundAmount != null) op.refund_amount = String(refundAmount);
- this._operations.push(op);
- return this;
- }
-
- updateDRep(credentialHash, credentialType = 'key', { anchorUrl, anchorDataHash } = {}) {
- const op = { type: 'update_drep', credential_hash: credentialHash, credential_type: credentialType };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- // Voting
- delegateVotingPowerTo(address, drepType, drepHash = null) {
- const op = { type: 'delegate_voting_power_to', address, drep_type: drepType };
- if (drepHash) op.drep_hash = drepHash;
- this._operations.push(op);
- return this;
- }
-
- createVote(voterType, voterHash, govActionTxHash, govActionIndex, vote, { anchorUrl, anchorDataHash } = {}) {
- const op = {
- type: 'create_vote', voter_type: voterType, voter_hash: voterHash,
- gov_action_tx_hash: govActionTxHash, gov_action_index: govActionIndex, vote,
- };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- // Governance
- createProposal(govActionType, returnAddress, anchorUrl, anchorDataHash, options = {}) {
- const op = {
- type: 'create_proposal', gov_action_type: govActionType,
- return_address: returnAddress, anchor_url: anchorUrl, anchor_data_hash: anchorDataHash,
- };
- if (options.withdrawals) op.withdrawals = options.withdrawals;
- if (options.govActionTxHash) op.gov_action_tx_hash = options.govActionTxHash;
- if (options.govActionIndex != null) op.gov_action_index = options.govActionIndex;
- if (options.membersToRemove) op.members_to_remove = options.membersToRemove;
- if (options.newMembers) op.new_members = options.newMembers;
- if (options.quorumNumerator != null) op.quorum_numerator = String(options.quorumNumerator);
- if (options.quorumDenominator != null) op.quorum_denominator = String(options.quorumDenominator);
- if (options.constitutionAnchorUrl) op.constitution_anchor_url = options.constitutionAnchorUrl;
- if (options.constitutionAnchorDataHash) op.constitution_anchor_data_hash = options.constitutionAnchorDataHash;
- if (options.constitutionScriptHash) op.constitution_script_hash = options.constitutionScriptHash;
- if (options.protocolVersionMajor != null) op.protocol_version_major = options.protocolVersionMajor;
- if (options.protocolVersionMinor != null) op.protocol_version_minor = options.protocolVersionMinor;
- if (options.policyHash) op.policy_hash = options.policyHash;
- this._operations.push(op);
- return this;
- }
-
- // Pool operations
- registerPool(operator, vrfKeyHash, pledge, cost, marginNumerator, marginDenominator,
- rewardAddress, poolOwners, options = {}) {
- const op = {
- type: 'register_pool', operator, vrf_key_hash: vrfKeyHash,
- pledge: String(pledge), cost: String(cost),
- margin_numerator: String(marginNumerator), margin_denominator: String(marginDenominator),
- reward_address: rewardAddress, pool_owners: poolOwners,
- };
- if (options.relays) op.relays = options.relays;
- if (options.poolMetadataUrl) op.pool_metadata_url = options.poolMetadataUrl;
- if (options.poolMetadataHash) op.pool_metadata_hash = options.poolMetadataHash;
- this._operations.push(op);
- return this;
- }
-
- updatePool(operator, vrfKeyHash, pledge, cost, marginNumerator, marginDenominator,
- rewardAddress, poolOwners, options = {}) {
- const op = {
- type: 'update_pool', operator, vrf_key_hash: vrfKeyHash,
- pledge: String(pledge), cost: String(cost),
- margin_numerator: String(marginNumerator), margin_denominator: String(marginDenominator),
- reward_address: rewardAddress, pool_owners: poolOwners,
- };
- if (options.relays) op.relays = options.relays;
- if (options.poolMetadataUrl) op.pool_metadata_url = options.poolMetadataUrl;
- if (options.poolMetadataHash) op.pool_metadata_hash = options.poolMetadataHash;
- this._operations.push(op);
- return this;
- }
-
- retirePool(poolId, epoch) {
- this._operations.push({ type: 'retire_pool', pool_id: poolId, epoch });
- return this;
- }
-
- // Treasury donation
- donateToTreasury(treasuryValue, donationAmount) {
- this._operations.push({
- type: 'donate_to_treasury',
- treasury_value: String(treasuryValue),
- donation_amount: String(donationAmount),
- });
- return this;
- }
-
- // Native script attachment
- attachNativeScript(scriptJson) {
- this._operations.push({
- type: 'attach_native_script',
- script_json: typeof scriptJson === 'string' ? scriptJson : JSON.stringify(scriptJson),
- });
- return this;
- }
-
- from(address) {
- this._from = address;
- return this;
- }
-
- changeAddress(address) {
- this._changeAddress = address;
- return this;
- }
-
- _toSpec() {
- const spec = {
- from: this._from,
- operations: this._operations,
- };
- if (this._changeAddress) spec.change_address = this._changeAddress;
- return spec;
- }
-}
-
-export class ComposeTxBuilder {
- constructor(bridge, txs) {
- this._b = bridge;
- this._txs = [...txs];
- this._feePayer = null;
- this._utxos = null;
- this._protocolParams = null;
- this._validity = {};
- this._mergeOutputs = null;
- this._signerCount = null;
- }
-
- feePayer(address) {
- this._feePayer = address;
- return this;
- }
-
- withUtxos(utxos) {
- this._utxos = utxos;
- return this;
- }
-
- withProtocolParams(params) {
- this._protocolParams = params;
- return this;
- }
-
- validFrom(slot) {
- this._validity.valid_from = slot;
- return this;
- }
-
- validTo(slot) {
- this._validity.valid_to = slot;
- return this;
- }
-
- mergeOutputs(merge) {
- this._mergeOutputs = merge;
- return this;
- }
-
- signerCount(count) {
- this._signerCount = count;
- return this;
- }
-
- build(providerConfig = null) {
- const spec = {
- transactions: this._txs.map(tx => tx._toSpec()),
- fee_payer: this._feePayer,
- };
-
- if (providerConfig) {
- spec.provider = { name: providerConfig.name, url: providerConfig.url };
- if (providerConfig.apiKey) spec.provider.api_key = providerConfig.apiKey;
- if (providerConfig.enableCostEvaluation !== undefined) spec.provider.enable_cost_evaluation = providerConfig.enableCostEvaluation;
- if (this._protocolParams !== null) spec.protocol_params = this._protocolParams;
- } else {
- spec.utxos = this._utxos;
- spec.protocol_params = this._protocolParams;
- }
-
- if (this._signerCount !== null) spec.signer_count = this._signerCount;
- if (Object.keys(this._validity).length > 0) spec.validity = this._validity;
- if (this._mergeOutputs !== null) spec.merge_outputs = this._mergeOutputs;
-
- const specJson = JSON.stringify(spec);
- const rc = this._b._lib.ccl_quicktx_build(this._b._thread, cstr(specJson));
- return JSON.parse(this._b._check(rc));
- }
-
- async buildWithProvider(provider) {
- if (this._utxos === null) {
- const addresses = new Set();
- for (const tx of this._txs) {
- if (tx._from) addresses.add(tx._from);
- }
- const allUtxos = [];
- for (const addr of addresses) {
- const utxos = await provider.getUtxos(addr);
- allUtxos.push(...utxos);
- }
- this._utxos = allUtxos;
- }
- if (this._protocolParams === null) {
- this._protocolParams = await provider.getProtocolParams();
- }
- return this.build();
- }
-}
-
-export class ScriptTxBuilder {
- constructor(bridge) {
- this._b = bridge;
- this._operations = [];
- this._from = null;
- this._changeAddress = null;
- this._feePayer = null;
- this._utxos = null;
- this._protocolParams = null;
- this._validity = {};
- this._mergeOutputs = null;
- this._signerCount = 1;
- this._changeDatumCborHex = null;
- this._changeDatumHash = null;
- }
-
- payToAddress(address, ...args) {
- let amounts = args;
- let options = {};
- if (args.length > 0) {
- const last = args[args.length - 1];
- if (last && typeof last === 'object' && !last.unit) {
- options = args[args.length - 1];
- amounts = args.slice(0, -1);
- }
- }
- const op = {
- type: 'pay_to_address',
- address,
- amounts: [...amounts],
- };
- if (options.scriptRefCborHex) op.script_ref_cbor_hex = options.scriptRefCborHex;
- if (options.scriptRefType) op.script_ref_type = options.scriptRefType;
- this._operations.push(op);
- return this;
- }
-
- payToContract(address, amounts, { datumCborHex, datumHash, scriptRefCborHex, scriptRefType } = {}) {
- const op = {
- type: 'pay_to_contract',
- address,
- amounts: Array.isArray(amounts) ? amounts : [amounts],
- };
- if (datumCborHex) op.datum_cbor_hex = datumCborHex;
- if (datumHash) op.datum_hash = datumHash;
- if (scriptRefCborHex) op.script_ref_cbor_hex = scriptRefCborHex;
- if (scriptRefType) op.script_ref_type = scriptRefType;
- this._operations.push(op);
- return this;
- }
-
- attachMetadata(label, metadata) {
- this._operations.push({
- type: 'attach_metadata',
- label,
- metadata,
- });
- return this;
- }
-
- collectFrom(utxos) {
- this._operations.push({
- type: 'collect_from',
- collect_utxos: utxos,
- });
- return this;
- }
-
- collectFromScript(utxos, redeemerCborHex, datumCborHex = null) {
- const op = {
- type: 'collect_from',
- collect_utxos: utxos,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (datumCborHex) op.datum_cbor_hex = datumCborHex;
- this._operations.push(op);
- return this;
- }
-
- readFrom(referenceInputs) {
- this._operations.push({
- type: 'read_from',
- reference_inputs: referenceInputs.map(ri => ({ tx_hash: ri.txHash, output_index: ri.outputIndex })),
- });
- return this;
- }
-
- mintPlutusAssets(scriptCborHex, scriptType, assets, redeemerCborHex, receiver = null, outputDatumCborHex = null) {
- const op = {
- type: 'mint_plutus_assets',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- assets,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (receiver) op.receiver = receiver;
- if (outputDatumCborHex) op.output_datum_cbor_hex = outputDatumCborHex;
- this._operations.push(op);
- return this;
- }
-
- attachSpendingValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_spending_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- attachCertificateValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_certificate_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- attachRewardValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_reward_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- attachProposingValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_proposing_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- attachVotingValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_voting_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- // Staking (with redeemer)
- deregisterStakeAddress(address, redeemerCborHex, refundAddress = null) {
- const op = { type: 'deregister_stake_address', address, redeemer_cbor_hex: redeemerCborHex };
- if (refundAddress) op.refund_address = refundAddress;
- this._operations.push(op);
- return this;
- }
-
- delegateTo(address, poolId, redeemerCborHex) {
- this._operations.push({
- type: 'delegate_to', address, pool_id: poolId, redeemer_cbor_hex: redeemerCborHex,
- });
- return this;
- }
-
- withdraw(rewardAddress, amount, redeemerCborHex, receiver = null) {
- const op = {
- type: 'withdraw', reward_address: rewardAddress, amount: String(amount),
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (receiver) op.receiver = receiver;
- this._operations.push(op);
- return this;
- }
-
- // DRep (with redeemer)
- registerDRep(credentialHash, credentialType, redeemerCborHex, { anchorUrl, anchorDataHash } = {}) {
- const op = {
- type: 'register_drep', credential_hash: credentialHash, credential_type: credentialType,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- unregisterDRep(credentialHash, credentialType, redeemerCborHex, { refundAddress, refundAmount } = {}) {
- const op = {
- type: 'unregister_drep', credential_hash: credentialHash, credential_type: credentialType,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (refundAddress) op.refund_address = refundAddress;
- if (refundAmount != null) op.refund_amount = String(refundAmount);
- this._operations.push(op);
- return this;
- }
-
- updateDRep(credentialHash, credentialType, redeemerCborHex, { anchorUrl, anchorDataHash } = {}) {
- const op = {
- type: 'update_drep', credential_hash: credentialHash, credential_type: credentialType,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- // Voting (with redeemer)
- delegateVotingPowerTo(address, drepType, drepHash, redeemerCborHex) {
- this._operations.push({
- type: 'delegate_voting_power_to', address, drep_type: drepType,
- drep_hash: drepHash, redeemer_cbor_hex: redeemerCborHex,
- });
- return this;
- }
-
- createVote(voterType, voterHash, govActionTxHash, govActionIndex, vote, redeemerCborHex, { anchorUrl, anchorDataHash } = {}) {
- const op = {
- type: 'create_vote', voter_type: voterType, voter_hash: voterHash,
- gov_action_tx_hash: govActionTxHash, gov_action_index: govActionIndex, vote,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- // Governance (with redeemer)
- createProposal(govActionType, returnAddress, anchorUrl, anchorDataHash, redeemerCborHex, options = {}) {
- const op = {
- type: 'create_proposal', gov_action_type: govActionType,
- return_address: returnAddress, anchor_url: anchorUrl, anchor_data_hash: anchorDataHash,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (options.withdrawals) op.withdrawals = options.withdrawals;
- if (options.govActionTxHash) op.gov_action_tx_hash = options.govActionTxHash;
- if (options.govActionIndex != null) op.gov_action_index = options.govActionIndex;
- if (options.membersToRemove) op.members_to_remove = options.membersToRemove;
- if (options.newMembers) op.new_members = options.newMembers;
- if (options.quorumNumerator != null) op.quorum_numerator = String(options.quorumNumerator);
- if (options.quorumDenominator != null) op.quorum_denominator = String(options.quorumDenominator);
- if (options.constitutionAnchorUrl) op.constitution_anchor_url = options.constitutionAnchorUrl;
- if (options.constitutionAnchorDataHash) op.constitution_anchor_data_hash = options.constitutionAnchorDataHash;
- if (options.constitutionScriptHash) op.constitution_script_hash = options.constitutionScriptHash;
- if (options.protocolVersionMajor != null) op.protocol_version_major = options.protocolVersionMajor;
- if (options.protocolVersionMinor != null) op.protocol_version_minor = options.protocolVersionMinor;
- if (options.policyHash) op.policy_hash = options.policyHash;
- this._operations.push(op);
- return this;
- }
-
- // Treasury donation (with redeemer)
- donateToTreasury(treasuryValue, donationAmount, redeemerCborHex) {
- this._operations.push({
- type: 'donate_to_treasury',
- treasury_value: String(treasuryValue),
- donation_amount: String(donationAmount),
- redeemer_cbor_hex: redeemerCborHex,
- });
- return this;
- }
-
- from(address) {
- this._from = address;
- return this;
- }
-
- changeAddress(address) {
- this._changeAddress = address;
- return this;
- }
-
- changeDatum(datumCborHex) {
- this._changeDatumCborHex = datumCborHex;
- return this;
- }
-
- changeDatumHash(hash) {
- this._changeDatumHash = hash;
- return this;
- }
-
- feePayer(address) {
- this._feePayer = address;
- return this;
- }
-
- withUtxos(utxos) {
- this._utxos = utxos;
- return this;
- }
-
- withProtocolParams(params) {
- this._protocolParams = params;
- return this;
- }
-
- validFrom(slot) {
- this._validity.valid_from = slot;
- return this;
- }
-
- validTo(slot) {
- this._validity.valid_to = slot;
- return this;
- }
-
- mergeOutputs(merge) {
- this._mergeOutputs = merge;
- return this;
- }
-
- signerCount(count) {
- this._signerCount = count;
- return this;
- }
-
- build(providerConfig = null) {
- const spec = {
- tx_type: 'script_tx',
- operations: this._operations,
- from: this._from,
- signer_count: this._signerCount,
- };
-
- if (providerConfig) {
- spec.provider = { name: providerConfig.name, url: providerConfig.url };
- if (providerConfig.apiKey) spec.provider.api_key = providerConfig.apiKey;
- if (providerConfig.enableCostEvaluation !== undefined) spec.provider.enable_cost_evaluation = providerConfig.enableCostEvaluation;
- if (this._protocolParams !== null) spec.protocol_params = this._protocolParams;
- } else {
- spec.utxos = this._utxos;
- spec.protocol_params = this._protocolParams;
- }
-
- if (this._changeAddress) spec.change_address = this._changeAddress;
- if (this._feePayer) spec.fee_payer = this._feePayer;
- if (Object.keys(this._validity).length > 0) spec.validity = this._validity;
- if (this._mergeOutputs !== null) spec.merge_outputs = this._mergeOutputs;
- if (this._changeDatumCborHex) spec.change_datum_cbor_hex = this._changeDatumCborHex;
- if (this._changeDatumHash) spec.change_datum_hash = this._changeDatumHash;
-
- const specJson = JSON.stringify(spec);
- const rc = this._b._lib.ccl_quicktx_build(this._b._thread, cstr(specJson));
- return JSON.parse(this._b._check(rc));
- }
-
- async buildWithProvider(provider) {
- if (this._utxos === null && this._from) {
- this._utxos = await provider.getUtxos(this._from);
- }
- if (this._protocolParams === null) {
- this._protocolParams = await provider.getProtocolParams();
- }
- return this.build();
- }
-}
-
-export class ScriptTx {
- constructor() {
- this._operations = [];
- this._from = null;
- this._changeAddress = null;
- this._changeDatumCborHex = null;
- this._changeDatumHash = null;
- }
-
- payToAddress(address, ...args) {
- let amounts = args;
- let options = {};
- if (args.length > 0) {
- const last = args[args.length - 1];
- if (last && typeof last === 'object' && !last.unit) {
- options = args[args.length - 1];
- amounts = args.slice(0, -1);
- }
- }
- const op = {
- type: 'pay_to_address',
- address,
- amounts: [...amounts],
- };
- if (options.scriptRefCborHex) op.script_ref_cbor_hex = options.scriptRefCborHex;
- if (options.scriptRefType) op.script_ref_type = options.scriptRefType;
- this._operations.push(op);
- return this;
- }
-
- payToContract(address, amounts, { datumCborHex, datumHash, scriptRefCborHex, scriptRefType } = {}) {
- const op = {
- type: 'pay_to_contract',
- address,
- amounts: Array.isArray(amounts) ? amounts : [amounts],
- };
- if (datumCborHex) op.datum_cbor_hex = datumCborHex;
- if (datumHash) op.datum_hash = datumHash;
- if (scriptRefCborHex) op.script_ref_cbor_hex = scriptRefCborHex;
- if (scriptRefType) op.script_ref_type = scriptRefType;
- this._operations.push(op);
- return this;
- }
-
- attachMetadata(label, metadata) {
- this._operations.push({
- type: 'attach_metadata',
- label,
- metadata,
- });
- return this;
- }
-
- collectFrom(utxos) {
- this._operations.push({
- type: 'collect_from',
- collect_utxos: utxos,
- });
- return this;
- }
-
- collectFromScript(utxos, redeemerCborHex, datumCborHex = null) {
- const op = {
- type: 'collect_from',
- collect_utxos: utxos,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (datumCborHex) op.datum_cbor_hex = datumCborHex;
- this._operations.push(op);
- return this;
- }
-
- readFrom(referenceInputs) {
- this._operations.push({
- type: 'read_from',
- reference_inputs: referenceInputs.map(ri => ({ tx_hash: ri.txHash, output_index: ri.outputIndex })),
- });
- return this;
- }
-
- mintPlutusAssets(scriptCborHex, scriptType, assets, redeemerCborHex, receiver = null, outputDatumCborHex = null) {
- const op = {
- type: 'mint_plutus_assets',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- assets,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (receiver) op.receiver = receiver;
- if (outputDatumCborHex) op.output_datum_cbor_hex = outputDatumCborHex;
- this._operations.push(op);
- return this;
- }
-
- attachSpendingValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_spending_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- attachCertificateValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_certificate_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- attachRewardValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_reward_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- attachProposingValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_proposing_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- attachVotingValidator(scriptCborHex, scriptType) {
- this._operations.push({
- type: 'attach_voting_validator',
- script_cbor_hex: scriptCborHex,
- script_type: scriptType,
- });
- return this;
- }
-
- // Staking (with redeemer)
- deregisterStakeAddress(address, redeemerCborHex, refundAddress = null) {
- const op = { type: 'deregister_stake_address', address, redeemer_cbor_hex: redeemerCborHex };
- if (refundAddress) op.refund_address = refundAddress;
- this._operations.push(op);
- return this;
- }
-
- delegateTo(address, poolId, redeemerCborHex) {
- this._operations.push({
- type: 'delegate_to', address, pool_id: poolId, redeemer_cbor_hex: redeemerCborHex,
- });
- return this;
- }
-
- withdraw(rewardAddress, amount, redeemerCborHex, receiver = null) {
- const op = {
- type: 'withdraw', reward_address: rewardAddress, amount: String(amount),
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (receiver) op.receiver = receiver;
- this._operations.push(op);
- return this;
- }
-
- // DRep (with redeemer)
- registerDRep(credentialHash, credentialType, redeemerCborHex, { anchorUrl, anchorDataHash } = {}) {
- const op = {
- type: 'register_drep', credential_hash: credentialHash, credential_type: credentialType,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- unregisterDRep(credentialHash, credentialType, redeemerCborHex, { refundAddress, refundAmount } = {}) {
- const op = {
- type: 'unregister_drep', credential_hash: credentialHash, credential_type: credentialType,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (refundAddress) op.refund_address = refundAddress;
- if (refundAmount != null) op.refund_amount = String(refundAmount);
- this._operations.push(op);
- return this;
- }
-
- updateDRep(credentialHash, credentialType, redeemerCborHex, { anchorUrl, anchorDataHash } = {}) {
- const op = {
- type: 'update_drep', credential_hash: credentialHash, credential_type: credentialType,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- // Voting (with redeemer)
- delegateVotingPowerTo(address, drepType, drepHash, redeemerCborHex) {
- this._operations.push({
- type: 'delegate_voting_power_to', address, drep_type: drepType,
- drep_hash: drepHash, redeemer_cbor_hex: redeemerCborHex,
- });
- return this;
- }
-
- createVote(voterType, voterHash, govActionTxHash, govActionIndex, vote, redeemerCborHex, { anchorUrl, anchorDataHash } = {}) {
- const op = {
- type: 'create_vote', voter_type: voterType, voter_hash: voterHash,
- gov_action_tx_hash: govActionTxHash, gov_action_index: govActionIndex, vote,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (anchorUrl) op.anchor_url = anchorUrl;
- if (anchorDataHash) op.anchor_data_hash = anchorDataHash;
- this._operations.push(op);
- return this;
- }
-
- // Governance (with redeemer)
- createProposal(govActionType, returnAddress, anchorUrl, anchorDataHash, redeemerCborHex, options = {}) {
- const op = {
- type: 'create_proposal', gov_action_type: govActionType,
- return_address: returnAddress, anchor_url: anchorUrl, anchor_data_hash: anchorDataHash,
- redeemer_cbor_hex: redeemerCborHex,
- };
- if (options.withdrawals) op.withdrawals = options.withdrawals;
- if (options.govActionTxHash) op.gov_action_tx_hash = options.govActionTxHash;
- if (options.govActionIndex != null) op.gov_action_index = options.govActionIndex;
- if (options.membersToRemove) op.members_to_remove = options.membersToRemove;
- if (options.newMembers) op.new_members = options.newMembers;
- if (options.quorumNumerator != null) op.quorum_numerator = String(options.quorumNumerator);
- if (options.quorumDenominator != null) op.quorum_denominator = String(options.quorumDenominator);
- if (options.constitutionAnchorUrl) op.constitution_anchor_url = options.constitutionAnchorUrl;
- if (options.constitutionAnchorDataHash) op.constitution_anchor_data_hash = options.constitutionAnchorDataHash;
- if (options.constitutionScriptHash) op.constitution_script_hash = options.constitutionScriptHash;
- if (options.protocolVersionMajor != null) op.protocol_version_major = options.protocolVersionMajor;
- if (options.protocolVersionMinor != null) op.protocol_version_minor = options.protocolVersionMinor;
- if (options.policyHash) op.policy_hash = options.policyHash;
- this._operations.push(op);
- return this;
- }
-
- // Treasury donation (with redeemer)
- donateToTreasury(treasuryValue, donationAmount, redeemerCborHex) {
- this._operations.push({
- type: 'donate_to_treasury',
- treasury_value: String(treasuryValue),
- donation_amount: String(donationAmount),
- redeemer_cbor_hex: redeemerCborHex,
- });
- return this;
- }
-
- from(address) {
- this._from = address;
- return this;
- }
-
- changeAddress(address) {
- this._changeAddress = address;
- return this;
- }
-
- changeDatum(datumCborHex) {
- this._changeDatumCborHex = datumCborHex;
- return this;
- }
-
- changeDatumHash(hash) {
- this._changeDatumHash = hash;
- return this;
}
- _toSpec() {
- const spec = {
- tx_type: 'script_tx',
- from: this._from,
- operations: this._operations,
- };
- if (this._changeAddress) spec.change_address = this._changeAddress;
- if (this._changeDatumCborHex) spec.change_datum_cbor_hex = this._changeDatumCborHex;
- if (this._changeDatumHash) spec.change_datum_hash = this._changeDatumHash;
- return spec;
+ /**
+ * Build an unsigned transaction from a CCL TxPlan (YAML), fully offline.
+ *
+ * @param {string} txplanYaml - the TxPlan YAML document defining the transaction(s).
+ * @param {Array} utxos - UTXOs (CCL Utxo model) available to the sender.
+ * @param {object} protocolParams - protocol parameters (CCL ProtocolParams model).
+ * @param {Array<{mem: (number|string), steps: (number|string)}>} [execUnits] - optional redeemer
+ * execution units (one per redeemer, in transaction order) for Plutus script transactions.
+ * Compute these with any evaluator (Ogmios, Blockfrost, Aiken, Scalus); the bridge does not run
+ * the script.
+ * @returns {{tx_cbor: string, tx_hash: string, fee: string}}
+ */
+ build(txplanYaml, utxos, protocolParams, execUnits = null) {
+ const rc = this._b._lib.ccl_quicktx_build(
+ this._b._thread,
+ cstr(txplanYaml),
+ cstr(JSON.stringify(utxos)),
+ cstr(JSON.stringify(protocolParams)),
+ execUnits != null ? cstr(JSON.stringify(execUnits)) : null,
+ );
+ // The build result is a YAML document.
+ return parseYaml(this._b._check(rc));
}
}
diff --git a/wrappers/js/src/provider.js b/wrappers/js/src/provider.js
deleted file mode 100644
index 4589548..0000000
--- a/wrappers/js/src/provider.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Provider abstraction for fetching UTXOs, protocol params, and submitting transactions.
- *
- * Providers allow TxBuilder to automatically fetch required data from a backend
- * (DevKit, Blockfrost, Koios, etc.) instead of requiring manual fetching.
- */
-
-export class Provider {
- async getUtxos(address) {
- throw new Error('not implemented');
- }
-
- async getProtocolParams() {
- throw new Error('not implemented');
- }
-
- async submitTx(txCborHex) {
- throw new Error('not implemented');
- }
-}
-
-export class YaciDevKitProvider extends Provider {
- constructor(baseUrl = "http://localhost:8080/api/v1", adminBaseUrl = "http://localhost:10000/local-cluster/api") {
- super();
- this.baseUrl = baseUrl;
- this.adminBaseUrl = adminBaseUrl;
- }
-
- async getUtxos(address) {
- const resp = await fetch(`${this.baseUrl}/addresses/${address}/utxos`);
- return resp.json();
- }
-
- async getProtocolParams() {
- const resp = await fetch(`${this.baseUrl}/epochs/parameters`);
- return resp.json();
- }
-
- async submitTx(txCborHex) {
- const txBytes = Buffer.from(txCborHex, "hex");
- const resp = await fetch(`${this.baseUrl}/tx/submit`, {
- method: "POST",
- headers: { "Content-Type": "application/cbor" },
- body: txBytes,
- });
- const text = await resp.text();
- return text.trim().replace(/"/g, "");
- }
-
- // Convenience methods (not part of Provider interface)
-
- async topup(address, adaAmount = 100) {
- const resp = await fetch(`${this.adminBaseUrl}/addresses/topup`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ address, adaAmount }),
- });
- return resp.json();
- }
-
- async reset() {
- const resp = await fetch(`${this.adminBaseUrl}/admin/devnet/reset`, {
- method: "POST",
- });
- return resp.status;
- }
-
- async waitForBlock(ms = 2000) {
- return new Promise((resolve) => setTimeout(resolve, ms));
- }
-
- async isAvailable() {
- try {
- const resp = await fetch(`${this.adminBaseUrl}/admin/devnet`, {
- signal: AbortSignal.timeout(3000),
- });
- return resp.status === 200;
- } catch {
- return false;
- }
- }
-}
diff --git a/wrappers/js/test/ccl.test.js b/wrappers/js/test/ccl.test.js
index 40fca71..b403566 100644
--- a/wrappers/js/test/ccl.test.js
+++ b/wrappers/js/test/ccl.test.js
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
-import { CclBridge, CclError, MAINNET, TESTNET, Amount } from '../src/index.js';
+import { CclBridge, CclError, MAINNET, TESTNET } from '../src/index.js';
// A known valid transaction CBOR hex (built from Java tests)
const SAMPLE_TX_CBOR = '84a300d901028182582073198b7ad003862b9798106b88fbccfca464b1a38afb34958275c4a7d7d8d002010181825839009493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc1a001e8480021a00029810a0f5f6';
@@ -294,413 +294,84 @@ describe('CCL Bridge', () => {
}];
}
- it('should build simple ADA payment', () => {
- const sender = bridge.account.create(TESTNET);
- const receiver = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx.newTx()
- .payToAddress(receiver.base_address, Amount.ada(5))
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
+ function paymentYaml(from, to, quantity) {
+ return `
+version: 1.0
+transaction:
+ - tx:
+ from: ${from}
+ intents:
+ - type: payment
+ address: ${to}
+ amounts:
+ - unit: lovelace
+ quantity: "${quantity}"
+`;
+ }
+ function assertBuilt(result) {
expect(result.tx_cbor.length).toBeGreaterThan(0);
expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
+ expect(parseInt(result.fee, 10)).toBeGreaterThan(0);
+ }
+
+ it('should build a simple payment from TxPlan YAML', () => {
+ const sender = bridge.account.create(TESTNET);
+ const receiver = bridge.account.create(TESTNET);
+ const yaml = paymentYaml(sender.base_address, receiver.base_address, '5000000');
+ assertBuilt(bridge.quicktx.build(yaml, makeUtxos(sender.base_address), PROTOCOL_PARAMS));
});
it('should build multiple payments', () => {
const sender = bridge.account.create(TESTNET);
const r1 = bridge.account.create(TESTNET);
const r2 = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx.newTx()
- .payToAddress(r1.base_address, Amount.ada(5))
- .payToAddress(r2.base_address, Amount.ada(3))
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build tx with metadata', () => {
+ const yaml = `
+version: 1.0
+transaction:
+ - tx:
+ from: ${sender.base_address}
+ intents:
+ - type: payment
+ address: ${r1.base_address}
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+ - type: payment
+ address: ${r2.base_address}
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
+`;
+ assertBuilt(bridge.quicktx.build(yaml, makeUtxos(sender.base_address), PROTOCOL_PARAMS));
+ });
+
+ it('should substitute variables', () => {
const sender = bridge.account.create(TESTNET);
const receiver = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx.newTx()
- .payToAddress(receiver.base_address, Amount.ada(2))
- .attachMetadata(674, { msg: ['Hello from JS'] })
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- });
-
- it('should build tx with validity interval', () => {
- const sender = bridge.account.create(TESTNET);
- const receiver = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx.newTx()
- .payToAddress(receiver.base_address, Amount.ada(2))
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .validFrom(1000)
- .validTo(50000)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
+ const yaml = `
+version: 1.0
+variables:
+ to: ${receiver.base_address}
+ amount: "4000000"
+transaction:
+ - tx:
+ from: ${sender.base_address}
+ intents:
+ - type: payment
+ address: \${to}
+ amounts:
+ - unit: lovelace
+ quantity: \${amount}
+`;
+ assertBuilt(bridge.quicktx.build(yaml, makeUtxos(sender.base_address), PROTOCOL_PARAMS));
});
it('should throw on insufficient funds', () => {
const sender = bridge.account.create(TESTNET);
const receiver = bridge.account.create(TESTNET);
-
- expect(() => {
- bridge.quicktx.newTx()
- .payToAddress(receiver.base_address, Amount.ada(200))
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address, 1_000_000))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
- }).toThrow();
- });
-
- it('should build multi-asset payment', () => {
- const sender = bridge.account.create(TESTNET);
- const receiver = bridge.account.create(TESTNET);
- const policyId = 'a'.repeat(56);
- const unit = policyId + '546f6b656e';
-
- const utxos = [{
- tx_hash: FAKE_TX_HASH,
- output_index: 0,
- address: sender.base_address,
- amount: [
- { unit: 'lovelace', quantity: '100000000' },
- { unit, quantity: '500' },
- ],
- }];
-
- const result = bridge.quicktx.newTx()
- .payToAddress(receiver.base_address, Amount.lovelace(2_000_000), Amount.asset(unit, 100))
- .from(sender.base_address)
- .withUtxos(utxos)
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('Amount helpers should produce correct values', () => {
- expect(Amount.ada(5)).toEqual({ unit: 'lovelace', quantity: '5000000' });
- expect(Amount.lovelace(2000000)).toEqual({ unit: 'lovelace', quantity: '2000000' });
- expect(Amount.asset('abc123', 100)).toEqual({ unit: 'abc123', quantity: '100' });
- });
-
- // --- Compose (multi-Tx) ---
-
- it('should compose two senders into one transaction', () => {
- const sender1 = bridge.account.create(TESTNET);
- const sender2 = bridge.account.create(TESTNET);
- const r1 = bridge.account.create(TESTNET);
- const r2 = bridge.account.create(TESTNET);
-
- const tx1 = bridge.quicktx.tx()
- .payToAddress(r1.base_address, Amount.ada(5))
- .from(sender1.base_address);
-
- const tx2 = bridge.quicktx.tx()
- .payToAddress(r2.base_address, Amount.ada(3))
- .from(sender2.base_address);
-
- const utxos = [
- {
- tx_hash: FAKE_TX_HASH,
- output_index: 0,
- address: sender1.base_address,
- amount: [{ unit: 'lovelace', quantity: '100000000' }],
- },
- {
- tx_hash: 'b'.repeat(64),
- output_index: 0,
- address: sender2.base_address,
- amount: [{ unit: 'lovelace', quantity: '100000000' }],
- },
- ];
-
- const result = bridge.quicktx.compose(tx1, tx2)
- .feePayer(sender1.base_address)
- .withUtxos(utxos)
- .withProtocolParams(PROTOCOL_PARAMS)
- .signerCount(2)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
- });
-
- // --- Staking ---
-
- it('should build register stake address tx', () => {
- const sender = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx.newTx()
- .registerStakeAddress(sender.base_address)
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build deregister stake address tx', () => {
- const sender = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx.newTx()
- .deregisterStakeAddress(sender.base_address)
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build delegate to pool tx', () => {
- const sender = bridge.account.create(TESTNET);
- const poolId = 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy';
-
- const result = bridge.quicktx.newTx()
- .delegateTo(sender.base_address, poolId)
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build withdraw rewards tx', () => {
- const sender = bridge.account.create(TESTNET);
- const info = bridge.account.fromMnemonic(sender.mnemonic, TESTNET);
-
- const result = bridge.quicktx.newTx()
- .withdraw(info.stake_address, '5000000')
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- // --- DRep ---
-
- it('should build register DRep tx', () => {
- const sender = bridge.account.create(TESTNET);
- const credentialHash = 'ab'.repeat(28);
-
- const result = bridge.quicktx.newTx()
- .registerDRep(credentialHash, 'key')
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build register DRep with anchor tx', () => {
- const sender = bridge.account.create(TESTNET);
- const credentialHash = 'ab'.repeat(28);
- const dataHash = 'cd'.repeat(32);
-
- const result = bridge.quicktx.newTx()
- .registerDRep(credentialHash, 'key', {
- anchorUrl: 'https://example.com/drep.json',
- anchorDataHash: dataHash,
- })
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build unregister DRep tx', () => {
- const sender = bridge.account.create(TESTNET);
- const credentialHash = 'ab'.repeat(28);
-
- const result = bridge.quicktx.newTx()
- .unregisterDRep(credentialHash, 'key')
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build update DRep tx', () => {
- const sender = bridge.account.create(TESTNET);
- const credentialHash = 'ab'.repeat(28);
- const dataHash = 'cd'.repeat(32);
-
- const result = bridge.quicktx.newTx()
- .updateDRep(credentialHash, 'key', {
- anchorUrl: 'https://example.com/drep-v2.json',
- anchorDataHash: dataHash,
- })
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- // --- Voting ---
-
- it('should build delegate voting power to key hash', () => {
- const sender = bridge.account.create(TESTNET);
- const drepHash = 'ab'.repeat(28);
-
- const result = bridge.quicktx.newTx()
- .delegateVotingPowerTo(sender.base_address, 'key_hash', drepHash)
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build delegate voting power to abstain', () => {
- const sender = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx.newTx()
- .delegateVotingPowerTo(sender.base_address, 'abstain')
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build create vote tx', () => {
- const sender = bridge.account.create(TESTNET);
- const voterHash = 'ab'.repeat(28);
- const govTxHash = 'cd'.repeat(32);
-
- const result = bridge.quicktx.newTx()
- .createVote('drep_key_hash', voterHash, govTxHash, 0, 'yes')
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build create vote with anchor tx', () => {
- const sender = bridge.account.create(TESTNET);
- const voterHash = 'ab'.repeat(28);
- const govTxHash = 'cd'.repeat(32);
- const anchorDataHash = 'ef'.repeat(32);
-
- const result = bridge.quicktx.newTx()
- .createVote('drep_key_hash', voterHash, govTxHash, 0, 'no', {
- anchorUrl: 'https://example.com/rationale.json',
- anchorDataHash,
- })
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- // --- Governance proposals ---
-
- it('should build info action proposal', () => {
- const sender = bridge.account.create(TESTNET);
- const info = bridge.account.fromMnemonic(sender.mnemonic, TESTNET);
- const anchorDataHash = 'ab'.repeat(32);
-
- const result = bridge.quicktx.newTx()
- .createProposal('info_action', info.stake_address,
- 'https://example.com/proposal.json', anchorDataHash)
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address, 2_000_000_000))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should build treasury withdrawals proposal', () => {
- const sender = bridge.account.create(TESTNET);
- const info = bridge.account.fromMnemonic(sender.mnemonic, TESTNET);
- const anchorDataHash = 'ab'.repeat(32);
-
- const result = bridge.quicktx.newTx()
- .createProposal('treasury_withdrawals', info.stake_address,
- 'https://example.com/proposal.json', anchorDataHash, {
- withdrawals: [{ reward_address: info.stake_address, amount: '1000000' }],
- })
- .from(sender.base_address)
- .withUtxos(makeUtxos(sender.base_address, 2_000_000_000))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- });
-
- it('should throw on compose without fee_payer', () => {
- const sender1 = bridge.account.create(TESTNET);
- const sender2 = bridge.account.create(TESTNET);
- const r1 = bridge.account.create(TESTNET);
- const r2 = bridge.account.create(TESTNET);
-
- const tx1 = bridge.quicktx.tx()
- .payToAddress(r1.base_address, Amount.ada(5))
- .from(sender1.base_address);
-
- const tx2 = bridge.quicktx.tx()
- .payToAddress(r2.base_address, Amount.ada(3))
- .from(sender2.base_address);
-
- expect(() => {
- bridge.quicktx.compose(tx1, tx2)
- .withUtxos(makeUtxos(sender1.base_address))
- .withProtocolParams(PROTOCOL_PARAMS)
- .build();
- }).toThrow();
+ const yaml = paymentYaml(sender.base_address, receiver.base_address, '200000000');
+ expect(() => bridge.quicktx.build(yaml, makeUtxos(sender.base_address, 1_000_000), PROTOCOL_PARAMS)).toThrow();
});
// --- Negative / Error Tests ---
diff --git a/wrappers/js/test/compose.integration.test.js b/wrappers/js/test/compose.integration.test.js
deleted file mode 100644
index 9c4358a..0000000
--- a/wrappers/js/test/compose.integration.test.js
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * Integration tests for QuickTx compose (multi-Tx) with Yaci DevKit.
- *
- * Requires:
- * - Yaci DevKit running on port 10000
- * - Native library built: ./gradlew :core:nativeCompile
- *
- * Run with:
- * CCL_LIB_PATH=core/build/native/nativeCompile bun test wrappers/js/test/compose.integration.test.js
- */
-import { describe, it, expect, beforeAll, afterAll, setDefaultTimeout } from "bun:test";
-import { CclBridge, TESTNET, Amount } from "../src/index.js";
-import { DevKitHelper } from "./devkit-helper.js";
-
-setDefaultTimeout(60_000);
-
-describe("QuickTx Compose Integration (DevKit)", () => {
- let bridge;
- let devkit;
- let skip = false;
-
- beforeAll(async () => {
- devkit = new DevKitHelper();
- const available = await devkit.isAvailable();
- if (!available) {
- skip = true;
- console.log("Skipping: Yaci DevKit not available on port 10000");
- return;
- }
- bridge = new CclBridge();
- await devkit.reset();
- await devkit.waitForBlock(3000);
- });
-
- afterAll(() => {
- if (bridge) bridge.close();
- });
-
- async function fundAccount(adaAmount = 150) {
- const account = bridge.account.create(TESTNET);
- await devkit.topup(account.base_address, adaAmount);
- await devkit.waitForBlock(2000);
- return account;
- }
-
- function getLovelace(utxos) {
- return utxos.reduce((sum, u) => {
- const lovelace = u.amount.find((a) => a.unit === "lovelace");
- return sum + (lovelace ? Number(lovelace.quantity) : 0);
- }, 0);
- }
-
- it("should compose two senders, sign with both, submit and verify", async () => {
- if (skip) return;
-
- const sender1 = await fundAccount();
- const sender2 = await fundAccount();
- const r1 = bridge.account.create(TESTNET);
- const r2 = bridge.account.create(TESTNET);
-
- const tx1 = bridge.quicktx.tx()
- .payToAddress(r1.base_address, Amount.ada(5))
- .from(sender1.base_address);
-
- const tx2 = bridge.quicktx.tx()
- .payToAddress(r2.base_address, Amount.ada(3))
- .from(sender2.base_address);
-
- // Gather UTXOs for both senders
- const utxos1 = await devkit.getUtxos(sender1.base_address);
- const utxos2 = await devkit.getUtxos(sender2.base_address);
- const pp = await devkit.getProtocolParams();
-
- const result = bridge.quicktx.compose(tx1, tx2)
- .feePayer(sender1.base_address)
- .withUtxos([...utxos1, ...utxos2])
- .withProtocolParams(pp)
- .signerCount(2)
- .build();
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
-
- // Sign with both senders
- let signed = bridge.account.signTx(sender1.mnemonic, TESTNET, 0, 0, result.tx_cbor);
- signed = bridge.account.signTx(sender2.mnemonic, TESTNET, 0, 0, signed);
-
- // Submit
- await devkit.submitTx(signed);
- await devkit.waitForBlock(3000);
-
- // Verify both receivers
- const r1Utxos = await devkit.getUtxos(r1.base_address);
- const r2Utxos = await devkit.getUtxos(r2.base_address);
- expect(getLovelace(r1Utxos)).toBe(5_000_000);
- expect(getLovelace(r2Utxos)).toBe(3_000_000);
- });
-
- it("should compose with metadata", async () => {
- if (skip) return;
-
- const sender1 = await fundAccount();
- const sender2 = await fundAccount();
- const r1 = bridge.account.create(TESTNET);
- const r2 = bridge.account.create(TESTNET);
-
- const tx1 = bridge.quicktx.tx()
- .payToAddress(r1.base_address, Amount.ada(5))
- .attachMetadata(674, { msg: ["Compose integration test"] })
- .from(sender1.base_address);
-
- const tx2 = bridge.quicktx.tx()
- .payToAddress(r2.base_address, Amount.ada(3))
- .from(sender2.base_address);
-
- const utxos1 = await devkit.getUtxos(sender1.base_address);
- const utxos2 = await devkit.getUtxos(sender2.base_address);
- const pp = await devkit.getProtocolParams();
-
- const result = bridge.quicktx.compose(tx1, tx2)
- .feePayer(sender1.base_address)
- .withUtxos([...utxos1, ...utxos2])
- .withProtocolParams(pp)
- .signerCount(2)
- .build();
-
- let signed = bridge.account.signTx(sender1.mnemonic, TESTNET, 0, 0, result.tx_cbor);
- signed = bridge.account.signTx(sender2.mnemonic, TESTNET, 0, 0, signed);
-
- await devkit.submitTx(signed);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
-
- const r1Utxos = await devkit.getUtxos(r1.base_address);
- const r2Utxos = await devkit.getUtxos(r2.base_address);
- expect(getLovelace(r1Utxos)).toBe(5_000_000);
- expect(getLovelace(r2Utxos)).toBe(3_000_000);
- });
-
- it("should compose with provider for auto-fetching", async () => {
- if (skip) return;
-
- const sender1 = await fundAccount();
- const sender2 = await fundAccount();
- const r1 = bridge.account.create(TESTNET);
- const r2 = bridge.account.create(TESTNET);
-
- const tx1 = bridge.quicktx.tx()
- .payToAddress(r1.base_address, Amount.ada(5))
- .from(sender1.base_address);
-
- const tx2 = bridge.quicktx.tx()
- .payToAddress(r2.base_address, Amount.ada(3))
- .from(sender2.base_address);
-
- const result = await bridge.quicktx.compose(tx1, tx2)
- .feePayer(sender1.base_address)
- .signerCount(2)
- .buildWithProvider(devkit);
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
-
- let signed = bridge.account.signTx(sender1.mnemonic, TESTNET, 0, 0, result.tx_cbor);
- signed = bridge.account.signTx(sender2.mnemonic, TESTNET, 0, 0, signed);
-
- await devkit.submitTx(signed);
- await devkit.waitForBlock(3000);
-
- const r1Utxos = await devkit.getUtxos(r1.base_address);
- const r2Utxos = await devkit.getUtxos(r2.base_address);
- expect(getLovelace(r1Utxos)).toBe(5_000_000);
- expect(getLovelace(r2Utxos)).toBe(3_000_000);
- });
-});
diff --git a/wrappers/js/test/intents.e2e.test.js b/wrappers/js/test/intents.e2e.test.js
new file mode 100644
index 0000000..4ccdf5d
--- /dev/null
+++ b/wrappers/js/test/intents.e2e.test.js
@@ -0,0 +1,101 @@
+// End-to-end coverage of every TxPlan intent through the native library.
+//
+// The fixtures in test-fixtures/quicktx-intents/ are generated by the JVM QuickTxIntentsTest via
+// TxPlan.from(tx).toYaml(); here each is built through the native library via the JS wrapper.
+// Mirrors the Go intents_test.go for cross-wrapper parity.
+
+import { describe, it, expect, beforeAll, afterAll } from "bun:test";
+import { CclBridge } from "../src/index.js";
+import { readdirSync, readFileSync } from "fs";
+import { join, dirname } from "path";
+import { fileURLToPath } from "url";
+
+const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "../../../test-fixtures/quicktx-intents");
+
+const SENDER = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp";
+const SENDER2 = "addr_test1qz7svwszky8gcmhrfza7a89z9u0dfzd3l7h23sqlc5yml7ejcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqcqrvr0";
+const SCRIPT_ADDR = "addr_test1wpunlryvl7aqsxe22erzlsseej87v5kk5vutvtrmzdy8dect48z0w";
+const SCRIPT_DATUM_HASH = "9e1199a988ba72ffd6e9c269cadb3b53b5f360ff99f112d9b2ee30c4d74ad88b";
+const SCRIPT_TX_HASH = "b".repeat(64);
+const EXEC_UNITS = [{ mem: 2000000, steps: 500000000 }];
+
+// Protocol parameters incl. the Conway deposits the governance/staking/pool intents need.
+const PROTOCOL_PARAMS = {
+ min_fee_a: 44, min_fee_b: 155381, max_tx_size: 16384, max_val_size: "5000",
+ key_deposit: "2000000", pool_deposit: "500000000", drep_deposit: "2000000",
+ gov_action_deposit: "1000000000", coins_per_utxo_size: "4310",
+ max_tx_ex_mem: "14000000", max_tx_ex_steps: "10000000000",
+ price_mem: 0.0577, price_step: 0.0000721, collateral_percent: 150,
+ max_collateral_inputs: 3, min_fee_ref_script_cost_per_byte: 15,
+};
+
+function utxos() {
+ return [
+ { tx_hash: "a".repeat(64), output_index: 0, address: SENDER,
+ amount: [{ unit: "lovelace", quantity: "2000000000" }] },
+ { tx_hash: "c".repeat(64), output_index: 0, address: SENDER,
+ amount: [{ unit: "lovelace", quantity: "5000000" }] },
+ { tx_hash: "a".repeat(64), output_index: 1, address: SENDER2,
+ amount: [{ unit: "lovelace", quantity: "2000000000" }] },
+ ];
+}
+
+// A successfully built tx: non-empty CBOR, a 64-char hash, and a positive fee. Mirrors the Python
+// reference's _assert_built so the wrappers assert the same shape.
+function assertBuilt(result) {
+ expect(result.tx_cbor.length).toBeGreaterThan(0);
+ expect(result.tx_hash.length).toBe(64);
+ expect(Number(result.fee)).toBeGreaterThan(0);
+}
+
+describe("QuickTx intents E2E", () => {
+ let bridge;
+ beforeAll(() => { bridge = new CclBridge(); });
+ afterAll(() => { if (bridge) bridge.close(); });
+
+ const fixtures = readdirSync(FIXTURES).filter((f) => f.endsWith(".yaml")).sort();
+
+ for (const f of fixtures) {
+ it(`builds ${f.replace(".yaml", "")}`, () => {
+ const yaml = readFileSync(join(FIXTURES, f), "utf8");
+ assertBuilt(bridge.quicktx.build(yaml, utxos(), PROTOCOL_PARAMS));
+ });
+ }
+
+ // --- Plutus / script paths (the plutus/ sub-directory, not covered by the top-level loop) ---
+
+ it("builds a Plutus mint with execution units", () => {
+ const yaml = readFileSync(join(FIXTURES, "plutus", "script_minting.yaml"), "utf8");
+ const u = [{ tx_hash: "a".repeat(64), output_index: 0, address: SENDER,
+ amount: [{ unit: "lovelace", quantity: "2000000000" }] }];
+ assertBuilt(bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS, EXEC_UNITS));
+ // The Plutus path requires caller-supplied exec units; without them the build is rejected.
+ expect(() => bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS)).toThrow();
+ });
+
+ it("builds a Plutus spend with execution units", () => {
+ const yaml = readFileSync(join(FIXTURES, "plutus", "script_collect_from.yaml"), "utf8");
+ const u = [
+ { tx_hash: SCRIPT_TX_HASH, output_index: 0, address: SCRIPT_ADDR,
+ amount: [{ unit: "lovelace", quantity: "10000000" }], data_hash: SCRIPT_DATUM_HASH },
+ { tx_hash: "a".repeat(64), output_index: 0, address: SENDER,
+ amount: [{ unit: "lovelace", quantity: "2000000000" }] },
+ ];
+ assertBuilt(bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS, EXEC_UNITS));
+ expect(() => bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS)).toThrow();
+ });
+
+ it("builds a Plutus lock (pay to a script address with a datum hash)", () => {
+ // Locking funds at a script address is a plain payment carrying a datum hash — no script runs,
+ // so no exec units are required (unlike the spend that later unlocks it).
+ const yaml = readFileSync(join(FIXTURES, "plutus", "plutus_lock.yaml"), "utf8");
+ assertBuilt(bridge.quicktx.build(yaml, utxos(), PROTOCOL_PARAMS));
+ });
+
+ it("derives the datum hash the lock fixture commits to", () => {
+ // plutus_lock.yaml locks under datum_hash 9e1199… — the Plutus data hash of the integer 42
+ // (CBOR 182a). Assert the bridge computes the exact value the fixture embeds, tying the
+ // plutus.dataHash primitive to the on-fixture datum.
+ expect(bridge.plutus.dataHash("182a")).toBe(SCRIPT_DATUM_HASH);
+ });
+});
diff --git a/wrappers/js/test/new-features.integration.test.js b/wrappers/js/test/new-features.integration.test.js
deleted file mode 100644
index 5af1a6c..0000000
--- a/wrappers/js/test/new-features.integration.test.js
+++ /dev/null
@@ -1,411 +0,0 @@
-/**
- * Integration tests for new QuickTx features with Yaci DevKit.
- *
- * Tests reference scripts, governance action types, pool ops, treasury donation,
- * native script attachment, and unregisterDRep refundAmount.
- *
- * Requires:
- * - Yaci DevKit running on port 10000
- * - Native library built: ./gradlew :core:nativeCompile
- *
- * Run with:
- * CCL_LIB_PATH=core/build/native/nativeCompile bun test wrappers/js/test/new-features.integration.test.js
- */
-import { describe, it, expect, beforeAll, afterAll, setDefaultTimeout } from "bun:test";
-import { CclBridge, TESTNET, Amount } from "../src/index.js";
-import { DevKitHelper } from "./devkit-helper.js";
-
-setDefaultTimeout(60_000);
-
-const ANCHOR_URL = "https://bit.ly/3zCH2HL";
-const ANCHOR_DATA_HASH = "cafef700c0039a2efb056a665b3a8bcd94f8670b88d659f7f3db68340f6f0937";
-const ALWAYS_TRUE_PLUTUS_V3 = "46450101002499";
-const DEVKIT_PROVIDER_URL = "http://localhost:10000/local-cluster/api";
-
-describe("New QuickTx Features Integration (DevKit)", () => {
- let bridge;
- let devkit;
- let skip = false;
-
- beforeAll(async () => {
- devkit = new DevKitHelper();
- const available = await devkit.isAvailable();
- if (!available) {
- skip = true;
- console.log("Skipping: Yaci DevKit not available on port 10000");
- return;
- }
- bridge = new CclBridge();
- await devkit.reset();
- await devkit.waitForBlock(3000);
- });
-
- afterAll(() => {
- if (bridge) bridge.close();
- });
-
- async function fundAccount(ada = 500) {
- const account = bridge.account.create(TESTNET);
- await devkit.topup(account.base_address, ada);
- await devkit.waitForBlock(2000);
- return account;
- }
-
- function buildSignSubmit(account, result) {
- const signedTx = bridge.account.signTx(
- account.mnemonic, TESTNET, 0, 0, result.tx_cbor
- );
- return devkit.submitTx(signedTx);
- }
-
- async function registerStake(account) {
- const result = bridge.quicktx
- .newTx()
- .registerStakeAddress(account.stake_address)
- .from(account.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
- await buildSignSubmit(account, result);
- await devkit.waitForBlock(3000);
- return result;
- }
-
- // --- Full E2E tests (payment key signing only) ---
-
- it("should send ADA with reference script attached", async () => {
- if (skip) return;
-
- const sender = await fundAccount(150);
- const receiver = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx
- .newTx()
- .payToAddress(
- receiver.base_address,
- Amount.ada(10),
- { scriptRefCborHex: ALWAYS_TRUE_PLUTUS_V3, scriptRefType: "plutus_v3" }
- )
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const receiverUtxos = await devkit.getUtxos(receiver.base_address);
- const total = receiverUtxos.reduce((sum, u) => {
- const lovelace = u.amount.find((a) => a.unit === "lovelace");
- return sum + (lovelace ? Number(lovelace.quantity) : 0);
- }, 0);
- expect(total).toBeGreaterThanOrEqual(10_000_000);
- });
-
- it("should attach native script to transaction", async () => {
- if (skip) return;
-
- const sender = await fundAccount(150);
- const receiver = bridge.account.create(TESTNET);
-
- // Get payment key hash from sender address
- const addrInfo = bridge.address.info(sender.base_address);
- const keyHash = addrInfo.payment_credential_hash;
-
- const nativeScript = { type: "sig", keyHash };
-
- const result = bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(5))
- .attachNativeScript(nativeScript)
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- it("should register stake address", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
-
- const result = bridge.quicktx
- .newTx()
- .registerStakeAddress(sender.stake_address)
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(Number(result.fee)).toBeGreaterThan(0);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- it("should delegate voting power to always_abstain", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
- await registerStake(sender);
-
- const result = bridge.quicktx
- .newTx()
- .delegateVotingPowerTo(sender.stake_address, "abstain")
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- it("should create info_action proposal", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
- await registerStake(sender);
-
- const result = bridge.quicktx
- .newTx()
- .createProposal("info_action", sender.stake_address, ANCHOR_URL, ANCHOR_DATA_HASH)
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(Number(result.fee)).toBeGreaterThan(0);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- it("should create no_confidence proposal", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
- await registerStake(sender);
-
- const result = bridge.quicktx
- .newTx()
- .createProposal("no_confidence", sender.stake_address, ANCHOR_URL, ANCHOR_DATA_HASH)
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- it("should create new_constitution proposal", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
- await registerStake(sender);
-
- const result = bridge.quicktx
- .newTx()
- .createProposal("new_constitution", sender.stake_address, ANCHOR_URL, ANCHOR_DATA_HASH, {
- constitutionAnchorUrl: ANCHOR_URL,
- constitutionAnchorDataHash: ANCHOR_DATA_HASH,
- })
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- it("should create update_committee proposal", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
- await registerStake(sender);
-
- const memberHash = "a".repeat(56);
-
- const result = bridge.quicktx
- .newTx()
- .createProposal("update_committee", sender.stake_address, ANCHOR_URL, ANCHOR_DATA_HASH, {
- newMembers: [{ hash: memberHash, type: "key", epoch: 100 }],
- quorumNumerator: 2,
- quorumDenominator: 3,
- })
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- it("should create hard_fork_initiation proposal", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
- await registerStake(sender);
-
- const result = bridge.quicktx
- .newTx()
- .createProposal("hard_fork_initiation", sender.stake_address, ANCHOR_URL, ANCHOR_DATA_HASH, {
- protocolVersionMajor: 10,
- protocolVersionMinor: 0,
- })
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
-
- await buildSignSubmit(sender, result);
- await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- // --- Build-only tests (need additional key signatures) ---
-
- it("should build registerDRep tx (build-only)", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
-
- const drepKey = bridge.gov.drepKeyFromMnemonic(sender.mnemonic, TESTNET, 0);
- const credentialHash = drepKey.verification_key_hash;
-
- const result = bridge.quicktx
- .newTx()
- .registerDRep(credentialHash, "key", {
- anchorUrl: ANCHOR_URL,
- anchorDataHash: ANCHOR_DATA_HASH,
- })
- .from(sender.base_address)
- .signerCount(2)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
- });
-
- it("should build unregisterDRep tx with refundAmount (build-only)", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
-
- const drepKey = bridge.gov.drepKeyFromMnemonic(sender.mnemonic, TESTNET, 0);
- const credentialHash = drepKey.verification_key_hash;
-
- const result = bridge.quicktx
- .newTx()
- .unregisterDRep(credentialHash, "key", {
- refundAddress: sender.base_address,
- refundAmount: 500_000_000,
- })
- .from(sender.base_address)
- .signerCount(2)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
- });
-
- it("should build registerPool tx (build-only)", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
-
- const operatorHash = "ab".repeat(14); // 28-byte hex
- const vrfKeyHash = "cd".repeat(16); // 32-byte hex
-
- const result = bridge.quicktx
- .newTx()
- .registerPool(
- operatorHash,
- vrfKeyHash,
- 500_000_000, // pledge
- 340_000_000, // cost
- 1, // margin numerator
- 100, // margin denominator
- sender.stake_address,
- [operatorHash]
- )
- .from(sender.base_address)
- .signerCount(2)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
- });
-
- it("should build donateToTreasury tx (build-only)", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
-
- const result = bridge.quicktx
- .newTx()
- .donateToTreasury(0, 5_000_000)
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
- });
-
- it("should build createVote tx (build-only)", async () => {
- if (skip) return;
-
- const sender = await fundAccount();
-
- const drepKey = bridge.gov.drepKeyFromMnemonic(sender.mnemonic, TESTNET, 0);
- const credentialHash = drepKey.verification_key_hash;
-
- const fakeGovTxHash = "ab".repeat(32);
-
- const result = bridge.quicktx
- .newTx()
- .createVote("drep_key_hash", credentialHash, fakeGovTxHash, 0, "yes", {
- anchorUrl: ANCHOR_URL,
- anchorDataHash: ANCHOR_DATA_HASH,
- })
- .from(sender.base_address)
- .signerCount(2)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
- });
-});
diff --git a/wrappers/js/test/provider.integration.test.js b/wrappers/js/test/provider.integration.test.js
deleted file mode 100644
index eb11c76..0000000
--- a/wrappers/js/test/provider.integration.test.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- * Integration tests for Provider pattern with Yaci DevKit.
- *
- * Requires:
- * - Yaci DevKit running on port 10000
- * - Native library built: ./gradlew :core:nativeCompile
- *
- * Run with:
- * CCL_LIB_PATH=core/build/native/nativeCompile bun test wrappers/js/test/provider.integration.test.js
- */
-import { describe, it, expect, beforeAll, afterAll, setDefaultTimeout } from "bun:test";
-import { CclBridge, TESTNET, Amount, YaciDevKitProvider } from "../src/index.js";
-
-setDefaultTimeout(60_000);
-
-describe("Provider Integration (DevKit)", () => {
- let bridge;
- let provider;
- let skip = false;
-
- beforeAll(async () => {
- provider = new YaciDevKitProvider();
- const available = await provider.isAvailable();
- if (!available) {
- skip = true;
- console.log("Skipping: Yaci DevKit not available on port 10000");
- return;
- }
- bridge = new CclBridge();
- await provider.reset();
- await provider.waitForBlock(3000);
- });
-
- afterAll(() => {
- if (bridge) bridge.close();
- });
-
- async function fundSender() {
- const account = bridge.account.create(TESTNET);
- await provider.topup(account.base_address, 150);
- await provider.waitForBlock(2000);
- return account;
- }
-
- it("should build with provider (auto-fetch UTXOs + PP)", async () => {
- if (skip) return;
-
- const sender = await fundSender();
- const receiver = bridge.account.create(TESTNET);
-
- // Build with provider - no manual withUtxos/withProtocolParams
- const result = await bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(5))
- .from(sender.base_address)
- .buildWithProvider(provider);
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
-
- // Sign
- const signedTx = bridge.account.signTx(
- sender.mnemonic, TESTNET, 0, 0, result.tx_cbor
- );
-
- // Submit via provider
- const txHash = await provider.submitTx(signedTx);
- expect(txHash).toBeTruthy();
-
- // Verify
- await provider.waitForBlock(3000);
- const receiverUtxos = await provider.getUtxos(receiver.base_address);
- const total = receiverUtxos.reduce((sum, u) => {
- const lovelace = u.amount.find((a) => a.unit === "lovelace");
- return sum + (lovelace ? Number(lovelace.quantity) : 0);
- }, 0);
- expect(total).toBe(5_000_000);
- });
-
- it("should allow manual UTXOs to override provider", async () => {
- if (skip) return;
-
- const sender = await fundSender();
- const receiver = bridge.account.create(TESTNET);
-
- // Manually fetch UTXOs
- const utxos = await provider.getUtxos(sender.base_address);
-
- // Build with provider but override UTXOs
- const result = await bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(3))
- .from(sender.base_address)
- .withUtxos(utxos)
- .buildWithProvider(provider);
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
-
- const signedTx = bridge.account.signTx(
- sender.mnemonic, TESTNET, 0, 0, result.tx_cbor
- );
- const txHash = await provider.submitTx(signedTx);
- expect(txHash).toBeTruthy();
- });
-
- it("should send to multiple receivers with provider", async () => {
- if (skip) return;
-
- const sender = await fundSender();
- const r1 = bridge.account.create(TESTNET);
- const r2 = bridge.account.create(TESTNET);
-
- const result = await bridge.quicktx
- .newTx()
- .payToAddress(r1.base_address, Amount.ada(3))
- .payToAddress(r2.base_address, Amount.ada(2))
- .from(sender.base_address)
- .buildWithProvider(provider);
-
- const signedTx = bridge.account.signTx(
- sender.mnemonic, TESTNET, 0, 0, result.tx_cbor
- );
- await provider.submitTx(signedTx);
- await provider.waitForBlock(3000);
-
- const r1Utxos = await provider.getUtxos(r1.base_address);
- const r2Utxos = await provider.getUtxos(r2.base_address);
-
- const r1Total = r1Utxos.reduce((s, u) => {
- const l = u.amount.find((a) => a.unit === "lovelace");
- return s + (l ? Number(l.quantity) : 0);
- }, 0);
- const r2Total = r2Utxos.reduce((s, u) => {
- const l = u.amount.find((a) => a.unit === "lovelace");
- return s + (l ? Number(l.quantity) : 0);
- }, 0);
-
- expect(r1Total).toBe(3_000_000);
- expect(r2Total).toBe(2_000_000);
- });
-
- it("should send with metadata using provider", async () => {
- if (skip) return;
-
- const sender = await fundSender();
- const receiver = bridge.account.create(TESTNET);
-
- const result = await bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(2))
- .attachMetadata(674, { msg: ["Hello from Provider"] })
- .from(sender.base_address)
- .buildWithProvider(provider);
-
- const signedTx = bridge.account.signTx(
- sender.mnemonic, TESTNET, 0, 0, result.tx_cbor
- );
- const txHash = await provider.submitTx(signedTx);
- expect(txHash).toBeTruthy();
-
- await provider.waitForBlock(3000);
- const receiverUtxos = await provider.getUtxos(receiver.base_address);
- const total = receiverUtxos.reduce((sum, u) => {
- const lovelace = u.amount.find((a) => a.unit === "lovelace");
- return sum + (lovelace ? Number(lovelace.quantity) : 0);
- }, 0);
- expect(total).toBe(2_000_000);
- });
-});
diff --git a/wrappers/js/test/quicktx.integration.test.js b/wrappers/js/test/quicktx.integration.test.js
index d810c39..3a7d5aa 100644
--- a/wrappers/js/test/quicktx.integration.test.js
+++ b/wrappers/js/test/quicktx.integration.test.js
@@ -1,19 +1,54 @@
-/**
- * Integration tests for QuickTx with Yaci DevKit.
- *
- * Requires:
- * - Yaci DevKit running on port 10000
- * - Native library built: ./gradlew :core:nativeCompile
- *
- * Run with:
- * CCL_LIB_PATH=core/build/native/nativeCompile bun test wrappers/js/test/quicktx.integration.test.js
- */
+// Integration tests for QuickTx (TxPlan YAML) with Yaci DevKit.
+//
+// Requires:
+// - Yaci DevKit running on port 10000
+// - Native library built: ./gradlew :core:nativeCompile
+//
+// Run with:
+// cd wrappers/js && CCL_LIB_PATH=../../core/build/native/nativeCompile \
+// DYLD_LIBRARY_PATH=../../core/build/native/nativeCompile bun test test/quicktx.integration.test.js
+
import { describe, it, expect, beforeAll, afterAll, setDefaultTimeout } from "bun:test";
-import { CclBridge, CclError, TESTNET, Amount } from "../src/index.js";
+import { CclBridge, TESTNET } from "../src/index.js";
import { DevKitHelper } from "./devkit-helper.js";
+import { readFileSync } from "fs";
+import { join, dirname } from "path";
+import { fileURLToPath } from "url";
setDefaultTimeout(60_000);
+const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "../../../test-fixtures/quicktx-intents");
+
+// The fixed test account the quicktx-intents fixtures are derived from (account 0/0). A Plutus
+// fixture bakes this address in as the fee payer, so submitting it means funding and signing with
+// this exact account rather than a freshly-created one.
+const INTENT_MNEMONIC = "test walk nut penalty hip pave soap entry language right filter choice";
+const INTENT_SENDER = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp";
+// The enterprise address the mint fixtures pay the freshly minted asset to.
+const MINT_RECEIVER = "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz";
+
+function paymentYaml(from, to, quantity) {
+ return `
+version: 1.0
+transaction:
+ - tx:
+ from: ${from}
+ intents:
+ - type: payment
+ address: ${to}
+ amounts:
+ - unit: lovelace
+ quantity: "${quantity}"
+`;
+}
+
+function totalLovelace(utxos) {
+ return utxos.reduce((sum, u) => {
+ const lovelace = u.amount.find((a) => a.unit === "lovelace");
+ return sum + (lovelace ? Number(lovelace.quantity) : 0);
+ }, 0);
+}
+
describe("QuickTx Integration (DevKit)", () => {
let bridge;
let devkit;
@@ -36,14 +71,14 @@ describe("QuickTx Integration (DevKit)", () => {
if (bridge) bridge.close();
});
- async function fundSender() {
+ async function fundSender(ada = 150) {
const account = bridge.account.create(TESTNET);
- await devkit.topup(account.base_address, 150);
+ await devkit.topup(account.base_address, ada);
await devkit.waitForBlock(2000);
return account;
}
- it("should build, sign, and submit simple ADA transfer", async () => {
+ it("should build, sign, and submit a simple ADA transfer", async () => {
if (skip) return;
const sender = await fundSender();
@@ -52,40 +87,19 @@ describe("QuickTx Integration (DevKit)", () => {
const utxos = await devkit.getUtxos(sender.base_address);
const pp = await devkit.getProtocolParams();
- // Build
- const result = bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(5))
- .from(sender.base_address)
- .withUtxos(utxos)
- .withProtocolParams(pp)
- .build();
-
+ const yaml = paymentYaml(sender.base_address, receiver.base_address, "5000000");
+ const result = bridge.quicktx.build(yaml, utxos, pp);
expect(result.tx_cbor.length).toBeGreaterThan(0);
expect(result.tx_hash.length).toBe(64);
expect(Number(result.fee)).toBeGreaterThan(0);
- // Sign
- const signedTx = bridge.account.signTx(
- sender.mnemonic,
- TESTNET,
- 0,
- 0,
- result.tx_cbor
- );
-
- // Submit
+ const signedTx = bridge.account.signTx(sender.mnemonic, TESTNET, 0, 0, result.tx_cbor);
const txHash = await devkit.submitTx(signedTx);
expect(txHash).toBeTruthy();
- // Verify
await devkit.waitForBlock(3000);
const receiverUtxos = await devkit.getUtxos(receiver.base_address);
- const total = receiverUtxos.reduce((sum, u) => {
- const lovelace = u.amount.find((a) => a.unit === "lovelace");
- return sum + (lovelace ? Number(lovelace.quantity) : 0);
- }, 0);
- expect(total).toBe(5_000_000);
+ expect(totalLovelace(receiverUtxos)).toBe(5_000_000);
});
it("should send to multiple receivers", async () => {
@@ -98,233 +112,86 @@ describe("QuickTx Integration (DevKit)", () => {
const utxos = await devkit.getUtxos(sender.base_address);
const pp = await devkit.getProtocolParams();
- const result = bridge.quicktx
- .newTx()
- .payToAddress(r1.base_address, Amount.ada(3))
- .payToAddress(r2.base_address, Amount.ada(2))
- .from(sender.base_address)
- .withUtxos(utxos)
- .withProtocolParams(pp)
- .build();
-
- const signedTx = bridge.account.signTx(
- sender.mnemonic,
- TESTNET,
- 0,
- 0,
- result.tx_cbor
- );
+ const yaml = `
+version: 1.0
+transaction:
+ - tx:
+ from: ${sender.base_address}
+ intents:
+ - type: payment
+ address: ${r1.base_address}
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
+ - type: payment
+ address: ${r2.base_address}
+ amounts:
+ - unit: lovelace
+ quantity: "2000000"
+`;
+ const result = bridge.quicktx.build(yaml, utxos, pp);
+ const signedTx = bridge.account.signTx(sender.mnemonic, TESTNET, 0, 0, result.tx_cbor);
await devkit.submitTx(signedTx);
- await devkit.waitForBlock(3000);
-
- const r1Utxos = await devkit.getUtxos(r1.base_address);
- const r2Utxos = await devkit.getUtxos(r2.base_address);
-
- const r1Total = r1Utxos.reduce((s, u) => {
- const l = u.amount.find((a) => a.unit === "lovelace");
- return s + (l ? Number(l.quantity) : 0);
- }, 0);
- const r2Total = r2Utxos.reduce((s, u) => {
- const l = u.amount.find((a) => a.unit === "lovelace");
- return s + (l ? Number(l.quantity) : 0);
- }, 0);
-
- expect(r1Total).toBe(3_000_000);
- expect(r2Total).toBe(2_000_000);
- });
-
- it("should send with metadata", async () => {
- if (skip) return;
- const sender = await fundSender();
- const receiver = bridge.account.create(TESTNET);
-
- const utxos = await devkit.getUtxos(sender.base_address);
- const pp = await devkit.getProtocolParams();
-
- const result = bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(2))
- .attachMetadata(674, { msg: ["Hello from JS"] })
- .from(sender.base_address)
- .withUtxos(utxos)
- .withProtocolParams(pp)
- .build();
-
- const signedTx = bridge.account.signTx(
- sender.mnemonic,
- TESTNET,
- 0,
- 0,
- result.tx_cbor
- );
- await devkit.submitTx(signedTx);
await devkit.waitForBlock(3000);
-
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
- });
-
- it("should fail on insufficient funds", async () => {
- if (skip) return;
-
- const sender = bridge.account.create(TESTNET);
- await devkit.topup(sender.base_address, 2);
- await devkit.waitForBlock(2000);
-
- const utxos = await devkit.getUtxos(sender.base_address);
- const pp = await devkit.getProtocolParams();
- const receiver = bridge.account.create(TESTNET);
-
- expect(() => {
- bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(100))
- .from(sender.base_address)
- .withUtxos(utxos)
- .withProtocolParams(pp)
- .build();
- }).toThrow();
+ const r1Utxos = await devkit.getUtxos(r1.base_address);
+ expect(totalLovelace(r1Utxos)).toBe(3_000_000);
});
- it("should complete full round-trip: build -> sign -> submit -> confirm", async () => {
+ it("should throw on insufficient funds", async () => {
if (skip) return;
- const sender = await fundSender();
+ const sender = await fundSender(2);
const receiver = bridge.account.create(TESTNET);
const utxos = await devkit.getUtxos(sender.base_address);
const pp = await devkit.getProtocolParams();
- // Build
- const result = bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(10))
- .from(sender.base_address)
- .withUtxos(utxos)
- .withProtocolParams(pp)
- .build();
-
- // Sign
- const signedTx = bridge.account.signTx(
- sender.mnemonic,
- TESTNET,
- 0,
- 0,
- result.tx_cbor
- );
-
- // Submit
- await devkit.submitTx(signedTx);
- await devkit.waitForBlock(3000);
-
- // Confirm on-chain
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
-
- // Check receiver balance
- const receiverUtxos = await devkit.getUtxos(receiver.base_address);
- const total = receiverUtxos.reduce((sum, u) => {
- const lovelace = u.amount.find((a) => a.unit === "lovelace");
- return sum + (lovelace ? Number(lovelace.quantity) : 0);
- }, 0);
- expect(total).toBe(10_000_000);
+ const yaml = paymentYaml(sender.base_address, receiver.base_address, "100000000");
+ expect(() => bridge.quicktx.build(yaml, utxos, pp)).toThrow();
});
- // --- Provider Config (server-side lazy UTXO fetching) tests ---
-
- const DEVKIT_PROVIDER_URL = "http://localhost:10000/local-cluster/api";
-
- it("should build with providerConfig (Java-side lazy UTXO fetch)", async () => {
+ // Plutus round-trip: build the script_minting fixture with caller-supplied exec units, sign with
+ // the fee payer's payment key, submit, and assert the minted asset landed on-chain. "Submit
+ // accepted" alone doesn't prove the script ran and minted — the receiver holding a non-lovelace
+ // asset does. Mirrors the Go TestIntegrationPlutusMint.
+ //
+ // We build WITHOUT the devnet's fetched cost models, so the native lib uses its built-in standard
+ // Conway cost models (which the devnet runs). Passing DevKit's fetched cost models instead is
+ // rejected with PPViewHashesDontMatch: /epochs/parameters returns them as a map keyed by
+ // zero-padded indices ("000".."165"), and JS's JSON parse reorders the non-padded integer-like
+ // keys ("100".."165") ahead of the padded ones, scrambling the cost-model order vs the ledger's
+ // canonical order and corrupting the script-integrity hash. Go's lexicographic map marshalling
+ // preserves the order, which is why its equivalent test passes with the fetched params. Threading
+ // fetched cost models through to Plutus builds needs an order-preserving fix in the wrapper —
+ // tracked as a follow-up in TODO.md §3.
+ it("should build, sign, and submit a Plutus mint", async () => {
if (skip) return;
- const sender = await fundSender();
- const receiver = bridge.account.create(TESTNET);
-
- // Build using providerConfig — Java fetches UTXOs and PP lazily via HTTP
- const result = bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(5))
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- expect(result.tx_cbor.length).toBeGreaterThan(0);
- expect(result.tx_hash.length).toBe(64);
- expect(Number(result.fee)).toBeGreaterThan(0);
-
- // Sign and submit
- const signedTx = bridge.account.signTx(
- sender.mnemonic, TESTNET, 0, 0, result.tx_cbor
- );
- const txHash = await devkit.submitTx(signedTx);
- expect(txHash).toBeTruthy();
-
+ await devkit.reset();
await devkit.waitForBlock(3000);
- const receiverUtxos = await devkit.getUtxos(receiver.base_address);
- const total = receiverUtxos.reduce((sum, u) => {
- const lovelace = u.amount.find((a) => a.unit === "lovelace");
- return sum + (lovelace ? Number(lovelace.quantity) : 0);
- }, 0);
- expect(total).toBe(5_000_000);
- });
-
- it("should build with providerConfig and multiple receivers", async () => {
- if (skip) return;
-
- const sender = await fundSender();
- const r1 = bridge.account.create(TESTNET);
- const r2 = bridge.account.create(TESTNET);
-
- const result = bridge.quicktx
- .newTx()
- .payToAddress(r1.base_address, Amount.ada(3))
- .payToAddress(r2.base_address, Amount.ada(2))
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
-
- const signedTx = bridge.account.signTx(
- sender.mnemonic, TESTNET, 0, 0, result.tx_cbor
- );
- await devkit.submitTx(signedTx);
+ await devkit.topup(INTENT_SENDER, 6000);
await devkit.waitForBlock(3000);
- const r1Utxos = await devkit.getUtxos(r1.base_address);
- const r2Utxos = await devkit.getUtxos(r2.base_address);
-
- const r1Total = r1Utxos.reduce((s, u) => {
- const l = u.amount.find((a) => a.unit === "lovelace");
- return s + (l ? Number(l.quantity) : 0);
- }, 0);
- const r2Total = r2Utxos.reduce((s, u) => {
- const l = u.amount.find((a) => a.unit === "lovelace");
- return s + (l ? Number(l.quantity) : 0);
- }, 0);
-
- expect(r1Total).toBe(3_000_000);
- expect(r2Total).toBe(2_000_000);
- });
-
- it("should build with providerConfig and metadata", async () => {
- if (skip) return;
+ const utxos = await devkit.getUtxos(INTENT_SENDER);
+ const pp = await devkit.getProtocolParams();
+ const yaml = readFileSync(join(FIXTURES, "plutus", "script_minting.yaml"), "utf8");
- const sender = await fundSender();
- const receiver = bridge.account.create(TESTNET);
+ const ppForBuild = { ...pp };
+ for (const k of ["cost_models", "costModels", "cost_mdls", "costMdls"]) delete ppForBuild[k];
- const result = bridge.quicktx
- .newTx()
- .payToAddress(receiver.base_address, Amount.ada(2))
- .attachMetadata(674, { msg: ["Hello from providerConfig"] })
- .from(sender.base_address)
- .build({ name: "yaci", url: DEVKIT_PROVIDER_URL });
+ const result = bridge.quicktx.build(yaml, utxos, ppForBuild, [{ mem: 2000000, steps: 500000000 }]);
+ expect(result.tx_hash.length).toBe(64);
- const signedTx = bridge.account.signTx(
- sender.mnemonic, TESTNET, 0, 0, result.tx_cbor
- );
- const txHash = await devkit.submitTx(signedTx);
- expect(txHash).toBeTruthy();
+ const signedTx = bridge.account.signTxWithKeys(INTENT_MNEMONIC, TESTNET, 0, 0, result.tx_cbor, ["payment"]);
+ // A successful submit returns the 64-char tx hash; a rejection returns an error body. Assert the
+ // hash so a failed Plutus validation surfaces here, not as a missing asset further down.
+ const submitResult = await devkit.submitTx(signedTx);
+ expect(submitResult).toMatch(/^[0-9a-f]{64}$/);
await devkit.waitForBlock(3000);
- const txInfo = await devkit.getTx(result.tx_hash);
- expect(txInfo).toBeTruthy();
+ const receiverUtxos = await devkit.getUtxos(MINT_RECEIVER);
+ const hasMintedAsset = receiverUtxos.some((u) => u.amount.some((a) => a.unit !== "lovelace"));
+ expect(hasMintedAsset).toBe(true);
});
});
diff --git a/wrappers/python/README.md b/wrappers/python/README.md
new file mode 100644
index 0000000..d5ff01f
--- /dev/null
+++ b/wrappers/python/README.md
@@ -0,0 +1,101 @@
+# CCL Bridge — Python
+
+Python bindings for [Cardano Client Lib](https://github.com/bloxbean/cardano-client-lib)
+via the CCL Bridge native library. Pure `ctypes` — no JVM, no compiler, no C extension.
+
+> Part of the [CCL Bridge](../../README.md) project. See the
+> [top-level README](../../README.md) for the full API reference and
+> [`docs/quicktx.md`](../../docs/quicktx.md) for transaction building.
+
+## Requirements
+
+- Python 3.8+
+- The native library `libccl.{dylib,so,dll}` for your platform.
+
+## Getting the native library
+
+The bindings load a shared library at runtime; they do not bundle it (yet — see the
+project [`TODO.md`](../../TODO.md)). Two ways to get it:
+
+**Build from source** (needs Oracle GraalVM 25.0.3 — see the top-level README):
+
+```bash
+# from the repo root
+./gradlew :core:nativeCompile
+# produces core/build/native/nativeCompile/libccl.{dylib,so}
+```
+
+**Or download a pre-built binary:**
+
+```bash
+make download-lib # fetches into core/build/native/nativeCompile/
+```
+
+## Running the examples
+
+The package finds the library via the `CCL_LIB_PATH` environment variable, and the OS
+loader needs it on its search path too. From the repo root:
+
+```bash
+LIB_DIR=core/build/native/nativeCompile
+
+PYTHONPATH=wrappers/python \
+CCL_LIB_PATH=$LIB_DIR \
+DYLD_LIBRARY_PATH=$LIB_DIR \
+LD_LIBRARY_PATH=$LIB_DIR \
+ python3 wrappers/python/examples/01_account_and_keys.py
+```
+
+(`DYLD_LIBRARY_PATH` is for macOS, `LD_LIBRARY_PATH` for Linux — set both, the unused one
+is harmless.)
+
+The [`examples/`](examples/) directory contains:
+
+| File | What it shows |
+|------|---------------|
+| [`01_account_and_keys.py`](examples/01_account_and_keys.py) | Create an account, restore from mnemonic, derive keys and a DRep ID |
+| [`02_primitives.py`](examples/02_primitives.py) | Mnemonics, Blake2b hashing, Ed25519 signing, address parsing/validation |
+| [`03_build_and_sign_tx.py`](examples/03_build_and_sign_tx.py) | Build an unsigned payment **offline** (QuickTx) and sign it — no node/DevKit needed |
+
+## Quick start
+
+```python
+from ccl._ffi import CclLib
+
+lib = CclLib() # loads libccl, starts a GraalVM isolate
+try:
+ account = lib.account.create(CclLib.TESTNET)
+ print(account["base_address"]) # addr_test1...
+ print(account["mnemonic"]) # 24-word phrase
+finally:
+ lib.close() # tears down the isolate
+```
+
+## API namespaces
+
+A `CclLib` instance exposes these namespaces (all offline operations):
+
+| Namespace | Examples |
+|-----------|----------|
+| `lib.account` | `create`, `from_mnemonic`, `get_private_key`, `get_public_key`, `get_drep_id`, `sign_tx` |
+| `lib.address` | `info`, `validate`, `to_bytes`, `from_bytes` |
+| `lib.crypto` | `blake2b_256`, `blake2b_224`, `generate_mnemonic`, `validate_mnemonic`, `sign`, `verify` |
+| `lib.tx` | `hash`, `sign_with_secret_key`, `to_json`, `from_json`, `deserialize` |
+| `lib.plutus` | `data_hash`, `data_to_json`, `data_from_json` |
+| `lib.script` | `native_from_json`, `hash` |
+| `lib.gov` | `drep_key_from_mnemonic`, `committee_cold_key_from_mnemonic`, `committee_hot_key_from_mnemonic` |
+| `lib.wallet` | `create`, `from_mnemonic`, `get_address` |
+| `lib.quicktx` | `build(yaml, utxos, protocol_params)` — build an unsigned tx from a TxPlan YAML document |
+
+Network IDs: `CclLib.MAINNET` (0), `CclLib.TESTNET` (1), `CclLib.PREPROD` (2), `CclLib.PREVIEW` (3).
+
+Errors raise `ccl.CclError`.
+
+Transactions are defined as a [TxPlan](https://github.com/bloxbean/cardano-client-lib)
+**YAML** document and built fully offline — you supply the UTXOs and protocol parameters:
+
+```python
+result = lib.quicktx.build(txplan_yaml, utxos, protocol_params) # -> {"tx_cbor","tx_hash","fee"}
+```
+
+See [`examples/03_build_and_sign_tx.py`](examples/03_build_and_sign_tx.py).
diff --git a/wrappers/python/build.gradle b/wrappers/python/build.gradle
index 8ce5d7e..8fe2dc9 100644
--- a/wrappers/python/build.gradle
+++ b/wrappers/python/build.gradle
@@ -21,3 +21,18 @@ task test(type: Exec) {
commandLine 'bash', '-c',
'python3 -m pytest tests/ -v --ignore=tests/test_quicktx_integration.py --ignore=tests/test_new_features_integration.py'
}
+
+// Full suite including the DevKit integration tests. Requires a running Yaci DevKit
+// on :10000 (the integration tests skip themselves if it is not available).
+task integrationTest(type: Exec) {
+ dependsOn copyNativeLib
+ workingDir projectDir
+
+ def nativeDir = layout.buildDirectory.dir('native').get().asFile.absolutePath
+ environment 'CCL_LIB_PATH', nativeDir
+ environment 'DYLD_LIBRARY_PATH', nativeDir
+ environment 'LD_LIBRARY_PATH', nativeDir
+ environment 'PYTHONPATH', projectDir.absolutePath
+
+ commandLine 'bash', '-c', 'python3 -m pytest tests/ -v'
+}
diff --git a/wrappers/python/ccl/__init__.py b/wrappers/python/ccl/__init__.py
index 3bdd9ec..e3df83a 100644
--- a/wrappers/python/ccl/__init__.py
+++ b/wrappers/python/ccl/__init__.py
@@ -7,10 +7,7 @@
from ccl.script import Script
from ccl.governance import Governance
from ccl.wallet import Wallet
-from ccl.quicktx import QuickTx, TxBuilder, Amount, Tx, ComposeTxBuilder, ScriptTxBuilder, ScriptTx, ProviderConfig
-from ccl.provider import Provider, YaciDevKitProvider
+from ccl.quicktx import QuickTx
__all__ = ['CclLib', 'CclError', 'Account', 'Address', 'Crypto', 'Transaction',
- 'Plutus', 'Script', 'Governance', 'Wallet', 'QuickTx', 'TxBuilder', 'Amount',
- 'Tx', 'ComposeTxBuilder', 'ScriptTxBuilder', 'ScriptTx', 'ProviderConfig',
- 'Provider', 'YaciDevKitProvider']
+ 'Plutus', 'Script', 'Governance', 'Wallet', 'QuickTx']
diff --git a/wrappers/python/ccl/_ffi.py b/wrappers/python/ccl/_ffi.py
index cb01fa6..2b81e9b 100644
--- a/wrappers/python/ccl/_ffi.py
+++ b/wrappers/python/ccl/_ffi.py
@@ -103,6 +103,8 @@ def _setup_functions(self):
lib.ccl_account_sign_tx.argtypes = [c_void_p, c_char_p, c_int, c_int, c_int, c_char_p]
lib.ccl_account_sign_tx.restype = c_int
+ lib.ccl_account_sign_tx_multi.argtypes = [c_void_p, c_char_p, c_int, c_int, c_int, c_char_p, c_char_p]
+ lib.ccl_account_sign_tx_multi.restype = c_int
lib.ccl_account_get_drep_id.argtypes = [c_void_p, c_char_p, c_int, c_int]
lib.ccl_account_get_drep_id.restype = c_int
@@ -193,7 +195,7 @@ def _setup_functions(self):
lib.ccl_script_hash.restype = c_int
# QuickTx API
- lib.ccl_quicktx_build.argtypes = [c_void_p, c_char_p]
+ lib.ccl_quicktx_build.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p, c_char_p]
lib.ccl_quicktx_build.restype = c_int
def _get_result(self):
diff --git a/wrappers/python/ccl/account.py b/wrappers/python/ccl/account.py
index 7f2c0ba..524de13 100644
--- a/wrappers/python/ccl/account.py
+++ b/wrappers/python/ccl/account.py
@@ -37,6 +37,20 @@ def sign_tx(self, mnemonic, tx_cbor_hex, network_id=0, account_index=0, address_
self._b._encode(tx_cbor_hex))
return self._b._check(rc)
+ def sign_tx_with_keys(self, mnemonic, tx_cbor_hex, keys, network_id=0, account_index=0, address_index=0):
+ """Sign a transaction with one or more keys.
+
+ ``keys`` is a list (or comma-separated string) of roles applied in order: ``payment``,
+ ``stake``, ``drep``, ``committee_cold``, ``committee_hot``. Use this for transactions whose
+ certificates also need the stake or DRep key (stake registration/delegation/withdrawal,
+ DRep and vote operations), which the payment key alone cannot witness.
+ """
+ keys_str = keys if isinstance(keys, str) else ",".join(keys)
+ rc = self._b._lib.ccl_account_sign_tx_multi(
+ self._b._thread, self._b._encode(mnemonic), network_id, account_index, address_index,
+ self._b._encode(tx_cbor_hex), self._b._encode(keys_str))
+ return self._b._check(rc)
+
def get_drep_id(self, mnemonic, network_id=0, account_index=0):
"""Get DRep ID (bech32) from mnemonic."""
rc = self._b._lib.ccl_account_get_drep_id(
diff --git a/wrappers/python/ccl/provider.py b/wrappers/python/ccl/provider.py
deleted file mode 100644
index 0acf0ed..0000000
--- a/wrappers/python/ccl/provider.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""Provider abstraction for fetching UTXOs, protocol params, and submitting transactions.
-
-Providers allow TxBuilder to automatically fetch required data from a backend
-(DevKit, Blockfrost, Koios, etc.) instead of requiring manual fetching.
-"""
-import json
-import time
-import urllib.request
-import urllib.error
-
-
-class Provider:
- """Base provider interface for UTXOs, protocol params, and tx submission."""
-
- def get_utxos(self, address):
- """Fetch all UTXOs for an address.
-
- Args:
- address: Bech32 address string
-
- Returns:
- List of UTXO dicts in Blockfrost/Koios/DevKit format.
- """
- raise NotImplementedError
-
- def get_protocol_params(self):
- """Fetch current protocol parameters.
-
- Returns:
- Protocol params dict in Blockfrost/Koios/DevKit format.
- """
- raise NotImplementedError
-
- def submit_tx(self, tx_cbor_hex):
- """Submit a signed transaction.
-
- Args:
- tx_cbor_hex: Signed transaction CBOR hex string
-
- Returns:
- Transaction hash string.
- """
- raise NotImplementedError
-
-
-class YaciDevKitProvider(Provider):
- """Provider backed by Yaci DevKit local cluster."""
-
- def __init__(self, store_url="http://localhost:8080/api/v1", admin_url="http://localhost:10000/local-cluster/api"):
- self.store_url = store_url
- self.admin_url = admin_url
-
- def get_utxos(self, address):
- """Fetch UTXOs for an address from DevKit."""
- url = f"{self.store_url}/addresses/{address}/utxos"
- with urllib.request.urlopen(url) as resp:
- return json.loads(resp.read())
-
- def get_protocol_params(self):
- """Fetch current protocol parameters from DevKit."""
- url = f"{self.store_url}/epochs/parameters"
- with urllib.request.urlopen(url) as resp:
- return json.loads(resp.read())
-
- def submit_tx(self, tx_cbor_hex):
- """Submit a signed transaction to DevKit."""
- tx_bytes = bytes.fromhex(tx_cbor_hex)
- req = urllib.request.Request(
- f"{self.store_url}/tx/submit",
- method="POST",
- data=tx_bytes,
- headers={"Content-Type": "application/cbor"},
- )
- with urllib.request.urlopen(req) as resp:
- return resp.read().decode("utf-8").strip().strip('"')
-
- # Convenience methods (not part of Provider interface)
-
- def topup(self, address, ada_amount=100):
- """Fund an address with ADA (DevKit only)."""
- data = json.dumps({"address": address, "adaAmount": ada_amount}).encode()
- req = urllib.request.Request(
- f"{self.admin_url}/addresses/topup",
- method="POST",
- data=data,
- headers={"Content-Type": "application/json"},
- )
- with urllib.request.urlopen(req) as resp:
- return json.loads(resp.read())
-
- def reset(self):
- """Reset the devnet to initial state."""
- req = urllib.request.Request(
- f"{self.admin_url}/admin/devnet/reset",
- method="POST",
- data=b"",
- )
- with urllib.request.urlopen(req) as resp:
- return resp.status
-
- def wait_for_block(self, seconds=2):
- """Wait for a new block to be produced."""
- time.sleep(seconds)
-
- def is_available(self):
- """Check if DevKit is running."""
- try:
- req = urllib.request.Request(
- f"{self.admin_url}/admin/devnet",
- method="GET",
- )
- with urllib.request.urlopen(req, timeout=3) as resp:
- return resp.status == 200
- except (urllib.error.URLError, OSError):
- return False
diff --git a/wrappers/python/ccl/quicktx.py b/wrappers/python/ccl/quicktx.py
index 19779f7..f3ec12d 100644
--- a/wrappers/python/ccl/quicktx.py
+++ b/wrappers/python/ccl/quicktx.py
@@ -1,1699 +1,41 @@
import json
+import yaml
-class Amount:
- """Helper to build amount objects for transaction operations."""
- @staticmethod
- def lovelace(quantity):
- """Create a lovelace amount."""
- return {"unit": "lovelace", "quantity": str(int(quantity))}
-
- @staticmethod
- def ada(ada_amount):
- """Create a lovelace amount from ADA (1 ADA = 1,000,000 lovelace)."""
- return {"unit": "lovelace", "quantity": str(int(ada_amount * 1_000_000))}
-
- @staticmethod
- def asset(unit, quantity):
- """Create a native asset amount. Unit = policyId + hex asset name."""
- return {"unit": unit, "quantity": str(int(quantity))}
-
-
-class ProviderConfig:
- """Configuration for Java-side provider (lazy UTXO fetching via HTTP)."""
-
- def __init__(self, name, url, api_key=None, enable_cost_evaluation=None):
- self.name = name
- self.url = url
- self.api_key = api_key
- self.enable_cost_evaluation = enable_cost_evaluation
-
- def to_dict(self):
- d = {"name": self.name, "url": self.url}
- if self.api_key:
- d["api_key"] = self.api_key
- if self.enable_cost_evaluation is not None:
- d["enable_cost_evaluation"] = self.enable_cost_evaluation
- return d
-
-
-def _build_provider_dict(provider_config):
- """Convert a provider_config (dict or ProviderConfig) to a spec dict."""
- if isinstance(provider_config, ProviderConfig):
- return provider_config.to_dict()
- # Assume it's already a dict; copy and add enable_cost_evaluation if present
- d = {"name": provider_config["name"], "url": provider_config["url"]}
- if provider_config.get("api_key"):
- d["api_key"] = provider_config["api_key"]
- if provider_config.get("enable_cost_evaluation") is not None:
- d["enable_cost_evaluation"] = provider_config["enable_cost_evaluation"]
- return d
-
-
-class TxBuilder:
- """Builder for QuickTx transaction specifications.
+class QuickTx:
+ """Builds unsigned transactions from a CCL TxPlan (YAML), fully offline.
- Builds a JSON spec that is sent to ccl_quicktx_build for automatic
- coin selection, fee calculation, and change balancing.
+ The transaction is defined by a TxPlan YAML document; the caller supplies the chain data
+ (UTXOs and protocol parameters). Nothing is fetched and nothing is submitted — the result
+ is the unsigned transaction CBOR plus its hash and fee.
"""
def __init__(self, bridge):
self._bridge = bridge
- self._operations = []
- self._from = None
- self._change_address = None
- self._fee_payer = None
- self._utxos = None
- self._protocol_params = None
- self._validity = {}
- self._merge_outputs = None
- self._signer_count = 1
-
- def pay_to_address(self, address, *amounts, script_ref_cbor_hex=None, script_ref_type=None):
- """Add a payment to an address.
-
- Args:
- address: Bech32 destination address
- *amounts: One or more Amount dicts (from Amount.ada(), Amount.lovelace(), etc.)
- script_ref_cbor_hex: Optional reference script CBOR hex to attach to output
- script_ref_type: Script type for ref script ('plutus_v1', 'plutus_v2', 'plutus_v3')
- """
- amount_list = list(amounts)
- op = {
- "type": "pay_to_address",
- "address": address,
- "amounts": amount_list,
- }
- if script_ref_cbor_hex:
- op["script_ref_cbor_hex"] = script_ref_cbor_hex
- if script_ref_type:
- op["script_ref_type"] = script_ref_type
- self._operations.append(op)
- return self
-
- def pay_to_contract(self, address, amounts, datum_cbor_hex=None, datum_hash=None,
- script_ref_cbor_hex=None, script_ref_type=None):
- """Add a payment to a contract address with datum.
-
- Args:
- address: Contract address
- amounts: List of Amount dicts
- datum_cbor_hex: Inline datum as CBOR hex
- datum_hash: Datum hash hex
- script_ref_cbor_hex: Optional reference script CBOR hex
- script_ref_type: Script type for ref script
- """
- op = {
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts if isinstance(amounts, list) else [amounts],
- }
- if datum_cbor_hex:
- op["datum_cbor_hex"] = datum_cbor_hex
- if datum_hash:
- op["datum_hash"] = datum_hash
- if script_ref_cbor_hex:
- op["script_ref_cbor_hex"] = script_ref_cbor_hex
- if script_ref_type:
- op["script_ref_type"] = script_ref_type
- self._operations.append(op)
- return self
-
- def mint_assets(self, script_json, assets, receiver):
- """Mint native assets.
-
- Args:
- script_json: Native script JSON string
- assets: List of {"name": "...", "quantity": "..."} dicts
- receiver: Address to receive minted assets
- """
- self._operations.append({
- "type": "mint_assets",
- "script_json": script_json if isinstance(script_json, str) else json.dumps(script_json),
- "assets": assets,
- "receiver": receiver,
- })
- return self
-
- def attach_metadata(self, label, metadata):
- """Attach metadata to the transaction.
-
- Args:
- label: Integer metadata label (e.g. 674 for CIP-20)
- metadata: Metadata value (string, number, list, or dict)
- """
- self._operations.append({
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- })
- return self
-
- def collect_from(self, utxos):
- """Specify explicit UTXOs as transaction inputs (bypasses coin selection).
-
- Args:
- utxos: List of UTXO dicts with tx_hash, output_index, address, amount
- """
- self._operations.append({
- "type": "collect_from",
- "collect_utxos": utxos,
- })
- return self
-
- # Staking
-
- def register_stake_address(self, address):
- """Register a stake address."""
- self._operations.append({"type": "register_stake_address", "address": address})
- return self
-
- def deregister_stake_address(self, address, refund_address=None):
- """Deregister a stake address."""
- op = {"type": "deregister_stake_address", "address": address}
- if refund_address:
- op["refund_address"] = refund_address
- self._operations.append(op)
- return self
-
- def delegate_to(self, address, pool_id):
- """Delegate stake to a pool."""
- self._operations.append({"type": "delegate_to", "address": address, "pool_id": pool_id})
- return self
-
- def withdraw(self, reward_address, amount, receiver=None):
- """Withdraw staking rewards."""
- op = {"type": "withdraw", "reward_address": reward_address, "amount": str(amount)}
- if receiver:
- op["receiver"] = receiver
- self._operations.append(op)
- return self
-
- # DRep
-
- def register_drep(self, credential_hash, credential_type='key', anchor_url=None, anchor_data_hash=None):
- """Register a DRep."""
- op = {"type": "register_drep", "credential_hash": credential_hash, "credential_type": credential_type}
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- def unregister_drep(self, credential_hash, credential_type='key', refund_address=None, refund_amount=None):
- """Unregister a DRep."""
- op = {"type": "unregister_drep", "credential_hash": credential_hash, "credential_type": credential_type}
- if refund_address:
- op["refund_address"] = refund_address
- if refund_amount is not None:
- op["refund_amount"] = str(refund_amount)
- self._operations.append(op)
- return self
-
- def update_drep(self, credential_hash, credential_type='key', anchor_url=None, anchor_data_hash=None):
- """Update DRep metadata."""
- op = {"type": "update_drep", "credential_hash": credential_hash, "credential_type": credential_type}
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- # Voting
-
- def delegate_voting_power_to(self, address, drep_type, drep_hash=None):
- """Delegate voting power to a DRep."""
- op = {"type": "delegate_voting_power_to", "address": address, "drep_type": drep_type}
- if drep_hash:
- op["drep_hash"] = drep_hash
- self._operations.append(op)
- return self
-
- def create_vote(self, voter_type, voter_hash, gov_action_tx_hash, gov_action_index, vote,
- anchor_url=None, anchor_data_hash=None):
- """Cast a governance vote."""
- op = {
- "type": "create_vote", "voter_type": voter_type, "voter_hash": voter_hash,
- "gov_action_tx_hash": gov_action_tx_hash, "gov_action_index": gov_action_index, "vote": vote,
- }
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- # Governance
-
- def create_proposal(self, gov_action_type, return_address, anchor_url, anchor_data_hash, **kwargs):
- """Create a governance proposal.
-
- Supported gov_action_types: info_action, treasury_withdrawals, no_confidence,
- update_committee, new_constitution, hard_fork_initiation, parameter_change.
- """
- op = {
- "type": "create_proposal", "gov_action_type": gov_action_type,
- "return_address": return_address, "anchor_url": anchor_url, "anchor_data_hash": anchor_data_hash,
- }
- if "withdrawals" in kwargs:
- op["withdrawals"] = kwargs["withdrawals"]
- # Previous governance action reference
- if "gov_action_tx_hash" in kwargs:
- op["gov_action_tx_hash"] = kwargs["gov_action_tx_hash"]
- if "gov_action_index" in kwargs:
- op["gov_action_index"] = kwargs["gov_action_index"]
- # update_committee fields
- if "members_to_remove" in kwargs:
- op["members_to_remove"] = kwargs["members_to_remove"]
- if "new_members" in kwargs:
- op["new_members"] = kwargs["new_members"]
- if "quorum_numerator" in kwargs:
- op["quorum_numerator"] = str(kwargs["quorum_numerator"])
- if "quorum_denominator" in kwargs:
- op["quorum_denominator"] = str(kwargs["quorum_denominator"])
- # new_constitution fields
- if "constitution_anchor_url" in kwargs:
- op["constitution_anchor_url"] = kwargs["constitution_anchor_url"]
- if "constitution_anchor_data_hash" in kwargs:
- op["constitution_anchor_data_hash"] = kwargs["constitution_anchor_data_hash"]
- if "constitution_script_hash" in kwargs:
- op["constitution_script_hash"] = kwargs["constitution_script_hash"]
- # hard_fork_initiation fields
- if "protocol_version_major" in kwargs:
- op["protocol_version_major"] = kwargs["protocol_version_major"]
- if "protocol_version_minor" in kwargs:
- op["protocol_version_minor"] = kwargs["protocol_version_minor"]
- # parameter_change fields
- if "policy_hash" in kwargs:
- op["policy_hash"] = kwargs["policy_hash"]
- self._operations.append(op)
- return self
-
- # Pool operations
-
- def register_pool(self, operator, vrf_key_hash, pledge, cost, margin_numerator, margin_denominator,
- reward_address, pool_owners, relays=None, pool_metadata_url=None, pool_metadata_hash=None):
- """Register a staking pool."""
- op = {
- "type": "register_pool", "operator": operator, "vrf_key_hash": vrf_key_hash,
- "pledge": str(pledge), "cost": str(cost),
- "margin_numerator": str(margin_numerator), "margin_denominator": str(margin_denominator),
- "reward_address": reward_address, "pool_owners": pool_owners,
- }
- if relays:
- op["relays"] = relays
- if pool_metadata_url:
- op["pool_metadata_url"] = pool_metadata_url
- if pool_metadata_hash:
- op["pool_metadata_hash"] = pool_metadata_hash
- self._operations.append(op)
- return self
-
- def update_pool(self, operator, vrf_key_hash, pledge, cost, margin_numerator, margin_denominator,
- reward_address, pool_owners, relays=None, pool_metadata_url=None, pool_metadata_hash=None):
- """Update a staking pool."""
- op = {
- "type": "update_pool", "operator": operator, "vrf_key_hash": vrf_key_hash,
- "pledge": str(pledge), "cost": str(cost),
- "margin_numerator": str(margin_numerator), "margin_denominator": str(margin_denominator),
- "reward_address": reward_address, "pool_owners": pool_owners,
- }
- if relays:
- op["relays"] = relays
- if pool_metadata_url:
- op["pool_metadata_url"] = pool_metadata_url
- if pool_metadata_hash:
- op["pool_metadata_hash"] = pool_metadata_hash
- self._operations.append(op)
- return self
-
- def retire_pool(self, pool_id, epoch):
- """Retire a staking pool."""
- self._operations.append({"type": "retire_pool", "pool_id": pool_id, "epoch": epoch})
- return self
-
- # Treasury donation
-
- def donate_to_treasury(self, treasury_value, donation_amount):
- """Donate ADA to the treasury."""
- self._operations.append({
- "type": "donate_to_treasury",
- "treasury_value": str(treasury_value),
- "donation_amount": str(donation_amount),
- })
- return self
-
- # Native script attachment
-
- def attach_native_script(self, script_json):
- """Attach a native script to the transaction witness set."""
- self._operations.append({
- "type": "attach_native_script",
- "script_json": script_json if isinstance(script_json, str) else json.dumps(script_json),
- })
- return self
-
- def from_address(self, address):
- """Set the sender address."""
- self._from = address
- return self
-
- def change_address(self, address):
- """Set the change address (defaults to sender)."""
- self._change_address = address
- return self
-
- def fee_payer(self, address):
- """Set the fee payer address."""
- self._fee_payer = address
- return self
-
- def with_utxos(self, utxos):
- """Provide UTXOs for coin selection.
-
- Args:
- utxos: List of UTXO dicts (Blockfrost/Koios/DevKit format)
- """
- self._utxos = utxos
- return self
-
- def with_protocol_params(self, params):
- """Provide protocol parameters.
-
- Args:
- params: Protocol params dict (Blockfrost/Koios/DevKit format)
- """
- self._protocol_params = params
- return self
-
- def valid_from(self, slot):
- """Set transaction validity start slot."""
- self._validity["valid_from"] = slot
- return self
-
- def valid_to(self, slot):
- """Set transaction validity end slot (TTL)."""
- self._validity["valid_to"] = slot
- return self
- def merge_outputs(self, merge):
- """Whether to merge outputs to the same address (default: True)."""
- self._merge_outputs = merge
- return self
-
- def signer_count(self, count):
- """Set the number of signers for fee estimation (default: 1)."""
- self._signer_count = count
- return self
-
- def build(self, provider=None, provider_config=None):
- """Build the transaction. Returns dict with tx_cbor, tx_hash, fee.
+ def build(self, txplan_yaml, utxos, protocol_params, exec_units=None):
+ """Build an unsigned transaction from a TxPlan YAML document.
Args:
- provider: Optional Provider instance for auto-fetching UTXOs
- and protocol params (wrapper-side).
- provider_config: Optional ProviderConfig or dict with 'name', 'url',
- and optionally 'api_key' and 'enable_cost_evaluation' for
- Java-side lazy UTXO fetching via HTTP.
+ txplan_yaml: the TxPlan YAML string defining the transaction(s).
+ utxos: list of UTXO dicts (CCL ``Utxo`` model) available to the sender.
+ protocol_params: protocol parameters dict (CCL ``ProtocolParams`` model).
+ exec_units: optional list of redeemer execution units (``[{"mem","steps"}]``), one per
+ redeemer in transaction order, for Plutus script transactions. Compute these with any
+ evaluator (Ogmios, Blockfrost, Aiken, Scalus); the bridge does not run the script.
- Raises:
- ValueError: If both provider and provider_config are specified.
+ Returns:
+ dict with ``tx_cbor``, ``tx_hash`` and ``fee`` (parsed from the YAML result).
"""
- if provider and provider_config:
- raise ValueError("Cannot specify both 'provider' and 'provider_config'")
-
- utxos = self._utxos
- protocol_params = self._protocol_params
-
- if provider_config:
- spec = {
- "operations": self._operations,
- "from": self._from,
- "provider": _build_provider_dict(provider_config),
- "signer_count": self._signer_count,
- }
- if protocol_params is not None:
- spec["protocol_params"] = protocol_params
- elif provider:
- if utxos is None and self._from:
- utxos = provider.get_utxos(self._from)
- if protocol_params is None:
- protocol_params = provider.get_protocol_params()
- spec = {
- "operations": self._operations,
- "from": self._from,
- "utxos": utxos,
- "protocol_params": protocol_params,
- "signer_count": self._signer_count,
- }
- else:
- spec = {
- "operations": self._operations,
- "from": self._from,
- "utxos": utxos,
- "protocol_params": protocol_params,
- "signer_count": self._signer_count,
- }
-
- if self._change_address:
- spec["change_address"] = self._change_address
- if self._fee_payer:
- spec["fee_payer"] = self._fee_payer
- if self._validity:
- spec["validity"] = self._validity
- if self._merge_outputs is not None:
- spec["merge_outputs"] = self._merge_outputs
-
- spec_json = json.dumps(spec)
+ utxos_json = json.dumps(utxos)
+ pp_json = json.dumps(protocol_params)
+ exec_units_json = json.dumps(exec_units) if exec_units is not None else None
rc = self._bridge._lib.ccl_quicktx_build(
- self._bridge._thread, self._bridge._encode(spec_json))
- return json.loads(self._bridge._check(rc))
-
-
-class Tx:
- """Lightweight operation collector for one transaction in a compose group.
-
- Use QuickTx.tx() to create, then chain operations and set sender address.
- """
-
- def __init__(self):
- self._operations = []
- self._from = None
- self._change_address = None
-
- def pay_to_address(self, address, *amounts, script_ref_cbor_hex=None, script_ref_type=None):
- op = {
- "type": "pay_to_address",
- "address": address,
- "amounts": list(amounts),
- }
- if script_ref_cbor_hex:
- op["script_ref_cbor_hex"] = script_ref_cbor_hex
- if script_ref_type:
- op["script_ref_type"] = script_ref_type
- self._operations.append(op)
- return self
-
- def pay_to_contract(self, address, amounts, datum_cbor_hex=None, datum_hash=None,
- script_ref_cbor_hex=None, script_ref_type=None):
- op = {
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts if isinstance(amounts, list) else [amounts],
- }
- if datum_cbor_hex:
- op["datum_cbor_hex"] = datum_cbor_hex
- if datum_hash:
- op["datum_hash"] = datum_hash
- if script_ref_cbor_hex:
- op["script_ref_cbor_hex"] = script_ref_cbor_hex
- if script_ref_type:
- op["script_ref_type"] = script_ref_type
- self._operations.append(op)
- return self
-
- def mint_assets(self, script_json, assets, receiver):
- self._operations.append({
- "type": "mint_assets",
- "script_json": script_json if isinstance(script_json, str) else json.dumps(script_json),
- "assets": assets,
- "receiver": receiver,
- })
- return self
-
- def attach_metadata(self, label, metadata):
- self._operations.append({
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- })
- return self
-
- def collect_from(self, utxos):
- self._operations.append({
- "type": "collect_from",
- "collect_utxos": utxos,
- })
- return self
-
- # Staking
-
- def register_stake_address(self, address):
- self._operations.append({"type": "register_stake_address", "address": address})
- return self
-
- def deregister_stake_address(self, address, refund_address=None):
- op = {"type": "deregister_stake_address", "address": address}
- if refund_address:
- op["refund_address"] = refund_address
- self._operations.append(op)
- return self
-
- def delegate_to(self, address, pool_id):
- self._operations.append({"type": "delegate_to", "address": address, "pool_id": pool_id})
- return self
-
- def withdraw(self, reward_address, amount, receiver=None):
- op = {"type": "withdraw", "reward_address": reward_address, "amount": str(amount)}
- if receiver:
- op["receiver"] = receiver
- self._operations.append(op)
- return self
-
- # DRep
-
- def register_drep(self, credential_hash, credential_type='key', anchor_url=None, anchor_data_hash=None):
- op = {"type": "register_drep", "credential_hash": credential_hash, "credential_type": credential_type}
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- def unregister_drep(self, credential_hash, credential_type='key', refund_address=None, refund_amount=None):
- op = {"type": "unregister_drep", "credential_hash": credential_hash, "credential_type": credential_type}
- if refund_address:
- op["refund_address"] = refund_address
- if refund_amount is not None:
- op["refund_amount"] = str(refund_amount)
- self._operations.append(op)
- return self
-
- def update_drep(self, credential_hash, credential_type='key', anchor_url=None, anchor_data_hash=None):
- op = {"type": "update_drep", "credential_hash": credential_hash, "credential_type": credential_type}
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- # Voting
-
- def delegate_voting_power_to(self, address, drep_type, drep_hash=None):
- op = {"type": "delegate_voting_power_to", "address": address, "drep_type": drep_type}
- if drep_hash:
- op["drep_hash"] = drep_hash
- self._operations.append(op)
- return self
-
- def create_vote(self, voter_type, voter_hash, gov_action_tx_hash, gov_action_index, vote,
- anchor_url=None, anchor_data_hash=None):
- op = {
- "type": "create_vote", "voter_type": voter_type, "voter_hash": voter_hash,
- "gov_action_tx_hash": gov_action_tx_hash, "gov_action_index": gov_action_index, "vote": vote,
- }
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- # Governance
-
- def create_proposal(self, gov_action_type, return_address, anchor_url, anchor_data_hash, **kwargs):
- op = {
- "type": "create_proposal", "gov_action_type": gov_action_type,
- "return_address": return_address, "anchor_url": anchor_url, "anchor_data_hash": anchor_data_hash,
- }
- if "withdrawals" in kwargs:
- op["withdrawals"] = kwargs["withdrawals"]
- if "gov_action_tx_hash" in kwargs:
- op["gov_action_tx_hash"] = kwargs["gov_action_tx_hash"]
- if "gov_action_index" in kwargs:
- op["gov_action_index"] = kwargs["gov_action_index"]
- if "members_to_remove" in kwargs:
- op["members_to_remove"] = kwargs["members_to_remove"]
- if "new_members" in kwargs:
- op["new_members"] = kwargs["new_members"]
- if "quorum_numerator" in kwargs:
- op["quorum_numerator"] = str(kwargs["quorum_numerator"])
- if "quorum_denominator" in kwargs:
- op["quorum_denominator"] = str(kwargs["quorum_denominator"])
- if "constitution_anchor_url" in kwargs:
- op["constitution_anchor_url"] = kwargs["constitution_anchor_url"]
- if "constitution_anchor_data_hash" in kwargs:
- op["constitution_anchor_data_hash"] = kwargs["constitution_anchor_data_hash"]
- if "constitution_script_hash" in kwargs:
- op["constitution_script_hash"] = kwargs["constitution_script_hash"]
- if "protocol_version_major" in kwargs:
- op["protocol_version_major"] = kwargs["protocol_version_major"]
- if "protocol_version_minor" in kwargs:
- op["protocol_version_minor"] = kwargs["protocol_version_minor"]
- if "policy_hash" in kwargs:
- op["policy_hash"] = kwargs["policy_hash"]
- self._operations.append(op)
- return self
-
- # Pool operations
-
- def register_pool(self, operator, vrf_key_hash, pledge, cost, margin_numerator, margin_denominator,
- reward_address, pool_owners, relays=None, pool_metadata_url=None, pool_metadata_hash=None):
- op = {
- "type": "register_pool", "operator": operator, "vrf_key_hash": vrf_key_hash,
- "pledge": str(pledge), "cost": str(cost),
- "margin_numerator": str(margin_numerator), "margin_denominator": str(margin_denominator),
- "reward_address": reward_address, "pool_owners": pool_owners,
- }
- if relays:
- op["relays"] = relays
- if pool_metadata_url:
- op["pool_metadata_url"] = pool_metadata_url
- if pool_metadata_hash:
- op["pool_metadata_hash"] = pool_metadata_hash
- self._operations.append(op)
- return self
-
- def update_pool(self, operator, vrf_key_hash, pledge, cost, margin_numerator, margin_denominator,
- reward_address, pool_owners, relays=None, pool_metadata_url=None, pool_metadata_hash=None):
- op = {
- "type": "update_pool", "operator": operator, "vrf_key_hash": vrf_key_hash,
- "pledge": str(pledge), "cost": str(cost),
- "margin_numerator": str(margin_numerator), "margin_denominator": str(margin_denominator),
- "reward_address": reward_address, "pool_owners": pool_owners,
- }
- if relays:
- op["relays"] = relays
- if pool_metadata_url:
- op["pool_metadata_url"] = pool_metadata_url
- if pool_metadata_hash:
- op["pool_metadata_hash"] = pool_metadata_hash
- self._operations.append(op)
- return self
-
- def retire_pool(self, pool_id, epoch):
- self._operations.append({"type": "retire_pool", "pool_id": pool_id, "epoch": epoch})
- return self
-
- # Treasury donation
-
- def donate_to_treasury(self, treasury_value, donation_amount):
- self._operations.append({
- "type": "donate_to_treasury",
- "treasury_value": str(treasury_value),
- "donation_amount": str(donation_amount),
- })
- return self
-
- # Native script attachment
-
- def attach_native_script(self, script_json):
- self._operations.append({
- "type": "attach_native_script",
- "script_json": script_json if isinstance(script_json, str) else json.dumps(script_json),
- })
- return self
-
- def from_address(self, address):
- self._from = address
- return self
-
- def change_address(self, address):
- self._change_address = address
- return self
-
- def _to_spec(self):
- spec = {
- "from": self._from,
- "operations": self._operations,
- }
- if self._change_address:
- spec["change_address"] = self._change_address
- return spec
-
-
-class ScriptTxBuilder:
- """Builder for script (Plutus) transaction specifications.
-
- Like TxBuilder but produces a spec with tx_type: "script_tx".
- Supports script-specific operations such as collect_from with redeemer,
- read_from reference inputs, mint_plutus_assets, and attach validators.
- """
-
- def __init__(self, bridge):
- self._bridge = bridge
- self._operations = []
- self._from = None
- self._change_address = None
- self._fee_payer = None
- self._utxos = None
- self._protocol_params = None
- self._validity = {}
- self._merge_outputs = None
- self._signer_count = 1
- self._change_datum_cbor_hex = None
- self._change_datum_hash = None
-
- # --- Common operations (same as TxBuilder) ---
-
- def pay_to_address(self, address, *amounts, script_ref_cbor_hex=None, script_ref_type=None):
- """Add a payment to an address."""
- op = {
- "type": "pay_to_address",
- "address": address,
- "amounts": list(amounts),
- }
- if script_ref_cbor_hex:
- op["script_ref_cbor_hex"] = script_ref_cbor_hex
- if script_ref_type:
- op["script_ref_type"] = script_ref_type
- self._operations.append(op)
- return self
-
- def pay_to_contract(self, address, amounts, datum_cbor_hex=None, datum_hash=None,
- script_ref_cbor_hex=None, script_ref_type=None):
- """Add a payment to a contract address with datum."""
- op = {
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts if isinstance(amounts, list) else [amounts],
- }
- if datum_cbor_hex:
- op["datum_cbor_hex"] = datum_cbor_hex
- if datum_hash:
- op["datum_hash"] = datum_hash
- if script_ref_cbor_hex:
- op["script_ref_cbor_hex"] = script_ref_cbor_hex
- if script_ref_type:
- op["script_ref_type"] = script_ref_type
- self._operations.append(op)
- return self
-
- def attach_metadata(self, label, metadata):
- """Attach metadata to the transaction."""
- self._operations.append({
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- })
- return self
-
- def collect_from(self, utxos):
- """Specify explicit UTXOs as transaction inputs (without redeemer)."""
- self._operations.append({
- "type": "collect_from",
- "collect_utxos": utxos,
- })
- return self
-
- # --- Script-specific operations ---
-
- def collect_from_script(self, utxos, redeemer_cbor_hex, datum_cbor_hex=None):
- """Collect UTXOs from a script address with redeemer and optional datum.
-
- Args:
- utxos: List of UTXO dicts with tx_hash, output_index, address, amount
- redeemer_cbor_hex: Redeemer CBOR hex string
- datum_cbor_hex: Optional datum CBOR hex string
- """
- op = {
- "type": "collect_from",
- "collect_utxos": utxos,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if datum_cbor_hex:
- op["datum_cbor_hex"] = datum_cbor_hex
- self._operations.append(op)
- return self
-
- def read_from(self, reference_inputs):
- """Add reference inputs to the transaction.
-
- Args:
- reference_inputs: List of dicts with 'tx_hash' and 'output_index'
- """
- self._operations.append({
- "type": "read_from",
- "reference_inputs": reference_inputs,
- })
- return self
-
- def mint_plutus_assets(self, script_cbor_hex, script_type, assets, redeemer_cbor_hex,
- receiver=None, output_datum_cbor_hex=None):
- """Mint assets using a Plutus script.
-
- Args:
- script_cbor_hex: Plutus script CBOR hex
- script_type: Script type ('plutus_v1', 'plutus_v2', 'plutus_v3')
- assets: List of {"name": "...", "quantity": "..."} dicts
- redeemer_cbor_hex: Redeemer CBOR hex string
- receiver: Optional receiver address for minted assets
- output_datum_cbor_hex: Optional output datum CBOR hex
- """
- op = {
- "type": "mint_plutus_assets",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- "assets": assets,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if receiver:
- op["receiver"] = receiver
- if output_datum_cbor_hex:
- op["output_datum_cbor_hex"] = output_datum_cbor_hex
- self._operations.append(op)
- return self
-
- def attach_spending_validator(self, script_cbor_hex, script_type):
- """Attach a spending validator script."""
- self._operations.append({
- "type": "attach_spending_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- def attach_certificate_validator(self, script_cbor_hex, script_type):
- """Attach a certificate validator script."""
- self._operations.append({
- "type": "attach_certificate_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- def attach_reward_validator(self, script_cbor_hex, script_type):
- """Attach a reward validator script."""
- self._operations.append({
- "type": "attach_reward_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- def attach_proposing_validator(self, script_cbor_hex, script_type):
- """Attach a proposing validator script."""
- self._operations.append({
- "type": "attach_proposing_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- def attach_voting_validator(self, script_cbor_hex, script_type):
- """Attach a voting validator script."""
- self._operations.append({
- "type": "attach_voting_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- # --- Staking (with redeemer) ---
-
- def deregister_stake_address(self, address, redeemer_cbor_hex, refund_address=None):
- """Deregister a stake address with redeemer."""
- op = {
- "type": "deregister_stake_address",
- "address": address,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if refund_address:
- op["refund_address"] = refund_address
- self._operations.append(op)
- return self
-
- def delegate_to(self, address, pool_id, redeemer_cbor_hex):
- """Delegate stake to a pool with redeemer."""
- self._operations.append({
- "type": "delegate_to",
- "address": address,
- "pool_id": pool_id,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- })
- return self
-
- def withdraw(self, reward_address, amount, redeemer_cbor_hex, receiver=None):
- """Withdraw staking rewards with redeemer."""
- op = {
- "type": "withdraw",
- "reward_address": reward_address,
- "amount": str(amount),
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if receiver:
- op["receiver"] = receiver
- self._operations.append(op)
- return self
-
- # --- DRep (with redeemer) ---
-
- def register_drep(self, credential_hash, credential_type, redeemer_cbor_hex,
- anchor_url=None, anchor_data_hash=None):
- """Register a DRep with redeemer."""
- op = {
- "type": "register_drep",
- "credential_hash": credential_hash,
- "credential_type": credential_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- def unregister_drep(self, credential_hash, credential_type, redeemer_cbor_hex,
- refund_address=None, refund_amount=None):
- """Unregister a DRep with redeemer."""
- op = {
- "type": "unregister_drep",
- "credential_hash": credential_hash,
- "credential_type": credential_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if refund_address:
- op["refund_address"] = refund_address
- if refund_amount is not None:
- op["refund_amount"] = str(refund_amount)
- self._operations.append(op)
- return self
-
- def update_drep(self, credential_hash, credential_type, redeemer_cbor_hex,
- anchor_url=None, anchor_data_hash=None):
- """Update DRep metadata with redeemer."""
- op = {
- "type": "update_drep",
- "credential_hash": credential_hash,
- "credential_type": credential_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- # --- Voting (with redeemer) ---
-
- def delegate_voting_power_to(self, address, drep_type, drep_hash, redeemer_cbor_hex):
- """Delegate voting power to a DRep with redeemer."""
- op = {
- "type": "delegate_voting_power_to",
- "address": address,
- "drep_type": drep_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if drep_hash:
- op["drep_hash"] = drep_hash
- self._operations.append(op)
- return self
-
- def create_vote(self, voter_type, voter_hash, gov_action_tx_hash, gov_action_index, vote,
- redeemer_cbor_hex, anchor_url=None, anchor_data_hash=None):
- """Cast a governance vote with redeemer."""
- op = {
- "type": "create_vote",
- "voter_type": voter_type,
- "voter_hash": voter_hash,
- "gov_action_tx_hash": gov_action_tx_hash,
- "gov_action_index": gov_action_index,
- "vote": vote,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- # --- Governance (with redeemer) ---
-
- def create_proposal(self, gov_action_type, return_address, anchor_url, anchor_data_hash,
- redeemer_cbor_hex, **kwargs):
- """Create a governance proposal with redeemer."""
- op = {
- "type": "create_proposal",
- "gov_action_type": gov_action_type,
- "return_address": return_address,
- "anchor_url": anchor_url,
- "anchor_data_hash": anchor_data_hash,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if "withdrawals" in kwargs:
- op["withdrawals"] = kwargs["withdrawals"]
- if "gov_action_tx_hash" in kwargs:
- op["gov_action_tx_hash"] = kwargs["gov_action_tx_hash"]
- if "gov_action_index" in kwargs:
- op["gov_action_index"] = kwargs["gov_action_index"]
- if "members_to_remove" in kwargs:
- op["members_to_remove"] = kwargs["members_to_remove"]
- if "new_members" in kwargs:
- op["new_members"] = kwargs["new_members"]
- if "quorum_numerator" in kwargs:
- op["quorum_numerator"] = str(kwargs["quorum_numerator"])
- if "quorum_denominator" in kwargs:
- op["quorum_denominator"] = str(kwargs["quorum_denominator"])
- if "constitution_anchor_url" in kwargs:
- op["constitution_anchor_url"] = kwargs["constitution_anchor_url"]
- if "constitution_anchor_data_hash" in kwargs:
- op["constitution_anchor_data_hash"] = kwargs["constitution_anchor_data_hash"]
- if "constitution_script_hash" in kwargs:
- op["constitution_script_hash"] = kwargs["constitution_script_hash"]
- if "protocol_version_major" in kwargs:
- op["protocol_version_major"] = kwargs["protocol_version_major"]
- if "protocol_version_minor" in kwargs:
- op["protocol_version_minor"] = kwargs["protocol_version_minor"]
- if "policy_hash" in kwargs:
- op["policy_hash"] = kwargs["policy_hash"]
- self._operations.append(op)
- return self
-
- # Treasury donation
-
- def donate_to_treasury(self, treasury_value, donation_amount, redeemer_cbor_hex):
- """Donate ADA to the treasury with redeemer."""
- self._operations.append({
- "type": "donate_to_treasury",
- "treasury_value": str(treasury_value),
- "donation_amount": str(donation_amount),
- "redeemer_cbor_hex": redeemer_cbor_hex,
- })
- return self
-
- # --- Builder configuration ---
-
- def from_address(self, address):
- """Set the sender address."""
- self._from = address
- return self
-
- def change_address(self, address):
- """Set the change address (defaults to sender)."""
- self._change_address = address
- return self
-
- def change_datum(self, datum_cbor_hex):
- """Set inline datum for the change output."""
- self._change_datum_cbor_hex = datum_cbor_hex
- return self
-
- def change_datum_hash(self, hash):
- """Set datum hash for the change output."""
- self._change_datum_hash = hash
- return self
-
- def fee_payer(self, address):
- """Set the fee payer address."""
- self._fee_payer = address
- return self
-
- def with_utxos(self, utxos):
- """Provide UTXOs for coin selection."""
- self._utxos = utxos
- return self
-
- def with_protocol_params(self, params):
- """Provide protocol parameters."""
- self._protocol_params = params
- return self
-
- def valid_from(self, slot):
- """Set transaction validity start slot."""
- self._validity["valid_from"] = slot
- return self
-
- def valid_to(self, slot):
- """Set transaction validity end slot (TTL)."""
- self._validity["valid_to"] = slot
- return self
-
- def merge_outputs(self, merge):
- """Whether to merge outputs to the same address (default: True)."""
- self._merge_outputs = merge
- return self
-
- def signer_count(self, count):
- """Set the number of signers for fee estimation (default: 1)."""
- self._signer_count = count
- return self
-
- def build(self, provider=None, provider_config=None):
- """Build the script transaction. Returns dict with tx_cbor, tx_hash, fee.
-
- Args:
- provider: Optional Provider instance for auto-fetching UTXOs
- and protocol params (wrapper-side).
- provider_config: Optional ProviderConfig or dict with 'name', 'url',
- and optionally 'api_key' and 'enable_cost_evaluation' for
- Java-side lazy UTXO fetching via HTTP.
-
- Raises:
- ValueError: If both provider and provider_config are specified.
- """
- if provider and provider_config:
- raise ValueError("Cannot specify both 'provider' and 'provider_config'")
-
- utxos = self._utxos
- protocol_params = self._protocol_params
-
- if provider_config:
- spec = {
- "tx_type": "script_tx",
- "operations": self._operations,
- "from": self._from,
- "provider": _build_provider_dict(provider_config),
- "signer_count": self._signer_count,
- }
- if protocol_params is not None:
- spec["protocol_params"] = protocol_params
- elif provider:
- if utxos is None and self._from:
- utxos = provider.get_utxos(self._from)
- if protocol_params is None:
- protocol_params = provider.get_protocol_params()
- spec = {
- "tx_type": "script_tx",
- "operations": self._operations,
- "from": self._from,
- "utxos": utxos,
- "protocol_params": protocol_params,
- "signer_count": self._signer_count,
- }
- else:
- spec = {
- "tx_type": "script_tx",
- "operations": self._operations,
- "from": self._from,
- "utxos": utxos,
- "protocol_params": protocol_params,
- "signer_count": self._signer_count,
- }
-
- if self._change_address:
- spec["change_address"] = self._change_address
- if self._fee_payer:
- spec["fee_payer"] = self._fee_payer
- if self._validity:
- spec["validity"] = self._validity
- if self._merge_outputs is not None:
- spec["merge_outputs"] = self._merge_outputs
- if self._change_datum_cbor_hex:
- spec["change_datum_cbor_hex"] = self._change_datum_cbor_hex
- if self._change_datum_hash:
- spec["change_datum_hash"] = self._change_datum_hash
-
- spec_json = json.dumps(spec)
- rc = self._bridge._lib.ccl_quicktx_build(
- self._bridge._thread, self._bridge._encode(spec_json))
- return json.loads(self._bridge._check(rc))
-
- def build_with_provider(self, provider):
- """Build with a Provider that auto-fetches UTXOs and protocol params.
-
- Args:
- provider: Provider instance (e.g. YaciDevKitProvider)
- """
- if self._utxos is None and self._from:
- self._utxos = provider.get_utxos(self._from)
- if self._protocol_params is None:
- self._protocol_params = provider.get_protocol_params()
- return self.build()
-
-
-class ScriptTx:
- """Lightweight operation collector for a script transaction in a compose group.
-
- Like Tx but with tx_type: "script_tx" in _to_spec(). Supports script-specific
- operations such as collect_from with redeemer, read_from, mint_plutus_assets,
- and attach validators.
- """
-
- def __init__(self):
- self._operations = []
- self._from = None
- self._change_address = None
- self._change_datum_cbor_hex = None
- self._change_datum_hash = None
-
- # --- Common operations ---
-
- def pay_to_address(self, address, *amounts, script_ref_cbor_hex=None, script_ref_type=None):
- op = {
- "type": "pay_to_address",
- "address": address,
- "amounts": list(amounts),
- }
- if script_ref_cbor_hex:
- op["script_ref_cbor_hex"] = script_ref_cbor_hex
- if script_ref_type:
- op["script_ref_type"] = script_ref_type
- self._operations.append(op)
- return self
-
- def pay_to_contract(self, address, amounts, datum_cbor_hex=None, datum_hash=None,
- script_ref_cbor_hex=None, script_ref_type=None):
- op = {
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts if isinstance(amounts, list) else [amounts],
- }
- if datum_cbor_hex:
- op["datum_cbor_hex"] = datum_cbor_hex
- if datum_hash:
- op["datum_hash"] = datum_hash
- if script_ref_cbor_hex:
- op["script_ref_cbor_hex"] = script_ref_cbor_hex
- if script_ref_type:
- op["script_ref_type"] = script_ref_type
- self._operations.append(op)
- return self
-
- def attach_metadata(self, label, metadata):
- self._operations.append({
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- })
- return self
-
- def collect_from(self, utxos):
- self._operations.append({
- "type": "collect_from",
- "collect_utxos": utxos,
- })
- return self
-
- # --- Script-specific operations ---
-
- def collect_from_script(self, utxos, redeemer_cbor_hex, datum_cbor_hex=None):
- """Collect UTXOs from a script address with redeemer and optional datum."""
- op = {
- "type": "collect_from",
- "collect_utxos": utxos,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if datum_cbor_hex:
- op["datum_cbor_hex"] = datum_cbor_hex
- self._operations.append(op)
- return self
-
- def read_from(self, reference_inputs):
- """Add reference inputs to the transaction."""
- self._operations.append({
- "type": "read_from",
- "reference_inputs": reference_inputs,
- })
- return self
-
- def mint_plutus_assets(self, script_cbor_hex, script_type, assets, redeemer_cbor_hex,
- receiver=None, output_datum_cbor_hex=None):
- """Mint assets using a Plutus script."""
- op = {
- "type": "mint_plutus_assets",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- "assets": assets,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if receiver:
- op["receiver"] = receiver
- if output_datum_cbor_hex:
- op["output_datum_cbor_hex"] = output_datum_cbor_hex
- self._operations.append(op)
- return self
-
- def attach_spending_validator(self, script_cbor_hex, script_type):
- self._operations.append({
- "type": "attach_spending_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- def attach_certificate_validator(self, script_cbor_hex, script_type):
- self._operations.append({
- "type": "attach_certificate_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- def attach_reward_validator(self, script_cbor_hex, script_type):
- self._operations.append({
- "type": "attach_reward_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- def attach_proposing_validator(self, script_cbor_hex, script_type):
- self._operations.append({
- "type": "attach_proposing_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- def attach_voting_validator(self, script_cbor_hex, script_type):
- self._operations.append({
- "type": "attach_voting_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- })
- return self
-
- # --- Staking (with redeemer) ---
-
- def deregister_stake_address(self, address, redeemer_cbor_hex, refund_address=None):
- op = {
- "type": "deregister_stake_address",
- "address": address,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if refund_address:
- op["refund_address"] = refund_address
- self._operations.append(op)
- return self
-
- def delegate_to(self, address, pool_id, redeemer_cbor_hex):
- self._operations.append({
- "type": "delegate_to",
- "address": address,
- "pool_id": pool_id,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- })
- return self
-
- def withdraw(self, reward_address, amount, redeemer_cbor_hex, receiver=None):
- op = {
- "type": "withdraw",
- "reward_address": reward_address,
- "amount": str(amount),
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if receiver:
- op["receiver"] = receiver
- self._operations.append(op)
- return self
-
- # --- DRep (with redeemer) ---
-
- def register_drep(self, credential_hash, credential_type, redeemer_cbor_hex,
- anchor_url=None, anchor_data_hash=None):
- op = {
- "type": "register_drep",
- "credential_hash": credential_hash,
- "credential_type": credential_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- def unregister_drep(self, credential_hash, credential_type, redeemer_cbor_hex,
- refund_address=None, refund_amount=None):
- op = {
- "type": "unregister_drep",
- "credential_hash": credential_hash,
- "credential_type": credential_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if refund_address:
- op["refund_address"] = refund_address
- if refund_amount is not None:
- op["refund_amount"] = str(refund_amount)
- self._operations.append(op)
- return self
-
- def update_drep(self, credential_hash, credential_type, redeemer_cbor_hex,
- anchor_url=None, anchor_data_hash=None):
- op = {
- "type": "update_drep",
- "credential_hash": credential_hash,
- "credential_type": credential_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- # --- Voting (with redeemer) ---
-
- def delegate_voting_power_to(self, address, drep_type, drep_hash, redeemer_cbor_hex):
- op = {
- "type": "delegate_voting_power_to",
- "address": address,
- "drep_type": drep_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if drep_hash:
- op["drep_hash"] = drep_hash
- self._operations.append(op)
- return self
-
- def create_vote(self, voter_type, voter_hash, gov_action_tx_hash, gov_action_index, vote,
- redeemer_cbor_hex, anchor_url=None, anchor_data_hash=None):
- op = {
- "type": "create_vote",
- "voter_type": voter_type,
- "voter_hash": voter_hash,
- "gov_action_tx_hash": gov_action_tx_hash,
- "gov_action_index": gov_action_index,
- "vote": vote,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if anchor_url:
- op["anchor_url"] = anchor_url
- if anchor_data_hash:
- op["anchor_data_hash"] = anchor_data_hash
- self._operations.append(op)
- return self
-
- # --- Governance (with redeemer) ---
-
- def create_proposal(self, gov_action_type, return_address, anchor_url, anchor_data_hash,
- redeemer_cbor_hex, **kwargs):
- op = {
- "type": "create_proposal",
- "gov_action_type": gov_action_type,
- "return_address": return_address,
- "anchor_url": anchor_url,
- "anchor_data_hash": anchor_data_hash,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }
- if "withdrawals" in kwargs:
- op["withdrawals"] = kwargs["withdrawals"]
- if "gov_action_tx_hash" in kwargs:
- op["gov_action_tx_hash"] = kwargs["gov_action_tx_hash"]
- if "gov_action_index" in kwargs:
- op["gov_action_index"] = kwargs["gov_action_index"]
- if "members_to_remove" in kwargs:
- op["members_to_remove"] = kwargs["members_to_remove"]
- if "new_members" in kwargs:
- op["new_members"] = kwargs["new_members"]
- if "quorum_numerator" in kwargs:
- op["quorum_numerator"] = str(kwargs["quorum_numerator"])
- if "quorum_denominator" in kwargs:
- op["quorum_denominator"] = str(kwargs["quorum_denominator"])
- if "constitution_anchor_url" in kwargs:
- op["constitution_anchor_url"] = kwargs["constitution_anchor_url"]
- if "constitution_anchor_data_hash" in kwargs:
- op["constitution_anchor_data_hash"] = kwargs["constitution_anchor_data_hash"]
- if "constitution_script_hash" in kwargs:
- op["constitution_script_hash"] = kwargs["constitution_script_hash"]
- if "protocol_version_major" in kwargs:
- op["protocol_version_major"] = kwargs["protocol_version_major"]
- if "protocol_version_minor" in kwargs:
- op["protocol_version_minor"] = kwargs["protocol_version_minor"]
- if "policy_hash" in kwargs:
- op["policy_hash"] = kwargs["policy_hash"]
- self._operations.append(op)
- return self
-
- # Treasury donation
-
- def donate_to_treasury(self, treasury_value, donation_amount, redeemer_cbor_hex):
- self._operations.append({
- "type": "donate_to_treasury",
- "treasury_value": str(treasury_value),
- "donation_amount": str(donation_amount),
- "redeemer_cbor_hex": redeemer_cbor_hex,
- })
- return self
-
- # --- Address configuration ---
-
- def from_address(self, address):
- self._from = address
- return self
-
- def change_address(self, address):
- self._change_address = address
- return self
-
- def change_datum(self, datum_cbor_hex):
- """Set inline datum for the change output."""
- self._change_datum_cbor_hex = datum_cbor_hex
- return self
-
- def change_datum_hash(self, hash):
- """Set datum hash for the change output."""
- self._change_datum_hash = hash
- return self
-
- def _to_spec(self):
- spec = {
- "tx_type": "script_tx",
- "from": self._from,
- "operations": self._operations,
- }
- if self._change_address:
- spec["change_address"] = self._change_address
- if self._change_datum_cbor_hex:
- spec["change_datum_cbor_hex"] = self._change_datum_cbor_hex
- if self._change_datum_hash:
- spec["change_datum_hash"] = self._change_datum_hash
- return spec
-
-
-class ComposeTxBuilder:
- """Builder for composing multiple Tx/ScriptTx objects into a single transaction."""
-
- def __init__(self, bridge, txs):
- self._bridge = bridge
- self._txs = list(txs)
- self._fee_payer = None
- self._utxos = None
- self._protocol_params = None
- self._validity = {}
- self._merge_outputs = None
- self._signer_count = None
-
- def fee_payer(self, address):
- self._fee_payer = address
- return self
-
- def with_utxos(self, utxos):
- self._utxos = utxos
- return self
-
- def with_protocol_params(self, params):
- self._protocol_params = params
- return self
-
- def valid_from(self, slot):
- self._validity["valid_from"] = slot
- return self
-
- def valid_to(self, slot):
- self._validity["valid_to"] = slot
- return self
-
- def merge_outputs(self, merge):
- self._merge_outputs = merge
- return self
-
- def signer_count(self, count):
- self._signer_count = count
- return self
-
- def build(self, provider=None, provider_config=None):
- """Build the composed transaction.
-
- Args:
- provider: Optional Provider instance for auto-fetching UTXOs
- and protocol params (wrapper-side).
- provider_config: Optional ProviderConfig or dict with 'name', 'url',
- and optionally 'api_key' and 'enable_cost_evaluation' for
- Java-side lazy UTXO fetching via HTTP.
-
- Raises:
- ValueError: If both provider and provider_config are specified.
- """
- if provider and provider_config:
- raise ValueError("Cannot specify both 'provider' and 'provider_config'")
-
- utxos = self._utxos
- protocol_params = self._protocol_params
-
- if provider_config:
- spec = {
- "transactions": [tx._to_spec() for tx in self._txs],
- "fee_payer": self._fee_payer,
- "provider": _build_provider_dict(provider_config),
- }
- if protocol_params is not None:
- spec["protocol_params"] = protocol_params
- elif provider:
- if utxos is None:
- addresses = set()
- for tx in self._txs:
- if tx._from:
- addresses.add(tx._from)
- all_utxos = []
- for addr in addresses:
- all_utxos.extend(provider.get_utxos(addr))
- utxos = all_utxos
- if protocol_params is None:
- protocol_params = provider.get_protocol_params()
- spec = {
- "transactions": [tx._to_spec() for tx in self._txs],
- "fee_payer": self._fee_payer,
- "utxos": utxos,
- "protocol_params": protocol_params,
- }
- else:
- spec = {
- "transactions": [tx._to_spec() for tx in self._txs],
- "fee_payer": self._fee_payer,
- "utxos": utxos,
- "protocol_params": protocol_params,
- }
-
- if self._signer_count is not None:
- spec["signer_count"] = self._signer_count
- if self._validity:
- spec["validity"] = self._validity
- if self._merge_outputs is not None:
- spec["merge_outputs"] = self._merge_outputs
-
- spec_json = json.dumps(spec)
- rc = self._bridge._lib.ccl_quicktx_build(
- self._bridge._thread, self._bridge._encode(spec_json))
- return json.loads(self._bridge._check(rc))
-
-
-class QuickTx:
- """QuickTx namespace for CCL bridge."""
-
- def __init__(self, bridge):
- self._bridge = bridge
-
- def new_tx(self):
- """Create a new TxBuilder."""
- return TxBuilder(self._bridge)
-
- def tx(self):
- """Create a new Tx for use with compose()."""
- return Tx()
-
- def new_script_tx(self):
- """Create a new ScriptTxBuilder for Plutus script transactions."""
- return ScriptTxBuilder(self._bridge)
-
- def script_tx(self):
- """Create a new ScriptTx for use with compose()."""
- return ScriptTx()
-
- def compose(self, *txs):
- """Compose multiple Tx/ScriptTx objects into a single transaction.
-
- Args:
- *txs: Tx or ScriptTx objects to compose
- """
- return ComposeTxBuilder(self._bridge, txs)
+ self._bridge._thread,
+ self._bridge._encode(txplan_yaml),
+ self._bridge._encode(utxos_json),
+ self._bridge._encode(pp_json),
+ self._bridge._encode(exec_units_json),
+ )
+ return yaml.safe_load(self._bridge._check(rc))
diff --git a/wrappers/python/examples/01_account_and_keys.py b/wrappers/python/examples/01_account_and_keys.py
new file mode 100644
index 0000000..5f40e26
--- /dev/null
+++ b/wrappers/python/examples/01_account_and_keys.py
@@ -0,0 +1,42 @@
+"""Account creation and key derivation (offline).
+
+Run from the repo root:
+
+ LIB_DIR=core/build/native/nativeCompile
+ PYTHONPATH=wrappers/python CCL_LIB_PATH=$LIB_DIR \
+ DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+ python3 wrappers/python/examples/01_account_and_keys.py
+"""
+from ccl._ffi import CclLib
+
+
+def main():
+ lib = CclLib()
+ try:
+ # 1. Create a brand-new testnet account (random mnemonic).
+ account = lib.account.create(CclLib.TESTNET)
+ mnemonic = account["mnemonic"]
+ print("Created account")
+ print(" base address:", account["base_address"])
+ print(" mnemonic :", mnemonic)
+
+ # 2. Restore the same account from its mnemonic — the address must match.
+ restored = lib.account.from_mnemonic(mnemonic, CclLib.TESTNET, 0, 0)
+ assert restored["base_address"] == account["base_address"]
+ print("Restored from mnemonic — address matches:", restored["base_address"])
+
+ # 3. Derive keys.
+ priv = lib.account.get_private_key(mnemonic, CclLib.TESTNET)
+ pub = lib.account.get_public_key(mnemonic, CclLib.TESTNET)
+ print(" private key (extended, hex):", priv)
+ print(" public key (hex) :", pub)
+
+ # 4. Derive the governance DRep ID.
+ drep_id = lib.account.get_drep_id(mnemonic, CclLib.TESTNET)
+ print(" DRep ID:", drep_id)
+ finally:
+ lib.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/wrappers/python/examples/02_primitives.py b/wrappers/python/examples/02_primitives.py
new file mode 100644
index 0000000..1bcc14e
--- /dev/null
+++ b/wrappers/python/examples/02_primitives.py
@@ -0,0 +1,50 @@
+"""Crypto and address primitives (offline).
+
+Run from the repo root:
+
+ LIB_DIR=core/build/native/nativeCompile
+ PYTHONPATH=wrappers/python CCL_LIB_PATH=$LIB_DIR \
+ DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+ python3 wrappers/python/examples/02_primitives.py
+"""
+from ccl._ffi import CclLib
+
+
+def main():
+ lib = CclLib()
+ try:
+ # --- Mnemonics ---
+ mnemonic = lib.crypto.generate_mnemonic(24)
+ print("Generated 24-word mnemonic:", mnemonic)
+ print(" valid? ", lib.crypto.validate_mnemonic(mnemonic))
+ print(" 'not a real mnemonic' valid?", lib.crypto.validate_mnemonic("not a real mnemonic"))
+
+ # --- Blake2b hashing (hex in -> hex out). "Hello" == 48656c6c6f ---
+ print("Blake2b-256('Hello'):", lib.crypto.blake2b_256("48656c6c6f"))
+ print("Blake2b-224('Hello'):", lib.crypto.blake2b_224("48656c6c6f"))
+
+ # --- Ed25519 signing ---
+ # account_get_private_key returns the 64-byte extended key; ccl_crypto_sign
+ # expects a 32-byte Ed25519 key, so take the first 32 bytes (64 hex chars).
+ acct = lib.account.create(CclLib.TESTNET)
+ sk = lib.account.get_private_key(acct["mnemonic"], CclLib.TESTNET)[:64]
+ pk = lib.account.get_public_key(acct["mnemonic"], CclLib.TESTNET)
+ message_hex = "68656c6c6f" # "hello"
+ signature = lib.crypto.sign(message_hex, sk)
+ print("Ed25519 signature:", signature)
+ # A tampered signature is correctly rejected.
+ print(" verify(fake signature) ->", lib.crypto.verify("00" * 64, message_hex, pk))
+
+ # --- Address parsing & validation ---
+ addr = acct["base_address"]
+ print("Address valid?", lib.address.validate(addr))
+ print("Address info :", lib.address.info(addr))
+ raw = lib.address.to_bytes(addr)
+ print("Address -> bytes -> address round-trips:",
+ lib.address.from_bytes(raw) == addr)
+ finally:
+ lib.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/wrappers/python/examples/03_build_and_sign_tx.py b/wrappers/python/examples/03_build_and_sign_tx.py
new file mode 100644
index 0000000..3180bf5
--- /dev/null
+++ b/wrappers/python/examples/03_build_and_sign_tx.py
@@ -0,0 +1,70 @@
+"""Build and sign a payment transaction fully offline from a TxPlan (YAML).
+
+The transaction is defined as a TxPlan YAML document; we supply the UTXOs and protocol
+parameters ourselves (no node / no provider), build the unsigned transaction, then sign it
+locally. Submitting it to a network is a separate, online step.
+
+Run from the repo root:
+
+ LIB_DIR=core/build/native/nativeCompile
+ PYTHONPATH=wrappers/python CCL_LIB_PATH=$LIB_DIR \
+ DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+ python3 wrappers/python/examples/03_build_and_sign_tx.py
+"""
+from ccl._ffi import CclLib
+
+# Minimal protocol parameters (CCL ProtocolParams model).
+PROTOCOL_PARAMS = {
+ "min_fee_a": 44, "min_fee_b": 155381, "max_tx_size": 16384,
+ "key_deposit": "2000000", "pool_deposit": "500000000",
+ "coins_per_utxo_size": "4310", "max_val_size": "5000",
+ "max_tx_ex_mem": "10000000", "max_tx_ex_steps": "10000000000",
+ "price_mem": 0.0577, "price_step": 0.0000721, "collateral_percent": 150,
+ "max_collateral_inputs": 3,
+}
+
+
+def main():
+ lib = CclLib()
+ try:
+ sender = lib.account.create(CclLib.TESTNET)
+ receiver = lib.account.create(CclLib.TESTNET)
+
+ # A static UTXO the sender controls (100 ADA), instead of querying a node.
+ utxos = [{
+ "tx_hash": "a" * 64,
+ "output_index": 0,
+ "address": sender["base_address"],
+ "amount": [{"unit": "lovelace", "quantity": "100000000"}],
+ }]
+
+ # Define the transaction as a TxPlan YAML document: pay 5 ADA to the receiver.
+ txplan_yaml = f"""
+version: 1.0
+transaction:
+ - tx:
+ from: {sender['base_address']}
+ intents:
+ - type: payment
+ address: {receiver['base_address']}
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+"""
+
+ result = lib.quicktx.build(txplan_yaml, utxos, PROTOCOL_PARAMS)
+ print("Built unsigned transaction from TxPlan YAML")
+ print(" tx hash:", result["tx_hash"])
+ print(" fee :", result["fee"])
+ print(" cbor :", result["tx_cbor"][:80], "...")
+
+ signed = lib.account.sign_tx(
+ sender["mnemonic"], result["tx_cbor"], CclLib.TESTNET, 0, 0)
+ print("Signed transaction cbor:", signed[:80], "...")
+ print("\nNext step (not shown): submit `signed` to a Cardano node over HTTP.")
+ finally:
+ lib.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/wrappers/python/pyproject.toml b/wrappers/python/pyproject.toml
index ae4b836..3a27875 100644
--- a/wrappers/python/pyproject.toml
+++ b/wrappers/python/pyproject.toml
@@ -3,6 +3,7 @@ name = "ccl"
version = "0.1.0"
description = "Python bindings for Cardano Client Lib (CCL) via GraalVM native library"
requires-python = ">=3.8"
+dependencies = ["pyyaml>=6.0"]
[build-system]
requires = ["setuptools>=61.0"]
diff --git a/wrappers/python/tests/devkit_helper.py b/wrappers/python/tests/devkit_helper.py
index eb84d47..7b6841b 100644
--- a/wrappers/python/tests/devkit_helper.py
+++ b/wrappers/python/tests/devkit_helper.py
@@ -62,8 +62,12 @@ def submit_tx(self, tx_cbor_hex):
data=tx_bytes,
headers={"Content-Type": "application/cbor"},
)
- with urllib.request.urlopen(req) as resp:
- return resp.read().decode("utf-8").strip().strip('"')
+ try:
+ with urllib.request.urlopen(req) as resp:
+ return resp.read().decode("utf-8").strip().strip('"')
+ except urllib.error.HTTPError as e:
+ body = e.read().decode("utf-8", "replace")
+ raise RuntimeError(f"tx submit failed: HTTP {e.code}: {body}") from None
def get_tx(self, tx_hash):
"""Get transaction details by hash."""
diff --git a/wrappers/python/tests/test_compose_integration.py b/wrappers/python/tests/test_compose_integration.py
deleted file mode 100644
index b539be9..0000000
--- a/wrappers/python/tests/test_compose_integration.py
+++ /dev/null
@@ -1,183 +0,0 @@
-"""Integration tests for QuickTx compose (multi-Tx) with Yaci DevKit.
-
-Requires:
-- Yaci DevKit running on port 10000
-- Native library built: ./gradlew :core:nativeCompile
-
-Run with:
- PYTHONPATH=wrappers/python CCL_LIB_PATH=core/build/native/nativeCompile \
- pytest wrappers/python/tests/test_compose_integration.py -v
-"""
-import time
-import pytest
-from ccl._ffi import CclLib, CclError
-from ccl.quicktx import Amount
-from tests.devkit_helper import DevKitHelper
-
-
-@pytest.fixture(scope="module")
-def devkit():
- """Provide a DevKit helper, skip if DevKit is not running."""
- helper = DevKitHelper()
- if not helper.is_available():
- pytest.skip("Yaci DevKit is not running on port 10000")
- helper.reset()
- time.sleep(3) # wait for devnet reset
- return helper
-
-
-@pytest.fixture(scope="module")
-def ccl_lib():
- """Create a shared CclLib instance."""
- lib = CclLib()
- yield lib
- lib.close()
-
-
-def fund_account(ccl_lib, devkit, ada=150):
- """Create and fund a new account."""
- account = ccl_lib.account.create(CclLib.TESTNET)
- devkit.topup(account["base_address"], ada)
- devkit.wait_for_block(2)
- return account
-
-
-def get_lovelace(devkit, address):
- """Sum all lovelace at an address."""
- utxos = devkit.get_utxos(address)
- return sum(
- int(a["quantity"])
- for u in utxos
- for a in u["amount"]
- if a["unit"] == "lovelace"
- )
-
-
-def test_compose_two_senders(ccl_lib, devkit):
- """Compose two Txs from different senders, sign with both, submit, verify."""
- sender1 = fund_account(ccl_lib, devkit)
- sender2 = fund_account(ccl_lib, devkit)
- receiver1 = ccl_lib.account.create(CclLib.TESTNET)
- receiver2 = ccl_lib.account.create(CclLib.TESTNET)
-
- tx1 = ccl_lib.quicktx.tx() \
- .pay_to_address(receiver1["base_address"], Amount.ada(5)) \
- .from_address(sender1["base_address"])
-
- tx2 = ccl_lib.quicktx.tx() \
- .pay_to_address(receiver2["base_address"], Amount.ada(3)) \
- .from_address(sender2["base_address"])
-
- # Gather UTXOs for both senders
- utxos1 = devkit.get_utxos(sender1["base_address"])
- utxos2 = devkit.get_utxos(sender2["base_address"])
- all_utxos = utxos1 + utxos2
- pp = devkit.get_protocol_params()
-
- result = ccl_lib.quicktx.compose(tx1, tx2) \
- .fee_payer(sender1["base_address"]) \
- .with_utxos(all_utxos) \
- .with_protocol_params(pp) \
- .signer_count(2) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
- # Sign with both senders
- signed = ccl_lib.account.sign_tx(
- sender1["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
- signed = ccl_lib.account.sign_tx(
- sender2["mnemonic"], signed,
- CclLib.TESTNET, 0, 0)
-
- # Submit
- devkit.submit_tx(signed)
- devkit.wait_for_block(3)
-
- # Verify both receivers got their ADA
- assert get_lovelace(devkit, receiver1["base_address"]) == 5_000_000
- assert get_lovelace(devkit, receiver2["base_address"]) == 3_000_000
-
-
-def test_compose_with_metadata(ccl_lib, devkit):
- """Compose two Txs where one has metadata attached."""
- sender1 = fund_account(ccl_lib, devkit)
- sender2 = fund_account(ccl_lib, devkit)
- receiver1 = ccl_lib.account.create(CclLib.TESTNET)
- receiver2 = ccl_lib.account.create(CclLib.TESTNET)
-
- tx1 = ccl_lib.quicktx.tx() \
- .pay_to_address(receiver1["base_address"], Amount.ada(5)) \
- .attach_metadata(674, {"msg": ["Compose integration test"]}) \
- .from_address(sender1["base_address"])
-
- tx2 = ccl_lib.quicktx.tx() \
- .pay_to_address(receiver2["base_address"], Amount.ada(3)) \
- .from_address(sender2["base_address"])
-
- utxos1 = devkit.get_utxos(sender1["base_address"])
- utxos2 = devkit.get_utxos(sender2["base_address"])
- pp = devkit.get_protocol_params()
-
- result = ccl_lib.quicktx.compose(tx1, tx2) \
- .fee_payer(sender1["base_address"]) \
- .with_utxos(utxos1 + utxos2) \
- .with_protocol_params(pp) \
- .signer_count(2) \
- .build()
-
- signed = ccl_lib.account.sign_tx(
- sender1["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
- signed = ccl_lib.account.sign_tx(
- sender2["mnemonic"], signed,
- CclLib.TESTNET, 0, 0)
-
- devkit.submit_tx(signed)
- devkit.wait_for_block(3)
-
- # Verify tx on-chain
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
- assert get_lovelace(devkit, receiver1["base_address"]) == 5_000_000
- assert get_lovelace(devkit, receiver2["base_address"]) == 3_000_000
-
-
-def test_compose_with_provider(ccl_lib, devkit):
- """Compose using provider for auto-fetching UTXOs and protocol params."""
- sender1 = fund_account(ccl_lib, devkit)
- sender2 = fund_account(ccl_lib, devkit)
- receiver1 = ccl_lib.account.create(CclLib.TESTNET)
- receiver2 = ccl_lib.account.create(CclLib.TESTNET)
-
- tx1 = ccl_lib.quicktx.tx() \
- .pay_to_address(receiver1["base_address"], Amount.ada(5)) \
- .from_address(sender1["base_address"])
-
- tx2 = ccl_lib.quicktx.tx() \
- .pay_to_address(receiver2["base_address"], Amount.ada(3)) \
- .from_address(sender2["base_address"])
-
- result = ccl_lib.quicktx.compose(tx1, tx2) \
- .fee_payer(sender1["base_address"]) \
- .signer_count(2) \
- .build(provider=devkit)
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
- signed = ccl_lib.account.sign_tx(
- sender1["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
- signed = ccl_lib.account.sign_tx(
- sender2["mnemonic"], signed,
- CclLib.TESTNET, 0, 0)
-
- devkit.submit_tx(signed)
- devkit.wait_for_block(3)
-
- assert get_lovelace(devkit, receiver1["base_address"]) == 5_000_000
- assert get_lovelace(devkit, receiver2["base_address"]) == 3_000_000
diff --git a/wrappers/python/tests/test_new_features_integration.py b/wrappers/python/tests/test_new_features_integration.py
deleted file mode 100644
index 7bf5d3a..0000000
--- a/wrappers/python/tests/test_new_features_integration.py
+++ /dev/null
@@ -1,419 +0,0 @@
-"""Integration tests for new QuickTx features with Yaci DevKit.
-
-Tests reference scripts, governance action types, pool ops, treasury donation,
-native script attachment, and unregisterDRep refundAmount.
-
-Requires:
-- Yaci DevKit running on port 10000
-- Native library built: ./gradlew :core:nativeCompile
-
-Run with:
- PYTHONPATH=wrappers/python CCL_LIB_PATH=core/build/native/nativeCompile \
- pytest wrappers/python/tests/test_new_features_integration.py -v
-"""
-import time
-import json
-import pytest
-from ccl._ffi import CclLib, CclError
-from ccl.quicktx import Amount
-from tests.devkit_helper import DevKitHelper
-
-ANCHOR_URL = "https://bit.ly/3zCH2HL"
-ANCHOR_DATA_HASH = "cafef700c0039a2efb056a665b3a8bcd94f8670b88d659f7f3db68340f6f0937"
-ALWAYS_TRUE_PLUTUS_V3 = "46450101002499"
-
-DEVKIT_PROVIDER_URL = "http://localhost:10000/local-cluster/api"
-
-
-@pytest.fixture(scope="module")
-def devkit():
- """Provide a DevKit helper, skip if DevKit is not running."""
- helper = DevKitHelper()
- if not helper.is_available():
- pytest.skip("Yaci DevKit is not running on port 10000")
- helper.reset()
- time.sleep(3)
- return helper
-
-
-@pytest.fixture(scope="module")
-def ccl_lib():
- """Create a shared CclLib instance."""
- lib = CclLib()
- yield lib
- lib.close()
-
-
-def fund_account(ccl_lib, devkit, ada=500):
- """Create and fund a new account."""
- account = ccl_lib.account.create(CclLib.TESTNET)
- devkit.topup(account["base_address"], ada)
- devkit.wait_for_block(2)
- return account
-
-
-def build_sign_submit(ccl_lib, devkit, account, result):
- """Sign and submit a built transaction."""
- signed_tx = ccl_lib.account.sign_tx(
- account["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
- tx_hash = devkit.submit_tx(signed_tx)
- assert tx_hash is not None
- return tx_hash
-
-
-def register_stake(ccl_lib, devkit, account):
- """Register the stake address for an account. Returns the tx result."""
- result = ccl_lib.quicktx.new_tx() \
- .register_stake_address(account["stake_address"]) \
- .from_address(account["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
- build_sign_submit(ccl_lib, devkit, account, result)
- devkit.wait_for_block(3)
- return result
-
-
-# --- Full E2E tests (payment key signing only) ---
-
-
-def test_pay_to_address_with_reference_script(ccl_lib, devkit):
- """Send ADA to address with a PlutusV3 reference script attached."""
- sender = fund_account(ccl_lib, devkit, 150)
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(
- receiver["base_address"], Amount.ada(10),
- script_ref_cbor_hex=ALWAYS_TRUE_PLUTUS_V3,
- script_ref_type="plutus_v3") \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- receiver_utxos = devkit.get_utxos(receiver["base_address"])
- total_lovelace = sum(
- int(a["quantity"])
- for u in receiver_utxos
- for a in u["amount"]
- if a["unit"] == "lovelace"
- )
- assert total_lovelace >= 10_000_000
-
-
-def test_attach_native_script(ccl_lib, devkit):
- """Build a tx with attachNativeScript and submit."""
- sender = fund_account(ccl_lib, devkit, 150)
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- # Get the payment key hash from the sender address
- addr_info = ccl_lib.address.info(sender["base_address"])
- key_hash = addr_info["payment_credential_hash"]
-
- native_script = {"type": "sig", "keyHash": key_hash}
-
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(5)) \
- .attach_native_script(native_script) \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
-
-def test_register_stake_address(ccl_lib, devkit):
- """Register sender's stake address and submit."""
- sender = fund_account(ccl_lib, devkit)
-
- result = ccl_lib.quicktx.new_tx() \
- .register_stake_address(sender["stake_address"]) \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert int(result["fee"]) > 0
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
-
-def test_delegate_voting_power_to_always_abstain(ccl_lib, devkit):
- """Register stake address, then delegate voting power to always_abstain."""
- sender = fund_account(ccl_lib, devkit)
-
- # Step 1: Register stake address
- register_stake(ccl_lib, devkit, sender)
-
- # Step 2: Delegate voting power to always_abstain
- result = ccl_lib.quicktx.new_tx() \
- .delegate_voting_power_to(sender["stake_address"], "abstain") \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
-
-def test_create_proposal_info_action(ccl_lib, devkit):
- """Create an info_action governance proposal and submit."""
- sender = fund_account(ccl_lib, devkit)
- register_stake(ccl_lib, devkit, sender)
-
- result = ccl_lib.quicktx.new_tx() \
- .create_proposal(
- "info_action",
- sender["stake_address"],
- ANCHOR_URL,
- ANCHOR_DATA_HASH) \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert int(result["fee"]) > 0
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
-
-def test_create_proposal_no_confidence(ccl_lib, devkit):
- """Create a no_confidence governance proposal and submit."""
- sender = fund_account(ccl_lib, devkit)
- register_stake(ccl_lib, devkit, sender)
-
- result = ccl_lib.quicktx.new_tx() \
- .create_proposal(
- "no_confidence",
- sender["stake_address"],
- ANCHOR_URL,
- ANCHOR_DATA_HASH) \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
-
-def test_create_proposal_new_constitution(ccl_lib, devkit):
- """Create a new_constitution governance proposal and submit."""
- sender = fund_account(ccl_lib, devkit)
- register_stake(ccl_lib, devkit, sender)
-
- result = ccl_lib.quicktx.new_tx() \
- .create_proposal(
- "new_constitution",
- sender["stake_address"],
- ANCHOR_URL,
- ANCHOR_DATA_HASH,
- constitution_anchor_url=ANCHOR_URL,
- constitution_anchor_data_hash=ANCHOR_DATA_HASH) \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
-
-def test_create_proposal_update_committee(ccl_lib, devkit):
- """Create an update_committee governance proposal and submit."""
- sender = fund_account(ccl_lib, devkit)
- register_stake(ccl_lib, devkit, sender)
-
- # Use a deterministic hash for committee member
- member_hash = "a" * 56
-
- result = ccl_lib.quicktx.new_tx() \
- .create_proposal(
- "update_committee",
- sender["stake_address"],
- ANCHOR_URL,
- ANCHOR_DATA_HASH,
- new_members=[{"hash": member_hash, "type": "key", "epoch": 100}],
- quorum_numerator=2,
- quorum_denominator=3) \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
-
-def test_create_proposal_hard_fork_initiation(ccl_lib, devkit):
- """Create a hard_fork_initiation governance proposal and submit."""
- sender = fund_account(ccl_lib, devkit)
- register_stake(ccl_lib, devkit, sender)
-
- result = ccl_lib.quicktx.new_tx() \
- .create_proposal(
- "hard_fork_initiation",
- sender["stake_address"],
- ANCHOR_URL,
- ANCHOR_DATA_HASH,
- protocol_version_major=10,
- protocol_version_minor=0) \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
-
- build_sign_submit(ccl_lib, devkit, sender, result)
- devkit.wait_for_block(3)
-
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
-
-# --- Build-only tests (need additional key signatures) ---
-
-
-def test_register_drep_build_only(ccl_lib, devkit):
- """Build a registerDRep tx and verify structure (no submit — needs DRep key)."""
- sender = fund_account(ccl_lib, devkit)
-
- drep_key = ccl_lib.gov.drep_key_from_mnemonic(
- sender["mnemonic"], CclLib.TESTNET, 0)
- credential_hash = drep_key["verification_key_hash"]
-
- result = ccl_lib.quicktx.new_tx() \
- .register_drep(
- credential_hash, "key",
- anchor_url=ANCHOR_URL,
- anchor_data_hash=ANCHOR_DATA_HASH) \
- .from_address(sender["base_address"]) \
- .signer_count(2) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
-
-def test_unregister_drep_with_refund_build_only(ccl_lib, devkit):
- """Build an unregisterDRep tx with refundAmount (no submit — needs DRep key)."""
- sender = fund_account(ccl_lib, devkit)
-
- drep_key = ccl_lib.gov.drep_key_from_mnemonic(
- sender["mnemonic"], CclLib.TESTNET, 0)
- credential_hash = drep_key["verification_key_hash"]
-
- result = ccl_lib.quicktx.new_tx() \
- .unregister_drep(
- credential_hash, "key",
- refund_address=sender["base_address"],
- refund_amount=500_000_000) \
- .from_address(sender["base_address"]) \
- .signer_count(2) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
-
-def test_register_pool_build_only(ccl_lib, devkit):
- """Build a registerPool tx and verify structure (no submit — needs pool operator key)."""
- sender = fund_account(ccl_lib, devkit)
-
- # Deterministic hashes for pool operator and VRF
- operator_hash = "ab" * 14 # 28-byte hex
- vrf_key_hash = "cd" * 16 # 32-byte hex
-
- result = ccl_lib.quicktx.new_tx() \
- .register_pool(
- operator=operator_hash,
- vrf_key_hash=vrf_key_hash,
- pledge=500_000_000,
- cost=340_000_000,
- margin_numerator=1,
- margin_denominator=100,
- reward_address=sender["stake_address"],
- pool_owners=[operator_hash]) \
- .from_address(sender["base_address"]) \
- .signer_count(2) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
-
-def test_donate_to_treasury_build_only(ccl_lib, devkit):
- """Build a donateToTreasury tx and verify structure."""
- sender = fund_account(ccl_lib, devkit)
-
- result = ccl_lib.quicktx.new_tx() \
- .donate_to_treasury(
- treasury_value=0,
- donation_amount=5_000_000) \
- .from_address(sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
-
-def test_create_vote_build_only(ccl_lib, devkit):
- """Build a createVote tx (no submit — needs DRep key)."""
- sender = fund_account(ccl_lib, devkit)
-
- drep_key = ccl_lib.gov.drep_key_from_mnemonic(
- sender["mnemonic"], CclLib.TESTNET, 0)
- credential_hash = drep_key["verification_key_hash"]
-
- # Use a fake governance action tx hash
- fake_gov_tx_hash = "ab" * 32
-
- result = ccl_lib.quicktx.new_tx() \
- .create_vote(
- voter_type="drep_key_hash",
- voter_hash=credential_hash,
- gov_action_tx_hash=fake_gov_tx_hash,
- gov_action_index=0,
- vote="yes",
- anchor_url=ANCHOR_URL,
- anchor_data_hash=ANCHOR_DATA_HASH) \
- .from_address(sender["base_address"]) \
- .signer_count(2) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
diff --git a/wrappers/python/tests/test_provider_integration.py b/wrappers/python/tests/test_provider_integration.py
deleted file mode 100644
index 4b3335f..0000000
--- a/wrappers/python/tests/test_provider_integration.py
+++ /dev/null
@@ -1,159 +0,0 @@
-"""Integration tests for Provider pattern with Yaci DevKit.
-
-Requires:
-- Yaci DevKit running on port 10000
-- Native library built: ./gradlew :core:nativeCompile
-
-Run with:
- PYTHONPATH=wrappers/python CCL_LIB_PATH=core/build/native/nativeCompile \
- pytest wrappers/python/tests/test_provider_integration.py -v
-"""
-import time
-import pytest
-from ccl._ffi import CclLib, CclError
-from ccl.quicktx import Amount
-from ccl.provider import YaciDevKitProvider
-
-
-@pytest.fixture(scope="module")
-def provider():
- """Provide a YaciDevKitProvider, skip if DevKit is not running."""
- p = YaciDevKitProvider()
- if not p.is_available():
- pytest.skip("Yaci DevKit is not running on port 10000")
- p.reset()
- time.sleep(3) # wait for devnet reset
- return p
-
-
-@pytest.fixture(scope="module")
-def ccl_lib():
- """Create a shared CclLib instance."""
- lib = CclLib()
- yield lib
- lib.close()
-
-
-@pytest.fixture
-def funded_sender(ccl_lib, provider):
- """Create and fund a sender account."""
- account = ccl_lib.account.create(CclLib.TESTNET)
- provider.topup(account["base_address"], 150)
- provider.wait_for_block(2)
- return account
-
-
-def test_build_with_provider(ccl_lib, provider, funded_sender):
- """Auto-fetch UTXOs + PP via provider, build, sign, submit, verify."""
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- # Build with provider - no manual withUtxos/withProtocolParams needed
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(5)) \
- .from_address(funded_sender["base_address"]) \
- .build(provider=provider)
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
- # Sign
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
-
- # Submit via provider
- tx_hash = provider.submit_tx(signed_tx)
- assert tx_hash is not None
-
- # Verify
- provider.wait_for_block(3)
- receiver_utxos = provider.get_utxos(receiver["base_address"])
- total_lovelace = sum(
- int(a["quantity"])
- for u in receiver_utxos
- for a in u["amount"]
- if a["unit"] == "lovelace"
- )
- assert total_lovelace == 5_000_000
-
-
-def test_provider_with_manual_utxo_override(ccl_lib, provider, funded_sender):
- """withUtxos() should override provider auto-fetch."""
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- # Manually fetch UTXOs
- utxos = provider.get_utxos(funded_sender["base_address"])
-
- # Build with provider but override UTXOs manually
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(3)) \
- .from_address(funded_sender["base_address"]) \
- .with_utxos(utxos) \
- .build(provider=provider)
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
-
- tx_hash = provider.submit_tx(signed_tx)
- assert tx_hash is not None
-
-
-def test_multiple_receivers_with_provider(ccl_lib, provider, funded_sender):
- """Send to two receivers using provider."""
- r1 = ccl_lib.account.create(CclLib.TESTNET)
- r2 = ccl_lib.account.create(CclLib.TESTNET)
-
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(r1["base_address"], Amount.ada(3)) \
- .pay_to_address(r2["base_address"], Amount.ada(2)) \
- .from_address(funded_sender["base_address"]) \
- .build(provider=provider)
-
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
-
- provider.submit_tx(signed_tx)
- provider.wait_for_block(3)
-
- r1_utxos = provider.get_utxos(r1["base_address"])
- r2_utxos = provider.get_utxos(r2["base_address"])
-
- r1_lovelace = sum(int(a["quantity"]) for u in r1_utxos for a in u["amount"] if a["unit"] == "lovelace")
- r2_lovelace = sum(int(a["quantity"]) for u in r2_utxos for a in u["amount"] if a["unit"] == "lovelace")
-
- assert r1_lovelace == 3_000_000
- assert r2_lovelace == 2_000_000
-
-
-def test_metadata_with_provider(ccl_lib, provider, funded_sender):
- """Send ADA with metadata using provider."""
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(2)) \
- .attach_metadata(674, {"msg": ["Hello from Provider"]}) \
- .from_address(funded_sender["base_address"]) \
- .build(provider=provider)
-
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
-
- tx_hash = provider.submit_tx(signed_tx)
- assert tx_hash is not None
-
- provider.wait_for_block(3)
- receiver_utxos = provider.get_utxos(receiver["base_address"])
- total = sum(
- int(a["quantity"])
- for u in receiver_utxos
- for a in u["amount"]
- if a["unit"] == "lovelace"
- )
- assert total == 2_000_000
diff --git a/wrappers/python/tests/test_quicktx.py b/wrappers/python/tests/test_quicktx.py
index 291d4da..623b7a0 100644
--- a/wrappers/python/tests/test_quicktx.py
+++ b/wrappers/python/tests/test_quicktx.py
@@ -1,62 +1,21 @@
-import json
+import pytest
+
from ccl._ffi import CclLib, CclError
-from ccl.quicktx import Amount
-# Protocol params matching CCL's test resource (Blockfrost/Koios format)
+# Minimal protocol parameters (CCL ProtocolParams model).
PROTOCOL_PARAMS = {
- "min_fee_a": 44,
- "min_fee_b": 155381,
- "max_block_size": 65536,
- "max_tx_size": 16384,
- "max_block_header_size": 1100,
- "key_deposit": "2000000",
- "pool_deposit": "500000000",
- "e_max": 18,
- "n_opt": 500,
- "a0": 0.3,
- "rho": 0.003,
- "tau": 0.2,
- "min_utxo": "34482",
- "min_pool_cost": "340000000",
- "price_mem": 0.0577,
- "price_step": 0.0000721,
- "max_tx_ex_mem": "10000000",
- "max_tx_ex_steps": "10000000000",
- "max_block_ex_mem": "50000000",
- "max_block_ex_steps": "40000000000",
- "max_val_size": "5000",
- "collateral_percent": 150,
+ "min_fee_a": 44, "min_fee_b": 155381, "max_tx_size": 16384,
+ "key_deposit": "2000000", "pool_deposit": "500000000",
+ "coins_per_utxo_size": "4310", "max_val_size": "5000",
+ "max_tx_ex_mem": "10000000", "max_tx_ex_steps": "10000000000",
+ "price_mem": 0.0577, "price_step": 0.0000721, "collateral_percent": 150,
"max_collateral_inputs": 3,
- "coins_per_utxo_size": "4310",
- "coins_per_utxo_word": "34482",
- "pvt_motion_no_confidence": 0.51,
- "pvt_committee_normal": 0.51,
- "pvt_committee_no_confidence": 0.51,
- "pvt_hard_fork_initiation": 0.51,
- "dvt_motion_no_confidence": 0.51,
- "dvt_committee_normal": 0.51,
- "dvt_committee_no_confidence": 0.51,
- "dvt_update_to_constitution": 0.51,
- "dvt_hard_fork_initiation": 0.51,
- "dvt_ppnetwork_group": 0.51,
- "dvt_ppeconomic_group": 0.51,
- "dvt_pptechnical_group": 0.51,
- "dvt_ppgov_group": 0.51,
- "dvt_treasury_withdrawal": 0.51,
- "committee_min_size": 0,
- "committee_max_term_length": 200,
- "gov_action_lifetime": 10,
- "gov_action_deposit": 1000000000,
- "drep_deposit": 2000000,
- "drep_activity": 20,
- "min_fee_ref_script_cost_per_byte": 44,
}
FAKE_TX_HASH = "a" * 64
-def _make_utxos(address, lovelace=100_000_000):
- """Create a simple UTXO list for testing."""
+def _utxos(address, lovelace=100_000_000):
return [{
"tx_hash": FAKE_TX_HASH,
"output_index": 0,
@@ -65,443 +24,124 @@ def _make_utxos(address, lovelace=100_000_000):
}]
-def test_simple_ada_payment(ccl):
- """Build a simple ADA payment transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- receiver = ccl.account.create(CclLib.TESTNET)
+def _payment_yaml(from_addr, to_addr, quantity):
+ return f"""
+version: 1.0
+transaction:
+ - tx:
+ from: {from_addr}
+ intents:
+ - type: payment
+ address: {to_addr}
+ amounts:
+ - unit: lovelace
+ quantity: "{quantity}"
+"""
- result = ccl.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(5)) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
- assert "tx_cbor" in result
+def _assert_built(result):
+ assert isinstance(result, dict)
assert len(result["tx_cbor"]) > 0
- assert "tx_hash" in result
assert len(result["tx_hash"]) == 64
- assert "fee" in result
assert int(result["fee"]) > 0
-def test_multiple_payments(ccl):
- """Build a transaction with multiple payment outputs."""
- sender = ccl.account.create(CclLib.TESTNET)
- receiver1 = ccl.account.create(CclLib.TESTNET)
- receiver2 = ccl.account.create(CclLib.TESTNET)
-
- result = ccl.quicktx.new_tx() \
- .pay_to_address(receiver1["base_address"], Amount.ada(5)) \
- .pay_to_address(receiver2["base_address"], Amount.ada(3)) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
-
-def test_with_metadata(ccl):
- """Build a transaction with CIP-20 metadata."""
+def test_simple_payment(ccl):
sender = ccl.account.create(CclLib.TESTNET)
receiver = ccl.account.create(CclLib.TESTNET)
-
- result = ccl.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(2)) \
- .attach_metadata(674, {"msg": ["Hello from Python"]}) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert int(result["fee"]) > 0
+ yaml_str = _payment_yaml(sender["base_address"], receiver["base_address"], "5000000")
+ _assert_built(ccl.quicktx.build(yaml_str, _utxos(sender["base_address"]), PROTOCOL_PARAMS))
-def test_with_validity_interval(ccl):
- """Build a transaction with validity interval."""
+def test_multiple_payments(ccl):
sender = ccl.account.create(CclLib.TESTNET)
- receiver = ccl.account.create(CclLib.TESTNET)
-
- result = ccl.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(2)) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .valid_from(1000) \
- .valid_to(50000) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
-
-
-def test_insufficient_funds(ccl):
- """Should raise error when UTXOs don't have enough funds."""
+ r1 = ccl.account.create(CclLib.TESTNET)
+ r2 = ccl.account.create(CclLib.TESTNET)
+ yaml_str = f"""
+version: 1.0
+transaction:
+ - tx:
+ from: {sender['base_address']}
+ intents:
+ - type: payment
+ address: {r1['base_address']}
+ amounts:
+ - unit: lovelace
+ quantity: "5000000"
+ - type: payment
+ address: {r2['base_address']}
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
+"""
+ _assert_built(ccl.quicktx.build(yaml_str, _utxos(sender["base_address"]), PROTOCOL_PARAMS))
+
+
+def test_variable_substitution(ccl):
sender = ccl.account.create(CclLib.TESTNET)
receiver = ccl.account.create(CclLib.TESTNET)
-
- try:
- ccl.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(200)) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"], lovelace=1_000_000)) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
- assert False, "Should have raised CclError"
- except CclError:
- pass # expected
+ yaml_str = f"""
+version: 1.0
+variables:
+ to: {receiver['base_address']}
+ amount: "4000000"
+transaction:
+ - tx:
+ from: {sender['base_address']}
+ intents:
+ - type: payment
+ address: ${{to}}
+ amounts:
+ - unit: lovelace
+ quantity: ${{amount}}
+"""
+ _assert_built(ccl.quicktx.build(yaml_str, _utxos(sender["base_address"]), PROTOCOL_PARAMS))
-def test_multi_asset_payment(ccl):
- """Build a transaction with native asset payment."""
+def test_insufficient_funds(ccl):
sender = ccl.account.create(CclLib.TESTNET)
receiver = ccl.account.create(CclLib.TESTNET)
-
- policy_id = "a" * 56
- asset_name_hex = "546f6b656e" # "Token"
- unit = policy_id + asset_name_hex
-
- utxos = [{
- "tx_hash": FAKE_TX_HASH,
- "output_index": 0,
- "address": sender["base_address"],
- "amount": [
- {"unit": "lovelace", "quantity": "100000000"},
- {"unit": unit, "quantity": "500"},
- ],
- }]
-
- result = ccl.quicktx.new_tx() \
- .pay_to_address(
- receiver["base_address"],
- Amount.lovelace(2_000_000),
- Amount.asset(unit, 100),
- ) \
- .from_address(sender["base_address"]) \
- .with_utxos(utxos) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_hash"]) == 64
-
-
-def test_compose_two_senders(ccl):
- """Compose two Tx objects from different senders into one transaction."""
- sender1 = ccl.account.create(CclLib.TESTNET)
- sender2 = ccl.account.create(CclLib.TESTNET)
- receiver1 = ccl.account.create(CclLib.TESTNET)
- receiver2 = ccl.account.create(CclLib.TESTNET)
-
- tx1 = ccl.quicktx.tx() \
- .pay_to_address(receiver1["base_address"], Amount.ada(5)) \
- .from_address(sender1["base_address"])
-
- tx2 = ccl.quicktx.tx() \
- .pay_to_address(receiver2["base_address"], Amount.ada(3)) \
- .from_address(sender2["base_address"])
-
- utxos = [
- {
- "tx_hash": FAKE_TX_HASH,
- "output_index": 0,
- "address": sender1["base_address"],
- "amount": [{"unit": "lovelace", "quantity": "100000000"}],
- },
- {
- "tx_hash": "b" * 64,
- "output_index": 0,
- "address": sender2["base_address"],
- "amount": [{"unit": "lovelace", "quantity": "100000000"}],
- },
- ]
-
- result = ccl.quicktx.compose(tx1, tx2) \
- .fee_payer(sender1["base_address"]) \
- .with_utxos(utxos) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .signer_count(2) \
- .build()
-
- assert "tx_cbor" in result
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
-
-def test_compose_missing_fee_payer(ccl):
- """Compose should fail when fee_payer is not set."""
- sender1 = ccl.account.create(CclLib.TESTNET)
- sender2 = ccl.account.create(CclLib.TESTNET)
- receiver1 = ccl.account.create(CclLib.TESTNET)
- receiver2 = ccl.account.create(CclLib.TESTNET)
-
- tx1 = ccl.quicktx.tx() \
- .pay_to_address(receiver1["base_address"], Amount.ada(5)) \
- .from_address(sender1["base_address"])
-
- tx2 = ccl.quicktx.tx() \
- .pay_to_address(receiver2["base_address"], Amount.ada(3)) \
- .from_address(sender2["base_address"])
-
- try:
- ccl.quicktx.compose(tx1, tx2) \
- .with_utxos(_make_utxos(sender1["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
- assert False, "Should have raised CclError"
- except CclError:
- pass # expected
-
-
-# --- Staking ---
-
-def test_register_stake_address(ccl):
- """Build a register stake address transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
-
- result = ccl.quicktx.new_tx() \
- .register_stake_address(sender["base_address"]) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_deregister_stake_address(ccl):
- """Build a deregister stake address transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
-
- result = ccl.quicktx.new_tx() \
- .deregister_stake_address(sender["base_address"]) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_delegate_to(ccl):
- """Build a delegate to pool transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- pool_id = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
-
- result = ccl.quicktx.new_tx() \
- .delegate_to(sender["base_address"], pool_id) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_withdraw(ccl):
- """Build a withdraw rewards transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- info = ccl.account.from_mnemonic(sender["mnemonic"], CclLib.TESTNET)
-
- result = ccl.quicktx.new_tx() \
- .withdraw(info["stake_address"], "5000000") \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-# --- DRep ---
-
-def test_register_drep(ccl):
- """Build a register DRep transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- credential_hash = "ab" * 28
-
- result = ccl.quicktx.new_tx() \
- .register_drep(credential_hash, "key") \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_register_drep_with_anchor(ccl):
- """Build a register DRep with anchor transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- credential_hash = "ab" * 28
- data_hash = "cd" * 32
-
- result = ccl.quicktx.new_tx() \
- .register_drep(credential_hash, "key",
- anchor_url="https://example.com/drep.json",
- anchor_data_hash=data_hash) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_unregister_drep(ccl):
- """Build an unregister DRep transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- credential_hash = "ab" * 28
-
- result = ccl.quicktx.new_tx() \
- .unregister_drep(credential_hash, "key") \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_update_drep(ccl):
- """Build an update DRep transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- credential_hash = "ab" * 28
- data_hash = "cd" * 32
-
- result = ccl.quicktx.new_tx() \
- .update_drep(credential_hash, "key",
- anchor_url="https://example.com/drep-v2.json",
- anchor_data_hash=data_hash) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-# --- Voting ---
-
-def test_delegate_voting_power_to_key_hash(ccl):
- """Build a delegate voting power to key hash transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- drep_hash = "ab" * 28
-
- result = ccl.quicktx.new_tx() \
- .delegate_voting_power_to(sender["base_address"], "key_hash", drep_hash) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_delegate_voting_power_to_abstain(ccl):
- """Build a delegate voting power to abstain transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
-
- result = ccl.quicktx.new_tx() \
- .delegate_voting_power_to(sender["base_address"], "abstain") \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_create_vote(ccl):
- """Build a create vote transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- voter_hash = "ab" * 28
- gov_tx_hash = "cd" * 32
-
- result = ccl.quicktx.new_tx() \
- .create_vote("drep_key_hash", voter_hash, gov_tx_hash, 0, "yes") \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_create_vote_with_anchor(ccl):
- """Build a create vote with anchor transaction."""
- sender = ccl.account.create(CclLib.TESTNET)
- voter_hash = "ab" * 28
- gov_tx_hash = "cd" * 32
- anchor_data_hash = "ef" * 32
-
- result = ccl.quicktx.new_tx() \
- .create_vote("drep_key_hash", voter_hash, gov_tx_hash, 0, "no",
- anchor_url="https://example.com/rationale.json",
- anchor_data_hash=anchor_data_hash) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"])) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-# --- Governance proposals ---
-
-def test_create_info_action_proposal(ccl):
- """Build an info action governance proposal."""
- sender = ccl.account.create(CclLib.TESTNET)
- info = ccl.account.from_mnemonic(sender["mnemonic"], CclLib.TESTNET)
- anchor_data_hash = "ab" * 32
-
- result = ccl.quicktx.new_tx() \
- .create_proposal("info_action", info["stake_address"],
- "https://example.com/proposal.json", anchor_data_hash) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"], lovelace=2_000_000_000)) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_create_treasury_withdrawals_proposal(ccl):
- """Build a treasury withdrawals governance proposal."""
- sender = ccl.account.create(CclLib.TESTNET)
- info = ccl.account.from_mnemonic(sender["mnemonic"], CclLib.TESTNET)
- anchor_data_hash = "ab" * 32
-
- result = ccl.quicktx.new_tx() \
- .create_proposal("treasury_withdrawals", info["stake_address"],
- "https://example.com/proposal.json", anchor_data_hash,
- withdrawals=[{"reward_address": info["stake_address"], "amount": "1000000"}]) \
- .from_address(sender["base_address"]) \
- .with_utxos(_make_utxos(sender["base_address"], lovelace=2_000_000_000)) \
- .with_protocol_params(PROTOCOL_PARAMS) \
- .build()
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
-
-
-def test_amount_helpers():
- """Test Amount helper methods."""
- assert Amount.ada(5) == {"unit": "lovelace", "quantity": "5000000"}
- assert Amount.lovelace(2000000) == {"unit": "lovelace", "quantity": "2000000"}
- assert Amount.asset("abc123", 100) == {"unit": "abc123", "quantity": "100"}
+ yaml_str = _payment_yaml(sender["base_address"], receiver["base_address"], "200000000")
+ with pytest.raises(CclError):
+ ccl.quicktx.build(yaml_str, _utxos(sender["base_address"], 1_000_000), PROTOCOL_PARAMS)
+
+
+# A Plutus mint TxPlan (always-succeeds V2 policy). The script is not executed offline; the
+# caller-supplied execution units are stamped onto the redeemer by the static evaluator.
+MINT_ADDR = ("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8"
+ "ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp")
+MINT_YAML = f"""
+version: 1.0
+context:
+ fee_payer: {MINT_ADDR}
+transaction:
+- tx:
+ from: {MINT_ADDR}
+ intents:
+ - policyId: 793f8c8cffba081b2a56462fc219cc8fe652d6a338b62c7b134876e7
+ type: script_minting
+ assets:
+ - name: TestToken
+ value: 1
+ receiver: {MINT_ADDR}
+ redeemer:
+ int: 0
+ scripts:
+ - type: validator
+ role: mint
+ cbor_hex: 4e4d01000033222220051200120011
+ version: v2
+"""
+
+
+def test_plutus_mint_with_exec_units(ccl):
+ # One redeemer (the mint) -> one ExUnits, supplied by the caller.
+ result = ccl.quicktx.build(
+ MINT_YAML, _utxos(MINT_ADDR), PROTOCOL_PARAMS,
+ exec_units=[{"mem": 2000000, "steps": 500000000}])
+ _assert_built(result)
+
+
+def test_plutus_mint_without_exec_units_fails(ccl):
+ with pytest.raises(CclError):
+ ccl.quicktx.build(MINT_YAML, _utxos(MINT_ADDR), PROTOCOL_PARAMS)
diff --git a/wrappers/python/tests/test_quicktx_integration.py b/wrappers/python/tests/test_quicktx_integration.py
index caab581..2941a27 100644
--- a/wrappers/python/tests/test_quicktx_integration.py
+++ b/wrappers/python/tests/test_quicktx_integration.py
@@ -1,4 +1,4 @@
-"""Integration tests for QuickTx with Yaci DevKit.
+"""Integration tests for QuickTx (TxPlan YAML) with Yaci DevKit.
Requires:
- Yaci DevKit running on port 10000
@@ -9,26 +9,25 @@
pytest wrappers/python/tests/test_quicktx_integration.py -v
"""
import time
+
import pytest
+
from ccl._ffi import CclLib, CclError
-from ccl.quicktx import Amount
from tests.devkit_helper import DevKitHelper
@pytest.fixture(scope="module")
def devkit():
- """Provide a DevKit helper, skip if DevKit is not running."""
helper = DevKitHelper()
if not helper.is_available():
pytest.skip("Yaci DevKit is not running on port 10000")
helper.reset()
- time.sleep(3) # wait for devnet reset
+ time.sleep(3)
return helper
@pytest.fixture(scope="module")
def ccl_lib():
- """Create a shared CclLib instance."""
lib = CclLib()
yield lib
lib.close()
@@ -36,253 +35,97 @@ def ccl_lib():
@pytest.fixture
def funded_sender(ccl_lib, devkit):
- """Create and fund a sender account."""
account = ccl_lib.account.create(CclLib.TESTNET)
devkit.topup(account["base_address"], 150)
devkit.wait_for_block(2)
return account
+def _payment_yaml(from_addr, to_addr, quantity):
+ return f"""
+version: 1.0
+transaction:
+ - tx:
+ from: {from_addr}
+ intents:
+ - type: payment
+ address: {to_addr}
+ amounts:
+ - unit: lovelace
+ quantity: "{quantity}"
+"""
+
+
def test_simple_ada_transfer(ccl_lib, devkit, funded_sender):
- """Send 5 ADA to a new address and verify on-chain."""
+ """Build a 5 ADA payment from TxPlan YAML, sign, submit, and verify on-chain."""
receiver = ccl_lib.account.create(CclLib.TESTNET)
- # Fetch UTXOs and protocol params from DevKit
utxos = devkit.get_utxos(funded_sender["base_address"])
pp = devkit.get_protocol_params()
- # Build transaction
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(5)) \
- .from_address(funded_sender["base_address"]) \
- .with_utxos(utxos) \
- .with_protocol_params(pp) \
- .build()
-
+ yaml_str = _payment_yaml(funded_sender["base_address"], receiver["base_address"], "5000000")
+ result = ccl_lib.quicktx.build(yaml_str, utxos, pp)
assert len(result["tx_cbor"]) > 0
assert len(result["tx_hash"]) == 64
assert int(result["fee"]) > 0
- # Sign
signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
-
- # Submit
+ funded_sender["mnemonic"], result["tx_cbor"], CclLib.TESTNET, 0, 0)
tx_hash = devkit.submit_tx(signed_tx)
- assert tx_hash is not None
+ assert tx_hash
- # Wait and verify
devkit.wait_for_block(3)
receiver_utxos = devkit.get_utxos(receiver["base_address"])
- total_lovelace = sum(
- int(a["quantity"])
- for u in receiver_utxos
- for a in u["amount"]
- if a["unit"] == "lovelace"
- )
- assert total_lovelace == 5_000_000
+ total = sum(int(a["quantity"]) for u in receiver_utxos
+ for a in u["amount"] if a["unit"] == "lovelace")
+ assert total == 5_000_000
def test_multiple_receivers(ccl_lib, devkit, funded_sender):
- """Send to two receivers in one transaction."""
r1 = ccl_lib.account.create(CclLib.TESTNET)
r2 = ccl_lib.account.create(CclLib.TESTNET)
utxos = devkit.get_utxos(funded_sender["base_address"])
pp = devkit.get_protocol_params()
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(r1["base_address"], Amount.ada(3)) \
- .pay_to_address(r2["base_address"], Amount.ada(2)) \
- .from_address(funded_sender["base_address"]) \
- .with_utxos(utxos) \
- .with_protocol_params(pp) \
- .build()
-
+ yaml_str = f"""
+version: 1.0
+transaction:
+ - tx:
+ from: {funded_sender['base_address']}
+ intents:
+ - type: payment
+ address: {r1['base_address']}
+ amounts:
+ - unit: lovelace
+ quantity: "3000000"
+ - type: payment
+ address: {r2['base_address']}
+ amounts:
+ - unit: lovelace
+ quantity: "2000000"
+"""
+ result = ccl_lib.quicktx.build(yaml_str, utxos, pp)
signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
+ funded_sender["mnemonic"], result["tx_cbor"], CclLib.TESTNET, 0, 0)
+ assert devkit.submit_tx(signed_tx)
- devkit.submit_tx(signed_tx)
devkit.wait_for_block(3)
-
r1_utxos = devkit.get_utxos(r1["base_address"])
- r2_utxos = devkit.get_utxos(r2["base_address"])
-
- r1_lovelace = sum(int(a["quantity"]) for u in r1_utxos for a in u["amount"] if a["unit"] == "lovelace")
- r2_lovelace = sum(int(a["quantity"]) for u in r2_utxos for a in u["amount"] if a["unit"] == "lovelace")
-
- assert r1_lovelace == 3_000_000
- assert r2_lovelace == 2_000_000
-
-
-def test_metadata_transfer(ccl_lib, devkit, funded_sender):
- """Send ADA with CIP-20 metadata and verify."""
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- utxos = devkit.get_utxos(funded_sender["base_address"])
- pp = devkit.get_protocol_params()
-
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(2)) \
- .attach_metadata(674, {"msg": ["Hello from CCL Bridge"]}) \
- .from_address(funded_sender["base_address"]) \
- .with_utxos(utxos) \
- .with_protocol_params(pp) \
- .build()
-
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
-
- tx_hash = devkit.submit_tx(signed_tx)
- devkit.wait_for_block(3)
-
- # Verify tx exists on-chain
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
+ total = sum(int(a["quantity"]) for u in r1_utxos
+ for a in u["amount"] if a["unit"] == "lovelace")
+ assert total == 3_000_000
-def test_insufficient_funds_error(ccl_lib, devkit):
- """Should fail when UTXOs don't have enough funds."""
+def test_insufficient_funds(ccl_lib, devkit):
sender = ccl_lib.account.create(CclLib.TESTNET)
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- # Fund with only 2 ADA
devkit.topup(sender["base_address"], 2)
devkit.wait_for_block(2)
+ receiver = ccl_lib.account.create(CclLib.TESTNET)
utxos = devkit.get_utxos(sender["base_address"])
pp = devkit.get_protocol_params()
+ yaml_str = _payment_yaml(sender["base_address"], receiver["base_address"], "100000000")
with pytest.raises(CclError):
- ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(100)) \
- .from_address(sender["base_address"]) \
- .with_utxos(utxos) \
- .with_protocol_params(pp) \
- .build()
-
-
-def test_round_trip_sign_submit(ccl_lib, devkit, funded_sender):
- """Full round trip: build -> sign -> submit -> confirm -> check balance."""
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- utxos = devkit.get_utxos(funded_sender["base_address"])
- pp = devkit.get_protocol_params()
-
- # Build
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(10)) \
- .from_address(funded_sender["base_address"]) \
- .with_utxos(utxos) \
- .with_protocol_params(pp) \
- .build()
-
- # Sign
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
-
- # Submit
- tx_hash = devkit.submit_tx(signed_tx)
- devkit.wait_for_block(3)
-
- # Confirm on-chain
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
-
- # Check receiver balance
- receiver_utxos = devkit.get_utxos(receiver["base_address"])
- total = sum(
- int(a["quantity"])
- for u in receiver_utxos
- for a in u["amount"]
- if a["unit"] == "lovelace"
- )
- assert total == 10_000_000
-
-
-# --- Provider Config (server-side lazy UTXO fetching) tests ---
-
-DEVKIT_PROVIDER_URL = "http://localhost:10000/local-cluster/api"
-
-
-def test_provider_config_simple_transfer(ccl_lib, devkit, funded_sender):
- """Build with provider_config — Java fetches UTXOs lazily via HTTP."""
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(5)) \
- .from_address(funded_sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- assert len(result["tx_cbor"]) > 0
- assert len(result["tx_hash"]) == 64
- assert int(result["fee"]) > 0
-
- # Sign and submit
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
- tx_hash = devkit.submit_tx(signed_tx)
- assert tx_hash is not None
-
- devkit.wait_for_block(3)
- receiver_utxos = devkit.get_utxos(receiver["base_address"])
- total = sum(
- int(a["quantity"])
- for u in receiver_utxos
- for a in u["amount"]
- if a["unit"] == "lovelace"
- )
- assert total == 5_000_000
-
-
-def test_provider_config_multiple_receivers(ccl_lib, devkit, funded_sender):
- """Build with provider_config and multiple payment outputs."""
- r1 = ccl_lib.account.create(CclLib.TESTNET)
- r2 = ccl_lib.account.create(CclLib.TESTNET)
-
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(r1["base_address"], Amount.ada(3)) \
- .pay_to_address(r2["base_address"], Amount.ada(2)) \
- .from_address(funded_sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
- devkit.submit_tx(signed_tx)
- devkit.wait_for_block(3)
-
- r1_utxos = devkit.get_utxos(r1["base_address"])
- r2_utxos = devkit.get_utxos(r2["base_address"])
-
- r1_lovelace = sum(int(a["quantity"]) for u in r1_utxos for a in u["amount"] if a["unit"] == "lovelace")
- r2_lovelace = sum(int(a["quantity"]) for u in r2_utxos for a in u["amount"] if a["unit"] == "lovelace")
-
- assert r1_lovelace == 3_000_000
- assert r2_lovelace == 2_000_000
-
-
-def test_provider_config_with_metadata(ccl_lib, devkit, funded_sender):
- """Build with provider_config and metadata attachment."""
- receiver = ccl_lib.account.create(CclLib.TESTNET)
-
- result = ccl_lib.quicktx.new_tx() \
- .pay_to_address(receiver["base_address"], Amount.ada(2)) \
- .attach_metadata(674, {"msg": ["Hello from providerConfig"]}) \
- .from_address(funded_sender["base_address"]) \
- .build(provider_config={"name": "yaci", "url": DEVKIT_PROVIDER_URL})
-
- signed_tx = ccl_lib.account.sign_tx(
- funded_sender["mnemonic"], result["tx_cbor"],
- CclLib.TESTNET, 0, 0)
- tx_hash = devkit.submit_tx(signed_tx)
- assert tx_hash is not None
-
- devkit.wait_for_block(3)
- tx_info = devkit.get_tx(result["tx_hash"])
- assert tx_info is not None
+ ccl_lib.quicktx.build(yaml_str, utxos, pp)
diff --git a/wrappers/python/tests/test_quicktx_intents.py b/wrappers/python/tests/test_quicktx_intents.py
new file mode 100644
index 0000000..5b4e6d6
--- /dev/null
+++ b/wrappers/python/tests/test_quicktx_intents.py
@@ -0,0 +1,97 @@
+"""End-to-end coverage of every TxPlan intent through the native library.
+
+The fixtures in ``test-fixtures/quicktx-intents/`` are generated by the JVM QuickTxIntentsTest via
+``TxPlan.from(tx).toYaml()`` (CCL's exact intent shapes); here each one is built through the native
+library via the Python wrapper. Mirrors the Go ``intents_test.go`` for cross-wrapper parity.
+"""
+import glob
+from pathlib import Path
+
+import pytest
+
+from ccl._ffi import CclLib, CclError
+
+FIXTURES = Path(__file__).resolve().parents[3] / "test-fixtures" / "quicktx-intents"
+
+# The fee_payer baked into the fixtures (derived from the core test mnemonic), and the deterministic
+# always-succeeds-V2 script address + datum hash used by the Plutus spend fixture.
+SENDER = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp"
+SENDER2 = "addr_test1qz7svwszky8gcmhrfza7a89z9u0dfzd3l7h23sqlc5yml7ejcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqcqrvr0"
+SCRIPT_ADDR = "addr_test1wpunlryvl7aqsxe22erzlsseej87v5kk5vutvtrmzdy8dect48z0w"
+SCRIPT_DATUM_HASH = "9e1199a988ba72ffd6e9c269cadb3b53b5f360ff99f112d9b2ee30c4d74ad88b"
+SCRIPT_TX_HASH = "b" * 64
+EXEC_UNITS = [{"mem": 2000000, "steps": 500000000}]
+
+# Protocol parameters incl. the Conway deposits the governance/staking/pool intents need.
+PROTOCOL_PARAMS = {
+ "min_fee_a": 44, "min_fee_b": 155381, "max_tx_size": 16384, "max_val_size": "5000",
+ "key_deposit": "2000000", "pool_deposit": "500000000", "drep_deposit": "2000000",
+ "gov_action_deposit": "1000000000", "coins_per_utxo_size": "4310",
+ "max_tx_ex_mem": "14000000", "max_tx_ex_steps": "10000000000",
+ "price_mem": 0.0577, "price_step": 0.0000721, "collateral_percent": 150,
+ "max_collateral_inputs": 3, "min_fee_ref_script_cost_per_byte": 15,
+}
+
+
+def _utxos():
+ # A 2000-ADA UTXO (covers the gov-action deposit) + a small one for the reference-input fixture.
+ return [
+ {"tx_hash": "a" * 64, "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]},
+ {"tx_hash": "c" * 64, "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "5000000"}]},
+ {"tx_hash": "a" * 64, "output_index": 1, "address": SENDER2,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]},
+ ]
+
+
+def _assert_built(result):
+ assert len(result["tx_cbor"]) > 0
+ assert len(result["tx_hash"]) == 64
+ assert int(result["fee"]) > 0
+
+
+@pytest.mark.parametrize("fixture", sorted(glob.glob(str(FIXTURES / "*.yaml"))),
+ ids=lambda p: Path(p).stem)
+def test_intent_builds(ccl, fixture):
+ yaml = Path(fixture).read_text()
+ _assert_built(ccl.quicktx.build(yaml, _utxos(), PROTOCOL_PARAMS))
+
+
+# The mnemonic the fixtures are derived from (account 0/0 == SENDER).
+INTENT_MNEMONIC = "test walk nut penalty hip pave soap entry language right filter choice"
+
+
+def test_sign_with_stake_key(ccl):
+ # A stake registration must be witnessed by the stake key too; sign_tx_with_keys adds it.
+ yaml = (FIXTURES / "stake_registration.yaml").read_text()
+ utxos = [{"tx_hash": "a" * 64, "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}]
+ built = ccl.quicktx.build(yaml, utxos, PROTOCOL_PARAMS)
+
+ signed_payment = ccl.account.sign_tx(INTENT_MNEMONIC, built["tx_cbor"], CclLib.TESTNET, 0, 0)
+ signed_stake = ccl.account.sign_tx_with_keys(
+ INTENT_MNEMONIC, built["tx_cbor"], ["payment", "stake"], CclLib.TESTNET, 0, 0)
+ assert len(signed_stake) > len(signed_payment)
+
+
+def test_plutus_mint(ccl):
+ yaml = (FIXTURES / "plutus" / "script_minting.yaml").read_text()
+ utxos = [{"tx_hash": "a" * 64, "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}]
+ _assert_built(ccl.quicktx.build(yaml, utxos, PROTOCOL_PARAMS, exec_units=EXEC_UNITS))
+ with pytest.raises(CclError):
+ ccl.quicktx.build(yaml, utxos, PROTOCOL_PARAMS)
+
+
+def test_plutus_spend(ccl):
+ yaml = (FIXTURES / "plutus" / "script_collect_from.yaml").read_text()
+ utxos = [
+ {"tx_hash": SCRIPT_TX_HASH, "output_index": 0, "address": SCRIPT_ADDR,
+ "amount": [{"unit": "lovelace", "quantity": "10000000"}], "data_hash": SCRIPT_DATUM_HASH},
+ {"tx_hash": "a" * 64, "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]},
+ ]
+ _assert_built(ccl.quicktx.build(yaml, utxos, PROTOCOL_PARAMS, exec_units=EXEC_UNITS))
+ with pytest.raises(CclError):
+ ccl.quicktx.build(yaml, utxos, PROTOCOL_PARAMS)
diff --git a/wrappers/rust/Cargo.lock b/wrappers/rust/Cargo.lock
index 37bf4d9..dc211ad 100644
--- a/wrappers/rust/Cargo.lock
+++ b/wrappers/rust/Cargo.lock
@@ -30,6 +30,7 @@ version = "0.1.0"
dependencies = [
"serde",
"serde_json",
+ "serde_yaml",
"ureq",
]
@@ -59,6 +60,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -95,6 +102,12 @@ dependencies = [
"wasi",
]
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
[[package]]
name = "icu_collections"
version = "2.1.1"
@@ -197,6 +210,16 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
[[package]]
name = "itoa"
version = "1.0.17"
@@ -325,6 +348,12 @@ dependencies = [
"untrusted",
]
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -368,6 +397,19 @@ dependencies = [
"zmij",
]
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -436,6 +478,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
[[package]]
name = "untrusted"
version = "0.9.0"
diff --git a/wrappers/rust/Cargo.toml b/wrappers/rust/Cargo.toml
index 85c6102..aa3dd82 100644
--- a/wrappers/rust/Cargo.toml
+++ b/wrappers/rust/Cargo.toml
@@ -7,6 +7,7 @@ description = "Rust bindings for Cardano Client Lib (CCL) via GraalVM native lib
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+serde_yaml = "0.9"
[dev-dependencies]
ureq = { version = "2", features = ["json"] }
diff --git a/wrappers/rust/README.md b/wrappers/rust/README.md
new file mode 100644
index 0000000..fb92e42
--- /dev/null
+++ b/wrappers/rust/README.md
@@ -0,0 +1,84 @@
+# CCL Bridge — Rust
+
+Rust bindings for [Cardano Client Lib](https://github.com/bloxbean/cardano-client-lib)
+via the CCL Bridge native library.
+
+> Part of the [CCL Bridge](../../README.md) project. See the
+> [top-level README](../../README.md) for the full API reference and
+> [`docs/quicktx.md`](../../docs/quicktx.md) for transaction building.
+
+## Requirements
+
+- Rust (stable, 2021 edition).
+- The native library `libccl.{dylib,so,dll}` for your platform.
+
+## Getting the native library
+
+`build.rs` links against `libccl`, looking in `CCL_LIB_PATH` (default:
+`../../core/build/native/nativeCompile`, relative to this crate). Build or download it
+there first. From the repo root:
+
+```bash
+./gradlew :core:nativeCompile # build from source (needs Oracle GraalVM 25.0.3)
+# or:
+make download-lib # download a pre-built binary
+```
+
+At **runtime** the OS loader also needs the library via `DYLD_LIBRARY_PATH` (macOS) /
+`LD_LIBRARY_PATH` (Linux).
+
+## Running the examples
+
+From `wrappers/rust`:
+
+```bash
+LIB_DIR=../../core/build/native/nativeCompile
+
+CCL_LIB_PATH=$LIB_DIR DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+ cargo run --example account
+```
+
+The [`examples/`](examples/) directory contains:
+
+| `--example` | What it shows |
+|-------------|---------------|
+| [`account`](examples/account.rs) | Create an account, restore from mnemonic, derive keys and a DRep ID |
+| [`primitives`](examples/primitives.rs) | Mnemonics, Blake2b hashing, Ed25519 signing, address parsing/validation |
+| [`transaction`](examples/transaction.rs) | Build an unsigned payment **offline** (QuickTx) and sign it — no node/DevKit needed |
+
+## Quick start
+
+```rust
+use ccl::{Bridge, network};
+
+fn main() -> Result<(), Box> {
+ let bridge = Bridge::new()?; // loads libccl, starts a GraalVM isolate
+
+ // API methods return JSON strings; parse with serde_json.
+ let account = bridge.account().create(network::TESTNET)?;
+ let json: serde_json::Value = serde_json::from_str(&account)?;
+ println!("{}", json["base_address"]); // addr_test1...
+ println!("{}", json["mnemonic"]); // 24-word phrase
+ Ok(())
+} // Bridge's Drop tears down the isolate
+```
+
+## API surface
+
+A `Bridge` exposes namespaced accessors (all offline operations):
+`bridge.account()`, `.address()`, `.crypto()`, `.tx()`, `.plutus()`, `.script()`,
+`.gov()`, `.wallet()`, `.quicktx()`.
+
+Most methods return `Result` where the `String` is JSON — parse it with
+`serde_json`.
+
+Transactions are defined as a [TxPlan](https://github.com/bloxbean/cardano-client-lib)
+**YAML** document and built fully offline — you supply the UTXOs and protocol parameters
+(as `serde_json::Value`):
+
+```rust
+let result = bridge.quicktx().build(&yaml, &utxos, &protocol_params)?; // -> TxResult { tx_cbor, tx_hash, fee }
+```
+
+Network IDs: `network::MAINNET` (0), `network::TESTNET` (1), `network::PREPROD` (2),
+`network::PREVIEW` (3). Errors are `ccl::CclError`.
diff --git a/wrappers/rust/examples/account.rs b/wrappers/rust/examples/account.rs
new file mode 100644
index 0000000..93a7a3e
--- /dev/null
+++ b/wrappers/rust/examples/account.rs
@@ -0,0 +1,40 @@
+//! Account creation and key derivation (offline).
+//!
+//! Run from wrappers/rust:
+//!
+//! ```text
+//! LIB_DIR=../../core/build/native/nativeCompile
+//! CCL_LIB_PATH=$LIB_DIR DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+//! cargo run --example account
+//! ```
+use ccl::{network, Bridge};
+
+fn main() -> Result<(), Box> {
+ let bridge = Bridge::new()?;
+
+ // 1. Create a brand-new testnet account (random mnemonic). Methods return JSON.
+ let created = bridge.account().create(network::TESTNET)?;
+ let account: serde_json::Value = serde_json::from_str(&created)?;
+ let mnemonic = account["mnemonic"].as_str().unwrap();
+ let base_address = account["base_address"].as_str().unwrap();
+ println!("Created account");
+ println!(" base address: {}", base_address);
+ println!(" mnemonic : {}", mnemonic);
+
+ // 2. Restore the same account from its mnemonic — the address must match.
+ let restored = bridge.account().from_mnemonic(mnemonic, network::TESTNET, 0, 0)?;
+ let restored: serde_json::Value = serde_json::from_str(&restored)?;
+ assert_eq!(restored["base_address"].as_str().unwrap(), base_address);
+ println!("Restored from mnemonic — address matches: {}", base_address);
+
+ // 3. Derive keys.
+ let priv_key = bridge.account().get_private_key(mnemonic, network::TESTNET, 0, 0)?;
+ let pub_key = bridge.account().get_public_key(mnemonic, network::TESTNET, 0, 0)?;
+ println!(" private key (extended, hex): {}", priv_key);
+ println!(" public key (hex) : {}", pub_key);
+
+ // 4. Derive the governance DRep ID.
+ let drep_id = bridge.account().get_drep_id(mnemonic, network::TESTNET, 0)?;
+ println!(" DRep ID: {}", drep_id);
+ Ok(())
+}
diff --git a/wrappers/rust/examples/primitives.rs b/wrappers/rust/examples/primitives.rs
new file mode 100644
index 0000000..761f23d
--- /dev/null
+++ b/wrappers/rust/examples/primitives.rs
@@ -0,0 +1,56 @@
+//! Crypto and address primitives (offline).
+//!
+//! Run from wrappers/rust:
+//!
+//! ```text
+//! LIB_DIR=../../core/build/native/nativeCompile
+//! CCL_LIB_PATH=$LIB_DIR DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+//! cargo run --example primitives
+//! ```
+use ccl::{network, Bridge};
+
+fn main() -> Result<(), Box> {
+ let bridge = Bridge::new()?;
+
+ // --- Mnemonics ---
+ let mnemonic = bridge.crypto().generate_mnemonic(24)?;
+ println!("Generated 24-word mnemonic: {}", mnemonic);
+ println!(" valid? {}", bridge.crypto().validate_mnemonic(&mnemonic));
+ println!(
+ " 'not a real mnemonic' valid? {}",
+ bridge.crypto().validate_mnemonic("not a real mnemonic")
+ );
+
+ // --- Blake2b hashing (hex in -> hex out). "Hello" == 48656c6c6f ---
+ println!("Blake2b-256('Hello'): {}", bridge.crypto().blake2b_256("48656c6c6f")?);
+ println!("Blake2b-224('Hello'): {}", bridge.crypto().blake2b_224("48656c6c6f")?);
+
+ // --- Ed25519 signing ---
+ // get_private_key returns the 64-byte extended key; sign expects a 32-byte
+ // Ed25519 key, so take the first 32 bytes (64 hex chars).
+ let created = bridge.account().create(network::TESTNET)?;
+ let account: serde_json::Value = serde_json::from_str(&created)?;
+ let mnemonic = account["mnemonic"].as_str().unwrap();
+ let priv_ext = bridge.account().get_private_key(mnemonic, network::TESTNET, 0, 0)?;
+ let pub_key = bridge.account().get_public_key(mnemonic, network::TESTNET, 0, 0)?;
+ let message_hex = "68656c6c6f"; // "hello"
+ let signature = bridge.crypto().sign(message_hex, &priv_ext[..64])?;
+ println!("Ed25519 signature: {}", signature);
+ // A tampered signature is correctly rejected.
+ let fake_sig = "00".repeat(64);
+ println!(
+ " verify(fake signature) -> {}",
+ bridge.crypto().verify(&fake_sig, message_hex, &pub_key)
+ );
+
+ // --- Address parsing & validation ---
+ let addr = account["base_address"].as_str().unwrap();
+ println!("Address valid? {}", bridge.address().validate(addr));
+ println!("Address info : {}", bridge.address().info(addr)?);
+ let raw = bridge.address().to_bytes(addr)?;
+ println!(
+ "Address -> bytes -> address round-trips: {}",
+ bridge.address().from_bytes(&raw)? == addr
+ );
+ Ok(())
+}
diff --git a/wrappers/rust/examples/transaction.rs b/wrappers/rust/examples/transaction.rs
new file mode 100644
index 0000000..468bfc4
--- /dev/null
+++ b/wrappers/rust/examples/transaction.rs
@@ -0,0 +1,74 @@
+//! Build and sign a payment transaction fully offline from a TxPlan (YAML).
+//!
+//! The transaction is defined as a TxPlan YAML document; we supply the UTXOs and protocol
+//! parameters ourselves (no node / no provider), build the unsigned CBOR, then sign it locally.
+//! Submitting it is a separate, online step.
+//!
+//! Run from wrappers/rust:
+//!
+//! ```text
+//! LIB_DIR=../../core/build/native/nativeCompile
+//! CCL_LIB_PATH=$LIB_DIR DYLD_LIBRARY_PATH=$LIB_DIR LD_LIBRARY_PATH=$LIB_DIR \
+//! cargo run --example transaction
+//! ```
+use ccl::{network, Bridge};
+use serde_json::json;
+
+fn main() -> Result<(), Box> {
+ let bridge = Bridge::new()?;
+
+ let sender: serde_json::Value =
+ serde_json::from_str(&bridge.account().create(network::TESTNET)?)?;
+ let receiver: serde_json::Value =
+ serde_json::from_str(&bridge.account().create(network::TESTNET)?)?;
+ let sender_addr = sender["base_address"].as_str().unwrap();
+ let receiver_addr = receiver["base_address"].as_str().unwrap();
+
+ // Minimal protocol parameters (CCL test-resource values).
+ let protocol_params = json!({
+ "min_fee_a": 44, "min_fee_b": 155381, "max_tx_size": 16384,
+ "key_deposit": "2000000", "pool_deposit": "500000000",
+ "coins_per_utxo_size": "4310", "max_val_size": "5000",
+ "max_tx_ex_mem": "10000000", "max_tx_ex_steps": "10000000000",
+ "price_mem": 0.0577, "price_step": 0.0000721, "collateral_percent": 150,
+ "max_collateral_inputs": 3
+ });
+
+ // A static UTXO the sender controls (100 ADA), instead of querying a node.
+ let utxos = json!([{
+ "tx_hash": "a".repeat(64),
+ "output_index": 0,
+ "address": sender_addr,
+ "amount": [{"unit": "lovelace", "quantity": "100000000"}]
+ }]);
+
+ // Define the transaction as a TxPlan YAML document: pay 5 ADA to the receiver.
+ let yaml = format!(
+ "version: 1.0\n\
+ transaction:\n\
+ \x20 - tx:\n\
+ \x20 from: {sender_addr}\n\
+ \x20 intents:\n\
+ \x20 - type: payment\n\
+ \x20 address: {receiver_addr}\n\
+ \x20 amounts:\n\
+ \x20 - unit: lovelace\n\
+ \x20 quantity: \"5000000\"\n"
+ );
+
+ // Build the unsigned transaction offline.
+ let result = bridge.quicktx().build(&yaml, &utxos, &protocol_params, None)?;
+ println!("Built unsigned transaction from TxPlan YAML");
+ println!(" tx hash: {}", result.tx_hash);
+ println!(" fee : {}", result.fee);
+ println!(" cbor : {}...", &result.tx_cbor[..80]);
+
+ // Sign it with the sender's mnemonic.
+ let mnemonic = sender["mnemonic"].as_str().unwrap();
+ let signed = bridge
+ .account()
+ .sign_tx(mnemonic, network::TESTNET, 0, 0, &result.tx_cbor)?;
+ println!("Signed transaction cbor: {}...", &signed[..80]);
+ println!("\nNext step (not shown): submit `signed` to a Cardano node over HTTP.");
+ Ok(())
+}
diff --git a/wrappers/rust/src/ffi.rs b/wrappers/rust/src/ffi.rs
index 2146c4e..e68c177 100644
--- a/wrappers/rust/src/ffi.rs
+++ b/wrappers/rust/src/ffi.rs
@@ -59,6 +59,15 @@ extern "C" {
address_index: c_int,
tx_cbor_hex: *const c_char,
) -> c_int;
+ pub fn ccl_account_sign_tx_multi(
+ thread: *mut graal_isolatethread_t,
+ mnemonic: *const c_char,
+ network_id: c_int,
+ account_index: c_int,
+ address_index: c_int,
+ tx_cbor_hex: *const c_char,
+ keys: *const c_char,
+ ) -> c_int;
pub fn ccl_account_get_drep_id(
thread: *mut graal_isolatethread_t,
mnemonic: *const c_char,
@@ -184,6 +193,9 @@ extern "C" {
// QuickTx API
pub fn ccl_quicktx_build(
thread: *mut graal_isolatethread_t,
- spec_json: *const c_char,
+ yaml: *const c_char,
+ utxos_json: *const c_char,
+ protocol_params_json: *const c_char,
+ exec_units_json: *const c_char,
) -> c_int;
}
diff --git a/wrappers/rust/src/lib.rs b/wrappers/rust/src/lib.rs
index 69011c7..9eb1c26 100644
--- a/wrappers/rust/src/lib.rs
+++ b/wrappers/rust/src/lib.rs
@@ -1,12 +1,10 @@
mod ffi;
-use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
-use serde::Serialize;
-use serde_json::{json, Value};
+use serde_json::Value;
pub use ffi::*;
@@ -281,6 +279,36 @@ impl<'a> AccountApi<'a> {
self.bridge.check(rc)
}
+ /// Sign a transaction with one or more of the account's keys, selected by role (any of
+ /// `payment`, `stake`, `drep`, `committee_cold`, `committee_hot`, applied in order). Use this
+ /// for transactions whose certificates also need the stake or DRep key — stake
+ /// registration/delegation/withdrawal and DRep/vote operations.
+ pub fn sign_tx_with_keys(
+ &self,
+ mnemonic: &str,
+ network_id: i32,
+ account_index: i32,
+ address_index: i32,
+ tx_cbor_hex: &str,
+ keys: &[&str],
+ ) -> Result {
+ let cs_mnemonic = to_cstring(mnemonic)?;
+ let cs_tx = to_cstring(tx_cbor_hex)?;
+ let cs_keys = to_cstring(&keys.join(","))?;
+ let rc = unsafe {
+ ffi::ccl_account_sign_tx_multi(
+ self.bridge.thread,
+ cs_mnemonic.as_ptr(),
+ network_id,
+ account_index,
+ address_index,
+ cs_tx.as_ptr(),
+ cs_keys.as_ptr(),
+ )
+ };
+ self.bridge.check(rc)
+ }
+
pub fn get_drep_id(
&self,
mnemonic: &str,
@@ -575,2146 +603,69 @@ pub struct TxResult {
pub fee: String,
}
-/// Helper for creating amount values.
-pub struct Amount;
-
-impl Amount {
- pub fn lovelace(quantity: u64) -> Value {
- json!({"unit": "lovelace", "quantity": quantity.to_string()})
- }
-
- pub fn ada(ada: f64) -> Value {
- json!({"unit": "lovelace", "quantity": ((ada * 1_000_000.0) as u64).to_string()})
- }
-
- pub fn asset(unit: &str, quantity: u64) -> Value {
- json!({"unit": unit, "quantity": quantity.to_string()})
- }
-}
-
-/// Asset to mint.
-#[derive(Serialize)]
-pub struct MintAsset {
- pub name: String,
- pub quantity: String,
-}
-
-/// Provider configuration for Java-side lazy fetching.
-#[derive(Serialize)]
-pub struct ProviderConfig {
- pub name: String,
- pub url: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub api_key: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub enable_cost_evaluation: Option,
-}
-
-/// Withdrawal entry for treasury proposals.
-#[derive(Serialize)]
-pub struct ProposalWithdrawal {
- pub reward_address: String,
- pub amount: String,
-}
-
/// QuickTx namespace API.
pub struct QuickTxApi<'a> {
bridge: &'a Bridge,
}
impl<'a> QuickTxApi<'a> {
- /// Create a new TxBuilder for building a single transaction.
- pub fn new_tx(&self) -> TxBuilder<'a> {
- TxBuilder {
- bridge: self.bridge,
- operations: Vec::new(),
- from: None,
- change_address: None,
- fee_payer: None,
- utxos: None,
- protocol_params: None,
- validity: serde_json::Map::new(),
- merge_outputs: None,
- signer_count: 1,
- }
- }
-
- /// Create a new ScriptTxBuilder for building a single script transaction.
- pub fn new_script_tx(&self) -> ScriptTxBuilder<'a> {
- ScriptTxBuilder {
- bridge: self.bridge,
- operations: Vec::new(),
- from: None,
- change_address: None,
- fee_payer: None,
- utxos: None,
- protocol_params: None,
- validity: serde_json::Map::new(),
- merge_outputs: None,
- signer_count: 1,
- change_datum_cbor_hex: None,
- change_datum_hash: None,
- }
- }
-
- /// Create a new Tx for use with compose().
- pub fn tx(&self) -> Tx {
- Tx {
- operations: Vec::new(),
- from: None,
- change_address: None,
- }
- }
-
- /// Create a new ScriptTx for use with compose().
- pub fn script_tx(&self) -> ScriptTx {
- ScriptTx {
- operations: Vec::new(),
- from: None,
- change_address: None,
- change_datum_cbor_hex: None,
- change_datum_hash: None,
- }
- }
-
- /// Compose multiple Tx objects into a single transaction.
- pub fn compose(&self, txs: Vec) -> ComposeTxBuilder<'a> {
- ComposeTxBuilder {
- bridge: self.bridge,
- txs,
- fee_payer: None,
- utxos: None,
- protocol_params: None,
- validity: serde_json::Map::new(),
- merge_outputs: None,
- signer_count: None,
- }
- }
-}
-
-/// Builder for a single transaction spec.
-pub struct TxBuilder<'a> {
- bridge: &'a Bridge,
- operations: Vec,
- from: Option,
- change_address: Option,
- fee_payer: Option,
- utxos: Option,
- protocol_params: Option,
- validity: serde_json::Map,
- merge_outputs: Option,
- signer_count: i32,
-}
-
-impl<'a> TxBuilder<'a> {
- pub fn pay_to_address(
- &mut self,
- address: &str,
- amounts: &[Value],
- script_ref_cbor_hex: Option<&str>,
- script_ref_type: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "pay_to_address",
- "address": address,
- "amounts": amounts,
- });
- if let Some(s) = script_ref_cbor_hex {
- op["script_ref_cbor_hex"] = json!(s);
- }
- if let Some(t) = script_ref_type {
- op["script_ref_type"] = json!(t);
- }
- self.operations.push(op);
- self
- }
-
- pub fn pay_to_contract(
- &mut self,
- address: &str,
- amounts: &[Value],
- datum_cbor_hex: Option<&str>,
- datum_hash: Option<&str>,
- script_ref_cbor_hex: Option<&str>,
- script_ref_type: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts,
- });
- if let Some(d) = datum_cbor_hex {
- op["datum_cbor_hex"] = json!(d);
- }
- if let Some(h) = datum_hash {
- op["datum_hash"] = json!(h);
- }
- if let Some(s) = script_ref_cbor_hex {
- op["script_ref_cbor_hex"] = json!(s);
- }
- if let Some(t) = script_ref_type {
- op["script_ref_type"] = json!(t);
- }
- self.operations.push(op);
- self
- }
-
- pub fn mint_assets(
- &mut self,
- script_json: &str,
- assets: &[MintAsset],
- receiver: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "mint_assets",
- "script_json": script_json,
- "assets": assets,
- "receiver": receiver,
- }));
- self
- }
-
- pub fn attach_metadata(&mut self, label: u64, metadata: Value) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- }));
- self
- }
-
- pub fn collect_from(&mut self, utxos: &[Value]) -> &mut Self {
- self.operations.push(json!({
- "type": "collect_from",
- "collect_utxos": utxos,
- }));
- self
- }
-
- // Staking
-
- pub fn register_stake_address(&mut self, address: &str) -> &mut Self {
- self.operations
- .push(json!({"type": "register_stake_address", "address": address}));
- self
- }
-
- pub fn deregister_stake_address(
- &mut self,
- address: &str,
- refund_address: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({"type": "deregister_stake_address", "address": address});
- if let Some(r) = refund_address {
- op["refund_address"] = json!(r);
- }
- self.operations.push(op);
- self
- }
-
- pub fn delegate_to(&mut self, address: &str, pool_id: &str) -> &mut Self {
- self.operations
- .push(json!({"type": "delegate_to", "address": address, "pool_id": pool_id}));
- self
- }
-
- pub fn withdraw(
- &mut self,
- reward_address: &str,
- amount: u64,
- receiver: Option<&str>,
- ) -> &mut Self {
- let mut op =
- json!({"type": "withdraw", "reward_address": reward_address, "amount": amount.to_string()});
- if let Some(r) = receiver {
- op["receiver"] = json!(r);
- }
- self.operations.push(op);
- self
- }
-
- // DRep
-
- pub fn register_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "register_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn unregister_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- refund_address: Option<&str>,
- refund_amount: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "unregister_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- });
- if let Some(r) = refund_address {
- op["refund_address"] = json!(r);
- }
- if let Some(a) = refund_amount {
- op["refund_amount"] = json!(a);
- }
- self.operations.push(op);
- self
- }
-
- pub fn update_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "update_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- // Voting
-
- pub fn delegate_voting_power_to(
- &mut self,
- address: &str,
- drep_type: &str,
- drep_hash: Option<&str>,
- ) -> &mut Self {
- let mut op =
- json!({"type": "delegate_voting_power_to", "address": address, "drep_type": drep_type});
- if let Some(h) = drep_hash {
- op["drep_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn create_vote(
- &mut self,
- voter_type: &str,
- voter_hash: &str,
- gov_action_tx_hash: &str,
- gov_action_index: u32,
- vote: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "create_vote",
- "voter_type": voter_type,
- "voter_hash": voter_hash,
- "gov_action_tx_hash": gov_action_tx_hash,
- "gov_action_index": gov_action_index,
- "vote": vote,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- // Governance
-
- pub fn create_proposal(
- &mut self,
- gov_action_type: &str,
- return_address: &str,
- anchor_url: &str,
- anchor_data_hash: &str,
- withdrawals: Option<&[ProposalWithdrawal]>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "create_proposal",
- "gov_action_type": gov_action_type,
- "return_address": return_address,
- "anchor_url": anchor_url,
- "anchor_data_hash": anchor_data_hash,
- });
- if let Some(w) = withdrawals {
- op["withdrawals"] = serde_json::to_value(w).unwrap_or_default();
- }
- self.operations.push(op);
- self
- }
-
- // Pool Operations
-
- pub fn register_pool(
- &mut self,
- operator: &str,
- vrf_key_hash: &str,
- pledge: &str,
- cost: &str,
- margin_numerator: &str,
- margin_denominator: &str,
- reward_address: &str,
- pool_owners: &[&str],
- relays: Option<&[Value]>,
- pool_metadata_url: Option<&str>,
- pool_metadata_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "register_pool",
- "operator": operator,
- "vrf_key_hash": vrf_key_hash,
- "pledge": pledge,
- "cost": cost,
- "margin_numerator": margin_numerator,
- "margin_denominator": margin_denominator,
- "reward_address": reward_address,
- "pool_owners": pool_owners,
- });
- if let Some(r) = relays {
- op["relays"] = json!(r);
- }
- if let Some(u) = pool_metadata_url {
- op["pool_metadata_url"] = json!(u);
- }
- if let Some(h) = pool_metadata_hash {
- op["pool_metadata_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn update_pool(
- &mut self,
- operator: &str,
- vrf_key_hash: &str,
- pledge: &str,
- cost: &str,
- margin_numerator: &str,
- margin_denominator: &str,
- reward_address: &str,
- pool_owners: &[&str],
- relays: Option<&[Value]>,
- pool_metadata_url: Option<&str>,
- pool_metadata_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "update_pool",
- "operator": operator,
- "vrf_key_hash": vrf_key_hash,
- "pledge": pledge,
- "cost": cost,
- "margin_numerator": margin_numerator,
- "margin_denominator": margin_denominator,
- "reward_address": reward_address,
- "pool_owners": pool_owners,
- });
- if let Some(r) = relays {
- op["relays"] = json!(r);
- }
- if let Some(u) = pool_metadata_url {
- op["pool_metadata_url"] = json!(u);
- }
- if let Some(h) = pool_metadata_hash {
- op["pool_metadata_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn retire_pool(&mut self, pool_id: &str, epoch: u64) -> &mut Self {
- self.operations
- .push(json!({"type": "retire_pool", "pool_id": pool_id, "epoch": epoch}));
- self
- }
-
- // Treasury
-
- pub fn donate_to_treasury(&mut self, treasury_value: &str, donation_amount: &str) -> &mut Self {
- self.operations.push(json!({
- "type": "donate_to_treasury",
- "treasury_value": treasury_value,
- "donation_amount": donation_amount,
- }));
- self
- }
-
- // Native Script
-
- pub fn attach_native_script(&mut self, script_json: &str) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_native_script",
- "script_json": script_json,
- }));
- self
- }
-
- // Config
-
- pub fn from(&mut self, address: &str) -> &mut Self {
- self.from = Some(address.to_string());
- self
- }
-
- pub fn change_address(&mut self, address: &str) -> &mut Self {
- self.change_address = Some(address.to_string());
- self
- }
-
- pub fn fee_payer(&mut self, address: &str) -> &mut Self {
- self.fee_payer = Some(address.to_string());
- self
- }
-
- pub fn with_utxos(&mut self, utxos: Value) -> &mut Self {
- self.utxos = Some(utxos);
- self
- }
-
- pub fn with_protocol_params(&mut self, params: Value) -> &mut Self {
- self.protocol_params = Some(params);
- self
- }
-
- pub fn valid_from(&mut self, slot: u64) -> &mut Self {
- self.validity
- .insert("valid_from".to_string(), json!(slot));
- self
- }
-
- pub fn valid_to(&mut self, slot: u64) -> &mut Self {
- self.validity.insert("valid_to".to_string(), json!(slot));
- self
- }
-
- pub fn merge_outputs(&mut self, merge: bool) -> &mut Self {
- self.merge_outputs = Some(merge);
- self
- }
-
- pub fn signer_count(&mut self, count: i32) -> &mut Self {
- self.signer_count = count;
- self
- }
-
- fn build_spec(&self, provider_config: Option<&ProviderConfig>) -> Value {
- let mut spec = json!({
- "operations": self.operations,
- "from": self.from,
- "signer_count": self.signer_count,
- });
-
- if let Some(pc) = provider_config {
- spec["provider"] = serde_json::to_value(pc).unwrap_or_default();
- } else if let Some(ref u) = self.utxos {
- spec["utxos"] = u.clone();
- }
- if let Some(ref pp) = self.protocol_params {
- spec["protocol_params"] = pp.clone();
- }
- if let Some(ref ca) = self.change_address {
- spec["change_address"] = json!(ca);
- }
- if let Some(ref fp) = self.fee_payer {
- spec["fee_payer"] = json!(fp);
- }
- if !self.validity.is_empty() {
- spec["validity"] = Value::Object(self.validity.clone());
- }
- if let Some(m) = self.merge_outputs {
- spec["merge_outputs"] = json!(m);
- }
- spec
- }
-
- /// Build the transaction.
- pub fn build(&self) -> Result {
- self.do_build(None)
- }
-
- /// Build with a Java-side provider config for lazy UTXO fetching.
- pub fn build_with_provider(&self, config: &ProviderConfig) -> Result {
- self.do_build(Some(config))
- }
-
- fn do_build(&self, provider_config: Option<&ProviderConfig>) -> Result {
- let spec = self.build_spec(provider_config);
- let spec_json = serde_json::to_string(&spec).map_err(|e| CclError {
+ /// Build an unsigned transaction from a CCL TxPlan (YAML), fully offline.
+ ///
+ /// `utxos` and `protocol_params` are the caller-supplied chain data (the CCL `Utxo` /
+ /// `ProtocolParams` JSON models). The transaction is built offline and never submitted —
+ /// sign the returned `tx_cbor` and submit it yourself.
+ ///
+ /// For Plutus script transactions, pass the redeemers' execution units as `exec_units` — a JSON
+ /// array of `{mem, steps}` (one per redeemer, in transaction order). Compute them with any
+ /// evaluator (Ogmios, Blockfrost, Aiken, Scalus); the bridge does not run the script. Pass
+ /// `None` for non-script transactions.
+ pub fn build(
+ &self,
+ yaml: &str,
+ utxos: &Value,
+ protocol_params: &Value,
+ exec_units: Option<&Value>,
+ ) -> Result {
+ let utxos_json = serde_json::to_string(utxos).map_err(|e| CclError {
code: error_codes::CCL_ERROR_SERIALIZATION,
- message: format!("Failed to serialize spec: {}", e),
+ message: format!("Failed to serialize utxos: {}", e),
})?;
-
- let cs = to_cstring(&spec_json)?;
- let rc = unsafe { ffi::ccl_quicktx_build(self.bridge.thread, cs.as_ptr()) };
- let result = self.bridge.check(rc)?;
-
- serde_json::from_str(&result).map_err(|e| CclError {
- code: error_codes::CCL_ERROR_SERIALIZATION,
- message: format!("Failed to parse tx result: {}", e),
- })
- }
-}
-
-/// Lightweight operation collector for one transaction in a compose group.
-pub struct Tx {
- operations: Vec,
- from: Option,
- change_address: Option,
-}
-
-impl Tx {
- pub fn pay_to_address(
- &mut self,
- address: &str,
- amounts: &[Value],
- script_ref_cbor_hex: Option<&str>,
- script_ref_type: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "pay_to_address",
- "address": address,
- "amounts": amounts,
- });
- if let Some(s) = script_ref_cbor_hex {
- op["script_ref_cbor_hex"] = json!(s);
- }
- if let Some(t) = script_ref_type {
- op["script_ref_type"] = json!(t);
- }
- self.operations.push(op);
- self
- }
-
- pub fn pay_to_contract(
- &mut self,
- address: &str,
- amounts: &[Value],
- datum_cbor_hex: Option<&str>,
- datum_hash: Option<&str>,
- script_ref_cbor_hex: Option<&str>,
- script_ref_type: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts,
- });
- if let Some(d) = datum_cbor_hex {
- op["datum_cbor_hex"] = json!(d);
- }
- if let Some(h) = datum_hash {
- op["datum_hash"] = json!(h);
- }
- if let Some(s) = script_ref_cbor_hex {
- op["script_ref_cbor_hex"] = json!(s);
- }
- if let Some(t) = script_ref_type {
- op["script_ref_type"] = json!(t);
- }
- self.operations.push(op);
- self
- }
-
- pub fn mint_assets(
- &mut self,
- script_json: &str,
- assets: &[MintAsset],
- receiver: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "mint_assets",
- "script_json": script_json,
- "assets": assets,
- "receiver": receiver,
- }));
- self
- }
-
- pub fn attach_metadata(&mut self, label: u64, metadata: Value) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- }));
- self
- }
-
- pub fn collect_from(&mut self, utxos: &[Value]) -> &mut Self {
- self.operations.push(json!({
- "type": "collect_from",
- "collect_utxos": utxos,
- }));
- self
- }
-
- pub fn register_stake_address(&mut self, address: &str) -> &mut Self {
- self.operations
- .push(json!({"type": "register_stake_address", "address": address}));
- self
- }
-
- pub fn deregister_stake_address(
- &mut self,
- address: &str,
- refund_address: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({"type": "deregister_stake_address", "address": address});
- if let Some(r) = refund_address {
- op["refund_address"] = json!(r);
- }
- self.operations.push(op);
- self
- }
-
- pub fn delegate_to(&mut self, address: &str, pool_id: &str) -> &mut Self {
- self.operations
- .push(json!({"type": "delegate_to", "address": address, "pool_id": pool_id}));
- self
- }
-
- pub fn withdraw(
- &mut self,
- reward_address: &str,
- amount: u64,
- receiver: Option<&str>,
- ) -> &mut Self {
- let mut op =
- json!({"type": "withdraw", "reward_address": reward_address, "amount": amount.to_string()});
- if let Some(r) = receiver {
- op["receiver"] = json!(r);
- }
- self.operations.push(op);
- self
- }
-
- pub fn register_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "register_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn unregister_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- refund_address: Option<&str>,
- refund_amount: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "unregister_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- });
- if let Some(r) = refund_address {
- op["refund_address"] = json!(r);
- }
- if let Some(a) = refund_amount {
- op["refund_amount"] = json!(a);
- }
- self.operations.push(op);
- self
- }
-
- pub fn update_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "update_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn delegate_voting_power_to(
- &mut self,
- address: &str,
- drep_type: &str,
- drep_hash: Option<&str>,
- ) -> &mut Self {
- let mut op =
- json!({"type": "delegate_voting_power_to", "address": address, "drep_type": drep_type});
- if let Some(h) = drep_hash {
- op["drep_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn create_vote(
- &mut self,
- voter_type: &str,
- voter_hash: &str,
- gov_action_tx_hash: &str,
- gov_action_index: u32,
- vote: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "create_vote",
- "voter_type": voter_type,
- "voter_hash": voter_hash,
- "gov_action_tx_hash": gov_action_tx_hash,
- "gov_action_index": gov_action_index,
- "vote": vote,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn create_proposal(
- &mut self,
- gov_action_type: &str,
- return_address: &str,
- anchor_url: &str,
- anchor_data_hash: &str,
- withdrawals: Option<&[ProposalWithdrawal]>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "create_proposal",
- "gov_action_type": gov_action_type,
- "return_address": return_address,
- "anchor_url": anchor_url,
- "anchor_data_hash": anchor_data_hash,
- });
- if let Some(w) = withdrawals {
- op["withdrawals"] = serde_json::to_value(w).unwrap_or_default();
- }
- self.operations.push(op);
- self
- }
-
- // Pool Operations
-
- pub fn register_pool(
- &mut self,
- operator: &str,
- vrf_key_hash: &str,
- pledge: &str,
- cost: &str,
- margin_numerator: &str,
- margin_denominator: &str,
- reward_address: &str,
- pool_owners: &[&str],
- relays: Option<&[Value]>,
- pool_metadata_url: Option<&str>,
- pool_metadata_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "register_pool",
- "operator": operator,
- "vrf_key_hash": vrf_key_hash,
- "pledge": pledge,
- "cost": cost,
- "margin_numerator": margin_numerator,
- "margin_denominator": margin_denominator,
- "reward_address": reward_address,
- "pool_owners": pool_owners,
- });
- if let Some(r) = relays {
- op["relays"] = json!(r);
- }
- if let Some(u) = pool_metadata_url {
- op["pool_metadata_url"] = json!(u);
- }
- if let Some(h) = pool_metadata_hash {
- op["pool_metadata_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn update_pool(
- &mut self,
- operator: &str,
- vrf_key_hash: &str,
- pledge: &str,
- cost: &str,
- margin_numerator: &str,
- margin_denominator: &str,
- reward_address: &str,
- pool_owners: &[&str],
- relays: Option<&[Value]>,
- pool_metadata_url: Option<&str>,
- pool_metadata_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "update_pool",
- "operator": operator,
- "vrf_key_hash": vrf_key_hash,
- "pledge": pledge,
- "cost": cost,
- "margin_numerator": margin_numerator,
- "margin_denominator": margin_denominator,
- "reward_address": reward_address,
- "pool_owners": pool_owners,
- });
- if let Some(r) = relays {
- op["relays"] = json!(r);
- }
- if let Some(u) = pool_metadata_url {
- op["pool_metadata_url"] = json!(u);
- }
- if let Some(h) = pool_metadata_hash {
- op["pool_metadata_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn retire_pool(&mut self, pool_id: &str, epoch: u64) -> &mut Self {
- self.operations
- .push(json!({"type": "retire_pool", "pool_id": pool_id, "epoch": epoch}));
- self
- }
-
- // Treasury
-
- pub fn donate_to_treasury(&mut self, treasury_value: &str, donation_amount: &str) -> &mut Self {
- self.operations.push(json!({
- "type": "donate_to_treasury",
- "treasury_value": treasury_value,
- "donation_amount": donation_amount,
- }));
- self
- }
-
- // Native Script
-
- pub fn attach_native_script(&mut self, script_json: &str) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_native_script",
- "script_json": script_json,
- }));
- self
- }
-
- pub fn from(&mut self, address: &str) -> &mut Self {
- self.from = Some(address.to_string());
- self
- }
-
- pub fn change_address(&mut self, address: &str) -> &mut Self {
- self.change_address = Some(address.to_string());
- self
- }
-
- fn to_spec(&self) -> Value {
- let mut spec = json!({
- "from": self.from,
- "operations": self.operations,
- });
- if let Some(ref ca) = self.change_address {
- spec["change_address"] = json!(ca);
- }
- spec
- }
-}
-
-/// Builder for composing multiple Tx/ScriptTx objects into a single transaction.
-pub struct ComposeTxBuilder<'a> {
- bridge: &'a Bridge,
- txs: Vec,
- fee_payer: Option,
- utxos: Option,
- protocol_params: Option,
- validity: serde_json::Map,
- merge_outputs: Option,
- signer_count: Option,
-}
-
-impl<'a> ComposeTxBuilder<'a> {
- pub fn fee_payer(&mut self, address: &str) -> &mut Self {
- self.fee_payer = Some(address.to_string());
- self
- }
-
- pub fn with_utxos(&mut self, utxos: Value) -> &mut Self {
- self.utxos = Some(utxos);
- self
- }
-
- pub fn with_protocol_params(&mut self, params: Value) -> &mut Self {
- self.protocol_params = Some(params);
- self
- }
-
- pub fn valid_from(&mut self, slot: u64) -> &mut Self {
- self.validity
- .insert("valid_from".to_string(), json!(slot));
- self
- }
-
- pub fn valid_to(&mut self, slot: u64) -> &mut Self {
- self.validity.insert("valid_to".to_string(), json!(slot));
- self
- }
-
- pub fn merge_outputs(&mut self, merge: bool) -> &mut Self {
- self.merge_outputs = Some(merge);
- self
- }
-
- pub fn signer_count(&mut self, count: i32) -> &mut Self {
- self.signer_count = Some(count);
- self
- }
-
- /// Build the composed transaction.
- pub fn build(&self) -> Result {
- self.do_build(None)
- }
-
- /// Build with a Java-side provider config.
- pub fn build_with_provider(&self, config: &ProviderConfig) -> Result {
- self.do_build(Some(config))
- }
-
- fn do_build(&self, provider_config: Option<&ProviderConfig>) -> Result {
- let tx_specs: Vec = self
- .txs
- .iter()
- .map(|tx| match tx {
- ComposableTx::Regular(t) => t.to_spec(),
- ComposableTx::Script(s) => s.to_spec(),
- })
- .collect();
-
- let mut spec = json!({
- "transactions": tx_specs,
- "fee_payer": self.fee_payer,
- });
-
- if let Some(pc) = provider_config {
- spec["provider"] = serde_json::to_value(pc).unwrap_or_default();
- } else if let Some(ref u) = self.utxos {
- spec["utxos"] = u.clone();
- }
- if let Some(ref pp) = self.protocol_params {
- spec["protocol_params"] = pp.clone();
- }
- if let Some(sc) = self.signer_count {
- spec["signer_count"] = json!(sc);
- }
- if !self.validity.is_empty() {
- spec["validity"] = Value::Object(self.validity.clone());
- }
- if let Some(m) = self.merge_outputs {
- spec["merge_outputs"] = json!(m);
- }
-
- let spec_json = serde_json::to_string(&spec).map_err(|e| CclError {
+ let pp_json = serde_json::to_string(protocol_params).map_err(|e| CclError {
code: error_codes::CCL_ERROR_SERIALIZATION,
- message: format!("Failed to serialize compose spec: {}", e),
+ message: format!("Failed to serialize protocol params: {}", e),
})?;
- let cs = to_cstring(&spec_json)?;
- let rc = unsafe { ffi::ccl_quicktx_build(self.bridge.thread, cs.as_ptr()) };
- let result = self.bridge.check(rc)?;
-
- serde_json::from_str(&result).map_err(|e| CclError {
- code: error_codes::CCL_ERROR_SERIALIZATION,
- message: format!("Failed to parse tx result: {}", e),
- })
- }
-}
-
-// --- ReferenceInput ---
-
-/// A reference input for read_from operations.
-#[derive(Clone, Debug)]
-pub struct ReferenceInput {
- pub tx_hash: String,
- pub output_index: u32,
-}
-
-// --- ComposableTx ---
-
-/// Enum for composing regular Tx and ScriptTx objects.
-pub enum ComposableTx {
- Regular(Tx),
- Script(ScriptTx),
-}
-
-// --- ScriptTxBuilder ---
-
-/// Builder for a single script transaction spec.
-pub struct ScriptTxBuilder<'a> {
- bridge: &'a Bridge,
- operations: Vec,
- from: Option,
- change_address: Option,
- fee_payer: Option,
- utxos: Option,
- protocol_params: Option,
- validity: serde_json::Map,
- merge_outputs: Option,
- signer_count: i32,
- change_datum_cbor_hex: Option,
- change_datum_hash: Option,
-}
-
-impl<'a> ScriptTxBuilder<'a> {
- pub fn pay_to_address(
- &mut self,
- address: &str,
- amounts: &[Value],
- script_ref_cbor_hex: Option<&str>,
- script_ref_type: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "pay_to_address",
- "address": address,
- "amounts": amounts,
- });
- if let Some(s) = script_ref_cbor_hex {
- op["script_ref_cbor_hex"] = json!(s);
- }
- if let Some(t) = script_ref_type {
- op["script_ref_type"] = json!(t);
- }
- self.operations.push(op);
- self
- }
-
- pub fn pay_to_contract(
- &mut self,
- address: &str,
- amounts: &[Value],
- datum_cbor_hex: Option<&str>,
- datum_hash: Option<&str>,
- script_ref_cbor_hex: Option<&str>,
- script_ref_type: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts,
- });
- if let Some(d) = datum_cbor_hex {
- op["datum_cbor_hex"] = json!(d);
- }
- if let Some(h) = datum_hash {
- op["datum_hash"] = json!(h);
- }
- if let Some(s) = script_ref_cbor_hex {
- op["script_ref_cbor_hex"] = json!(s);
- }
- if let Some(t) = script_ref_type {
- op["script_ref_type"] = json!(t);
- }
- self.operations.push(op);
- self
- }
-
- pub fn attach_metadata(&mut self, label: u64, metadata: Value) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- }));
- self
- }
-
- pub fn collect_from(&mut self, utxos: &[Value]) -> &mut Self {
- self.operations.push(json!({
- "type": "collect_from",
- "collect_utxos": utxos,
- }));
- self
- }
-
- pub fn collect_from_script(
- &mut self,
- utxos: &[Value],
- redeemer_cbor_hex: &str,
- datum_cbor_hex: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "collect_from",
- "collect_utxos": utxos,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(d) = datum_cbor_hex {
- op["datum_cbor_hex"] = json!(d);
- }
- self.operations.push(op);
- self
- }
-
- pub fn read_from(&mut self, reference_inputs: &[ReferenceInput]) -> &mut Self {
- let refs: Vec = reference_inputs
- .iter()
- .map(|r| json!({"tx_hash": r.tx_hash, "output_index": r.output_index}))
- .collect();
- self.operations.push(json!({
- "type": "read_from",
- "reference_inputs": refs,
- }));
- self
- }
-
- pub fn mint_plutus_assets(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- assets: &[MintAsset],
- redeemer_cbor_hex: &str,
- receiver: Option<&str>,
- output_datum_cbor_hex: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "mint_plutus_assets",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- "assets": assets,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(r) = receiver {
- op["receiver"] = json!(r);
- }
- if let Some(d) = output_datum_cbor_hex {
- op["output_datum_cbor_hex"] = json!(d);
- }
- self.operations.push(op);
- self
- }
-
- pub fn attach_spending_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_spending_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- pub fn attach_certificate_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_certificate_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- pub fn attach_reward_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_reward_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- pub fn attach_proposing_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_proposing_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- pub fn attach_voting_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_voting_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- // Staking (redeemer-enhanced)
-
- pub fn deregister_stake_address(
- &mut self,
- address: &str,
- redeemer_cbor_hex: &str,
- refund_address: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "deregister_stake_address",
- "address": address,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(r) = refund_address {
- op["refund_address"] = json!(r);
- }
- self.operations.push(op);
- self
- }
-
- pub fn delegate_to(
- &mut self,
- address: &str,
- pool_id: &str,
- redeemer_cbor_hex: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "delegate_to",
- "address": address,
- "pool_id": pool_id,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }));
- self
- }
-
- pub fn withdraw(
- &mut self,
- reward_address: &str,
- amount: u64,
- redeemer_cbor_hex: &str,
- receiver: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "withdraw",
- "reward_address": reward_address,
- "amount": amount.to_string(),
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(r) = receiver {
- op["receiver"] = json!(r);
- }
- self.operations.push(op);
- self
- }
-
- // DRep (redeemer-enhanced)
-
- pub fn register_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- redeemer_cbor_hex: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "register_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn unregister_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- redeemer_cbor_hex: &str,
- refund_address: Option<&str>,
- refund_amount: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "unregister_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(r) = refund_address {
- op["refund_address"] = json!(r);
- }
- if let Some(a) = refund_amount {
- op["refund_amount"] = json!(a);
- }
- self.operations.push(op);
- self
- }
-
- pub fn update_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- redeemer_cbor_hex: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "update_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- // Voting (redeemer-enhanced)
-
- pub fn delegate_voting_power_to(
- &mut self,
- address: &str,
- drep_type: &str,
- drep_hash: Option<&str>,
- redeemer_cbor_hex: &str,
- ) -> &mut Self {
- let mut op = json!({
- "type": "delegate_voting_power_to",
- "address": address,
- "drep_type": drep_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(h) = drep_hash {
- op["drep_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn create_vote(
- &mut self,
- voter_type: &str,
- voter_hash: &str,
- gov_action_tx_hash: &str,
- gov_action_index: u32,
- vote: &str,
- redeemer_cbor_hex: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "create_vote",
- "voter_type": voter_type,
- "voter_hash": voter_hash,
- "gov_action_tx_hash": gov_action_tx_hash,
- "gov_action_index": gov_action_index,
- "vote": vote,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- // Governance (redeemer-enhanced)
-
- pub fn create_proposal(
- &mut self,
- gov_action_type: &str,
- return_address: &str,
- anchor_url: &str,
- anchor_data_hash: &str,
- redeemer_cbor_hex: &str,
- withdrawals: Option<&[HashMap]>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "create_proposal",
- "gov_action_type": gov_action_type,
- "return_address": return_address,
- "anchor_url": anchor_url,
- "anchor_data_hash": anchor_data_hash,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(w) = withdrawals {
- op["withdrawals"] = serde_json::to_value(w).unwrap_or_default();
- }
- self.operations.push(op);
- self
- }
-
- // Treasury
-
- pub fn donate_to_treasury(
- &mut self,
- treasury_value: &str,
- donation_amount: &str,
- redeemer_cbor_hex: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "donate_to_treasury",
- "treasury_value": treasury_value,
- "donation_amount": donation_amount,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }));
- self
- }
-
- // Config
-
- pub fn from(&mut self, address: &str) -> &mut Self {
- self.from = Some(address.to_string());
- self
- }
-
- pub fn change_address(&mut self, address: &str) -> &mut Self {
- self.change_address = Some(address.to_string());
- self
- }
-
- pub fn change_datum(&mut self, datum_cbor_hex: &str) -> &mut Self {
- self.change_datum_cbor_hex = Some(datum_cbor_hex.to_string());
- self
- }
-
- pub fn change_datum_hash(&mut self, hash: &str) -> &mut Self {
- self.change_datum_hash = Some(hash.to_string());
- self
- }
-
- pub fn fee_payer(&mut self, address: &str) -> &mut Self {
- self.fee_payer = Some(address.to_string());
- self
- }
-
- pub fn with_utxos(&mut self, utxos: Value) -> &mut Self {
- self.utxos = Some(utxos);
- self
- }
-
- pub fn with_protocol_params(&mut self, params: Value) -> &mut Self {
- self.protocol_params = Some(params);
- self
- }
-
- pub fn valid_from(&mut self, slot: u64) -> &mut Self {
- self.validity
- .insert("valid_from".to_string(), json!(slot));
- self
- }
-
- pub fn valid_to(&mut self, slot: u64) -> &mut Self {
- self.validity.insert("valid_to".to_string(), json!(slot));
- self
- }
-
- pub fn merge_outputs(&mut self, merge: bool) -> &mut Self {
- self.merge_outputs = Some(merge);
- self
- }
-
- pub fn signer_count(&mut self, count: i32) -> &mut Self {
- self.signer_count = count;
- self
- }
-
- fn build_spec(&self, provider_config: Option<&ProviderConfig>) -> Value {
- let mut spec = json!({
- "tx_type": "script_tx",
- "operations": self.operations,
- "from": self.from,
- "signer_count": self.signer_count,
- });
-
- if let Some(pc) = provider_config {
- spec["provider"] = serde_json::to_value(pc).unwrap_or_default();
- } else if let Some(ref u) = self.utxos {
- spec["utxos"] = u.clone();
- }
- if let Some(ref pp) = self.protocol_params {
- spec["protocol_params"] = pp.clone();
- }
- if let Some(ref ca) = self.change_address {
- spec["change_address"] = json!(ca);
- }
- if let Some(ref fp) = self.fee_payer {
- spec["fee_payer"] = json!(fp);
- }
- if !self.validity.is_empty() {
- spec["validity"] = Value::Object(self.validity.clone());
- }
- if let Some(m) = self.merge_outputs {
- spec["merge_outputs"] = json!(m);
- }
- if let Some(ref d) = self.change_datum_cbor_hex {
- spec["change_datum_cbor_hex"] = json!(d);
- }
- if let Some(ref h) = self.change_datum_hash {
- spec["change_datum_hash"] = json!(h);
- }
- spec
- }
-
- /// Build the script transaction.
- pub fn build(&self) -> Result {
- self.do_build(None)
- }
-
- /// Build with a Java-side provider config for lazy UTXO fetching.
- pub fn build_with_provider(&self, config: &ProviderConfig) -> Result {
- self.do_build(Some(config))
- }
-
- fn do_build(&self, provider_config: Option<&ProviderConfig>) -> Result {
- let spec = self.build_spec(provider_config);
- let spec_json = serde_json::to_string(&spec).map_err(|e| CclError {
- code: error_codes::CCL_ERROR_SERIALIZATION,
- message: format!("Failed to serialize spec: {}", e),
- })?;
+ let yaml_cs = to_cstring(yaml)?;
+ let utxos_cs = to_cstring(&utxos_json)?;
+ let pp_cs = to_cstring(&pp_json)?;
+
+ // Optional execution units; null pointer when absent. The CString must outlive the call.
+ let exec_cs = match exec_units {
+ Some(eu) => {
+ let s = serde_json::to_string(eu).map_err(|e| CclError {
+ code: error_codes::CCL_ERROR_SERIALIZATION,
+ message: format!("Failed to serialize exec units: {}", e),
+ })?;
+ Some(to_cstring(&s)?)
+ }
+ None => None,
+ };
+ let exec_ptr = exec_cs.as_ref().map_or(ptr::null(), |c| c.as_ptr());
- let cs = to_cstring(&spec_json)?;
- let rc = unsafe { ffi::ccl_quicktx_build(self.bridge.thread, cs.as_ptr()) };
+ let rc = unsafe {
+ ffi::ccl_quicktx_build(
+ self.bridge.thread,
+ yaml_cs.as_ptr(),
+ utxos_cs.as_ptr(),
+ pp_cs.as_ptr(),
+ exec_ptr,
+ )
+ };
+ // The build result is a YAML document.
let result = self.bridge.check(rc)?;
-
- serde_json::from_str(&result).map_err(|e| CclError {
+ serde_yaml::from_str(&result).map_err(|e| CclError {
code: error_codes::CCL_ERROR_SERIALIZATION,
message: format!("Failed to parse tx result: {}", e),
})
}
}
-
-// --- ScriptTx ---
-
-/// Lightweight operation collector for one script transaction in a compose group.
-pub struct ScriptTx {
- operations: Vec,
- from: Option,
- change_address: Option,
- change_datum_cbor_hex: Option,
- change_datum_hash: Option,
-}
-
-impl ScriptTx {
- pub fn pay_to_address(
- &mut self,
- address: &str,
- amounts: &[Value],
- script_ref_cbor_hex: Option<&str>,
- script_ref_type: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "pay_to_address",
- "address": address,
- "amounts": amounts,
- });
- if let Some(s) = script_ref_cbor_hex {
- op["script_ref_cbor_hex"] = json!(s);
- }
- if let Some(t) = script_ref_type {
- op["script_ref_type"] = json!(t);
- }
- self.operations.push(op);
- self
- }
-
- pub fn pay_to_contract(
- &mut self,
- address: &str,
- amounts: &[Value],
- datum_cbor_hex: Option<&str>,
- datum_hash: Option<&str>,
- script_ref_cbor_hex: Option<&str>,
- script_ref_type: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "pay_to_contract",
- "address": address,
- "amounts": amounts,
- });
- if let Some(d) = datum_cbor_hex {
- op["datum_cbor_hex"] = json!(d);
- }
- if let Some(h) = datum_hash {
- op["datum_hash"] = json!(h);
- }
- if let Some(s) = script_ref_cbor_hex {
- op["script_ref_cbor_hex"] = json!(s);
- }
- if let Some(t) = script_ref_type {
- op["script_ref_type"] = json!(t);
- }
- self.operations.push(op);
- self
- }
-
- pub fn attach_metadata(&mut self, label: u64, metadata: Value) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_metadata",
- "label": label,
- "metadata": metadata,
- }));
- self
- }
-
- pub fn collect_from(&mut self, utxos: &[Value]) -> &mut Self {
- self.operations.push(json!({
- "type": "collect_from",
- "collect_utxos": utxos,
- }));
- self
- }
-
- pub fn collect_from_script(
- &mut self,
- utxos: &[Value],
- redeemer_cbor_hex: &str,
- datum_cbor_hex: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "collect_from",
- "collect_utxos": utxos,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(d) = datum_cbor_hex {
- op["datum_cbor_hex"] = json!(d);
- }
- self.operations.push(op);
- self
- }
-
- pub fn read_from(&mut self, reference_inputs: &[ReferenceInput]) -> &mut Self {
- let refs: Vec = reference_inputs
- .iter()
- .map(|r| json!({"tx_hash": r.tx_hash, "output_index": r.output_index}))
- .collect();
- self.operations.push(json!({
- "type": "read_from",
- "reference_inputs": refs,
- }));
- self
- }
-
- pub fn mint_plutus_assets(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- assets: &[MintAsset],
- redeemer_cbor_hex: &str,
- receiver: Option<&str>,
- output_datum_cbor_hex: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "mint_plutus_assets",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- "assets": assets,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(r) = receiver {
- op["receiver"] = json!(r);
- }
- if let Some(d) = output_datum_cbor_hex {
- op["output_datum_cbor_hex"] = json!(d);
- }
- self.operations.push(op);
- self
- }
-
- pub fn attach_spending_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_spending_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- pub fn attach_certificate_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_certificate_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- pub fn attach_reward_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_reward_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- pub fn attach_proposing_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_proposing_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- pub fn attach_voting_validator(
- &mut self,
- script_cbor_hex: &str,
- script_type: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "attach_voting_validator",
- "script_cbor_hex": script_cbor_hex,
- "script_type": script_type,
- }));
- self
- }
-
- // Staking (redeemer-enhanced)
-
- pub fn deregister_stake_address(
- &mut self,
- address: &str,
- redeemer_cbor_hex: &str,
- refund_address: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "deregister_stake_address",
- "address": address,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(r) = refund_address {
- op["refund_address"] = json!(r);
- }
- self.operations.push(op);
- self
- }
-
- pub fn delegate_to(
- &mut self,
- address: &str,
- pool_id: &str,
- redeemer_cbor_hex: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "delegate_to",
- "address": address,
- "pool_id": pool_id,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }));
- self
- }
-
- pub fn withdraw(
- &mut self,
- reward_address: &str,
- amount: u64,
- redeemer_cbor_hex: &str,
- receiver: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "withdraw",
- "reward_address": reward_address,
- "amount": amount.to_string(),
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(r) = receiver {
- op["receiver"] = json!(r);
- }
- self.operations.push(op);
- self
- }
-
- // DRep (redeemer-enhanced)
-
- pub fn register_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- redeemer_cbor_hex: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "register_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn unregister_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- redeemer_cbor_hex: &str,
- refund_address: Option<&str>,
- refund_amount: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "unregister_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(r) = refund_address {
- op["refund_address"] = json!(r);
- }
- if let Some(a) = refund_amount {
- op["refund_amount"] = json!(a);
- }
- self.operations.push(op);
- self
- }
-
- pub fn update_drep(
- &mut self,
- cred_hash: &str,
- cred_type: &str,
- redeemer_cbor_hex: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "update_drep",
- "credential_hash": cred_hash,
- "credential_type": cred_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- // Voting (redeemer-enhanced)
-
- pub fn delegate_voting_power_to(
- &mut self,
- address: &str,
- drep_type: &str,
- drep_hash: Option<&str>,
- redeemer_cbor_hex: &str,
- ) -> &mut Self {
- let mut op = json!({
- "type": "delegate_voting_power_to",
- "address": address,
- "drep_type": drep_type,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(h) = drep_hash {
- op["drep_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- pub fn create_vote(
- &mut self,
- voter_type: &str,
- voter_hash: &str,
- gov_action_tx_hash: &str,
- gov_action_index: u32,
- vote: &str,
- redeemer_cbor_hex: &str,
- anchor_url: Option<&str>,
- anchor_data_hash: Option<&str>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "create_vote",
- "voter_type": voter_type,
- "voter_hash": voter_hash,
- "gov_action_tx_hash": gov_action_tx_hash,
- "gov_action_index": gov_action_index,
- "vote": vote,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(u) = anchor_url {
- op["anchor_url"] = json!(u);
- }
- if let Some(h) = anchor_data_hash {
- op["anchor_data_hash"] = json!(h);
- }
- self.operations.push(op);
- self
- }
-
- // Governance (redeemer-enhanced)
-
- pub fn create_proposal(
- &mut self,
- gov_action_type: &str,
- return_address: &str,
- anchor_url: &str,
- anchor_data_hash: &str,
- redeemer_cbor_hex: &str,
- withdrawals: Option<&[HashMap]>,
- ) -> &mut Self {
- let mut op = json!({
- "type": "create_proposal",
- "gov_action_type": gov_action_type,
- "return_address": return_address,
- "anchor_url": anchor_url,
- "anchor_data_hash": anchor_data_hash,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- });
- if let Some(w) = withdrawals {
- op["withdrawals"] = serde_json::to_value(w).unwrap_or_default();
- }
- self.operations.push(op);
- self
- }
-
- // Treasury
-
- pub fn donate_to_treasury(
- &mut self,
- treasury_value: &str,
- donation_amount: &str,
- redeemer_cbor_hex: &str,
- ) -> &mut Self {
- self.operations.push(json!({
- "type": "donate_to_treasury",
- "treasury_value": treasury_value,
- "donation_amount": donation_amount,
- "redeemer_cbor_hex": redeemer_cbor_hex,
- }));
- self
- }
-
- // Config
-
- pub fn from(&mut self, address: &str) -> &mut Self {
- self.from = Some(address.to_string());
- self
- }
-
- pub fn change_address(&mut self, address: &str) -> &mut Self {
- self.change_address = Some(address.to_string());
- self
- }
-
- pub fn change_datum(&mut self, datum_cbor_hex: &str) -> &mut Self {
- self.change_datum_cbor_hex = Some(datum_cbor_hex.to_string());
- self
- }
-
- pub fn change_datum_hash(&mut self, hash: &str) -> &mut Self {
- self.change_datum_hash = Some(hash.to_string());
- self
- }
-
- fn to_spec(&self) -> Value {
- let mut spec = json!({
- "tx_type": "script_tx",
- "from": self.from,
- "operations": self.operations,
- });
- if let Some(ref ca) = self.change_address {
- spec["change_address"] = json!(ca);
- }
- if let Some(ref d) = self.change_datum_cbor_hex {
- spec["change_datum_cbor_hex"] = json!(d);
- }
- if let Some(ref h) = self.change_datum_hash {
- spec["change_datum_hash"] = json!(h);
- }
- spec
- }
-}
diff --git a/wrappers/rust/tests/integration_test.rs b/wrappers/rust/tests/integration_test.rs
index b654b8b..0edc8ba 100644
--- a/wrappers/rust/tests/integration_test.rs
+++ b/wrappers/rust/tests/integration_test.rs
@@ -1,4 +1,4 @@
-use ccl::{Amount, Bridge, ComposableTx, MintAsset, ProviderConfig, ProposalWithdrawal, TxResult};
+use ccl::{Bridge, TxResult};
use serde_json::{json, Value};
// A known valid transaction CBOR hex (built from Java tests)
@@ -523,632 +523,72 @@ fn assert_tx_result(result: &TxResult) {
assert!(fee > 0, "fee should be positive");
}
-#[test]
-fn test_quicktx_simple_ada_payment() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let receiver = get_testnet_addr(&bridge);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(5.0)], None, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
+fn payment_yaml(from: &str, to: &str, quantity: &str) -> String {
+ format!(
+ "version: 1.0\n\
+ transaction:\n\
+ \x20 - tx:\n\
+ \x20 from: {from}\n\
+ \x20 intents:\n\
+ \x20 - type: payment\n\
+ \x20 address: {to}\n\
+ \x20 amounts:\n\
+ \x20 - unit: lovelace\n\
+ \x20 quantity: \"{quantity}\"\n"
+ )
}
#[test]
-fn test_quicktx_multiple_receivers() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let receiver1 = get_testnet_addr(&bridge);
- let receiver2 = get_testnet_addr(&bridge);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver1, &[Amount::ada(5.0)], None, None)
- .pay_to_address(&receiver2, &[Amount::ada(3.0)], None, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_pay_to_contract() {
+fn test_quicktx_simple_payment() {
let bridge = Bridge::new().expect("Failed to create bridge");
let sender = get_testnet_addr(&bridge);
let receiver = get_testnet_addr(&bridge);
+ let yaml = payment_yaml(&sender, &receiver, "5000000");
let result = bridge
.quicktx()
- .new_tx()
- .pay_to_contract(&receiver, &[Amount::ada(5.0)], Some("182a"), None, None, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
+ .build(&yaml, &make_utxos(&sender, 100_000_000), &test_protocol_params(), None)
.expect("Build failed");
assert_tx_result(&result);
}
#[test]
-fn test_quicktx_mint_assets() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
-
- let info_str = bridge
- .address()
- .info(&sender)
- .expect("Failed to get address info");
- let info: Value = serde_json::from_str(&info_str).expect("Invalid JSON");
- let key_hash = info["payment_credential_hash"].as_str().unwrap();
-
- let script_json = format!(r#"{{"type":"sig","keyHash":"{}"}}"#, key_hash);
- let assets = vec![MintAsset {
- name: "TestToken".to_string(),
- quantity: "1000".to_string(),
- }];
-
- let result = bridge
- .quicktx()
- .new_tx()
- .mint_assets(&script_json, &assets, &sender)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_attach_metadata() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let receiver = get_testnet_addr(&bridge);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(2.0)], None, None)
- .attach_metadata(674, json!({"msg": ["Hello from Rust"]}))
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_collect_from() {
+fn test_quicktx_variable_substitution() {
let bridge = Bridge::new().expect("Failed to create bridge");
let sender = get_testnet_addr(&bridge);
let receiver = get_testnet_addr(&bridge);
- let utxos = make_utxos(&sender, 100_000_000);
- let utxo_array = utxos.as_array().unwrap().clone();
-
- let result = bridge
- .quicktx()
- .new_tx()
- .collect_from(&utxo_array)
- .pay_to_address(&receiver, &[Amount::ada(2.0)], None, None)
- .from(&sender)
- .with_utxos(utxos)
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_register_stake_address() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .register_stake_address(&sender)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_deregister_stake_address() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .deregister_stake_address(&sender, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_delegate_to() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let pool_id = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy";
-
- let result = bridge
- .quicktx()
- .new_tx()
- .delegate_to(&sender, pool_id)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_withdraw() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let (sender, mnemonic) = get_testnet_address(&bridge);
-
- let restored = bridge
- .account()
- .from_mnemonic(&mnemonic, ccl::network::TESTNET, 0, 0)
- .expect("Failed to restore");
- let restored_json: Value = serde_json::from_str(&restored).expect("Invalid JSON");
- let stake_addr = restored_json["stake_address"].as_str().unwrap();
-
- let result = bridge
- .quicktx()
- .new_tx()
- .withdraw(stake_addr, 5000000, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_register_drep() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let cred_hash = "ab".repeat(28);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .register_drep(&cred_hash, "key", None, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_unregister_drep() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let cred_hash = "ab".repeat(28);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .unregister_drep(&cred_hash, "key", None, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_update_drep() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let cred_hash = "ab".repeat(28);
- let data_hash = "cd".repeat(32);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .update_drep(
- &cred_hash,
- "key",
- Some("https://example.com/drep-v2.json"),
- Some(&data_hash),
- )
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_delegate_voting_power_to() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let drep_hash = "ab".repeat(28);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .delegate_voting_power_to(&sender, "key_hash", Some(&drep_hash))
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_create_vote() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let voter_hash = "ab".repeat(28);
- let gov_tx_hash = "cd".repeat(32);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .create_vote(
- "drep_key_hash",
- &voter_hash,
- &gov_tx_hash,
- 0,
- "yes",
- None,
- None,
- )
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_create_info_proposal() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let (sender, mnemonic) = get_testnet_address(&bridge);
-
- let restored = bridge
- .account()
- .from_mnemonic(&mnemonic, ccl::network::TESTNET, 0, 0)
- .expect("Failed to restore");
- let restored_json: Value = serde_json::from_str(&restored).expect("Invalid JSON");
- let stake_addr = restored_json["stake_address"].as_str().unwrap();
- let anchor_data_hash = "ab".repeat(32);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .create_proposal("info_action", stake_addr, "https://example.com/proposal.json", &anchor_data_hash, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 2_000_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_create_treasury_withdrawals_proposal() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let (sender, mnemonic) = get_testnet_address(&bridge);
-
- let restored = bridge
- .account()
- .from_mnemonic(&mnemonic, ccl::network::TESTNET, 0, 0)
- .expect("Failed to restore");
- let restored_json: Value = serde_json::from_str(&restored).expect("Invalid JSON");
- let stake_addr = restored_json["stake_address"].as_str().unwrap();
- let anchor_data_hash = "ab".repeat(32);
-
- let withdrawals = vec![ProposalWithdrawal {
- reward_address: stake_addr.to_string(),
- amount: "1000000".to_string(),
- }];
-
- let result = bridge
- .quicktx()
- .new_tx()
- .create_proposal(
- "treasury_withdrawals",
- stake_addr,
- "https://example.com/proposal.json",
- &anchor_data_hash,
- Some(&withdrawals),
- )
- .from(&sender)
- .with_utxos(make_utxos(&sender, 2_000_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_compose() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender1 = get_testnet_addr(&bridge);
- let sender2 = get_testnet_addr(&bridge);
- let receiver1 = get_testnet_addr(&bridge);
- let receiver2 = get_testnet_addr(&bridge);
-
- let mut tx1 = bridge.quicktx().tx();
- tx1.pay_to_address(&receiver1, &[Amount::ada(5.0)], None, None)
- .from(&sender1);
-
- let mut tx2 = bridge.quicktx().tx();
- tx2.pay_to_address(&receiver2, &[Amount::ada(3.0)], None, None)
- .from(&sender2);
-
- let utxos = json!([
- {
- "tx_hash": FAKE_TX_HASH,
- "output_index": 0,
- "address": sender1,
- "amount": [{"unit": "lovelace", "quantity": "100000000"}],
- },
- {
- "tx_hash": "b".repeat(64),
- "output_index": 0,
- "address": sender2,
- "amount": [{"unit": "lovelace", "quantity": "100000000"}],
- },
- ]);
-
+ let yaml = format!(
+ "version: 1.0\n\
+ variables:\n\
+ \x20 to: {receiver}\n\
+ \x20 amount: \"4000000\"\n\
+ transaction:\n\
+ \x20 - tx:\n\
+ \x20 from: {sender}\n\
+ \x20 intents:\n\
+ \x20 - type: payment\n\
+ \x20 address: ${{to}}\n\
+ \x20 amounts:\n\
+ \x20 - unit: lovelace\n\
+ \x20 quantity: ${{amount}}\n"
+ );
let result = bridge
.quicktx()
- .compose(vec![ComposableTx::Regular(tx1), ComposableTx::Regular(tx2)])
- .fee_payer(&sender1)
- .with_utxos(utxos)
- .with_protocol_params(test_protocol_params())
- .signer_count(2)
- .build()
+ .build(&yaml, &make_utxos(&sender, 100_000_000), &test_protocol_params(), None)
.expect("Build failed");
assert_tx_result(&result);
}
-#[test]
-fn test_quicktx_provider_config() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let receiver = get_testnet_addr(&bridge);
-
- let config = ProviderConfig {
- name: "yaci_devkit".to_string(),
- url: "http://localhost:3000".to_string(),
- api_key: None,
- enable_cost_evaluation: None,
- };
-
- // Provider config will attempt Java-side HTTP; expect an error since no provider is running
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(5.0)], None, None)
- .from(&sender)
- .with_protocol_params(test_protocol_params())
- .build_with_provider(&config);
-
- if result.is_ok() {
- println!("BuildWithProvider succeeded (provider was reachable)");
- }
-}
-
#[test]
fn test_quicktx_insufficient_funds() {
let bridge = Bridge::new().expect("Failed to create bridge");
let sender = get_testnet_addr(&bridge);
let receiver = get_testnet_addr(&bridge);
+ let yaml = payment_yaml(&sender, &receiver, "200000000");
let result = bridge
.quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(200.0)], None, None)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 1_000_000))
- .with_protocol_params(test_protocol_params())
- .build();
-
- assert!(result.is_err(), "Expected error for insufficient funds");
- let err = result.unwrap_err();
- assert!(err.code < 0, "Expected negative error code, got {}", err.code);
-}
-
-#[test]
-fn test_quicktx_register_pool() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let operator_hash = "ab".repeat(28); // 28-byte (56 hex chars) pool operator key hash
- let vrf_key_hash = "cd".repeat(32); // 32-byte (64 hex chars) VRF key hash
- let reward_account_hex = format!("e0{}", "ab".repeat(28)); // testnet reward address (hex)
-
- let result = bridge
- .quicktx()
- .new_tx()
- .register_pool(
- &operator_hash,
- &vrf_key_hash,
- "500000000",
- "340000000",
- "1",
- "5",
- &reward_account_hex,
- &[operator_hash.as_str()],
- None,
- None,
- None,
- )
- .from(&sender)
- .with_utxos(make_utxos(&sender, 1_000_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_update_pool() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let operator_hash = "ab".repeat(28); // 28-byte (56 hex chars) pool operator key hash
- let vrf_key_hash = "cd".repeat(32); // 32-byte (64 hex chars) VRF key hash
- let metadata_hash = "ef".repeat(32); // 32-byte (64 hex chars) metadata hash
- let reward_account_hex = format!("e0{}", "ab".repeat(28)); // testnet reward address (hex)
-
- let result = bridge
- .quicktx()
- .new_tx()
- .update_pool(
- &operator_hash,
- &vrf_key_hash,
- "600000000",
- "340000000",
- "1",
- "10",
- &reward_account_hex,
- &[operator_hash.as_str()],
- None,
- Some("https://example.com/pool.json"),
- Some(&metadata_hash),
- )
- .from(&sender)
- .with_utxos(make_utxos(&sender, 1_000_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_retire_pool() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let pool_id = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy";
-
- let result = bridge
- .quicktx()
- .new_tx()
- .retire_pool(pool_id, 100)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_donate_to_treasury() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .donate_to_treasury("5000000", "1000000")
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_attach_native_script() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
-
- let info_str = bridge
- .address()
- .info(&sender)
- .expect("Failed to get address info");
- let info: Value = serde_json::from_str(&info_str).expect("Invalid JSON");
- let key_hash = info["payment_credential_hash"].as_str().unwrap();
-
- let script_json = format!(r#"{{"type":"sig","keyHash":"{}"}}"#, key_hash);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&sender, &[Amount::ada(2.0)], None, None)
- .attach_native_script(&script_json)
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_pay_with_script_ref() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let receiver = get_testnet_addr(&bridge);
-
- // Always-succeeds PlutusV3 script CBOR
- let script_ref_cbor = "46450101002499";
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(
- &receiver,
- &[Amount::ada(5.0)],
- Some(script_ref_cbor),
- Some("plutus_v3"),
- )
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
-}
-
-#[test]
-fn test_quicktx_unregister_drep_with_refund_amount() {
- let bridge = Bridge::new().expect("Failed to create bridge");
- let sender = get_testnet_addr(&bridge);
- let cred_hash = "ab".repeat(28);
-
- let result = bridge
- .quicktx()
- .new_tx()
- .unregister_drep(&cred_hash, "key", None, Some("2000000"))
- .from(&sender)
- .with_utxos(make_utxos(&sender, 100_000_000))
- .with_protocol_params(test_protocol_params())
- .build()
- .expect("Build failed");
- assert_tx_result(&result);
+ .build(&yaml, &make_utxos(&sender, 1_000_000), &test_protocol_params(), None);
+ assert!(result.is_err(), "expected insufficient funds error");
}
diff --git a/wrappers/rust/tests/intents_test.rs b/wrappers/rust/tests/intents_test.rs
new file mode 100644
index 0000000..34bd2b8
--- /dev/null
+++ b/wrappers/rust/tests/intents_test.rs
@@ -0,0 +1,101 @@
+//! End-to-end coverage of every TxPlan intent through the native library.
+//!
+//! The fixtures in `test-fixtures/quicktx-intents/` are generated by the JVM QuickTxIntentsTest via
+//! `TxPlan.from(tx).toYaml()`; here each is built through the native library via the Rust wrapper.
+//! Mirrors the Go `intents_test.go` for cross-wrapper parity.
+//!
+//! Run from wrappers/rust:
+//! CCL_LIB_PATH=../../core/build/native/nativeCompile \
+//! cargo test --test intents_test -- --test-threads=1
+
+use ccl::Bridge;
+use serde_json::{json, Value};
+use std::fs;
+use std::path::{Path, PathBuf};
+
+const SENDER: &str = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp";
+const SENDER2: &str = "addr_test1qz7svwszky8gcmhrfza7a89z9u0dfzd3l7h23sqlc5yml7ejcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqcqrvr0";
+const SCRIPT_ADDR: &str = "addr_test1wpunlryvl7aqsxe22erzlsseej87v5kk5vutvtrmzdy8dect48z0w";
+const SCRIPT_DATUM_HASH: &str = "9e1199a988ba72ffd6e9c269cadb3b53b5f360ff99f112d9b2ee30c4d74ad88b";
+const SCRIPT_TX_HASH: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
+
+fn fixtures_dir() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-fixtures/quicktx-intents")
+}
+
+// Protocol parameters incl. the Conway deposits the governance/staking/pool intents need.
+fn protocol_params() -> Value {
+ json!({
+ "min_fee_a": 44, "min_fee_b": 155381, "max_tx_size": 16384, "max_val_size": "5000",
+ "key_deposit": "2000000", "pool_deposit": "500000000", "drep_deposit": "2000000",
+ "gov_action_deposit": "1000000000", "coins_per_utxo_size": "4310",
+ "max_tx_ex_mem": "14000000", "max_tx_ex_steps": "10000000000",
+ "price_mem": 0.0577, "price_step": 0.0000721, "collateral_percent": 150,
+ "max_collateral_inputs": 3, "min_fee_ref_script_cost_per_byte": 15
+ })
+}
+
+fn utxos() -> Value {
+ json!([
+ {"tx_hash": "a".repeat(64), "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]},
+ {"tx_hash": "c".repeat(64), "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "5000000"}]},
+ {"tx_hash": "a".repeat(64), "output_index": 1, "address": SENDER2,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
+ ])
+}
+
+#[test]
+fn intents_build_e2e() {
+ let bridge = Bridge::new().expect("create bridge");
+ let pp = protocol_params();
+ let u = utxos();
+
+ let mut count = 0;
+ for entry in fs::read_dir(fixtures_dir()).expect("read fixtures dir") {
+ let path = entry.unwrap().path();
+ if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
+ continue;
+ }
+ let yaml = fs::read_to_string(&path).unwrap();
+ let result = bridge
+ .quicktx()
+ .build(&yaml, &u, &pp, None)
+ .unwrap_or_else(|e| panic!("build {:?}: {:?}", path.file_name().unwrap(), e));
+ assert_eq!(result.tx_hash.len(), 64, "{:?}", path.file_name().unwrap());
+ assert!(!result.tx_cbor.is_empty());
+ count += 1;
+ }
+ assert!(count >= 19, "expected >= 19 intent fixtures, found {}", count);
+}
+
+#[test]
+fn plutus_mint_e2e() {
+ let bridge = Bridge::new().expect("create bridge");
+ let yaml = fs::read_to_string(fixtures_dir().join("plutus/script_minting.yaml")).unwrap();
+ let u = json!([{"tx_hash": "a".repeat(64), "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}]);
+ let exec = json!([{"mem": 2000000, "steps": 500000000}]);
+
+ let result = bridge.quicktx().build(&yaml, &u, &protocol_params(), Some(&exec)).expect("mint");
+ assert_eq!(result.tx_hash.len(), 64);
+ assert!(bridge.quicktx().build(&yaml, &u, &protocol_params(), None).is_err());
+}
+
+#[test]
+fn plutus_spend_e2e() {
+ let bridge = Bridge::new().expect("create bridge");
+ let yaml = fs::read_to_string(fixtures_dir().join("plutus/script_collect_from.yaml")).unwrap();
+ let u = json!([
+ {"tx_hash": SCRIPT_TX_HASH, "output_index": 0, "address": SCRIPT_ADDR,
+ "amount": [{"unit": "lovelace", "quantity": "10000000"}], "data_hash": SCRIPT_DATUM_HASH},
+ {"tx_hash": "a".repeat(64), "output_index": 0, "address": SENDER,
+ "amount": [{"unit": "lovelace", "quantity": "2000000000"}]}
+ ]);
+ let exec = json!([{"mem": 2000000, "steps": 500000000}]);
+
+ let result = bridge.quicktx().build(&yaml, &u, &protocol_params(), Some(&exec)).expect("spend");
+ assert_eq!(result.tx_hash.len(), 64);
+ assert!(bridge.quicktx().build(&yaml, &u, &protocol_params(), None).is_err());
+}
diff --git a/wrappers/rust/tests/quicktx_integration_test.rs b/wrappers/rust/tests/quicktx_integration_test.rs
index a6bbc8d..ff0e960 100644
--- a/wrappers/rust/tests/quicktx_integration_test.rs
+++ b/wrappers/rust/tests/quicktx_integration_test.rs
@@ -8,7 +8,7 @@
//! cd wrappers/rust && DYLD_LIBRARY_PATH=../../core/build/native/nativeCompile \
//! cargo test --test quicktx_integration_test -- --test-threads=1
-use ccl::{Amount, Bridge, ProviderConfig};
+use ccl::Bridge;
use serde_json::{json, Value};
use std::thread;
use std::time::Duration;
@@ -144,9 +144,26 @@ fn total_lovelace(utxos: &Value) -> u64 {
// --- Integration Tests ---
+fn payment_yaml(from: &str, to: &str, quantity: &str) -> String {
+ format!(
+ "version: 1.0\n\
+ transaction:\n\
+ \x20 - tx:\n\
+ \x20 from: {from}\n\
+ \x20 intents:\n\
+ \x20 - type: payment\n\
+ \x20 address: {to}\n\
+ \x20 amounts:\n\
+ \x20 - unit: lovelace\n\
+ \x20 quantity: \"{quantity}\"\n"
+ )
+}
+
#[test]
fn test_integration_simple_ada_transfer() {
- if skip_if_no_devkit() { return; }
+ if skip_if_no_devkit() {
+ return;
+ }
devkit_reset();
wait_for_block();
@@ -157,110 +174,30 @@ fn test_integration_simple_ada_transfer() {
let utxos = devkit_get_utxos(&sender);
let pp = devkit_get_protocol_params();
- // Build
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(5.0)], None, None)
- .from(&sender)
- .with_utxos(utxos)
- .with_protocol_params(pp)
- .build()
- .expect("build failed");
-
+ let yaml = payment_yaml(&sender, &receiver, "5000000");
+ let result = bridge.quicktx().build(&yaml, &utxos, &pp, None).expect("build failed");
assert!(!result.tx_cbor.is_empty());
assert_eq!(result.tx_hash.len(), 64);
- assert!(result.fee.parse::().unwrap() > 0);
- // Sign
let signed_tx = bridge
.account()
.sign_tx(&mnemonic, ccl::network::TESTNET, 0, 0, &result.tx_cbor)
.expect("sign failed");
-
- // Submit
let tx_hash = devkit_submit_tx(&signed_tx);
assert!(!tx_hash.is_empty());
- // Verify
wait_for_block();
let receiver_utxos = devkit_get_utxos(&receiver);
- let total = total_lovelace(&receiver_utxos);
- assert_eq!(total, 5_000_000, "expected 5 ADA, got {} lovelace", total);
-}
-
-#[test]
-fn test_integration_multiple_receivers() {
- if skip_if_no_devkit() { return; }
-
- let bridge = Bridge::new().expect("create bridge");
- let (sender, mnemonic) = fund_sender(&bridge, 150);
- let (r1, _, _) = get_testnet_account(&bridge);
- let (r2, _, _) = get_testnet_account(&bridge);
-
- let utxos = devkit_get_utxos(&sender);
- let pp = devkit_get_protocol_params();
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&r1, &[Amount::ada(3.0)], None, None)
- .pay_to_address(&r2, &[Amount::ada(2.0)], None, None)
- .from(&sender)
- .with_utxos(utxos)
- .with_protocol_params(pp)
- .build()
- .expect("build failed");
-
- let signed_tx = bridge
- .account()
- .sign_tx(&mnemonic, ccl::network::TESTNET, 0, 0, &result.tx_cbor)
- .expect("sign failed");
- devkit_submit_tx(&signed_tx);
- wait_for_block();
-
- let r1_utxos = devkit_get_utxos(&r1);
- let r2_utxos = devkit_get_utxos(&r2);
- assert_eq!(total_lovelace(&r1_utxos), 3_000_000);
- assert_eq!(total_lovelace(&r2_utxos), 2_000_000);
-}
-
-#[test]
-fn test_integration_with_metadata() {
- if skip_if_no_devkit() { return; }
-
- let bridge = Bridge::new().expect("create bridge");
- let (sender, mnemonic) = fund_sender(&bridge, 150);
- let (receiver, _, _) = get_testnet_account(&bridge);
-
- let utxos = devkit_get_utxos(&sender);
- let pp = devkit_get_protocol_params();
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(2.0)], None, None)
- .attach_metadata(674, json!({"msg": ["Hello from Rust integration"]}))
- .from(&sender)
- .with_utxos(utxos)
- .with_protocol_params(pp)
- .build()
- .expect("build failed");
-
- let signed_tx = bridge
- .account()
- .sign_tx(&mnemonic, ccl::network::TESTNET, 0, 0, &result.tx_cbor)
- .expect("sign failed");
- devkit_submit_tx(&signed_tx);
- wait_for_block();
-
- let tx_info = devkit_get_tx(&result.tx_hash);
- assert!(tx_info.is_some(), "tx not found on-chain");
+ assert_eq!(total_lovelace(&receiver_utxos), 5_000_000);
}
#[test]
fn test_integration_insufficient_funds() {
- if skip_if_no_devkit() { return; }
+ if skip_if_no_devkit() {
+ return;
+ }
+ devkit_reset();
+ wait_for_block();
let bridge = Bridge::new().expect("create bridge");
let (sender, _) = fund_sender(&bridge, 2);
@@ -269,176 +206,7 @@ fn test_integration_insufficient_funds() {
let utxos = devkit_get_utxos(&sender);
let pp = devkit_get_protocol_params();
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(100.0)], None, None)
- .from(&sender)
- .with_utxos(utxos)
- .with_protocol_params(pp)
- .build();
-
- assert!(result.is_err(), "expected error for insufficient funds");
-}
-
-#[test]
-fn test_integration_full_round_trip() {
- if skip_if_no_devkit() { return; }
-
- let bridge = Bridge::new().expect("create bridge");
- let (sender, mnemonic) = fund_sender(&bridge, 150);
- let (receiver, _, _) = get_testnet_account(&bridge);
-
- let utxos = devkit_get_utxos(&sender);
- let pp = devkit_get_protocol_params();
-
- // Build
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(10.0)], None, None)
- .from(&sender)
- .with_utxos(utxos)
- .with_protocol_params(pp)
- .build()
- .expect("build failed");
-
- assert!(!result.tx_cbor.is_empty());
- assert_eq!(result.tx_hash.len(), 64);
-
- // Sign
- let signed_tx = bridge
- .account()
- .sign_tx(&mnemonic, ccl::network::TESTNET, 0, 0, &result.tx_cbor)
- .expect("sign failed");
-
- // Submit
- devkit_submit_tx(&signed_tx);
- wait_for_block();
-
- // Confirm on-chain
- let tx_info = devkit_get_tx(&result.tx_hash);
- assert!(tx_info.is_some(), "tx not found on-chain");
-
- // Check receiver balance
- let receiver_utxos = devkit_get_utxos(&receiver);
- let total = total_lovelace(&receiver_utxos);
- assert_eq!(total, 10_000_000, "expected 10 ADA, got {} lovelace", total);
-}
-
-// --- Provider Config (Java-side lazy UTXO fetching) tests ---
-
-#[test]
-fn test_integration_provider_config_simple_transfer() {
- if skip_if_no_devkit() { return; }
-
- let bridge = Bridge::new().expect("create bridge");
- let (sender, mnemonic) = fund_sender(&bridge, 150);
- let (receiver, _, _) = get_testnet_account(&bridge);
-
- let config = ProviderConfig {
- name: "yaci".to_string(),
- url: DEVKIT_PROVIDER_URL.to_string(),
- api_key: None,
- enable_cost_evaluation: None,
- };
-
- // Build using ProviderConfig — Java fetches UTXOs and PP lazily via HTTP
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(5.0)], None, None)
- .from(&sender)
- .build_with_provider(&config)
- .expect("build with provider failed");
-
- assert!(!result.tx_cbor.is_empty());
- assert_eq!(result.tx_hash.len(), 64);
- assert!(result.fee.parse::().unwrap() > 0);
-
- // Sign and submit
- let signed_tx = bridge
- .account()
- .sign_tx(&mnemonic, ccl::network::TESTNET, 0, 0, &result.tx_cbor)
- .expect("sign failed");
- let tx_hash = devkit_submit_tx(&signed_tx);
- assert!(!tx_hash.is_empty());
-
- wait_for_block();
- let receiver_utxos = devkit_get_utxos(&receiver);
- let total = total_lovelace(&receiver_utxos);
- assert_eq!(total, 5_000_000, "expected 5 ADA, got {} lovelace", total);
-}
-
-#[test]
-fn test_integration_provider_config_multiple_receivers() {
- if skip_if_no_devkit() { return; }
-
- let bridge = Bridge::new().expect("create bridge");
- let (sender, mnemonic) = fund_sender(&bridge, 150);
- let (r1, _, _) = get_testnet_account(&bridge);
- let (r2, _, _) = get_testnet_account(&bridge);
-
- let config = ProviderConfig {
- name: "yaci".to_string(),
- url: DEVKIT_PROVIDER_URL.to_string(),
- api_key: None,
- enable_cost_evaluation: None,
- };
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&r1, &[Amount::ada(3.0)], None, None)
- .pay_to_address(&r2, &[Amount::ada(2.0)], None, None)
- .from(&sender)
- .build_with_provider(&config)
- .expect("build with provider failed");
-
- let signed_tx = bridge
- .account()
- .sign_tx(&mnemonic, ccl::network::TESTNET, 0, 0, &result.tx_cbor)
- .expect("sign failed");
- devkit_submit_tx(&signed_tx);
- wait_for_block();
-
- let r1_utxos = devkit_get_utxos(&r1);
- let r2_utxos = devkit_get_utxos(&r2);
- assert_eq!(total_lovelace(&r1_utxos), 3_000_000);
- assert_eq!(total_lovelace(&r2_utxos), 2_000_000);
-}
-
-#[test]
-fn test_integration_provider_config_with_metadata() {
- if skip_if_no_devkit() { return; }
-
- let bridge = Bridge::new().expect("create bridge");
- let (sender, mnemonic) = fund_sender(&bridge, 150);
- let (receiver, _, _) = get_testnet_account(&bridge);
-
- let config = ProviderConfig {
- name: "yaci".to_string(),
- url: DEVKIT_PROVIDER_URL.to_string(),
- api_key: None,
- enable_cost_evaluation: None,
- };
-
- let result = bridge
- .quicktx()
- .new_tx()
- .pay_to_address(&receiver, &[Amount::ada(2.0)], None, None)
- .attach_metadata(674, json!({"msg": ["Hello from Rust providerConfig"]}))
- .from(&sender)
- .build_with_provider(&config)
- .expect("build with provider failed");
-
- let signed_tx = bridge
- .account()
- .sign_tx(&mnemonic, ccl::network::TESTNET, 0, 0, &result.tx_cbor)
- .expect("sign failed");
- devkit_submit_tx(&signed_tx);
- wait_for_block();
-
- let tx_info = devkit_get_tx(&result.tx_hash);
- assert!(tx_info.is_some(), "tx not found on-chain");
+ let yaml = payment_yaml(&sender, &receiver, "100000000");
+ let result = bridge.quicktx().build(&yaml, &utxos, &pp, None);
+ assert!(result.is_err(), "expected insufficient funds error");
}