@swarm-tips/client 0.2.11 → 0.2.13
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/SBOM.spdx.json +5 -5
- package/dist/contracts/generated/shillbot.d.ts +9 -3
- package/dist/contracts/idl/shillbot.json +9 -3
- package/dist/coordination-game/solana.js +25 -3
- package/dist/http.js +4 -2
- package/dist/shillbot/api.js +13 -2
- package/dist/shillbot/index.js +71 -24
- package/dist/vow/decoders/shillbot.d.ts +2 -0
- package/dist/vow/decoders/shillbot.js +6 -4
- package/dist/vow/schema.js +5 -2
- package/dist/vow/types.d.ts +3 -0
- package/dist/vow/verify.d.ts +3 -2
- package/dist/vow/verify.js +14 -3
- package/package.json +1 -1
package/SBOM.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@swarm-tips/client-0.2.
|
|
6
|
-
"documentNamespace": "https://swarm.tips/sbom/client/0.2.
|
|
5
|
+
"name": "@swarm-tips/client-0.2.13",
|
|
6
|
+
"documentNamespace": "https://swarm.tips/sbom/client/0.2.13",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-
|
|
8
|
+
"created": "2026-11-06T04:27:18.192Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: sdk/client/scripts/generate-sbom.mjs",
|
|
11
11
|
"Organization: Swarm Tips"
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
{
|
|
16
16
|
"name": "@swarm-tips/client",
|
|
17
17
|
"SPDXID": "SPDXRef-Package",
|
|
18
|
-
"versionInfo": "0.2.
|
|
19
|
-
"downloadLocation": "https://www.npmjs.com/package/@swarm-tips/client/v/0.2.
|
|
18
|
+
"versionInfo": "0.2.13",
|
|
19
|
+
"downloadLocation": "https://www.npmjs.com/package/@swarm-tips/client/v/0.2.13",
|
|
20
20
|
"filesAnalyzed": false,
|
|
21
21
|
"licenseConcluded": "MIT",
|
|
22
22
|
"licenseDeclared": "MIT"
|
|
@@ -2114,9 +2114,10 @@ export type Shillbot = {
|
|
|
2114
2114
|
{
|
|
2115
2115
|
"name": "switchboardFeed";
|
|
2116
2116
|
"docs": [
|
|
2117
|
-
"The
|
|
2118
|
-
"`
|
|
2119
|
-
"
|
|
2117
|
+
"The oracle authority signer (`GlobalState.oracle_authority` or",
|
|
2118
|
+
"`GlobalState.authority`) or legacy Switchboard pull feed account.",
|
|
2119
|
+
"/ `global_state.authority`, or validated against `global_state.switchboard_feed`",
|
|
2120
|
+
"and parsed via `PullFeedAccountData::parse()`."
|
|
2120
2121
|
];
|
|
2121
2122
|
}
|
|
2122
2123
|
];
|
|
@@ -2888,6 +2889,11 @@ export type Shillbot = {
|
|
|
2888
2889
|
"code": 6056;
|
|
2889
2890
|
"name": "invalidAccountOwner";
|
|
2890
2891
|
"msg": "Account is not owned by this program";
|
|
2892
|
+
},
|
|
2893
|
+
{
|
|
2894
|
+
"code": 6057;
|
|
2895
|
+
"name": "invalidTimeoutOrdering";
|
|
2896
|
+
"msg": "verification_timeout must exceed attestation_delay + staleness_window";
|
|
2891
2897
|
}
|
|
2892
2898
|
];
|
|
2893
2899
|
"types": [
|
|
@@ -2108,9 +2108,10 @@
|
|
|
2108
2108
|
{
|
|
2109
2109
|
"name": "switchboard_feed",
|
|
2110
2110
|
"docs": [
|
|
2111
|
-
"The
|
|
2112
|
-
"`
|
|
2113
|
-
"
|
|
2111
|
+
"The oracle authority signer (`GlobalState.oracle_authority` or",
|
|
2112
|
+
"`GlobalState.authority`) or legacy Switchboard pull feed account.",
|
|
2113
|
+
"/ `global_state.authority`, or validated against `global_state.switchboard_feed`",
|
|
2114
|
+
"and parsed via `PullFeedAccountData::parse()`."
|
|
2114
2115
|
]
|
|
2115
2116
|
}
|
|
2116
2117
|
],
|
|
@@ -2882,6 +2883,11 @@
|
|
|
2882
2883
|
"code": 6056,
|
|
2883
2884
|
"name": "InvalidAccountOwner",
|
|
2884
2885
|
"msg": "Account is not owned by this program"
|
|
2886
|
+
},
|
|
2887
|
+
{
|
|
2888
|
+
"code": 6057,
|
|
2889
|
+
"name": "InvalidTimeoutOrdering",
|
|
2890
|
+
"msg": "verification_timeout must exceed attestation_delay + staleness_window"
|
|
2885
2891
|
}
|
|
2886
2892
|
],
|
|
2887
2893
|
"types": [
|
|
@@ -10,10 +10,18 @@ const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
10
10
|
const MEMO_PROGRAM_ID = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
|
|
11
11
|
export const SESSION_FEE_HEADROOM_LAMPORTS = 10_000_000;
|
|
12
12
|
export function sessionFundLamports(stakeLamports) {
|
|
13
|
-
|
|
13
|
+
const total = stakeLamports + BigInt(SESSION_FEE_HEADROOM_LAMPORTS);
|
|
14
|
+
if (total > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
15
|
+
throw new SwarmClientError({ code: "INVALID_ARGUMENT", operation: "solana.sessionFundLamports", message: "stakeLamports exceeds safe integer range" });
|
|
16
|
+
}
|
|
17
|
+
return Number(total);
|
|
14
18
|
}
|
|
15
19
|
export function sessionBalanceFloor(stakeLamports) {
|
|
16
|
-
|
|
20
|
+
const total = stakeLamports + BigInt(SESSION_FEE_HEADROOM_LAMPORTS / 2);
|
|
21
|
+
if (total > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
22
|
+
throw new SwarmClientError({ code: "INVALID_ARGUMENT", operation: "solana.sessionBalanceFloor", message: "stakeLamports exceeds safe integer range" });
|
|
23
|
+
}
|
|
24
|
+
return Number(total);
|
|
17
25
|
}
|
|
18
26
|
function walletForSession(session) {
|
|
19
27
|
return {
|
|
@@ -140,6 +148,13 @@ export class CoordinationGameSolanaClient {
|
|
|
140
148
|
for (let index = 0; index < attempts; index += 1) {
|
|
141
149
|
await sleep(options.intervalMs ?? 1_500);
|
|
142
150
|
const status = await this.connection.getSignatureStatus(signature);
|
|
151
|
+
if (status.value?.err) {
|
|
152
|
+
throw new SwarmClientError({
|
|
153
|
+
code: "TRANSACTION_FAILED",
|
|
154
|
+
operation: "solana.waitForSignature",
|
|
155
|
+
message: `transaction ${signature} failed on-chain: ${JSON.stringify(status.value.err)}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
143
158
|
const confirmation = status.value?.confirmationStatus;
|
|
144
159
|
if (confirmation === "confirmed" || confirmation === "finalized")
|
|
145
160
|
return;
|
|
@@ -250,7 +265,14 @@ export class CoordinationGameSolanaClient {
|
|
|
250
265
|
signed = await wallet.signTransaction(transaction);
|
|
251
266
|
}
|
|
252
267
|
const signature = await this.connection.sendRawTransaction(signed.serialize());
|
|
253
|
-
await this.connection.confirmTransaction(signature, this.commitment);
|
|
268
|
+
const confirmed = await this.connection.confirmTransaction(signature, this.commitment);
|
|
269
|
+
if (confirmed?.value?.err) {
|
|
270
|
+
throw new SwarmClientError({
|
|
271
|
+
code: "TRANSACTION_FAILED",
|
|
272
|
+
operation: "solana.signBroadcastAndConfirm",
|
|
273
|
+
message: `transaction ${signature} failed on-chain: ${JSON.stringify(confirmed.value.err)}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
254
276
|
return signature;
|
|
255
277
|
}
|
|
256
278
|
async commitGuess(commitment, game, session) {
|
package/dist/http.js
CHANGED
|
@@ -14,11 +14,13 @@ export class HttpClient {
|
|
|
14
14
|
operation: "http.constructor",
|
|
15
15
|
message: "A fetch implementation is required",
|
|
16
16
|
});
|
|
17
|
-
this.fetchImpl =
|
|
17
|
+
this.fetchImpl = options.fetch
|
|
18
|
+
? (input, init) => options.fetch(input, init)
|
|
19
|
+
: candidate.bind(globalThis);
|
|
18
20
|
this.getToken = options.getToken;
|
|
19
21
|
}
|
|
20
22
|
withNetwork(path) {
|
|
21
|
-
if (!this.network || this.network === "mainnet")
|
|
23
|
+
if (!this.network || this.network === "mainnet" || /[?&]network=/.test(path))
|
|
22
24
|
return path;
|
|
23
25
|
const separator = path.includes("?") ? "&" : "?";
|
|
24
26
|
return `${path}${separator}network=${encodeURIComponent(this.network)}`;
|
package/dist/shillbot/api.js
CHANGED
|
@@ -67,10 +67,21 @@ export function validatePreparedTransaction(encoded, expected) {
|
|
|
67
67
|
const shillbotId = SHILLBOT_PROGRAM_ID.toBase58();
|
|
68
68
|
const shillbotIxs = inspected.instructions.filter((instruction) => instruction.program_id === shillbotId);
|
|
69
69
|
const lifecycle = shillbotIxs.filter((instruction) => matchesDiscriminator(instruction.data_base64, expected.action));
|
|
70
|
-
|
|
70
|
+
const companions = shillbotIxs.filter((instruction) => instruction !== lifecycle[0]);
|
|
71
|
+
const payoutDisc = shillbotInstructionDiscriminator("set_payout_to");
|
|
72
|
+
const validCompanions = companions.length === 0 ||
|
|
73
|
+
(expected.action === "claim" &&
|
|
74
|
+
companions.length === 1 &&
|
|
75
|
+
Buffer.from(companions[0].data_base64, "base64").subarray(0, 8).equals(payoutDisc) &&
|
|
76
|
+
inspected.instructions.indexOf(companions[0]) > inspected.instructions.indexOf(lifecycle[0]) &&
|
|
77
|
+
companions[0].accounts.length === 2 &&
|
|
78
|
+
(!expected.taskPda || companions[0].accounts[0] === expected.taskPda) &&
|
|
79
|
+
companions[0].accounts[1] === expected.wallet);
|
|
80
|
+
if (lifecycle.length !== 1 || !validCompanions) {
|
|
71
81
|
throw new SwarmClientError({ code: "TRANSACTION_MISMATCH", operation: "shillbot.validatePreparedTransaction", message: "Expected exactly one matching Shillbot lifecycle instruction" });
|
|
72
82
|
}
|
|
73
|
-
|
|
83
|
+
const taskAccountIndex = expected.action === "create" ? 1 : 0;
|
|
84
|
+
if (expected.taskPda && lifecycle[0].accounts[taskAccountIndex] !== expected.taskPda) {
|
|
74
85
|
throw new SwarmClientError({ code: "TRANSACTION_MISMATCH", operation: "shillbot.validatePreparedTransaction", message: "Task PDA differs from expectation" });
|
|
75
86
|
}
|
|
76
87
|
const alwaysAllowed = new Set([
|
package/dist/shillbot/index.js
CHANGED
|
@@ -210,10 +210,25 @@ export function buildTransaction(request) {
|
|
|
210
210
|
},
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
|
+
const COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
214
|
+
const SECP256K1_PROGRAM_ID = "KeccakSecp256k11111111111111111111111111111";
|
|
215
|
+
const SWITCHBOARD_PROGRAM_IDS = {
|
|
216
|
+
mainnet: "SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv",
|
|
217
|
+
devnet: "Aio4gaXjXzJNVLtzwtNVmSqGKpANtXhybbkhtAC94ji2",
|
|
218
|
+
};
|
|
213
219
|
export function inspectTransaction(encoded) {
|
|
214
220
|
const bytes = Buffer.from(encoded, "base64");
|
|
221
|
+
let tx;
|
|
215
222
|
try {
|
|
216
|
-
|
|
223
|
+
tx = VersionedTransaction.deserialize(bytes);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
tx = undefined;
|
|
227
|
+
}
|
|
228
|
+
if (tx) {
|
|
229
|
+
if (tx.message.addressTableLookups.length > 0) {
|
|
230
|
+
throw new Error("address lookup tables are not allowed in Shillbot lifecycle transactions");
|
|
231
|
+
}
|
|
217
232
|
const keys = tx.message.staticAccountKeys;
|
|
218
233
|
return {
|
|
219
234
|
version: tx.version === "legacy" ? "legacy" : "v0",
|
|
@@ -228,22 +243,20 @@ export function inspectTransaction(encoded) {
|
|
|
228
243
|
})),
|
|
229
244
|
};
|
|
230
245
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
};
|
|
246
|
-
}
|
|
246
|
+
const legacyTx = Transaction.from(bytes);
|
|
247
|
+
const message = legacyTx.compileMessage();
|
|
248
|
+
return {
|
|
249
|
+
version: "legacy",
|
|
250
|
+
fee_payer: message.accountKeys[0].toBase58(),
|
|
251
|
+
signers: message.accountKeys
|
|
252
|
+
.slice(0, message.header.numRequiredSignatures)
|
|
253
|
+
.map((key) => key.toBase58()),
|
|
254
|
+
instructions: message.instructions.map((ix) => ({
|
|
255
|
+
program_id: message.accountKeys[ix.programIdIndex].toBase58(),
|
|
256
|
+
accounts: ix.accounts.map((index) => message.accountKeys[index].toBase58()),
|
|
257
|
+
data_base64: Buffer.from(bs58.decode(ix.data)).toString("base64"),
|
|
258
|
+
})),
|
|
259
|
+
};
|
|
247
260
|
}
|
|
248
261
|
export function verifyIntent(encoded, intent) {
|
|
249
262
|
const { digest: _digest, ...rest } = intent;
|
|
@@ -255,14 +268,27 @@ export function verifyIntent(encoded, intent) {
|
|
|
255
268
|
throw new Error("fee payer differs from intent");
|
|
256
269
|
if (!inspected.signers.includes(intent.wallet))
|
|
257
270
|
throw new Error("wallet is not a required signer");
|
|
271
|
+
const shillbotProgramId = SHILLBOT_PROGRAM_ID.toBase58();
|
|
272
|
+
const allowedPrograms = new Set([
|
|
273
|
+
shillbotProgramId,
|
|
274
|
+
COMPUTE_BUDGET_PROGRAM_ID,
|
|
275
|
+
...(intent.action === "verify"
|
|
276
|
+
? [SWITCHBOARD_PROGRAM_IDS[intent.network], SECP256K1_PROGRAM_ID]
|
|
277
|
+
: []),
|
|
278
|
+
]);
|
|
279
|
+
for (const ix of inspected.instructions) {
|
|
280
|
+
if (!allowedPrograms.has(ix.program_id)) {
|
|
281
|
+
throw new Error(`unexpected companion program ${ix.program_id}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
258
284
|
const expectedDiscriminator = shillbotInstructionDiscriminator(intent.action === "submit" ? "submit_work" : `${intent.action}_task`);
|
|
259
|
-
const lifecycle = inspected.instructions.filter((ix) => ix.program_id ===
|
|
285
|
+
const lifecycle = inspected.instructions.filter((ix) => ix.program_id === shillbotProgramId &&
|
|
260
286
|
Buffer.from(ix.data_base64, "base64")
|
|
261
287
|
.subarray(0, 8)
|
|
262
288
|
.equals(expectedDiscriminator));
|
|
263
289
|
if (lifecycle.length !== 1)
|
|
264
290
|
throw new Error("expected exactly one Shillbot lifecycle instruction");
|
|
265
|
-
const shillbotInstructions = inspected.instructions.filter((ix) => ix.program_id ===
|
|
291
|
+
const shillbotInstructions = inspected.instructions.filter((ix) => ix.program_id === shillbotProgramId);
|
|
266
292
|
const payoutDiscriminator = shillbotInstructionDiscriminator("set_payout_to");
|
|
267
293
|
const companions = shillbotInstructions.filter((ix) => ix !== lifecycle[0]);
|
|
268
294
|
const lifecycleIndex = inspected.instructions.indexOf(lifecycle[0]);
|
|
@@ -289,6 +315,27 @@ export function verifyIntent(encoded, intent) {
|
|
|
289
315
|
if (lifecycle[0].accounts.join(",") !== intent.accounts.join(",")) {
|
|
290
316
|
throw new Error("lifecycle accounts differ from intent");
|
|
291
317
|
}
|
|
318
|
+
if (intent.action === "create" &&
|
|
319
|
+
intent.arguments.escrowLamports !== undefined) {
|
|
320
|
+
const data = Buffer.from(lifecycle[0].data_base64, "base64");
|
|
321
|
+
if (data.length < 24)
|
|
322
|
+
throw new Error("create instruction data is truncated");
|
|
323
|
+
const view = new DataView(data.buffer, data.byteOffset + 16, 8);
|
|
324
|
+
if (view.getBigUint64(0, true) !== BigInt(String(intent.arguments.escrowLamports))) {
|
|
325
|
+
throw new Error("escrow amount differs from intent");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (intent.action === "submit" &&
|
|
329
|
+
typeof intent.arguments.contentId === "string") {
|
|
330
|
+
const data = Buffer.from(lifecycle[0].data_base64, "base64");
|
|
331
|
+
if (data.length < 12)
|
|
332
|
+
throw new Error("submit instruction data is truncated");
|
|
333
|
+
const len = data.readUInt32LE(8);
|
|
334
|
+
if (data.length !== 12 + len ||
|
|
335
|
+
data.subarray(12, 12 + len).toString("utf8") !== intent.arguments.contentId) {
|
|
336
|
+
throw new Error("content id differs from intent");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
292
339
|
return inspected;
|
|
293
340
|
}
|
|
294
341
|
export async function signAndBroadcast(connection, encodedUnsigned, sign, confirmation) {
|
|
@@ -308,11 +355,11 @@ export async function signAndBroadcast(connection, encodedUnsigned, sign, confir
|
|
|
308
355
|
verifySignatures: true,
|
|
309
356
|
});
|
|
310
357
|
const signature = await connection.sendRawTransaction(wire);
|
|
311
|
-
|
|
312
|
-
await connection.confirmTransaction({ signature, ...confirmation }, "confirmed")
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
358
|
+
const confirmed = confirmation
|
|
359
|
+
? await connection.confirmTransaction({ signature, ...confirmation }, "confirmed")
|
|
360
|
+
: await connection.confirmTransaction(signature, "confirmed");
|
|
361
|
+
if (confirmed?.value?.err) {
|
|
362
|
+
throw new Error(`transaction ${signature} failed on-chain: ${JSON.stringify(confirmed.value.err)}`);
|
|
316
363
|
}
|
|
317
364
|
return signature;
|
|
318
365
|
}
|
|
@@ -55,5 +55,7 @@ export declare const resolveShillbotState: StateResolver;
|
|
|
55
55
|
* §6 capture window: only Verified is attestable. Finalize closes the on-chain
|
|
56
56
|
* account, so post-finalize the attestation is permanently unverifiable. */
|
|
57
57
|
export declare const shillbotValidStates: (accountKind: string) => readonly string[];
|
|
58
|
+
export declare const SHILLBOT_VOW_PROGRAM_ID = "2tR37nqMpwdV4DVUHjzUmL1rH2DtkA8zrRA4EAhT7KMi";
|
|
59
|
+
export declare const SHILLBOT_SCORE_MAX = 1000000n;
|
|
58
60
|
/** Convenience bundle for callers that just want the canonical Shillbot triple. */
|
|
59
61
|
export declare const shillbotProtocol: ProtocolHandler;
|
|
@@ -56,10 +56,7 @@ export const decodeShillbotTask = (data, accountKind) => {
|
|
|
56
56
|
const client = new PublicKey(data.slice(8, 40)).toBase58();
|
|
57
57
|
const agent = new PublicKey(data.slice(40, 72)).toBase58();
|
|
58
58
|
const state = data[72];
|
|
59
|
-
|
|
60
|
-
// format carries it directly; verifier's field-equality check
|
|
61
|
-
// covers it via the v1 §4 step 5 platform comparison done by the
|
|
62
|
-
// caller, not here).
|
|
59
|
+
const platform = data[73];
|
|
63
60
|
const content_hash = bytesToHex(data.slice(82, 114));
|
|
64
61
|
const content_id_hash = bytesToHex(data.slice(114, 146));
|
|
65
62
|
const composite_score = view.getBigUint64(162, true);
|
|
@@ -70,6 +67,7 @@ export const decodeShillbotTask = (data, accountKind) => {
|
|
|
70
67
|
client,
|
|
71
68
|
agent,
|
|
72
69
|
state,
|
|
70
|
+
platform,
|
|
73
71
|
composite_score,
|
|
74
72
|
verified_at,
|
|
75
73
|
verification_hash,
|
|
@@ -115,11 +113,15 @@ export const shillbotValidStates = (accountKind) => {
|
|
|
115
113
|
return [];
|
|
116
114
|
return ["verified"];
|
|
117
115
|
};
|
|
116
|
+
export const SHILLBOT_VOW_PROGRAM_ID = "2tR37nqMpwdV4DVUHjzUmL1rH2DtkA8zrRA4EAhT7KMi";
|
|
117
|
+
export const SHILLBOT_SCORE_MAX = 1000000n;
|
|
118
118
|
/** Convenience bundle for callers that just want the canonical Shillbot triple. */
|
|
119
119
|
export const shillbotProtocol = {
|
|
120
120
|
decode: decodeShillbotTask,
|
|
121
121
|
resolveState: resolveShillbotState,
|
|
122
122
|
validStates: shillbotValidStates,
|
|
123
|
+
programId: SHILLBOT_VOW_PROGRAM_ID,
|
|
124
|
+
scoreMax: SHILLBOT_SCORE_MAX,
|
|
123
125
|
};
|
|
124
126
|
function bytesToHex(b) {
|
|
125
127
|
let s = "";
|
package/dist/vow/schema.js
CHANGED
|
@@ -116,9 +116,12 @@ function isRfc3339NoFraction(v) {
|
|
|
116
116
|
if (!pattern.test(v))
|
|
117
117
|
return false;
|
|
118
118
|
// Round-trip via Date to confirm the components describe a real
|
|
119
|
-
// moment (rejects e.g. "2026-13-32T25:61:99Z").
|
|
119
|
+
// moment (rejects e.g. "2026-13-32T25:61:99Z" and rollover dates like "2026-02-31T00:00:00Z").
|
|
120
120
|
const ms = Date.parse(v);
|
|
121
|
-
|
|
121
|
+
if (Number.isNaN(ms))
|
|
122
|
+
return false;
|
|
123
|
+
const canonicalZ = v.replace(/\+00:00$/, "Z");
|
|
124
|
+
return new Date(ms).toISOString().replace(/\.\d+Z$/, "Z") === canonicalZ;
|
|
122
125
|
}
|
|
123
126
|
function isPlainObject(v) {
|
|
124
127
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
package/dist/vow/types.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export interface DecodedAccount {
|
|
|
67
67
|
verification_hash: string;
|
|
68
68
|
content_hash: string;
|
|
69
69
|
content_id_hash: string;
|
|
70
|
+
platform?: number;
|
|
70
71
|
}
|
|
71
72
|
/**
|
|
72
73
|
* Per-protocol pluggable decoder. The verifier calls this with the
|
|
@@ -104,4 +105,6 @@ export interface ProtocolHandler {
|
|
|
104
105
|
decode: AccountDecoder;
|
|
105
106
|
resolveState: StateResolver;
|
|
106
107
|
validStates: ValidStates;
|
|
108
|
+
programId?: string;
|
|
109
|
+
scoreMax?: bigint | number | string;
|
|
107
110
|
}
|
package/dist/vow/verify.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Connection } from "@solana/web3.js";
|
|
2
2
|
import type { AasV1Attestation, FailureReason, ProtocolHandler, Verdict } from "./types.js";
|
|
3
|
+
export declare const DEFAULT_SCORE_MAX = 1000000n;
|
|
3
4
|
/**
|
|
4
5
|
* Step-by-step verifier per `docs/specs/vow-v1.md` §4.
|
|
5
6
|
*
|
|
@@ -10,7 +11,7 @@ import type { AasV1Attestation, FailureReason, ProtocolHandler, Verdict } from "
|
|
|
10
11
|
*
|
|
11
12
|
* Splitting lets unit tests cover the pure parts without an RPC.
|
|
12
13
|
*/
|
|
13
|
-
export declare function verifyV1(attestation: unknown, protocol: ProtocolHandler, rpc: Connection): Promise<Verdict>;
|
|
14
|
+
export declare function verifyV1(attestation: unknown, protocol: ProtocolHandler, rpc: Connection, expectedScoreMax?: bigint | number | string): Promise<Verdict>;
|
|
14
15
|
/**
|
|
15
16
|
* Pure step 1 + step 6 of the protocol. Returns null on success or
|
|
16
17
|
* the first failure reason.
|
|
@@ -20,4 +21,4 @@ export declare function verifyV1Schema(attestation: unknown): FailureReason | nu
|
|
|
20
21
|
* Steps 2-5 + 7 — touches RPC. Returns null on success or the first
|
|
21
22
|
* failure reason.
|
|
22
23
|
*/
|
|
23
|
-
export declare function verifyV1OnChain(a: AasV1Attestation, protocol: ProtocolHandler, rpc: Connection): Promise<FailureReason | null>;
|
|
24
|
+
export declare function verifyV1OnChain(a: AasV1Attestation, protocol: ProtocolHandler, rpc: Connection, expectedScoreMax?: bigint | number | string): Promise<FailureReason | null>;
|
package/dist/vow/verify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PublicKey } from "@solana/web3.js";
|
|
2
2
|
import { anchorDiscriminator, bytesEqual } from "./discriminator.js";
|
|
3
3
|
import { asV1, checkSchema } from "./schema.js";
|
|
4
|
+
export const DEFAULT_SCORE_MAX = 1000000n;
|
|
4
5
|
/**
|
|
5
6
|
* Step-by-step verifier per `docs/specs/vow-v1.md` §4.
|
|
6
7
|
*
|
|
@@ -11,7 +12,7 @@ import { asV1, checkSchema } from "./schema.js";
|
|
|
11
12
|
*
|
|
12
13
|
* Splitting lets unit tests cover the pure parts without an RPC.
|
|
13
14
|
*/
|
|
14
|
-
export async function verifyV1(attestation, protocol, rpc) {
|
|
15
|
+
export async function verifyV1(attestation, protocol, rpc, expectedScoreMax) {
|
|
15
16
|
const checkedAt = nowRfc3339();
|
|
16
17
|
const rpcEndpoint = rpc.rpcEndpoint;
|
|
17
18
|
const schemaFail = verifyV1Schema(attestation);
|
|
@@ -25,7 +26,7 @@ export async function verifyV1(attestation, protocol, rpc) {
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
const a = asV1(attestation);
|
|
28
|
-
const onChainFail = await verifyV1OnChain(a, protocol, rpc);
|
|
29
|
+
const onChainFail = await verifyV1OnChain(a, protocol, rpc, expectedScoreMax);
|
|
29
30
|
if (onChainFail) {
|
|
30
31
|
return {
|
|
31
32
|
valid: false,
|
|
@@ -61,7 +62,11 @@ export function verifyV1Schema(attestation) {
|
|
|
61
62
|
* Steps 2-5 + 7 — touches RPC. Returns null on success or the first
|
|
62
63
|
* failure reason.
|
|
63
64
|
*/
|
|
64
|
-
export async function verifyV1OnChain(a, protocol, rpc) {
|
|
65
|
+
export async function verifyV1OnChain(a, protocol, rpc, expectedScoreMax) {
|
|
66
|
+
const expectedMax = BigInt(expectedScoreMax ?? protocol.scoreMax ?? DEFAULT_SCORE_MAX);
|
|
67
|
+
if (BigInt(a.score_max) !== expectedMax) {
|
|
68
|
+
return "field_mismatch:score_max";
|
|
69
|
+
}
|
|
65
70
|
// Step 2 — fetch the on-chain account.
|
|
66
71
|
const accountInfo = await rpc.getAccountInfo(new PublicKey(a.account));
|
|
67
72
|
if (accountInfo === null) {
|
|
@@ -72,6 +77,9 @@ export async function verifyV1OnChain(a, protocol, rpc) {
|
|
|
72
77
|
if (accountInfo.owner.toBase58() !== a.program_id) {
|
|
73
78
|
return "owner_mismatch";
|
|
74
79
|
}
|
|
80
|
+
if (protocol.programId !== undefined && accountInfo.owner.toBase58() !== protocol.programId) {
|
|
81
|
+
return "owner_mismatch";
|
|
82
|
+
}
|
|
75
83
|
// Step 4 — Anchor discriminator check.
|
|
76
84
|
if (data.length < 8) {
|
|
77
85
|
return "discriminator_mismatch";
|
|
@@ -103,6 +111,9 @@ export async function verifyV1OnChain(a, protocol, rpc) {
|
|
|
103
111
|
if (decoded.agent !== a.agent) {
|
|
104
112
|
return "field_mismatch:agent";
|
|
105
113
|
}
|
|
114
|
+
if (decoded.platform !== undefined && decoded.platform !== a.platform) {
|
|
115
|
+
return "field_mismatch:platform";
|
|
116
|
+
}
|
|
106
117
|
if (decoded.composite_score.toString() !== a.composite_score) {
|
|
107
118
|
return "field_mismatch:composite_score";
|
|
108
119
|
}
|