@goplausible/openclaw-algorand-plugin 1.9.4 → 2.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/index.ts +74 -529
- package/lib/mcporter.ts +75 -0
- package/lib/workspace.ts +216 -0
- package/memory/MEMORY.md +6 -0
- package/memory/algorand-plugin.md +11 -0
- package/openclaw.plugin.json +25 -2
- package/package.json +12 -3
- package/setup.ts +7 -23
- package/skills/algorand-interaction/SKILL.md +22 -1
- package/skills/algorand-interaction/references/examples-algorand-mcp.md +50 -3
- package/skills/alpha-arcade-interaction/SKILL.md +15 -0
- package/skills/haystack-router-interaction/SKILL.md +9 -0
- package/scripts/backup-keyring.js +0 -117
- package/scripts/setup-keyring.sh +0 -428
package/lib/mcporter.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import { ALGORAND_MCP } from "./mcp-servers.js";
|
|
6
|
+
|
|
7
|
+
export function getMcpBinaryPath(pluginRoot: string): string {
|
|
8
|
+
const pluginBin = join(pluginRoot, "node_modules", ".bin", "algorand-mcp");
|
|
9
|
+
return existsSync(pluginBin) ? pluginBin : "algorand-mcp";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isMcpBinaryBundled(pluginRoot: string): boolean {
|
|
13
|
+
return existsSync(join(pluginRoot, "node_modules", ".bin", "algorand-mcp"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function mcporterConfigPath(): string {
|
|
17
|
+
return join(homedir(), ".mcporter", "mcporter.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type McporterServerEntry = {
|
|
21
|
+
command: string;
|
|
22
|
+
args?: string[];
|
|
23
|
+
env?: Record<string, string>;
|
|
24
|
+
description?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type McporterConfig = {
|
|
28
|
+
mcpServers: Record<string, McporterServerEntry>;
|
|
29
|
+
imports: string[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function isMcporterConfigured(): boolean {
|
|
33
|
+
const cfgPath = mcporterConfigPath();
|
|
34
|
+
if (!existsSync(cfgPath)) return false;
|
|
35
|
+
try {
|
|
36
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
37
|
+
return Boolean(cfg.mcpServers?.[ALGORAND_MCP.id]);
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function upsertMcporterConfig(pluginRoot: string): { success: boolean; message: string } {
|
|
44
|
+
const cfgPath = mcporterConfigPath();
|
|
45
|
+
const cfgDir = dirname(cfgPath);
|
|
46
|
+
|
|
47
|
+
if (!existsSync(cfgDir)) mkdirSync(cfgDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
let cfg: McporterConfig = { mcpServers: {}, imports: [] };
|
|
50
|
+
if (existsSync(cfgPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
53
|
+
cfg = {
|
|
54
|
+
mcpServers: parsed.mcpServers ?? {},
|
|
55
|
+
imports: Array.isArray(parsed.imports) ? parsed.imports : [],
|
|
56
|
+
};
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { success: false, message: `Failed to parse ${cfgPath}: ${err}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const entry: McporterServerEntry = {
|
|
63
|
+
command: getMcpBinaryPath(pluginRoot),
|
|
64
|
+
description: "Algorand blockchain MCP (GoPlausible)",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const existing = cfg.mcpServers[ALGORAND_MCP.id];
|
|
68
|
+
if (existing?.command === entry.command && existing?.description === entry.description) {
|
|
69
|
+
return { success: true, message: `algorand-mcp already registered in ${cfgPath}` };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
cfg.mcpServers[ALGORAND_MCP.id] = entry;
|
|
73
|
+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
74
|
+
return { success: true, message: `algorand-mcp registered in ${cfgPath}` };
|
|
75
|
+
}
|
package/lib/workspace.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import { upsertMcporterConfig } from "./mcporter.js";
|
|
6
|
+
|
|
7
|
+
const PLUGIN_ID = "openclaw-algorand-plugin";
|
|
8
|
+
|
|
9
|
+
export type WorkspaceApi = {
|
|
10
|
+
logger: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void };
|
|
11
|
+
runtime?: {
|
|
12
|
+
agent?: {
|
|
13
|
+
resolveAgentWorkspaceDir?: () => string;
|
|
14
|
+
ensureAgentWorkspace?: () => Promise<string> | string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
config: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function resolveWorkspaceDir(api: WorkspaceApi): string {
|
|
21
|
+
const rt = api.runtime?.agent;
|
|
22
|
+
if (rt?.resolveAgentWorkspaceDir) {
|
|
23
|
+
try { return rt.resolveAgentWorkspaceDir(); } catch { /* fall through */ }
|
|
24
|
+
}
|
|
25
|
+
const cfg = api.config as { agents?: { defaults?: { workspace?: string } } };
|
|
26
|
+
return cfg.agents?.defaults?.workspace ?? join(homedir(), ".openclaw", "workspace");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeMemoryFile(pluginRoot: string, workspacePath: string): { success: boolean; message: string } {
|
|
30
|
+
const sourceFile = join(pluginRoot, "memory", "algorand-plugin.md");
|
|
31
|
+
const memoryDir = join(workspacePath, "memory");
|
|
32
|
+
const targetFile = join(memoryDir, "algorand-plugin.md");
|
|
33
|
+
|
|
34
|
+
if (!existsSync(sourceFile)) {
|
|
35
|
+
return { success: false, message: `Source memory/algorand-plugin.md not found at ${sourceFile}` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
writeFileSync(targetFile, readFileSync(sourceFile, "utf-8"));
|
|
41
|
+
return { success: true, message: `Plugin memory written to ${targetFile}` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ensureWorkspaceMemoryIndex(pluginRoot: string, workspacePath: string): { success: boolean; message: string } {
|
|
45
|
+
const templateFile = join(pluginRoot, "memory", "MEMORY.md");
|
|
46
|
+
|
|
47
|
+
if (!existsSync(templateFile)) {
|
|
48
|
+
return { success: false, message: "Template MEMORY.md not found in plugin" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const templateContent = readFileSync(templateFile, "utf-8");
|
|
52
|
+
|
|
53
|
+
const neverForgetMatch = templateContent.match(/## NEVER FORGET\n([\s\S]*?)(?=\n## (?!NEVER)|$)/);
|
|
54
|
+
if (!neverForgetMatch) {
|
|
55
|
+
return { success: false, message: "No NEVER FORGET section found in template MEMORY.md" };
|
|
56
|
+
}
|
|
57
|
+
const templateNeverForget = neverForgetMatch[1].trimEnd();
|
|
58
|
+
|
|
59
|
+
const memoryMdPath = join(workspacePath, "MEMORY.md");
|
|
60
|
+
const memoryMdLower = join(workspacePath, "memory.md");
|
|
61
|
+
|
|
62
|
+
const existingPath = existsSync(memoryMdPath) ? memoryMdPath
|
|
63
|
+
: existsSync(memoryMdLower) ? memoryMdLower
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
if (!existingPath) {
|
|
67
|
+
if (!existsSync(workspacePath)) mkdirSync(workspacePath, { recursive: true });
|
|
68
|
+
writeFileSync(memoryMdPath, templateContent);
|
|
69
|
+
return { success: true, message: `Created ${memoryMdPath} with NEVER FORGET section` };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let existing = readFileSync(existingPath, "utf-8");
|
|
73
|
+
|
|
74
|
+
if (!/## NEVER FORGET/i.test(existing)) {
|
|
75
|
+
const firstHeadingEnd = existing.match(/^# .+\n/m);
|
|
76
|
+
if (firstHeadingEnd) {
|
|
77
|
+
const insertPos = (firstHeadingEnd.index ?? 0) + firstHeadingEnd[0].length;
|
|
78
|
+
existing = existing.slice(0, insertPos) + "\n## NEVER FORGET\n" + templateNeverForget + "\n\n" + existing.slice(insertPos);
|
|
79
|
+
} else {
|
|
80
|
+
existing = "# OpenClaw Agent Long-Term Memory\n\n## NEVER FORGET\n" + templateNeverForget + "\n\n" + existing;
|
|
81
|
+
}
|
|
82
|
+
writeFileSync(existingPath, existing);
|
|
83
|
+
return { success: true, message: `Added NEVER FORGET section to ${existingPath}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parseSubsections = (text: string): { header: string; content: string }[] => {
|
|
87
|
+
const sections: { header: string; content: string }[] = [];
|
|
88
|
+
const lines = text.split("\n");
|
|
89
|
+
let currentHeader = "";
|
|
90
|
+
let currentLines: string[] = [];
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
if (line.startsWith("### ")) {
|
|
93
|
+
if (currentHeader) sections.push({ header: currentHeader, content: currentLines.join("\n").trimEnd() });
|
|
94
|
+
currentHeader = line;
|
|
95
|
+
currentLines = [];
|
|
96
|
+
} else if (currentHeader) {
|
|
97
|
+
currentLines.push(line);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (currentHeader) sections.push({ header: currentHeader, content: currentLines.join("\n").trimEnd() });
|
|
101
|
+
return sections;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const templateSections = parseSubsections(templateNeverForget);
|
|
105
|
+
const nfSectionMatch = existing.match(/(## NEVER FORGET\n)([\s\S]*?)(?=\n## (?!#)|$)/);
|
|
106
|
+
if (!nfSectionMatch) {
|
|
107
|
+
return { success: true, message: `NEVER FORGET section in ${existingPath} is up to date` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let nfContent = nfSectionMatch[2];
|
|
111
|
+
let updated = false;
|
|
112
|
+
|
|
113
|
+
for (const templateSec of templateSections) {
|
|
114
|
+
if (templateSec.header === "### Never Do This") {
|
|
115
|
+
const neverDoRegex = /(### Never Do This\n)([\s\S]*?)(?=\n### |$)/;
|
|
116
|
+
const existingNeverDoMatch = nfContent.match(neverDoRegex);
|
|
117
|
+
|
|
118
|
+
if (!existingNeverDoMatch) {
|
|
119
|
+
nfContent = nfContent.trimEnd() + "\n\n" + templateSec.header + "\n" + templateSec.content + "\n";
|
|
120
|
+
updated = true;
|
|
121
|
+
} else {
|
|
122
|
+
let existingBullets = existingNeverDoMatch[2];
|
|
123
|
+
const templateBullets = templateSec.content.split("\n").filter((l: string) => l.startsWith("* "));
|
|
124
|
+
const existingBulletLines = existingBullets.split("\n").filter((l: string) => l.startsWith("* "));
|
|
125
|
+
|
|
126
|
+
for (const bullet of templateBullets) {
|
|
127
|
+
const fingerprint = bullet.slice(2, 52).trim();
|
|
128
|
+
const existingMatch = existingBulletLines.find((l: string) => l.includes(fingerprint));
|
|
129
|
+
if (existingMatch) {
|
|
130
|
+
if (existingMatch !== bullet) {
|
|
131
|
+
nfContent = nfContent.replace(existingMatch, bullet);
|
|
132
|
+
updated = true;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
existingBullets = existingBullets.trimEnd() + "\n" + bullet;
|
|
136
|
+
nfContent = nfContent.replace(existingNeverDoMatch[2], existingBullets);
|
|
137
|
+
updated = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
const escapedHeader = templateSec.header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
143
|
+
const sectionRegex = new RegExp("(" + escapedHeader + "\\n)([\\s\\S]*?)(?=\\n### |$)");
|
|
144
|
+
const existingSecMatch = nfContent.match(sectionRegex);
|
|
145
|
+
|
|
146
|
+
if (existingSecMatch) {
|
|
147
|
+
if (existingSecMatch[2].trimEnd() !== templateSec.content) {
|
|
148
|
+
nfContent = nfContent.replace(existingSecMatch[0], templateSec.header + "\n" + templateSec.content);
|
|
149
|
+
updated = true;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
const neverDoPos = nfContent.indexOf("### Never Do This");
|
|
153
|
+
if (neverDoPos !== -1) {
|
|
154
|
+
nfContent = nfContent.slice(0, neverDoPos) + templateSec.header + "\n" + templateSec.content + "\n\n" + nfContent.slice(neverDoPos);
|
|
155
|
+
} else {
|
|
156
|
+
nfContent = nfContent.trimEnd() + "\n\n" + templateSec.header + "\n" + templateSec.content + "\n";
|
|
157
|
+
}
|
|
158
|
+
updated = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!updated) return { success: true, message: `NEVER FORGET section in ${existingPath} is up to date` };
|
|
164
|
+
|
|
165
|
+
existing = existing.replace(nfSectionMatch[2], nfContent);
|
|
166
|
+
writeFileSync(existingPath, existing);
|
|
167
|
+
return { success: true, message: `Updated NEVER FORGET subsections in ${existingPath}` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function runFirstLoadInit(api: WorkspaceApi, pluginRoot: string, workspacePath: string): void {
|
|
171
|
+
const markerPath = join(workspacePath, ".openclaw", `${PLUGIN_ID}.initialized`);
|
|
172
|
+
if (existsSync(markerPath)) return;
|
|
173
|
+
|
|
174
|
+
const markerDir = dirname(markerPath);
|
|
175
|
+
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
|
|
176
|
+
|
|
177
|
+
const mem = writeMemoryFile(pluginRoot, workspacePath);
|
|
178
|
+
if (mem.success) api.logger.info(`[algorand-plugin] ${mem.message}`);
|
|
179
|
+
else api.logger.warn(`[algorand-plugin] ${mem.message}`);
|
|
180
|
+
|
|
181
|
+
const memIdx = ensureWorkspaceMemoryIndex(pluginRoot, workspacePath);
|
|
182
|
+
if (memIdx.success) api.logger.info(`[algorand-plugin] ${memIdx.message}`);
|
|
183
|
+
else api.logger.warn(`[algorand-plugin] ${memIdx.message}`);
|
|
184
|
+
|
|
185
|
+
const mcp = upsertMcporterConfig(pluginRoot);
|
|
186
|
+
if (mcp.success) api.logger.info(`[algorand-plugin] ${mcp.message}`);
|
|
187
|
+
else api.logger.warn(`[algorand-plugin] ${mcp.message}`);
|
|
188
|
+
|
|
189
|
+
writeFileSync(markerPath, new Date().toISOString());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function writePluginConfig(pluginConfig: Record<string, unknown>): { success: boolean; error?: string } {
|
|
193
|
+
try {
|
|
194
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
195
|
+
const configDir = dirname(configPath);
|
|
196
|
+
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
|
|
197
|
+
|
|
198
|
+
let config: Record<string, any> = {};
|
|
199
|
+
if (existsSync(configPath)) {
|
|
200
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
config.plugins ??= {};
|
|
204
|
+
config.plugins.entries ??= {};
|
|
205
|
+
config.plugins.entries[PLUGIN_ID] ??= {};
|
|
206
|
+
config.plugins.entries[PLUGIN_ID].config = pluginConfig;
|
|
207
|
+
|
|
208
|
+
config.plugins.allow ??= [];
|
|
209
|
+
if (!config.plugins.allow.includes(PLUGIN_ID)) config.plugins.allow.push(PLUGIN_ID);
|
|
210
|
+
|
|
211
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
212
|
+
return { success: true };
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return { success: false, error: String(err) };
|
|
215
|
+
}
|
|
216
|
+
}
|
package/memory/MEMORY.md
CHANGED
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
* `generate_algorand_qrcode` returns `qr` (UTF-8 text QR), `uri` (algorand:// URI), `link` (shareable hosted QR URL via QRClaw), and `expires_in` (link validity).
|
|
27
27
|
* **Channel-aware output**: In TUI/Web channels, include UTF-8 QR block + URI + shareable link. In social channels (Telegram, Discord, WhatsApp, Slack, etc.), skip the QR block (too bulky) and show only URI + shareable link.
|
|
28
28
|
|
|
29
|
+
### Transaction ID Delivery
|
|
30
|
+
* ALWAYS present transaction IDs to the user after any successful transaction submission.
|
|
31
|
+
* **Mainnet** explorer link: `https://allo.info/tx/{txId}`
|
|
32
|
+
* **Testnet** explorer link: `https://lora.algokit.io/testnet/transaction/{txId}`
|
|
33
|
+
* This applies to ALL transaction types: payments, transfers, opt-ins, app calls, atomic groups, Haystack swaps, Alpha Arcade trades, and x402 payments.
|
|
34
|
+
|
|
29
35
|
### Documentation
|
|
30
36
|
* Use `get_knowledge_doc` MCP tool for Algorand developer documentation (categories: arcs, sdks, algokit, algokit-utils, tealscript, puya, liquid-auth, python, developers, clis, nodes, details).
|
|
31
37
|
|
|
@@ -84,6 +84,17 @@ Shareable QR: [link URL]
|
|
|
84
84
|
- **URI string** — for wallet deep links
|
|
85
85
|
- **Shareable link** — renders nicely in-app as a clickable QR image
|
|
86
86
|
|
|
87
|
+
## Post-Transaction: Deliver Transaction ID
|
|
88
|
+
|
|
89
|
+
**ALWAYS** present the transaction ID to the user after any successful transaction. Use the correct explorer link:
|
|
90
|
+
|
|
91
|
+
| Network | Explorer Link Template |
|
|
92
|
+
|---------|----------------------|
|
|
93
|
+
| `mainnet` | `https://allo.info/tx/{txId}` |
|
|
94
|
+
| `testnet` | `https://lora.algokit.io/testnet/transaction/{txId}` |
|
|
95
|
+
|
|
96
|
+
This applies to ALL operations that yield a transaction ID: payments, asset transfers, opt-ins, app calls, atomic groups, Haystack Router swaps, Alpha Arcade trades, and x402 payments.
|
|
97
|
+
|
|
87
98
|
## Key things to remember
|
|
88
99
|
|
|
89
100
|
- Always check wallet with `wallet_get_info` before blockchain operations
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-algorand-plugin",
|
|
3
3
|
"name": "Algorand Integration",
|
|
4
4
|
"description": "Algorand blockchain integration with MCP and skills — by GoPlausible",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "2.0.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills/algorand-development",
|
|
8
8
|
"skills/algorand-typescript",
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
"skills/haystack-router-interaction",
|
|
15
15
|
"skills/alpha-arcade-interaction"
|
|
16
16
|
],
|
|
17
|
+
"activation": {
|
|
18
|
+
"commands": ["algorand-plugin"]
|
|
19
|
+
},
|
|
20
|
+
"commandAliases": [
|
|
21
|
+
{
|
|
22
|
+
"name": "algorand-plugin",
|
|
23
|
+
"description": "Algorand blockchain integration (GoPlausible)"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
17
26
|
"configSchema": {
|
|
18
27
|
"type": "object",
|
|
19
28
|
"additionalProperties": false,
|
|
@@ -21,7 +30,7 @@
|
|
|
21
30
|
"enableX402": {
|
|
22
31
|
"type": "boolean",
|
|
23
32
|
"default": true,
|
|
24
|
-
"description": "Enable x402 micropayments skills"
|
|
33
|
+
"description": "Enable x402 micropayments skills and x402_fetch tool"
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
},
|
|
@@ -29,5 +38,19 @@
|
|
|
29
38
|
"enableX402": {
|
|
30
39
|
"label": "Enable x402 Micropayments"
|
|
31
40
|
}
|
|
41
|
+
},
|
|
42
|
+
"setup": {
|
|
43
|
+
"summary": "Configure Algorand blockchain integration",
|
|
44
|
+
"steps": [
|
|
45
|
+
{
|
|
46
|
+
"id": "x402",
|
|
47
|
+
"label": "x402 Micropayments",
|
|
48
|
+
"description": "Enable the x402 HTTP-native payment protocol support (x402_fetch tool + skills).",
|
|
49
|
+
"configKey": "enableX402"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"postSetup": {
|
|
53
|
+
"message": "Restart the OpenClaw gateway to apply changes. algorand-mcp is auto-registered in ~/.mcporter/mcporter.json on first load."
|
|
54
|
+
}
|
|
32
55
|
}
|
|
33
56
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goplausible/openclaw-algorand-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -27,16 +27,25 @@
|
|
|
27
27
|
"index.ts",
|
|
28
28
|
"setup.ts",
|
|
29
29
|
"lib/",
|
|
30
|
-
"scripts/",
|
|
31
30
|
"skills/",
|
|
32
31
|
"memory/",
|
|
33
32
|
"openclaw.plugin.json"
|
|
34
33
|
],
|
|
35
34
|
"openclaw": {
|
|
36
|
-
"extensions": ["./index.ts"]
|
|
35
|
+
"extensions": ["./index.ts"],
|
|
36
|
+
"compat": {
|
|
37
|
+
"pluginApi": ">=2026.3.14",
|
|
38
|
+
"minGatewayVersion": "2026.3.14"
|
|
39
|
+
}
|
|
37
40
|
},
|
|
38
41
|
"dependencies": {
|
|
39
42
|
"@clack/prompts": "^0.7.0",
|
|
40
43
|
"@goplausible/algorand-mcp": "latest"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"openclaw": ">=2026.3.14"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^20.0.0"
|
|
41
50
|
}
|
|
42
51
|
}
|
package/setup.ts
CHANGED
|
@@ -1,30 +1,15 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import { join, dirname } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
2
|
import { ALGORAND_MCP, GOPLAUSIBLE_SERVICES } from "./lib/mcp-servers.js";
|
|
6
3
|
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
4
|
export interface AlgorandPluginConfig {
|
|
10
5
|
enableX402: boolean;
|
|
11
6
|
}
|
|
12
7
|
|
|
13
8
|
export async function runSetup(
|
|
14
|
-
existingConfig?: Partial<AlgorandPluginConfig
|
|
9
|
+
existingConfig?: Partial<AlgorandPluginConfig>,
|
|
15
10
|
): Promise<AlgorandPluginConfig | null> {
|
|
16
11
|
p.intro("🔷 Algorand Plugin Setup — powered by GoPlausible");
|
|
17
12
|
|
|
18
|
-
// Step 1: Keyring persistence check & setup
|
|
19
|
-
p.log.step("Checking keyring persistence for wallet storage...");
|
|
20
|
-
try {
|
|
21
|
-
const scriptPath = join(__dirname, "scripts", "setup-keyring.sh");
|
|
22
|
-
execSync(`bash "${scriptPath}" --setup`, { stdio: "inherit" });
|
|
23
|
-
} catch {
|
|
24
|
-
p.log.warn("Keyring setup script failed — you may need to configure manually.");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Step 2: x402 integration
|
|
28
13
|
const enableX402 = await p.confirm({
|
|
29
14
|
message: "Enable x402 micropayments integration?",
|
|
30
15
|
initialValue: existingConfig?.enableX402 ?? true,
|
|
@@ -35,23 +20,22 @@ export async function runSetup(
|
|
|
35
20
|
return null;
|
|
36
21
|
}
|
|
37
22
|
|
|
38
|
-
// Summary
|
|
39
23
|
const config: AlgorandPluginConfig = {
|
|
40
24
|
enableX402: enableX402 as boolean,
|
|
41
25
|
};
|
|
42
26
|
|
|
43
27
|
p.note(
|
|
44
28
|
`x402 Micropayments: ${config.enableX402 ? "Enabled" : "Disabled"}\n\n` +
|
|
45
|
-
`MCP Server
|
|
46
|
-
`
|
|
47
|
-
`
|
|
29
|
+
`MCP Server:\n` +
|
|
30
|
+
` ${ALGORAND_MCP.name} (${ALGORAND_MCP.command}) — 107 blockchain tools.\n` +
|
|
31
|
+
` Registered in ~/.mcporter/mcporter.json on first load.\n` +
|
|
32
|
+
` x402 micropayment and AP2 mandate flows are fully supported.\n`,
|
|
48
33
|
);
|
|
49
34
|
|
|
50
35
|
p.outro(
|
|
51
36
|
`🔷 Algorand plugin configured!\n\n` +
|
|
52
|
-
` Next
|
|
53
|
-
`
|
|
54
|
-
` GoPlausible website: ${GOPLAUSIBLE_SERVICES.website}`
|
|
37
|
+
` Next step: restart OpenClaw gateway.\n\n` +
|
|
38
|
+
` Docs: ${GOPLAUSIBLE_SERVICES.website}`,
|
|
55
39
|
);
|
|
56
40
|
|
|
57
41
|
return config;
|
|
@@ -110,7 +110,26 @@ Always check asset's `decimals` field with `api_algod_get_asset_by_id` before co
|
|
|
110
110
|
| 3 | `make_*_txn` | Build the transaction |
|
|
111
111
|
| 4 | `wallet_sign_transaction` | Sign with active wallet account (enforces limits) |
|
|
112
112
|
| 5 | `send_raw_transaction` | Submit signed transaction to network |
|
|
113
|
-
| 6 |
|
|
113
|
+
| 6 | **Present txID** | Show transaction ID with explorer link (see below) |
|
|
114
|
+
|
|
115
|
+
### Post-Transaction: Deliver Transaction ID
|
|
116
|
+
|
|
117
|
+
**ALWAYS** present the transaction ID to the user after any successful transaction submission. Use the correct explorer link based on the network:
|
|
118
|
+
|
|
119
|
+
| Network | Explorer Link Template |
|
|
120
|
+
|---------|----------------------|
|
|
121
|
+
| `mainnet` | `https://allo.info/tx/{txId}` |
|
|
122
|
+
| `testnet` | `https://lora.algokit.io/testnet/transaction/{txId}` |
|
|
123
|
+
|
|
124
|
+
Example output after a testnet transaction:
|
|
125
|
+
> Transaction confirmed! `TXID123...`
|
|
126
|
+
> View on explorer: https://lora.algokit.io/testnet/transaction/TXID123...
|
|
127
|
+
|
|
128
|
+
Example output after a mainnet transaction:
|
|
129
|
+
> Transaction confirmed! `TXID456...`
|
|
130
|
+
> View on explorer: https://allo.info/tx/TXID456...
|
|
131
|
+
|
|
132
|
+
This applies to ALL transaction types: payments, asset transfers, opt-ins, app calls, atomic groups, and any other operation that yields a transaction ID.
|
|
114
133
|
|
|
115
134
|
### One-Step Asset Opt-In
|
|
116
135
|
|
|
@@ -128,6 +147,7 @@ When the user provides their own secret key (not using the wallet):
|
|
|
128
147
|
| 1 | `make_*_txn` | Build the transaction |
|
|
129
148
|
| 2 | `sign_transaction` | Sign with provided secret key hex |
|
|
130
149
|
| 3 | `send_raw_transaction` | Submit signed transaction |
|
|
150
|
+
| 4 | **Present txID** | Show transaction ID with explorer link |
|
|
131
151
|
|
|
132
152
|
## Atomic Group Transaction Workflow
|
|
133
153
|
|
|
@@ -139,6 +159,7 @@ For atomic (all-or-nothing) multi-transaction groups:
|
|
|
139
159
|
| 2 | `assign_group_id` | Assign group ID to all transactions |
|
|
140
160
|
| 3 | `wallet_sign_transaction_group` | Sign all transactions in group with wallet |
|
|
141
161
|
| 4 | `send_raw_transaction` | Submit all signed transactions |
|
|
162
|
+
| 5 | **Present txIDs** | Show all transaction IDs with explorer links |
|
|
142
163
|
|
|
143
164
|
## Tool Categories
|
|
144
165
|
|
|
@@ -79,10 +79,21 @@ send_raw_transaction {
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
### Step 5:
|
|
82
|
+
### Step 5: Present transaction ID to user
|
|
83
|
+
|
|
84
|
+
**ALWAYS** show the transaction ID with the correct explorer link:
|
|
85
|
+
|
|
86
|
+
- **Mainnet**: `https://allo.info/tx/{txId}`
|
|
87
|
+
- **Testnet**: `https://lora.algokit.io/testnet/transaction/{txId}`
|
|
88
|
+
|
|
89
|
+
Example (testnet):
|
|
90
|
+
> Transaction confirmed! `[txID_from_step_4]`
|
|
91
|
+
> View on explorer: https://lora.algokit.io/testnet/transaction/[txID_from_step_4]
|
|
92
|
+
|
|
93
|
+
Optionally verify on-chain:
|
|
83
94
|
```
|
|
84
95
|
api_indexer_lookup_transaction_by_id {
|
|
85
|
-
"txId": "[
|
|
96
|
+
"txId": "[txID_from_step_4]",
|
|
86
97
|
"network": "testnet"
|
|
87
98
|
}
|
|
88
99
|
```
|
|
@@ -100,7 +111,7 @@ wallet_optin_asset {
|
|
|
100
111
|
}
|
|
101
112
|
```
|
|
102
113
|
|
|
103
|
-
This creates, signs, and submits the opt-in transaction in a single call.
|
|
114
|
+
This creates, signs, and submits the opt-in transaction in a single call. Present the returned txID with explorer link.
|
|
104
115
|
|
|
105
116
|
---
|
|
106
117
|
|
|
@@ -142,6 +153,12 @@ send_raw_transaction {
|
|
|
142
153
|
}
|
|
143
154
|
```
|
|
144
155
|
|
|
156
|
+
### Step 5: Present transaction ID to user
|
|
157
|
+
|
|
158
|
+
Show the txID with explorer link:
|
|
159
|
+
- **Testnet**: `https://lora.algokit.io/testnet/transaction/{txId}`
|
|
160
|
+
- **Mainnet**: `https://allo.info/tx/{txId}`
|
|
161
|
+
|
|
145
162
|
---
|
|
146
163
|
|
|
147
164
|
## Asset Transfer Workflow (USDC Example)
|
|
@@ -204,6 +221,12 @@ send_raw_transaction {
|
|
|
204
221
|
}
|
|
205
222
|
```
|
|
206
223
|
|
|
224
|
+
### Step 8: Present transaction ID to user
|
|
225
|
+
|
|
226
|
+
Show the txID with explorer link:
|
|
227
|
+
- **Testnet**: `https://lora.algokit.io/testnet/transaction/{txId}`
|
|
228
|
+
- **Mainnet**: `https://allo.info/tx/{txId}`
|
|
229
|
+
|
|
207
230
|
---
|
|
208
231
|
|
|
209
232
|
## Atomic Group Transaction Workflow
|
|
@@ -252,6 +275,12 @@ send_raw_transaction {
|
|
|
252
275
|
|
|
253
276
|
> Atomic groups are all-or-nothing: either all transactions succeed or none do.
|
|
254
277
|
|
|
278
|
+
### Step 5: Present transaction IDs to user
|
|
279
|
+
|
|
280
|
+
Show all txIDs from the group with explorer links:
|
|
281
|
+
- **Testnet**: `https://lora.algokit.io/testnet/transaction/{txId}`
|
|
282
|
+
- **Mainnet**: `https://allo.info/tx/{txId}`
|
|
283
|
+
|
|
255
284
|
---
|
|
256
285
|
|
|
257
286
|
## Create an ASA (Algorand Standard Asset)
|
|
@@ -299,6 +328,12 @@ api_algod_get_pending_transaction {
|
|
|
299
328
|
```
|
|
300
329
|
> The `asset-index` field in the response contains the new ASA ID.
|
|
301
330
|
|
|
331
|
+
### Step 5: Present transaction ID to user
|
|
332
|
+
|
|
333
|
+
Show the txID with explorer link:
|
|
334
|
+
- **Testnet**: `https://lora.algokit.io/testnet/transaction/{txId}`
|
|
335
|
+
- **Mainnet**: `https://allo.info/tx/{txId}`
|
|
336
|
+
|
|
302
337
|
---
|
|
303
338
|
|
|
304
339
|
## Deploy a Smart Contract
|
|
@@ -347,6 +382,12 @@ send_raw_transaction {
|
|
|
347
382
|
}
|
|
348
383
|
```
|
|
349
384
|
|
|
385
|
+
### Step 5: Present transaction ID to user
|
|
386
|
+
|
|
387
|
+
Show the txID with explorer link:
|
|
388
|
+
- **Testnet**: `https://lora.algokit.io/testnet/transaction/{txId}`
|
|
389
|
+
- **Mainnet**: `https://allo.info/tx/{txId}`
|
|
390
|
+
|
|
350
391
|
---
|
|
351
392
|
|
|
352
393
|
## NFD Lookup
|
|
@@ -592,6 +633,12 @@ send_raw_transaction {
|
|
|
592
633
|
}
|
|
593
634
|
```
|
|
594
635
|
|
|
636
|
+
### Step 4: Present transaction ID to user
|
|
637
|
+
|
|
638
|
+
Show the txID with explorer link:
|
|
639
|
+
- **Testnet**: `https://lora.algokit.io/testnet/transaction/{txId}`
|
|
640
|
+
- **Mainnet**: `https://allo.info/tx/{txId}`
|
|
641
|
+
|
|
595
642
|
---
|
|
596
643
|
|
|
597
644
|
## Top-Up QR Code (Insufficient Funds)
|
|
@@ -138,6 +138,7 @@ If a trade fails with an "overspend" error, the wallet lacks sufficient ALGO or
|
|
|
138
138
|
2. `get_orderbook` — check available liquidity
|
|
139
139
|
3. `create_market_order` (auto-matches) or `create_limit_order` (rests on book)
|
|
140
140
|
4. Save the returned `escrowAppId` — you need it to cancel
|
|
141
|
+
5. **Present txID** — show transaction ID with explorer link (see Post-Transaction below)
|
|
141
142
|
|
|
142
143
|
### Checking your portfolio
|
|
143
144
|
1. `get_positions` — see all YES/NO token balances with market titles and asset IDs
|
|
@@ -151,15 +152,29 @@ If a trade fails with an "overspend" error, the wallet lacks sufficient ALGO or
|
|
|
151
152
|
### Cancelling an order
|
|
152
153
|
1. `get_open_orders` — find the `escrowAppId` and `owner` address
|
|
153
154
|
2. `cancel_order` with `marketAppId`, `escrowAppId`, and `orderOwner`
|
|
155
|
+
3. **Present txID** — show transaction ID with explorer link
|
|
154
156
|
|
|
155
157
|
### Claiming from a resolved market
|
|
156
158
|
1. `get_positions` — find markets with token balances; note the `yesAssetId` or `noAssetId`
|
|
157
159
|
2. `claim` with `marketAppId` and the winning token's `assetId`
|
|
160
|
+
3. **Present txID** — show transaction ID with explorer link
|
|
158
161
|
|
|
159
162
|
### Providing liquidity (split/merge)
|
|
160
163
|
1. `split_shares` — convert USDC into equal YES + NO tokens
|
|
161
164
|
2. Place limit orders on both sides of the book for market making
|
|
162
165
|
3. `merge_shares` — convert matched YES + NO tokens back to USDC
|
|
166
|
+
4. **Present txIDs** — show transaction IDs with explorer links for each operation
|
|
167
|
+
|
|
168
|
+
## Post-Transaction: Deliver Transaction ID
|
|
169
|
+
|
|
170
|
+
**ALWAYS** present the transaction ID to the user after any successful trading operation (orders, cancellations, amendments, claims, splits, merges). Use the correct explorer link based on the network:
|
|
171
|
+
|
|
172
|
+
| Network | Explorer Link Template |
|
|
173
|
+
|---------|----------------------|
|
|
174
|
+
| `mainnet` | `https://allo.info/tx/{txId}` |
|
|
175
|
+
| `testnet` | `https://lora.algokit.io/testnet/transaction/{txId}` |
|
|
176
|
+
|
|
177
|
+
This applies to all Alpha Arcade trading tools: `create_market_order`, `create_limit_order`, `cancel_order`, `amend_order`, `propose_match`, `split_shares`, `merge_shares`, and `claim`.
|
|
163
178
|
|
|
164
179
|
## Common Pitfalls
|
|
165
180
|
|