Appearance
Action and Blink
作者 崔棉大师 X:@MasterCui Youtube: 崔棉大师
1.概念
Solana Actions
是符合规范的 API,返回 Solana 区块链上的交易,供预 览、签名和在各种上下文中发送,包括二维码、按钮 + 小部件和互联网上的网站。 Actions 使开发人员能够轻松地将 Solana 生态系统中的操作集成到他们的环境中,允许你 执行区块链交易而无需导航到不同的应用程序或网页。
区块链链接
——或称 blinks——将任何 Solana Action 转化为可共享的、富含元 数据的链接。 Blinks 允许支持 Action 的客户端(浏览器扩展钱包、机器人)为用户显示 额外的功能。 在网站上,blink 可能会立即在钱包中触发交易预览,而无需访问去中心化 应用程序;在 Discord 中,机器人可能会将 blink 扩展为一组交互式按钮。 这将链上交 互的能力推向任何能够显示 URL 的网页表面。
2.生命周期
3.创建一个简单的 Memo Action
1. 创建环境
通过 npx 创建一个 Next 运行环境:
sh
npx create-next-app solana-action
输出:
sh
➜ ~ npx create-next-app solana-action
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in ~/solana-action.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- react
- react-dom
- next
Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next
npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated @humanwhocodes/config-array@0.11.14: Use @eslint/config-array instead
npm WARN deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm WARN deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
added 362 packages, and audited 363 packages in 2m
137 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
Success! Created solana-action at ~/solana-action
安装 Solana web3 依赖和 Solana action 依赖
sh
cd solana-action
npm i @solana/web3.js @solana/actions
输出:
sh
➜ ~ cd solana-action
➜ solana-action git:(main) npm i @solana/web3.js @solana/actions
added 76 packages, and audited 439 packages in 4m
146 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
2.将一张 Solana Logo 图片复制到 public 目录
3.创建 Solana action 的 route 程序文件
创建目录
sh
mkdir -p src/app/api/actions/memo/
创建 route 程序文件
sh
vim src/app/api/actions/memo/route.ts
4.代码讲解
TIP
首先我们创建一个链上 memo 的 action,通过一个简单的按钮实现向 Solana 链上存储一段 message:this is a simple memo message,用这个示例代码讲解 Solana action 主体结构,即在一个 URL 中兼容 GET 和 POST 两种请求格式,并且可以按照规范的格式响应 GET 请求和 POST 请求
- 引入依赖库
js
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";
- 创建 GET 请求的返回
GET请求返回格式规范
在 Solana action 标准中,链接地址默认要通过 GET 请求返回一个元数据格式,这个返回要满足以下接口规范:
js
export interface ActionGetResponse {
/** 图片的完整链接URL */
icon: string;
/** action的标题 */
title: string;
/** action的简介 */
description: string;
/** 按钮渲染后的标签文字 */
label: string;
/** 可选项:action按钮是否有效 */
disabled?: boolean;
/** 可选项:action的按钮数组 */
links?: {
actions: LinkedAction[],
};
/** 可选项:报错信息 */
error?: ActionError;
}
icon
- 必须是描述操作的图像的绝对 HTTP 或 HTTPS URL。支持的图像格式为 SVG、PNG 或 WebP 图像。如果以上都不是,客户端必须将其拒绝为格式错误。title
- 操作标题的 UTF-8 字符串。description
- 一个 UTF-8 字符串,提供操作的简要描述。label
- UTF-8 字符串将显示在用于执行操作的按钮上。标签不应超过 5 个词组,并且应以动词开头,以强调要采取的操作。例如,"铸造 NFT"、"投票赞成"或"质押 1 SOL"。disabled
- 可选 boolean 值。如果存在,则应禁用与操作相关的所有按钮。此可选值为 false,因为不存在该值相当于 enabled=true。disabled 使用中的示例可能包括已完成铸造的 NFT 集合,或投票期已结束的治理提案。error
- 非致命错误的可选错误指示,旨在便于用户阅读并呈现给最终用户。如果设置,它不应阻止客户端解释操作或将其显示给用户。例如,错误可以与 disabled 显示原因(如业务限制、授权、状态或外部资源错误)一起使用。
js
export interface ActionError {
/** 向用户展示的非致命错误信息字符串 */
message: string;
}
links.actions
- 与此操作相关的附加操作的可选数组。每个链接操作仅被赋予一个标签(将在按钮上呈现)和一个关联的操作 URL(位于)href。不提供其他图标、标题或描述。例如,治理投票操作端点可能会为用户返回三个选项:"投票赞成"、"投票反对"和"弃权"。- 如果没有
links.actions
提供,客户端应该使用根字符串呈现单个按钮 label,并向与初始 GET 请求相同的操作 URL 端点发出 POST 请求。 - 如果
links.actions
提供了任何内容,客户端应仅根据字段中列出的项目呈现按钮和输入字段 links.actions。客户端不应为根的内容呈现按钮 label。
- 如果没有
js
export interface LinkedAction {
/** 次action的URL */
href: string;
/** 按钮显示的文字标签 */
label: string;
/** 允许用户输入的参数 */
parameters?: [ActionParameter];
}
/** 允许用户输入的参数 */
export interface ActionParameter {
/** 参数名称 */
name: string;
/** 在输入框显示的替换文本 */
label?: string;
/** 是否必填(默认:否) */
required?: boolean;
}
示例代码:
js
export const GET = (req: Request) => {
const payload: ActionGetResponse = {
icon: new URL("/solana_devs.jpg", new URL(req.url).origin).toString(),
title: "Memo Demo",
description: "This is a super simple Action",
label: "Memo Demo",
};
return Response.json(payload, {
headers: ACTIONS_CORS_HEADERS,
});
};
export const OPTIONS = GET;
- 创建 POST 请求的返回
POST请求返回格式规范
Solana action 要支持 POST 请求,并且 Blink 客户端需要按照以下格式将用户的公钥以 HTTP JSON 格式发送给 action 的 URL
json
{
"account": "<account>"
}
其中,account 是发出请求的用户的公钥的 base58 编码表示形式。
客户端应该使用 Accept-Encoding 标头,而 Action 服务应该使用 Content-Encoding 标头进行响应以进行 HTTP 压缩。
返回格式:
action 服务应使用 HTTP OK JSON 响应和以下格式来响应 POST 请求。
js
export interface ActionPostResponse {
/** base64编码的交易数据 */
transaction: string;
/** 可选项:返回消息,例如描述交易细节 */
message?: string;
}
示例代码:
js
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 至少需要 1 条非备忘录指令
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 unknow error occurred", { status: 400 });
}
};
将代码保存后就可以提交到 GitHub 了
5.提交到 Github 仓库
- 创建新仓库
- 提交代码,按顺序允许以下三条命令
sh
git remote add origin https://github.com/whaler-academy/solana-action.git
git branch -M main
git push -u origin main
输出:
sh
Enumerating objects: 20, done.
Counting objects: 100% (20/20), done.
Delta compression using up to 8 threads
Compressing objects: 100% (18/18), done.
Writing objects: 100% (20/20), 56.17 KiB | 2.16 MiB/s, done.
Total 20 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/whaler-academy/solana-action.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
6.部署到 vercel
注册并登录https://vercel.com/
新建项目:
导入仓库:
部署项目:
部署项目时输入自定义的二级域名,例如我的项目是 solana-action,部署之后可以通过 solana-action.vercel.com 访问
完成:
接下来你就可以用 URL 访问你的 Solana action 了
https://solana-action.vercel.app/api/actions/memo
返回结果应该是 JSON 格式:
json
{
"icon": "https://solana-action.vercel.app/solana_devs.jpg",
"title": "Memo Demo",
"description": "This is a super simple Action",
"label": "Memo Demo"
}
7.创建 Blink 链接
编写和部署好 Solana action 之后我们可以通过https://dial.to/创建自己的 Blink 链接,这个方法的方便之处是无需代码即可创建一个 Blink 链接收取 SOL,缺点是金额和文字都无法自定义
打开https://dial.to/输入刚才创建的 memo action 的 URL
提交之后就可以看到创建好的 Blink 前端界面了,如果我们想要分享这个链接,只需要拷贝地址栏的链接,粘贴到 X 或者其他社交平台
4.无需代码创建自己的捐款 Blink
Solana action 的官方示例代码中提供了一个 action 链接,可以将链接中的收款地址替换成自己的收款地址完成一个允许其他人向自己发送 SOL 的 Blink 链接
action 链接地址:
https://solana-actions.vercel.app/api/actions/transfer-sol?to=你的SOL收款地址
例如:
链接中的CuiDdffKV38LjgRVtiA2QiMTKhnzkjX2LUxqSMbVnGjG
为我的收款地址
有了 action 链接我们就可以将 action 链接提交到https://dial.to/创建自己的 Blink 了
创建好的 Blink 链接为:
这个由官方提供的 action 链接目前是在 dial.to 白名单中的,所以在 X 平台是可以直接以标签页形式展现出来的
如果我们想自定义收款的文字和金额,接下来我们学习 Solana action 的高级应用
5.定制一个 Donate 捐款 Blink
在前面的课程内容中,我们看到发送 SOL 的官方的 Solana action 链接通过 GET 请求返回的 JSON 数据格式为:
TIP
如果你连 GET 请求是什么都不了解,没关系,只需要将 https://solana-actions.vercel.app/api/actions/transfer-sol?to=CuiDdffKV38LjgRVtiA2QiMTKhnzkjX2LUxqSMbVnGjG 这个地址输入到浏览器地址栏再回车即可!
json
"title": "Actions Example - Transfer Native SOL",
"icon": "https://solana-actions.vercel.app/solana_devs.jpg",
"description": "Transfer SOL to another Solana wallet",
"label": "Transfer",
"links": {
"actions": [
{
"label": "Send 1 SOL",
"href": "https://solana-actions.vercel.app/api/actions/transfer-sol?to=CuiDdffKV38LjgRVtiA2QiMTKhnzkjX2LUxqSMbVnGjG&amount=1"
},
{
"label": "Send 5 SOL",
"href": "https://solana-actions.vercel.app/api/actions/transfer-sol?to=CuiDdffKV38LjgRVtiA2QiMTKhnzkjX2LUxqSMbVnGjG&amount=5"
},
{
"label": "Send 10 SOL",
"href": "https://solana-actions.vercel.app/api/actions/transfer-sol?to=CuiDdffKV38LjgRVtiA2QiMTKhnzkjX2LUxqSMbVnGjG&amount=10"
},
{
"label": "Send SOL",
"href": "https://solana-actions.vercel.app/api/actions/transfer-sol?to=CuiDdffKV38LjgRVtiA2QiMTKhnzkjX2LUxqSMbVnGjG&amount={amount}",
"parameters": [
{
"name": "amount",
"label": "Enter the amount of SOL to send",
"required": true
}
]
}
]
}
}
现在我们需要让我们自己编写的 action 程序按照规范返回上面格式的内容就可以完成 action 的 GET 请求部分
首先创建目录
sh
mkdir -p src/app/api/actions/donate/
创建 route 程序文件
sh
vim src/app/api/actions/donate/route.ts
GET 请求部分的源码
js
const toPubkey = "CuiDdffKV38LjgRVtiA2QiMTKhnzkjX2LUxqSMbVnGjG";
export const GET = async (req: Request) => {
try {
const requestUrl = new URL(req.url);
const baseHref = new URL(
`/api/actions/donate?to=${toPubkey.toBase58()}`,
requestUrl.origin
).toString();
const payload: ActionGetResponse = {
title: "向崔棉大师@MasterCui捐赠SOL",
icon: new URL("/avator.jpg", requestUrl.origin).toString(),
description: "支持第一个中文Solana action教程作者",
label: "Transfer", // 这个标签将被忽略
links: {
actions: [
{
label: `Send 0.01 SOL`, // 按钮文字
href: `${baseHref}&amount=0.01`,
},
{
label: `Send 0.05 SOL`, // 按钮文字
href: `${baseHref}&amount=0.05`,
},
{
label: `Send 0.1 SOL`, // 按钮文字
href: `${baseHref}&amount=0.1`,
},
{
label: "Send SOL", // 按钮文字
href: `${baseHref}&amount={amount}`, // 这个链接将拥有一个input输入框
parameters: [
{
name: "amount", // href中的参数名
label: "Enter the amount of SOL to send", // 输入框的替代文字
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,
});
}
};
这样的代码就可以按照规范显示出 3 个发送 SOL 的按钮,同时还会显示一个输入框,让用户可以自己定制发送 SOL 的数额
修改好代码之后提交 GitHub
sh
git add .
git commit -m "add donate"
git push
提交代码后 vercel 会自动部署前端,稍微等待几秒钟,修改就会在前端生效,这是你就拥有了一个新的捐款的 Solana action 链接:https://solana-action.vercel.app/api/actions/donate
将链接提交到https://dial.to/创建捐款的 Blink
6.使用模版一键部署 Solana Action
打开我的 GitHub 仓库 点击 README 中的 Deploy 按钮
页面这是跳转到了 Vercel,在这里首先选择一个仓库名称,这个名称不影响运行,只要和自己的其它仓库名不重复即可,默认仓库名称为:my-blink-donate-action,不修改也可以,然后点击 Create 按钮
创建仓库之后接下来需要设置环境变量,这里包括收款地址,金额,卡片标题简介头像,设置好之后点击 Deploy 按钮
然后 Vercel 将自动运行部署,等待几分钟
- 部署中
- 部署成功
部署成功后,访问
https://<你的仓库名>.vercel.com/api/actions/donate
看到 json 返回结果就代表部署正确,并且核对你输入的环境变量访问
https://dial.to/?action=solana-action%3Ahttps%3A%2F%2F<你的仓库名>.vercel.app%2Fapi%2Factions%2Fdonate
可以看到你的 Blink 链接