solwink-cli 1.0.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 ADDED
@@ -0,0 +1,83 @@
1
+ # 🌊 SolWink: The Ultimate Solana Blink Generator
2
+
3
+ [![npm version](https://img.shields.io/npm/v/solwink.svg)](https://www.npmjs.com/package/solwink)
4
+
5
+ **SolWink** is a lightning-fast, interactive CLI that scaffolds production-ready **Solana Actions & Blinks** in seconds.
6
+
7
+ Stop fighting with CORS headers, Next.js App Router bloat, and `X-Blockchain-Ids` validation. SolWink generates lightweight, pure Express.js backends specifically architected to work perfectly with [Dial.to](https://dial.to), Phantom, and X (Twitter) right out of the box.
8
+
9
+ ---
10
+
11
+ ## ⚡ Quick Start
12
+
13
+ You don't need to install anything globally. Scaffold your first Blink instantly using `npx`:
14
+
15
+ ```bash
16
+ npx create-solwink@latest
17
+ # or
18
+ npx solwink@latest
19
+ Follow the beautiful interactive prompts to name your project, select your network, and inject your smart contract variables.
20
+
21
+ Once generated, simply step into your project and run:
22
+
23
+ Bash
24
+ cd my-solwink-app
25
+ npm install
26
+ npm run dev
27
+ ✨ Why SolWink?
28
+ Building Solana Blinks should be fun, not frustrating. SolWink focuses on Developer Velocity:
29
+
30
+ Zero-Config Express Backend: We ditched the heavy frontend frameworks. SolWink generates pure, lightweight Node.js/Express servers designed exclusively for API route handling.
31
+
32
+ Bulletproof CORS & Headers: Pre-configured with the exact X-Action-Version and Access-Control-Allow-Origin headers required by the Dialect validator.
33
+
34
+ Dynamic Network Selection: Choose between Mainnet-Beta or Devnet via interactive CLI dropdowns. SolWink automatically maps and injects the correct Genesis Hash / Chain ID into your project.
35
+
36
+ Smart Variable Injection: The CLI prompts you for your Treasury Wallet or Candy Machine ID and securely injects them directly into the generated codebase.
37
+
38
+ 🛠️ Phase 1 Templates
39
+ SolWink currently ships with the two most highly requested use cases in the Solana ecosystem:
40
+
41
+ 💰 1. SOL Donation (Crowdfund)
42
+ A dynamic Blink that allows users to send SOL to a specific treasury wallet.
43
+
44
+ Features: Customizable donation buttons (e.g., 0.1 SOL, 0.5 SOL, 1 SOL) and a custom amount input field.
45
+
46
+ CLI Inputs: Prompts for the destination Treasury Wallet address.
47
+
48
+ 🖼️ 2. NFT Mint (Metaplex Candy Machine)
49
+ A fully integrated NFT minting Blink using the modern @metaplex-foundation/umi standard.
50
+
51
+ Features: Direct integration with Candy Machine v2/v3, pre-configured Umi instance, and optimized image rendering for Twitter unfurls.
52
+
53
+ CLI Inputs: Prompts for your specific Metaplex Candy Machine ID.
54
+
55
+ 📁 Generated Architecture
56
+ SolWink generates a clean, "Headless" backend structure that is easy to deploy to Render, Railway, or Heroku:
57
+
58
+
59
+ 🚀 Roadmap (Phase 2 & Beyond)
60
+ We are actively expanding the SolWink template library to become the standard scaffolding tool for Solana Actions:
61
+
62
+ [ ] SPL Token Transfer: Support for sending USDC, BONK, and other custom tokens.
63
+
64
+ [ ] Jupiter Swaps: Direct token-to-token swaps right inside the Blink.
65
+
66
+ [ ] DAO Governance: On-chain voting for Realms/Snapshot.
67
+
68
+ [ ] Token Gating: Access control and exclusive claims based on NFT ownership.
69
+
70
+ 🤝 Contributing
71
+ Contributions, issues, and feature requests are welcome! Feel free to check the issues page.
72
+
73
+ To develop locally:
74
+
75
+ Clone the repository.
76
+
77
+ Run npm install.
78
+
79
+ Run npm run build.
80
+
81
+ Run npm link to test the CLI globally on your machine.
82
+
83
+
package/dist/cli.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import consola from "consola";
4
+ import fs from "fs-extra";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const program = new Command();
10
+ program
11
+ .name("solwink")
12
+ .description("The ultimate Solana Blink generator")
13
+ .version("1.0.0");
14
+ program.action(async () => {
15
+ consola.box("Welcome to SolWink - The Solana Blink Generator 🌊");
16
+ try {
17
+ const templateType = await consola.prompt("Which Blink template would you like to scaffold?", {
18
+ type: "select",
19
+ options: [
20
+ { label: "💰 SOL Donation (Crowdfund)", value: "donation" },
21
+ { label: "🖼️ NFT Mint (Metaplex Candy Machine)", value: "nft-mint" },
22
+ ],
23
+ });
24
+ const network = await consola.prompt("Which Solana cluster will this run on?", {
25
+ type: "select",
26
+ options: [
27
+ { label: "Mainnet (Production)", value: "mainnet-beta" },
28
+ { label: "Devnet (Testing)", value: "devnet" },
29
+ ],
30
+ });
31
+ const projectName = await consola.prompt("What is the name of your project?", {
32
+ type: "text",
33
+ default: "my-solwink-app",
34
+ });
35
+ let targetWallet = "";
36
+ let candyMachineId = "";
37
+ if (templateType === "donation") {
38
+ targetWallet = await consola.prompt("Enter the destination treasury wallet address:", {
39
+ type: "text",
40
+ });
41
+ }
42
+ else if (templateType === "nft-mint") {
43
+ candyMachineId = await consola.prompt("Enter your Metaplex Candy Machine ID:", {
44
+ type: "text",
45
+ });
46
+ }
47
+ const targetPath = path.join(process.cwd(), projectName);
48
+ const templatePath = path.join(__dirname, "..", "templates", templateType);
49
+ consola.start(`Initializing ${templateType} project in ./${projectName}...`);
50
+ if (!(await fs.pathExists(templatePath))) {
51
+ throw new Error(`Template folder not found at: ${templatePath}`);
52
+ }
53
+ const chainIds = {
54
+ "mainnet-beta": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
55
+ "devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
56
+ };
57
+ const selectedChainId = chainIds[network];
58
+ await fs.copy(templatePath, targetPath);
59
+ const routePath = path.join(targetPath, "api", "index.ts");
60
+ if (await fs.pathExists(routePath)) {
61
+ let routeContent = await fs.readFile(routePath, "utf8");
62
+ routeContent = routeContent.replace(/{{NETWORK}}/g, network);
63
+ routeContent = routeContent.replace(/{{CHAIN_ID}}/g, selectedChainId);
64
+ if (templateType === "donation") {
65
+ routeContent = routeContent.replace(/{{TREASURY_WALLET}}/g, targetWallet);
66
+ }
67
+ else if (templateType === "nft-mint") {
68
+ routeContent = routeContent.replace(/{{CANDY_MACHINE_ID}}/g, candyMachineId);
69
+ }
70
+ await fs.writeFile(routePath, routeContent);
71
+ }
72
+ consola.success(`Project initialized successfully! 🎉`);
73
+ consola.info(`\nNext steps:\n cd ${projectName}\n npm install\n npm run dev\n`);
74
+ }
75
+ catch (err) {
76
+ consola.error("Project Initialization Failed!", err);
77
+ }
78
+ });
79
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "solwink-cli",
3
+ "version": "1.0.0",
4
+ "description": "The ultimate interactive CLI to scaffold Solana Blinks and Actions",
5
+ "type": "module",
6
+ "bin": {
7
+ "solwink": "./dist/cli.js",
8
+ "create-solwink": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "templates"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/cli.js",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "solana",
21
+ "blinks",
22
+ "actions",
23
+ "cli",
24
+ "generator",
25
+ "superteam"
26
+ ],
27
+ "author": "Kartik Angiras",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "commander": "^12.1.0",
31
+ "consola": "^3.4.2",
32
+ "fs-extra": "^11.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/fs-extra": "^11.0.4",
36
+ "@types/node": "^20.0.0",
37
+ "typescript": "^5.9.3"
38
+ }
39
+ }
@@ -0,0 +1,123 @@
1
+ import express from "express";
2
+ import {
3
+ ActionGetResponse,
4
+ ActionPostRequest,
5
+ ActionPostResponse,
6
+ } from "@solana/actions";
7
+ import {
8
+ Connection,
9
+ PublicKey,
10
+ SystemProgram,
11
+ TransactionMessage,
12
+ VersionedTransaction,
13
+ clusterApiUrl,
14
+ } from "@solana/web3.js";
15
+
16
+ const app = express();
17
+ app.use(express.json());
18
+
19
+ const ACTION_HEADERS = {
20
+ "Access-Control-Allow-Origin": "*",
21
+ "Access-Control-Allow-Methods": "GET,POST,PUT,OPTIONS",
22
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, Content-Encoding, Accept-Encoding",
23
+ "Access-Control-Expose-Headers": "X-Action-Version, X-Blockchain-Ids",
24
+ "X-Action-Version": "2.1.3",
25
+ "X-Blockchain-Ids": "{{CHAIN_ID}}",
26
+ };
27
+
28
+ app.use((req, res, next) => {
29
+ res.set(ACTION_HEADERS);
30
+ if (req.method === "OPTIONS") {
31
+ return res.status(200).end();
32
+ }
33
+ next();
34
+ });
35
+
36
+ app.get("/actions.json", (req, res) => {
37
+ res.json({
38
+ rules: [
39
+ { pathPattern: "/*", apiPath: "/api/donate" },
40
+ { pathPattern: "/api/donate", apiPath: "/api/donate" }
41
+ ]
42
+ });
43
+ });
44
+
45
+ app.get("/api/donate", (req, res) => {
46
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
47
+
48
+ const payload: ActionGetResponse = {
49
+ type: "action",
50
+ icon: "https://ucarecdn.com/7aa46c85-08a4-4bc7-9376-88ec48bb1f43/-/preview/880x864/-/quality/smart/-/format/auto/",
51
+ title: "Support the Project",
52
+ description: "Your SOL donation helps fund continuous open-source development.",
53
+ label: "Donate SOL",
54
+ links: {
55
+ actions: [
56
+ {
57
+ type: "transaction",
58
+ label: "Send Donation",
59
+ href: `${baseUrl}/api/donate?amount={amount}`,
60
+ parameters: [
61
+ {
62
+ name: "amount",
63
+ label: "Enter SOL amount",
64
+ required: true
65
+ }
66
+ ],
67
+ },
68
+ ],
69
+ },
70
+ };
71
+
72
+ res.json(payload);
73
+ });
74
+
75
+ app.post("/api/donate", async (req, res) => {
76
+ try {
77
+ const body: ActionPostRequest = req.body;
78
+ const donorPubkey = new PublicKey(body.account);
79
+
80
+ const amountStr = req.query.amount as string;
81
+ const amount = parseFloat(amountStr);
82
+
83
+ if (isNaN(amount) || amount <= 0) {
84
+ return res.status(400).json({ error: "Invalid amount entered." });
85
+ }
86
+
87
+ const connection = new Connection(clusterApiUrl("{{NETWORK}}" as any), "confirmed");
88
+ const DONATION_DESTINATION_WALLET = new PublicKey("{{TREASURY_WALLET}}");
89
+
90
+ const transferIx = SystemProgram.transfer({
91
+ fromPubkey: donorPubkey,
92
+ toPubkey: DONATION_DESTINATION_WALLET,
93
+ lamports: amount * 1_000_000_000,
94
+ });
95
+
96
+ const { blockhash } = await connection.getLatestBlockhash();
97
+
98
+ const messageV0 = new TransactionMessage({
99
+ payerKey: donorPubkey,
100
+ recentBlockhash: blockhash,
101
+ instructions: [transferIx],
102
+ }).compileToV0Message();
103
+
104
+ const transaction = new VersionedTransaction(messageV0);
105
+ const serializedTransaction = Buffer.from(transaction.serialize()).toString("base64");
106
+
107
+ const payload: ActionPostResponse = {
108
+ type: "transaction",
109
+ transaction: serializedTransaction,
110
+ message: `Successfully donated ${amount} SOL. Thank you!`,
111
+ };
112
+
113
+ res.json(payload);
114
+ } catch (err) {
115
+ console.error(err);
116
+ res.status(500).json({ error: "Failed to build transaction" });
117
+ }
118
+ });
119
+
120
+ const PORT = process.env.PORT || 8080;
121
+ app.listen(PORT, () => {
122
+ console.log(`🚀 SolWink Donation Server running on port ${PORT}`);
123
+ });
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "solwink-donation",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "ts-node api/index.ts"
7
+ },
8
+ "dependencies": {
9
+ "@metaplex-foundation/mpl-candy-machine": "^6.0.0",
10
+ "@metaplex-foundation/mpl-toolbox": "^0.9.4",
11
+ "@metaplex-foundation/umi": "^0.9.1",
12
+ "@metaplex-foundation/umi-bundle-defaults": "^0.9.1",
13
+ "@metaplex-foundation/umi-web3js-adapters": "^0.9.1",
14
+ "@solana/actions": "^1.6.6",
15
+ "@solana/web3.js": "^1.98.4",
16
+ "cors": "^2.8.5",
17
+ "express": "^4.19.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/cors": "^2.8.17",
21
+ "@types/express": "^4.17.21",
22
+ "ts-node": "^10.9.2",
23
+ "typescript": "^5.0.0"
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "rules": [
3
+ {
4
+ "pathPattern": "/*",
5
+ "apiPath": "/api/donate"
6
+ },
7
+ {
8
+ "pathPattern": "/api/donate",
9
+ "apiPath": "/api/donate"
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "esModuleInterop": true,
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "outDir": "./dist"
9
+ },
10
+ "include": ["api/**/*.ts"]
11
+ }
@@ -0,0 +1,123 @@
1
+ import express from "express";
2
+ import {
3
+ ActionGetResponse,
4
+ ActionPostRequest,
5
+ ActionPostResponse,
6
+ } from "@solana/actions";
7
+ import { clusterApiUrl } from "@solana/web3.js";
8
+
9
+
10
+ import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
11
+ import { fetchCandyMachine, mintV2, mplCandyMachine } from "@metaplex-foundation/mpl-candy-machine";
12
+ import { createNoopSigner, generateSigner, publicKey, transactionBuilder } from "@metaplex-foundation/umi";
13
+ import { setComputeUnitLimit } from "@metaplex-foundation/mpl-toolbox";
14
+ import { toWeb3JsTransaction } from "@metaplex-foundation/umi-web3js-adapters";
15
+
16
+ const app = express();
17
+ app.use(express.json());
18
+
19
+ const ACTION_HEADERS = {
20
+ "Access-Control-Allow-Origin": "*",
21
+ "Access-Control-Allow-Methods": "GET,POST,PUT,OPTIONS",
22
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, Content-Encoding, Accept-Encoding",
23
+ "Access-Control-Expose-Headers": "X-Action-Version, X-Blockchain-Ids",
24
+ "X-Action-Version": "2.1.3",
25
+ "X-Blockchain-Ids": "{{CHAIN_ID}}",
26
+ };
27
+
28
+ app.use((req, res, next) => {
29
+ res.set(ACTION_HEADERS);
30
+ if (req.method === "OPTIONS") {
31
+ return res.status(200).end();
32
+ }
33
+ next();
34
+ });
35
+
36
+ app.get("/actions.json", (req, res) => {
37
+ res.json({
38
+ rules: [
39
+ { pathPattern: "/*", apiPath: "/api/mint" },
40
+ { pathPattern: "/api/mint", apiPath: "/api/mint" }
41
+ ]
42
+ });
43
+ });
44
+
45
+ app.get("/api/mint", (req, res) => {
46
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
47
+
48
+ const payload: ActionGetResponse = {
49
+ type: "action",
50
+ icon: "https://metadata.y00ts.com/y/13877.png",
51
+ title: "Mint Your Exclusive NFT",
52
+ description: "Mint a digital collectible directly from this Blink using Metaplex Candy Machine.",
53
+ label: "Mint NFT",
54
+ links: {
55
+ actions: [
56
+ {
57
+ type: "transaction",
58
+ label: "Mint NFT",
59
+ href: `${baseUrl}/api/mint?amount={amount}`,
60
+ parameters: [
61
+ {
62
+ name: "amount",
63
+ label: "Enter number of NFTs to mint",
64
+ required: true,
65
+ }
66
+ ],
67
+ }
68
+ ],
69
+ },
70
+ };
71
+
72
+ res.json(payload);
73
+ } );
74
+
75
+ app.post("/api/mint", async (req, res) => {
76
+ try {
77
+ const body: ActionPostRequest = req.body;
78
+
79
+ const umi = createUmi(clusterApiUrl("{{NETWORK}}" as any)).use(mplCandyMachine());
80
+ const minterPubkey = publicKey(body.account);
81
+ const cmPubkey = publicKey("{{CANDY_MACHINE_ID}}");
82
+
83
+ umi.use({
84
+ install(umi) {
85
+ umi.payer = createNoopSigner(minterPubkey);
86
+ }
87
+ });
88
+
89
+ const candyMachine = await fetchCandyMachine(umi, cmPubkey);
90
+ const nftSigner = generateSigner(umi);
91
+
92
+ const builder = transactionBuilder()
93
+ .add(setComputeUnitLimit(umi, { units: 800_000 }))
94
+ .add(
95
+ mintV2(umi, {
96
+ candyMachine: candyMachine.publicKey,
97
+ nftMint: nftSigner,
98
+ collectionMint: candyMachine.collectionMint,
99
+ collectionUpdateAuthority: candyMachine.authority,
100
+ })
101
+ );
102
+
103
+ const umiTransaction = await builder.buildAndSign(umi);
104
+ const web3Transaction = toWeb3JsTransaction(umiTransaction);
105
+ const serializedTransaction = Buffer.from(web3Transaction.serialize()).toString("base64");
106
+
107
+ const payload: ActionPostResponse = {
108
+ type: "transaction",
109
+ transaction: serializedTransaction,
110
+ message: `Successfully minted NFT! Address: ${nftSigner.publicKey.toString().slice(0, 8)}...`,
111
+ };
112
+
113
+ res.json(payload);
114
+ } catch (err) {
115
+ console.error("Minting Error:", err);
116
+ res.status(500).json({ error: "Failed to build mint transaction." });
117
+ }
118
+ });
119
+
120
+ const PORT = process.env.PORT || 8080;
121
+ app.listen(PORT, () => {
122
+ console.log(`🚀 SolWink NFT Server running on port ${PORT}`);
123
+ });
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "solwink-nft-mint",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "ts-node api/index.ts"
7
+ },
8
+ "dependencies": {
9
+ "@metaplex-foundation/mpl-candy-machine": "^6.0.0",
10
+ "@metaplex-foundation/mpl-toolbox": "^0.9.4",
11
+ "@metaplex-foundation/umi": "^0.9.1",
12
+ "@metaplex-foundation/umi-bundle-defaults": "^0.9.1",
13
+ "@metaplex-foundation/umi-web3js-adapters": "^0.9.1",
14
+ "@solana/actions": "^1.6.6",
15
+ "@solana/web3.js": "^1.98.4",
16
+ "cors": "^2.8.5",
17
+ "express": "^4.19.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/cors": "^2.8.17",
21
+ "@types/express": "^4.17.21",
22
+ "ts-node": "^10.9.2",
23
+ "typescript": "^5.0.0"
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "rules": [
3
+ {
4
+ "pathPattern": "/*",
5
+ "apiPath": "/api/mint"
6
+ },
7
+ {
8
+ "pathPattern": "/api/mint",
9
+ "apiPath": "/api/mint"
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "esModuleInterop": true,
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "outDir": "./dist"
9
+ },
10
+ "include": ["api/**/*.ts"]
11
+ }