Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6ec7307
feat: remove manual bot creation flow and implement claim-only regist…
Andre-Diamond Mar 4, 2026
49c0aa0
feat: enhance bot registration and claiming process with new API endp…
Andre-Diamond Mar 5, 2026
6558209
feat: remove manual bot creation flow and implement claim-based onboa…
Andre-Diamond Mar 6, 2026
5b815f4
feat: enhance wallet session management and bot access controls
Andre-Diamond Mar 6, 2026
c48e08b
feat: remove cron job cleanup functionality and related documentation
Andre-Diamond Mar 6, 2026
acc3697
feat: update bot API to require requester address for key operations …
Andre-Diamond Mar 9, 2026
bede315
feat: simplify wallet session checks and streamline API response stru…
Andre-Diamond Mar 10, 2026
72b44b0
feat: refactor wallet connection checks in DRep registration and reti…
Andre-Diamond Mar 11, 2026
ddffe06
feat: remove manual bot creation flow and implement claim-only regist…
Andre-Diamond Mar 4, 2026
4382cb2
feat: enhance bot registration and claiming process with new API endp…
Andre-Diamond Mar 5, 2026
af4bea3
feat: remove manual bot creation flow and implement claim-based onboa…
Andre-Diamond Mar 6, 2026
c12eb76
feat: enhance wallet session management and bot access controls
Andre-Diamond Mar 6, 2026
06947f0
feat: remove cron job cleanup functionality and related documentation
Andre-Diamond Mar 6, 2026
6230312
feat: update bot API to require requester address for key operations …
Andre-Diamond Mar 9, 2026
dad0e69
feat: simplify wallet session checks and streamline API response stru…
Andre-Diamond Mar 10, 2026
685524d
feat: refactor wallet connection checks in DRep registration and reti…
Andre-Diamond Mar 11, 2026
dba55e2
Merge branch 'fix/legacy-drep-retirement' of https://github.com/MeshJ…
Andre-Diamond Mar 11, 2026
0fdc0ed
Merge pull request #210 from MeshJS/fix/legacy-drep-retirement
Andre-Diamond Mar 11, 2026
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ PINATA_JWT="your-pinata-jwt-token"
NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET="your-blockfrost-mainnet-api-key"
NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD="your-blockfrost-preprod-api-key"

# Snapshot Auth Token
# Used to authenticate the balance snapshot batch endpoint
# SNAPSHOT_AUTH_TOKEN="your-snapshot-auth-token"

# Optional: Skip environment validation during builds
# Useful for Docker builds where env vars are set at runtime
# SKIP_ENV_VALIDATION=true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-- CreateEnum
CREATE TYPE "PendingBotStatus" AS ENUM ('UNCLAIMED', 'CLAIMED');

-- CreateTable
CREATE TABLE "PendingBot" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"paymentAddress" TEXT NOT NULL,
"stakeAddress" TEXT,
"requestedScopes" TEXT NOT NULL,
"status" "PendingBotStatus" NOT NULL DEFAULT 'UNCLAIMED',
"claimedBy" TEXT,
"secretCipher" TEXT,
"pickedUp" BOOLEAN NOT NULL DEFAULT false,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "PendingBot_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "BotClaimToken" (
"id" TEXT NOT NULL,
"pendingBotId" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"attempts" INTEGER NOT NULL DEFAULT 0,
"expiresAt" TIMESTAMP(3) NOT NULL,
"consumedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "BotClaimToken_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "PendingBot_paymentAddress_idx" ON "PendingBot"("paymentAddress");

-- CreateIndex
CREATE INDEX "PendingBot_expiresAt_idx" ON "PendingBot"("expiresAt");

-- CreateIndex
CREATE UNIQUE INDEX "BotClaimToken_pendingBotId_key" ON "BotClaimToken"("pendingBotId");

-- CreateIndex
CREATE INDEX "BotClaimToken_tokenHash_idx" ON "BotClaimToken"("tokenHash");

-- AddForeignKey
ALTER TABLE "BotClaimToken" ADD CONSTRAINT "BotClaimToken_pendingBotId_fkey" FOREIGN KEY ("pendingBotId") REFERENCES "PendingBot"("id") ON DELETE CASCADE ON UPDATE CASCADE;
36 changes: 36 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,39 @@ model WalletBotAccess {
@@index([walletId])
@@index([botId])
}

enum PendingBotStatus {
UNCLAIMED
CLAIMED
}

model PendingBot {
id String @id @default(cuid())
name String
paymentAddress String
stakeAddress String?
requestedScopes String // JSON array of requested scopes
status PendingBotStatus @default(UNCLAIMED)
claimedBy String? // ownerAddress of the claiming human
secretCipher String? // Encrypted secret (set on claim, cleared on pickup)
pickedUp Boolean @default(false)
expiresAt DateTime
createdAt DateTime @default(now())
claimToken BotClaimToken?

@@index([paymentAddress])
@@index([expiresAt])
}

model BotClaimToken {
id String @id @default(cuid())
pendingBotId String @unique
pendingBot PendingBot @relation(fields: [pendingBotId], references: [id], onDelete: Cascade)
tokenHash String // SHA-256 hash of the claim code
attempts Int @default(0)
expiresAt DateTime
consumedAt DateTime?
createdAt DateTime @default(now())

@@index([tokenHash])
}
56 changes: 45 additions & 11 deletions scripts/bot-ref/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,30 @@ Minimal client to test the multisig v1 bot API. Use it from the Cursor agent or

## Config

One JSON blob (from the "Create bot" UI or manually):
Use config in two phases:

1. Registration/claim phase (before credentials exist):

```json
{
"baseUrl": "http://localhost:3000",
"botKeyId": "<from Create bot>",
"secret": "<from Create bot, shown once>",
"paymentAddress": "<Cardano payment address for this bot>"
}
```

2. Authenticated phase (after pickup):

```json
{
"baseUrl": "http://localhost:3000",
"botKeyId": "<from GET /api/v1/botPickupSecret>",
"secret": "<from GET /api/v1/botPickupSecret>",
"paymentAddress": "<Cardano payment address for this bot>"
}
```

- **baseUrl**: API base (e.g. `http://localhost:3000` for dev).
- **botKeyId** / **secret**: From the Create bot dialog (copy the JSON blob, fill `paymentAddress`).
- **botKeyId** / **secret**: Returned by `GET /api/v1/botPickupSecret` after a human claims the bot.
- **paymentAddress**: The bot’s **own** Cardano payment address (a wallet the bot controls, not the owner’s address). One bot, one address. Required for `auth` and for all authenticated calls.

Provide config in one of these ways:
Expand All @@ -36,7 +47,29 @@ cd scripts/bot-ref
npm install
```

### 1. Register / get token
### 1. Register -> claim -> pickup -> auth

1. Bot self-registers and receives a claim code:

```bash
curl -sS -X POST http://localhost:3000/api/v1/botRegister \
-H "Content-Type: application/json" \
-d '{"name":"Reference Bot","paymentAddress":"addr1_xxx","scopes":["multisig:read"]}'
```

Response includes `pendingBotId` and `claimCode`.

2. Human claims the bot in the app by entering `pendingBotId` and `claimCode`.

3. Bot picks up credentials:

```bash
curl -sS "http://localhost:3000/api/v1/botPickupSecret?pendingBotId=<pendingBotId>"
```

Response includes `botKeyId` and `secret`.

4. Set config with `botKeyId`, `secret`, and `paymentAddress`, then authenticate to get a JWT:

```bash
BOT_CONFIG='{"baseUrl":"http://localhost:3000","botKeyId":"YOUR_KEY","secret":"YOUR_SECRET","paymentAddress":"addr1_xxx"}' npx tsx bot-client.ts auth
Expand Down Expand Up @@ -77,7 +110,7 @@ npx tsx bot-client.ts freeUtxos <walletId>
npx tsx bot-client.ts botMe
```

Returns the bot’s own info: `botId`, `paymentAddress`, `displayName`, `botName`, **`ownerAddress`** (the address of the human who created the bot). No `paymentAddress` in config needed for this command.
Returns the bot’s own info: `botId`, `paymentAddress`, `displayName`, `botName`, **`ownerAddress`** (the address of the human who claimed the bot). No `paymentAddress` in config needed for this command.

### 6. Owner info

Expand Down Expand Up @@ -111,13 +144,14 @@ From **repo root**: `npx tsx scripts/bot-ref/generate-bot-wallet.ts` — creates
cd scripts/bot-ref && npx tsx create-wallet-us.ts
```

Uses the owner’s address from `botMe` and the bot’s address from config. **The bot must have its own wallet and address** (not the same as the owner). Set `paymentAddress` in `bot-config.json` to the bot’s Cardano address, register it with POST /api/v1/botAuth, then run the script.
Uses the owner’s address from `botMe` and the bot’s address from config. **The bot must have its own wallet and address** (not the same as the owner). Set `paymentAddress` in `bot-config.json` to the bot’s Cardano address, complete register -> claim -> pickup, then run `auth` and this script.

## Cursor agent testing

1. Create a bot in the app (User page → Create bot). Copy the JSON blob and add the bot’s `paymentAddress`.
2. Save as `scripts/bot-ref/bot-config.json` (or pass via `BOT_CONFIG`).
3. Run auth and use the token:
1. Self-register the bot (`POST /api/v1/botRegister`) and capture `pendingBotId` + `claimCode`.
2. Claim it in the app using that ID/code (User page -> Claim a bot).
3. Call `GET /api/v1/botPickupSecret?pendingBotId=...` and place `botKeyId` + `secret` in `scripts/bot-ref/bot-config.json` with the bot `paymentAddress`.
4. Run auth and use the token:

```bash
cd /path/to/multisig/scripts/bot-ref
Expand All @@ -130,7 +164,7 @@ The reference client only uses **bot-key auth** (POST /api/v1/botAuth). Wallet-b

## Governance bot flow

For governance automation, grant these bot scopes when creating the bot key:
For governance automation, request and approve these bot scopes during register/claim:

- `governance:read` to call `GET /api/v1/governanceActiveProposals`
- `ballot:write` to call `POST /api/v1/botBallotsUpsert`
Expand Down
119 changes: 110 additions & 9 deletions scripts/bot-ref/bot-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,29 @@
* Used by Cursor agent and local scripts to test bot flows.
*
* Usage:
* BOT_CONFIG='{"baseUrl":"http://localhost:3000","paymentAddress":"addr1_..."}' npx tsx bot-client.ts register "Reference Bot" multisig:read
* BOT_CONFIG='{"baseUrl":"http://localhost:3000"}' npx tsx bot-client.ts pickup <pendingBotId>
* BOT_CONFIG='{"baseUrl":"http://localhost:3000","botKeyId":"...","secret":"...","paymentAddress":"addr1_..."}' npx tsx bot-client.ts auth
* npx tsx bot-client.ts walletIds
* npx tsx bot-client.ts pendingTransactions <walletId>
*/

export type BotConfig = {
baseUrl: string;
botKeyId: string;
secret: string;
paymentAddress: string;
botKeyId?: string;
secret?: string;
paymentAddress?: string;
};

export async function loadConfig(): Promise<BotConfig> {
const fromEnv = process.env.BOT_CONFIG;
if (fromEnv) {
try {
return JSON.parse(fromEnv) as BotConfig;
const parsed = JSON.parse(fromEnv) as BotConfig;
if (!parsed.baseUrl || typeof parsed.baseUrl !== "string") {
throw new Error("baseUrl is required in config");
}
return parsed;
} catch (e) {
throw new Error("BOT_CONFIG is invalid JSON: " + (e as Error).message);
}
Expand All @@ -31,7 +37,11 @@ export async function loadConfig(): Promise<BotConfig> {
const fullPath = path.startsWith("/") ? path : join(process.cwd(), path);
try {
const raw = readFileSync(fullPath, "utf8");
return JSON.parse(raw) as BotConfig;
const parsed = JSON.parse(raw) as BotConfig;
if (!parsed.baseUrl || typeof parsed.baseUrl !== "string") {
throw new Error("baseUrl is required in config");
}
return parsed;
} catch (e) {
throw new Error(`Failed to load config from ${path}: ${(e as Error).message}`);
}
Expand All @@ -43,6 +53,9 @@ function ensureSlash(url: string): string {

/** Authenticate with bot key + payment address; returns JWT. */
export async function botAuth(config: BotConfig): Promise<{ token: string; botId: string }> {
if (!config.botKeyId || !config.secret || !config.paymentAddress) {
throw new Error("auth requires botKeyId, secret, and paymentAddress in config");
}
const base = ensureSlash(config.baseUrl);
const res = await fetch(`${base}/api/v1/botAuth`, {
method: "POST",
Expand All @@ -61,6 +74,45 @@ export async function botAuth(config: BotConfig): Promise<{ token: string; botId
return { token: data.token, botId: data.botId };
}

/** Register a pending bot and receive a claim code for human claim in UI. */
export async function registerBot(
baseUrl: string,
body: {
name: string;
paymentAddress: string;
requestedScopes: string[];
stakeAddress?: string;
},
): Promise<{ pendingBotId: string; claimCode: string; claimExpiresAt: string }> {
const base = ensureSlash(baseUrl);
const res = await fetch(`${base}/api/v1/botRegister`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`botRegister failed ${res.status}: ${text}`);
}
return (await res.json()) as { pendingBotId: string; claimCode: string; claimExpiresAt: string };
}

/** Pickup claimed bot credentials once human claim is complete. */
export async function pickupBotSecret(
baseUrl: string,
pendingBotId: string,
): Promise<{ botKeyId: string; secret: string; paymentAddress: string }> {
const base = ensureSlash(baseUrl);
const res = await fetch(
`${base}/api/v1/botPickupSecret?pendingBotId=${encodeURIComponent(pendingBotId)}`,
);
if (!res.ok) {
const text = await res.text();
throw new Error(`botPickupSecret failed ${res.status}: ${text}`);
}
return (await res.json()) as { botKeyId: string; secret: string; paymentAddress: string };
}

/** Get wallet IDs for the bot (requires prior auth; pass JWT). */
export async function getWalletIds(baseUrl: string, token: string, address: string): Promise<{ walletId: string; walletName: string }[]> {
const base = ensureSlash(baseUrl);
Expand Down Expand Up @@ -190,8 +242,10 @@ async function main() {
const config = await loadConfig();
const cmd = process.argv[2];
if (!cmd) {
console.error("Usage: bot-client.ts <auth|walletIds|pendingTransactions|freeUtxos|ownerInfo|createWallet> [args]");
console.error(" auth - register/login and print token");
console.error("Usage: bot-client.ts <register|pickup|auth|walletIds|pendingTransactions|freeUtxos|botMe|ownerInfo|createWallet> [args]");
console.error(" register <name> [scope1,scope2,...] [paymentAddress] - create pending bot + claim code");
console.error(" pickup <pendingBotId> - pickup botKeyId + secret after human claim");
console.error(" auth - authenticate and print token");
console.error(" walletIds - list wallet IDs (requires auth first; set BOT_TOKEN)");
console.error(" pendingTransactions <walletId>");
console.error(" freeUtxos <walletId>");
Expand All @@ -202,9 +256,56 @@ async function main() {
process.exit(1);
}

if (cmd === "register") {
const name = process.argv[3];
const scopesArg = process.argv[4] ?? "multisig:read";
const paymentAddress = process.argv[5] ?? config.paymentAddress;

if (!name) {
console.error("Usage: bot-client.ts register <name> [scope1,scope2,...] [paymentAddress]");
process.exit(1);
}

if (!paymentAddress) {
console.error("paymentAddress is required for register (arg or config).");
process.exit(1);
}

const requestedScopes = scopesArg
.split(",")
.map((s) => s.trim())
.filter(Boolean);

if (requestedScopes.length === 0) {
console.error("At least one scope is required for register.");
process.exit(1);
}

const result = await registerBot(config.baseUrl, {
name,
paymentAddress,
requestedScopes,
});
console.log(JSON.stringify(result, null, 2));
console.error("Human must now claim this bot in UI using pendingBotId + claimCode.");
return;
}

if (cmd === "pickup") {
const pendingBotId = process.argv[3];
if (!pendingBotId) {
console.error("Usage: bot-client.ts pickup <pendingBotId>");
process.exit(1);
}
const creds = await pickupBotSecret(config.baseUrl, pendingBotId);
console.log(JSON.stringify(creds, null, 2));
console.error("Store botKeyId + secret in config, then run 'auth'.");
return;
}

if (cmd === "auth") {
if (!config.paymentAddress) {
console.error("paymentAddress is required in config for auth.");
if (!config.paymentAddress || !config.botKeyId || !config.secret) {
console.error("auth requires paymentAddress, botKeyId, and secret in config.");
process.exit(1);
}
const { token, botId } = await botAuth(config);
Expand Down
7 changes: 4 additions & 3 deletions scripts/bot-ref/bot-config.sample.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"baseUrl": "http://localhost:3000",
"botKeyId": "",
"secret": "",
"paymentAddress": ""
"paymentAddress": "addr1_your_bot_payment_address_here",
"pendingBotId": "optional_pending_bot_id_from_register",
"botKeyId": "set_after_pickup",
"secret": "set_after_pickup"
}
2 changes: 1 addition & 1 deletion src/components/common/overall-layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -731,4 +731,4 @@ export default function RootLayout({
)}
</div>
);
}
}
Loading
Loading