Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr-multisig-v1-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
CI_ROUTE_SCENARIOS: ${{ github.event_name == 'workflow_dispatch' && inputs.route_scenarios || '' }}
CI_CONTEXT_PATH: /tmp/ci-wallet-context.json
CI_DREP_ANCHOR_URL: ${{ secrets.CI_DREP_ANCHOR_URL }}
CI_DREP_ANCHOR_JSON: ${{ secrets.CI_DREP_ANCHOR_JSON }}
CI_STAKE_POOL_ID_HEX: ${{ secrets.CI_STAKE_POOL_ID_HEX }}

steps:
Expand Down
1 change: 1 addition & 0 deletions docker-compose.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ services:
CI_ROUTE_CHAIN_REPORT_PATH: ${CI_ROUTE_CHAIN_REPORT_PATH:-/artifacts/ci-route-chain-report.md}
CI_CONTEXT_PATH: ${CI_CONTEXT_PATH:-/tmp/ci-wallet-context.json}
CI_DREP_ANCHOR_URL: ${CI_DREP_ANCHOR_URL:-}
CI_DREP_ANCHOR_JSON: ${CI_DREP_ANCHOR_JSON:-}
CI_STAKE_POOL_ID_HEX: ${CI_STAKE_POOL_ID_HEX:-}
depends_on:
app:
Expand Down
48 changes: 43 additions & 5 deletions scripts/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ For each wallet type the scenario runs a pre-hygiene step followed by two sequen

**Main test phases:**

1. Fetch free UTxOs from the wallet, call `POST /api/v1/botDRepCertificate` with `action: "register"` and `anchorUrl`. The API fetches the anchor document and computes the anchor data hash server-side.
1. Fetch free UTxOs from the wallet, call `POST /api/v1/botDRepCertificate` with `action: "register"`, `anchorUrl`, and `anchorJson` (the parsed JSON from `CI_DREP_ANCHOR_JSON`). The API computes the anchor data hash server-side from `anchorJson` — no outbound fetch anywhere.
2. Assert the transaction appears in pending.
3. Signer 1 (`CI_MNEMONIC_2`, index 1) adds a payment-key witness, no broadcast.
4. Signer 2 (`CI_MNEMONIC_3`, index 2) adds a payment-key witness and broadcasts.
Expand Down Expand Up @@ -218,7 +218,8 @@ Primary variables (in workflow/compose):
- `SIGN_BROADCAST`
- `CI_ROUTE_SCENARIOS` (optional scenario id filter)
- `CI_TRANSFER_LOVELACE` (optional transfer amount)
- `CI_DREP_ANCHOR_URL` (required for `scenario.drep-certificates`): publicly reachable URL of a CIP-119 DRep metadata document. The API fetches the document and computes the anchor data hash server-side; only the URL needs to be supplied.
- `CI_DREP_ANCHOR_URL` (required for `scenario.drep-certificates`): the URL string stored in the on-chain anchor — passed as-is to the API, never fetched.
- `CI_DREP_ANCHOR_JSON` (required for `scenario.drep-certificates`): the raw JSON content of the CIP-119 DRep metadata document. Parsed and sent as `anchorJson`; the API computes the anchor data hash server-side — no outbound fetch anywhere. Both vars are forwarded into the `ci-runner` container via `docker-compose.ci.yml`.
- `CI_STAKE_POOL_ID_HEX` (**required** for `scenario.stake-certificates`): hex stake pool id stored in bootstrap context and used as `poolId` in the `register_and_delegate` certificate body.

Validation notes:
Expand Down Expand Up @@ -333,10 +334,28 @@ $env:CI_NETWORK_ID="0"
$env:CI_WALLET_TYPES="legacy,hierarchical,sdk"
$env:CI_TRANSFER_LOVELACE="2000000"
$env:SIGN_BROADCAST="true"
$env:CI_DREP_ANCHOR_URL="https://..." # required for scenario.drep-certificates
$env:CI_DREP_ANCHOR_URL="https://..." # required for scenario.drep-certificates; stored as on-chain anchor URL, never fetched
$env:CI_STAKE_POOL_ID_HEX="..." # optional; stored in context for future delegate tests
```

`CI_DREP_ANCHOR_JSON` contains the full CIP-119 JSON document and must be set separately using a PowerShell here-string so the double quotes are preserved:

```powershell
$env:CI_DREP_ANCHOR_JSON = @'
{
"@context": {
"CIP100": "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#",
"CIP119": "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0119/README.md#",
...
},
"hashAlgorithm": "blake2b-256",
"body": { ... }
}
'@
```

In GitHub Actions, store the full JSON as a repository secret — the runner injects it verbatim, no quoting required.

Optional (recommended for full flow):

```powershell
Expand Down Expand Up @@ -404,10 +423,29 @@ export CI_NETWORK_ID="0"
export CI_WALLET_TYPES="legacy,hierarchical,sdk"
export CI_TRANSFER_LOVELACE="2000000"
export SIGN_BROADCAST="true"
export CI_DREP_ANCHOR_URL="https://..." # required for scenario.drep-certificates
export CI_STAKE_POOL_ID_HEX="..." # optional; stored in context for future delegate tests
export CI_DREP_ANCHOR_URL="https://..." # required for scenario.drep-certificates; stored as on-chain anchor URL, never fetched
export CI_STAKE_POOL_ID_HEX="..." # optional; stored in context for future delegate tests
```

`CI_DREP_ANCHOR_JSON` contains the full CIP-119 JSON document and must be set separately using a heredoc so the double quotes are preserved:

```bash
export CI_DREP_ANCHOR_JSON=$(cat <<'EOF'
{
"@context": {
"CIP100": "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#",
"CIP119": "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0119/README.md#",
...
},
"hashAlgorithm": "blake2b-256",
"body": { ... }
}
EOF
)
```

In GitHub Actions, store the full JSON as a repository secret — the runner injects it verbatim, no quoting required.

Optional (recommended for full flow):

```bash
Expand Down
19 changes: 12 additions & 7 deletions scripts/ci/scenarios/steps/certificates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { getDefaultBot } from "../../framework/botContext";
import { authenticateBot } from "../../framework/botAuth";
import { stringifyRedacted } from "../../framework/redact";
import { boolFromEnv } from "../../framework/env";
import { hashDrepAnchor } from "@meshsdk/core";

type ScriptUtxo = {
input: { txHash: string; outputIndex: number };
Expand Down Expand Up @@ -506,16 +505,22 @@ export function createScenarioDRepCertificates(): Scenario {
const sdkReg: { transactionId?: string; spentUtxoRefs?: { txHash: string; outputIndex: number }[] } = {};
const sdkRetire: { transactionId?: string; spentUtxoRefs?: { txHash: string; outputIndex: number }[] } = {};

async function buildDRepRegBody(): Promise<Record<string, unknown>> {
function buildDRepRegBody(): Record<string, unknown> {
const anchorUrl = process.env.CI_DREP_ANCHOR_URL?.trim();
if (!anchorUrl) {
throw new Error("CI_DREP_ANCHOR_URL is required for DRep registration");
}
const res = await fetch(anchorUrl);
if (!res.ok) throw new Error(`Failed to fetch DRep anchor URL: HTTP ${res.status}`);
const json = await res.json() as object;
const anchorDataHash = hashDrepAnchor(json);
return { anchorUrl, anchorDataHash };
const anchorJsonRaw = process.env.CI_DREP_ANCHOR_JSON?.trim();
if (!anchorJsonRaw) {
throw new Error("CI_DREP_ANCHOR_JSON is required for DRep registration");
}
let anchorJson: object;
try {
anchorJson = JSON.parse(anchorJsonRaw) as object;
} catch {
throw new Error("CI_DREP_ANCHOR_JSON is not valid JSON");
}
return { anchorUrl, anchorJson };
}

return {
Expand Down
11 changes: 6 additions & 5 deletions src/pages/api/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ A comprehensive REST API implementation for the multisig wallet application, pro
- **Purpose**: Server-build a DRep **registration** or **retirement** transaction (non-proxy flows only), then persist or submit like `addTransaction`.
- **Authentication**: Same as `botStakeCertificate` (JWT; body `address` must match JWT; bots need **`multisig:sign`** and cosigner access).
- **Wallet support**: **Summon** wallets return **400** (unsupported in v1). **Legacy** and **SDK** paths mirror `registerDrep` / `retire` in the app (script and change-address selection). If DRep metadata cannot be derived (`getDRep` / `dRepId`), the handler returns **400**.
- **Register — anchor**: `anchorUrl` is required. The server performs an HTTPS fetch (timeout, size limit, SSRF hardening), expects **JSON**, and computes **`hashDrepAnchor`** from `@meshsdk/core`. Optional `anchorDataHash` must match the computed hash or the request fails (**400**).
- **Register — anchor**: `anchorUrl` and `anchorJson` are both required. The caller provides the JSON document at `anchorUrl` directly in the request body — the server never fetches any URL. The server computes **`hashDrepAnchor`** from `@meshsdk/core` using the provided `anchorJson` object.
- **UTxOs**: Same `utxoRefs` policy as `botStakeCertificate` (chain-resolved, address-validated).
- **Request Body**:
- `walletId`: string (required)
Expand All @@ -129,9 +129,9 @@ A comprehensive REST API implementation for the multisig wallet application, pro
- `utxoRefs`: `{ txHash: string; outputIndex: number }[]` (required)
- `description`: string (optional)
- `anchorUrl`: string (required when `action === "register"`)
- `anchorDataHash`: string (optional; hex verification only)
- `anchorJson`: object (required when `action === "register"`; the JSON document at `anchorUrl` — server computes the hash)
- **Response**: Same pattern as `addTransaction` / `botStakeCertificate` (**201**).
- **Error Handling**: 400 (validation, anchor fetch/hash mismatch, unsupported wallet), 401 (auth), 403 (signer/bot scope/access), 405 (method), 500 (server)
- **Error Handling**: 400 (validation, invalid anchorJson, unsupported wallet), 401 (auth), 403 (signer/bot scope/access), 405 (method), 500 (server)

### Wallet Management

Expand Down Expand Up @@ -554,7 +554,7 @@ await fetch("/api/v1/botStakeCertificate", {
}),
});

// DRep register (anchorUrl returns JSON; server computes anchor hash)
// DRep register — caller supplies anchorUrl + anchorJson; server computes the hash
await fetch("/api/v1/botDRepCertificate", {
method: "POST",
headers: {
Expand All @@ -566,7 +566,8 @@ await fetch("/api/v1/botDRepCertificate", {
address: botPaymentAddress,
action: "register",
utxoRefs: [{ txHash: "...", outputIndex: 0 }],
anchorUrl: "https://example.com/metadata.json",
anchorUrl: "https://example.com/drep-metadata.jsonld",
anchorJson: { "@context": { ... }, "hashAlgorithm": "blake2b-256", "body": { ... } },
}),
});
```
Expand Down
16 changes: 11 additions & 5 deletions src/pages/api/v1/botDRepCertificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createPendingMultisigTransaction } from "@/lib/server/createPendingMult
import type { DbWalletWithLegacy } from "@/types/wallet";
import type { Wallet as AppWallet } from "@/types/wallet";
import type { MultisigWallet } from "@/utils/multisigSDK";
import { hashDrepAnchor } from "@meshsdk/core";

type DRepAction = "register" | "retire";

Expand Down Expand Up @@ -93,7 +94,7 @@ export default async function handler(
utxoRefs?: { txHash: string; outputIndex: number }[];
description?: string;
anchorUrl?: string;
anchorDataHash?: string;
anchorJson?: unknown;
};

const walletId = typeof body.walletId === "string" ? body.walletId : "";
Expand Down Expand Up @@ -176,10 +177,15 @@ export default async function handler(
if (!anchorUrl) {
return res.status(400).json({ error: "anchorUrl is required for register" });
}
const anchorDataHash =
typeof body.anchorDataHash === "string" ? body.anchorDataHash.trim() : "";
if (!anchorDataHash) {
return res.status(400).json({ error: "anchorDataHash is required for register — compute it from the anchor JSON before calling this endpoint" });
const anchorJson = body.anchorJson;
if (anchorJson === null || typeof anchorJson !== "object" || Array.isArray(anchorJson)) {
return res.status(400).json({ error: "anchorJson is required for register — provide the JSON object at anchorUrl so the server can compute the hash" });
}
let anchorDataHash: string;
try {
anchorDataHash = hashDrepAnchor(anchorJson as object);
} catch {
return res.status(400).json({ error: "Failed to compute anchor data hash from anchorJson" });
}

for (const utxo of utxos) {
Expand Down
Loading