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
27 changes: 27 additions & 0 deletions .claude/commands/tx-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
description: Fetch a Cardano transaction CBOR from Maestro and print the parsed ECSL JSON
argument-hint: <mainnet|testnet-preprod|testnet-preview> <txHash>
allowed-tools: [Bash]
---

# /tx-info

Runs the script at [apps/example/src/tx-info.ts](apps/example/src/tx-info.ts) to fetch a transaction from Maestro and print its decoded JSON.

## Arguments

`$ARGUMENTS` = `<network> <txHash>`, e.g. `mainnet 1867a29bf1243f5850ccebdeaf58466325c3c8a13be7a057f3063edc1ff0c144`

Valid networks: `mainnet`, `testnet-preprod`, `testnet-preview`.

## Instructions

Run exactly this command from the repo root, passing `$ARGUMENTS` through verbatim:

```bash
cd apps/example && pnpm tsx src/tx-info.ts $ARGUMENTS
```

The script reads `MAESTRO_MAINNET_KEY` / `MAESTRO_PREPROD_KEY` / `MAESTRO_PREVIEW_KEY` from the environment depending on the network. If the corresponding env var is missing, the Maestro fetch returns `{"message": "..."}` rather than `{"data": "..."}` and `ECSL.Transaction.from_hex` will throw — surface that error to the user and suggest setting the right env var.

Comment thread
ljttl3q04t marked this conversation as resolved.
Do not modify or re-interpret the arguments. Print the command's stdout back to the user.
40 changes: 40 additions & 0 deletions Dockerfile.w2
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Image for running CLI scripts in apps/example (e.g. src/w2.ts).
# Same multi-stage turbo-prune layout as Dockerfile.backend.

# Stage 1: Base
FROM node:22-slim AS base
WORKDIR /usr/src/app

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate

ENV PNPM_HOME="/root/.local/share/pnpm"
ENV PATH="${PNPM_HOME}:${PATH}"

# Stage 2: Prune
FROM base AS cleaner
RUN pnpm add -g turbo
COPY . .
ARG APP_NAME=@apps/example
RUN turbo prune ${APP_NAME} --docker

# Stage 3: Install
FROM base AS installer
COPY --from=cleaner /usr/src/app/out/json/ .
COPY --from=cleaner /usr/src/app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile

# Stage 4: Build (compiles all workspace deps of @apps/example)
FROM base AS builder
COPY --from=installer /usr/src/app ./
COPY --from=cleaner /usr/src/app/out/full/ .
COPY turbo.json turbo.json
ARG APP_NAME=@apps/example
RUN pnpm turbo build --filter=${APP_NAME}

# Stage 5: Release
FROM base AS release
COPY --from=builder /usr/src/app ./

# Override in k8s with `args:` — defaults to printing CLI help.
CMD ["pnpm", "--filter=@apps/example", "exec", "tsx", "src/w2.ts", "--help"]
2 changes: 1 addition & 1 deletion apps/example/data/minswap-dex-v2-map-pool.json
Original file line number Diff line number Diff line change
Expand Up @@ -12207,4 +12207,4 @@
"assetA": "lovelace",
"assetB": "cc98659e68801f2827779bbdbb95af5aeaccfdcc80b01e55c5b92f7c.4348414c4945"
}
}
}
2 changes: 1 addition & 1 deletion apps/example/data/sundaeswap-v1-map-pool.json
Original file line number Diff line number Diff line change
Expand Up @@ -5799,4 +5799,4 @@
"assetA": "lovelace",
"assetB": "1dd1a7dde0e1e82761325ee5f4719d0d4b7c24dfba77d9bee01eed4b.4d454f57"
}
}
}
2 changes: 1 addition & 1 deletion apps/example/data/sundaeswap-v3-map-pool.json
Original file line number Diff line number Diff line change
Expand Up @@ -8707,4 +8707,4 @@
"assetA": "8db269c3ec630e06ae29f74bc39edd1f87c819f1056206e879a1cd61.446a65644d6963726f555344",
"assetB": "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad.0014df105553444d"
}
}
}
4 changes: 4 additions & 0 deletions apps/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
"@types/json-bigint": "^1.0.4",
"@types/node": "^22.15.3",
"dpdm": "^3.15.1",
"tsx": "^4.19.4",
"typescript": "5.8.2"
},
"dependencies": {
"@cardano-ogmios/client": "^6.11.0",
"@minswap/felis-build-tx": "workspace:*",
"@minswap/felis-cip": "workspace:*",
"@minswap/felis-dex-v2": "workspace:*",
"@minswap/felis-ledger-core": "workspace:*",
"@minswap/felis-ledger-utils": "workspace:*",
Expand All @@ -28,6 +31,7 @@
"@minswap/felis-sundaeswap-v3": "workspace:*",
"@minswap/felis-syncer": "workspace:*",
"@minswap/felis-tx-builder": "workspace:*",
"@minswap/felis-wingriders-v2": "workspace:*",
"@minswap/tiny-invariant": "^1.2.0",
"socket.io-client": "^4.8.3"
}
Expand Down
26 changes: 21 additions & 5 deletions apps/example/src/cardanoscan.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import fs from "node:fs";
import { Bytes, NetworkEnvironment } from "@minswap/felis-ledger-core";
import { RustModule } from "@minswap/felis-ledger-utils";
import { MinswapStableswapSyncer, MinswapV1Syncer, MinswapV2Syncer, SplashSyncer, SundaeSwapV1Syncer, SundaeSwapV3Syncer, Transaction, WingridersV1Syncer, WingridersV2Syncer } from "@minswap/felis-syncer";
import {
MinswapStableswapSyncer,
MinswapV1Syncer,
MinswapV2Syncer,
SplashSyncer,
SundaeSwapV1Syncer,
SundaeSwapV3Syncer,
Transaction,
WingridersV1Syncer,
WingridersV2Syncer,
} from "@minswap/felis-syncer";
import socketIO from "socket.io-client";
import { Bytes, NetworkEnvironment } from "@minswap/felis-ledger-core";

const main = async () => {
await RustModule.load();
const minswapV2MapPool: MinswapV2Syncer.MapPool = JSON.parse(fs.readFileSync("data/minswap-dex-v2-map-pool.json", "utf-8"));
const sundaeswapV1MapPool: SundaeSwapV1Syncer.MapPool = JSON.parse(fs.readFileSync("data/sundaeswap-v1-map-pool.json", "utf-8"));
const sundaeswapV3MapPool: SundaeSwapV3Syncer.MapPool = JSON.parse(fs.readFileSync("data/sundaeswap-v3-map-pool.json", "utf-8"));
const minswapV2MapPool: MinswapV2Syncer.MapPool = JSON.parse(
fs.readFileSync("data/minswap-dex-v2-map-pool.json", "utf-8"),
);
const sundaeswapV1MapPool: SundaeSwapV1Syncer.MapPool = JSON.parse(
fs.readFileSync("data/sundaeswap-v1-map-pool.json", "utf-8"),
);
const sundaeswapV3MapPool: SundaeSwapV3Syncer.MapPool = JSON.parse(
fs.readFileSync("data/sundaeswap-v3-map-pool.json", "utf-8"),
);

const socketCardano = socketIO("https://socket.cardanoscan.io/private", {
auth: {
Expand Down
15 changes: 9 additions & 6 deletions apps/example/src/fetchMapPools.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import fs from "node:fs";
import { Bytes, NetworkEnvironment, Utxo, XJSON } from "@minswap/felis-ledger-core";
import { getDexV2Configs, PoolV2 } from "@minswap/felis-dex-v2";
import { Bytes, NetworkEnvironment } from "@minswap/felis-ledger-core";
import { KupoService } from "@minswap/felis-provider";
import { MinswapV2Syncer, SundaeSwapV1Syncer, SundaeSwapV3Syncer } from "@minswap/felis-syncer";
import { SundaeSwapV1 } from "@minswap/felis-sundaeswap-v1";
import { SundaeSwapV3 } from "@minswap/felis-sundaeswap-v3";
import type { MinswapV2Syncer, SundaeSwapV1Syncer, SundaeSwapV3Syncer } from "@minswap/felis-syncer";

const fetchMinswapV2Pools = async (kupo: KupoService, networkEnv: NetworkEnvironment) => {
const _fetchMinswapV2Pools = async (kupo: KupoService, networkEnv: NetworkEnvironment) => {
const minswapDexV2Configs = getDexV2Configs(networkEnv);
const poolScriptHash = minswapDexV2Configs.poolEnterpriseAddress.payment.payload;
const poolAuthenAsset = minswapDexV2Configs.poolAuthenAsset;
Expand All @@ -16,7 +16,7 @@ const fetchMinswapV2Pools = async (kupo: KupoService, networkEnv: NetworkEnviron
const mapPool: MinswapV2Syncer.MapPool = JSON.parse(fs.readFileSync("data/minswap-dex-v2-map-pool.json", "utf-8"));
for (const utxo of minswapV2Utxos) {
const pool = PoolV2.fromUtxo(utxo, networkEnv);
if (pool.type === "ok") {``
if (pool.type === "ok") {
const lpAsset = pool.value.lpAsset.toString();
const assetA = pool.value.assetA.toString();
const assetB = pool.value.assetB.toString();
Expand All @@ -30,8 +30,11 @@ const fetchMinswapV2Pools = async (kupo: KupoService, networkEnv: NetworkEnviron
console.log("Minswap V2 | Done fetching map pool");
};

const fetchSundaeswapV1Pools = async (kupo: KupoService, networkEnv: NetworkEnvironment) => {
const sundaeswapV1Utxos = await kupo.utxoAtScriptHashWithPolicyId(Bytes.fromHex(SundaeSwapV1.POOL_SCRIPT_HASH), Bytes.fromHex(SundaeSwapV1.AUTHEN_POLICY_ID));
const _fetchSundaeswapV1Pools = async (kupo: KupoService, networkEnv: NetworkEnvironment) => {
const sundaeswapV1Utxos = await kupo.utxoAtScriptHashWithPolicyId(
Bytes.fromHex(SundaeSwapV1.POOL_SCRIPT_HASH),
Bytes.fromHex(SundaeSwapV1.AUTHEN_POLICY_ID),
);
console.log("Fetched sundaeswap v1 Utxos:", sundaeswapV1Utxos.length);
const mapPool: SundaeSwapV1Syncer.MapPool = JSON.parse(fs.readFileSync("data/sundaeswap-v1-map-pool.json", "utf-8"));
for (const utxo of sundaeswapV1Utxos) {
Expand Down
204 changes: 204 additions & 0 deletions apps/example/src/kupo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* Kupo CLI — query a Kupo indexer for UTxOs, datums, and health.
*
* Usage:
* pnpm tsx src/kupo.ts <query> --network <mainnet|testnet-preprod|testnet-preview> [targets...] [--raw]
*
* Queries (pick one):
* --health Ping the Kupo instance.
* --datum --datum-hash <hash> [...] Resolve one or more datum hashes.
* --utxo <target flags...> Fetch UTxOs matching any of the targets.
*
* Targets for --utxo (repeatable, combined as a union):
* --tx-in <txId#index> UTxO at a specific output reference.
* --address <bech32> All UTxOs at an address.
* --payment-credential <hex> All UTxOs at a payment credential hash.
* --asset <policyId.tokenName|lovelace> UTxOs containing the asset.
* --policy-id <hex> UTxOs containing any asset under the policy.
*
* Output:
* Default: pretty XJSON (BigInt-safe).
* --raw Print one hex-encoded UTxO per line (only with --utxo).
*
* Env:
* KUPO_MAINNET_URL, KUPO_PREPROD_URL, KUPO_PREVIEW_URL override defaults.
*
* Examples:
* pnpm tsx src/kupo.ts --health --network mainnet
* pnpm tsx src/kupo.ts --utxo --network testnet-preprod --address addr_test1...
* pnpm tsx src/kupo.ts --utxo --network mainnet --tx-in abcd...#0 --raw
* pnpm tsx src/kupo.ts --datum --network mainnet --datum-hash <hash>
*/
import { Address, Asset, Bytes, NetworkEnvironment, TxIn, Utxo, XJSON } from "@minswap/felis-ledger-core";
import { RustModule } from "@minswap/felis-ledger-utils";
import { KupoService } from "@minswap/felis-provider";

type Flags = {
// Query kind
utxo: boolean;
health: boolean;
datum: boolean;
// Output
raw: boolean;
// Target
network?: string;
txIns: string[];
addresses: string[];
paymentCredentials: string[];
assets: string[];
policyIds: string[];
datumHashes: string[];
};

const parseArgs = (argv: string[]): Flags => {
const flags: Flags = {
utxo: false,
health: false,
datum: false,
raw: false,
txIns: [],
addresses: [],
paymentCredentials: [],
assets: [],
policyIds: [],
datumHashes: [],
};
const take = (i: number): string => {
const v = argv[i + 1];
if (!v) throw new Error(`Missing value for ${argv[i]}`);
return v;
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
switch (a) {
case "--utxo":
flags.utxo = true;
break;
case "--health":
flags.health = true;
break;
case "--datum":
flags.datum = true;
break;
case "--raw":
flags.raw = true;
break;
case "--network":
flags.network = take(i);
i++;
break;
case "--tx-in":
flags.txIns.push(take(i));
i++;
break;
case "--address":
flags.addresses.push(take(i));
i++;
break;
case "--payment-credential":
flags.paymentCredentials.push(take(i));
i++;
break;
case "--asset":
flags.assets.push(take(i));
i++;
break;
case "--policy-id":
flags.policyIds.push(take(i));
i++;
break;
case "--datum-hash":
flags.datumHashes.push(take(i));
i++;
break;
default:
throw new Error(`Unknown argument: ${a}`);
}
}
return flags;
};

const parseNetwork = (s?: string): NetworkEnvironment => {
switch (s) {
case "mainnet":
return NetworkEnvironment.MAINNET;
case "testnet-preview":
return NetworkEnvironment.TESTNET_PREVIEW;
case "testnet-preprod":
return NetworkEnvironment.TESTNET_PREPROD;
default:
throw new Error(`--network must be one of mainnet|testnet-preprod|testnet-preview (got ${s})`);
}
};

const resolveKupoUrl = (network: NetworkEnvironment): string => {
switch (network) {
case NetworkEnvironment.MAINNET:
return process.env["KUPO_MAINNET_URL"] ?? "http://mainnet-staging:1442";
case NetworkEnvironment.TESTNET_PREPROD:
return process.env["KUPO_PREPROD_URL"] ?? "http://testnet-preprod:1442";
case NetworkEnvironment.TESTNET_PREVIEW:
return process.env["KUPO_PREVIEW_URL"] ?? "http://dev-3:1442";
}
};

const printJson = (v: unknown) => {
console.log(XJSON.stringify(v, 2));
};

const main = async () => {
await RustModule.load();
const flags = parseArgs(process.argv.slice(2));
const network = parseNetwork(flags.network);
const kupo = new KupoService(resolveKupoUrl(network));

if (flags.health) {
printJson(await kupo.health());
return;
}

if (flags.datum) {
if (flags.datumHashes.length === 0) throw new Error("--datum requires --datum-hash <hash>");
const mapDatum = await kupo.getDatums(flags.datumHashes);
printJson(mapDatum);
return;
}

if (!flags.utxo) throw new Error("Specify a query: --utxo | --health | --datum");
const utxos: Utxo[] = [];
if (flags.txIns.length > 0) {
const txIns = flags.txIns.map((s) => TxIn.fromString(s));
utxos.push(...(await kupo.utxosByTxIn(...txIns)));
}
if (flags.addresses.length > 0) {
const addresses = flags.addresses.map((s) => Address.fromBech32(s));
utxos.push(...(await kupo.utxosByAddress(addresses)));
}
for (const pc of flags.paymentCredentials) {
utxos.push(...(await kupo.getUtxosByPaymentCredential(Bytes.fromHex(pc))));
}
for (const asset of flags.assets) {
const kupoUtxos = await kupo.kupoUtxosByAsset(Asset.fromString(asset));
for (const u of kupoUtxos) utxos.push(Utxo.fromKupo(u));
}
for (const policyId of flags.policyIds) {
const kupoUtxos = await kupo.kupoUtxosByPolicyID(Bytes.fromHex(policyId));
for (const u of kupoUtxos) utxos.push(Utxo.fromKupo(u));
}

if (utxos.length === 0) {
console.error("No UTXOs found");
return;
}

if (flags.raw) {
for (const u of utxos) console.log(Utxo.toHex(u));
} else {
printJson(utxos);
}
};

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading
Loading