diff --git a/.gitignore b/.gitignore index 3252825..9bd41c7 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,10 @@ infra/terraform/.terraform* test/ -./scripts/.env \ No newline at end of file +./scripts/.env + +# 2026 budget generated artifacts (keep the directory, ignore its contents) +scripts/budget-2026/output/* +!scripts/budget-2026/output/.gitkeep +# Candidate proposal list generated by budget-proposals-fetch.sh +scripts/budget-2026/proposals.candidate.json \ No newline at end of file diff --git a/README.md b/README.md index cdf7c4b..c9504d0 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This repository holds shell scripts that Intersect uses to engage in Cardano on- - Creates a governance-action JSON-LD file (CIP-108 body + CIP-169 extension + CIP-116 ProposalProcedure on-chain format, including [Intersect CIP108 schemas](https://github.com/IntersectMBO/governance-actions/tree/main/schemas)) - Requires a `.md` input file with H2 sections (`## Title`, `## Abstract`, `## Motivation`, `## Rationale`, `## References`, `## Authors`) - Requires `--governance-action-type ` and `--deposit-return-addr ` + - Optional `--withdrawal-addr ` (treasury only) supplies the withdrawal address non-interactively, skipping the interactive prompt - Optional `--language ` sets the JSON-LD `@context.@language` (default: `en`) - [metadata-validate.sh](./scripts/metadata-validate.sh) @@ -82,7 +83,11 @@ Note: These are really only useful for archival reasons. ### Documentation -- [2025 Budget Treasury Withdrawals](./docs/2025-budget-withdrawals.md) +- [Treasury Withdrawal Action Procedure](./docs/treasury-withdrawal-procedure.md) + - Step-by-step single-item flow for building a treasury withdrawal governance action. +- [Info Action Procedure](./docs/info-action-procedure.md) + - Step-by-step flow for building an info governance action. +- [2025 Budget Treasury Withdrawals](./docs/archive/2025-budget-withdrawals.md) - Documents the scripts and high level process to create the treasury withdrawal governance actions for the Intersect 2025 budget. ## Dependencies diff --git a/docs/budget-2026-procedure.md b/docs/budget-2026-procedure.md new file mode 100644 index 0000000..d7f87ba --- /dev/null +++ b/docs/budget-2026-procedure.md @@ -0,0 +1,130 @@ +# 2026 Budget Treasury Withdrawal Procedure + +Here we document the batch flow for building the ~15 treasury withdrawal governance actions of the +Intersect 2026 budget process. Per-proposal data is pulled from the [hydra voting](https://hydra-voting.intersectmbo.org/votes/cardano-budget-2026) +API and merged into a shared metadata template, so all 15 are built, validated and actioned together. + +This wraps the single-item tooling described in [treasury-withdrawal-procedure.md](./treasury-withdrawal-procedure.md); +read that first to understand each underlying step. All budget-specific files live in `scripts/budget-2026/`. + +> Note: like the single-item flow, this is only set up for treasury withdrawals with a single +> withdrawal address and amount per action. + +## One-time setup + +### 1. Fill in the config + +Edit `scripts/budget-2026/config.sh` and replace every `REPLACEME` value: + +- `WITHDRAWAL_ADDR` — the single funds-receiving stake address (2026 Treasury Reserve Smart Contract stake address), reused by all actions +- `DEPOSIT_RETURN_ADDR` — stake address for the governance-action deposit refund +- `TRSC_STAKE_ADDR` / `TRSC_PAYMENT_ADDR` / `PSSC_PAYMENT_ADDR` — shown verbatim in the Rationale +- `SUCCESSFUL_PROPOSALS_CSV_URL` — link to the successful-proposals CSV (hosted `ipfs://` or `https://` URI) +- `HYDRA_PROPOSAL_URL_BASE` — confirm the public proposal-page path before submitting (the validator checks reference links are reachable) + +### 2. List the proposals + +`scripts/budget-2026/proposals.json` holds one entry per withdrawal: + +```json +[ + { "id": "69fdc9b261c4f060e2fef6c9", "name": "Dano Finance", "title": "Dano Finance: DeFi Kernel, American Options, and Orderbook SDK" } +] +``` + +- `id` — the hydra proposal `_id` (from `…/api/v0/proposals/`) +- `name` — human-readable project name; used in the on-chain title (truncated to 80 chars) and slugified for the output filenames (e.g. `dano-finance.jsonld`) +- `title` *(optional)* — the full proposal title, kept only for readability (e.g. to tell apart multiple projects from the same proposer); not used by the scripts + +To avoid hand-copying IDs, generate a candidate list from the vote (uses `HYDRA_VOTE_ID` +from config), then **prune it to the winning proposals** and copy it into place: + +```shell +./scripts/budget-2026/budget-proposals-fetch.sh +# prints an overview of all submissions and writes proposals.candidate.json +# edit it down to the winners, then: +mv scripts/budget-2026/proposals.candidate.json scripts/budget-2026/proposals.json +``` + +> Note: the hydra API lists **every** submission and has no "successful/passed" filter, +> so you must select the winners yourself (the successful-proposals CSV is the source of truth). + +### 3. Set environment variables + +```shell +source ./scripts/.env +``` + +Make sure `CARDANO_NODE_NETWORK_ID` and `CARDANO_NODE_SOCKET_PATH` are set (needed for step 6). + +## Build the metadata + +### 4. Build all metadata + +For each proposal, this fetches the hydra data, fills `scripts/budget-2026/template.md`, and runs +`metadata-create.sh` to produce an **unsigned** `.jsonld` in `scripts/budget-2026/output/`. + +```shell +./scripts/budget-2026/budget-metadata-build-all.sh +``` + +The on-chain title is composed as `Withdraw ada for administered by Intersect`. Because +`body.title` is capped at 80 characters, `` is truncated to fit; the **full** proposal title is +always preserved at the top of the Abstract. The withdrawal amount comes from `metaData.totalBudget` +(ADA) in the hydra API. + +> Build a single proposal directly while testing: +> `./scripts/budget-2026/budget-metadata-build.sh --name ""` + +### 5. Validate all metadata + +Runs `metadata-validate.sh` (CIP-108 / CIP-169 / Intersect schema) on every file, plus a budget +cross-check that the ada figure in the title matches the on-chain withdrawal amount. Pre-signing, so +it runs `--draft` by default: + +```shell +./scripts/budget-2026/budget-metadata-validate-all.sh +``` + +Pass extra `metadata-validate.sh` flags after `--`, e.g. `-- --no-link-check`. After signing +(step 6), re-run strictly with `--strict` to enforce the non-empty-authors check. + +### 6. Sign each metadata file (manual) + +Author signing is **not** batched. Sign each `.jsonld` in `scripts/budget-2026/output/` with the author +key(s), as in the single-item procedure: + +```shell +./scripts/author-create.sh scripts/budget-2026/output/.jsonld +./scripts/author-validate.sh scripts/budget-2026/output/.jsonld +``` + +Then re-validate strictly: + +```shell +./scripts/budget-2026/budget-metadata-validate-all.sh --strict +``` + +## Build the actions + +### 7. Build all actions + +For each **signed** `.jsonld`, this pins the metadata to IPFS (`ipfs-pin.sh`) and creates the treasury +withdrawal action (`action-create-tw.sh`) using the addresses from `config.sh`. It requires a live +node and refuses to run while the config still holds placeholder addresses. + +```shell +./scripts/budget-2026/budget-action-build-all.sh +``` + +Use `--no-ipfs-pin` if the metadata is already hosted. Each action is written as +`.jsonld.action` (with a `.action.json` view) in `scripts/budget-2026/output/`. + +### 8. Submit on testnet first + +As with all actions, submit on a testnet before mainnet to confirm explorers pick up and render each +action's metadata correctly. Include an action file in a transaction with: + +```shell +cardano-cli latest transaction build --proposal-file scripts/budget-2026/output/.jsonld.action ... +``` diff --git a/scripts/budget-2026/README.md b/scripts/budget-2026/README.md new file mode 100644 index 0000000..e0d09bc --- /dev/null +++ b/scripts/budget-2026/README.md @@ -0,0 +1,53 @@ +# Intersect 2026 Budget — Treasury Withdrawal Pipeline + +Automation layer that turns the ~15 approved 2026 budget proposals into CIP-108/CIP-169 +treasury-withdrawal governance actions, sourcing per-proposal data from the hydra voting API +and filling the standardised metadata template. + +## Files + +| File | Purpose | +|------|---------| +| `config.sh` | Static config: hydra API base, the single Intersect withdrawal + deposit-return stake addresses, the smart-contract addresses shown in the Rationale, and the successful-proposals CSV link. **Fill in the REPLACEME values before real runs.** | +| `template.md` | Tokenised metadata template (`{{TOKEN}}` placeholders), kept compatible with `metadata-create.sh` (plain `## H2` headers, `* [label](url)` references). | +| `proposals.json` | The list of proposals to process: `{ id, name, title? }` per item. `id` is the hydra proposal `_id`; `name` is the human-readable project name, used in the on-chain title (hard-capped at 80 chars — see below) and slugified for the output filenames; `title` (optional) is the full proposal title, kept only for human readability and otherwise ignored. If two entries slug to the same filename (e.g. one proposer with several projects), the duplicates get a `-2`, `-3`… suffix and a warning — give them distinct `name`s to avoid it. | +| `output/` | Generated `.md`, `.jsonld`, `.jsonld.action`, `.jsonld.action.json` artifacts. | + +## Workflow + +All scripts live in this directory (`scripts/budget-2026/`); run them from the repo root. + +```bash +# 0. (optional) Generate a candidate proposals.json from the budget vote, then +# prune it to the winning proposals and copy it to proposals.json +./scripts/budget-2026/budget-proposals-fetch.sh +# -> writes proposals.candidate.json (all submissions; the API has no "successful" filter) + +# 1. Build all unsigned metadata (hydra -> .md -> .jsonld) +./scripts/budget-2026/budget-metadata-build-all.sh + +# 2. Validate all metadata (pre-signing / draft) +./scripts/budget-2026/budget-metadata-validate-all.sh + +# 3. MANUAL: author-sign each .jsonld +# ./scripts/author-create.sh scripts/budget-2026/output/.jsonld ... + +# 4. (optional) re-validate strictly, now that authors are present +./scripts/budget-2026/budget-metadata-validate-all.sh --strict + +# 5. Pin to IPFS + build the on-chain actions (needs a live node + ipfs) +./scripts/budget-2026/budget-action-build-all.sh +``` + +Build a single item directly (handy for testing): + +```bash +./scripts/budget-2026/budget-metadata-build.sh 69fdc9b261c4f060e2fef6c9 --name "Dano Finance" +``` + +## Title length note + +`body.title` is hard-capped at 80 characters by `metadata-validate.sh`. The title is composed as +`Withdraw ada for administered by Intersect`, where `` is the `name` from +`proposals.json` (**truncated** if needed to fit 80 chars). The full, untruncated proposal title is +always preserved at the top of the Abstract. diff --git a/scripts/budget-2026/budget-action-build-all.sh b/scripts/budget-2026/budget-action-build-all.sh new file mode 100755 index 0000000..af9b413 --- /dev/null +++ b/scripts/budget-2026/budget-action-build-all.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# +# budget-action-build-all.sh +# +# For every signed 2026 budget metadata file: pin it to IPFS (ipfs-pin.sh) and +# create the treasury-withdrawal governance action (action-create-tw.sh), using +# the single Intersect withdrawal + deposit-return addresses from config.sh. +# +# Run this AFTER the metadata has been author-signed. Requires a live cardano-cli +# node (CARDANO_NODE_SOCKET_PATH, CARDANO_NODE_NETWORK_ID) and the ipfs CLI. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BUDGET_DIR="$SCRIPT_DIR" +# shellcheck source=../lib/messages.sh +source "$SCRIPTS_DIR/lib/messages.sh" + +CONFIG_FILE="$BUDGET_DIR/config.sh" +DEFAULT_DIR="$BUDGET_DIR/output" + +usage() { + printf '%s%sPin + create treasury-withdrawal actions for all 2026 budget metadata%s\n\n' "$UNDERLINE" "$BOLD" "$NC" + printf 'Syntax:%s %s [%s%s] [%s--no-ipfs-pin%s]\n' "$BOLD" "$0" "$GREEN" "$NC" "$GREEN" "$NC" + print_usage_option "[]" "Directory of signed .jsonld files (default: $(fmt_path "$DEFAULT_DIR"))" + print_usage_option "[--no-ipfs-pin]" "Skip the IPFS pinning step (metadata must already be reachable)" + print_usage_option "-h, --help" "Show this help message and exit" + exit 1 +} + +dir="$DEFAULT_DIR" +skip_pin="false" +while [[ $# -gt 0 ]]; do + case $1 in + --no-ipfs-pin) skip_pin="true"; shift ;; + -h|--help) usage ;; + -*) print_fail "Unknown option: $1"; usage ;; + *) dir="$1"; shift ;; + esac +done + +[ -f "$CONFIG_FILE" ] || { print_fail "Config not found: $(fmt_path "$CONFIG_FILE")"; exit 1; } +[ -d "$dir" ] || { print_fail "Not a directory: $(fmt_path "$dir")"; exit 1; } +# shellcheck source=config.sh +source "$CONFIG_FILE" + +# These addresses move real treasury funds — refuse to run with placeholders. +for var in WITHDRAWAL_ADDR DEPOSIT_RETURN_ADDR; do + if [[ "${!var:-}" == *REPLACEME* || -z "${!var:-}" ]]; then + print_fail "config.sh: $var is not set to a real stake address. Refusing to build on-chain actions." + exit 1 + fi +done + +if [ -z "${CARDANO_NODE_SOCKET_PATH:-}" ] || [ -z "${CARDANO_NODE_NETWORK_ID:-}" ]; then + print_fail "CARDANO_NODE_SOCKET_PATH and CARDANO_NODE_NETWORK_ID must be set (a live node is required)." + exit 1 +fi + +jsonld_files=() +while IFS= read -r -d '' f; do jsonld_files+=("$f"); done \ + < <(find "$dir" -maxdepth 1 -type f -name "*.jsonld" -print0 | sort -z) + +[ "${#jsonld_files[@]}" -eq 0 ] && { print_fail "No .jsonld files found in $(fmt_path "$dir")"; exit 1; } + +print_banner "Building ${#jsonld_files[@]} treasury-withdrawal action(s)" +print_info "Withdrawal address: ${YELLOW}${WITHDRAWAL_ADDR}${NC}" +print_info "Deposit-return address: ${YELLOW}${DEPOSIT_RETURN_ADDR}${NC}" + +ok=0 +for f in "${jsonld_files[@]}"; do + print_section "$(basename "$f")" + + if [ "$skip_pin" = "false" ]; then + if ! "$SCRIPTS_DIR/ipfs-pin.sh" "$f"; then + print_fail "IPFS pinning failed for $(fmt_path "$f"). Stopping." + print_hint "Already-created actions are unaffected; re-run after fixing the issue." + exit 1 + fi + else + print_info "Skipping IPFS pinning step" + fi + + if ! "$SCRIPTS_DIR/action-create-tw.sh" "$f" \ + --deposit-return-addr "$DEPOSIT_RETURN_ADDR" \ + --withdrawal-addr "$WITHDRAWAL_ADDR"; then + print_fail "Action creation failed for $(fmt_path "$f"). Stopping." + print_hint "$ok action(s) created so far; re-run to resume with the remaining files." + exit 1 + fi + ok=$((ok + 1)) +done + +print_section "Summary" +print_pass "Created ${ok}/${#jsonld_files[@]} treasury-withdrawal action(s) in $(fmt_path "$dir")" +print_next "Each action is in .jsonld.action — include it in a transaction:" \ + " cardano-cli latest transaction build --proposal-file .jsonld.action ..." diff --git a/scripts/budget-2026/budget-metadata-build-all.sh b/scripts/budget-2026/budget-metadata-build-all.sh new file mode 100755 index 0000000..2628e43 --- /dev/null +++ b/scripts/budget-2026/budget-metadata-build-all.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# +# budget-metadata-build-all.sh +# +# Build unsigned treasury-withdrawal metadata for every proposal listed in +# proposals.json by calling budget-metadata-build.sh for each. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BUDGET_DIR="$SCRIPT_DIR" +# shellcheck source=../lib/messages.sh +source "$SCRIPTS_DIR/lib/messages.sh" + +DEFAULT_PROPOSALS="$BUDGET_DIR/proposals.candidate.json" +DEFAULT_OUT_DIR="$BUDGET_DIR/output" + +usage() { + printf '%s%sBuild unsigned metadata for all 2026 budget proposals%s\n\n' "$UNDERLINE" "$BOLD" "$NC" + printf 'Syntax:%s %s [%s--proposals%s ] [%s--out-dir%s ]\n' "$BOLD" "$0" "$GREEN" "$NC" "$GREEN" "$NC" + print_usage_option "[--proposals ]" "Proposals list (default: $(fmt_path "$DEFAULT_PROPOSALS"))" + print_usage_option "[--out-dir ]" "Output directory (default: $(fmt_path "$DEFAULT_OUT_DIR"))" + print_usage_option "-h, --help" "Show this help message and exit" + exit 1 +} + +proposals_file="$DEFAULT_PROPOSALS" +out_dir="$DEFAULT_OUT_DIR" +while [[ $# -gt 0 ]]; do + case $1 in + --proposals) if [ -n "${2:-}" ]; then proposals_file="$2"; shift 2; else print_fail "--proposals requires a value"; usage; fi ;; + --out-dir) if [ -n "${2:-}" ]; then out_dir="$2"; shift 2; else print_fail "--out-dir requires a value"; usage; fi ;; + -h|--help) usage ;; + *) print_fail "Unexpected argument: $1"; usage ;; + esac +done + +[ -f "$proposals_file" ] || { print_fail "Proposals file not found: $(fmt_path "$proposals_file")"; exit 1; } +if ! jq -e 'type == "array"' "$proposals_file" >/dev/null 2>&1; then + print_fail "$(fmt_path "$proposals_file") must contain a JSON array of {id, name} objects (an optional 'title' is allowed for readability and ignored)." + exit 1 +fi + +count=$(jq 'length' "$proposals_file") +print_banner "Building metadata for $count proposal(s)" + +# Filesystem-safe slug (matches budget-metadata-build.sh) — used here only to detect +# filename collisions up front so same-named proposals don't silently overwrite. +slugify() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' +} + +ok=0; failed=(); seen_stems="" +# Read entries as TSV so values with spaces survive intact. +while IFS=$'\t' read -r id name; do + [ -z "$id" ] && continue + [ "$id" = "null" ] && { print_fail "An entry is missing 'id' in $(fmt_path "$proposals_file")"; failed+=(""); continue; } + [ "$name" = "null" ] && name="" + + # Resolve the output filename stem and disambiguate collisions (e.g. one proposer + # with several projects all slugging to the same name) instead of overwriting. + base="$(slugify "$name")"; [ -z "$base" ] && base="$id" + prior=$(printf '%s\n' "$seen_stems" | grep -cxF -- "$base" || true) + seen_stems="${seen_stems}${base}"$'\n' + if [ "${prior:-0}" -gt 0 ]; then + stem="${base}-$((prior + 1))" + print_warn "Duplicate output name for '${name:-$id}' -> using '${stem}'. Give it a distinct name in proposals.json to avoid the suffix." + else + stem="$base" + fi + + print_section "Proposal $id (${name:-$id}) -> ${stem}" + args=("$id" --out-dir "$out_dir" --file-name "$stem") + [ -n "$name" ] && args+=(--name "$name") + + if "$SCRIPT_DIR/budget-metadata-build.sh" "${args[@]}"; then + ok=$((ok + 1)) + else + print_fail "Build failed for proposal $id (${name:-$id})" + failed+=("$id (${name:-$id})") + fi +done < <(jq -r '.[] | [.id, (.name // "null")] | @tsv' "$proposals_file") + +print_section "Summary" +print_pass "Built ${ok}/${count} metadata file(s) into $(fmt_path "$out_dir")" +if [ "${#failed[@]}" -gt 0 ]; then + print_fail "${#failed[@]} failed:" + for f in "${failed[@]}"; do print_hint "$f"; done + exit 1 +fi +print_next "Validate them (pre-signing):" \ + " ./scripts/budget-2026/budget-metadata-validate-all.sh" diff --git a/scripts/budget-2026/budget-metadata-build.sh b/scripts/budget-2026/budget-metadata-build.sh new file mode 100755 index 0000000..0f10429 --- /dev/null +++ b/scripts/budget-2026/budget-metadata-build.sh @@ -0,0 +1,240 @@ +#!/bin/bash +# +# budget-metadata-build.sh +# +# Build the CIP-108/CIP-169 treasury-withdrawal metadata for ONE 2026 budget +# proposal + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BUDGET_DIR="$SCRIPT_DIR" +# shellcheck source=../lib/messages.sh +source "$SCRIPTS_DIR/lib/messages.sh" + +CONFIG_FILE="$BUDGET_DIR/config.sh" +TEMPLATE_FILE="$BUDGET_DIR/template.md" +DEFAULT_OUT_DIR="$BUDGET_DIR/output" +MAX_TITLE_LEN=80 + +# --- dependencies --- +for dep in curl jq awk; do + if ! command -v "$dep" >/dev/null 2>&1; then + print_fail "$dep is not installed or not in your PATH." + exit 1 + fi +done + +usage() { + printf '%s%sBuild treasury-withdrawal metadata for one 2026 budget proposal%s\n\n' "$UNDERLINE" "$BOLD" "$NC" + printf 'Syntax:%s %s %s%s [%s--name%s ] [%s--file-name%s ] [%s--out-dir%s ]\n' "$BOLD" "$0" "$GREEN" "$NC" "$GREEN" "$NC" "$GREEN" "$NC" "$GREEN" "$NC" + print_usage_option "" "Hydra proposal _id (e.g. 69fdc9b261c4f060e2fef6c9)" + print_usage_option "[--name ]" "Human-readable project name. Used in the on-chain title (truncated to fit 80 chars) and slugified for output filenames. Defaults to the hydra title / proposal id." + print_usage_option "[--file-name ]" "Override just the output filename stem (slugified). Defaults to the slug of --name. Useful to disambiguate same-named proposals." + print_usage_option "[--out-dir ]" "Output directory (default: $(fmt_path "$DEFAULT_OUT_DIR"))" + print_usage_option "-h, --help" "Show this help message and exit" + exit 1 +} + +# Filesystem-safe slug from a human-readable name: lowercase, non-alphanumerics to +# hyphens, collapsed and trimmed. +slugify() { + printf '%s' "$1" \ + | tr '[:upper:]' '[:lower:]' \ + | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' +} + +# --- args --- +proposal_id="" +proj_name="" +file_name="" +out_dir="$DEFAULT_OUT_DIR" + +while [[ $# -gt 0 ]]; do + case $1 in + --name) + if [ -n "${2:-}" ]; then proj_name="$2"; shift 2; else print_fail "--name requires a value"; usage; fi ;; + --file-name) + if [ -n "${2:-}" ]; then file_name="$2"; shift 2; else print_fail "--file-name requires a value"; usage; fi ;; + --out-dir) + if [ -n "${2:-}" ]; then out_dir="$2"; shift 2; else print_fail "--out-dir requires a value"; usage; fi ;; + -h|--help) + usage ;; + *) + if [ -z "$proposal_id" ]; then proposal_id="$1"; else print_fail "Unexpected argument: $1"; usage; fi + shift ;; + esac +done + +[ -z "$proposal_id" ] && { print_fail "No proposal id specified"; usage; } +[ -f "$CONFIG_FILE" ] || { print_fail "Config not found: $(fmt_path "$CONFIG_FILE")"; exit 1; } +[ -f "$TEMPLATE_FILE" ] || { print_fail "Template not found: $(fmt_path "$TEMPLATE_FILE")"; exit 1; } + +# Output filename stem: explicit --file-name wins, else slug of the project name, +# falling back to the proposal id. +out_name="$proposal_id" +if [ -n "$file_name" ]; then + out_name="$(slugify "$file_name")" +elif [ -n "$proj_name" ]; then + out_name="$(slugify "$proj_name")" +fi +[ -z "$out_name" ] && out_name="$proposal_id" + +# shellcheck source=config.sh +source "$CONFIG_FILE" + +print_banner "Building 2026 budget metadata for proposal $proposal_id" + +# Warn (don't block) on un-filled config so dry runs still work. +for var in WITHDRAWAL_ADDR DEPOSIT_RETURN_ADDR TRSC_STAKE_ADDR TRSC_PAYMENT_ADDR PSSC_PAYMENT_ADDR ; do + if [[ "${!var:-}" == *REPLACEME* ]]; then + print_warn "config.sh: $var still contains a REPLACEME placeholder." + fi +done + +mkdir -p "$out_dir" + +# --- fetch --- +print_section "Fetching proposal from hydra" +PROPOSAL_JSON=$(mktemp /tmp/budget_proposal.XXXXXX) +cleanup() { rm -f "$PROPOSAL_JSON"; } +trap cleanup EXIT + +api_url="${HYDRA_API_BASE%/}/proposals/$proposal_id" +print_info "GET ${YELLOW}${api_url}${NC}" +if ! curl -sSfL "$api_url" -o "$PROPOSAL_JSON"; then + print_fail "Failed to fetch proposal from $api_url" + exit 1 +fi +if ! jq empty "$PROPOSAL_JSON" >/dev/null 2>&1; then + print_fail "Hydra response was not valid JSON." + exit 1 +fi + +# --- extract fields --- +FULL_TITLE=$(jq -r '.title // empty' "$PROPOSAL_JSON") +SUMMARY=$(jq -r '.summary // empty' "$PROPOSAL_JSON") +PILLAR=$(jq -r '.metaData.strategyFramework.pillarRationale // empty' "$PROPOSAL_JSON") +PRIOR_FUNDING=$(jq -r '.metaData.priorFunding // empty' "$PROPOSAL_JSON") +total_budget=$(jq -r '.metaData.totalBudget // empty' "$PROPOSAL_JSON") + +[ -z "$FULL_TITLE" ] && { print_fail "Proposal has no 'title' field."; exit 1; } +[ -z "$total_budget" ] && { print_fail "Proposal has no 'totalBudget' field."; exit 1; } +if [[ ! "$total_budget" =~ ^[0-9]+$ ]]; then + print_fail "totalBudget is not a whole number of ADA: '$total_budget'" + print_hint "Expected an integer ADA amount; got a non-integer. Verify the proposal data." + exit 1 +fi +[ -z "$SUMMARY" ] && print_warn "Proposal 'summary' is empty (Motivation will be blank)." +[ -z "$PILLAR" ] && print_warn "Proposal 'metaData.strategyFramework.pillarRationale' is empty." +[ -z "$PRIOR_FUNDING" ] && print_warn "Proposal 'metaData.priorFunding' is empty." + +print_kv "Title" "$FULL_TITLE" +print_kv "Total budget" "${total_budget} ADA" + +# --- amount, formatted with thousands separators (kept parseable by metadata-create) --- +format_commas() { + awk -v n="$1" 'BEGIN{ + len=length(n); out=""; + for(i=1;i<=len;i++){ out=out substr(n,i,1); rem=len-i; if(rem>0 && rem%3==0) out=out","; } + print out; + }' +} +AMOUNT=$(format_commas "$total_budget") + +# --- compose the on-chain title, truncating the project name to fit 80 chars --- +name_for_title="$FULL_TITLE" +[ -n "$proj_name" ] && name_for_title="$proj_name" + +prefix="Withdraw ${AMOUNT} ada for " +suffix=" administered by Intersect" +avail=$(( MAX_TITLE_LEN - ${#prefix} - ${#suffix} )) + +TITLE_NAME="$name_for_title" +if [ "$avail" -lt 1 ]; then + print_warn "Fixed title wording leaves no room for a project name within ${MAX_TITLE_LEN} chars." + TITLE_NAME="" +elif [ "${#name_for_title}" -gt "$avail" ]; then + TITLE_NAME="${name_for_title:0:$avail}" + # prefer a clean word boundary, then trim trailing space/punctuation + [[ "$TITLE_NAME" == *" "* ]] && TITLE_NAME="${TITLE_NAME% *}" + TITLE_NAME="$(printf '%s' "$TITLE_NAME" | sed -E 's/[[:space:],:;.-]+$//')" + print_warn "Title name truncated to fit ${MAX_TITLE_LEN} chars (full title preserved in the abstract)." +fi + +composed_title="${prefix}${TITLE_NAME}${suffix}" +if [ "${#composed_title}" -gt "$MAX_TITLE_LEN" ]; then + print_fail "Composed title is ${#composed_title} chars (> ${MAX_TITLE_LEN}). Set a shorter --name." + exit 1 +fi +print_kv "On-chain title" "$composed_title" + +# --- build budget breakdown markdown table --- +BUDGET_BREAKDOWN="| Work Package | Total (ADA) |"$'\n' +BUDGET_BREAKDOWN+="|---|---|"$'\n' +while IFS= read -r entry; do + wp_name=$(printf '%s' "$entry" | jq -r '.wp_name') + total=$(printf '%s' "$entry" | jq -r '.total') + formatted_total=$(format_commas "$total") + BUDGET_BREAKDOWN+="| ${wp_name} | ${formatted_total} |"$'\n' +done < <(jq -c '.metaData.proposalDetails.workPackages[] | {wp_name: .name, total: (.budgetBreakdown | map(.total) | add)}' "$PROPOSAL_JSON") +breakdown_sum=$(jq '[.metaData.proposalDetails.workPackages[].budgetBreakdown[].total] | add' "$PROPOSAL_JSON") +# admin_fee=$(( (breakdown_sum * 3 + 99)/ 100 )) +admin_fee=$(($total_budget-$breakdown_sum)) +grand_total=$(( breakdown_sum + admin_fee )) +BUDGET_BREAKDOWN+="| Intersect Budget Administration fee | $(format_commas "$admin_fee") |"$'\n' +BUDGET_BREAKDOWN+="| **Total** | **$(format_commas "$grand_total")** |"$'\n' + +# --- render the template --- +print_section "Rendering template" +md_file="$out_dir/$out_name.md" + +export AMOUNT TITLE_NAME FULL_TITLE SUMMARY PILLAR PRIOR_FUNDING BUDGET_BREAKDOWN +export PROPOSAL_LINK="${HYDRA_PROPOSAL_URL_BASE%/}/$proposal_id" +export TRSC_STAKE_ADDR TRSC_PAYMENT_ADDR PSSC_PAYMENT_ADDR + + +awk ' + function lrep(s, tok, val, p, out) { + out="" + while ((p=index(s, tok)) > 0) { + out = out substr(s, 1, p-1) val + s = substr(s, p+length(tok)) + } + return out s + } + { + line=$0 + line=lrep(line, "{{WITHDRAW_AMOUNT}}", ENVIRON["AMOUNT"]) + line=lrep(line, "{{TITLE_NAME}}", ENVIRON["TITLE_NAME"]) + line=lrep(line, "{{FULL_TITLE}}", ENVIRON["FULL_TITLE"]) + line=lrep(line, "{{PROJECT_HIGH_LEVEL}}", ENVIRON["SUMMARY"]) + line=lrep(line, "{{BUDGET_BREAKDOWN}}", ENVIRON["BUDGET_BREAKDOWN"]) + line=lrep(line, "{{PILLAR_RATIONALE}}", ENVIRON["PILLAR"]) + line=lrep(line, "{{PRIOR_FUNDING}}", ENVIRON["PRIOR_FUNDING"]) + line=lrep(line, "{{HYDRA_PROPOSAL_LINK}}", ENVIRON["PROPOSAL_LINK"]) + line=lrep(line, "{{TRSC_STAKE_ADDR}}", ENVIRON["TRSC_STAKE_ADDR"]) + line=lrep(line, "{{TRSC_PAYMENT_ADDR}}", ENVIRON["TRSC_PAYMENT_ADDR"]) + line=lrep(line, "{{PSSC_PAYMENT_ADDR}}", ENVIRON["PSSC_PAYMENT_ADDR"]) + line=lrep(line, "{{SUCCESSFUL_PROPOSALS_CSV}}", ENVIRON["CSV_URL"]) + print line + } +' "$TEMPLATE_FILE" > "$md_file" + +print_pass "Markdown written to $(fmt_path "$md_file")" + +# --- hand off to metadata-create.sh (produces unsigned .jsonld) --- +print_section "Creating JSON-LD via metadata-create.sh" +"$SCRIPTS_DIR/metadata-create.sh" "$md_file" \ + --governance-action-type treasury \ + --deposit-return-addr "$DEPOSIT_RETURN_ADDR" \ + --withdrawal-addr "$WITHDRAWAL_ADDR" \ + --inline-context + +print_section "Summary" +print_pass "Built unsigned metadata for proposal $proposal_id" +print_kv "Markdown" "$(fmt_path "$md_file")" +print_kv "JSON-LD" "$(fmt_path "$out_dir/$out_name.jsonld")" +print_next "Validate it (pre-signing):" \ + " ./scripts/budget-2026/budget-metadata-validate-all.sh" diff --git a/scripts/budget-2026/budget-metadata-validate-all.sh b/scripts/budget-2026/budget-metadata-validate-all.sh new file mode 100755 index 0000000..3dd2643 --- /dev/null +++ b/scripts/budget-2026/budget-metadata-validate-all.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# +# budget-metadata-validate-all.sh +# +# Validate every generated 2026 budget metadata file: run the standard +# metadata-validate.sh (CIP-108 / CIP-169 / Intersect schema) and a budget +# cross-check that the ada amount in the title matches the on-chain withdrawal. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BUDGET_DIR="$SCRIPT_DIR" +# shellcheck source=../lib/messages.sh +source "$SCRIPTS_DIR/lib/messages.sh" + +DEFAULT_DIR="$BUDGET_DIR/output" + +usage() { + printf '%s%sValidate all 2026 budget metadata files%s\n\n' "$UNDERLINE" "$BOLD" "$NC" + printf 'Syntax:%s %s [%s%s] [%s--strict%s] [-- ]\n' "$BOLD" "$0" "$GREEN" "$NC" "$GREEN" "$NC" + print_usage_option "[]" "Directory of .jsonld files (default: $(fmt_path "$DEFAULT_DIR"))" + print_usage_option "[--strict]" "Validate without --draft (use after author-signing; requires non-empty authors)" + print_usage_option "[-- ...]" "Pass remaining flags through to metadata-validate.sh (e.g. --no-link-check)" + print_usage_option "-h, --help" "Show this help message and exit" + exit 1 +} + +dir="$DEFAULT_DIR" +draft="--draft" +passthrough=() +while [[ $# -gt 0 ]]; do + case $1 in + --strict) draft=""; shift ;; + --) shift; passthrough+=("$@"); break ;; + -h|--help) usage ;; + -*) print_fail "Unknown option: $1 (put metadata-validate.sh flags after --)"; usage ;; + *) dir="$1"; shift ;; + esac +done + +[ -d "$dir" ] || { print_fail "Not a directory: $(fmt_path "$dir")"; exit 1; } + +jsonld_files=() +while IFS= read -r -d '' f; do jsonld_files+=("$f"); done \ + < <(find "$dir" -maxdepth 1 -type f -name "*.jsonld" -print0 | sort -z) + +[ "${#jsonld_files[@]}" -eq 0 ] && { print_fail "No .jsonld files found in $(fmt_path "$dir")"; exit 1; } + +print_banner "Validating ${#jsonld_files[@]} metadata file(s)${draft:+ (draft / pre-signing)}" + +# Budget-specific cross-check: the ada figure stated in body.title must equal the +# on-chain withdrawal amount (lovelace / 1,000,000). +budget_crosscheck() { + local file="$1" title ada lovelace_from_title onchain tag + title=$(jq -r '.body.title // empty' "$file") + tag=$(jq -r '.body.onChain.gov_action.tag // empty' "$file") + onchain=$(jq -r '.body.onChain.gov_action.rewards[0].value // empty' "$file") + + if [ "$tag" != "treasury_withdrawals_action" ]; then + print_fail "gov_action.tag is '${tag:-}', expected 'treasury_withdrawals_action'" + return 1 + fi + ada=$(printf '%s' "$title" | sed -n -E 's/.* ([0-9,]+) ada .*/\1/p' | tr -d ',') + if [ -z "$ada" ]; then + print_fail "Could not parse an ada amount from title: '$title'" + return 1 + fi + lovelace_from_title=$(awk -v a="$ada" 'BEGIN{ printf "%.0f", a*1000000 }') + if [ "$lovelace_from_title" != "$onchain" ]; then + print_fail "Title amount ${ada} ada = ${lovelace_from_title} lovelace, but on-chain value is ${onchain}" + return 1 + fi + print_pass "Title/on-chain amount match: ${ada} ada (${onchain} lovelace)" + return 0 +} + +ok=0; failed=() +for f in "${jsonld_files[@]}"; do + print_section "$(basename "$f")" + file_ok=1 + + if ! "$SCRIPTS_DIR/metadata-validate.sh" "$f" --cip108 --cip169 --intersect-schema ${draft:+$draft} ${passthrough[@]+"${passthrough[@]}"}; then + file_ok=0 + fi + if ! budget_crosscheck "$f"; then + file_ok=0 + fi + + if [ "$file_ok" -eq 1 ]; then + ok=$((ok + 1)) + else + failed+=("$(basename "$f")") + fi +done + +print_section "Summary" +print_pass "Validated ${ok}/${#jsonld_files[@]} file(s)" +if [ "${#failed[@]}" -gt 0 ]; then + print_fail "${#failed[@]} file(s) failed:" + for f in "${failed[@]}"; do print_hint "$f"; done + exit 1 +fi diff --git a/scripts/budget-2026/budget-proposals-fetch.sh b/scripts/budget-2026/budget-proposals-fetch.sh new file mode 100755 index 0000000..ee1335c --- /dev/null +++ b/scripts/budget-2026/budget-proposals-fetch.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# +# Enumerate all proposals in a hydra budget vote and write a CANDIDATE proposals.json + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BUDGET_DIR="$SCRIPT_DIR" +# shellcheck source=../lib/messages.sh +source "$SCRIPTS_DIR/lib/messages.sh" + +CONFIG_FILE="$BUDGET_DIR/config.sh" +DEFAULT_OUT="$BUDGET_DIR/proposals.candidate.json" +MAX_PAGES=100 + +for dep in curl jq; do + command -v "$dep" >/dev/null 2>&1 || { print_fail "$dep is not installed or not in your PATH."; exit 1; } +done + +usage() { + printf '%s%sFetch a budget vote'\''s proposals into a candidate proposals.json%s\n\n' "$UNDERLINE" "$BOLD" "$NC" + printf 'Syntax:%s %s [%s--vote%s ] [%s--out%s ] [%s--force%s]\n' "$BOLD" "$0" "$GREEN" "$NC" "$GREEN" "$NC" "$GREEN" "$NC" + print_usage_option "[--vote ]" "Hydra vote/cycle id (default: \$HYDRA_VOTE_ID from config.sh)" + print_usage_option "[--out ]" "Output file (default: $(fmt_path "$DEFAULT_OUT"))" + print_usage_option "[--force]" "Overwrite the output file if it already exists" + print_usage_option "-h, --help" "Show this help message and exit" + exit 1 +} + +vote_id="" +out_file="$DEFAULT_OUT" +force="false" +while [[ $# -gt 0 ]]; do + case $1 in + --vote) if [ -n "${2:-}" ]; then vote_id="$2"; shift 2; else print_fail "--vote requires a value"; usage; fi ;; + --out) if [ -n "${2:-}" ]; then out_file="$2"; shift 2; else print_fail "--out requires a value"; usage; fi ;; + --force) force="true"; shift ;; + -h|--help) usage ;; + *) print_fail "Unexpected argument: $1"; usage ;; + esac +done + +[ -f "$CONFIG_FILE" ] || { print_fail "Config not found: $(fmt_path "$CONFIG_FILE")"; exit 1; } +# shellcheck source=config.sh +source "$CONFIG_FILE" +[ -z "$vote_id" ] && vote_id="${HYDRA_VOTE_ID:-}" +[ -z "$vote_id" ] && { print_fail "No vote id (pass --vote or set HYDRA_VOTE_ID in config.sh)"; exit 1; } + +if [ -e "$out_file" ] && [ "$force" != "true" ]; then + print_fail "Output file already exists: $(fmt_path "$out_file")" + print_hint "Use --force to overwrite, or --out to write elsewhere." + exit 1 +fi + +print_banner "Fetching proposals for vote $vote_id" + +PAGES_DIR=$(mktemp -d /tmp/budget_pages.XXXXXX) +cleanup() { rm -rf "$PAGES_DIR"; } +trap cleanup EXIT + +page=1 +total="?" +while [ "$page" -le "$MAX_PAGES" ]; do + url="${HYDRA_API_BASE%/}/proposals?vote=${vote_id}&page=${page}" + if ! curl -sSfL "$url" -o "$PAGES_DIR/page-$(printf '%03d' "$page").json"; then + print_fail "Failed to fetch $url" + exit 1 + fi + pfile="$PAGES_DIR/page-$(printf '%03d' "$page").json" + if ! jq -e '.data | type == "array"' "$pfile" >/dev/null 2>&1; then + print_fail "Unexpected response shape on page $page (expected .data array)." + exit 1 + fi + total=$(jq -r '.meta.total // "?"' "$pfile") + got=$(jq -r '.data | length' "$pfile") + print_info "page ${page}: ${got} proposal(s)" + [ "$(jq -r '.meta.hasNextPage // false' "$pfile")" = "true" ] || break + page=$((page + 1)) +done + +# Merge all pages and derive a concise name from each title: +# strip from the first ':' or en/em dash (or ' - '), then trim; fall back to the +# full title when nothing remains. +jq -s ' + [ .[].data[] ] + | map({ + id: ._id, + name: ( ( .title + | gsub("–|—"; ":") + | split(":")[0] + | sub("\\s+-\\s.*$"; "") + | gsub("^\\s+|\\s+$"; "") ) as $n + | if ($n | length) > 0 then $n else .title end ), + title: .title + }) +' "$PAGES_DIR"/page-*.json > "$out_file" + +written=$(jq 'length' "$out_file") + +print_section "Overview (id — budget ADA — title)" +jq -rs '[ .[].data[] ] | .[] | " \(._id) \(.metaData.totalBudget // "?")\tADA \(.title)"' "$PAGES_DIR"/page-*.json + +print_section "Summary" +print_pass "Wrote ${written} candidate proposal(s) to $(fmt_path "$out_file")" +print_info "Vote total reported by API: ${total}" +print_warn "This is EVERY submission — the API has no 'successful' filter." diff --git a/scripts/budget-2026/config.sh b/scripts/budget-2026/config.sh new file mode 100644 index 0000000..07fb6f5 --- /dev/null +++ b/scripts/budget-2026/config.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# +# Static configuration for the Intersect 2026 budget treasury-withdrawal pipeline. +# The budget-* scripts source this file automatically. + +HYDRA_API_BASE="https://hydra-voting.intersectmbo.org/api/v0" + +# Hydra vote (cycle) id for the 2026 budget process. Used by budget-proposals-fetch.sh +# to enumerate proposals via $HYDRA_API_BASE/proposals?vote=$HYDRA_VOTE_ID. +HYDRA_VOTE_ID="69dfeabdc3904a3d239858da" + +HYDRA_PROPOSAL_URL_BASE="https://hydra-voting.intersectmbo.org/votes/cardano-budget-2026" + +WITHDRAWAL_ADDR="stake1784sdxt6jjennmstphgdu7l7c2scf5d02a6cve2dgn5s2kq5u3j9v" +DEPOSIT_RETURN_ADDR="stake1uyvjdz9rxsfsmv44rtk75k2rqyqskrga96dgdfrqjvjjpwsefcjnp" + +# --- Addresses shown verbatim in the Rationale "Budget Management Tooling" section --- +TRSC_STAKE_ADDR="stake1784sdxt6jjennmstphgdu7l7c2scf5d02a6cve2dgn5s2kq5u3j9v" +TRSC_PAYMENT_ADDR="addr1x84sdxt6jjennmstphgdu7l7c2scf5d02a6cve2dgn5s2k8tq6vh499n88hqkrwsmealas4psng674m4sej5638fq4vqmxs59w" +PSSC_PAYMENT_ADDR="addr1x9d6k9z6t6fvsetj2djmerargk475lef9gfvshy4rwh4h7jm4v295h5jepjhy5m9hj86x3dtafljj2sjepwf2xa0t0aq048cay" + diff --git a/scripts/budget-2026/output/.gitkeep b/scripts/budget-2026/output/.gitkeep new file mode 100644 index 0000000..30d3a5d --- /dev/null +++ b/scripts/budget-2026/output/.gitkeep @@ -0,0 +1 @@ +# Generated metadata, action files and IPFS artifacts land here. diff --git a/scripts/budget-2026/proposals.json b/scripts/budget-2026/proposals.json new file mode 100644 index 0000000..597e30b --- /dev/null +++ b/scripts/budget-2026/proposals.json @@ -0,0 +1,57 @@ +[ + { + "id": "69fc5a8785ddd26899aaf208", + "name": "Tx3 by TxPipe", + "title": "Tx3 by TxPipe: Open API Layer for Cardano's dApp Protocols" + }, + { + "id": "69f9dabb92f043aa2df1a282", + "name": "Oura by TxPipe", + "title": "Oura by TxPipe: Maintaining Cardano’s Event Pipeline" + }, + { + "id": "69f9d3db0a4abf2b3c1da737", + "name": "UTxO RPC by TxPipe", + "title": "UTxO RPC by TxPipe: Maintaining Cardano’s Integration Standard, Year 2" + }, + { + "id": "69f9ca0e4b815ecdc8d91a17", + "name": "Dolos by TxPipe", + "title": "Dolos by TxPipe: Maintaining Cardano's Lightweight Data Node, Year 2" + }, + { + "id": "69f8c6dec9c351b28aa8d7b8", + "name": "Pallas by TxPipe", + "title": "Pallas by TxPipe: Maintaining Cardano's Core Rust Libraries, Year 2" + }, + { + "id": "69fae404f9ebc26d057f51bf", + "name": "MLabs Core Tool Maintenance & Enhancement", + "title": "MLabs Core Tool Maintenance & Enhancement: Plutarch and Ply" + }, + { + "id": "69f9d441f86410919eef6bd5", + "name": "Bringing Real-World Payments to Cardano with Wirex", + "title": "Bringing Real-World Payments to Cardano with Wirex" + }, + { + "id": "69f3529e1e63d3708e0dadb6", + "name": "Mithril Protocol", + "title": "Mithril Protocol" + }, + { + "id": "69f38e85985067f4ae957a5e", + "name": "Intersect Technical Steering Committee Support", + "title": "Intersect Technical Steering Committee Support" + }, + { + "id": "69e8b481b637aef81b4fadd7", + "name": "Hardware Wallet Maintenance 2026", + "title": "Hardware Wallet Maintenance 2026" + }, + { + "id": "69e7a834d6a29288536b1200", + "name": "Intersect", + "title": "Intersect: Governance coordination and technical stewardship for the Cardano ecosystem" + } +] \ No newline at end of file diff --git a/scripts/budget-2026/template.md b/scripts/budget-2026/template.md new file mode 100644 index 0000000..1d4420b --- /dev/null +++ b/scripts/budget-2026/template.md @@ -0,0 +1,89 @@ +## Title +Withdraw {{WITHDRAW_AMOUNT}} ada for {{TITLE_NAME}} administered by Intersect + +## Abstract + +This Treasury Withdrawal funds {{FULL_TITLE}}. + +This Treasury Withdrawal is submitted by Intersect on behalf of the vendor. The content for the following sections; Abstract, Motivation and Rationale have been sourced from the approved proposal submitted by the Vendor as part of the Intersect budget process. + +## Motivation + +{{PROJECT_HIGH_LEVEL}} + +## Rationale + +### Strategic Pillar Alignment + +{{PILLAR_RATIONALE}} + +### Intersect Budget Process + +This proposal achieved the required 67% support threshold during the 2026 Intersect Budget Process Hydra Voting phase and has therefore been advanced for on-chain Treasury Withdrawal Governance Action submission in accordance with the approved Budget Process Framework. + +### Net Change Limit Compliance + +The requested amount does not at time of submission, on its own or in aggregate, breach the applicable [350M Net Change Limit](https://explorer.cardano.org/governance-action/gov_action1m3xx08yv788vfxqh6nfvrjtvmqpwezsy0ggaczctkyjmttc2wmxsq4jsr7q) covering Epoch 613 to Epoch 713. +In accordance with the guardrail TREASURY-02a, this withdrawal does not exceed the NCL at the moment of submission. + +### Audit & Oversight + +Audit and oversight costs are included within the overhead applied to this proposal. The Intersect administration fee covers administrative oversight and is reflected within the cost of this proposal. Independent oversight will be provided through Intersect and technically capable third-party, including reporting obligations and milestone-based disbursement controls. + +### Prior Treasury Funding Disclosure + +{{PRIOR_FUNDING}} + +### Budget Summary + +{{BUDGET_BREAKDOWN}} +### Intersect Budget Management Tooling + +To administrate treasury funds on-chain, Intersect will utilize the treasury management smart contract framework developed by Sundae Labs. A new instance of these smart contracts has been deployed for 2026, mirroring the contracts from the 2025 budget cycle. + +- The 2026 Treasury Reserve Smart Contract stake address: `stake1784sdxt6jjennmstphgdu7l7c2scf5d02a6cve2dgn5s2kq5u3j9v` +- The 2026 Treasury Reserve Smart Contract payment address: `addr1x84sdxt6jjennmstphgdu7l7c2scf5d02a6cve2dgn5s2k8tq6vh499n88hqkrwsmealas4psng674m4sej5638fq4vqmxs59w` +- The 2026 Project Specific Smart Contract payment address: `addr1x9d6k9z6t6fvsetj2djmerargk475lef9gfvshy4rwh4h7jm4v295h5jepjhy5m9hj86x3dtafljj2sjepwf2xa0t0aq048cay` + +#### Specifics + +Intersect will utilize a single Treasury Reserve Smart Contract (TRSC), with one Project-Specific Smart Contracts (PSSC). Intersect’s management consists of five ‘admin’ and three Intersect ‘leadership’ roles. An Oversight Committee consisting of six external, independent third-party entities will provide checks and balances on Intersect, and safeguard against errors and unilateral control. The administration of both TRSC and PSSC will be managed by Intersect, with external oversight on certain actions from the Oversight Committee. + +The Oversight Committee consists of Sundae Labs, Cardano Foundation, Dquadrant, NMKR, Sundial and Eternl. Their role is to independently verify key administrative actions using on-chain logic, ensuring accuracy and consistency without exercising discretion over governance decisions. + +For all details on Intersect’s configuration please see the [**Smart Contract Guide**](https://admin-services.docs.intersectmbo.org/governance/smart-contracts) on the knowledgebase. + +The high level permissions are as follows: + +* TRSC Fund and PSSC Modify + * Two of the five Intersect admins, two of the six trusted entities and one of the three Intersect leadership sign-off must authorize +* TRSC Disburse + * Two of five Intersect admins, three of six trusted entities and two of three Intersect leadership sign-off must authorize +* TRSC Pause and Resume + * Two of five Intersect admins, and one of three Intersect leadership sign-off must authorize +* TRSC Sweep + * One of five Intersect admins, and one of three Intersect leadership sign-off must authorize +* TRSC Reorganize + * Two of five Intersect admins and three of six trusted entities must authorize + +#### Processes + +Upon enactment of this governance action, funding for this project will be directed into the TRSC’s stake address. All instances of TRSC and PSSC can not be staked with a SPO and are delegated to the auto-abstain predefined DRep. From here funds will be withdrawn into a UTxO remaining at the TRSC payment address. + +When the Legal contract is prepared and the vendor is ready, funding for this project will be transferred using the Fund action to the PSSC. All milestones will be outlined within the metadata. + +A dashboard is available ([treasury.sundae.fi](https://treasury.sundae.fi/budgets/51486a2f1496d4d3a688a9b111971aa9b731ed045d900b601345ca4e)) for the community to audit the TRSC or PSSC and track metrics related to this withdrawn ada as well as being immutably verifiable on chain. + +## References + +* [Project Proposal via Intersect Budget Process Hydra Voting]({{HYDRA_PROPOSAL_LINK}}) +* [Intersect Budget Process Hydra Voting Documentation](https://docs.hydra-voting.intersectmbo.org/) +* [Intersect Budget Process Hydra Voting Auditor Guide](https://docs.ekklesia.vote/audit/technical) +* [Intersect Budget Process Hydra Voting Final Audited Results (Gitbook)](https://docs.intersectmbo.org/intersect-knowledge-base/cardano-facilitation-services/cardano-budget-2026/2026-budget-process-final-audited-results) +* [Intersect Budget Process Hydra Voting Audit Report (PDF)](ipfs://bafkreibbn432apngjzth2kahjkhp2fgw6zmvwcjnl4w6gvz6j7yq5gyaiu) +* [Automating Accountability: Cardano's Smart Contract Framework Blog](ipfs://bafybeihqx4ae72z7suqfnxrpqpqithp43cai7o2uuewnqtezgaoyc3ptyq) +* [Sundae Labs Budget Management Smart Contracts Github Repository](https://github.com/SundaeSwap-finance/treasury-contracts) +* [Budget Management Smart Contracts TxPipe Audit Report](ipfs://bafybeiccnwejbgj43wo6hrlseckkkmprtoqc5cfuy2hesm6c6yealwho3e) +* [Budget Management Smart Contracts MLabs Audit Report](ipfs://bafybeiah5fnjhda5hemj3qvaehc4mre3qllqzw2l7mkdsguytn4ftgafw4) + +## Authors diff --git a/scripts/metadata-create.sh b/scripts/metadata-create.sh index e5dcb39..f77c70c 100755 --- a/scripts/metadata-create.sh +++ b/scripts/metadata-create.sh @@ -41,10 +41,11 @@ fi # Usage message usage() { printf '%s%sCreate JSON-LD metadata from a Markdown file%s\n\n' "$UNDERLINE" "$BOLD" "$NC" - printf 'Syntax:%s %s %s<.md-file> --governance-action-type%s %s--deposit-return-addr%s [%s--inline-context%s]\n' "$BOLD" "$0" "$GREEN" "$NC" "$GREEN" "$NC" "$GREEN" "$NC" + printf 'Syntax:%s %s %s<.md-file> --governance-action-type%s %s--deposit-return-addr%s [%s--withdrawal-addr%s ] [%s--inline-context%s]\n' "$BOLD" "$0" "$GREEN" "$NC" "$GREEN" "$NC" "$GREEN" "$NC" "$GREEN" "$NC" print_usage_option "<.md-file>" "Path to the .md file as input" print_usage_option "--governance-action-type " "Type of governance action" print_usage_option "--deposit-return-addr " "Stake address for deposit return (bech32)" + print_usage_option "[--withdrawal-addr ]" "Treasury only: withdrawal address (bech32). When set, skips the interactive prompt" print_usage_option "[--inline-context]" "Embed the full @context object in the document instead of referencing the URL" print_usage_option "-h, --help" "Show this help message and exit" exit 1 @@ -54,6 +55,7 @@ usage() { input_file="" governance_action_type="" deposit_return_address="" +withdrawal_address_input="" inline_context="false" # Create temporary files in /tmp/ @@ -98,6 +100,15 @@ while [[ $# -gt 0 ]]; do usage fi ;; + --withdrawal-addr) + if [ -n "${2:-}" ]; then + withdrawal_address_input="$2" + shift 2 + else + print_fail "--withdrawal-addr requires a value" + usage + fi + ;; --inline-context) inline_context="true" shift @@ -397,9 +408,15 @@ generate_ppu_onchain() { } treasury_collect_inputs() { - # Prompt & read address from the TTY - printf 'Please enter withdrawal address: ' >&2 - IFS= read -r T_WITHDRAWAL_ADDRESS &2 + else + printf 'Please enter withdrawal address: ' >&2 + IFS= read -r T_WITHDRAWAL_ADDRESS &2 print_info " Address: ${YELLOW}${T_WITHDRAWAL_ADDRESS}${NC}" >&2 - # confirm with user - if ! confirm "Is this correct?"; then - print_fail "Cancelled by user" - exit 1 + # Skip the interactive confirmation when running non-interactively + # (withdrawal address provided via flag). + if [ -z "$withdrawal_address_input" ]; then + if ! confirm "Is this correct?"; then + print_fail "Cancelled by user" + exit 1 + fi fi }