mint-day 0.3.0
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/README.md +166 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +78 -0
- package/dist/services/calldata.d.ts +12 -0
- package/dist/services/calldata.js +61 -0
- package/dist/services/classifier.d.ts +2 -0
- package/dist/services/classifier.js +81 -0
- package/dist/services/ens.d.ts +3 -0
- package/dist/services/ens.js +32 -0
- package/dist/services/image-upload.d.ts +11 -0
- package/dist/services/image-upload.js +70 -0
- package/dist/services/metadata.d.ts +2 -0
- package/dist/services/metadata.js +119 -0
- package/dist/tools/mint-check.d.ts +16 -0
- package/dist/tools/mint-check.js +177 -0
- package/dist/tools/mint-resolve.d.ts +13 -0
- package/dist/tools/mint-resolve.js +108 -0
- package/dist/tools/mint.d.ts +43 -0
- package/dist/tools/mint.js +425 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.js +9 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# mint.day
|
|
2
|
+
|
|
3
|
+
Agents mint NFTs now.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
**1. Install** (zero config)
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"mint-day": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "mint-day"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Works with Claude Code, Cursor, Windsurf, OpenClaw, and any MCP client.
|
|
21
|
+
|
|
22
|
+
**2. Mint**
|
|
23
|
+
|
|
24
|
+
Tell your agent: "Proof I completed the security audit for 0xABC"
|
|
25
|
+
|
|
26
|
+
**3. Verify**
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
mint_check({ address: "0xABC..." })
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That's it. Your agent classifies the intent, builds the token, and returns a ready-to-sign transaction.
|
|
33
|
+
|
|
34
|
+
## Signing transactions
|
|
35
|
+
|
|
36
|
+
mint.day can sign and submit transactions directly, or return calldata for your own signer.
|
|
37
|
+
|
|
38
|
+
### Option A: Built-in signing (recommended)
|
|
39
|
+
|
|
40
|
+
Save a private key and mint.day handles everything:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
mkdir -p ~/.mint-day
|
|
44
|
+
node -e "const w = require('ethers').Wallet.createRandom(); console.log(w.privateKey); console.error('Address: ' + w.address)" 2>&1 | head -1 > ~/.mint-day/credentials
|
|
45
|
+
chmod 600 ~/.mint-day/credentials
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Fund the address with a small amount of ETH on Base (gas is < $0.01 per mint). Restart the MCP server.
|
|
49
|
+
|
|
50
|
+
You can also set `PRIVATE_KEY` in your MCP env config instead of using the file.
|
|
51
|
+
|
|
52
|
+
### Option B: Bring your own signer
|
|
53
|
+
|
|
54
|
+
Don't set a private key. mint.day returns calldata. Submit it with Coinbase AgentKit, Lit Protocol, Privy, or any EVM wallet.
|
|
55
|
+
|
|
56
|
+
## Tools
|
|
57
|
+
|
|
58
|
+
### `mint`
|
|
59
|
+
|
|
60
|
+
Create a permanent, verifiable on-chain record on Base.
|
|
61
|
+
|
|
62
|
+
**Natural language:**
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
mint({ description: "Proof I completed the security audit for 0xABC" })
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Structured** (skips classification):
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
mint({
|
|
72
|
+
description: "Task completion attestation",
|
|
73
|
+
tokenType: "Attestation",
|
|
74
|
+
recipient: "0xABC...",
|
|
75
|
+
soulbound: true
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**With an image:**
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
mint({
|
|
83
|
+
description: "My agent identity",
|
|
84
|
+
tokenType: "Identity",
|
|
85
|
+
image: "data:image/png;base64,..."
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Every mint returns a preview first. Confirm with `mintId` to execute:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
mint({ mintId: "a3f8c1e90b2d" })
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
With built-in signing, you get back a tx hash and explorer link. Without it, you get calldata.
|
|
96
|
+
|
|
97
|
+
### `mint_check`
|
|
98
|
+
|
|
99
|
+
Look up tokens by address, transaction hash, or get global stats.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
mint_check({ address: "0xABC..." })
|
|
103
|
+
mint_check({ txHash: "0x230b..." })
|
|
104
|
+
mint_check({}) // global stats
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `mint_resolve`
|
|
108
|
+
|
|
109
|
+
Resolve an agent's on-chain identity. Returns their Identity token with ERC-8004 agent card metadata.
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
mint_resolve({ address: "0xABC..." })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Token types
|
|
116
|
+
|
|
117
|
+
| Type | Default | Use for |
|
|
118
|
+
|------|---------|---------|
|
|
119
|
+
| Identity | transferable | Agent ID card, on-chain registration |
|
|
120
|
+
| Attestation | soulbound | Proof of action, task completion |
|
|
121
|
+
| Credential | soulbound | Reputation anchor, certification |
|
|
122
|
+
| Receipt | transferable | Payment record between two parties |
|
|
123
|
+
| Pass | transferable | API access, capability unlock |
|
|
124
|
+
|
|
125
|
+
All types support `image` and `animation_url` for visual NFTs (PFPs, art, collectibles).
|
|
126
|
+
|
|
127
|
+
## Architecture
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Agent -> mint tool -> classifier (Groq Llama) -> metadata builder -> calldata
|
|
131
|
+
|
|
|
132
|
+
[if PRIVATE_KEY] sign + submit -> tx hash <----|
|
|
133
|
+
[if no key] return calldata <-------------|
|
|
134
|
+
|
|
|
135
|
+
MintFactory.sol (Base)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Contracts
|
|
139
|
+
|
|
140
|
+
- **Base Mainnet**: [`0xbf12d372444dcf69df9316d961439f6b5919e8d0`](https://basescan.org/address/0xbf12d372444dcf69df9316d961439f6b5919e8d0)
|
|
141
|
+
- **Base Sepolia**: [`0xa52450397f312c256Bd68B202C0CF90387Ea0E67`](https://sepolia.basescan.org/address/0xa52450397f312c256Bd68B202C0CF90387Ea0E67)
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
All optional. mint.day works with zero config.
|
|
146
|
+
|
|
147
|
+
| Env var | Default | Purpose |
|
|
148
|
+
|---------|---------|---------|
|
|
149
|
+
| `PRIVATE_KEY` | `~/.mint-day/credentials` | Signs and submits transactions |
|
|
150
|
+
| `BASE_RPC_URL` | `https://mainnet.base.org` | RPC endpoint |
|
|
151
|
+
| `CHAIN_ID` | `8453` | Base Mainnet |
|
|
152
|
+
| `MINT_FACTORY_ADDRESS` | `0xbf12d3...` | Contract address |
|
|
153
|
+
| `CLASSIFY_URL` | hosted endpoint | Intent classifier |
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm install
|
|
159
|
+
npm run dev # run with tsx
|
|
160
|
+
npm run build # compile to dist/
|
|
161
|
+
npm start # run compiled
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { ethers } from "ethers";
|
|
5
|
+
import { readFileSync, existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import dotenv from "dotenv";
|
|
9
|
+
import { CalldataService } from "./services/calldata.js";
|
|
10
|
+
import { mintSchema, handleMint } from "./tools/mint.js";
|
|
11
|
+
import { mintCheckSchema, handleMintCheck } from "./tools/mint-check.js";
|
|
12
|
+
import { mintResolveSchema, handleMintResolve } from "./tools/mint-resolve.js";
|
|
13
|
+
dotenv.config();
|
|
14
|
+
// Base mainnet
|
|
15
|
+
const MAINNET_CONTRACT = "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
|
|
16
|
+
const MAINNET_RPC = "https://mainnet.base.org";
|
|
17
|
+
const MAINNET_CHAIN = 8453;
|
|
18
|
+
// Sponsored gas wallet (Base mainnet, pre-funded with small ETH balance)
|
|
19
|
+
// Used for zero-config mints when no PRIVATE_KEY is set
|
|
20
|
+
const SPONSORED_KEY = process.env.SPONSORED_KEY || "0x1c9acdf008d822c2b0093d9194c6115b94e9c0003c2fd1a1ff03abff2c77a22e";
|
|
21
|
+
const SPONSORED_CONTRACT = MAINNET_CONTRACT;
|
|
22
|
+
const SPONSORED_RPC = MAINNET_RPC;
|
|
23
|
+
const SPONSORED_CHAIN = MAINNET_CHAIN;
|
|
24
|
+
// Sepolia fallback (when sponsored limits are exhausted)
|
|
25
|
+
const FALLBACK_CONTRACT = "0x16e0072Eb4131106e266035E98Cfd07532B88EAa";
|
|
26
|
+
const FALLBACK_RPC = "https://sepolia.base.org";
|
|
27
|
+
const FALLBACK_CHAIN = 84532;
|
|
28
|
+
// Load private key: env var > ~/.mint-day/credentials file
|
|
29
|
+
function loadPrivateKey() {
|
|
30
|
+
if (process.env.PRIVATE_KEY)
|
|
31
|
+
return process.env.PRIVATE_KEY;
|
|
32
|
+
const credPath = join(homedir(), ".mint-day", "credentials");
|
|
33
|
+
if (existsSync(credPath)) {
|
|
34
|
+
const key = readFileSync(credPath, "utf-8").trim();
|
|
35
|
+
if (key)
|
|
36
|
+
return key;
|
|
37
|
+
}
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
const userKey = loadPrivateKey();
|
|
41
|
+
const hasUserKey = !!userKey;
|
|
42
|
+
// Always mainnet. Sponsored gas wallet handles zero-config mints.
|
|
43
|
+
const chainId = parseInt(process.env.CHAIN_ID || String(hasUserKey ? MAINNET_CHAIN : MAINNET_CHAIN));
|
|
44
|
+
const contractAddress = process.env.MINT_FACTORY_ADDRESS || MAINNET_CONTRACT;
|
|
45
|
+
const rpcUrl = process.env.BASE_RPC_URL || MAINNET_RPC;
|
|
46
|
+
let signerAddress = "";
|
|
47
|
+
if (hasUserKey) {
|
|
48
|
+
try {
|
|
49
|
+
const wallet = new ethers.Wallet(userKey);
|
|
50
|
+
signerAddress = wallet.address;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
console.error("Invalid private key");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const defaultRecipient = process.env.DEFAULT_RECIPIENT || signerAddress || "";
|
|
58
|
+
const calldataService = new CalldataService(rpcUrl, contractAddress, chainId);
|
|
59
|
+
const server = new McpServer({
|
|
60
|
+
name: "mint-day",
|
|
61
|
+
version: "0.3.0",
|
|
62
|
+
});
|
|
63
|
+
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 (local files and base64 are auto-uploaded to Imgur if IMGUR_CLIENT_ID is configured). Returns a preview with mintId first; call again with mintId to confirm. If PRIVATE_KEY is configured, the server signs and submits the transaction directly and returns a tx hash. If not, returns raw calldata for your own signer (Coinbase AgentKit, Lit Protocol, or any EVM wallet).", mintSchema, async (params) => handleMint(params, calldataService, defaultRecipient, userKey, {
|
|
64
|
+
sponsoredKey: SPONSORED_KEY,
|
|
65
|
+
sponsoredContract: SPONSORED_CONTRACT,
|
|
66
|
+
sponsoredRpc: SPONSORED_RPC,
|
|
67
|
+
sponsoredChain: SPONSORED_CHAIN,
|
|
68
|
+
fallbackContract: FALLBACK_CONTRACT,
|
|
69
|
+
fallbackRpc: FALLBACK_RPC,
|
|
70
|
+
fallbackChain: FALLBACK_CHAIN,
|
|
71
|
+
}));
|
|
72
|
+
server.tool("mint_check", "Look up mint.day tokens. With an address: returns all tokens held with type, metadata, soulbound status, and mint timestamp. Without an address: returns global stats (total minted, current fee). Use to verify credentials, check attestations, or browse on-chain records.", mintCheckSchema, async (params) => handleMintCheck(params, calldataService, calldataService.provider, contractAddress, chainId));
|
|
73
|
+
server.tool("mint_resolve", "Resolve an agent's on-chain identity. Returns their Identity token with ERC-8004 agent card metadata: did, capabilities, endpoints, and image. Use this before transacting with another agent to verify who they are.", mintResolveSchema, async (params) => handleMintResolve(params, calldataService.provider, contractAddress, chainId));
|
|
74
|
+
async function main() {
|
|
75
|
+
const transport = new StdioServerTransport();
|
|
76
|
+
await server.connect(transport);
|
|
77
|
+
}
|
|
78
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import { MintIntent, CalldataResult } from "../types.js";
|
|
3
|
+
export declare class CalldataService {
|
|
4
|
+
private contract;
|
|
5
|
+
private contractAddress;
|
|
6
|
+
private chainId;
|
|
7
|
+
readonly provider: ethers.JsonRpcProvider;
|
|
8
|
+
constructor(rpcUrl: string, contractAddress: string, chainId: number);
|
|
9
|
+
buildMintCalldata(intent: MintIntent): Promise<CalldataResult>;
|
|
10
|
+
totalMinted(): Promise<string>;
|
|
11
|
+
mintFee(): Promise<string>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import { TOKEN_TYPE_NAMES } from "../types.js";
|
|
3
|
+
import { encodeMetadata } from "./metadata.js";
|
|
4
|
+
const MINT_FACTORY_ABI = [
|
|
5
|
+
"function mint(address to, string uri, uint8 tokenType, bool soulbound) payable returns (uint256)",
|
|
6
|
+
"function mintFee() view returns (uint256)",
|
|
7
|
+
"function totalMinted() view returns (uint256)",
|
|
8
|
+
];
|
|
9
|
+
export class CalldataService {
|
|
10
|
+
contract;
|
|
11
|
+
contractAddress;
|
|
12
|
+
chainId;
|
|
13
|
+
provider;
|
|
14
|
+
constructor(rpcUrl, contractAddress, chainId) {
|
|
15
|
+
this.provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
16
|
+
this.contract = new ethers.Contract(contractAddress, MINT_FACTORY_ABI, this.provider);
|
|
17
|
+
this.contractAddress = contractAddress;
|
|
18
|
+
this.chainId = chainId;
|
|
19
|
+
}
|
|
20
|
+
async buildMintCalldata(intent) {
|
|
21
|
+
const tokenURI = encodeMetadata(intent.metadata);
|
|
22
|
+
const calldata = this.contract.interface.encodeFunctionData("mint", [
|
|
23
|
+
intent.recipient,
|
|
24
|
+
tokenURI,
|
|
25
|
+
intent.tokenType,
|
|
26
|
+
intent.soulbound,
|
|
27
|
+
]);
|
|
28
|
+
const mintFee = await this.contract.mintFee();
|
|
29
|
+
// Dynamic gas estimation with 20% buffer, fallback to safe static value
|
|
30
|
+
let estimatedGas = 600000;
|
|
31
|
+
try {
|
|
32
|
+
const estimate = await this.provider.estimateGas({
|
|
33
|
+
to: this.contractAddress,
|
|
34
|
+
data: calldata,
|
|
35
|
+
value: mintFee,
|
|
36
|
+
});
|
|
37
|
+
estimatedGas = Math.ceil(Number(estimate) * 1.2);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Estimation can fail if caller has no funds; use safe default
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
to: this.contractAddress,
|
|
44
|
+
calldata,
|
|
45
|
+
value: mintFee.toString(),
|
|
46
|
+
estimatedGas,
|
|
47
|
+
chainId: this.chainId,
|
|
48
|
+
metadata: intent.metadata,
|
|
49
|
+
tokenType: TOKEN_TYPE_NAMES[intent.tokenType],
|
|
50
|
+
soulbound: intent.soulbound,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async totalMinted() {
|
|
54
|
+
const total = await this.contract.totalMinted();
|
|
55
|
+
return total.toString();
|
|
56
|
+
}
|
|
57
|
+
async mintFee() {
|
|
58
|
+
const fee = await this.contract.mintFee();
|
|
59
|
+
return ethers.formatEther(fee);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { TokenType, TOKEN_TYPE_NAMES } from "../types.js";
|
|
2
|
+
const CLASSIFY_URL = process.env.CLASSIFY_URL || "https://agent-mint-nine.vercel.app/api/classify";
|
|
3
|
+
function extractAddress(text) {
|
|
4
|
+
const match = text.match(/0x[a-fA-F0-9]{40}/);
|
|
5
|
+
return match ? match[0] : null;
|
|
6
|
+
}
|
|
7
|
+
async function classify(text) {
|
|
8
|
+
const response = await fetch(CLASSIFY_URL, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
body: JSON.stringify({ text }),
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`Classify API error: ${response.status}`);
|
|
15
|
+
}
|
|
16
|
+
return await response.json();
|
|
17
|
+
}
|
|
18
|
+
export async function classifyIntent(text, fallbackRecipient) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = await classify(text);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// If API fails, use keyword fallback
|
|
25
|
+
parsed = keywordFallback(text);
|
|
26
|
+
}
|
|
27
|
+
const typeName = String(parsed.tokenType || "Attestation");
|
|
28
|
+
const typeIndex = TOKEN_TYPE_NAMES.indexOf(typeName);
|
|
29
|
+
const tokenType = typeIndex >= 0 ? typeIndex : TokenType.Attestation;
|
|
30
|
+
const soulbound = parsed.soulbound ?? (tokenType === TokenType.Attestation || tokenType === TokenType.Credential);
|
|
31
|
+
const name = String(parsed.name || "mint.day token");
|
|
32
|
+
const recipient = extractAddress(text) || fallbackRecipient;
|
|
33
|
+
const extractedFields = parsed.extractedFields || {};
|
|
34
|
+
const missingFields = parsed.missingFields || [];
|
|
35
|
+
// For Identity tokens, auto-generate did if missing
|
|
36
|
+
if (tokenType === TokenType.Identity && !extractedFields.did) {
|
|
37
|
+
extractedFields.did = `did:pkh:eip155:${parseInt(process.env.CHAIN_ID || "8453")}:${recipient}`;
|
|
38
|
+
}
|
|
39
|
+
// Default empty arrays for Identity ERC-8004 fields
|
|
40
|
+
if (tokenType === TokenType.Identity) {
|
|
41
|
+
if (!extractedFields.capabilities)
|
|
42
|
+
extractedFields.capabilities = [];
|
|
43
|
+
if (!extractedFields.endpoints)
|
|
44
|
+
extractedFields.endpoints = [];
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
tokenType,
|
|
48
|
+
soulbound,
|
|
49
|
+
recipient,
|
|
50
|
+
missingFields: tokenType === TokenType.Identity ? missingFields.filter((f) => f !== "did" && !extractedFields[f]?.length) : [],
|
|
51
|
+
metadata: {
|
|
52
|
+
name,
|
|
53
|
+
description: text,
|
|
54
|
+
tokenType: TOKEN_TYPE_NAMES[tokenType],
|
|
55
|
+
soulbound,
|
|
56
|
+
creator: fallbackRecipient,
|
|
57
|
+
recipient,
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
chainId: parseInt(process.env.CHAIN_ID || "8453"),
|
|
60
|
+
mintday_version: "1",
|
|
61
|
+
...extractedFields,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function keywordFallback(text) {
|
|
66
|
+
const lower = text.toLowerCase();
|
|
67
|
+
if (/identity|who i am|register|agent card/.test(lower)) {
|
|
68
|
+
return { tokenType: "Identity", soulbound: false, name: "mint.day token", extractedFields: {}, missingFields: [] };
|
|
69
|
+
}
|
|
70
|
+
if (/credential|vetted|reputation|certified|qualified/.test(lower)) {
|
|
71
|
+
return { tokenType: "Credential", soulbound: true, name: "mint.day token", extractedFields: {}, missingFields: [] };
|
|
72
|
+
}
|
|
73
|
+
if (/receipt|payment|settled|paid|invoice/.test(lower)) {
|
|
74
|
+
return { tokenType: "Receipt", soulbound: false, name: "mint.day token", extractedFields: {}, missingFields: [] };
|
|
75
|
+
}
|
|
76
|
+
if (/pass|access|api|grant|permission|key/.test(lower)) {
|
|
77
|
+
return { tokenType: "Pass", soulbound: false, name: "mint.day token", extractedFields: {}, missingFields: [] };
|
|
78
|
+
}
|
|
79
|
+
// Default: Attestation (proof of action)
|
|
80
|
+
return { tokenType: "Attestation", soulbound: true, name: "mint.day token", extractedFields: {}, missingFields: [] };
|
|
81
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
// ENS lives on Ethereum mainnet, need an L1 provider for resolution
|
|
3
|
+
const ENS_RPC = process.env.ENS_RPC_URL || "https://eth.llamarpc.com";
|
|
4
|
+
let ensProvider = null;
|
|
5
|
+
function getEnsProvider() {
|
|
6
|
+
if (!ensProvider) {
|
|
7
|
+
ensProvider = new ethers.JsonRpcProvider(ENS_RPC);
|
|
8
|
+
}
|
|
9
|
+
return ensProvider;
|
|
10
|
+
}
|
|
11
|
+
export function looksLikeEns(input) {
|
|
12
|
+
return /^[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$/.test(input) && !input.startsWith("0x");
|
|
13
|
+
}
|
|
14
|
+
export async function resolveEns(nameOrAddress) {
|
|
15
|
+
if (!looksLikeEns(nameOrAddress))
|
|
16
|
+
return nameOrAddress;
|
|
17
|
+
const provider = getEnsProvider();
|
|
18
|
+
const resolved = await provider.resolveName(nameOrAddress);
|
|
19
|
+
if (!resolved) {
|
|
20
|
+
throw new Error(`Could not resolve ENS name: ${nameOrAddress}`);
|
|
21
|
+
}
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
export async function reverseResolve(address) {
|
|
25
|
+
try {
|
|
26
|
+
const provider = getEnsProvider();
|
|
27
|
+
return await provider.lookupAddress(address);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an image value to a URL suitable for on-chain metadata.
|
|
3
|
+
*
|
|
4
|
+
* - URLs pass through unchanged.
|
|
5
|
+
* - Local file paths and data URIs are uploaded to 0x0.st (no API key needed).
|
|
6
|
+
* - Falls back to base64 data URI if upload fails.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveImage(image: string): Promise<{
|
|
9
|
+
url: string;
|
|
10
|
+
uploaded: boolean;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { basename } from "path";
|
|
3
|
+
function isLocalPath(value) {
|
|
4
|
+
return (value.startsWith("/") ||
|
|
5
|
+
value.startsWith("~/") ||
|
|
6
|
+
value.startsWith("./") ||
|
|
7
|
+
value.startsWith("../"));
|
|
8
|
+
}
|
|
9
|
+
function isDataUri(value) {
|
|
10
|
+
return value.startsWith("data:");
|
|
11
|
+
}
|
|
12
|
+
function isUrl(value) {
|
|
13
|
+
return value.startsWith("http://") || value.startsWith("https://");
|
|
14
|
+
}
|
|
15
|
+
function resolvePath(path) {
|
|
16
|
+
return path.startsWith("~/")
|
|
17
|
+
? path.replace("~", process.env.HOME || "")
|
|
18
|
+
: path;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve an image value to a URL suitable for on-chain metadata.
|
|
22
|
+
*
|
|
23
|
+
* - URLs pass through unchanged.
|
|
24
|
+
* - Local file paths and data URIs are uploaded to 0x0.st (no API key needed).
|
|
25
|
+
* - Falls back to base64 data URI if upload fails.
|
|
26
|
+
*/
|
|
27
|
+
export async function resolveImage(image) {
|
|
28
|
+
if (isUrl(image)) {
|
|
29
|
+
return { url: image, uploaded: false };
|
|
30
|
+
}
|
|
31
|
+
let fileBuffer;
|
|
32
|
+
let fileName = "image.png";
|
|
33
|
+
if (isLocalPath(image)) {
|
|
34
|
+
const resolved = resolvePath(image);
|
|
35
|
+
if (!existsSync(resolved)) {
|
|
36
|
+
throw new Error(`Image file not found: ${resolved}`);
|
|
37
|
+
}
|
|
38
|
+
fileBuffer = readFileSync(resolved);
|
|
39
|
+
fileName = basename(resolved);
|
|
40
|
+
}
|
|
41
|
+
else if (isDataUri(image)) {
|
|
42
|
+
const match = image.match(/^data:([^;]+);base64,(.+)$/);
|
|
43
|
+
if (!match) {
|
|
44
|
+
throw new Error("Invalid data URI format");
|
|
45
|
+
}
|
|
46
|
+
fileBuffer = Buffer.from(match[2], "base64");
|
|
47
|
+
const ext = match[1].split("/")[1] || "png";
|
|
48
|
+
fileName = `image.${ext}`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return { url: image, uploaded: false };
|
|
52
|
+
}
|
|
53
|
+
const url = await uploadTo0x0(fileBuffer, fileName);
|
|
54
|
+
return { url, uploaded: true };
|
|
55
|
+
}
|
|
56
|
+
async function uploadTo0x0(buffer, fileName) {
|
|
57
|
+
const uint8 = new Uint8Array(buffer);
|
|
58
|
+
const blob = new Blob([uint8]);
|
|
59
|
+
const formData = new FormData();
|
|
60
|
+
formData.append("file", blob, fileName);
|
|
61
|
+
const res = await fetch("https://0x0.st", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "User-Agent": "mint.day/0.2.0" },
|
|
64
|
+
body: formData,
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(`0x0.st upload failed (${res.status})`);
|
|
68
|
+
}
|
|
69
|
+
return (await res.text()).trim();
|
|
70
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const TYPE_COLORS = {
|
|
2
|
+
Identity: { bg: "#0c1220", accent: "#60a5fa", text: "#93bbfc" },
|
|
3
|
+
Attestation: { bg: "#0a1510", accent: "#22c55e", text: "#6ee7a0" },
|
|
4
|
+
Credential: { bg: "#100c1e", accent: "#a78bfa", text: "#c4b5fd" },
|
|
5
|
+
Receipt: { bg: "#151008", accent: "#fb923c", text: "#fdba74" },
|
|
6
|
+
Pass: { bg: "#0a1518", accent: "#22d3ee", text: "#67e8f9" },
|
|
7
|
+
};
|
|
8
|
+
function generateDefaultImage(metadata) {
|
|
9
|
+
const colors = TYPE_COLORS[metadata.tokenType] || TYPE_COLORS.Attestation;
|
|
10
|
+
const name = metadata.name || "mint.day token";
|
|
11
|
+
const tokenType = metadata.tokenType || "Token";
|
|
12
|
+
const soulbound = metadata.soulbound;
|
|
13
|
+
// Truncate name for display
|
|
14
|
+
const displayName = name.length > 80 ? name.slice(0, 77) + "..." : name;
|
|
15
|
+
// Split into lines if long
|
|
16
|
+
const words = displayName.split(" ");
|
|
17
|
+
const lines = [];
|
|
18
|
+
let current = "";
|
|
19
|
+
for (const word of words) {
|
|
20
|
+
if ((current + " " + word).trim().length > 30) {
|
|
21
|
+
lines.push(current.trim());
|
|
22
|
+
current = word;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
current = (current + " " + word).trim();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (current)
|
|
29
|
+
lines.push(current.trim());
|
|
30
|
+
const nameLines = lines.slice(0, 4).map((line, i) => `<text x="40" y="${185 + i * 28}" font-family="monospace" font-size="18" font-weight="600" fill="${colors.text}">${escapeXml(line)}</text>`).join("\n ");
|
|
31
|
+
// Subtle pattern based on token type
|
|
32
|
+
const patternId = `p-${tokenType.toLowerCase()}`;
|
|
33
|
+
let pattern = "";
|
|
34
|
+
if (tokenType === "Identity") {
|
|
35
|
+
pattern = `<pattern id="${patternId}" width="40" height="40" patternUnits="userSpaceOnUse">
|
|
36
|
+
<circle cx="20" cy="20" r="1" fill="${colors.accent}" opacity="0.08"/>
|
|
37
|
+
</pattern>`;
|
|
38
|
+
}
|
|
39
|
+
else if (tokenType === "Attestation") {
|
|
40
|
+
pattern = `<pattern id="${patternId}" width="32" height="32" patternUnits="userSpaceOnUse">
|
|
41
|
+
<line x1="0" y1="32" x2="32" y2="0" stroke="${colors.accent}" stroke-width="0.5" opacity="0.06"/>
|
|
42
|
+
</pattern>`;
|
|
43
|
+
}
|
|
44
|
+
else if (tokenType === "Credential") {
|
|
45
|
+
pattern = `<pattern id="${patternId}" width="24" height="24" patternUnits="userSpaceOnUse">
|
|
46
|
+
<rect x="11" y="11" width="2" height="2" fill="${colors.accent}" opacity="0.08"/>
|
|
47
|
+
</pattern>`;
|
|
48
|
+
}
|
|
49
|
+
else if (tokenType === "Receipt") {
|
|
50
|
+
pattern = `<pattern id="${patternId}" width="36" height="36" patternUnits="userSpaceOnUse">
|
|
51
|
+
<line x1="0" y1="18" x2="36" y2="18" stroke="${colors.accent}" stroke-width="0.5" opacity="0.06"/>
|
|
52
|
+
</pattern>`;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
pattern = `<pattern id="${patternId}" width="28" height="28" patternUnits="userSpaceOnUse">
|
|
56
|
+
<circle cx="14" cy="14" r="0.8" fill="${colors.accent}" opacity="0.08"/>
|
|
57
|
+
</pattern>`;
|
|
58
|
+
}
|
|
59
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
|
|
60
|
+
<defs>
|
|
61
|
+
${pattern}
|
|
62
|
+
</defs>
|
|
63
|
+
<rect width="400" height="400" fill="${colors.bg}"/>
|
|
64
|
+
<rect width="400" height="400" fill="url(#${patternId})"/>
|
|
65
|
+
|
|
66
|
+
<!-- Top accent line -->
|
|
67
|
+
<rect x="40" y="40" width="48" height="3" rx="1.5" fill="${colors.accent}" opacity="0.8"/>
|
|
68
|
+
|
|
69
|
+
<!-- Token type label -->
|
|
70
|
+
<text x="40" y="80" font-family="monospace" font-size="12" fill="${colors.accent}" opacity="0.6" text-transform="uppercase" letter-spacing="2">${escapeXml(tokenType.toUpperCase())}</text>
|
|
71
|
+
|
|
72
|
+
<!-- Soulbound badge -->
|
|
73
|
+
${soulbound ? `<text x="40" y="100" font-family="monospace" font-size="10" fill="${colors.accent}" opacity="0.35">SOULBOUND</text>` : ""}
|
|
74
|
+
|
|
75
|
+
<!-- Token name -->
|
|
76
|
+
${nameLines}
|
|
77
|
+
|
|
78
|
+
<!-- Bottom branding -->
|
|
79
|
+
<text x="40" y="350" font-family="monospace" font-size="11" fill="${colors.accent}" opacity="0.2">mint.day</text>
|
|
80
|
+
|
|
81
|
+
<!-- Bottom accent line -->
|
|
82
|
+
<rect x="40" y="362" width="320" height="1" rx="0.5" fill="${colors.accent}" opacity="0.1"/>
|
|
83
|
+
</svg>`;
|
|
84
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
|
85
|
+
}
|
|
86
|
+
function escapeXml(str) {
|
|
87
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
88
|
+
}
|
|
89
|
+
export function encodeMetadata(metadata) {
|
|
90
|
+
// Auto-generate image if none provided
|
|
91
|
+
if (!metadata.image) {
|
|
92
|
+
metadata.image = generateDefaultImage(metadata);
|
|
93
|
+
}
|
|
94
|
+
// Build OpenSea-compatible attributes array
|
|
95
|
+
const attributes = [
|
|
96
|
+
{ trait_type: "Token Type", value: metadata.tokenType },
|
|
97
|
+
{ trait_type: "Soulbound", value: metadata.soulbound ? "Yes" : "No" },
|
|
98
|
+
];
|
|
99
|
+
if (metadata.creator && metadata.creator !== "0x0000000000000000000000000000000000000000") {
|
|
100
|
+
attributes.push({ trait_type: "Creator", value: metadata.creator });
|
|
101
|
+
}
|
|
102
|
+
if (metadata.recipient) {
|
|
103
|
+
attributes.push({ trait_type: "Recipient", value: metadata.recipient });
|
|
104
|
+
}
|
|
105
|
+
// Add any capabilities as traits
|
|
106
|
+
if (Array.isArray(metadata.capabilities)) {
|
|
107
|
+
for (const cap of metadata.capabilities) {
|
|
108
|
+
attributes.push({ trait_type: "Capability", value: cap });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const enriched = {
|
|
112
|
+
...metadata,
|
|
113
|
+
attributes,
|
|
114
|
+
external_url: "https://agent-mint-nine.vercel.app",
|
|
115
|
+
};
|
|
116
|
+
const json = JSON.stringify(enriched);
|
|
117
|
+
const base64 = Buffer.from(json).toString("base64");
|
|
118
|
+
return `data:application/json;base64,${base64}`;
|
|
119
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ethers } from "ethers";
|
|
3
|
+
import { CalldataService } from "../services/calldata.js";
|
|
4
|
+
export declare const mintCheckSchema: {
|
|
5
|
+
address: z.ZodOptional<z.ZodString>;
|
|
6
|
+
txHash: z.ZodOptional<z.ZodString>;
|
|
7
|
+
};
|
|
8
|
+
export declare function handleMintCheck(params: {
|
|
9
|
+
address?: string;
|
|
10
|
+
txHash?: string;
|
|
11
|
+
}, calldataService: CalldataService, provider: ethers.JsonRpcProvider, contractAddress: string, chainId: number): Promise<{
|
|
12
|
+
content: {
|
|
13
|
+
type: "text";
|
|
14
|
+
text: string;
|
|
15
|
+
}[];
|
|
16
|
+
}>;
|