mint-day 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/sentry-triage.yml +69 -0
- package/api/classify.ts +2 -0
- package/api/feed.ts +2 -0
- package/api/lib/sentry.ts +28 -0
- package/api/sentry-webhook.ts +91 -0
- package/api/sponsor.ts +2 -0
- package/api/verify.ts +2 -0
- package/dist/index.js +4 -4
- package/dist/services/rare-protocol.d.ts +19 -0
- package/dist/services/rare-protocol.js +77 -0
- package/dist/tools/mint.d.ts +5 -0
- package/dist/tools/mint.js +48 -10
- package/package.json +4 -1
- package/scripts/deploy-rare-collection.mjs +64 -0
- package/site/feed.html +442 -279
- package/site/index.html +40 -7
- package/src/index.ts +4 -4
- package/src/rare-cli.d.ts +37 -0
- package/src/services/rare-protocol.ts +119 -0
- package/src/tools/mint.ts +50 -11
- package/vercel.json +2 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Sentry Auto-Triage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
repository_dispatch:
|
|
5
|
+
types: [sentry_issue]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
issues: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
triage:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
|
|
18
|
+
env:
|
|
19
|
+
SENTRY_TITLE: ${{ github.event.client_payload.title }}
|
|
20
|
+
SENTRY_CULPRIT: ${{ github.event.client_payload.culprit }}
|
|
21
|
+
SENTRY_LEVEL: ${{ github.event.client_payload.level }}
|
|
22
|
+
SENTRY_FIRST_SEEN: ${{ github.event.client_payload.first_seen }}
|
|
23
|
+
SENTRY_COUNT: ${{ github.event.client_payload.count }}
|
|
24
|
+
SENTRY_URL: ${{ github.event.client_payload.url }}
|
|
25
|
+
SENTRY_SHORT_ID: ${{ github.event.client_payload.short_id }}
|
|
26
|
+
SENTRY_TAGS: ${{ toJSON(github.event.client_payload.tags) }}
|
|
27
|
+
SENTRY_METADATA: ${{ toJSON(github.event.client_payload.metadata) }}
|
|
28
|
+
|
|
29
|
+
steps:
|
|
30
|
+
- name: Checkout
|
|
31
|
+
uses: actions/checkout@v4
|
|
32
|
+
|
|
33
|
+
- name: Triage with Claude Code
|
|
34
|
+
uses: anthropics/claude-code-action@v1
|
|
35
|
+
with:
|
|
36
|
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
37
|
+
prompt: |
|
|
38
|
+
A Sentry error was detected in the mint.day codebase. Triage it.
|
|
39
|
+
|
|
40
|
+
## Error Details
|
|
41
|
+
- **Title**: ${SENTRY_TITLE}
|
|
42
|
+
- **Culprit**: ${SENTRY_CULPRIT}
|
|
43
|
+
- **Level**: ${SENTRY_LEVEL}
|
|
44
|
+
- **First seen**: ${SENTRY_FIRST_SEEN}
|
|
45
|
+
- **Count**: ${SENTRY_COUNT}
|
|
46
|
+
- **Sentry URL**: ${SENTRY_URL}
|
|
47
|
+
- **Tags**: ${SENTRY_TAGS}
|
|
48
|
+
- **Metadata**: ${SENTRY_METADATA}
|
|
49
|
+
|
|
50
|
+
## Your job
|
|
51
|
+
|
|
52
|
+
1. Read the relevant source files in `api/` and `src/` to understand the error.
|
|
53
|
+
2. Determine: is this a real bug, or noise (transient network error, user input, rate limit)?
|
|
54
|
+
|
|
55
|
+
**If real bug:**
|
|
56
|
+
- Create a new branch `fix/sentry-${SENTRY_SHORT_ID}`
|
|
57
|
+
- Write the fix
|
|
58
|
+
- Commit and push
|
|
59
|
+
- Open a PR with the Sentry link in the description
|
|
60
|
+
|
|
61
|
+
**If noise:**
|
|
62
|
+
- Create an issue titled "Noise" with label "noise" and explain why it's not actionable
|
|
63
|
+
- Reference the Sentry short ID in the issue body
|
|
64
|
+
|
|
65
|
+
## Context
|
|
66
|
+
- This is a Vercel serverless project (api/ directory has the functions)
|
|
67
|
+
- The MCP server is in src/
|
|
68
|
+
- Contract ABIs and addresses are in the source files
|
|
69
|
+
- Base mainnet RPC can be flaky — transient RPC errors are noise
|
package/api/classify.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
2
|
+
import { captureError } from "./lib/sentry.js";
|
|
2
3
|
|
|
3
4
|
const GROQ_API_KEY = process.env.GROQ_API_KEY;
|
|
4
5
|
const GROQ_MODEL = "llama-3.3-70b-versatile";
|
|
@@ -92,6 +93,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
92
93
|
const parsed = JSON.parse(content);
|
|
93
94
|
return res.status(200).json(parsed);
|
|
94
95
|
} catch (err) {
|
|
96
|
+
captureError(err, { route: "classify" });
|
|
95
97
|
return res.status(500).json({ error: "Classification failed", detail: String(err) });
|
|
96
98
|
}
|
|
97
99
|
}
|
package/api/feed.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
2
2
|
import { ethers } from "ethers";
|
|
3
|
+
import { captureError } from "./lib/sentry.js";
|
|
3
4
|
|
|
4
5
|
const RPC_URL = process.env.BASE_RPC_URL || "https://base-mainnet.public.blastapi.io";
|
|
5
6
|
const CONTRACT = process.env.MINT_FACTORY_ADDRESS || "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
|
|
@@ -66,6 +67,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
66
67
|
tokens: tokens.reverse(),
|
|
67
68
|
});
|
|
68
69
|
} catch (err: any) {
|
|
70
|
+
captureError(err, { route: "feed" });
|
|
69
71
|
res.status(500).json({ error: err.message || "Failed to fetch feed" });
|
|
70
72
|
}
|
|
71
73
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Lightweight Sentry error reporting for Vercel serverless functions
|
|
2
|
+
// No framework SDK needed — just the Node SDK
|
|
3
|
+
|
|
4
|
+
import * as Sentry from "@sentry/node";
|
|
5
|
+
|
|
6
|
+
const SENTRY_DSN = process.env.SENTRY_DSN;
|
|
7
|
+
|
|
8
|
+
if (SENTRY_DSN) {
|
|
9
|
+
Sentry.init({
|
|
10
|
+
dsn: SENTRY_DSN,
|
|
11
|
+
environment: process.env.VERCEL_ENV || "development",
|
|
12
|
+
release: process.env.VERCEL_GIT_COMMIT_SHA,
|
|
13
|
+
tracesSampleRate: 1.0,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { Sentry };
|
|
18
|
+
|
|
19
|
+
export function captureError(err: unknown, context?: Record<string, string>) {
|
|
20
|
+
if (!SENTRY_DSN) {
|
|
21
|
+
console.error("[sentry-disabled]", err);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (context) {
|
|
25
|
+
Sentry.setContext("mint.day", context);
|
|
26
|
+
}
|
|
27
|
+
Sentry.captureException(err);
|
|
28
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
2
|
+
import { createHmac } from "crypto";
|
|
3
|
+
|
|
4
|
+
// Sentry sends webhooks when new issues are created.
|
|
5
|
+
// This endpoint validates the signature and dispatches a
|
|
6
|
+
// repository_dispatch event to GitHub so the Claude Code Action
|
|
7
|
+
// can triage + fix.
|
|
8
|
+
|
|
9
|
+
const SENTRY_CLIENT_SECRET = process.env.SENTRY_CLIENT_SECRET;
|
|
10
|
+
const GITHUB_TOKEN = process.env.GH_DISPATCH_TOKEN;
|
|
11
|
+
const GITHUB_REPO = "jordanlyall/mintday";
|
|
12
|
+
|
|
13
|
+
function verifySentrySignature(
|
|
14
|
+
body: string,
|
|
15
|
+
signature: string | undefined,
|
|
16
|
+
secret: string
|
|
17
|
+
): boolean {
|
|
18
|
+
if (!signature) return false;
|
|
19
|
+
const hmac = createHmac("sha256", secret);
|
|
20
|
+
hmac.update(body, "utf8");
|
|
21
|
+
const digest = hmac.digest("hex");
|
|
22
|
+
return signature === digest;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
26
|
+
if (req.method !== "POST") {
|
|
27
|
+
return res.status(405).json({ error: "POST only" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!SENTRY_CLIENT_SECRET || !GITHUB_TOKEN) {
|
|
31
|
+
return res.status(500).json({ error: "Missing env vars" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Verify Sentry webhook signature
|
|
35
|
+
const rawBody = JSON.stringify(req.body);
|
|
36
|
+
const signature = req.headers["sentry-hook-signature"] as string | undefined;
|
|
37
|
+
|
|
38
|
+
if (!verifySentrySignature(rawBody, signature, SENTRY_CLIENT_SECRET)) {
|
|
39
|
+
return res.status(401).json({ error: "Invalid signature" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const resource = req.headers["sentry-hook-resource"] as string;
|
|
43
|
+
|
|
44
|
+
// Only act on new issues (not updates/resolves)
|
|
45
|
+
if (resource !== "issue" || req.body?.action !== "created") {
|
|
46
|
+
return res.status(200).json({ status: "ignored", resource, action: req.body?.action });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const issue = req.body.data?.issue;
|
|
50
|
+
if (!issue) {
|
|
51
|
+
return res.status(200).json({ status: "no issue data" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract useful context for the agent
|
|
55
|
+
const payload = {
|
|
56
|
+
title: issue.title,
|
|
57
|
+
culprit: issue.culprit,
|
|
58
|
+
level: issue.level,
|
|
59
|
+
first_seen: issue.firstSeen,
|
|
60
|
+
count: issue.count,
|
|
61
|
+
url: issue.permalink,
|
|
62
|
+
short_id: issue.shortId,
|
|
63
|
+
metadata: issue.metadata,
|
|
64
|
+
tags: issue.tags?.map((t: { key: string; value: string }) => `${t.key}:${t.value}`),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Dispatch to GitHub Actions
|
|
68
|
+
const ghRes = await fetch(
|
|
69
|
+
`https://api.github.com/repos/${GITHUB_REPO}/dispatches`,
|
|
70
|
+
{
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
Authorization: `Bearer ${GITHUB_TOKEN}`,
|
|
74
|
+
Accept: "application/vnd.github+json",
|
|
75
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
event_type: "sentry_issue",
|
|
79
|
+
client_payload: payload,
|
|
80
|
+
}),
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (!ghRes.ok) {
|
|
85
|
+
const err = await ghRes.text();
|
|
86
|
+
console.error("GitHub dispatch failed:", err);
|
|
87
|
+
return res.status(502).json({ error: "GitHub dispatch failed" });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return res.status(200).json({ status: "dispatched", issue: payload.short_id });
|
|
91
|
+
}
|
package/api/sponsor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
2
2
|
import { ethers } from "ethers";
|
|
3
|
+
import { captureError } from "./lib/sentry.js";
|
|
3
4
|
|
|
4
5
|
const PRIVATE_KEY = process.env.SPONSORED_KEY;
|
|
5
6
|
const RPC_URL = process.env.BASE_RPC_URL || "https://mainnet.base.org";
|
|
@@ -100,6 +101,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
100
101
|
explorer: `https://basescan.org/tx/${receipt!.hash}`,
|
|
101
102
|
});
|
|
102
103
|
} catch (err) {
|
|
104
|
+
captureError(err, { route: "sponsor", recipient: recipient || "unknown" });
|
|
103
105
|
const message = err instanceof Error ? err.message : String(err);
|
|
104
106
|
return res.status(500).json({ error: `Transaction failed: ${message}` });
|
|
105
107
|
}
|
package/api/verify.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
2
2
|
import { ethers } from "ethers";
|
|
3
|
+
import { captureError } from "./lib/sentry.js";
|
|
3
4
|
|
|
4
5
|
const RPC_URL = process.env.BASE_RPC_URL || "https://base-mainnet.public.blastapi.io";
|
|
5
6
|
const CONTRACT = process.env.MINT_FACTORY_ADDRESS || "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
|
|
@@ -88,6 +89,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
88
89
|
tokens,
|
|
89
90
|
});
|
|
90
91
|
} catch (err: any) {
|
|
92
|
+
captureError(err, { route: "verify", address });
|
|
91
93
|
res.status(500).json({ error: err.message || "Failed to verify" });
|
|
92
94
|
}
|
|
93
95
|
}
|
package/dist/index.js
CHANGED
|
@@ -12,11 +12,11 @@ import { mintCheckSchema, handleMintCheck } from "./tools/mint-check.js";
|
|
|
12
12
|
import { mintResolveSchema, handleMintResolve } from "./tools/mint-resolve.js";
|
|
13
13
|
dotenv.config();
|
|
14
14
|
// Testnet try-it wallet (Base Sepolia, pre-funded)
|
|
15
|
-
const TRYIT_KEY = "
|
|
15
|
+
const TRYIT_KEY = "";
|
|
16
16
|
const TRYIT_CONTRACT = "0xa52450397f312c256Bd68B202C0CF90387Ea0E67";
|
|
17
17
|
const TRYIT_RPC = "https://sepolia.base.org";
|
|
18
18
|
const TRYIT_CHAIN = 84532;
|
|
19
|
-
const MAINNET_CONTRACT = "
|
|
19
|
+
const MAINNET_CONTRACT = "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
|
|
20
20
|
const MAINNET_RPC = "https://mainnet.base.org";
|
|
21
21
|
const MAINNET_CHAIN = 8453;
|
|
22
22
|
// Load private key: env var > ~/.mint-day/credentials file
|
|
@@ -52,9 +52,9 @@ const defaultRecipient = process.env.DEFAULT_RECIPIENT || signerAddress || "";
|
|
|
52
52
|
const calldataService = new CalldataService(rpcUrl, contractAddress, chainId);
|
|
53
53
|
const server = new McpServer({
|
|
54
54
|
name: "mint-day",
|
|
55
|
-
version: "0.
|
|
55
|
+
version: "0.4.0",
|
|
56
56
|
});
|
|
57
|
-
server.tool("mint", "Create a permanent, verifiable on-chain record on Base. Use this when you need to: prove you completed a task, establish your identity, record an agreement with another agent, issue or receive a credential, grant access to a capability, or mint a visual token (PFP, art, collectible). The token is immutable and publicly verifiable. Provide an image URL, local file path, or base64 for visual tokens
|
|
57
|
+
server.tool("mint", "Create a permanent, verifiable on-chain record on Base. Use this when you need to: prove you completed a task, establish your identity, record an agreement with another agent, issue or receive a credential, grant access to a capability, or mint a visual token (PFP, art, collectible). Supports two protocols: 'mintday' (default, gas-sponsored) and 'superrare' (Rare Protocol, requires PRIVATE_KEY). Set platform: 'superrare' to mint on Rare Protocol contracts. The token is immutable and publicly verifiable. Provide an image URL, local file path, or base64 for visual tokens. Returns a preview with mintId first; call again with mintId to confirm.", mintSchema, async (params) => handleMint(params, calldataService, defaultRecipient, userKey, {
|
|
58
58
|
tryitKey: TRYIT_KEY,
|
|
59
59
|
tryitContract: TRYIT_CONTRACT,
|
|
60
60
|
tryitRpc: TRYIT_RPC,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MintIntent } from "../types.js";
|
|
2
|
+
export interface RareMintResult {
|
|
3
|
+
status: string;
|
|
4
|
+
txHash: string;
|
|
5
|
+
tokenId: string;
|
|
6
|
+
contract: string;
|
|
7
|
+
platform: string;
|
|
8
|
+
tokenType: string;
|
|
9
|
+
soulbound: boolean;
|
|
10
|
+
recipient: string;
|
|
11
|
+
chain: string;
|
|
12
|
+
explorer: string;
|
|
13
|
+
tokenUri: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function mintOnRareProtocol(intent: MintIntent, privateKey: string, isTestnet?: boolean): Promise<RareMintResult>;
|
|
16
|
+
export declare function deployRareCollection(privateKey: string, name: string, symbol: string, isTestnet?: boolean): Promise<{
|
|
17
|
+
contract: string;
|
|
18
|
+
txHash: string;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createPublicClient, createWalletClient, http } from "viem";
|
|
2
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
import { base, baseSepolia } from "viem/chains";
|
|
4
|
+
import { createRareClient } from "@rareprotocol/rare-cli/client";
|
|
5
|
+
import { encodeMetadata } from "./metadata.js";
|
|
6
|
+
import { TOKEN_TYPE_NAMES } from "../types.js";
|
|
7
|
+
// Default mint.day collection on Rare Protocol (Base mainnet)
|
|
8
|
+
// Set via RARE_CONTRACT env var, or deploy one with scripts/deploy-rare-collection.ts
|
|
9
|
+
const DEFAULT_RARE_CONTRACT = process.env.RARE_CONTRACT || "";
|
|
10
|
+
export async function mintOnRareProtocol(intent, privateKey, isTestnet = false) {
|
|
11
|
+
const chain = isTestnet ? baseSepolia : base;
|
|
12
|
+
const contractAddress = DEFAULT_RARE_CONTRACT;
|
|
13
|
+
if (!contractAddress) {
|
|
14
|
+
throw new Error("RARE_CONTRACT not set. Deploy a collection first: node scripts/deploy-rare-collection.mjs");
|
|
15
|
+
}
|
|
16
|
+
const account = privateKeyToAccount(privateKey);
|
|
17
|
+
const publicClient = createPublicClient({
|
|
18
|
+
chain,
|
|
19
|
+
transport: http(),
|
|
20
|
+
});
|
|
21
|
+
const walletClient = createWalletClient({
|
|
22
|
+
account,
|
|
23
|
+
chain,
|
|
24
|
+
transport: http(),
|
|
25
|
+
});
|
|
26
|
+
// Cast clients to satisfy rare-cli's bundled viem types (runtime-compatible)
|
|
27
|
+
const rare = createRareClient({
|
|
28
|
+
publicClient: publicClient,
|
|
29
|
+
walletClient: walletClient,
|
|
30
|
+
account: account.address,
|
|
31
|
+
});
|
|
32
|
+
// Build metadata URI (same format as MintFactory path)
|
|
33
|
+
const tokenUri = encodeMetadata(intent.metadata);
|
|
34
|
+
// Mint via Rare Protocol
|
|
35
|
+
const result = await rare.mint.mintTo({
|
|
36
|
+
contract: contractAddress,
|
|
37
|
+
tokenUri,
|
|
38
|
+
to: intent.recipient,
|
|
39
|
+
});
|
|
40
|
+
const explorerBase = isTestnet ? "sepolia.basescan.org" : "basescan.org";
|
|
41
|
+
return {
|
|
42
|
+
status: "minted",
|
|
43
|
+
txHash: result.txHash,
|
|
44
|
+
tokenId: result.tokenId?.toString() || "unknown",
|
|
45
|
+
contract: contractAddress,
|
|
46
|
+
platform: "superrare",
|
|
47
|
+
tokenType: TOKEN_TYPE_NAMES[intent.tokenType],
|
|
48
|
+
soulbound: intent.soulbound,
|
|
49
|
+
recipient: intent.recipient,
|
|
50
|
+
chain: isTestnet ? "Base Sepolia (testnet)" : "Base",
|
|
51
|
+
explorer: `https://${explorerBase}/tx/${result.txHash}`,
|
|
52
|
+
tokenUri,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function deployRareCollection(privateKey, name, symbol, isTestnet = false) {
|
|
56
|
+
const chain = isTestnet ? baseSepolia : base;
|
|
57
|
+
const account = privateKeyToAccount(privateKey);
|
|
58
|
+
const publicClient = createPublicClient({
|
|
59
|
+
chain,
|
|
60
|
+
transport: http(),
|
|
61
|
+
});
|
|
62
|
+
const walletClient = createWalletClient({
|
|
63
|
+
account,
|
|
64
|
+
chain,
|
|
65
|
+
transport: http(),
|
|
66
|
+
});
|
|
67
|
+
const rare = createRareClient({
|
|
68
|
+
publicClient: publicClient,
|
|
69
|
+
walletClient: walletClient,
|
|
70
|
+
account: account.address,
|
|
71
|
+
});
|
|
72
|
+
const result = await rare.deploy.erc721({ name, symbol });
|
|
73
|
+
return {
|
|
74
|
+
contract: result.contract || "unknown",
|
|
75
|
+
txHash: result.txHash,
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/tools/mint.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export declare const mintSchema: {
|
|
|
15
15
|
animation_url: z.ZodOptional<z.ZodString>;
|
|
16
16
|
mintId: z.ZodOptional<z.ZodString>;
|
|
17
17
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
18
|
+
platform: z.ZodOptional<z.ZodEnum<{
|
|
19
|
+
superrare: "superrare";
|
|
20
|
+
mintday: "mintday";
|
|
21
|
+
}>>;
|
|
18
22
|
};
|
|
19
23
|
interface TryitConfig {
|
|
20
24
|
tryitKey: string;
|
|
@@ -31,6 +35,7 @@ export declare function handleMint(params: {
|
|
|
31
35
|
animation_url?: string;
|
|
32
36
|
mintId?: string;
|
|
33
37
|
metadata?: Record<string, unknown>;
|
|
38
|
+
platform?: string;
|
|
34
39
|
}, calldataService: CalldataService, defaultRecipient: string, userKey?: string, tryit?: TryitConfig): Promise<{
|
|
35
40
|
content: {
|
|
36
41
|
type: "text";
|
package/dist/tools/mint.js
CHANGED
|
@@ -5,18 +5,19 @@ import { TokenType, TOKEN_TYPE_NAMES } from "../types.js";
|
|
|
5
5
|
import { classifyIntent } from "../services/classifier.js";
|
|
6
6
|
import { CalldataService } from "../services/calldata.js";
|
|
7
7
|
import { resolveImage } from "../services/image-upload.js";
|
|
8
|
-
|
|
8
|
+
import { mintOnRareProtocol } from "../services/rare-protocol.js";
|
|
9
|
+
// Intent cache: preview mintId -> frozen intent + platform
|
|
9
10
|
const intentCache = new Map();
|
|
10
11
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
|
-
function cacheMintId(intent) {
|
|
12
|
+
function cacheMintId(intent, platform = "mintday") {
|
|
12
13
|
const hash = createHash("sha256")
|
|
13
14
|
.update(JSON.stringify(intent))
|
|
14
15
|
.digest("hex")
|
|
15
16
|
.slice(0, 12);
|
|
16
|
-
intentCache.set(hash, { intent, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
17
|
+
intentCache.set(hash, { intent, platform, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
17
18
|
return hash;
|
|
18
19
|
}
|
|
19
|
-
function
|
|
20
|
+
function getCachedEntry(mintId) {
|
|
20
21
|
const entry = intentCache.get(mintId);
|
|
21
22
|
if (!entry)
|
|
22
23
|
return null;
|
|
@@ -24,7 +25,7 @@ function getCachedIntent(mintId) {
|
|
|
24
25
|
intentCache.delete(mintId);
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
|
-
return entry.intent;
|
|
28
|
+
return { intent: entry.intent, platform: entry.platform };
|
|
28
29
|
}
|
|
29
30
|
export const mintSchema = {
|
|
30
31
|
description: z
|
|
@@ -59,8 +60,12 @@ export const mintSchema = {
|
|
|
59
60
|
.record(z.string(), z.unknown())
|
|
60
61
|
.optional()
|
|
61
62
|
.describe("Additional key-value metadata. Supports strings, arrays, and objects (e.g. capabilities: ['mint'])."),
|
|
63
|
+
platform: z
|
|
64
|
+
.enum(["mintday", "superrare"])
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Which protocol to mint on. 'mintday' uses the MintFactory contract (default). 'superrare' mints via Rare Protocol on Base."),
|
|
62
67
|
};
|
|
63
|
-
const SPONSOR_URL = process.env.SPONSOR_URL || "https://
|
|
68
|
+
const SPONSOR_URL = process.env.SPONSOR_URL || "https://www.mint.day/api/sponsor";
|
|
64
69
|
async function sponsoredMint(result, recipient) {
|
|
65
70
|
try {
|
|
66
71
|
const response = await fetch(SPONSOR_URL, {
|
|
@@ -106,8 +111,8 @@ export async function handleMint(params, calldataService, defaultRecipient, user
|
|
|
106
111
|
const privateKey = userKey || (isTestnet ? tryit.tryitKey : "");
|
|
107
112
|
// Confirmation path: replay cached intent
|
|
108
113
|
if (params.mintId) {
|
|
109
|
-
const
|
|
110
|
-
if (!
|
|
114
|
+
const entry = getCachedEntry(params.mintId);
|
|
115
|
+
if (!entry) {
|
|
111
116
|
return {
|
|
112
117
|
content: [{
|
|
113
118
|
type: "text",
|
|
@@ -115,7 +120,38 @@ export async function handleMint(params, calldataService, defaultRecipient, user
|
|
|
115
120
|
}],
|
|
116
121
|
};
|
|
117
122
|
}
|
|
123
|
+
const cached = entry.intent;
|
|
124
|
+
const cachedPlatform = entry.platform;
|
|
118
125
|
intentCache.delete(params.mintId);
|
|
126
|
+
// Rare Protocol path: mint via SuperRare contracts on Base
|
|
127
|
+
if (cachedPlatform === "superrare") {
|
|
128
|
+
if (!privateKey) {
|
|
129
|
+
return {
|
|
130
|
+
content: [{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: JSON.stringify({ error: "SuperRare minting requires a private key. Save one to ~/.mint-day/credentials." }, null, 2),
|
|
133
|
+
}],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const rareResult = await mintOnRareProtocol(cached, privateKey, isTestnet);
|
|
138
|
+
return {
|
|
139
|
+
content: [{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: JSON.stringify(rareResult, null, 2),
|
|
142
|
+
}],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
147
|
+
return {
|
|
148
|
+
content: [{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: JSON.stringify({ error: `SuperRare mint failed: ${message}` }, null, 2),
|
|
151
|
+
}],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
119
155
|
// Use testnet calldataService if in try-it mode
|
|
120
156
|
let activeCalldataService = calldataService;
|
|
121
157
|
if (isTestnet) {
|
|
@@ -326,11 +362,13 @@ export async function handleMint(params, calldataService, defaultRecipient, user
|
|
|
326
362
|
}
|
|
327
363
|
}
|
|
328
364
|
// Cache intent and return preview
|
|
329
|
-
const
|
|
365
|
+
const selectedPlatform = params.platform || "mintday";
|
|
366
|
+
const mintId = cacheMintId(intent, selectedPlatform);
|
|
330
367
|
const preview = {
|
|
331
368
|
status: "preview",
|
|
332
369
|
mintId,
|
|
333
|
-
|
|
370
|
+
platform: selectedPlatform,
|
|
371
|
+
message: `I'll mint a ${TOKEN_TYPE_NAMES[intent.tokenType]} token${intent.soulbound ? " (soulbound)" : ""} to ${intent.recipient} via ${selectedPlatform === "superrare" ? "Rare Protocol (SuperRare)" : "mint.day"}.`,
|
|
334
372
|
tokenType: TOKEN_TYPE_NAMES[intent.tokenType],
|
|
335
373
|
soulbound: intent.soulbound,
|
|
336
374
|
recipient: intent.recipient,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mint-day",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Agent-native minting on Base. One tool call, any token type.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
27
|
+
"@rareprotocol/rare-cli": "^0.3.0",
|
|
28
|
+
"@sentry/node": "^10.45.0",
|
|
27
29
|
"dotenv": "^17.3.1",
|
|
28
30
|
"ethers": "^6.16.0",
|
|
31
|
+
"viem": "^2.47.6",
|
|
29
32
|
"zod": "^4.3.6"
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deploy a mint.day collection on Rare Protocol (SuperRare).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/deploy-rare-collection.mjs [--testnet]
|
|
7
|
+
*
|
|
8
|
+
* Requires: PRIVATE_KEY env var or ~/.mint-day/credentials
|
|
9
|
+
* Output: prints the contract address to set as RARE_CONTRACT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createPublicClient, createWalletClient, http } from "viem";
|
|
13
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
14
|
+
import { base, baseSepolia } from "viem/chains";
|
|
15
|
+
import { createRareClient } from "@rareprotocol/rare-cli/client";
|
|
16
|
+
import { readFileSync, existsSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
|
|
20
|
+
const isTestnet = process.argv.includes("--testnet");
|
|
21
|
+
const chain = isTestnet ? baseSepolia : base;
|
|
22
|
+
|
|
23
|
+
// Load private key
|
|
24
|
+
let privateKey = process.env.PRIVATE_KEY || "";
|
|
25
|
+
if (!privateKey) {
|
|
26
|
+
const credPath = join(homedir(), ".mint-day", "credentials");
|
|
27
|
+
if (existsSync(credPath)) {
|
|
28
|
+
privateKey = readFileSync(credPath, "utf-8").trim();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!privateKey) {
|
|
33
|
+
console.error("No private key found. Set PRIVATE_KEY env var or save to ~/.mint-day/credentials");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const account = privateKeyToAccount(privateKey);
|
|
38
|
+
console.log(`Deploying mint.day collection on ${isTestnet ? "Base Sepolia" : "Base"}...`);
|
|
39
|
+
console.log(`Deployer: ${account.address}`);
|
|
40
|
+
|
|
41
|
+
const publicClient = createPublicClient({ chain, transport: http() });
|
|
42
|
+
const walletClient = createWalletClient({ account, chain, transport: http() });
|
|
43
|
+
|
|
44
|
+
const rare = createRareClient({
|
|
45
|
+
publicClient,
|
|
46
|
+
walletClient,
|
|
47
|
+
account: account.address,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await rare.deploy.erc721({
|
|
52
|
+
name: "mint.day",
|
|
53
|
+
symbol: "MINTDAY",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log(`\nCollection deployed!`);
|
|
57
|
+
console.log(`Contract: ${result.contract}`);
|
|
58
|
+
console.log(`TX: https://${isTestnet ? "sepolia." : ""}basescan.org/tx/${result.txHash}`);
|
|
59
|
+
console.log(`\nSet this in your environment:`);
|
|
60
|
+
console.log(` export RARE_CONTRACT=${result.contract}`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error("Deploy failed:", err.message || err);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|