Build your first Solana Blink
Hello and welcome! This page is all about Solana Action and Blinks Insight. Here, I’ll explain what action and blinks mean and also teach you how to build your first Solana Blink. Let’s get started!
1. What are Action & Blinks:
Solana Actions is an API that makes it easy for users to interact with the Solana blockchain without leaving the comfort of your favorite apps and websites. Whether you’re on a social media platform, browsing an e-commerce site, or engaging in any web-based activity, these Actions bring the blockchain to you. Think of them as convenient, built-in features that let you send payments, donation, vote on proposals, or buy NFTs directly from within the application you’re using.
1.1 Action
Solana Actions are essentially a set of standardized APIs designed for a straightforward purpose: they provide signable transactions or messages directly from an application to a user.
Below is the key aspect:
Standardized APIs: Actions follow a defined format, ensuring compatibility across different applications and wallets.
Signable transactions/messages: The primary function is to deliver information for users to sign, authorizing blockchain actions.
Publicly accessible URLs: Anyone with the URL can interact with an Action.
Metadata and Signable Element: Actions provide details about the transaction and the element requiring a user's signature.
Seamless User Experience: Users can review and sign directly from their wallets without needing separate tools.
In simpler terms, Actions are pre-built transactions waiting for user approval through their wallets. This streamlines interaction with the Solana blockchain within various applications.
1.2 Blinks (Blockchain Links)
Blinks (Blockchain Links) are client applications (interface) specifically designed to bridge the gap between Solana Actions and users, enhancing the blockchain interaction experience.
These applications are adept at detecting Action-compatible URLs, parsing them, and enabling users to engage with them through standardized, user-friendly interfaces. Essentially, Blinks act as the interpreters and facilitators of Solana Actions.
Here’s how Blinks function in the ecosystem:
Client Applications: Blinks are standalone programs that integrate seamlessly with your crypto wallet.
Understanding Actions: They possess the capability to “read” and interpret the APIs embedded within Solana Actions.
Building User Interfaces: Based on what each Action requires, Blinks create user-friendly interfaces—like buttons or windows—that make it simpler for you to engage with the blockchain.
To visualize their role, consider this analogy:
Imagine Solana Actions as recipes containing detailed instructions (APIs) for executing blockchain transactions. In this scenario, Blinks are like skilled chefs who:
• Understand the recipe (Action).
• Prepare the ingredients (parse the details of the Action).
• Present the dish (develop a user-friendly interface) for you to review and approve (sign the transaction).
By designing these interfaces, Blinks simplify the process, making it more straightforward for user to understand and interact with the transaction before committing to it with your wallet. This setup not only makes blockchain interactions more accessible but also enhances the overall user experience.
1.3 Introduction to Dialect
Dialect is a software development company that is involved with Solana Actions and Blinks.
Here's how Dialect plays a role:
Developer Tools: Dialect provides tools and resources to help developers create and deploy Solana Actions easily.
Blink Clients: Dialect offers a Blink client extension that allows interaction with Blinks directly within your web browser.
Actions Registry: Dialect maintains a public registry of verified Actions to enhance security and user trust when encountering Blinks.
Although not being the core technology behind Actions and Blinks, Dialect plays a significant role in their development, user experience, and overall ecosystem.
2. Use cases
Prediction Market
Donation
Program game
Bidding NFT
Send money request in chat
Staking
3. How to activate the Blink function in your wallet?
In Phantom wallet:
Go to setting, Select “Experimental Features", then enable the Solana Actions on X.com.
4. How to create your first Blink ? (Hands-on)
We will build the first version first; then, once you get familiar with it, we will implement the donate feature like in figure 2.
Create a workspace in VS Code.
Open your terminal and type:
npx create-next-app solana-action
Select the highlighted option:
cd solana-action
npm i @solana/web3.js @solana/actions
code . (If you using vs code, type this command to open the workspace)
Paste a picture in the public folder
This will be used for the background.
Create folder and file
Open your terminal and type below command to create a api/actions/memo folder:
mkdir -p src/app/api/actions/memo/
Then, create a route.tsx file in the folder:
touch src/app/api/actions/memo/route.tsx
Inside the /route.tsx file, paste the code below:
import {
ACTIONS_CORS_HEADERS,
ActionGetResponse,
ActionPostRequest,
ActionPostResponse,
MEMO_PROGRAM_ID,
createPostResponse,
} from "@solana/actions";
import {
ComputeBudgetProgram,
Connection,
PublicKey,
Transaction,
TransactionInstruction,
clusterApiUrl,
} from "@solana/web3.js";
export const GET = (req: Request) => {
const payload: ActionGetResponse = {
icon: new URL("/solana_devs.jpg", new URL(req.url).origin).toString(),
label: "Memo Demo",
title: "Memo Demo",
description: "This is a super simple Action",
};
return Response.json(payload, {
headers: ACTIONS_CORS_HEADERS,
});
};
export const OPTIONS = GET;
export const POST = async (req: Request) => {
try {
const body: ActionPostRequest = await req.json();
let account: PublicKey;
try {
account = new PublicKey(body.account);
} catch (err) {
return new Response("Invalid 'account' provided", {
status: 400,
headers: ACTIONS_CORS_HEADERS,
});
}
const transaction = new Transaction();
transaction.add(
// note: createPostResponse
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 1000,
}),
new TransactionInstruction({
programId: new PublicKey(MEMO_PROGRAM_ID),
data: Buffer.from("this is a simple memo message", "utf8"),
keys: [],
})
);
transaction.feePayer = account;
const connection = new Connection(
process.env.RPC_URL_MAINNET ?? clusterApiUrl("mainnet-beta")
);
transaction.recentBlockhash = (
await connection.getLatestBlockhash()
).blockhash;
const payload: ActionPostResponse = await createPostResponse({
fields: {
transaction,
},
});
return Response.json(payload, { headers: ACTIONS_CORS_HEADERS });
} catch (err) {
return Response.json("An unknown error occurred", { status: 400 });
}
};
After completing the steps above, publish the project to GitHub.
Deploy the repository into Vercel
Click here to continue to Vercel
Sign in with your GitHub account, and once you locate the repository, click ‘Import’.
Enter a project name then click “Deploy”.
Once deployed successfully, click ‘Continue to Dashboard
After deployment, you will receive a URL like this: https://solana-action-mu.vercel.app/
Append ‘/api/actions/memo’ to the end of https://solana-action-mu.vercel.app/, resulting in https://solana-action-mu.vercel.app/api/actions/memo. You will see a JSON formatted string.
Paste this URL into Dialect to see your first Blink! Congratulations!
** Check out the full code here !
Customize the Amount and Details:
After creating your first Blink, let’s take it to the next level by implementing a donate function.
Open your terminal and type the command below to create a donate folder:
mkdir -p src/app/api/actions/donate/
Create and configure const.tsx in the donate folder:
touch src/app/api/actions/donate/const.tsx
Paste below code inside the const.tsx
**Replace the default recipient address on the line marked // donate wallet.
import { PublicKey, clusterApiUrl } from "@solana/web3.js";
import dotenv from "dotenv";
dotenv.config();
export const DEFAULT_SOL_ADDRESS: PublicKey = new PublicKey(
process.env.RECIPIENT ?? "7J2qom6uYS1sExNZmxVyUbgjG7WQ4KuLe1T3NMDfHbfh" // donate wallet
);
export const DEFAULT_SOL_AMOUNT: number = process.env.DEFAULTAMOUNT
? parseFloat(process.env.DEFAULTAMOUNT)
: 0.1;
export const DEFAULT_RPC =
process.env.RPC_URL_MAINNET ?? clusterApiUrl("mainnet-beta");
export const DEFAULT_TITLE = process.env.TITLE ?? "Coffee tips";
export const DEFAULT_AVATAR = process.env.AVATAR;
export const DEFAULT_DESCRIPTION =
process.env.DESCRIPTION ?? "Thanks for your supporting";
Install dotenv
npm install dotenv
Create a .env file to set your environment variables.
# .env
RECIPIENT=xxxxxxxxxxx (RECIPIENT ADDRESS)
DEFAULTAMOUNT=1
RPC_URL_MAINNET=https://api.mainnet-beta.solana.com
TITLE=Coffee tips
AVATAR=https://example.com/avatar.png
DESCRIPTION=Thanks for your supporting yayyy
Type the command below in terminal to create a route.tsx file in the donate folder
touch src/app/api/actions/donate/route.tsx
Paste the code inside src/app/api/actions/donate/route.tsx.
import {
ActionPostResponse,
ACTIONS_CORS_HEADERS,
createPostResponse,
ActionGetResponse,
ActionPostRequest,
} from "@solana/actions";
import {
Connection,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
DEFAULT_SOL_ADDRESS,
DEFAULT_SOL_AMOUNT,
DEFAULT_RPC,
DEFAULT_TITLE,
DEFAULT_AVATOR,
DEFAULT_DESCRIPTION,
} from "./const";
export const GET = async (req: Request) => {
try {
const requestUrl = new URL(req.url);
const { toPubkey, amount } = validatedQueryParams(requestUrl);
const baseHref = new URL(
`/api/actions/donate?to=${toPubkey.toBase58()}`,
requestUrl.origin
).toString();
const payload: ActionGetResponse = {
title: DEFAULT_TITLE,
icon:
DEFAULT_AVATAR ?? new URL("/cute_pic.jpeg", requestUrl.origin).toString(),
description: DEFAULT_DESCRIPTION,
label: "Transfer", // this value will be ignored since `links.actions` exists
links: {
actions: [
{
label: `Send ${amount} SOL`, // button text
href: `${baseHref}&amount=${amount}`,
},
{
label: `Send ${amount * 5} SOL`, // button text
href: `${baseHref}&amount=${amount * 5}`,
},
{
label: `Send ${amount * 10} SOL`, // button text
href: `${baseHref}&amount=${amount * 10}`,
},
{
label: "Send SOL", // button text
href: `${baseHref}&amount={amount}`, // this href will have a text input
parameters: [
{
name: "amount", // parameter name in the `href` above
label: "Enter the amount of SOL to send", // placeholder of the text input
required: true,
},
],
},
],
},
};
return Response.json(payload, {
headers: ACTIONS_CORS_HEADERS,
});
} catch (err) {
console.log(err);
let message = "An unknown error occurred";
if (typeof err == "string") message = err;
return new Response(message, {
status: 400,
headers: ACTIONS_CORS_HEADERS,
});
}
};
// DO NOT FORGET TO INCLUDE THE `OPTIONS` HTTP METHOD
// THIS WILL ENSURE CORS WORKS FOR BLINKS
export const OPTIONS = GET;
export const POST = async (req: Request) => {
try {
const requestUrl = new URL(req.url);
const { amount, toPubkey } = validatedQueryParams(requestUrl);
const body: ActionPostRequest = await req.json();
// validate the client provided input
let account: PublicKey;
try {
account = new PublicKey(body.account);
} catch (err) {
return new Response('Invalid "account" provided', {
status: 400,
headers: ACTIONS_CORS_HEADERS,
});
}
const connection = new Connection(DEFAULT_RPC);
// ensure the receiving account will be rent exempt
const minimumBalance = await connection.getMinimumBalanceForRentExemption(
0 // note: simple accounts that just store native SOL have `0` bytes of data
);
if (amount * LAMPORTS_PER_SOL < minimumBalance) {
throw `account may not be rent exempt: ${toPubkey.toBase58()}`;
}
const transaction = new Transaction();
transaction.feePayer = account;
transaction.add(
SystemProgram.transfer({
fromPubkey: account,
toPubkey: toPubkey,
lamports: amount * LAMPORTS_PER_SOL,
})
);
// set the end user as the fee payer
transaction.feePayer = account;
transaction.recentBlockhash = (
await connection.getLatestBlockhash()
).blockhash;
const payload: ActionPostResponse = await createPostResponse({
fields: {
transaction,
message: `Send ${amount} SOL to ${toPubkey.toBase58()}`,
},
// note: no additional signers are needed
// signers: [],
});
return Response.json(payload, {
headers: ACTIONS_CORS_HEADERS,
});
} catch (err) {
console.log(err);
let message = "An unknown error occurred";
if (typeof err == "string") message = err;
return new Response(message, {
status: 400,
headers: ACTIONS_CORS_HEADERS,
});
}
};
function validatedQueryParams(requestUrl: URL) {
let toPubkey: PublicKey = DEFAULT_SOL_ADDRESS;
let amount: number = DEFAULT_SOL_AMOUNT;
try {
if (requestUrl.searchParams.get("to")) {
toPubkey = new PublicKey(requestUrl.searchParams.get("to")!);
}
} catch (err) {
throw "Invalid input query parameter: to";
}
try {
if (requestUrl.searchParams.get("amount")) {
amount = parseFloat(requestUrl.searchParams.get("amount")!);
}
if (amount <= 0) throw "amount is too small";
} catch (err) {
throw "Invalid input query parameter: amount";
}
return {
amount,
toPubkey,
};
}
Commit the latest code to Github.
git add .
git commit -m "add donate"
git push
Check out the full code here !
After deployment, use the following new link:
https://solana-action-mu.vercel.app/api/actions/donate
Paste your own link into Dialect.
You will see your first functioning Blink generated! Give yourself a round of applause !!
5. Conclusion
Thank you for reading to the end. If you found this guide helpful, it’s my pleasure and so happy to have assisted you. Please consider subscribing for more updates and insights. Let’s continue exploring the possibilities of blockchain together!
I am Yee Chian, a researcher at Superteam Malaysia !
Reference:
https://solana.com/solutions/actions
https://solana.com/docs/advanced/actions
https://dashboard.dialect.to/actions
https://www.jinse.cn/blockchain/3689947.html
https://twitter.com/fjun99/status/1805876326391542035
https://www.solana-cn.com/SolanaAction/001.html