@swarm-tips/client 0.2.11 → 0.2.12
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/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 +1 -0
- package/dist/vow/decoders/shillbot.js +4 -4
- package/dist/vow/schema.js +5 -2
- package/dist/vow/types.d.ts +2 -0
- package/dist/vow/verify.js +6 -0
- 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.12",
|
|
6
|
+
"documentNamespace": "https://swarm.tips/sbom/client/0.2.12",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-10-
|
|
8
|
+
"created": "2026-10-16T05:41:05.183Z",
|
|
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.12",
|
|
19
|
+
"downloadLocation": "https://www.npmjs.com/package/@swarm-tips/client/v/0.2.12",
|
|
20
20
|
"filesAnalyzed": false,
|
|
21
21
|
"licenseConcluded": "MIT",
|
|
22
22
|
"licenseDeclared": "MIT"
|
|
@@ -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,6 @@ 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";
|
|
58
59
|
/** Convenience bundle for callers that just want the canonical Shillbot triple. */
|
|
59
60
|
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,13 @@ export const shillbotValidStates = (accountKind) => {
|
|
|
115
113
|
return [];
|
|
116
114
|
return ["verified"];
|
|
117
115
|
};
|
|
116
|
+
export const SHILLBOT_VOW_PROGRAM_ID = "2tR37nqMpwdV4DVUHjzUmL1rH2DtkA8zrRA4EAhT7KMi";
|
|
118
117
|
/** Convenience bundle for callers that just want the canonical Shillbot triple. */
|
|
119
118
|
export const shillbotProtocol = {
|
|
120
119
|
decode: decodeShillbotTask,
|
|
121
120
|
resolveState: resolveShillbotState,
|
|
122
121
|
validStates: shillbotValidStates,
|
|
122
|
+
programId: SHILLBOT_VOW_PROGRAM_ID,
|
|
123
123
|
};
|
|
124
124
|
function bytesToHex(b) {
|
|
125
125
|
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,5 @@ export interface ProtocolHandler {
|
|
|
104
105
|
decode: AccountDecoder;
|
|
105
106
|
resolveState: StateResolver;
|
|
106
107
|
validStates: ValidStates;
|
|
108
|
+
programId?: string;
|
|
107
109
|
}
|
package/dist/vow/verify.js
CHANGED
|
@@ -72,6 +72,9 @@ export async function verifyV1OnChain(a, protocol, rpc) {
|
|
|
72
72
|
if (accountInfo.owner.toBase58() !== a.program_id) {
|
|
73
73
|
return "owner_mismatch";
|
|
74
74
|
}
|
|
75
|
+
if (protocol.programId !== undefined && accountInfo.owner.toBase58() !== protocol.programId) {
|
|
76
|
+
return "owner_mismatch";
|
|
77
|
+
}
|
|
75
78
|
// Step 4 — Anchor discriminator check.
|
|
76
79
|
if (data.length < 8) {
|
|
77
80
|
return "discriminator_mismatch";
|
|
@@ -103,6 +106,9 @@ export async function verifyV1OnChain(a, protocol, rpc) {
|
|
|
103
106
|
if (decoded.agent !== a.agent) {
|
|
104
107
|
return "field_mismatch:agent";
|
|
105
108
|
}
|
|
109
|
+
if (decoded.platform !== undefined && decoded.platform !== a.platform) {
|
|
110
|
+
return "field_mismatch:platform";
|
|
111
|
+
}
|
|
106
112
|
if (decoded.composite_score.toString() !== a.composite_score) {
|
|
107
113
|
return "field_mismatch:composite_score";
|
|
108
114
|
}
|