@molpha/mcp 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/dist/cli/doctor.js +27 -0
  4. package/dist/cli/provision.js +84 -0
  5. package/dist/src/apiconfig.js +47 -0
  6. package/dist/src/artifacts.js +80 -0
  7. package/dist/src/clients.js +51 -0
  8. package/dist/src/config.js +102 -0
  9. package/dist/src/determinism.js +31 -0
  10. package/dist/src/env.js +14 -0
  11. package/dist/src/errors.js +104 -0
  12. package/dist/src/feed.js +53 -0
  13. package/dist/src/guardrails.js +62 -0
  14. package/dist/src/hex.js +45 -0
  15. package/dist/src/mcp.js +82 -0
  16. package/dist/src/sdk.js +12 -0
  17. package/dist/src/server.js +17 -0
  18. package/dist/src/setup-validation.js +340 -0
  19. package/dist/src/signer/backends/memory.js +41 -0
  20. package/dist/src/signer/backends/privy.js +48 -0
  21. package/dist/src/signer/backends/turnkey.js +53 -0
  22. package/dist/src/signer/factory.js +37 -0
  23. package/dist/src/signer/types.js +1 -0
  24. package/dist/src/solana-address.js +29 -0
  25. package/dist/src/solana-compat.js +22 -0
  26. package/dist/src/submit.js +46 -0
  27. package/dist/src/subscription.js +48 -0
  28. package/dist/src/tools/agent_status.js +26 -0
  29. package/dist/src/tools/derive_feed.js +39 -0
  30. package/dist/src/tools/describe_feed.js +44 -0
  31. package/dist/src/tools/execute.js +60 -0
  32. package/dist/src/tools/fetch_verified.js +116 -0
  33. package/dist/src/tools/get_capabilities.js +45 -0
  34. package/dist/src/tools/get_latest.js +22 -0
  35. package/dist/src/tools/index.js +19 -0
  36. package/dist/src/tools/schemas.js +9 -0
  37. package/dist/src/tools/types.js +1 -0
  38. package/dist/src/tools/verify.js +28 -0
  39. package/dist/src/verifiers.js +95 -0
  40. package/dist/src/x402.js +552 -0
  41. package/package.json +81 -0
@@ -0,0 +1,340 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { createSolanaRpc } from "@solana/kit";
4
+ import { formatUsdcAtomic, isInlineKeypair, loadConfig, loadKeypair, resolveEnvString, resolvePath } from "./config.js";
5
+ import { createSigner } from "./signer/factory.js";
6
+ import { validateSolanaPubkey } from "./solana-address.js";
7
+ export function validateSignerEnv(env = process.env) {
8
+ const backend = resolveEnvString(env.SIGNER_BACKEND) ?? "memory";
9
+ const checks = [
10
+ {
11
+ name: "signer_backend",
12
+ ok: backend === "memory" || backend === "keychain",
13
+ message: backend === "memory" || backend === "keychain"
14
+ ? `SIGNER_BACKEND=${backend}`
15
+ : `unsupported SIGNER_BACKEND="${backend}" (expected memory or keychain)`
16
+ }
17
+ ];
18
+ if (backend === "memory") {
19
+ const ownerKeypair = resolveEnvString(env.OWNER_KEYPAIR ?? env.AGENT_KEYPAIR);
20
+ if (!ownerKeypair?.trim()) {
21
+ checks.push({
22
+ name: "owner_keypair",
23
+ ok: false,
24
+ message: "OWNER_KEYPAIR is required for SIGNER_BACKEND=memory"
25
+ });
26
+ return appendOptionalPubkeyChecks(checks, env);
27
+ }
28
+ if (isInlineKeypair(ownerKeypair)) {
29
+ try {
30
+ loadKeypair(ownerKeypair);
31
+ checks.push({
32
+ name: "owner_keypair",
33
+ ok: true,
34
+ message: "OWNER_KEYPAIR=<inline-json-keypair>"
35
+ });
36
+ }
37
+ catch (error) {
38
+ checks.push({
39
+ name: "owner_keypair",
40
+ ok: false,
41
+ message: error instanceof Error
42
+ ? `OWNER_KEYPAIR inline JSON is invalid: ${error.message}`
43
+ : "OWNER_KEYPAIR inline JSON is invalid"
44
+ });
45
+ }
46
+ return appendOptionalPubkeyChecks(checks, env);
47
+ }
48
+ const resolved = resolveKeypairPath(ownerKeypair);
49
+ if (!existsSync(resolved)) {
50
+ checks.push({
51
+ name: "owner_keypair",
52
+ ok: false,
53
+ message: `OWNER_KEYPAIR file not found: ${resolved}`
54
+ });
55
+ return appendOptionalPubkeyChecks(checks, env);
56
+ }
57
+ checks.push({
58
+ name: "owner_keypair",
59
+ ok: true,
60
+ message: `OWNER_KEYPAIR=${resolved}`
61
+ });
62
+ return appendOptionalPubkeyChecks(checks, env);
63
+ }
64
+ const provider = resolveEnvString(env.KEYCHAIN_BACKEND);
65
+ if (provider !== "privy" && provider !== "turnkey") {
66
+ checks.push({
67
+ name: "keychain_backend",
68
+ ok: false,
69
+ message: `KEYCHAIN_BACKEND must be privy or turnkey for SIGNER_BACKEND=keychain (got "${provider ?? ""}")`
70
+ });
71
+ return checks;
72
+ }
73
+ checks.push({
74
+ name: "keychain_backend",
75
+ ok: true,
76
+ message: `KEYCHAIN_BACKEND=${provider}`
77
+ });
78
+ const required = provider === "privy"
79
+ ? ["PRIVY_APP_ID", "PRIVY_APP_SECRET", "PRIVY_WALLET_ID", "PRIVY_WALLET_ADDRESS"]
80
+ : [
81
+ "TURNKEY_API_PUBLIC_KEY",
82
+ "TURNKEY_API_PRIVATE_KEY",
83
+ "TURNKEY_ORGANIZATION_ID",
84
+ "TURNKEY_WALLET_ADDRESS"
85
+ ];
86
+ const walletAddressVar = provider === "privy" ? "PRIVY_WALLET_ADDRESS" : "TURNKEY_WALLET_ADDRESS";
87
+ for (const name of required) {
88
+ const value = resolveEnvString(env[name]);
89
+ if (name === walletAddressVar) {
90
+ if (!value?.trim()) {
91
+ checks.push({
92
+ name: name.toLowerCase(),
93
+ ok: false,
94
+ message: `${name} is required for ${provider}`
95
+ });
96
+ continue;
97
+ }
98
+ const validation = validateSolanaPubkey(value, name);
99
+ checks.push({
100
+ name: name.toLowerCase(),
101
+ ok: validation.ok,
102
+ message: validation.ok ? `${name} is a valid Solana address` : validation.message
103
+ });
104
+ continue;
105
+ }
106
+ checks.push({
107
+ name: name.toLowerCase(),
108
+ ok: Boolean(value && value.trim().length > 0),
109
+ message: value && value.trim().length > 0 ? `${name} is set` : `${name} is required for ${provider}`
110
+ });
111
+ }
112
+ for (const [name, value] of [["MOLPHA_X402_GATEWAY_PDA", resolveEnvString(env.MOLPHA_X402_GATEWAY_PDA)]]) {
113
+ if (!value?.trim()) {
114
+ continue;
115
+ }
116
+ const validation = validateSolanaPubkey(value, name);
117
+ checks.push({
118
+ name: name.toLowerCase(),
119
+ ok: validation.ok,
120
+ message: validation.ok ? `${name} is a valid Solana address` : validation.message
121
+ });
122
+ }
123
+ return checks;
124
+ }
125
+ export async function checkSignerAvailability() {
126
+ try {
127
+ const signer = await createSigner(loadConfig());
128
+ const available = await signer.isAvailable();
129
+ return {
130
+ name: "signer",
131
+ ok: available,
132
+ message: available
133
+ ? `signer ready for ${signer.publicKey}`
134
+ : "signer backend is not available"
135
+ };
136
+ }
137
+ catch (error) {
138
+ return {
139
+ name: "signer",
140
+ ok: false,
141
+ message: error instanceof Error ? error.message : String(error)
142
+ };
143
+ }
144
+ }
145
+ function looksLikeSolanaRpc(endpoint) {
146
+ try {
147
+ const host = new URL(endpoint).hostname.toLowerCase();
148
+ return (host.includes("helius") ||
149
+ host.includes("quicknode") ||
150
+ host.includes("alchemy") ||
151
+ host.endsWith("solana.com") ||
152
+ host.includes("rpc."));
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
158
+ export async function checkGatewayEndpoints(endpoints) {
159
+ if (endpoints.length === 0) {
160
+ return {
161
+ name: "gateway_endpoints",
162
+ ok: false,
163
+ message: "no gateway endpoints configured"
164
+ };
165
+ }
166
+ const solanaRpcEndpoint = endpoints.find(looksLikeSolanaRpc);
167
+ if (solanaRpcEndpoint) {
168
+ return {
169
+ name: "gateway_endpoints",
170
+ ok: false,
171
+ message: `${solanaRpcEndpoint} looks like a Solana RPC URL — set GATEWAY_ENDPOINTS to a Molpha gateway (not SOLANA_RPC)`
172
+ };
173
+ }
174
+ let lastError = "no reachable gateway";
175
+ for (const endpoint of endpoints) {
176
+ const base = endpoint.replace(/\/$/, "");
177
+ try {
178
+ const nodesRes = await fetch(`${base}/v1/nodes`, { method: "GET", signal: AbortSignal.timeout(8_000) });
179
+ if (!nodesRes.ok) {
180
+ lastError = `GET /v1/nodes failed (${nodesRes.status}) at ${base}`;
181
+ continue;
182
+ }
183
+ const probeRes = await fetch(`${base}/v1/agent/execute`, {
184
+ method: "POST",
185
+ headers: { "content-type": "application/json" },
186
+ body: JSON.stringify({}),
187
+ signal: AbortSignal.timeout(8_000)
188
+ });
189
+ const probeText = (await probeRes.text()).trim();
190
+ if (probeText.includes('"jsonrpc"') && probeText.includes("Method not found")) {
191
+ lastError = `${base} returned a Solana JSON-RPC error for POST /v1/agent/execute — this is not a Molpha gateway`;
192
+ continue;
193
+ }
194
+ if (probeRes.status === 404 && probeText.includes("page not found")) {
195
+ lastError = `${base} serves /v1/nodes but not /v1/agent/execute (signing routes missing — use https://dev-gateway.molpha.io)`;
196
+ continue;
197
+ }
198
+ if (probeRes.status === 400 || probeRes.status === 401 || probeRes.status === 402) {
199
+ return {
200
+ name: "gateway_endpoints",
201
+ ok: true,
202
+ message: `${base} reachable (nodes + signing routes)`
203
+ };
204
+ }
205
+ lastError = `${base} unexpected POST /v1/agent/execute response (${probeRes.status})`;
206
+ }
207
+ catch (error) {
208
+ lastError = error instanceof Error ? `${base}: ${error.message}` : `${base}: unreachable`;
209
+ }
210
+ }
211
+ return {
212
+ name: "gateway_endpoints",
213
+ ok: false,
214
+ message: lastError
215
+ };
216
+ }
217
+ export async function checkSolanaRpc(rpc) {
218
+ try {
219
+ const version = await createSolanaRpc(rpc).getVersion().send();
220
+ return {
221
+ name: "solana_rpc",
222
+ ok: true,
223
+ message: `reachable (${version["solana-core"] ?? "ok"})`
224
+ };
225
+ }
226
+ catch (error) {
227
+ return {
228
+ name: "solana_rpc",
229
+ ok: false,
230
+ message: error instanceof Error ? error.message : String(error)
231
+ };
232
+ }
233
+ }
234
+ export function checkBuildArtifact(repoRoot = process.cwd()) {
235
+ const built = resolve(repoRoot, "dist/src/server.js");
236
+ return {
237
+ name: "build",
238
+ ok: existsSync(built),
239
+ message: existsSync(built)
240
+ ? `server entry found at ${built}`
241
+ : `missing ${built} — run npm run build`
242
+ };
243
+ }
244
+ export function resolveServerEntry(repoRoot = process.cwd()) {
245
+ return resolve(repoRoot, "dist/src/server.js");
246
+ }
247
+ export function buildMcpEnvBlock(env = process.env) {
248
+ const config = loadConfig(env);
249
+ const out = {
250
+ SOLANA_RPC: config.solanaRpc,
251
+ GATEWAY_ENDPOINTS: config.gatewayEndpoints.join(","),
252
+ MOLPHA_EVM_NETWORKS: config.evmNetworks.join(","),
253
+ MOLPHA_STARKNET_NETWORKS: config.starknetNetworks.join(","),
254
+ MOLPHA_MAX_EXECUTES_PER_DAY: String(config.guardrails.maxExecutesPerDay),
255
+ MOLPHA_X402_MAX_PRICE_USDC: formatUsdcAtomic(config.x402.maxPriceUsdcAtomic),
256
+ MOLPHA_X402_MAX_SPEND_PER_DAY_USDC: formatUsdcAtomic(config.x402.maxSpendPerDayUsdcAtomic)
257
+ };
258
+ if (config.x402.gatewayPda) {
259
+ out.MOLPHA_X402_GATEWAY_PDA = config.x402.gatewayPda;
260
+ }
261
+ const backend = resolveEnvString(env.SIGNER_BACKEND) ?? "memory";
262
+ if (backend === "memory") {
263
+ out.SIGNER_BACKEND = "memory";
264
+ const ownerKeypair = resolveEnvString(env.OWNER_KEYPAIR ?? env.AGENT_KEYPAIR);
265
+ if (ownerKeypair) {
266
+ out.OWNER_KEYPAIR = resolveKeypairPath(ownerKeypair);
267
+ }
268
+ }
269
+ else {
270
+ out.SIGNER_BACKEND = "keychain";
271
+ const provider = resolveEnvString(env.KEYCHAIN_BACKEND);
272
+ if (provider) {
273
+ out.KEYCHAIN_BACKEND = provider;
274
+ }
275
+ const passthrough = provider === "privy"
276
+ ? ["PRIVY_APP_ID", "PRIVY_APP_SECRET", "PRIVY_WALLET_ID", "PRIVY_WALLET_ADDRESS"]
277
+ : provider === "turnkey"
278
+ ? [
279
+ "TURNKEY_API_PUBLIC_KEY",
280
+ "TURNKEY_API_PRIVATE_KEY",
281
+ "TURNKEY_ORGANIZATION_ID",
282
+ "TURNKEY_WALLET_ADDRESS"
283
+ ]
284
+ : [];
285
+ for (const name of passthrough) {
286
+ const value = resolveEnvString(env[name]);
287
+ if (value) {
288
+ out[name] = value;
289
+ }
290
+ }
291
+ }
292
+ if (config.guardrails.dryRunDefault) {
293
+ out.MOLPHA_DRY_RUN = "true";
294
+ }
295
+ return out;
296
+ }
297
+ export function buildMcpJsonSnippet(repoRoot = process.cwd(), env = process.env) {
298
+ const serverPath = resolveServerEntry(repoRoot);
299
+ return JSON.stringify({
300
+ mcpServers: {
301
+ molpha: {
302
+ command: "node",
303
+ args: [serverPath],
304
+ env: buildMcpEnvBlock(env)
305
+ }
306
+ }
307
+ }, null, 2);
308
+ }
309
+ export function buildCodexTomlSnippet(repoRoot = process.cwd(), env = process.env) {
310
+ const serverPath = resolveServerEntry(repoRoot);
311
+ const envBlock = buildMcpEnvBlock(env);
312
+ const lines = [
313
+ "[mcp_servers.molpha]",
314
+ `command = "node"`,
315
+ `args = ["${serverPath}"]`,
316
+ "",
317
+ "[mcp_servers.molpha.env]"
318
+ ];
319
+ for (const [key, value] of Object.entries(envBlock)) {
320
+ lines.push(`${key} = "${value.replaceAll('"', '\\"')}"`);
321
+ }
322
+ return `${lines.join("\n")}\n`;
323
+ }
324
+ function appendOptionalPubkeyChecks(checks, env) {
325
+ for (const [name, value] of [["MOLPHA_X402_GATEWAY_PDA", resolveEnvString(env.MOLPHA_X402_GATEWAY_PDA)]]) {
326
+ if (!value?.trim()) {
327
+ continue;
328
+ }
329
+ const validation = validateSolanaPubkey(value, name);
330
+ checks.push({
331
+ name: name.toLowerCase(),
332
+ ok: validation.ok,
333
+ message: validation.ok ? `${name} is a valid Solana address` : validation.message
334
+ });
335
+ }
336
+ return checks;
337
+ }
338
+ function resolveKeypairPath(path) {
339
+ return isInlineKeypair(path) ? "<inline-json-keypair>" : resolvePath(path);
340
+ }
@@ -0,0 +1,41 @@
1
+ import { createKeyPairFromBytes, getAddressFromPublicKey, signBytes } from "@solana/kit";
2
+ import { Transaction, VersionedTransaction } from "@solana/web3.js";
3
+ import { toLegacyPublicKey } from "../../solana-compat.js";
4
+ export class MemorySigner {
5
+ publicKey;
6
+ keyPair;
7
+ constructor(keyPair, address) {
8
+ this.keyPair = keyPair;
9
+ this.publicKey = address;
10
+ }
11
+ static async fromSecretKey(secretKey) {
12
+ const keyPair = await createKeyPairFromBytes(secretKey);
13
+ const address = await getAddressFromPublicKey(keyPair.publicKey);
14
+ return new MemorySigner(keyPair, address);
15
+ }
16
+ async isAvailable() {
17
+ return true;
18
+ }
19
+ async signTransaction(tx) {
20
+ const messageBytes = tx instanceof VersionedTransaction ? tx.message.serialize() : tx.serializeMessage();
21
+ const signature = await signBytes(this.keyPair.privateKey, toExactUint8Array(messageBytes));
22
+ tx.addSignature(toLegacyPublicKey(this.publicKey), Buffer.from(signature));
23
+ return tx;
24
+ }
25
+ async signAllTransactions(txs) {
26
+ return Promise.all(txs.map((tx) => this.signTransaction(tx)));
27
+ }
28
+ async signMessage(message) {
29
+ return new Uint8Array(await signBytes(this.keyPair.privateKey, toExactUint8Array(message)));
30
+ }
31
+ }
32
+ /**
33
+ * Node's Buffer pooling means small buffers (e.g. `Transaction.serializeMessage()`,
34
+ * `Buffer.concat(...)`) are frequently views into a much larger shared ArrayBuffer.
35
+ * WebCrypto's `subtle.sign` reads the view's backing buffer rather than respecting
36
+ * its byteOffset/byteLength, which silently signs the wrong bytes. Always copy into
37
+ * a tightly-sized Uint8Array before signing.
38
+ */
39
+ function toExactUint8Array(bytes) {
40
+ return bytes.byteLength === bytes.buffer.byteLength ? bytes : Uint8Array.from(bytes);
41
+ }
@@ -0,0 +1,48 @@
1
+ import { createRequire } from "node:module";
2
+ import { Transaction, VersionedTransaction } from "@solana/web3.js";
3
+ import { parseSolanaPubkey } from "../../solana-address.js";
4
+ const require = createRequire(import.meta.url);
5
+ export class PrivySigner {
6
+ publicKey;
7
+ walletId;
8
+ wallets;
9
+ constructor(config) {
10
+ this.publicKey = parseSolanaPubkey(config.address, "PRIVY_WALLET_ADDRESS");
11
+ this.walletId = config.walletId;
12
+ let PrivyClient;
13
+ try {
14
+ ({ PrivyClient } = require("@privy-io/node"));
15
+ }
16
+ catch (error) {
17
+ throw new Error("@privy-io/node is not installed. Run `npm install @privy-io/node` to use SIGNER_BACKEND=keychain with KEYCHAIN_BACKEND=privy.", { cause: error });
18
+ }
19
+ const privy = new PrivyClient({ appId: config.appId, appSecret: config.appSecret });
20
+ this.wallets = privy.wallets();
21
+ }
22
+ async isAvailable() {
23
+ try {
24
+ await this.wallets.get(this.walletId);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ async signTransaction(tx) {
32
+ const transaction = tx instanceof VersionedTransaction
33
+ ? tx.serialize()
34
+ : tx.serialize({ requireAllSignatures: false, verifySignatures: false });
35
+ const { signed_transaction: signedTransaction } = await this.wallets.solana().signTransaction(this.walletId, {
36
+ transaction,
37
+ });
38
+ const decoded = Buffer.from(signedTransaction, "base64");
39
+ return (tx instanceof VersionedTransaction ? VersionedTransaction.deserialize(decoded) : Transaction.from(decoded));
40
+ }
41
+ async signAllTransactions(txs) {
42
+ return Promise.all(txs.map((tx) => this.signTransaction(tx)));
43
+ }
44
+ async signMessage(message) {
45
+ const { signature } = await this.wallets.solana().signMessage(this.walletId, { message });
46
+ return new Uint8Array(Buffer.from(signature, "base64"));
47
+ }
48
+ }
@@ -0,0 +1,53 @@
1
+ import { createRequire } from "node:module";
2
+ import { Transaction, VersionedTransaction } from "@solana/web3.js";
3
+ import { parseSolanaPubkey } from "../../solana-address.js";
4
+ const require = createRequire(import.meta.url);
5
+ export class TurnkeySigner {
6
+ publicKey;
7
+ address;
8
+ signer;
9
+ constructor(config) {
10
+ this.publicKey = parseSolanaPubkey(config.address, "TURNKEY_WALLET_ADDRESS");
11
+ this.address = config.address;
12
+ let TurnkeyClass;
13
+ let TurnkeySignerClass;
14
+ try {
15
+ ({ Turnkey: TurnkeyClass } = require("@turnkey/sdk-server"));
16
+ ({ TurnkeySigner: TurnkeySignerClass } = require("@turnkey/solana"));
17
+ }
18
+ catch (error) {
19
+ throw new Error("@turnkey/sdk-server and @turnkey/solana are not installed. Run `npm install @turnkey/sdk-server @turnkey/solana`.", { cause: error });
20
+ }
21
+ const client = new TurnkeyClass({
22
+ apiBaseUrl: "https://api.turnkey.com",
23
+ apiPublicKey: config.apiPublicKey,
24
+ apiPrivateKey: config.apiPrivateKey,
25
+ defaultOrganizationId: config.organizationId,
26
+ });
27
+ this.signer = new TurnkeySignerClass({
28
+ organizationId: config.organizationId,
29
+ client: client.apiClient(),
30
+ });
31
+ }
32
+ async isAvailable() {
33
+ try {
34
+ const signature = await this.signer.signMessage(new Uint8Array(1), this.address);
35
+ return signature instanceof Uint8Array && signature.length > 0;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ async signTransaction(tx) {
42
+ await this.signer.addSignature(tx, this.address);
43
+ return tx;
44
+ }
45
+ async signAllTransactions(txs) {
46
+ await Promise.all(txs.map((tx) => this.signer.addSignature(tx, this.address)));
47
+ return txs;
48
+ }
49
+ async signMessage(message) {
50
+ const signature = await this.signer.signMessage(message, this.address);
51
+ return signature instanceof Uint8Array ? signature : new Uint8Array(signature);
52
+ }
53
+ }
@@ -0,0 +1,37 @@
1
+ import { loadOwnerKeypair } from "../config.js";
2
+ import { MemorySigner } from "./backends/memory.js";
3
+ import { PrivySigner } from "./backends/privy.js";
4
+ import { TurnkeySigner } from "./backends/turnkey.js";
5
+ export async function createSigner(config) {
6
+ const backend = process.env["SIGNER_BACKEND"] ?? "memory";
7
+ if (backend === "keychain") {
8
+ return createKeychainSigner();
9
+ }
10
+ return MemorySigner.fromSecretKey(loadOwnerKeypair(config));
11
+ }
12
+ function createKeychainSigner() {
13
+ const provider = process.env["KEYCHAIN_BACKEND"];
14
+ if (provider === "privy") {
15
+ return new PrivySigner({
16
+ appId: requireEnv("PRIVY_APP_ID"),
17
+ appSecret: requireEnv("PRIVY_APP_SECRET"),
18
+ walletId: requireEnv("PRIVY_WALLET_ID"),
19
+ address: requireEnv("PRIVY_WALLET_ADDRESS"),
20
+ });
21
+ }
22
+ if (provider === "turnkey") {
23
+ return new TurnkeySigner({
24
+ apiPublicKey: requireEnv("TURNKEY_API_PUBLIC_KEY"),
25
+ apiPrivateKey: requireEnv("TURNKEY_API_PRIVATE_KEY"),
26
+ organizationId: requireEnv("TURNKEY_ORGANIZATION_ID"),
27
+ address: requireEnv("TURNKEY_WALLET_ADDRESS"),
28
+ });
29
+ }
30
+ throw new Error(`Unknown KEYCHAIN_BACKEND="${provider ?? ""}". Supported values: privy, turnkey`);
31
+ }
32
+ function requireEnv(name) {
33
+ const value = process.env[name];
34
+ if (!value)
35
+ throw new Error(`${name} is required for SIGNER_BACKEND=keychain`);
36
+ return value;
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { address } from "@solana/kit";
2
+ export function parseSolanaPubkey(value, envVar) {
3
+ const trimmed = value.trim();
4
+ if (!trimmed) {
5
+ throw new Error(`${envVar} is required`);
6
+ }
7
+ try {
8
+ return address(trimmed);
9
+ }
10
+ catch (error) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ throw new Error(`${envVar} must be a valid Solana address (base58 pubkey), got ${JSON.stringify(value)}: ${message}`, { cause: error });
13
+ }
14
+ }
15
+ export function validateSolanaPubkey(value, envVar) {
16
+ if (!value?.trim()) {
17
+ return { ok: false, message: `${envVar} is required` };
18
+ }
19
+ try {
20
+ parseSolanaPubkey(value, envVar);
21
+ return { ok: true };
22
+ }
23
+ catch (error) {
24
+ return {
25
+ ok: false,
26
+ message: error instanceof Error ? error.message : String(error)
27
+ };
28
+ }
29
+ }
@@ -0,0 +1,22 @@
1
+ import { isSignerRole, isWritableRole } from "@solana/kit";
2
+ import { PublicKey, TransactionInstruction } from "@solana/web3.js";
3
+ /**
4
+ * Isolated legacy-interop boundary: @molpha-oracle/sdk, @turnkey/solana, and
5
+ * connection.sendRawTransaction/confirmTransaction still require classic web3.js
6
+ * types. Everything else in this codebase works in terms of @solana/kit's
7
+ * `Address`/`Instruction`.
8
+ */
9
+ export function toLegacyPublicKey(address) {
10
+ return new PublicKey(address);
11
+ }
12
+ export function toLegacyInstruction(instruction) {
13
+ return new TransactionInstruction({
14
+ programId: toLegacyPublicKey(instruction.programAddress),
15
+ keys: (instruction.accounts ?? []).map((account) => ({
16
+ pubkey: toLegacyPublicKey(account.address),
17
+ isSigner: isSignerRole(account.role),
18
+ isWritable: isWritableRole(account.role)
19
+ })),
20
+ data: Buffer.from(instruction.data ?? new Uint8Array())
21
+ });
22
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The single Solana settle path, shared by `molpha_execute` and
3
+ * `molpha_fetch_verified`'s `autoSubmit` leg — one guardrail check, one shape
4
+ * normalization, one place that knows what `submit_data_update` requires.
5
+ */
6
+ import { toSignedResult } from "./artifacts.js";
7
+ import { getMolphaContext, requireMethod } from "./clients.js";
8
+ import { enforceExecuteCap, previewWrite } from "./guardrails.js";
9
+ /**
10
+ * Accepts the `molpha_fetch_verified` artifact or the flat signed result, and
11
+ * returns the flat shape `submitDataUpdate` expects.
12
+ */
13
+ export function prepareSignedResult(input) {
14
+ const result = toSignedResult(input);
15
+ if (!result.feedId) {
16
+ throw new Error("signed result is missing `feedId`");
17
+ }
18
+ // `buildSubmitArgs` packs `valuePacked` as the on-chain value; `value` is the
19
+ // decimal rendering and is not interchangeable with it.
20
+ if (!result.valuePacked) {
21
+ throw new Error("signed result is missing `valuePacked` (the on-chain encoding of `value`); re-run molpha_fetch_verified and pass its output through unmodified");
22
+ }
23
+ return result;
24
+ }
25
+ export function previewSubmit(action, result, submitter) {
26
+ return previewWrite(action, {
27
+ chain: "solana",
28
+ action: "submit_data_update",
29
+ feedId: result.feedId,
30
+ registryVersion: result.registryVersion,
31
+ submitter
32
+ });
33
+ }
34
+ /** Enforces the daily execute cap, then submits. Callers must pass a prepared result. */
35
+ export async function submitSignedResult(result) {
36
+ const { config, solana } = await getMolphaContext();
37
+ enforceExecuteCap(config.guardrails);
38
+ const submitDataUpdate = requireMethod(solana, "submitDataUpdate");
39
+ const tx = await submitDataUpdate(result);
40
+ return {
41
+ chain: "solana",
42
+ action: "submit_data_update",
43
+ feedId: String(result.feedId),
44
+ signature: tx.signature
45
+ };
46
+ }
@@ -0,0 +1,48 @@
1
+ import { requireMethod } from "./clients.js";
2
+ export async function readSubscriptionStatus(solana) {
3
+ const readSubscription = requireMethod(solana, "readSubscription");
4
+ try {
5
+ const subscription = await readSubscription();
6
+ if (!subscription) {
7
+ return {
8
+ active: false,
9
+ message: "No active subscription found. Run `npm run provision -- subscribe` (or molpha-provision bootstrap) with OWNER_KEYPAIR, or use payment: \"x402\" for a self-funded pay-per-request round."
10
+ };
11
+ }
12
+ const validUntil = BigInt(String(subscription.validUntil ?? 0));
13
+ const now = BigInt(Math.floor(Date.now() / 1000));
14
+ const usedRounds = BigInt(String(subscription.usedRounds ?? 0));
15
+ const maxRounds = BigInt(String(subscription.maxRounds ?? 0));
16
+ const active = validUntil > now && (maxRounds === 0n || usedRounds < maxRounds);
17
+ return {
18
+ active,
19
+ owner: subscription.owner?.toString?.() ?? String(subscription.owner ?? ""),
20
+ planType: subscription.planType,
21
+ prepaidUsdc: String(subscription.prepaidUsdc ?? ""),
22
+ price: String(subscription.price ?? ""),
23
+ validUntil: validUntil.toString(),
24
+ usedRounds: Number(usedRounds),
25
+ maxRounds: Number(maxRounds),
26
+ ...(active
27
+ ? {}
28
+ : {
29
+ message: validUntil <= now
30
+ ? "Subscription expired. Extend via the bootstrap CLI before requesting data."
31
+ : "Subscription round quota exhausted for this period. Extend via the bootstrap CLI, or use payment: \"x402\"."
32
+ })
33
+ };
34
+ }
35
+ catch (error) {
36
+ return {
37
+ active: false,
38
+ message: error instanceof Error ? error.message : String(error)
39
+ };
40
+ }
41
+ }
42
+ export async function assertActiveSubscription(solana) {
43
+ const status = await readSubscriptionStatus(solana);
44
+ if (!status.active) {
45
+ throw new Error(status.message ?? "Subscription is inactive or missing");
46
+ }
47
+ return status;
48
+ }