@agenticstechnologies/verify 0.0.0-stage → 1.0.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/LICENSE +17 -0
- package/README.md +152 -2
- package/bin/verify.mjs +133 -0
- package/package.json +32 -4
- package/src/bundle.mjs +364 -0
- package/src/cleared.d.ts +11 -0
- package/src/cleared.mjs +120 -0
- package/src/evidence.mjs +112 -0
- package/src/index.d.ts +32 -0
- package/src/index.mjs +8 -0
- package/src/merkle.mjs +150 -0
- package/src/record.mjs +440 -0
- package/src/solana.mjs +235 -0
package/src/solana.mjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Public Solana memo read. Devnet and testnet only. No key, no signature,
|
|
2
|
+
// no mainnet RPC. A timeout or an unreachable RPC is a warning, not a hang
|
|
3
|
+
// and not a failed bundle.
|
|
4
|
+
|
|
5
|
+
export const MEMO_PROGRAM = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
6
|
+
|
|
7
|
+
export const PUBLIC_RPC = {
|
|
8
|
+
devnet: "https://api.devnet.solana.com",
|
|
9
|
+
testnet: "https://api.testnet.solana.com",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Free public Base Sepolia RPC. Testnet only. Never a mainnet endpoint. */
|
|
13
|
+
export const PUBLIC_EVM_RPC = {
|
|
14
|
+
baseSepolia: "https://sepolia.base.org",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_RPC_TIMEOUT_MS = 8000;
|
|
18
|
+
|
|
19
|
+
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
20
|
+
|
|
21
|
+
/** @param {string} input */
|
|
22
|
+
export function base58Decode(input) {
|
|
23
|
+
if (typeof input !== "string" || input.length === 0) return null;
|
|
24
|
+
const bytes = [];
|
|
25
|
+
for (const char of input) {
|
|
26
|
+
let carry = B58.indexOf(char);
|
|
27
|
+
if (carry < 0) return null;
|
|
28
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
29
|
+
carry += bytes[j] * 58;
|
|
30
|
+
bytes[j] = carry & 0xff;
|
|
31
|
+
carry >>= 8;
|
|
32
|
+
}
|
|
33
|
+
while (carry > 0) {
|
|
34
|
+
bytes.push(carry & 0xff);
|
|
35
|
+
carry >>= 8;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const char of input) {
|
|
39
|
+
if (char !== "1") break;
|
|
40
|
+
bytes.push(0);
|
|
41
|
+
}
|
|
42
|
+
return Uint8Array.from(bytes.reverse());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string | null | undefined} hint anchor.chain or anchor.cluster
|
|
47
|
+
* @param {string | null | undefined} explorer
|
|
48
|
+
* @returns {"devnet" | "testnet" | "mainnet" | null}
|
|
49
|
+
*/
|
|
50
|
+
export function classifyCluster(hint, explorer) {
|
|
51
|
+
const blob = `${hint || ""} ${explorer || ""}`.toLowerCase();
|
|
52
|
+
if (!blob.trim()) return null;
|
|
53
|
+
if (blob.includes("devnet")) return "devnet";
|
|
54
|
+
if (blob.includes("testnet")) return "testnet";
|
|
55
|
+
if (blob.includes("mainnet") || /(^|[^a-z])solana([^a-z]|$)/.test(blob)) return "mainnet";
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function memoBytes(data) {
|
|
60
|
+
if (typeof data === "string") return base58Decode(data);
|
|
61
|
+
if (data instanceof Uint8Array) return data;
|
|
62
|
+
if (Array.isArray(data) && data.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)) {
|
|
63
|
+
return Uint8Array.from(data);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Signers on the transaction message. jsonParsed keys carry `signer`. */
|
|
69
|
+
export function signersFromTransaction(tx) {
|
|
70
|
+
const msg = tx && tx.transaction && tx.transaction.message;
|
|
71
|
+
if (!msg) return [];
|
|
72
|
+
const keys = msg.accountKeys || msg.staticAccountKeys || [];
|
|
73
|
+
const flagged = [];
|
|
74
|
+
for (const key of keys) {
|
|
75
|
+
if (key && typeof key === "object" && key.signer === true && key.pubkey) {
|
|
76
|
+
flagged.push(String(key.pubkey));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (flagged.length) return flagged;
|
|
80
|
+
const count = msg.header && Number(msg.header.numRequiredSignatures);
|
|
81
|
+
if (!Number.isInteger(count) || count <= 0) return [];
|
|
82
|
+
const signers = [];
|
|
83
|
+
for (let i = 0; i < count && i < keys.length; i++) {
|
|
84
|
+
const key = keys[i];
|
|
85
|
+
const pubkey = typeof key === "string" ? key : key && key.pubkey;
|
|
86
|
+
if (pubkey) signers.push(String(pubkey));
|
|
87
|
+
}
|
|
88
|
+
return signers;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A memo counts only when the transaction succeeded and its one signer is
|
|
93
|
+
* `authority`. A non-null meta.err is `anchor_tx_failed`. Any other signer
|
|
94
|
+
* is `anchor_wrong_signer`.
|
|
95
|
+
* @param {unknown} tx
|
|
96
|
+
* @param {string} authority
|
|
97
|
+
* @returns {{ ok: true, memos: string[] } | { ok: false, reason: "anchor_tx_failed" | "anchor_wrong_signer" }}
|
|
98
|
+
*/
|
|
99
|
+
export function anchorMemoVerdict(tx, authority) {
|
|
100
|
+
const err = tx && tx.meta ? tx.meta.err : undefined;
|
|
101
|
+
if (err != null) return { ok: false, reason: "anchor_tx_failed" };
|
|
102
|
+
const expected = String(authority || "").trim();
|
|
103
|
+
const signers = signersFromTransaction(tx);
|
|
104
|
+
if (!expected || signers.length !== 1 || signers[0] !== expected) {
|
|
105
|
+
return { ok: false, reason: "anchor_wrong_signer" };
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, memos: memosFromTransaction(tx) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Memo strings on the transaction message. Inner instructions are ignored. */
|
|
111
|
+
export function memosFromTransaction(tx) {
|
|
112
|
+
const msg = tx && tx.transaction && tx.transaction.message;
|
|
113
|
+
if (!msg) return [];
|
|
114
|
+
const instructions = msg.instructions || msg.compiledInstructions || [];
|
|
115
|
+
const keys = msg.accountKeys || msg.staticAccountKeys || [];
|
|
116
|
+
const memos = [];
|
|
117
|
+
for (const ix of instructions) {
|
|
118
|
+
if (!ix || typeof ix !== "object") continue;
|
|
119
|
+
const parsedMemo = typeof ix.parsed === "string"
|
|
120
|
+
? ix.parsed
|
|
121
|
+
: (ix.parsed && typeof ix.parsed.memo === "string" ? ix.parsed.memo : null);
|
|
122
|
+
if (ix.programId === MEMO_PROGRAM || ix.program === "spl-memo") {
|
|
123
|
+
if (parsedMemo != null) memos.push(parsedMemo);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (ix.programIdIndex == null) continue;
|
|
127
|
+
const key = keys[ix.programIdIndex];
|
|
128
|
+
const pubkey = typeof key === "string" ? key : key && key.pubkey;
|
|
129
|
+
if (pubkey !== MEMO_PROGRAM) continue;
|
|
130
|
+
if (parsedMemo != null) {
|
|
131
|
+
memos.push(parsedMemo);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const bytes = memoBytes(ix.data);
|
|
135
|
+
if (!bytes) continue;
|
|
136
|
+
memos.push(new TextDecoder().decode(bytes));
|
|
137
|
+
}
|
|
138
|
+
return memos;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function unreachableKind(err) {
|
|
142
|
+
if (!err) return "unreachable";
|
|
143
|
+
if (err.code === "timeout" || err.name === "AbortError" || err.name === "TimeoutError") return "timeout";
|
|
144
|
+
const msg = String(err.message || err);
|
|
145
|
+
if (/abort|timeout|timed out/i.test(msg)) return "timeout";
|
|
146
|
+
return "unreachable";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {{ txid: string, cluster: "devnet" | "testnet", expected: string, timeoutMs?: number, fetchImpl?: typeof fetch }} opts
|
|
151
|
+
* @returns {Promise<{ ok: boolean, warn: boolean, reason: string | null }>}
|
|
152
|
+
*/
|
|
153
|
+
export async function checkSolanaMemo(opts) {
|
|
154
|
+
const rpc = PUBLIC_RPC[opts.cluster];
|
|
155
|
+
if (!rpc) {
|
|
156
|
+
return { ok: false, warn: true, reason: `cluster ${opts.cluster} is not queried` };
|
|
157
|
+
}
|
|
158
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
|
|
159
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
160
|
+
const ctrl = new AbortController();
|
|
161
|
+
let timer;
|
|
162
|
+
const timedOut = new Promise((_, reject) => {
|
|
163
|
+
timer = setTimeout(() => {
|
|
164
|
+
ctrl.abort();
|
|
165
|
+
const err = new Error("solana rpc timed out");
|
|
166
|
+
err.code = "timeout";
|
|
167
|
+
reject(err);
|
|
168
|
+
}, timeoutMs);
|
|
169
|
+
});
|
|
170
|
+
let pending;
|
|
171
|
+
try {
|
|
172
|
+
pending = Promise.resolve().then(() => fetchImpl(rpc, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
jsonrpc: "2.0",
|
|
177
|
+
id: 1,
|
|
178
|
+
method: "getTransaction",
|
|
179
|
+
params: [opts.txid, {
|
|
180
|
+
encoding: "jsonParsed",
|
|
181
|
+
maxSupportedTransactionVersion: 0,
|
|
182
|
+
commitment: "confirmed",
|
|
183
|
+
}],
|
|
184
|
+
}),
|
|
185
|
+
signal: ctrl.signal,
|
|
186
|
+
})).then((res) => res, (err) => {
|
|
187
|
+
if (err && (err.name === "AbortError" || err.name === "TimeoutError")) {
|
|
188
|
+
const wrapped = new Error("solana rpc timed out");
|
|
189
|
+
wrapped.code = "timeout";
|
|
190
|
+
throw wrapped;
|
|
191
|
+
}
|
|
192
|
+
throw err;
|
|
193
|
+
});
|
|
194
|
+
pending.catch(() => {});
|
|
195
|
+
const res = await Promise.race([pending, timedOut]);
|
|
196
|
+
if (!res || !res.ok) {
|
|
197
|
+
const status = res && res.status ? res.status : "no response";
|
|
198
|
+
return { ok: false, warn: true, reason: `solana rpc unreachable (http ${status})` };
|
|
199
|
+
}
|
|
200
|
+
const body = await res.json();
|
|
201
|
+
if (body && body.error) {
|
|
202
|
+
const msg = body.error.message || "rpc error";
|
|
203
|
+
return { ok: false, warn: true, reason: `solana rpc unreachable (${msg})` };
|
|
204
|
+
}
|
|
205
|
+
if (!body || body.result == null) {
|
|
206
|
+
return { ok: false, warn: false, reason: `transaction not found on ${opts.cluster}` };
|
|
207
|
+
}
|
|
208
|
+
if (opts.authority) {
|
|
209
|
+
const verdict = anchorMemoVerdict(body.result, opts.authority);
|
|
210
|
+
if (!verdict.ok) return { ok: false, warn: false, reason: verdict.reason };
|
|
211
|
+
if (verdict.memos.includes(opts.expected)) return { ok: true, warn: false, reason: null };
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
warn: false,
|
|
215
|
+
reason: `solana memo does not equal ${opts.expected}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (body.result && body.result.meta && body.result.meta.err != null) {
|
|
219
|
+
return { ok: false, warn: false, reason: "anchor_tx_failed" };
|
|
220
|
+
}
|
|
221
|
+
const memos = memosFromTransaction(body.result);
|
|
222
|
+
if (memos.includes(opts.expected)) return { ok: true, warn: false, reason: null };
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
warn: false,
|
|
226
|
+
reason: `solana memo does not equal ${opts.expected}`,
|
|
227
|
+
};
|
|
228
|
+
} catch (err) {
|
|
229
|
+
const kind = unreachableKind(err);
|
|
230
|
+
const why = kind === "timeout" ? "solana rpc timed out" : "solana rpc unreachable";
|
|
231
|
+
return { ok: false, warn: true, reason: why };
|
|
232
|
+
} finally {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
}
|
|
235
|
+
}
|