@swarm-tips/client 0.0.0 → 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 (60) hide show
  1. package/README.md +17 -1
  2. package/SBOM.spdx.json +1558 -0
  3. package/dist/cli/swarm-tx.d.ts +2 -0
  4. package/dist/cli/swarm-tx.js +46 -0
  5. package/dist/cli/vow-verify.d.ts +2 -0
  6. package/dist/cli/vow-verify.js +69 -0
  7. package/dist/contracts/generated/coordination_game.d.ts +6107 -0
  8. package/dist/contracts/generated/coordination_game.js +1 -0
  9. package/dist/contracts/generated/shillbot.d.ts +4209 -0
  10. package/dist/contracts/generated/shillbot.js +1 -0
  11. package/dist/contracts/idl/coordination_game.json +6101 -0
  12. package/dist/contracts/idl/shillbot.json +4203 -0
  13. package/dist/contracts/index.d.ts +33 -0
  14. package/dist/contracts/index.js +84 -0
  15. package/dist/coordination-game/battery.d.ts +40 -0
  16. package/dist/coordination-game/battery.js +82 -0
  17. package/dist/coordination-game/index.d.ts +4 -0
  18. package/dist/coordination-game/index.js +3 -0
  19. package/dist/coordination-game/outcome-oracle.d.ts +53 -0
  20. package/dist/coordination-game/outcome-oracle.js +136 -0
  21. package/dist/evm/index.d.ts +3 -0
  22. package/dist/evm/index.js +6 -0
  23. package/dist/evm/sign.d.ts +12 -0
  24. package/dist/evm/sign.js +23 -0
  25. package/dist/evm/testing.d.ts +21 -0
  26. package/dist/evm/testing.js +34 -0
  27. package/dist/evm/tx.d.ts +27 -0
  28. package/dist/evm/tx.js +18 -0
  29. package/dist/evm/wallet.d.ts +58 -0
  30. package/dist/evm/wallet.js +182 -0
  31. package/dist/inbox/client.d.ts +189 -0
  32. package/dist/inbox/client.js +334 -0
  33. package/dist/inbox/index.d.ts +2 -0
  34. package/dist/inbox/index.js +4 -0
  35. package/dist/inbox/solana.d.ts +11 -0
  36. package/dist/inbox/solana.js +13 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.js +3 -0
  39. package/dist/shillbot/index.d.ts +115 -0
  40. package/dist/shillbot/index.js +327 -0
  41. package/dist/shillbot/payout-oracle.d.ts +63 -0
  42. package/dist/shillbot/payout-oracle.js +172 -0
  43. package/dist/shillbot/switchboard.d.ts +27 -0
  44. package/dist/shillbot/switchboard.js +205 -0
  45. package/dist/vow/decoders/extension.d.ts +62 -0
  46. package/dist/vow/decoders/extension.js +48 -0
  47. package/dist/vow/decoders/shillbot.d.ts +59 -0
  48. package/dist/vow/decoders/shillbot.js +130 -0
  49. package/dist/vow/discriminator.d.ts +12 -0
  50. package/dist/vow/discriminator.js +25 -0
  51. package/dist/vow/index.d.ts +7 -0
  52. package/dist/vow/index.js +5 -0
  53. package/dist/vow/schema.d.ts +14 -0
  54. package/dist/vow/schema.js +132 -0
  55. package/dist/vow/types.d.ts +107 -0
  56. package/dist/vow/types.js +1 -0
  57. package/dist/vow/verify.d.ts +23 -0
  58. package/dist/vow/verify.js +145 -0
  59. package/package.json +92 -6
  60. package/index.js +0 -1
@@ -0,0 +1,327 @@
1
+ import { Buffer } from "buffer";
2
+ import { sha256 } from "js-sha256";
3
+ import { BN, SHILLBOT_PROGRAM_ID, agentStatePda, buildShillbotInstruction, clientStatePda, globalStatePda, shillbotInstructionDiscriminator, taskPda, } from "../contracts/index.js";
4
+ import bs58 from "bs58";
5
+ import { PublicKey, Transaction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js";
6
+ export { COORDINATION_GAME_IDL, COORDINATION_GAME_PROGRAM_ID, SHILLBOT_IDL, SHILLBOT_PROGRAM_ID, agentStatePda, buildShillbotInstruction, clientStatePda, globalStatePda, shillbotInstructionDiscriminator, taskPda, } from "../contracts/index.js";
7
+ export const SCHEMA_VERSION = "swarm.shillbot.transaction-intent/v1";
8
+ export function buildInstruction(request) {
9
+ const wallet = new PublicKey(request.wallet);
10
+ if (request.action === "create") {
11
+ const nonce = BigInt(request.nonce);
12
+ return buildShillbotInstruction({
13
+ name: "create_task",
14
+ accounts: {
15
+ global_state: globalStatePda(),
16
+ task: taskPda(nonce, wallet),
17
+ client_state: clientStatePda(wallet),
18
+ client: wallet,
19
+ },
20
+ args: {
21
+ nonce: new BN(request.nonce),
22
+ escrow_lamports: new BN(request.escrowLamports),
23
+ content_hash: Array.from(Buffer.from(request.contentHash, "hex")),
24
+ deadline: new BN(request.deadline),
25
+ submit_margin: new BN(request.submitMargin),
26
+ claim_buffer: new BN(request.claimBuffer),
27
+ platform: request.platform,
28
+ attestation_delay_override: request.attestationDelayOverride,
29
+ challenge_window_override: request.challengeWindowOverride,
30
+ verification_timeout_override: request.verificationTimeoutOverride,
31
+ requires_approval: request.requiresApproval,
32
+ verification_kind: request.verificationKind,
33
+ },
34
+ });
35
+ }
36
+ const task = new PublicKey(request.taskPda);
37
+ if (request.action === "claim") {
38
+ return buildShillbotInstruction({
39
+ name: "claim_task",
40
+ accounts: {
41
+ task,
42
+ global_state: globalStatePda(),
43
+ agent_state: agentStatePda(wallet),
44
+ agent: wallet,
45
+ },
46
+ });
47
+ }
48
+ if (request.action === "submit") {
49
+ return buildShillbotInstruction({
50
+ name: "submit_work",
51
+ accounts: {
52
+ task,
53
+ global_state: globalStatePda(),
54
+ agent_state: agentStatePda(wallet),
55
+ agent: wallet,
56
+ },
57
+ args: { content_id: Buffer.from(request.contentId, "utf8") },
58
+ });
59
+ }
60
+ if (request.action === "approve") {
61
+ return buildShillbotInstruction({
62
+ name: "approve_task",
63
+ accounts: { task, client: wallet },
64
+ });
65
+ }
66
+ if (request.action === "verify") {
67
+ return buildShillbotInstruction({
68
+ name: "verify_task",
69
+ accounts: {
70
+ task,
71
+ global_state: globalStatePda(),
72
+ switchboard_feed: request.switchboardFeed,
73
+ },
74
+ args: {
75
+ composite_score: new BN(request.compositeScore),
76
+ verification_hash: Array.from(Buffer.from(request.verificationHash, "hex")),
77
+ },
78
+ });
79
+ }
80
+ const finalize = request;
81
+ return buildShillbotInstruction({
82
+ name: "finalize_task",
83
+ accounts: {
84
+ task,
85
+ global_state: globalStatePda(),
86
+ agent: finalize.agent,
87
+ client: finalize.client,
88
+ treasury: finalize.treasury,
89
+ },
90
+ });
91
+ }
92
+ function payoutInstruction(request) {
93
+ if (!request.payoutTo)
94
+ return undefined;
95
+ return buildShillbotInstruction({
96
+ name: "set_payout_to",
97
+ accounts: { task: request.taskPda, agent: request.wallet },
98
+ args: { payout_to: new PublicKey(request.payoutTo) },
99
+ });
100
+ }
101
+ function stable(value) {
102
+ if (Array.isArray(value))
103
+ return value.map(stable);
104
+ if (value && typeof value === "object") {
105
+ return Object.fromEntries(Object.entries(value)
106
+ .filter(([, child]) => child !== undefined)
107
+ .sort(([a], [b]) => a.localeCompare(b))
108
+ .map(([key, child]) => [key, stable(child)]));
109
+ }
110
+ return value;
111
+ }
112
+ export function intentDigest(intent) {
113
+ return sha256(JSON.stringify(stable(intent)));
114
+ }
115
+ function actionRisk(action) {
116
+ if (action === "create")
117
+ return "spend";
118
+ if (action === "approve")
119
+ return "escrow_control";
120
+ return "earn";
121
+ }
122
+ function serializeUnsigned(payer, blockhash, instructions, version) {
123
+ if (version === "v0") {
124
+ const message = new TransactionMessage({
125
+ payerKey: payer,
126
+ recentBlockhash: blockhash,
127
+ instructions,
128
+ }).compileToV0Message();
129
+ return Buffer.from(new VersionedTransaction(message).serialize()).toString("base64");
130
+ }
131
+ const tx = new Transaction({
132
+ feePayer: payer,
133
+ recentBlockhash: blockhash,
134
+ }).add(...instructions);
135
+ return tx
136
+ .serialize({ requireAllSignatures: false, verifySignatures: false })
137
+ .toString("base64");
138
+ }
139
+ export function buildTransaction(request) {
140
+ const lifecycle = buildInstruction(request);
141
+ const sponsor = request.action === "claim" || request.action === "submit"
142
+ ? request.sponsor
143
+ : undefined;
144
+ const payer = new PublicKey(sponsor ?? request.wallet);
145
+ const companions = request.action === "verify"
146
+ ? request.crankInstructions
147
+ : request.action === "claim"
148
+ ? [payoutInstruction(request)].filter((ix) => ix !== undefined)
149
+ : [];
150
+ // set_payout_to authorizes against task.agent, which claim_task establishes.
151
+ // Keep the two operations atomic, but execute the claim first.
152
+ const instructions = request.action === "claim"
153
+ ? [lifecycle, ...companions]
154
+ : [...companions, lifecycle];
155
+ const unsignedTx = serializeUnsigned(payer, request.recentBlockhash, instructions, request.version ?? (request.action === "verify" ? "v0" : "legacy"));
156
+ const task = lifecycle.keys[request.action === "create" ? 1 : 0].pubkey.toBase58();
157
+ const accounts = lifecycle.keys.map((key) => key.pubkey.toBase58());
158
+ const signers = [payer.toBase58()];
159
+ if (payer.toBase58() !== request.wallet)
160
+ signers.push(request.wallet);
161
+ const movements = request.action === "create"
162
+ ? [
163
+ {
164
+ asset: "SOL",
165
+ from: request.wallet,
166
+ to: task,
167
+ amount: request.escrowLamports,
168
+ condition: "escrow deposit",
169
+ },
170
+ ]
171
+ : request.action === "approve"
172
+ ? [
173
+ {
174
+ asset: "escrow control",
175
+ from: task,
176
+ to: task,
177
+ condition: "authorizes verification; no immediate transfer",
178
+ },
179
+ ]
180
+ : [];
181
+ const actionArguments = Object.fromEntries(Object.entries(request).filter(([key, value]) => ![
182
+ "action",
183
+ "wallet",
184
+ "network",
185
+ "recentBlockhash",
186
+ "version",
187
+ "sponsor",
188
+ "crankInstructions",
189
+ ].includes(key) &&
190
+ ["string", "number", "boolean"].includes(typeof value)));
191
+ const withoutDigest = {
192
+ version: SCHEMA_VERSION,
193
+ action: request.action,
194
+ network: request.network,
195
+ wallet: request.wallet,
196
+ task_pda: task,
197
+ program_id: SHILLBOT_PROGRAM_ID.toBase58(),
198
+ fee_payer: payer.toBase58(),
199
+ accounts,
200
+ signers,
201
+ movements,
202
+ risk: actionRisk(request.action),
203
+ arguments: actionArguments,
204
+ };
205
+ return {
206
+ unsigned_tx: unsignedTx,
207
+ transaction_intent: {
208
+ ...withoutDigest,
209
+ digest: intentDigest(withoutDigest),
210
+ },
211
+ };
212
+ }
213
+ export function inspectTransaction(encoded) {
214
+ const bytes = Buffer.from(encoded, "base64");
215
+ try {
216
+ const tx = VersionedTransaction.deserialize(bytes);
217
+ const keys = tx.message.staticAccountKeys;
218
+ return {
219
+ version: tx.version === "legacy" ? "legacy" : "v0",
220
+ fee_payer: keys[0].toBase58(),
221
+ signers: keys
222
+ .slice(0, tx.message.header.numRequiredSignatures)
223
+ .map((key) => key.toBase58()),
224
+ instructions: tx.message.compiledInstructions.map((ix) => ({
225
+ program_id: keys[ix.programIdIndex].toBase58(),
226
+ accounts: ix.accountKeyIndexes.map((index) => keys[index].toBase58()),
227
+ data_base64: Buffer.from(ix.data).toString("base64"),
228
+ })),
229
+ };
230
+ }
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
+ }
247
+ }
248
+ export function verifyIntent(encoded, intent) {
249
+ const { digest: _digest, ...rest } = intent;
250
+ const expectedDigest = intentDigest(rest);
251
+ if (expectedDigest !== intent.digest)
252
+ throw new Error("intent digest mismatch");
253
+ const inspected = inspectTransaction(encoded);
254
+ if (inspected.fee_payer !== intent.fee_payer)
255
+ throw new Error("fee payer differs from intent");
256
+ if (!inspected.signers.includes(intent.wallet))
257
+ throw new Error("wallet is not a required signer");
258
+ 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() &&
260
+ Buffer.from(ix.data_base64, "base64")
261
+ .subarray(0, 8)
262
+ .equals(expectedDiscriminator));
263
+ if (lifecycle.length !== 1)
264
+ throw new Error("expected exactly one Shillbot lifecycle instruction");
265
+ const shillbotInstructions = inspected.instructions.filter((ix) => ix.program_id === SHILLBOT_PROGRAM_ID.toBase58());
266
+ const payoutDiscriminator = shillbotInstructionDiscriminator("set_payout_to");
267
+ const companions = shillbotInstructions.filter((ix) => ix !== lifecycle[0]);
268
+ const lifecycleIndex = inspected.instructions.indexOf(lifecycle[0]);
269
+ if (intent.action === "claim" &&
270
+ companions.some((ix) => Buffer.from(ix.data_base64, "base64")
271
+ .subarray(0, 8)
272
+ .equals(payoutDiscriminator) &&
273
+ inspected.instructions.indexOf(ix) < lifecycleIndex)) {
274
+ throw new Error("payout route must follow claim");
275
+ }
276
+ if (companions.length >
277
+ (intent.action === "claim" && intent.arguments.payoutTo ? 1 : 0) ||
278
+ companions.some((ix) => intent.action !== "claim" ||
279
+ !Buffer.from(ix.data_base64, "base64")
280
+ .subarray(0, 8)
281
+ .equals(payoutDiscriminator) ||
282
+ ix.accounts.join(",") !== [intent.task_pda, intent.wallet].join(",") ||
283
+ Buffer.from(ix.data_base64, "base64").subarray(8).toString("hex") !==
284
+ new PublicKey(String(intent.arguments.payoutTo))
285
+ .toBuffer()
286
+ .toString("hex"))) {
287
+ throw new Error("unexpected Shillbot companion instruction");
288
+ }
289
+ if (lifecycle[0].accounts.join(",") !== intent.accounts.join(",")) {
290
+ throw new Error("lifecycle accounts differ from intent");
291
+ }
292
+ return inspected;
293
+ }
294
+ export async function signAndBroadcast(connection, encodedUnsigned, sign, confirmation) {
295
+ const raw = Buffer.from(encodedUnsigned, "base64");
296
+ let transaction;
297
+ try {
298
+ transaction = VersionedTransaction.deserialize(raw);
299
+ }
300
+ catch {
301
+ transaction = Transaction.from(raw);
302
+ }
303
+ const signed = await sign(transaction);
304
+ const wire = signed instanceof VersionedTransaction
305
+ ? signed.serialize()
306
+ : signed.serialize({
307
+ requireAllSignatures: true,
308
+ verifySignatures: true,
309
+ });
310
+ 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");
316
+ }
317
+ return signature;
318
+ }
319
+ // Convenience aliases keep exact action names easy to discover and call.
320
+ export const buildCreate = (request) => buildTransaction({ ...request, action: "create" });
321
+ export const buildClaim = (request) => buildTransaction({ ...request, action: "claim" });
322
+ export const buildSubmit = (request) => buildTransaction({ ...request, action: "submit" });
323
+ export const buildApprove = (request) => buildTransaction({ ...request, action: "approve" });
324
+ export const buildVerify = (request) => buildTransaction({ ...request, action: "verify" });
325
+ export const buildFinalize = (request) => buildTransaction({ ...request, action: "finalize" });
326
+ export { buildSwitchboardCrankAndVerify } from "./switchboard.js";
327
+ export * from "./payout-oracle.js";
@@ -0,0 +1,63 @@
1
+ export declare const MAX_SCORE = 1000000;
2
+ /** The terminal settlement path a task took. */
3
+ export declare enum TaskOutcomeKind {
4
+ /** Challenge window elapsed unchallenged → finalize_task pays the agent. */
5
+ Finalized = 0,
6
+ /** Challenged, authority resolves agent-favor → payment + bond slashed to agent/treasury. */
7
+ ResolvedAgentWins = 1,
8
+ /** Challenged, authority resolves challenger-favor → escrow to client, bond returned. */
9
+ ResolvedChallengerWins = 2,
10
+ /** Disputed, permissionless liveness crank → pinned payment, bond un-slashed. */
11
+ DefaultResolved = 3,
12
+ /** Verification timeout / deadline → expire_task refunds the client in full. */
13
+ Expired = 4
14
+ }
15
+ export interface TaskScenario {
16
+ escrowLamports: bigint;
17
+ qualityThreshold: number;
18
+ protocolFeeBps: number;
19
+ compositeScore: number;
20
+ /** 0 = OracleMetrics (Switchboard), 1 = DeterministicAttested (score ∈ {0, MAX_SCORE}). */
21
+ verificationKind: 0 | 1;
22
+ /** Challenge bond = multiplier × escrow; bounded [2, 10] on-chain. */
23
+ challengeBondMultiplier: number;
24
+ /** Treasury share of a slashed bond, in basis points (e.g. 5000 = 50%). */
25
+ bondSlashTreasuryBps: number;
26
+ outcome: TaskOutcomeKind;
27
+ }
28
+ /** Realized escrow+bond distribution, EXCLUDING rent (the harness tolerates rent
29
+ * on player accounts via `>=` and reconciles protocol accounts exactly). */
30
+ export interface TaskPayout {
31
+ kind: TaskOutcomeKind;
32
+ agentLamports: bigint;
33
+ treasuryLamports: bigint;
34
+ /** Escrow returned to the client (remainder on payment, or full refund). */
35
+ clientLamports: bigint;
36
+ /** Bond returned to the challenger (0 when the bond was slashed away). */
37
+ challengerLamports: bigint;
38
+ }
39
+ export interface PaymentSplit {
40
+ payment: bigint;
41
+ fee: bigint;
42
+ remainder: bigint;
43
+ }
44
+ /**
45
+ * Mirror of `scoring.rs::compute_payment` (payment, fee) plus the escrow
46
+ * remainder. Floor division throughout, u128-safe via bigint. Throws on the
47
+ * same preconditions the program asserts (score/threshold ≤ MAX_SCORE).
48
+ */
49
+ export declare function computePayment(compositeScore: number, qualityThreshold: number, escrowLamports: bigint, protocolFeeBps: number): PaymentSplit;
50
+ /** Challenge bond = multiplier × escrow, bounded [2, 10] like the program. */
51
+ export declare function computeChallengeBond(escrowLamports: bigint, multiplier: number): bigint;
52
+ /** Treasury/agent split of a slashed bond, mirror of `resolve_challenge.rs::slash_bond`. */
53
+ export declare function splitSlashedBond(bond: bigint, bondSlashTreasuryBps: number): {
54
+ treasuryShare: bigint;
55
+ agentShare: bigint;
56
+ };
57
+ /**
58
+ * The canonical payout derivation. Given a task scenario (its escrow, score,
59
+ * verification kind, and the terminal PATH it took), returns the exact
60
+ * escrow+bond distribution the on-chain program produces. Conserves value:
61
+ * escrow + bond is fully redistributed (asserted before return).
62
+ */
63
+ export declare function deriveTaskOutcome(s: TaskScenario): TaskPayout;
@@ -0,0 +1,172 @@
1
+ // Task-outcome oracle — the canonical Shillbot payout matrix, in TypeScript.
2
+ //
3
+ // This is the off-chain test/tooling mirror of the on-chain payout logic that
4
+ // three implementations must agree on (pinned by the golden vectors in
5
+ // tests/fixtures/task-payout-vectors.json):
6
+ //
7
+ // - programs/shillbot/src/scoring.rs `compute_payment`
8
+ // - programs/shillbot/src/instructions/resolve_challenge.rs `slash_bond`
9
+ // - evm/src/ShillbotEscrow.sol payment + bond split
10
+ //
11
+ // The combinatorial harness uses it as the single source of truth for "what
12
+ // SHOULD this task pay out" so every scenario — Solana bankrun, Solana devnet,
13
+ // EVM ShillbotEscrow, MCP-driven, browser-driven — asserts on-chain reality
14
+ // against ONE expectation instead of re-deriving the formula per test. If this
15
+ // drifts from the on-chain payout, the parity test + the combinatorial suites
16
+ // fail.
17
+ //
18
+ // Unlike the coordination game (where the outcome is DERIVED from the realized
19
+ // transcript), a Shillbot task's terminal PATH is a choice of which crank/
20
+ // resolution ran — finalize vs challenge-resolution vs dispute-liveness default
21
+ // vs expiry. The scenario carries that choice; the oracle computes the exact
22
+ // lamport split it implies. Roles are explicit (client creates, agent claims),
23
+ // so there is no FIFO "expected family" — the split is fully determined.
24
+ export const MAX_SCORE = 1_000_000;
25
+ /** The terminal settlement path a task took. */
26
+ export var TaskOutcomeKind;
27
+ (function (TaskOutcomeKind) {
28
+ /** Challenge window elapsed unchallenged → finalize_task pays the agent. */
29
+ TaskOutcomeKind[TaskOutcomeKind["Finalized"] = 0] = "Finalized";
30
+ /** Challenged, authority resolves agent-favor → payment + bond slashed to agent/treasury. */
31
+ TaskOutcomeKind[TaskOutcomeKind["ResolvedAgentWins"] = 1] = "ResolvedAgentWins";
32
+ /** Challenged, authority resolves challenger-favor → escrow to client, bond returned. */
33
+ TaskOutcomeKind[TaskOutcomeKind["ResolvedChallengerWins"] = 2] = "ResolvedChallengerWins";
34
+ /** Disputed, permissionless liveness crank → pinned payment, bond un-slashed. */
35
+ TaskOutcomeKind[TaskOutcomeKind["DefaultResolved"] = 3] = "DefaultResolved";
36
+ /** Verification timeout / deadline → expire_task refunds the client in full. */
37
+ TaskOutcomeKind[TaskOutcomeKind["Expired"] = 4] = "Expired";
38
+ })(TaskOutcomeKind || (TaskOutcomeKind = {}));
39
+ /**
40
+ * Mirror of `scoring.rs::compute_payment` (payment, fee) plus the escrow
41
+ * remainder. Floor division throughout, u128-safe via bigint. Throws on the
42
+ * same preconditions the program asserts (score/threshold ≤ MAX_SCORE).
43
+ */
44
+ export function computePayment(compositeScore, qualityThreshold, escrowLamports, protocolFeeBps) {
45
+ if (compositeScore > MAX_SCORE) {
46
+ throw new Error(`score ${compositeScore} exceeds MAX_SCORE ${MAX_SCORE}`);
47
+ }
48
+ if (qualityThreshold > MAX_SCORE) {
49
+ throw new Error(`threshold ${qualityThreshold} exceeds MAX_SCORE ${MAX_SCORE}`);
50
+ }
51
+ if (escrowLamports < 0n)
52
+ throw new Error("escrow must be non-negative");
53
+ if (compositeScore < qualityThreshold) {
54
+ return { payment: 0n, fee: 0n, remainder: escrowLamports };
55
+ }
56
+ const scoreRange = MAX_SCORE - qualityThreshold;
57
+ // threshold == MAX_SCORE ⇒ no score can pay (division by zero guard).
58
+ if (scoreRange === 0) {
59
+ return { payment: 0n, fee: 0n, remainder: escrowLamports };
60
+ }
61
+ const scoreAbove = BigInt(compositeScore - qualityThreshold);
62
+ const gross = (escrowLamports * scoreAbove) / BigInt(scoreRange);
63
+ const fee = (gross * BigInt(protocolFeeBps)) / 10000n;
64
+ const payment = gross - fee;
65
+ const remainder = escrowLamports - payment - fee;
66
+ // Postcondition mirrors the program: payment + fee ≤ escrow.
67
+ if (payment + fee > escrowLamports) {
68
+ throw new Error("payment + fee exceeds escrow");
69
+ }
70
+ return { payment, fee, remainder };
71
+ }
72
+ /** Challenge bond = multiplier × escrow, bounded [2, 10] like the program. */
73
+ export function computeChallengeBond(escrowLamports, multiplier) {
74
+ if (multiplier < 2 || multiplier > 10) {
75
+ throw new Error(`bond multiplier ${multiplier} out of [2, 10]`);
76
+ }
77
+ return escrowLamports * BigInt(multiplier);
78
+ }
79
+ /** Treasury/agent split of a slashed bond, mirror of `resolve_challenge.rs::slash_bond`. */
80
+ export function splitSlashedBond(bond, bondSlashTreasuryBps) {
81
+ const treasuryShare = (bond * BigInt(bondSlashTreasuryBps)) / 10000n;
82
+ return { treasuryShare, agentShare: bond - treasuryShare };
83
+ }
84
+ /**
85
+ * The canonical payout derivation. Given a task scenario (its escrow, score,
86
+ * verification kind, and the terminal PATH it took), returns the exact
87
+ * escrow+bond distribution the on-chain program produces. Conserves value:
88
+ * escrow + bond is fully redistributed (asserted before return).
89
+ */
90
+ export function deriveTaskOutcome(s) {
91
+ // Kind-1 (attested) enforces a binary score on-chain — surface a drift early.
92
+ if (s.verificationKind === 1 &&
93
+ s.compositeScore !== 0 &&
94
+ s.compositeScore !== MAX_SCORE) {
95
+ throw new Error(`attested (kind 1) score must be 0 or MAX_SCORE, got ${s.compositeScore}`);
96
+ }
97
+ const { payment, fee, remainder } = computePayment(s.compositeScore, s.qualityThreshold, s.escrowLamports, s.protocolFeeBps);
98
+ const bond = computeChallengeBond(s.escrowLamports, s.challengeBondMultiplier);
99
+ const payout = build(s.outcome, {
100
+ escrow: s.escrowLamports,
101
+ bond,
102
+ payment,
103
+ fee,
104
+ remainder,
105
+ bondSlashTreasuryBps: s.bondSlashTreasuryBps,
106
+ });
107
+ assertConserves(s.outcome, payout, s.escrowLamports, bond);
108
+ return payout;
109
+ }
110
+ function build(kind, p) {
111
+ switch (kind) {
112
+ case TaskOutcomeKind.Finalized:
113
+ return {
114
+ kind,
115
+ agentLamports: p.payment,
116
+ treasuryLamports: p.fee,
117
+ clientLamports: p.remainder,
118
+ challengerLamports: 0n,
119
+ };
120
+ case TaskOutcomeKind.ResolvedAgentWins: {
121
+ const { treasuryShare, agentShare } = splitSlashedBond(p.bond, p.bondSlashTreasuryBps);
122
+ return {
123
+ kind,
124
+ agentLamports: p.payment + agentShare,
125
+ treasuryLamports: p.fee + treasuryShare,
126
+ clientLamports: p.remainder,
127
+ challengerLamports: 0n,
128
+ };
129
+ }
130
+ case TaskOutcomeKind.ResolvedChallengerWins:
131
+ return {
132
+ kind,
133
+ agentLamports: 0n,
134
+ treasuryLamports: 0n,
135
+ clientLamports: p.escrow,
136
+ challengerLamports: p.bond,
137
+ };
138
+ case TaskOutcomeKind.DefaultResolved:
139
+ return {
140
+ kind,
141
+ agentLamports: p.payment,
142
+ treasuryLamports: p.fee,
143
+ clientLamports: p.remainder,
144
+ challengerLamports: p.bond,
145
+ };
146
+ case TaskOutcomeKind.Expired:
147
+ return {
148
+ kind,
149
+ agentLamports: 0n,
150
+ treasuryLamports: 0n,
151
+ clientLamports: p.escrow,
152
+ challengerLamports: 0n,
153
+ };
154
+ default:
155
+ throw new Error(`unknown TaskOutcomeKind ${kind}`);
156
+ }
157
+ }
158
+ /** Every path redistributes exactly (escrow) or (escrow + bond) of value. */
159
+ function assertConserves(kind, o, escrow, bond) {
160
+ const total = o.agentLamports +
161
+ o.treasuryLamports +
162
+ o.clientLamports +
163
+ o.challengerLamports;
164
+ // A challenge occurred (bond posted) in every kind except Finalized/Expired.
165
+ const bondInPlay = kind === TaskOutcomeKind.ResolvedAgentWins ||
166
+ kind === TaskOutcomeKind.ResolvedChallengerWins ||
167
+ kind === TaskOutcomeKind.DefaultResolved;
168
+ const expected = bondInPlay ? escrow + bond : escrow;
169
+ if (total !== expected) {
170
+ throw new Error(`payout conservation broken for ${TaskOutcomeKind[kind]}: ${total} != ${expected}`);
171
+ }
172
+ }
@@ -0,0 +1,27 @@
1
+ import { Connection, PublicKey, TransactionInstruction, VersionedTransaction } from "@solana/web3.js";
2
+ import { type Network } from "./index.js";
3
+ export type SecpSignature = {
4
+ ethAddress: Uint8Array;
5
+ signature: Uint8Array;
6
+ message: Uint8Array;
7
+ recoveryId: number;
8
+ oracleIdx: number;
9
+ };
10
+ /** Build Solana's compact multi-signature secp256k1 verification instruction. */
11
+ export declare function buildSecp256k1Instruction(signatures: readonly SecpSignature[], instructionIndex: number): TransactionInstruction;
12
+ export interface SwitchboardVerifyRequest {
13
+ connection: Connection;
14
+ taskId: string;
15
+ payer: PublicKey;
16
+ taskPda: PublicKey;
17
+ feed: PublicKey;
18
+ compositeScore: bigint;
19
+ verificationHash: Uint8Array;
20
+ network: Network;
21
+ }
22
+ /**
23
+ * Fetch Switchboard consensus with the task-id variable override and build the
24
+ * exact v0 bundle: bounded compute budget, secp256k1 proof, feed update, then
25
+ * Shillbot verify. The returned transaction is unsigned.
26
+ */
27
+ export declare function buildSwitchboardCrankAndVerify(request: SwitchboardVerifyRequest): Promise<VersionedTransaction>;