@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,205 @@
1
+ import * as anchor from "@coral-xyz/anchor";
2
+ import { Buffer } from "buffer";
3
+ import { OracleJob } from "@switchboard-xyz/common/protos";
4
+ import { ComputeBudgetProgram, Keypair, PublicKey, Secp256k1Program, SystemProgram, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js";
5
+ import { buildInstruction } from "./index.js";
6
+ const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
7
+ const SPL_TOKEN_PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
8
+ const SOL_NATIVE_MINT = new PublicKey("So11111111111111111111111111111111111111112");
9
+ const SYSVAR_SLOT_HASHES = new PublicKey("SysvarS1otHashes111111111111111111111111111");
10
+ const SYSVAR_INSTRUCTIONS = new PublicKey("Sysvar1nstructions1111111111111111111111111");
11
+ const SWITCHBOARD_PROGRAMS = {
12
+ mainnet: new PublicKey("SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv"),
13
+ devnet: new PublicKey("Aio4gaXjXzJNVLtzwtNVmSqGKpANtXhybbkhtAC94ji2"),
14
+ };
15
+ const CROSSBAR_URL = "https://crossbar.switchboard.xyz";
16
+ const SWITCHBOARD_GATEWAY_API_VERSION = "1.0.0";
17
+ const anchorRuntime = anchor.default ?? anchor;
18
+ async function jsonResponse(response, description) {
19
+ if (!response.ok) {
20
+ throw new Error(`${description} failed with HTTP ${response.status}`);
21
+ }
22
+ return (await response.json());
23
+ }
24
+ function encodeOracleJob(job) {
25
+ const encoded = OracleJob.encodeDelimited(OracleJob.fromObject(job)).finish();
26
+ return Buffer.from(encoded).toString("base64");
27
+ }
28
+ async function fetchConsensus(network, jobs, maxVariance, minResponses, taskId) {
29
+ const gateways = await jsonResponse(await fetch(`${CROSSBAR_URL}/gateways?network=${network}`), "Switchboard gateway discovery");
30
+ const gateway = gateways[0];
31
+ if (!gateway)
32
+ throw new Error(`no Switchboard gateways for ${network}`);
33
+ return jsonResponse(await fetch(`${gateway.replace(/\/$/, "")}/gateway/api/v1/fetch_signatures_consensus`, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({
37
+ api_version: SWITCHBOARD_GATEWAY_API_VERSION,
38
+ recent_hash: "",
39
+ signature_scheme: "Secp256k1",
40
+ hash_scheme: "Sha256",
41
+ feed_requests: [
42
+ {
43
+ jobs_b64_encoded: jobs.map(encodeOracleJob),
44
+ max_variance: Math.floor(maxVariance * 1e9),
45
+ min_responses: minResponses,
46
+ },
47
+ ],
48
+ num_oracles: 1,
49
+ use_timestamp: false,
50
+ use_ed25519: false,
51
+ variable_overrides: { TASK_ID: taskId },
52
+ }),
53
+ }), "Switchboard consensus request");
54
+ }
55
+ function writeUInt16LE(value) {
56
+ if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
57
+ throw new Error(`invalid secp256k1 offset: ${value}`);
58
+ }
59
+ const bytes = new Uint8Array(2);
60
+ new DataView(bytes.buffer).setUint16(0, value, true);
61
+ return bytes;
62
+ }
63
+ /** Build Solana's compact multi-signature secp256k1 verification instruction. */
64
+ export function buildSecp256k1Instruction(signatures, instructionIndex) {
65
+ if (signatures.length === 0) {
66
+ throw new Error("Switchboard returned no secp256k1 signatures");
67
+ }
68
+ if (!Number.isInteger(instructionIndex) || instructionIndex < 0 || instructionIndex > 0xff) {
69
+ throw new Error("invalid secp256k1 instruction index");
70
+ }
71
+ const sorted = [...signatures].sort((a, b) => a.oracleIdx - b.oracleIdx);
72
+ const message = sorted[0].message;
73
+ if (sorted.some((signature) => !Buffer.from(signature.message).equals(Buffer.from(message)))) {
74
+ throw new Error("all Switchboard signatures must share one message");
75
+ }
76
+ for (const signature of sorted) {
77
+ if (signature.signature.length !== 64)
78
+ throw new Error("invalid secp256k1 signature length");
79
+ if (signature.ethAddress.length !== 20)
80
+ throw new Error("invalid secp256k1 address length");
81
+ if (!Number.isInteger(signature.recoveryId) || signature.recoveryId < 0 || signature.recoveryId > 0xff) {
82
+ throw new Error("invalid secp256k1 recovery id");
83
+ }
84
+ }
85
+ const offsetsSize = 1 + sorted.length * 11;
86
+ const signatureBlockSize = 64 + 1 + 20;
87
+ const messageOffset = offsetsSize + sorted.length * signatureBlockSize;
88
+ const data = new Uint8Array(messageOffset + message.length);
89
+ data[0] = sorted.length;
90
+ let blockOffset = offsetsSize;
91
+ sorted.forEach((signature, index) => {
92
+ const offset = 1 + index * 11;
93
+ data.set(writeUInt16LE(blockOffset), offset);
94
+ data[offset + 2] = instructionIndex;
95
+ data.set(writeUInt16LE(blockOffset + 65), offset + 3);
96
+ data[offset + 5] = instructionIndex;
97
+ data.set(writeUInt16LE(messageOffset), offset + 6);
98
+ data.set(writeUInt16LE(message.length), offset + 8);
99
+ data[offset + 10] = instructionIndex;
100
+ data.set(signature.signature, blockOffset);
101
+ data[blockOffset + 64] = signature.recoveryId;
102
+ data.set(signature.ethAddress, blockOffset + 65);
103
+ blockOffset += signatureBlockSize;
104
+ });
105
+ data.set(message, messageOffset);
106
+ return new TransactionInstruction({
107
+ programId: Secp256k1Program.programId,
108
+ keys: [],
109
+ data: Buffer.from(data),
110
+ });
111
+ }
112
+ function associatedTokenAddress(mint, owner) {
113
+ return PublicKey.findProgramAddressSync([owner.toBuffer(), SPL_TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID)[0];
114
+ }
115
+ /**
116
+ * Fetch Switchboard consensus with the task-id variable override and build the
117
+ * exact v0 bundle: bounded compute budget, secp256k1 proof, feed update, then
118
+ * Shillbot verify. The returned transaction is unsigned.
119
+ */
120
+ export async function buildSwitchboardCrankAndVerify(request) {
121
+ const switchboardProgramId = SWITCHBOARD_PROGRAMS[request.network];
122
+ const dummy = Keypair.generate();
123
+ const provider = new anchorRuntime.AnchorProvider(request.connection, new anchorRuntime.Wallet(dummy), { commitment: "confirmed" });
124
+ const idl = await anchorRuntime.Program.fetchIdl(switchboardProgramId, provider);
125
+ if (!idl)
126
+ throw new Error("failed to fetch Switchboard IDL");
127
+ const program = new anchorRuntime.Program(idl, provider);
128
+ const switchboardAccounts = program.account;
129
+ const feedData = (await switchboardAccounts.pullFeedAccountData.fetch(request.feed));
130
+ const feedHashHex = Buffer.from(feedData.feedHash).toString("hex");
131
+ const crossbar = (await fetch(`https://crossbar.switchboard.xyz/fetch/${feedHashHex}`).then((response) => response.json()));
132
+ const jobs = crossbar.jobs ?? [];
133
+ if (jobs.length === 0)
134
+ throw new Error(`no Switchboard jobs for feed ${feedHashHex}`);
135
+ const response = await fetchConsensus(request.network, jobs, feedData.maxVariance.toNumber() / 1e9, feedData.minResponses, request.taskId);
136
+ if (!response.oracle_responses?.length) {
137
+ throw new Error("Switchboard gateway returned no oracle responses");
138
+ }
139
+ const compute = [
140
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }),
141
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
142
+ ];
143
+ const secp = buildSecp256k1Instruction(response.oracle_responses.map((oracle, oracleIdx) => ({
144
+ ethAddress: Buffer.from(oracle.eth_address, "hex"),
145
+ signature: Buffer.from(oracle.signature, "base64"),
146
+ message: Buffer.from(oracle.checksum, "base64"),
147
+ recoveryId: oracle.recovery_id,
148
+ oracleIdx,
149
+ })), compute.length);
150
+ const queuePubkey = feedData.queue;
151
+ const oraclePubkeys = response.oracle_responses.map((oracle) => new PublicKey(Buffer.from(oracle.oracle_pubkey, "hex")));
152
+ const oracleStats = oraclePubkeys.map((oracle) => PublicKey.findProgramAddressSync([Buffer.from("OracleStats"), oracle.toBuffer()], switchboardProgramId)[0]);
153
+ const feeds = response.median_responses.map((median) => median.feed_hash === feedHashHex ? request.feed : PublicKey.default);
154
+ const submit = program.instruction.pullFeedSubmitResponseConsensus({
155
+ slot: new anchorRuntime.BN(response.slot),
156
+ values: response.median_responses.map((median) => new anchorRuntime.BN(median.value)),
157
+ }, {
158
+ accounts: {
159
+ queue: queuePubkey,
160
+ programState: PublicKey.findProgramAddressSync([Buffer.from("STATE")], switchboardProgramId)[0],
161
+ recentSlothashes: SYSVAR_SLOT_HASHES,
162
+ payer: request.payer,
163
+ systemProgram: SystemProgram.programId,
164
+ rewardVault: associatedTokenAddress(SOL_NATIVE_MINT, queuePubkey),
165
+ tokenProgram: SPL_TOKEN_PROGRAM_ID,
166
+ tokenMint: SOL_NATIVE_MINT,
167
+ ixSysvar: SYSVAR_INSTRUCTIONS,
168
+ },
169
+ remainingAccounts: [
170
+ ...feeds.map((pubkey) => ({
171
+ pubkey,
172
+ isSigner: false,
173
+ isWritable: true,
174
+ })),
175
+ ...oraclePubkeys.map((pubkey) => ({
176
+ pubkey,
177
+ isSigner: false,
178
+ isWritable: false,
179
+ })),
180
+ ...oracleStats.map((pubkey) => ({
181
+ pubkey,
182
+ isSigner: false,
183
+ isWritable: true,
184
+ })),
185
+ ],
186
+ });
187
+ const verify = buildInstruction({
188
+ action: "verify",
189
+ wallet: request.payer.toBase58(),
190
+ network: request.network,
191
+ recentBlockhash: PublicKey.default.toBase58(),
192
+ taskPda: request.taskPda.toBase58(),
193
+ switchboardFeed: request.feed.toBase58(),
194
+ compositeScore: request.compositeScore.toString(),
195
+ verificationHash: Buffer.from(request.verificationHash).toString("hex"),
196
+ crankInstructions: [],
197
+ });
198
+ const { blockhash } = await request.connection.getLatestBlockhash("confirmed");
199
+ const message = new TransactionMessage({
200
+ payerKey: request.payer,
201
+ recentBlockhash: blockhash,
202
+ instructions: [...compute, secp, submit, verify],
203
+ }).compileToV0Message();
204
+ return new VersionedTransaction(message);
205
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * extension-registry `Extension` account decoder — the mund-creanc-witer
3
+ * obligation edge (extender vouches for recipient, backed by a bonded SOL
4
+ * stake). Mirrors `programs/extension-registry/src/state/extension.rs` exactly.
5
+ *
6
+ * Unlike Shillbot's `Task`, an `Extension` has NO on-chain state enum: the
7
+ * account exists only while the vouch is LIVE. Both terminal transitions
8
+ * close it — `withdraw_extension` (extender reclaims bond) and
9
+ * `default_extension` (slash to treasury) — emitting `ExtensionWithdrawn` /
10
+ * `ExtensionDefaulted` events. So a present account is always "active"; a closed
11
+ * one is terminal (the verifier returns `account_closed`), exactly mirroring a
12
+ * finalized Task. The durable terminal history is the event stream, not an
13
+ * account.
14
+ *
15
+ * NB: this exposes a typed decoder + discriminator/state helpers for downstream
16
+ * consumers (the web-position indexer, an extension-aware verifier). It is NOT a
17
+ * drop-in `ProtocolHandler`: that interface's `DecodedAccount` is Task-shaped
18
+ * (task_id, composite_score, content hashes…), and `verify.ts` step 5 compares
19
+ * those Task fields directly. Verifying an Extension *attestation* end-to-end
20
+ * therefore needs the verifier's field-equality core generalized per
21
+ * account_kind (and matching wire fields) — a deliberate change beyond this
22
+ * decoder, tracked separately.
23
+ *
24
+ * Body offsets (after the 8-byte Anchor discriminator):
25
+ *
26
+ * off size field
27
+ * 0 32 extender (Pubkey)
28
+ * 32 32 recipient (Pubkey)
29
+ * 64 1 extension_type (u8)
30
+ * 65 8 bond_lamports (u64 LE)
31
+ * 73 8 created_at (i64 LE, unix seconds)
32
+ * 81 1 bump
33
+ * 82 16 _reserved (skipped)
34
+ *
35
+ * Body = 98 bytes; full account (with discriminator) = 106 = `Extension::SPACE`.
36
+ */
37
+ export interface ExtensionRecord {
38
+ extender: string;
39
+ recipient: string;
40
+ extension_type: number;
41
+ bond_lamports: bigint;
42
+ created_at: bigint;
43
+ bump: number;
44
+ }
45
+ /** The Anchor account name (→ discriminator `sha256("account:Extension")[0..8]`). */
46
+ export declare const EXTENSION_ACCOUNT_KIND = "Extension";
47
+ /** Minimum body length carrying every decoded field (through `bump`). */
48
+ export declare const EXTENSION_MIN_BODY_LEN = 82;
49
+ /** A live Extension has no state byte — existence == active. */
50
+ export declare const EXTENSION_ACTIVE_STATE = "active";
51
+ /**
52
+ * Decode an `Extension` account body (the bytes AFTER the 8-byte discriminator).
53
+ * Throws on the wrong `accountKind` or a body too short for the decoded fields.
54
+ */
55
+ export declare function decodeExtension(data: Uint8Array, accountKind: string): ExtensionRecord;
56
+ /**
57
+ * Extension-type discriminant → name. Mirrors the taxonomy in
58
+ * `programs/extension-registry/src/constants.rs`. MVP records only
59
+ * CapabilityValidation (0); the rest are reserved for weaker-verifiability
60
+ * handling. Returns `null` for unknown discriminants.
61
+ */
62
+ export declare function resolveExtensionType(extensionType: number): string | null;
@@ -0,0 +1,48 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ /** The Anchor account name (→ discriminator `sha256("account:Extension")[0..8]`). */
3
+ export const EXTENSION_ACCOUNT_KIND = "Extension";
4
+ /** Minimum body length carrying every decoded field (through `bump`). */
5
+ export const EXTENSION_MIN_BODY_LEN = 82;
6
+ /** A live Extension has no state byte — existence == active. */
7
+ export const EXTENSION_ACTIVE_STATE = "active";
8
+ /**
9
+ * Decode an `Extension` account body (the bytes AFTER the 8-byte discriminator).
10
+ * Throws on the wrong `accountKind` or a body too short for the decoded fields.
11
+ */
12
+ export function decodeExtension(data, accountKind) {
13
+ if (accountKind !== EXTENSION_ACCOUNT_KIND) {
14
+ throw new Error(`decodeExtension only handles account_kind="Extension", got "${accountKind}"`);
15
+ }
16
+ if (data.length < EXTENSION_MIN_BODY_LEN) {
17
+ throw new Error(`Extension body too short: ${data.length} bytes (need >= ${EXTENSION_MIN_BODY_LEN})`);
18
+ }
19
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
20
+ return {
21
+ extender: new PublicKey(data.slice(0, 32)).toBase58(),
22
+ recipient: new PublicKey(data.slice(32, 64)).toBase58(),
23
+ extension_type: data[64],
24
+ bond_lamports: view.getBigUint64(65, true),
25
+ created_at: view.getBigInt64(73, true),
26
+ bump: data[81],
27
+ };
28
+ }
29
+ /**
30
+ * Extension-type discriminant → name. Mirrors the taxonomy in
31
+ * `programs/extension-registry/src/constants.rs`. MVP records only
32
+ * CapabilityValidation (0); the rest are reserved for weaker-verifiability
33
+ * handling. Returns `null` for unknown discriminants.
34
+ */
35
+ export function resolveExtensionType(extensionType) {
36
+ switch (extensionType) {
37
+ case 0:
38
+ return "capability_validation";
39
+ case 1:
40
+ return "mentorship";
41
+ case 2:
42
+ return "sponsorship";
43
+ case 3:
44
+ return "validation";
45
+ default:
46
+ return null;
47
+ }
48
+ }
@@ -0,0 +1,59 @@
1
+ import type { AccountDecoder, ProtocolHandler, StateResolver } from "../types.js";
2
+ /**
3
+ * Shillbot Task account decoder (post-#12 layout).
4
+ *
5
+ * Accepts the on-chain Task account body — i.e. the bytes AFTER the
6
+ * 8-byte Anchor discriminator. Layout mirrors
7
+ * `programs/shillbot/src/state/task.rs::Task` exactly. Only the
8
+ * fields the VOW verifier needs are decoded; the rest of the body
9
+ * (escrow_lamports, deadlines, overrides, _reserved, bump) is
10
+ * skipped via byte offsets.
11
+ *
12
+ * Field offsets within the body (post-discriminator):
13
+ *
14
+ * off size field
15
+ * 0 8 task_id (u64 LE)
16
+ * 8 32 client (Pubkey)
17
+ * 40 32 agent (Pubkey)
18
+ * 72 1 state (TaskState u8)
19
+ * 73 1 platform (u8)
20
+ * 74 8 escrow_lamports (u64 LE) skipped
21
+ * 82 32 content_hash (sha256)
22
+ * 114 32 content_id_hash (sha256)
23
+ * 146 16 task_nonce (skipped)
24
+ * 162 8 composite_score (u64 LE)
25
+ * 170 8 payment_amount (skipped)
26
+ * 178 8 fee_amount (skipped)
27
+ * 186 8 deadline (skipped)
28
+ * 194 8 submit_margin (skipped)
29
+ * 202 8 claim_buffer (skipped)
30
+ * 210 8 created_at (skipped)
31
+ * 218 8 submitted_at (skipped)
32
+ * 226 8 verified_at (i64 LE)
33
+ * 234 8 challenge_deadline (skipped)
34
+ * 242 12 three u32 overrides (skipped)
35
+ * 254 32 verification_hash (sha256)
36
+ * 286 1 requires_approval (skipped; D1 — carved from _reserved)
37
+ * 287 1 verification_kind (skipped; 2026-07-07 — carved from _reserved)
38
+ * 288 18 _reserved (skipped)
39
+ * 306 1 bump (skipped)
40
+ * 307 32 payout_to (skipped; C2 — appended after bump)
41
+ *
42
+ * Total body = 339 bytes. With the 8-byte discriminator, the full
43
+ * account is 347 bytes (matches `Task::SPACE`); pre-C2 accounts are 315
44
+ * bytes until `migrate_task` reallocs them — the decoder only reads
45
+ * fields below offset 286, so both sizes decode identically.
46
+ */
47
+ export declare const decodeShillbotTask: AccountDecoder;
48
+ /**
49
+ * Shillbot TaskState enum → lowercase name. Mirrors
50
+ * `programs/shillbot/src/state/task.rs::TaskState` discriminants
51
+ * exactly. `Approved = 7` was appended in Phase 3 blocker #3a.
52
+ */
53
+ export declare const resolveShillbotState: StateResolver;
54
+ /** Shillbot's "states valid for attestation" policy, per `docs/specs/vow-v1.md`
55
+ * §6 capture window: only Verified is attestable. Finalize closes the on-chain
56
+ * account, so post-finalize the attestation is permanently unverifiable. */
57
+ export declare const shillbotValidStates: (accountKind: string) => readonly string[];
58
+ /** Convenience bundle for callers that just want the canonical Shillbot triple. */
59
+ export declare const shillbotProtocol: ProtocolHandler;
@@ -0,0 +1,130 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ /**
3
+ * Shillbot Task account decoder (post-#12 layout).
4
+ *
5
+ * Accepts the on-chain Task account body — i.e. the bytes AFTER the
6
+ * 8-byte Anchor discriminator. Layout mirrors
7
+ * `programs/shillbot/src/state/task.rs::Task` exactly. Only the
8
+ * fields the VOW verifier needs are decoded; the rest of the body
9
+ * (escrow_lamports, deadlines, overrides, _reserved, bump) is
10
+ * skipped via byte offsets.
11
+ *
12
+ * Field offsets within the body (post-discriminator):
13
+ *
14
+ * off size field
15
+ * 0 8 task_id (u64 LE)
16
+ * 8 32 client (Pubkey)
17
+ * 40 32 agent (Pubkey)
18
+ * 72 1 state (TaskState u8)
19
+ * 73 1 platform (u8)
20
+ * 74 8 escrow_lamports (u64 LE) skipped
21
+ * 82 32 content_hash (sha256)
22
+ * 114 32 content_id_hash (sha256)
23
+ * 146 16 task_nonce (skipped)
24
+ * 162 8 composite_score (u64 LE)
25
+ * 170 8 payment_amount (skipped)
26
+ * 178 8 fee_amount (skipped)
27
+ * 186 8 deadline (skipped)
28
+ * 194 8 submit_margin (skipped)
29
+ * 202 8 claim_buffer (skipped)
30
+ * 210 8 created_at (skipped)
31
+ * 218 8 submitted_at (skipped)
32
+ * 226 8 verified_at (i64 LE)
33
+ * 234 8 challenge_deadline (skipped)
34
+ * 242 12 three u32 overrides (skipped)
35
+ * 254 32 verification_hash (sha256)
36
+ * 286 1 requires_approval (skipped; D1 — carved from _reserved)
37
+ * 287 1 verification_kind (skipped; 2026-07-07 — carved from _reserved)
38
+ * 288 18 _reserved (skipped)
39
+ * 306 1 bump (skipped)
40
+ * 307 32 payout_to (skipped; C2 — appended after bump)
41
+ *
42
+ * Total body = 339 bytes. With the 8-byte discriminator, the full
43
+ * account is 347 bytes (matches `Task::SPACE`); pre-C2 accounts are 315
44
+ * bytes until `migrate_task` reallocs them — the decoder only reads
45
+ * fields below offset 286, so both sizes decode identically.
46
+ */
47
+ export const decodeShillbotTask = (data, accountKind) => {
48
+ if (accountKind !== "Task") {
49
+ throw new Error(`decodeShillbotTask only handles account_kind="Task", got "${accountKind}"`);
50
+ }
51
+ if (data.length < 286) {
52
+ throw new Error(`Shillbot Task body too short: ${data.length} bytes (need >= 286 for the VOW-relevant fields)`);
53
+ }
54
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
55
+ const task_id = view.getBigUint64(0, /* littleEndian */ true);
56
+ const client = new PublicKey(data.slice(8, 40)).toBase58();
57
+ const agent = new PublicKey(data.slice(40, 72)).toBase58();
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).
63
+ const content_hash = bytesToHex(data.slice(82, 114));
64
+ const content_id_hash = bytesToHex(data.slice(114, 146));
65
+ const composite_score = view.getBigUint64(162, true);
66
+ const verified_at = view.getBigInt64(226, true);
67
+ const verification_hash = bytesToHex(data.slice(254, 286));
68
+ const decoded = {
69
+ task_id,
70
+ client,
71
+ agent,
72
+ state,
73
+ composite_score,
74
+ verified_at,
75
+ verification_hash,
76
+ content_hash,
77
+ content_id_hash,
78
+ };
79
+ return decoded;
80
+ };
81
+ /**
82
+ * Shillbot TaskState enum → lowercase name. Mirrors
83
+ * `programs/shillbot/src/state/task.rs::TaskState` discriminants
84
+ * exactly. `Approved = 7` was appended in Phase 3 blocker #3a.
85
+ */
86
+ export const resolveShillbotState = (state, accountKind) => {
87
+ if (accountKind !== "Task")
88
+ return null;
89
+ switch (state) {
90
+ case 0:
91
+ return "open";
92
+ case 1:
93
+ return "claimed";
94
+ case 2:
95
+ return "submitted";
96
+ case 3:
97
+ return "verified";
98
+ case 4:
99
+ return "finalized";
100
+ case 5:
101
+ return "disputed";
102
+ case 6:
103
+ return "resolved";
104
+ case 7:
105
+ return "approved";
106
+ default:
107
+ return null;
108
+ }
109
+ };
110
+ /** Shillbot's "states valid for attestation" policy, per `docs/specs/vow-v1.md`
111
+ * §6 capture window: only Verified is attestable. Finalize closes the on-chain
112
+ * account, so post-finalize the attestation is permanently unverifiable. */
113
+ export const shillbotValidStates = (accountKind) => {
114
+ if (accountKind !== "Task")
115
+ return [];
116
+ return ["verified"];
117
+ };
118
+ /** Convenience bundle for callers that just want the canonical Shillbot triple. */
119
+ export const shillbotProtocol = {
120
+ decode: decodeShillbotTask,
121
+ resolveState: resolveShillbotState,
122
+ validStates: shillbotValidStates,
123
+ };
124
+ function bytesToHex(b) {
125
+ let s = "";
126
+ for (let i = 0; i < b.length; i++) {
127
+ s += b[i].toString(16).padStart(2, "0");
128
+ }
129
+ return s;
130
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Anchor account discriminator: sha256("account:" + name)[0..8].
3
+ *
4
+ * Used by step 4 of the verification protocol — the first 8 bytes of
5
+ * the on-chain account data MUST equal this for the named
6
+ * `account_kind` under the named `program_id`. Anchor accounts derive
7
+ * their discriminator deterministically from the struct name; the
8
+ * verifier reproduces it without needing the protocol's IDL.
9
+ */
10
+ export declare function anchorDiscriminator(accountKind: string): Uint8Array;
11
+ /** Constant-time byte equality. */
12
+ export declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
@@ -0,0 +1,25 @@
1
+ import { sha256 } from "js-sha256";
2
+ /**
3
+ * Anchor account discriminator: sha256("account:" + name)[0..8].
4
+ *
5
+ * Used by step 4 of the verification protocol — the first 8 bytes of
6
+ * the on-chain account data MUST equal this for the named
7
+ * `account_kind` under the named `program_id`. Anchor accounts derive
8
+ * their discriminator deterministically from the struct name; the
9
+ * verifier reproduces it without needing the protocol's IDL.
10
+ */
11
+ export function anchorDiscriminator(accountKind) {
12
+ const preimage = `account:${accountKind}`;
13
+ const hash = sha256.array(preimage);
14
+ return new Uint8Array(hash.slice(0, 8));
15
+ }
16
+ /** Constant-time byte equality. */
17
+ export function bytesEqual(a, b) {
18
+ if (a.length !== b.length)
19
+ return false;
20
+ let diff = 0;
21
+ for (let i = 0; i < a.length; i++) {
22
+ diff |= a[i] ^ b[i];
23
+ }
24
+ return diff === 0;
25
+ }
@@ -0,0 +1,7 @@
1
+ export type { VowV1Attestation, AasV1Attestation, AccountDecoder, DecodedAccount, FailureReason, ProtocolHandler, StateResolver, Verdict, } from "./types.js";
2
+ export { anchorDiscriminator, bytesEqual } from "./discriminator.js";
3
+ export { checkSchema, asV1 } from "./schema.js";
4
+ export { verifyV1, verifyV1Schema, verifyV1OnChain } from "./verify.js";
5
+ export { decodeShillbotTask, resolveShillbotState, shillbotProtocol, } from "./decoders/shillbot.js";
6
+ export type { ExtensionRecord } from "./decoders/extension.js";
7
+ export { decodeExtension, resolveExtensionType, EXTENSION_ACCOUNT_KIND, EXTENSION_MIN_BODY_LEN, EXTENSION_ACTIVE_STATE, } from "./decoders/extension.js";
@@ -0,0 +1,5 @@
1
+ export { anchorDiscriminator, bytesEqual } from "./discriminator.js";
2
+ export { checkSchema, asV1 } from "./schema.js";
3
+ export { verifyV1, verifyV1Schema, verifyV1OnChain } from "./verify.js";
4
+ export { decodeShillbotTask, resolveShillbotState, shillbotProtocol, } from "./decoders/shillbot.js";
5
+ export { decodeExtension, resolveExtensionType, EXTENSION_ACCOUNT_KIND, EXTENSION_MIN_BODY_LEN, EXTENSION_ACTIVE_STATE, } from "./decoders/extension.js";
@@ -0,0 +1,14 @@
1
+ import type { AasV1Attestation, FailureReason } from "./types.js";
2
+ /**
3
+ * Step 1 — schema check. Implements the well-formed-per-type table
4
+ * from `docs/specs/vow-v1.md` §4. Returns the first failure reason
5
+ * encountered, or null if the schema check passes.
6
+ *
7
+ * Pure — no RPC, no I/O.
8
+ */
9
+ export declare function checkSchema(input: unknown): FailureReason | null;
10
+ /**
11
+ * Once `checkSchema` passes, narrow the input to `AasV1Attestation`.
12
+ * Pure runtime cast — no defensive checks (those already ran).
13
+ */
14
+ export declare function asV1(a: unknown): AasV1Attestation;