@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 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.11",
6
- "documentNamespace": "https://swarm.tips/sbom/client/0.2.11",
5
+ "name": "@swarm-tips/client-0.2.13",
6
+ "documentNamespace": "https://swarm.tips/sbom/client/0.2.13",
7
7
  "creationInfo": {
8
- "created": "2026-10-14T05:01:22.104Z",
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.11",
19
- "downloadLocation": "https://www.npmjs.com/package/@swarm-tips/client/v/0.2.11",
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 Switchboard pull feed account containing the oracle-attested composite score.",
2118
- "`PullFeedAccountData::parse()` which checks the account discriminator.",
2119
- "Account ownership is verified by Switchboard's `Owner` implementation."
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 Switchboard pull feed account containing the oracle-attested composite score.",
2112
- "`PullFeedAccountData::parse()` which checks the account discriminator.",
2113
- "Account ownership is verified by Switchboard's `Owner` implementation."
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
- return Number(stakeLamports) + SESSION_FEE_HEADROOM_LAMPORTS;
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
- return Number(stakeLamports) + SESSION_FEE_HEADROOM_LAMPORTS / 2;
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 = candidate.bind(globalThis);
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)}`;
@@ -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
- if (lifecycle.length !== 1 || shillbotIxs.length !== 1) {
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
- if (expected.taskPda && !lifecycle[0].accounts.includes(expected.taskPda)) {
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([
@@ -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
- const tx = VersionedTransaction.deserialize(bytes);
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
- catch {
232
- const tx = Transaction.from(bytes);
233
- const message = tx.compileMessage();
234
- return {
235
- version: "legacy",
236
- fee_payer: message.accountKeys[0].toBase58(),
237
- signers: message.accountKeys
238
- .slice(0, message.header.numRequiredSignatures)
239
- .map((key) => key.toBase58()),
240
- instructions: message.instructions.map((ix) => ({
241
- program_id: message.accountKeys[ix.programIdIndex].toBase58(),
242
- accounts: ix.accounts.map((index) => message.accountKeys[index].toBase58()),
243
- data_base64: Buffer.from(bs58.decode(ix.data)).toString("base64"),
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 === SHILLBOT_PROGRAM_ID.toBase58() &&
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 === SHILLBOT_PROGRAM_ID.toBase58());
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
- if (confirmation) {
312
- await connection.confirmTransaction({ signature, ...confirmation }, "confirmed");
313
- }
314
- else {
315
- await connection.confirmTransaction(signature, "confirmed");
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
- // platform is at offset 73 but unused by the verifier (the wire
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 = "";
@@ -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
- return !Number.isNaN(ms);
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);
@@ -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
  }
@@ -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>;
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarm-tips/client",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Typed browser and Node.js clients for Swarm Tips, Shillbot, and Coordination Game",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",