@oobe-protocol-labs/sap-mcp-server 0.3.0 → 0.7.1
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/.env.example +3 -0
- package/CHANGELOG.md +222 -2
- package/README.md +63 -0
- package/USER_DOCS/00_START_HERE.md +23 -1
- package/USER_DOCS/01_HOSTED_REMOTE_MCP.md +4 -1
- package/USER_DOCS/03_PAYMENTS_X402_PAYSH.md +63 -7
- package/USER_DOCS/04_CLIENT_CONFIGS.md +134 -6
- package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +262 -0
- package/dist/adapters/mcp/sdk-compat.d.ts.map +1 -1
- package/dist/adapters/mcp/sdk-compat.js +32 -18
- package/dist/adapters/mcp/sdk-compat.js.map +1 -1
- package/dist/config/mcp-client-injection.d.ts +64 -2
- package/dist/config/mcp-client-injection.d.ts.map +1 -1
- package/dist/config/mcp-client-injection.js +668 -11
- package/dist/config/mcp-client-injection.js.map +1 -1
- package/dist/config/wizard.d.ts.map +1 -1
- package/dist/config/wizard.js +89 -5
- package/dist/config/wizard.js.map +1 -1
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +1 -1
- package/dist/payments/facilitator-rpc-fallback.d.ts +43 -0
- package/dist/payments/facilitator-rpc-fallback.d.ts.map +1 -0
- package/dist/payments/facilitator-rpc-fallback.js +228 -0
- package/dist/payments/facilitator-rpc-fallback.js.map +1 -0
- package/dist/payments/json-rpc.d.ts.map +1 -1
- package/dist/payments/json-rpc.js +2 -1
- package/dist/payments/json-rpc.js.map +1 -1
- package/dist/payments/monetization-gate.d.ts +43 -2
- package/dist/payments/monetization-gate.d.ts.map +1 -1
- package/dist/payments/monetization-gate.js +618 -9
- package/dist/payments/monetization-gate.js.map +1 -1
- package/dist/payments/oobe-facilitator-server.d.ts +6 -0
- package/dist/payments/oobe-facilitator-server.d.ts.map +1 -1
- package/dist/payments/oobe-facilitator-server.js +79 -4
- package/dist/payments/oobe-facilitator-server.js.map +1 -1
- package/dist/payments/pricing.d.ts.map +1 -1
- package/dist/payments/pricing.js +1 -0
- package/dist/payments/pricing.js.map +1 -1
- package/dist/payments/usage-ledger.d.ts +10 -0
- package/dist/payments/usage-ledger.d.ts.map +1 -1
- package/dist/payments/usage-ledger.js +51 -0
- package/dist/payments/usage-ledger.js.map +1 -1
- package/dist/payments/x402-paid-call.d.ts +53 -0
- package/dist/payments/x402-paid-call.d.ts.map +1 -0
- package/dist/payments/x402-paid-call.js +370 -0
- package/dist/payments/x402-paid-call.js.map +1 -0
- package/dist/prompts/context/sap-agent-context.prompt.js +9 -3
- package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
- package/dist/prompts/developer/debug-sap-error.prompt.js +1 -1
- package/dist/prompts/developer/debug-sap-error.prompt.js.map +1 -1
- package/dist/prompts/developer/generate-sap-integration.prompt.js +6 -2
- package/dist/prompts/developer/generate-sap-integration.prompt.js.map +1 -1
- package/dist/prompts/execution-proof/explain-proof-of-execution.prompt.js +1 -1
- package/dist/prompts/execution-proof/explain-proof-of-execution.prompt.js.map +1 -1
- package/dist/prompts/execution-proof/verify-execution-proof.prompt.js +1 -1
- package/dist/prompts/execution-proof/verify-execution-proof.prompt.js.map +1 -1
- package/dist/prompts/payments/create-paid-api.prompt.js +1 -1
- package/dist/prompts/payments/create-paid-api.prompt.js.map +1 -1
- package/dist/prompts/payments/explain-x402-settlement.prompt.js +1 -1
- package/dist/prompts/payments/explain-x402-settlement.prompt.js.map +1 -1
- package/dist/prompts/register-prompts.js +2 -2
- package/dist/prompts/register-prompts.js.map +1 -1
- package/dist/prompts/registry/analyze-sap-agent.prompt.js +1 -1
- package/dist/prompts/registry/analyze-sap-agent.prompt.js.map +1 -1
- package/dist/prompts/registry/register-sap-agent.prompt.js +2 -2
- package/dist/prompts/registry/register-sap-agent.prompt.js.map +1 -1
- package/dist/remote/server.d.ts +132 -1
- package/dist/remote/server.d.ts.map +1 -1
- package/dist/remote/server.js +930 -49
- package/dist/remote/server.js.map +1 -1
- package/dist/resources/current/sap-current-config.resource.d.ts.map +1 -1
- package/dist/resources/current/sap-current-config.resource.js +11 -1
- package/dist/resources/current/sap-current-config.resource.js.map +1 -1
- package/dist/resources/execution-proof/sap-execution-record.resource.js +1 -1
- package/dist/resources/execution-proof/sap-execution-record.resource.js.map +1 -1
- package/dist/resources/memory/sap-memory.resource.js +1 -1
- package/dist/resources/memory/sap-memory.resource.js.map +1 -1
- package/dist/resources/profile/sap-active-profile.resource.js +1 -1
- package/dist/resources/profile/sap-active-profile.resource.js.map +1 -1
- package/dist/resources/register-resources.js +2 -2
- package/dist/resources/register-resources.js.map +1 -1
- package/dist/resources/registry/sap-agent.resource.js +1 -1
- package/dist/resources/registry/sap-agent.resource.js.map +1 -1
- package/dist/resources/registry/sap-global-registry.resource.js +1 -1
- package/dist/resources/registry/sap-global-registry.resource.js.map +1 -1
- package/dist/resources/reputation/sap-reputation.resource.js +1 -1
- package/dist/resources/reputation/sap-reputation.resource.js.map +1 -1
- package/dist/resources/stats/sap-network-stats.resource.js +1 -1
- package/dist/resources/stats/sap-network-stats.resource.js.map +1 -1
- package/dist/resources/tool-schema/sap-tool-schema.resource.js +1 -1
- package/dist/resources/tool-schema/sap-tool-schema.resource.js.map +1 -1
- package/dist/sap/sap-client-manager.js +5 -5
- package/dist/sap/sap-client-manager.js.map +1 -1
- package/dist/security/tool-permissions.d.ts.map +1 -1
- package/dist/security/tool-permissions.js +1 -0
- package/dist/security/tool-permissions.js.map +1 -1
- package/dist/server/create-server.js +5 -5
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/register-capabilities.js +4 -4
- package/dist/server/register-capabilities.js.map +1 -1
- package/dist/signer/signer-resolver.js +1 -1
- package/dist/signer/signer-resolver.js.map +1 -1
- package/dist/tools/client-sdk-tools.js +7 -7
- package/dist/tools/client-sdk-tools.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/magicblock-tools.d.ts +28 -0
- package/dist/tools/magicblock-tools.d.ts.map +1 -0
- package/dist/tools/magicblock-tools.js +578 -0
- package/dist/tools/magicblock-tools.js.map +1 -0
- package/dist/tools/profile-tools.d.ts.map +1 -1
- package/dist/tools/profile-tools.js +20 -0
- package/dist/tools/profile-tools.js.map +1 -1
- package/dist/tools/register-tools.d.ts.map +1 -1
- package/dist/tools/register-tools.js +11 -2
- package/dist/tools/register-tools.js.map +1 -1
- package/dist/tools/sap-sdk-tools.js +2 -2
- package/dist/tools/sap-sdk-tools.js.map +1 -1
- package/dist/tools/sap-sns-tools.js +2 -2
- package/dist/tools/sap-sns-tools.js.map +1 -1
- package/dist/tools/skills-tools.d.ts +1 -1
- package/dist/tools/skills-tools.d.ts.map +1 -1
- package/dist/tools/skills-tools.js +10 -1
- package/dist/tools/skills-tools.js.map +1 -1
- package/dist/tools/tool-aliases.d.ts +10 -0
- package/dist/tools/tool-aliases.d.ts.map +1 -0
- package/dist/tools/tool-aliases.js +23 -0
- package/dist/tools/tool-aliases.js.map +1 -0
- package/dist/tools/x402-paid-call-tool.d.ts +12 -0
- package/dist/tools/x402-paid-call-tool.d.ts.map +1 -0
- package/dist/tools/x402-paid-call-tool.js +118 -0
- package/dist/tools/x402-paid-call-tool.js.map +1 -0
- package/dist/wizard-core/desktop-flow.d.ts +82 -0
- package/dist/wizard-core/desktop-flow.d.ts.map +1 -0
- package/dist/wizard-core/desktop-flow.js +181 -0
- package/dist/wizard-core/desktop-flow.js.map +1 -0
- package/docs/00_README.md +3 -0
- package/docs/03_CONFIGURATION_AND_WIZARD.md +31 -5
- package/docs/06_PAYMENTS_X402_AND_PAYSH.md +70 -7
- package/docs/07_ENDPOINTS_AND_CLIENTS.md +28 -1
- package/docs/10_OPERATIONS_RELEASE_AND_PM2.md +7 -6
- package/docs/13_BOUNTY_PROGRAM_PROPOSAL.md +469 -0
- package/docs/14_DESKTOP_WIZARD_RELEASE.md +264 -0
- package/docs/README.md +104 -0
- package/docs/_sidebar.md +31 -0
- package/docs/magicblock-tools.md +166 -0
- package/docs/x402-protocol-spec.md +984 -0
- package/package.json +23 -2
- package/server.json +71 -0
- package/skills/sap-defi/SKILL.md +8 -0
- package/skills/sap-mcp/SKILL.md +30 -0
- package/skills/sap-payments-x402/SKILL.md +36 -4
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MagicBlock MCP Tools
|
|
3
|
+
*
|
|
4
|
+
* Registers 20 MagicBlock tools across 3 protocol domains:
|
|
5
|
+
* - mb-router (6 read-only ER Router JSON-RPC tools)
|
|
6
|
+
* - mb-payments (12 Private Payment API REST tools)
|
|
7
|
+
* - mb-vrf (2 Solana VRF tools — on-chain via @solana/web3.js)
|
|
8
|
+
*
|
|
9
|
+
* Pricing:
|
|
10
|
+
* READ = $0.01/call (10_000 USDC base units) — 14 read-only tools
|
|
11
|
+
* WRITE = $0.05/call (50_000 USDC base units) — 6 transaction-building tools
|
|
12
|
+
*
|
|
13
|
+
* Write tools (deposit, transfer, withdraw, swap, initializeMint) return
|
|
14
|
+
* unsigned transactions. The caller must sign with sap_sign_transaction
|
|
15
|
+
* and submit with sap_submit_signed_transaction to the RPC indicated by
|
|
16
|
+
* the `sendTo` field ("base" or "ephemeral").
|
|
17
|
+
*/
|
|
18
|
+
import { PublicKey, TransactionInstruction, Transaction, SystemProgram } from '@solana/web3.js';
|
|
19
|
+
import { createTextResponse } from '../adapters/mcp/tool-response.js';
|
|
20
|
+
import { registerTool } from '../adapters/mcp/sdk-compat.js';
|
|
21
|
+
import { logger } from '../core/logger.js';
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
23
|
+
// Constants
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
25
|
+
const ROUTER_ENDPOINTS = {
|
|
26
|
+
mainnet: 'https://router.magicblock.app',
|
|
27
|
+
devnet: 'https://devnet-router.magicblock.app',
|
|
28
|
+
};
|
|
29
|
+
const PAYMENTS_ENDPOINT = 'https://payments.magicblock.app';
|
|
30
|
+
const JSONRPC_VERSION = '2.0';
|
|
31
|
+
// ─── VRF Program Constants (from ephemeral_vrf_sdk::consts) ───────
|
|
32
|
+
const VRF_PROGRAM_ID = new PublicKey('Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz');
|
|
33
|
+
const VRF_PROGRAM_IDENTITY = new PublicKey('9irBy75QS2BN81FUgXuHcjqceJJRuc9oDkAe8TKVvvAw');
|
|
34
|
+
const DEFAULT_QUEUE = new PublicKey('Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh');
|
|
35
|
+
const DEFAULT_EPHEMERAL_QUEUE = new PublicKey('5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc');
|
|
36
|
+
// ─── Pricing Constants ────────────────────────────────────────────
|
|
37
|
+
const READ_PRICE_USDC = 10000n;
|
|
38
|
+
const WRITE_PRICE_USDC = 50000n;
|
|
39
|
+
const READ_TOOLS = new Set([
|
|
40
|
+
'magicblock_getRoutes', 'magicblock_getIdentity', 'magicblock_getDelegationStatus',
|
|
41
|
+
'magicblock_getAccountInfo', 'magicblock_getBlockhashForAccounts', 'magicblock_getSignatureStatuses',
|
|
42
|
+
'magicblock_health', 'magicblock_challenge', 'magicblock_login',
|
|
43
|
+
'magicblock_balance', 'magicblock_privateBalance', 'magicblock_swapQuote',
|
|
44
|
+
'magicblock_isMintInitialized', 'magicblock_getRandomnessResult',
|
|
45
|
+
]);
|
|
46
|
+
const WRITE_TOOLS = new Set([
|
|
47
|
+
'magicblock_deposit', 'magicblock_transfer', 'magicblock_withdraw',
|
|
48
|
+
'magicblock_swap', 'magicblock_initializeMint', 'magicblock_requestRandomness',
|
|
49
|
+
]);
|
|
50
|
+
function getPriceForTool(toolName) {
|
|
51
|
+
if (READ_TOOLS.has(toolName))
|
|
52
|
+
return READ_PRICE_USDC;
|
|
53
|
+
if (WRITE_TOOLS.has(toolName))
|
|
54
|
+
return WRITE_PRICE_USDC;
|
|
55
|
+
throw new Error(`No pricing defined for tool: ${toolName}`);
|
|
56
|
+
}
|
|
57
|
+
async function rpcCall(endpoint, method, params) {
|
|
58
|
+
const url = ROUTER_ENDPOINTS[endpoint];
|
|
59
|
+
const res = await fetch(url, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ jsonrpc: JSONRPC_VERSION, id: 1, method, params }),
|
|
63
|
+
});
|
|
64
|
+
const text = await res.text();
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
throw new Error(`MagicBlock Router ${res.status}: ${text.slice(0, 200)}`);
|
|
67
|
+
const json = JSON.parse(text);
|
|
68
|
+
if (json.error)
|
|
69
|
+
throw new Error(`JSON-RPC error ${json.error.code}: ${json.error.message}`);
|
|
70
|
+
if (!json.result)
|
|
71
|
+
throw new Error(`JSON-RPC response missing result for ${method}`);
|
|
72
|
+
return json.result;
|
|
73
|
+
}
|
|
74
|
+
async function apiGet(path, query, authToken) {
|
|
75
|
+
const url = new URL(PAYMENTS_ENDPOINT + path);
|
|
76
|
+
if (query) {
|
|
77
|
+
for (const [key, value] of Object.entries(query)) {
|
|
78
|
+
if (value != null)
|
|
79
|
+
url.searchParams.set(key, value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const headers = {};
|
|
83
|
+
if (authToken)
|
|
84
|
+
headers['Authorization'] = `Bearer ${authToken}`;
|
|
85
|
+
const res = await fetch(url.toString(), { method: 'GET', headers });
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
throw new Error(`MagicBlock API ${res.status}: ${text.slice(0, 200)}`);
|
|
89
|
+
return JSON.parse(text);
|
|
90
|
+
}
|
|
91
|
+
async function apiPost(path, body, authToken) {
|
|
92
|
+
const url = PAYMENTS_ENDPOINT + path;
|
|
93
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
94
|
+
if (authToken)
|
|
95
|
+
headers['Authorization'] = `Bearer ${authToken}`;
|
|
96
|
+
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
97
|
+
const text = await res.text();
|
|
98
|
+
if (!res.ok)
|
|
99
|
+
throw new Error(`MagicBlock API ${res.status}: ${text.slice(0, 200)}`);
|
|
100
|
+
return JSON.parse(text);
|
|
101
|
+
}
|
|
102
|
+
/** Strip nullish values from an object. Returns a new object with only non-null values. */
|
|
103
|
+
function stripNullish(obj) {
|
|
104
|
+
const result = {};
|
|
105
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
106
|
+
if (value != null)
|
|
107
|
+
result[key] = value;
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
112
|
+
// JSON Schema Helpers
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
114
|
+
function schema(properties, required) {
|
|
115
|
+
return {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties,
|
|
118
|
+
...(required && required.length > 0 ? { required } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const f = {
|
|
122
|
+
pubkey: (d) => ({ type: 'string', description: d }),
|
|
123
|
+
string: (d) => ({ type: 'string', description: d }),
|
|
124
|
+
number: (d) => ({ type: 'number', description: d }),
|
|
125
|
+
boolean: (d) => ({ type: 'boolean', description: d }),
|
|
126
|
+
enum: (d, values) => ({ type: 'string', enum: values, description: d }),
|
|
127
|
+
array: (d, items) => ({ type: 'array', items, description: d }),
|
|
128
|
+
object: (d, props) => ({ type: 'object', properties: props, description: d }),
|
|
129
|
+
};
|
|
130
|
+
const endpointField = f.enum("MagicBlock Router endpoint: 'mainnet' or 'devnet'", ['mainnet', 'devnet']);
|
|
131
|
+
const clusterField = f.string("Cluster: 'mainnet', 'devnet', or custom RPC URL");
|
|
132
|
+
const validatorField = f.string('Optional ER validator pubkey. Defaults to the selected ephemeral RPC identity.');
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
134
|
+
// Registration
|
|
135
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
136
|
+
/**
|
|
137
|
+
* @name registerMagicBlockTools
|
|
138
|
+
* @description Registers 20 MagicBlock tools (ER Router, Private Payments, VRF)
|
|
139
|
+
* with the MCP server. Each tool is priced at $0.01 (read) or $0.05 (write).
|
|
140
|
+
* @param server - MCP server receiving tool definitions and handlers.
|
|
141
|
+
* @param context - Shared runtime context (VRF getRandomnessResult uses the Solana connection).
|
|
142
|
+
*/
|
|
143
|
+
export function registerMagicBlockTools(server, context) {
|
|
144
|
+
logger.debug('Registering MagicBlock tools');
|
|
145
|
+
let registered = 0;
|
|
146
|
+
function register(name, description, inputSchema, handler) {
|
|
147
|
+
registerTool(server, name, { title: name.replace(/_/g, ' '), description, inputSchema }, handler);
|
|
148
|
+
registered++;
|
|
149
|
+
}
|
|
150
|
+
function handleError(toolName, error) {
|
|
151
|
+
logger.error(`MagicBlock tool failed: ${toolName}`, { error });
|
|
152
|
+
const errorResponse = {
|
|
153
|
+
error: `MagicBlock tool ${toolName} failed`,
|
|
154
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
155
|
+
};
|
|
156
|
+
return createTextResponse(JSON.stringify(errorResponse, null, 2), { isError: true });
|
|
157
|
+
}
|
|
158
|
+
function success(data, toolName) {
|
|
159
|
+
const price = getPriceForTool(toolName);
|
|
160
|
+
const priceUsd = Number(price) / 1e6;
|
|
161
|
+
const response = {
|
|
162
|
+
success: true,
|
|
163
|
+
tool: toolName,
|
|
164
|
+
priceUsd: `$${priceUsd.toFixed(2)}`,
|
|
165
|
+
priceBaseUnits: price.toString(),
|
|
166
|
+
data,
|
|
167
|
+
};
|
|
168
|
+
return createTextResponse(JSON.stringify(response, null, 2));
|
|
169
|
+
}
|
|
170
|
+
function parseInput(raw) {
|
|
171
|
+
return raw;
|
|
172
|
+
}
|
|
173
|
+
// ═══════════════════════════════════════════════════════════════
|
|
174
|
+
// ER Router (6 read-only tools)
|
|
175
|
+
// ═══════════════════════════════════════════════════════════════
|
|
176
|
+
register('magicblock_getRoutes', 'List available Ephemeral Rollup nodes from the Magic Router (identity, FQDN, fee, block time, country). Price: $0.01.', schema({ endpoint: endpointField }), async (raw) => {
|
|
177
|
+
try {
|
|
178
|
+
const { endpoint = 'devnet' } = parseInput(raw);
|
|
179
|
+
const routes = await rpcCall(endpoint, 'getRoutes', []);
|
|
180
|
+
return success({ routes }, 'magicblock_getRoutes');
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
return handleError('magicblock_getRoutes', e);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
register('magicblock_getIdentity', 'Get the identity and FQDN of the current ER Validator node. Price: $0.01.', schema({ endpoint: endpointField }), async (raw) => {
|
|
187
|
+
try {
|
|
188
|
+
const { endpoint = 'devnet' } = parseInput(raw);
|
|
189
|
+
const identity = await rpcCall(endpoint, 'getIdentity', []);
|
|
190
|
+
return success(identity, 'magicblock_getIdentity');
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
return handleError('magicblock_getIdentity', e);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
register('magicblock_getDelegationStatus', 'Check whether a Solana account is delegated to an Ephemeral Rollup. Returns authority, owner, delegation slot, and lamports. Price: $0.01.', schema({ account: f.pubkey('Account pubkey to check delegation status for'), endpoint: endpointField }, ['account']), async (raw) => {
|
|
197
|
+
try {
|
|
198
|
+
const { account, endpoint = 'devnet' } = parseInput(raw);
|
|
199
|
+
const status = await rpcCall(endpoint, 'getDelegationStatus', [account]);
|
|
200
|
+
return success(status, 'magicblock_getDelegationStatus');
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
return handleError('magicblock_getDelegationStatus', e);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
register('magicblock_getAccountInfo', 'Fetch account information (data, lamports, owner, executable, space) via the Magic Router. Price: $0.01.', schema({ account: f.pubkey('Account pubkey to fetch info for'), encoding: f.enum('Encoding for account data', ['base64', 'base64+zstd']), endpoint: endpointField }, ['account']), async (raw) => {
|
|
207
|
+
try {
|
|
208
|
+
const { account, encoding = 'base64', endpoint = 'devnet' } = parseInput(raw);
|
|
209
|
+
const info = await rpcCall(endpoint, 'getAccountInfo', [account, { encoding }]);
|
|
210
|
+
return success(info, 'magicblock_getAccountInfo');
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
return handleError('magicblock_getAccountInfo', e);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
register('magicblock_getBlockhashForAccounts', 'Get a blockhash and last valid block height for a batch of account addresses (max 100). Price: $0.01.', schema({ accounts: f.array('Array of account addresses (max 100)', f.pubkey('Account pubkey')), endpoint: endpointField }, ['accounts']), async (raw) => {
|
|
217
|
+
try {
|
|
218
|
+
const { accounts, endpoint = 'devnet' } = parseInput(raw);
|
|
219
|
+
const blockhash = await rpcCall(endpoint, 'getBlockhashForAccounts', [accounts]);
|
|
220
|
+
return success(blockhash, 'magicblock_getBlockhashForAccounts');
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
return handleError('magicblock_getBlockhashForAccounts', e);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
register('magicblock_getSignatureStatuses', 'Check the confirmation status (processed/confirmed/finalized) of one or more transaction signatures. Price: $0.01.', schema({ signatures: f.array('Array of transaction signatures', f.string('Transaction signature (base58)')), endpoint: endpointField }, ['signatures']), async (raw) => {
|
|
227
|
+
try {
|
|
228
|
+
const { signatures, endpoint = 'devnet' } = parseInput(raw);
|
|
229
|
+
const statuses = await rpcCall(endpoint, 'getSignatureStatuses', [signatures]);
|
|
230
|
+
return success(statuses, 'magicblock_getSignatureStatuses');
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return handleError('magicblock_getSignatureStatuses', e);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════
|
|
237
|
+
// Private Payment API — Meta & Auth (3 tools)
|
|
238
|
+
// ═══════════════════════════════════════════════════════════════
|
|
239
|
+
register('magicblock_health', 'Check the health status of the MagicBlock Private Payments API. Price: $0.01.', schema({}), async () => {
|
|
240
|
+
try {
|
|
241
|
+
const health = await apiGet('/health');
|
|
242
|
+
return success(health, 'magicblock_health');
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
return handleError('magicblock_health', e);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
register('magicblock_challenge', 'Generate a challenge string for the wallet to sign (step 1 of the PER auth flow). Price: $0.01.', schema({ pubkey: f.pubkey('Wallet pubkey that will sign the challenge'), cluster: clusterField, mock: f.boolean('Use a mock challenge for testing (default false)') }, ['pubkey']), async (raw) => {
|
|
249
|
+
try {
|
|
250
|
+
const { pubkey, cluster, mock } = parseInput(raw);
|
|
251
|
+
const result = await apiGet('/v1/spl/challenge', {
|
|
252
|
+
pubkey, cluster: cluster ?? undefined, mock: mock ? 'true' : undefined,
|
|
253
|
+
});
|
|
254
|
+
return success(result, 'magicblock_challenge');
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
return handleError('magicblock_challenge', e);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
register('magicblock_login', 'Exchange a signed challenge for a bearer token (step 2 of PER auth flow). The token is used for private-balance and private transfers. Price: $0.01.', schema({ pubkey: f.pubkey('Wallet pubkey that signed the challenge'), challenge: f.string('Challenge string from magicblock_challenge'), signature: f.string('Wallet signature over the challenge string'), cluster: clusterField, mock: f.boolean('Use mock login flow (default false)') }, ['pubkey', 'challenge', 'signature']), async (raw) => {
|
|
261
|
+
try {
|
|
262
|
+
const { pubkey, challenge, signature, cluster, mock } = parseInput(raw);
|
|
263
|
+
const result = await apiPost('/v1/spl/login', {
|
|
264
|
+
pubkey, challenge, signature, cluster: cluster ?? undefined, mock: mock || undefined,
|
|
265
|
+
});
|
|
266
|
+
return success(result, 'magicblock_login');
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
return handleError('magicblock_login', e);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
// ═══════════════════════════════════════════════════════════════
|
|
273
|
+
// Private Payment API — Balance (2 tools)
|
|
274
|
+
// ═══════════════════════════════════════════════════════════════
|
|
275
|
+
register('magicblock_balance', 'Read the base-chain SPL token balance for an address (public, no auth required). Price: $0.01.', schema({ address: f.pubkey('Owner wallet pubkey'), mint: f.string('SPL mint pubkey'), cluster: clusterField }, ['address', 'mint']), async (raw) => {
|
|
276
|
+
try {
|
|
277
|
+
const { address, mint, cluster } = parseInput(raw);
|
|
278
|
+
const result = await apiGet('/v1/spl/balance', { address, mint, cluster: cluster ?? undefined });
|
|
279
|
+
return success(result, 'magicblock_balance');
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
return handleError('magicblock_balance', e);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
register('magicblock_privateBalance', 'Read the ephemeral-rollup SPL token balance for an address (requires bearer token from login). Price: $0.01.', schema({ address: f.pubkey('Owner wallet pubkey'), mint: f.string('SPL mint pubkey'), cluster: clusterField, authToken: f.string('Bearer token from magicblock_login (required for private reads)') }, ['address', 'mint', 'authToken']), async (raw) => {
|
|
286
|
+
try {
|
|
287
|
+
const { address, mint, cluster, authToken } = parseInput(raw);
|
|
288
|
+
const result = await apiGet('/v1/spl/private-balance', { address, mint, cluster: cluster ?? undefined }, authToken);
|
|
289
|
+
return success(result, 'magicblock_privateBalance');
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
return handleError('magicblock_privateBalance', e);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
// ═══════════════════════════════════════════════════════════════
|
|
296
|
+
// Private Payment API — SPL Token Flows (3 write tools)
|
|
297
|
+
// ═══════════════════════════════════════════════════════════════
|
|
298
|
+
register('magicblock_deposit', 'Build an unsigned transaction to deposit SPL tokens from Solana into an Ephemeral Rollup. Sign with sap_sign_transaction and submit to the RPC indicated by sendTo. Price: $0.05.', schema({ owner: f.pubkey('Wallet pubkey that owns the tokens and will sign'), amount: f.number('Base-unit amount to deposit (integer, minimum 1)'), mint: f.string('SPL mint. Defaults to USDC (mainnet) or devnet USDC'), cluster: clusterField, validator: validatorField, initIfMissing: f.boolean('Initialize the transfer queue if missing (default true)'), initVaultIfMissing: f.boolean('Initialize the vault if missing (default true)'), initAtasIfMissing: f.boolean('Initialize associated token accounts if missing (default true)'), idempotent: f.boolean('Use idempotent variants for preparatory init instructions (default true)') }, ['owner', 'amount']), async (raw) => {
|
|
299
|
+
try {
|
|
300
|
+
const input = parseInput(raw);
|
|
301
|
+
const result = await apiPost('/v1/spl/deposit', {
|
|
302
|
+
owner: input.owner, amount: input.amount, ...stripNullish({
|
|
303
|
+
mint: input.mint, cluster: input.cluster, validator: input.validator,
|
|
304
|
+
initIfMissing: input.initIfMissing, initVaultIfMissing: input.initVaultIfMissing,
|
|
305
|
+
initAtasIfMissing: input.initAtasIfMissing, idempotent: input.idempotent,
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
return success(result, 'magicblock_deposit');
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
return handleError('magicblock_deposit', e);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
register('magicblock_transfer', 'Build an unsigned SPL token transfer (public or private) through an Ephemeral Rollup. Supports base/ephemeral source and destination, delayed settlement, split transfers, and gasless mode. Price: $0.05.', schema({
|
|
315
|
+
from: f.pubkey('Sender wallet pubkey'), to: f.pubkey('Recipient wallet pubkey'), mint: f.string('SPL mint pubkey'),
|
|
316
|
+
amount: f.number('Base-unit amount to transfer (integer, minimum 1)'),
|
|
317
|
+
visibility: f.enum("'public' = transparent SPL transfer, 'private' = routed through Private ER with delayed+split settlement", ['public', 'private']),
|
|
318
|
+
fromBalance: f.enum("Where the sender's balance is held", ['base', 'ephemeral']),
|
|
319
|
+
toBalance: f.enum('Where the recipient should receive funds', ['base', 'ephemeral']),
|
|
320
|
+
cluster: clusterField, validator: f.string('Optional ER validator pubkey'),
|
|
321
|
+
authToken: f.string('Bearer token from login (required for private transfers)'),
|
|
322
|
+
initIfMissing: f.boolean('Initialize transfer queue if missing (default true)'),
|
|
323
|
+
initAtasIfMissing: f.boolean('Initialize recipient ATA if missing (default true)'),
|
|
324
|
+
initVaultIfMissing: f.boolean('Initialize vault if missing (default false)'),
|
|
325
|
+
memo: f.string('Optional memo appended to the transaction'),
|
|
326
|
+
minDelayMs: f.string("Private only. Earliest (ms) the queued transfer may settle. Default '0'"),
|
|
327
|
+
maxDelayMs: f.string('Private only. Latest (ms) the queued transfer may settle (<= 600000)'),
|
|
328
|
+
clientRefId: f.string('Private only. Encrypted client reference ID for payment confirmation'),
|
|
329
|
+
split: f.number('Private only. Number of queue entries to split across (1-15, default 1)'),
|
|
330
|
+
gasless: f.boolean('When true, uses configured sponsor as fee payer (default false)'),
|
|
331
|
+
legacy: f.boolean('Skip lookup-table compilation, return a legacy transaction (default false)'),
|
|
332
|
+
}, ['from', 'to', 'mint', 'amount', 'visibility', 'fromBalance', 'toBalance']), async (raw) => {
|
|
333
|
+
try {
|
|
334
|
+
const input = parseInput(raw);
|
|
335
|
+
const result = await apiPost('/v1/spl/transfer', {
|
|
336
|
+
from: input.from, to: input.to, mint: input.mint, amount: input.amount,
|
|
337
|
+
visibility: input.visibility, fromBalance: input.fromBalance, toBalance: input.toBalance,
|
|
338
|
+
...stripNullish({
|
|
339
|
+
cluster: input.cluster, validator: input.validator,
|
|
340
|
+
initIfMissing: input.initIfMissing, initAtasIfMissing: input.initAtasIfMissing,
|
|
341
|
+
initVaultIfMissing: input.initVaultIfMissing, memo: input.memo,
|
|
342
|
+
minDelayMs: input.minDelayMs, maxDelayMs: input.maxDelayMs,
|
|
343
|
+
clientRefId: input.clientRefId, split: input.split,
|
|
344
|
+
gasless: input.gasless, legacy: input.legacy,
|
|
345
|
+
}),
|
|
346
|
+
}, input.authToken);
|
|
347
|
+
return success(result, 'magicblock_transfer');
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
return handleError('magicblock_transfer', e);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
register('magicblock_withdraw', 'Build an unsigned transaction to withdraw SPL tokens from an Ephemeral Rollup back to Solana. Price: $0.05.', schema({ owner: f.pubkey('Wallet pubkey that owns the tokens and will sign'), mint: f.string('SPL mint on Solana'), amount: f.number('Base-unit amount to withdraw (integer, minimum 1)'), cluster: clusterField, validator: f.string('Optional ER validator pubkey'), initIfMissing: f.boolean('Initialize transfer queue if missing (default true)'), initAtasIfMissing: f.boolean('Initialize ATAs if missing (default true)'), escrowIndex: f.number('Optional escrow index for the withdrawal'), idempotent: f.boolean('Use idempotent variants for preparatory init instructions (default true)') }, ['owner', 'mint', 'amount']), async (raw) => {
|
|
354
|
+
try {
|
|
355
|
+
const input = parseInput(raw);
|
|
356
|
+
const result = await apiPost('/v1/spl/withdraw', {
|
|
357
|
+
owner: input.owner, mint: input.mint, amount: input.amount,
|
|
358
|
+
...stripNullish({
|
|
359
|
+
cluster: input.cluster, validator: input.validator,
|
|
360
|
+
initIfMissing: input.initIfMissing, initAtasIfMissing: input.initAtasIfMissing,
|
|
361
|
+
escrowIndex: input.escrowIndex, idempotent: input.idempotent,
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
return success(result, 'magicblock_withdraw');
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
return handleError('magicblock_withdraw', e);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════
|
|
371
|
+
// Private Payment API — Swap (2 tools)
|
|
372
|
+
// ═══════════════════════════════════════════════════════════════
|
|
373
|
+
register('magicblock_swapQuote', 'Get a swap quote between two SPL mints (proxies Triton Metis swap API). Pass the result into magicblock_swap. Price: $0.01.', schema({ inputMint: f.string('Input token mint address'), outputMint: f.string('Output token mint address'), amount: f.string("Raw amount to swap (unsigned integer string, e.g. '1000000')"), slippageBps: f.number('Slippage threshold in basis points (e.g. 50 = 0.5%)'), swapMode: f.enum('Swap mode: fixed input or fixed output amount', ['ExactIn', 'ExactOut']), onlyDirectRoutes: f.boolean('Limit routing to a single hop (default false)'), restrictIntermediateTokens: f.boolean('Restrict intermediate tokens to a more stable set (default false)'), platformFeeBps: f.number('Optional platform fee in basis points'), maxAccounts: f.number('Approximate maximum account budget for the route (default 64)') }, ['inputMint', 'outputMint', 'amount']), async (raw) => {
|
|
374
|
+
try {
|
|
375
|
+
const input = parseInput(raw);
|
|
376
|
+
const result = await apiGet('/v1/swap/quote', {
|
|
377
|
+
inputMint: input.inputMint, outputMint: input.outputMint, amount: input.amount,
|
|
378
|
+
slippageBps: input.slippageBps != null ? String(input.slippageBps) : undefined,
|
|
379
|
+
swapMode: input.swapMode,
|
|
380
|
+
onlyDirectRoutes: input.onlyDirectRoutes ? 'true' : undefined,
|
|
381
|
+
restrictIntermediateTokens: input.restrictIntermediateTokens ? 'true' : undefined,
|
|
382
|
+
platformFeeBps: input.platformFeeBps != null ? String(input.platformFeeBps) : undefined,
|
|
383
|
+
maxAccounts: input.maxAccounts != null ? String(input.maxAccounts) : undefined,
|
|
384
|
+
});
|
|
385
|
+
return success(result, 'magicblock_swapQuote');
|
|
386
|
+
}
|
|
387
|
+
catch (e) {
|
|
388
|
+
return handleError('magicblock_swapQuote', e);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
register('magicblock_swap', "Build an unsigned swap transaction from a quote. 'public' mode passes through Jupiter, 'private' mode routes output through a scheduled private transfer with delay and split. Price: $0.05.", schema({ userPublicKey: f.pubkey('Wallet that will sign the swap transaction'), quoteResponse: f.object('Quote response object from magicblock_swapQuote (pass as-is)', {}), visibility: f.enum("'public' = transparent Jupiter pass-through, 'private' = output routed through scheduled private transfer", ['public', 'private']), destination: f.pubkey("Final private-transfer recipient (required when visibility='private')"), minDelayMs: f.string("Private only. Earliest (ms) the queued transfer may settle"), maxDelayMs: f.string("Private only. Latest (ms) the queued transfer may settle (<= 600000)"), split: f.number('Private only. Number of queue entries to split across (1-14)'), clientRefId: f.string('Private only. Optional u64 client correlation ID'), validator: f.string('Optional validator pubkey for the transfer-queue PDA'), wrapAndUnwrapSol: f.boolean('Auto wrap/unwrap native SOL when needed (default true)'), asLegacyTransaction: f.boolean('Build a legacy transaction (not allowed when visibility=private, default false)') }, ['userPublicKey', 'quoteResponse']), async (raw) => {
|
|
392
|
+
try {
|
|
393
|
+
const input = parseInput(raw);
|
|
394
|
+
const result = await apiPost('/v1/swap/swap', {
|
|
395
|
+
userPublicKey: input.userPublicKey, quoteResponse: input.quoteResponse,
|
|
396
|
+
...stripNullish({
|
|
397
|
+
visibility: input.visibility, destination: input.destination,
|
|
398
|
+
minDelayMs: input.minDelayMs, maxDelayMs: input.maxDelayMs,
|
|
399
|
+
split: input.split, clientRefId: input.clientRefId,
|
|
400
|
+
validator: input.validator, wrapAndUnwrapSol: input.wrapAndUnwrapSol,
|
|
401
|
+
asLegacyTransaction: input.asLegacyTransaction,
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
return success(result, 'magicblock_swap');
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
return handleError('magicblock_swap', e);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
// ═══════════════════════════════════════════════════════════════
|
|
411
|
+
// Private Payment API — Mint Init (2 tools)
|
|
412
|
+
// ═══════════════════════════════════════════════════════════════
|
|
413
|
+
register('magicblock_initializeMint', 'Build an unsigned transaction that initializes a validator-scoped transfer queue for a mint. Price: $0.05.', schema({ owner: f.pubkey('Wallet pubkey that will sign the transaction'), mint: f.string('SPL mint to initialize a transfer queue for'), cluster: clusterField, validator: f.string('Optional ER validator pubkey') }, ['owner', 'mint']), async (raw) => {
|
|
414
|
+
try {
|
|
415
|
+
const input = parseInput(raw);
|
|
416
|
+
const result = await apiPost('/v1/spl/initialize-mint', {
|
|
417
|
+
owner: input.owner, mint: input.mint, ...stripNullish({ cluster: input.cluster, validator: input.validator }),
|
|
418
|
+
});
|
|
419
|
+
return success(result, 'magicblock_initializeMint');
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
return handleError('magicblock_initializeMint', e);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
register('magicblock_isMintInitialized', 'Check whether a mint has a validator-scoped transfer queue on the ephemeral RPC. Price: $0.01.', schema({ mint: f.string('SPL mint to check'), cluster: clusterField, validator: f.string('Optional ER validator pubkey') }, ['mint']), async (raw) => {
|
|
426
|
+
try {
|
|
427
|
+
const input = parseInput(raw);
|
|
428
|
+
const result = await apiGet('/v1/spl/is-mint-initialized', {
|
|
429
|
+
mint: input.mint, cluster: input.cluster ?? undefined, validator: input.validator ?? undefined,
|
|
430
|
+
});
|
|
431
|
+
return success(result, 'magicblock_isMintInitialized');
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
return handleError('magicblock_isMintInitialized', e);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
// ═══════════════════════════════════════════════════════════════
|
|
438
|
+
// VRF (2 tools — on-chain via @solana/web3.js)
|
|
439
|
+
// ═══════════════════════════════════════════════════════════════
|
|
440
|
+
register('magicblock_requestRandomness', 'Request provably fair on-chain randomness from the MagicBlock VRF oracle (Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz). Builds an unsigned transaction that invokes request_randomness on the VRF program. The caller must sign with sap_sign_transaction and submit to Solana. The oracle queue defaults to the base-layer queue (Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh); set ephemeral=true to use the ER queue for delegated programs. Price: $0.05.', schema({
|
|
441
|
+
payer: f.pubkey('Wallet pubkey that will pay for the request and sign the transaction'),
|
|
442
|
+
callbackProgramId: f.pubkey('Program ID of the callback program (the program that will consume the randomness)'),
|
|
443
|
+
callbackDiscriminator: f.string('Base58 or hex discriminator for the callback instruction in your program'),
|
|
444
|
+
callerSeed: f.string('Seed string for the VRF request — committed before randomness is produced. The seed is hashed to 32 bytes.'),
|
|
445
|
+
callbackAccounts: f.array('Accounts to pass to the callback instruction', f.object('Callback account metadata', { pubkey: f.pubkey('Account pubkey'), isSigner: f.boolean('Whether the account is a signer'), isWritable: f.boolean('Whether the account is writable') })),
|
|
446
|
+
ephemeral: f.boolean('Use the Ephemeral Rollup oracle queue instead of the base-layer queue (default false)'),
|
|
447
|
+
endpoint: endpointField,
|
|
448
|
+
}, ['payer', 'callbackProgramId', 'callbackDiscriminator', 'callerSeed', 'callbackAccounts']), async (raw) => {
|
|
449
|
+
try {
|
|
450
|
+
const input = parseInput(raw);
|
|
451
|
+
const payer = new PublicKey(input.payer);
|
|
452
|
+
const callbackProgramId = new PublicKey(input.callbackProgramId);
|
|
453
|
+
const oracleQueue = input.ephemeral ? DEFAULT_EPHEMERAL_QUEUE : DEFAULT_QUEUE;
|
|
454
|
+
const callerSeed = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input.callerSeed)));
|
|
455
|
+
// Parse callback discriminator: accept hex (0x-prefixed) or base58.
|
|
456
|
+
let callbackDiscriminator;
|
|
457
|
+
if (input.callbackDiscriminator.startsWith('0x')) {
|
|
458
|
+
callbackDiscriminator = Array.from(Buffer.from(input.callbackDiscriminator.slice(2), 'hex'));
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
const bs58 = await import('bs58');
|
|
462
|
+
callbackDiscriminator = Array.from(bs58.default.decode(input.callbackDiscriminator));
|
|
463
|
+
}
|
|
464
|
+
// Build callback account metas
|
|
465
|
+
const accountMetas = input.callbackAccounts.map((acc) => ({
|
|
466
|
+
pubkey: new PublicKey(acc.pubkey),
|
|
467
|
+
isSigner: acc.isSigner ?? false,
|
|
468
|
+
isWritable: acc.isWritable ?? false,
|
|
469
|
+
}));
|
|
470
|
+
// Derive the request PDA
|
|
471
|
+
const [requestKey] = PublicKey.findProgramAddressSync([Buffer.from('vrf_request'), payer.toBuffer(), oracleQueue.toBuffer(), callerSeed], VRF_PROGRAM_ID);
|
|
472
|
+
// Build the request_randomness instruction data
|
|
473
|
+
const data = Buffer.alloc(1024);
|
|
474
|
+
let offset = 0;
|
|
475
|
+
// Anchor instruction discriminator (first 8 bytes of sha256("global:request_randomness"))
|
|
476
|
+
const discHash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('global:request_randomness'));
|
|
477
|
+
const discBytes = new Uint8Array(discHash).slice(0, 8);
|
|
478
|
+
for (let i = 0; i < 8; i++)
|
|
479
|
+
data.writeUInt8(discBytes[i], offset++);
|
|
480
|
+
oracleQueue.toBuffer().copy(data, offset);
|
|
481
|
+
offset += 32;
|
|
482
|
+
callbackProgramId.toBuffer().copy(data, offset);
|
|
483
|
+
offset += 32;
|
|
484
|
+
data.writeUInt32LE(callbackDiscriminator.length, offset);
|
|
485
|
+
offset += 4;
|
|
486
|
+
Buffer.from(callbackDiscriminator).copy(data, offset);
|
|
487
|
+
offset += callbackDiscriminator.length;
|
|
488
|
+
Buffer.from(callerSeed).copy(data, offset);
|
|
489
|
+
offset += 32;
|
|
490
|
+
data.writeUInt32LE(accountMetas.length, offset);
|
|
491
|
+
offset += 4;
|
|
492
|
+
for (const meta of accountMetas) {
|
|
493
|
+
meta.pubkey.toBuffer().copy(data, offset);
|
|
494
|
+
offset += 32;
|
|
495
|
+
data.writeUInt8(meta.isSigner ? 1 : 0, offset++);
|
|
496
|
+
data.writeUInt8(meta.isWritable ? 1 : 0, offset++);
|
|
497
|
+
}
|
|
498
|
+
const ix = new TransactionInstruction({
|
|
499
|
+
programId: VRF_PROGRAM_ID,
|
|
500
|
+
keys: [
|
|
501
|
+
{ pubkey: payer, isSigner: true, isWritable: true },
|
|
502
|
+
{ pubkey: oracleQueue, isSigner: false, isWritable: true },
|
|
503
|
+
{ pubkey: requestKey, isSigner: false, isWritable: true },
|
|
504
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
505
|
+
{ pubkey: VRF_PROGRAM_IDENTITY, isSigner: false, isWritable: false },
|
|
506
|
+
],
|
|
507
|
+
data: data.subarray(0, offset),
|
|
508
|
+
});
|
|
509
|
+
const tx = new Transaction();
|
|
510
|
+
tx.add(ix);
|
|
511
|
+
const serializedTx = Buffer.from(tx.serialize({ requireAllSignatures: false })).toString('base64');
|
|
512
|
+
const result = {
|
|
513
|
+
requestKey: requestKey.toBase58(),
|
|
514
|
+
vrfProgramId: VRF_PROGRAM_ID.toBase58(),
|
|
515
|
+
oracleQueue: oracleQueue.toBase58(),
|
|
516
|
+
transactionBase64: serializedTx,
|
|
517
|
+
sendTo: 'base',
|
|
518
|
+
callbackProgramId: callbackProgramId.toBase58(),
|
|
519
|
+
callerSeedHash: Buffer.from(callerSeed).toString('hex'),
|
|
520
|
+
note: 'Sign with sap_sign_transaction, then submit with sap_submit_signed_transaction. After submission, poll magicblock_getRandomnessResult with the requestKey until fulfilled=true.',
|
|
521
|
+
};
|
|
522
|
+
return success(result, 'magicblock_requestRandomness');
|
|
523
|
+
}
|
|
524
|
+
catch (e) {
|
|
525
|
+
return handleError('magicblock_requestRandomness', e);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
register('magicblock_getRandomnessResult', 'Check whether a VRF request has been fulfilled by reading the RandomnessRequest account on-chain. Returns fulfilled status, random bytes (if available), and the request metadata. Price: $0.01.', schema({ requestKey: f.pubkey('VRF request PDA key from magicblock_requestRandomness'), endpoint: endpointField }, ['requestKey']), async (raw) => {
|
|
529
|
+
try {
|
|
530
|
+
const { requestKey: requestKeyStr } = parseInput(raw);
|
|
531
|
+
const requestKey = new PublicKey(requestKeyStr);
|
|
532
|
+
const accountInfo = await context.connection.getAccountInfo(requestKey, 'confirmed');
|
|
533
|
+
if (!accountInfo) {
|
|
534
|
+
const result = {
|
|
535
|
+
fulfilled: false, requestKey: requestKey.toBase58(),
|
|
536
|
+
randomness: null, randomnessHex: null, callerSeed: '',
|
|
537
|
+
owner: '', lamports: 0, executable: false, rentEpoch: 0, dataLength: 0,
|
|
538
|
+
message: 'Request account not found. The request may not have been submitted yet, or the transaction has not been confirmed.',
|
|
539
|
+
};
|
|
540
|
+
return success(result, 'magicblock_getRandomnessResult');
|
|
541
|
+
}
|
|
542
|
+
const data = accountInfo.data;
|
|
543
|
+
if (data.length < 73) {
|
|
544
|
+
const result = {
|
|
545
|
+
fulfilled: false, requestKey: requestKey.toBase58(),
|
|
546
|
+
randomness: null, randomnessHex: null, callerSeed: '',
|
|
547
|
+
owner: accountInfo.owner.toBase58(), lamports: accountInfo.lamports,
|
|
548
|
+
executable: accountInfo.executable, rentEpoch: accountInfo.rentEpoch, dataLength: data.length,
|
|
549
|
+
message: 'Account data too short — request may be in an unexpected state.',
|
|
550
|
+
};
|
|
551
|
+
return success(result, 'magicblock_getRandomnessResult');
|
|
552
|
+
}
|
|
553
|
+
// Layout: 8-byte discriminator + 32-byte caller_seed + 32-byte randomness
|
|
554
|
+
const discriminatorEnd = 8;
|
|
555
|
+
const callerSeed = data.subarray(discriminatorEnd, discriminatorEnd + 32);
|
|
556
|
+
const randomness = data.subarray(discriminatorEnd + 32, discriminatorEnd + 64);
|
|
557
|
+
const isZero = randomness.every((byte) => byte === 0);
|
|
558
|
+
const fulfilled = !isZero;
|
|
559
|
+
const result = {
|
|
560
|
+
fulfilled, requestKey: requestKey.toBase58(),
|
|
561
|
+
randomness: fulfilled ? Array.from(randomness) : null,
|
|
562
|
+
randomnessHex: fulfilled ? Buffer.from(randomness).toString('hex') : null,
|
|
563
|
+
callerSeed: Buffer.from(callerSeed).toString('hex'),
|
|
564
|
+
owner: accountInfo.owner.toBase58(), lamports: accountInfo.lamports,
|
|
565
|
+
executable: accountInfo.executable, rentEpoch: accountInfo.rentEpoch, dataLength: data.length,
|
|
566
|
+
message: fulfilled
|
|
567
|
+
? 'Randomness has been produced and verified. Use the random bytes in your callback logic.'
|
|
568
|
+
: 'Request submitted but not yet fulfilled. Poll again in a few seconds.',
|
|
569
|
+
};
|
|
570
|
+
return success(result, 'magicblock_getRandomnessResult');
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
return handleError('magicblock_getRandomnessResult', e);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
logger.debug('MagicBlock tools registered', { count: registered });
|
|
577
|
+
}
|
|
578
|
+
//# sourceMappingURL=magicblock-tools.js.map
|