@swarm-tips/client 0.0.0 → 0.1.1

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 +99 -6
  60. package/index.js +0 -1
@@ -0,0 +1,33 @@
1
+ import * as anchor from "@coral-xyz/anchor";
2
+ import type { Idl } from "@coral-xyz/anchor";
3
+ import { PublicKey, TransactionInstruction } from "@solana/web3.js";
4
+ import { Buffer } from "buffer";
5
+ declare const BN: typeof anchor.BN;
6
+ export type { CoordinationGame } from "./generated/coordination_game.js";
7
+ export type { Shillbot } from "./generated/shillbot.js";
8
+ /** Canonical raw Anchor IDLs used for runtime instruction encoding. */
9
+ export declare const COORDINATION_GAME_IDL: Idl;
10
+ export declare const SHILLBOT_IDL: Idl;
11
+ export declare const COORDINATION_GAME_PROGRAM_ID: anchor.web3.PublicKey;
12
+ export declare const SHILLBOT_PROGRAM_ID: anchor.web3.PublicKey;
13
+ export { BN };
14
+ export type PublicKeyInput = PublicKey | string;
15
+ export interface BuildShillbotInstructionRequest {
16
+ /** Snake-case instruction name from the canonical raw Anchor IDL. */
17
+ name: string;
18
+ /** Account names from the IDL mapped to their concrete public keys. */
19
+ accounts: Readonly<Record<string, PublicKeyInput>>;
20
+ /** Anchor/Borsh arguments. Integer values wider than u32 must be BN values. */
21
+ args?: Readonly<Record<string, unknown>>;
22
+ }
23
+ export declare function shillbotInstructionDiscriminator(name: string): Buffer;
24
+ /**
25
+ * Encode a Shillbot instruction using the generated IDL as the ABI authority.
26
+ * Account order, signer/writable flags, discriminators, and argument encoding
27
+ * therefore cannot drift independently in downstream clients.
28
+ */
29
+ export declare function buildShillbotInstruction(request: BuildShillbotInstructionRequest): TransactionInstruction;
30
+ export declare function globalStatePda(): PublicKey;
31
+ export declare function taskPda(nonce: bigint, client: PublicKey): PublicKey;
32
+ export declare function clientStatePda(client: PublicKey): PublicKey;
33
+ export declare function agentStatePda(agent: PublicKey): PublicKey;
@@ -0,0 +1,84 @@
1
+ import * as anchor from "@coral-xyz/anchor";
2
+ import { PublicKey, TransactionInstruction } from "@solana/web3.js";
3
+ import { Buffer } from "buffer";
4
+ import coordinationGameIdl from "./idl/coordination_game.json" with { type: "json" };
5
+ import shillbotIdl from "./idl/shillbot.json" with { type: "json" };
6
+ const anchorRuntime = Reflect.get(anchor, "default") ?? anchor;
7
+ const { BN, BorshInstructionCoder } = anchorRuntime;
8
+ /** Canonical raw Anchor IDLs used for runtime instruction encoding. */
9
+ export const COORDINATION_GAME_IDL = coordinationGameIdl;
10
+ export const SHILLBOT_IDL = shillbotIdl;
11
+ export const COORDINATION_GAME_PROGRAM_ID = new PublicKey(COORDINATION_GAME_IDL.address);
12
+ export const SHILLBOT_PROGRAM_ID = new PublicKey(SHILLBOT_IDL.address);
13
+ export { BN };
14
+ const shillbotCoder = new BorshInstructionCoder(SHILLBOT_IDL);
15
+ export function shillbotInstructionDiscriminator(name) {
16
+ const definition = SHILLBOT_IDL.instructions.find((instruction) => instruction.name === name);
17
+ if (!definition)
18
+ throw new Error(`unknown Shillbot instruction: ${name}`);
19
+ return Buffer.from(definition.discriminator);
20
+ }
21
+ function flattenAccounts(items) {
22
+ return items.flatMap((item) => item.accounts ? flattenAccounts(item.accounts) : [item]);
23
+ }
24
+ /**
25
+ * Encode a Shillbot instruction using the generated IDL as the ABI authority.
26
+ * Account order, signer/writable flags, discriminators, and argument encoding
27
+ * therefore cannot drift independently in downstream clients.
28
+ */
29
+ export function buildShillbotInstruction(request) {
30
+ const definition = SHILLBOT_IDL.instructions.find((instruction) => instruction.name === request.name);
31
+ if (!definition) {
32
+ throw new Error(`unknown Shillbot instruction: ${request.name}`);
33
+ }
34
+ const accountDefinitions = flattenAccounts(definition.accounts);
35
+ const knownAccounts = new Set(accountDefinitions.map((account) => account.name));
36
+ const unknownAccount = Object.keys(request.accounts).find((name) => !knownAccounts.has(name));
37
+ if (unknownAccount) {
38
+ throw new Error(`unknown account ${unknownAccount} for Shillbot instruction ${request.name}`);
39
+ }
40
+ const expectedArguments = new Set(definition.args.map((argument) => argument.name));
41
+ const suppliedArguments = Object.keys(request.args ?? {});
42
+ const unknownArgument = suppliedArguments.find((name) => !expectedArguments.has(name));
43
+ if (unknownArgument) {
44
+ throw new Error(`unknown argument ${unknownArgument} for Shillbot instruction ${request.name}`);
45
+ }
46
+ const missingArgument = definition.args.find((argument) => !(argument.name in (request.args ?? {})));
47
+ if (missingArgument) {
48
+ throw new Error(`missing argument ${missingArgument.name} for Shillbot instruction ${request.name}`);
49
+ }
50
+ const keys = accountDefinitions.map((account) => {
51
+ const supplied = request.accounts[account.name];
52
+ const value = supplied ?? account.address;
53
+ if (!value) {
54
+ throw new Error(`missing account ${account.name} for Shillbot instruction ${request.name}`);
55
+ }
56
+ return {
57
+ pubkey: new PublicKey(value),
58
+ isSigner: account.signer === true,
59
+ isWritable: account.writable === true,
60
+ };
61
+ });
62
+ return new TransactionInstruction({
63
+ programId: SHILLBOT_PROGRAM_ID,
64
+ keys,
65
+ data: shillbotCoder.encode(request.name, request.args ?? {}),
66
+ });
67
+ }
68
+ function u64(value) {
69
+ const out = Buffer.alloc(8);
70
+ out.writeBigUInt64LE(value);
71
+ return out;
72
+ }
73
+ export function globalStatePda() {
74
+ return PublicKey.findProgramAddressSync([Buffer.from("shillbot_global")], SHILLBOT_PROGRAM_ID)[0];
75
+ }
76
+ export function taskPda(nonce, client) {
77
+ return PublicKey.findProgramAddressSync([Buffer.from("task"), u64(nonce), client.toBuffer()], SHILLBOT_PROGRAM_ID)[0];
78
+ }
79
+ export function clientStatePda(client) {
80
+ return PublicKey.findProgramAddressSync([Buffer.from("client_state"), client.toBuffer()], SHILLBOT_PROGRAM_ID)[0];
81
+ }
82
+ export function agentStatePda(agent) {
83
+ return PublicKey.findProgramAddressSync([Buffer.from("agent_state"), agent.toBuffer()], SHILLBOT_PROGRAM_ID)[0];
84
+ }
@@ -0,0 +1,40 @@
1
+ import { OutcomeKind, Transcript as OracleInput } from "./outcome-oracle.js";
2
+ /** A realized same-chain game, decoded from the on-chain Game account. */
3
+ export interface RealizedGame {
4
+ matchupType: number;
5
+ p1Guess: number;
6
+ p2Guess: number;
7
+ firstCommitter: number;
8
+ p1Return: bigint;
9
+ p2Return: bigint;
10
+ tournamentGain: bigint;
11
+ stake: bigint;
12
+ }
13
+ /** The oracle input a fully-revealed (terminal) realized game represents.
14
+ * Rejects non-terminal games up front: a 255 sentinel in matchupType/guesses
15
+ * (unrevealed) or a firstCommitter outside {1,2} means the game has NOT fully
16
+ * resolved, and feeding it to the oracle would produce a nonsense verdict
17
+ * instead of a clear error. */
18
+ export declare function realizedTranscript(g: RealizedGame): OracleInput;
19
+ /**
20
+ * (1) Oracle-from-realized-transcript: the on-chain settlement
21
+ * (p1_return / p2_return / tournament_gain) must equal what the shared oracle
22
+ * derives from the realized transcript. The outcome is computed from what
23
+ * actually happened, never assumed — so a variable end state is fine. Returns
24
+ * the derived OutcomeKind.
25
+ */
26
+ export declare function assertPayoffMatchesChain(g: RealizedGame): OutcomeKind;
27
+ /** (2) Conservation: stake is never created or destroyed. */
28
+ export declare function assertConservation(g: RealizedGame): void;
29
+ /**
30
+ * (3) Cross-layer: the outcome OBSERVED on a surface (UI text / MCP get_result)
31
+ * must equal the outcome the chain + oracle agree on — catches a surface that
32
+ * reports a result the chain didn't actually produce.
33
+ */
34
+ export declare function assertOutcomeAgrees(g: RealizedGame, observed: OutcomeKind): void;
35
+ /**
36
+ * Full same-chain verdict in one call: conservation + payoff-from-transcript,
37
+ * plus optional cross-layer if a surface outcome was observed. Returns the
38
+ * derived OutcomeKind so callers can compare runs (metamorphic).
39
+ */
40
+ export declare function verifyRealizedGame(g: RealizedGame, observed?: OutcomeKind): OutcomeKind;
@@ -0,0 +1,82 @@
1
+ // Portable property battery for the coordination game — the path-independent
2
+ // checks over the REALIZED result, shipped in @swarm-tips/client so
3
+ // any e2e (on-chain, Playwright, MCP) verifies the same way. Pure: NO chai, no
4
+ // chain deps — throws plain Errors so it runs anywhere.
5
+ //
6
+ // The point: at the browser/MCP layer the end state is genuinely variable — the
7
+ // matchmaker hides matchup_type and the opponent (grok / another player) picks
8
+ // its own guess. So you don't assert a fixed outcome; you read the realized game
9
+ // from chain after resolution and verify properties that hold for ANY transcript.
10
+ import { OutcomeKind, deriveClaimOutcome, resolvePayoff, TERMINAL_STEP_COUNT, } from "./outcome-oracle.js";
11
+ function fail(msg) {
12
+ throw new Error(`battery: ${msg}`);
13
+ }
14
+ /** The oracle input a fully-revealed (terminal) realized game represents.
15
+ * Rejects non-terminal games up front: a 255 sentinel in matchupType/guesses
16
+ * (unrevealed) or a firstCommitter outside {1,2} means the game has NOT fully
17
+ * resolved, and feeding it to the oracle would produce a nonsense verdict
18
+ * instead of a clear error. */
19
+ export function realizedTranscript(g) {
20
+ if (g.matchupType !== 0 && g.matchupType !== 1)
21
+ fail(`not terminal: matchupType ${g.matchupType} (unset sentinel?)`);
22
+ if (g.p1Guess !== 0 && g.p1Guess !== 1)
23
+ fail(`not terminal: p1Guess ${g.p1Guess} (unrevealed sentinel?)`);
24
+ if (g.p2Guess !== 0 && g.p2Guess !== 1)
25
+ fail(`not terminal: p2Guess ${g.p2Guess} (unrevealed sentinel?)`);
26
+ if (g.firstCommitter !== 1 && g.firstCommitter !== 2)
27
+ fail(`not terminal: firstCommitter ${g.firstCommitter} (expected 1 or 2)`);
28
+ return {
29
+ stepCount: TERMINAL_STEP_COUNT,
30
+ matchupType: g.matchupType,
31
+ p1Guess: g.p1Guess,
32
+ p2Guess: g.p2Guess,
33
+ firstCommitter: g.firstCommitter,
34
+ };
35
+ }
36
+ /**
37
+ * (1) Oracle-from-realized-transcript: the on-chain settlement
38
+ * (p1_return / p2_return / tournament_gain) must equal what the shared oracle
39
+ * derives from the realized transcript. The outcome is computed from what
40
+ * actually happened, never assumed — so a variable end state is fine. Returns
41
+ * the derived OutcomeKind.
42
+ */
43
+ export function assertPayoffMatchesChain(g) {
44
+ const t = realizedTranscript(g);
45
+ const p = resolvePayoff({ ...t, stake: g.stake });
46
+ if (g.p1Return !== p.p1Return)
47
+ fail(`p1_return ${g.p1Return} != oracle ${p.p1Return}`);
48
+ if (g.p2Return !== p.p2Return)
49
+ fail(`p2_return ${g.p2Return} != oracle ${p.p2Return}`);
50
+ if (g.tournamentGain !== p.tournamentGain)
51
+ fail(`tournament_gain ${g.tournamentGain} != oracle ${p.tournamentGain}`);
52
+ return deriveClaimOutcome(t);
53
+ }
54
+ /** (2) Conservation: stake is never created or destroyed. */
55
+ export function assertConservation(g) {
56
+ const total = g.p1Return + g.p2Return + g.tournamentGain;
57
+ const expected = g.stake * 2n;
58
+ if (total !== expected)
59
+ fail(`conservation: ${total} != 2*stake ${expected}`);
60
+ }
61
+ /**
62
+ * (3) Cross-layer: the outcome OBSERVED on a surface (UI text / MCP get_result)
63
+ * must equal the outcome the chain + oracle agree on — catches a surface that
64
+ * reports a result the chain didn't actually produce.
65
+ */
66
+ export function assertOutcomeAgrees(g, observed) {
67
+ const expected = deriveClaimOutcome(realizedTranscript(g));
68
+ if (observed !== expected)
69
+ fail(`cross-layer: surface ${OutcomeKind[observed]} != chain/oracle ${OutcomeKind[expected]}`);
70
+ }
71
+ /**
72
+ * Full same-chain verdict in one call: conservation + payoff-from-transcript,
73
+ * plus optional cross-layer if a surface outcome was observed. Returns the
74
+ * derived OutcomeKind so callers can compare runs (metamorphic).
75
+ */
76
+ export function verifyRealizedGame(g, observed) {
77
+ assertConservation(g);
78
+ const outcome = assertPayoffMatchesChain(g);
79
+ if (observed !== undefined)
80
+ assertOutcomeAgrees(g, observed);
81
+ return outcome;
82
+ }
@@ -0,0 +1,4 @@
1
+ export { COORDINATION_GAME_IDL, COORDINATION_GAME_IDL as IDL, COORDINATION_GAME_PROGRAM_ID, } from "../contracts/index.js";
2
+ export type { CoordinationGame } from "../contracts/index.js";
3
+ export * from "./outcome-oracle.js";
4
+ export * from "./battery.js";
@@ -0,0 +1,3 @@
1
+ export { COORDINATION_GAME_IDL, COORDINATION_GAME_IDL as IDL, COORDINATION_GAME_PROGRAM_ID, } from "../contracts/index.js";
2
+ export * from "./outcome-oracle.js";
3
+ export * from "./battery.js";
@@ -0,0 +1,53 @@
1
+ export declare const GUESS_SAME_TEAM = 0;
2
+ export declare const GUESS_DIFF_TEAM = 1;
3
+ export declare const UNREVEALED = 255;
4
+ export declare const TERMINAL_STEP_COUNT = 4;
5
+ /** Wire-format outcome kinds (0..=8), identical to cert_schema.rs `OutcomeKind`. */
6
+ export declare enum OutcomeKind {
7
+ HomogBothCorrect = 0,
8
+ HomogP1Correct = 1,
9
+ HomogP2Correct = 2,
10
+ BothWrong = 3,
11
+ HeteroP1Wins = 4,
12
+ HeteroP2Wins = 5,
13
+ TimeoutP1Wins = 6,
14
+ TimeoutP2Wins = 7,
15
+ TimeoutBothForfeit = 8
16
+ }
17
+ export interface Transcript {
18
+ /** 0..=4 (TERMINAL_STEP_COUNT), or anything else for a malformed transcript. */
19
+ stepCount: number;
20
+ matchupType: number;
21
+ p1Guess: number;
22
+ p2Guess: number;
23
+ firstCommitter: number;
24
+ }
25
+ /** Mirrors `cert_schema.rs::derive_terminal_outcome` / `CertLib.deriveTerminalOutcome`. */
26
+ export declare function deriveTerminalOutcome(t: Transcript): OutcomeKind;
27
+ /** Mirrors `cert_schema.rs::derive_outcome_kind` / `CertLib.deriveClaimOutcome` (terminal + timeouts). */
28
+ export declare function deriveClaimOutcome(t: Transcript): OutcomeKind;
29
+ export interface Payoff {
30
+ p1Return: bigint;
31
+ p2Return: bigint;
32
+ tournamentGain: bigint;
33
+ }
34
+ /**
35
+ * Mirrors `programs/coordination-game/src/payoff.rs::resolve_game` — the
36
+ * same-chain settlement amounts (lamports). `firstCommitter` must be 1 or 2 in
37
+ * the heterogeneous both-correct case. Conserves stake: the three returns sum to
38
+ * `2 * stake`.
39
+ */
40
+ export declare function resolvePayoff(t: Transcript & {
41
+ stake: bigint;
42
+ }): Payoff;
43
+ /**
44
+ * Split a forfeited tournament gain into (treasury, prize-pool) shares the way
45
+ * the program does at resolution: treasury gets `gain * bps / 10_000` (floored),
46
+ * the prize pool gets the remainder. Bps is bounded to [2000, 8000] on-chain.
47
+ */
48
+ export declare function splitTournamentGain(gain: bigint, treasurySplitBps: number): {
49
+ treasuryGain: bigint;
50
+ prizeGain: bigint;
51
+ };
52
+ /** Matchup type encoded in the last bit of the 32-byte preimage (R_matchup[31] & 1). */
53
+ export declare function matchupTypeFromR(r: Uint8Array | number[]): number;
@@ -0,0 +1,136 @@
1
+ // Outcome oracle — the canonical coordination-game payoff matrix, in TypeScript.
2
+ //
3
+ // This is the off-chain test/tooling mirror of the on-chain derivation logic
4
+ // that three implementations already agree on (pinned by the golden vectors in
5
+ // tests/fixtures/cert-vectors.json):
6
+ //
7
+ // - crates/chain-core/src/cert_schema.rs `Checkpoint::derive_outcome_kind`
8
+ // - evm/src/CertLib.sol `deriveClaimOutcome`
9
+ // - programs/coordination-game/src/payoff.rs `resolve_game`
10
+ //
11
+ // The harness uses it as the single source of truth for "what SHOULD this game
12
+ // resolve to" so every scenario — same-chain Solana, same-chain EVM, cross-chain
13
+ // — asserts on-chain reality against ONE expectation, instead of re-deriving the
14
+ // matrix per test. If this drifts from the on-chain encoders, the combinatorial
15
+ // integration tests (which compare on-chain resolution to this oracle) fail.
16
+ export const GUESS_SAME_TEAM = 0;
17
+ export const GUESS_DIFF_TEAM = 1;
18
+ export const UNREVEALED = 255;
19
+ export const TERMINAL_STEP_COUNT = 4;
20
+ /** Wire-format outcome kinds (0..=8), identical to cert_schema.rs `OutcomeKind`. */
21
+ export var OutcomeKind;
22
+ (function (OutcomeKind) {
23
+ OutcomeKind[OutcomeKind["HomogBothCorrect"] = 0] = "HomogBothCorrect";
24
+ OutcomeKind[OutcomeKind["HomogP1Correct"] = 1] = "HomogP1Correct";
25
+ OutcomeKind[OutcomeKind["HomogP2Correct"] = 2] = "HomogP2Correct";
26
+ OutcomeKind[OutcomeKind["BothWrong"] = 3] = "BothWrong";
27
+ OutcomeKind[OutcomeKind["HeteroP1Wins"] = 4] = "HeteroP1Wins";
28
+ OutcomeKind[OutcomeKind["HeteroP2Wins"] = 5] = "HeteroP2Wins";
29
+ OutcomeKind[OutcomeKind["TimeoutP1Wins"] = 6] = "TimeoutP1Wins";
30
+ OutcomeKind[OutcomeKind["TimeoutP2Wins"] = 7] = "TimeoutP2Wins";
31
+ OutcomeKind[OutcomeKind["TimeoutBothForfeit"] = 8] = "TimeoutBothForfeit";
32
+ })(OutcomeKind || (OutcomeKind = {}));
33
+ /** Mirrors `cert_schema.rs::derive_terminal_outcome` / `CertLib.deriveTerminalOutcome`. */
34
+ export function deriveTerminalOutcome(t) {
35
+ const p1Correct = t.p1Guess === t.matchupType;
36
+ const p2Correct = t.p2Guess === t.matchupType;
37
+ if (t.matchupType === 0) {
38
+ if (p1Correct && p2Correct)
39
+ return OutcomeKind.HomogBothCorrect;
40
+ if (p1Correct)
41
+ return OutcomeKind.HomogP1Correct;
42
+ if (p2Correct)
43
+ return OutcomeKind.HomogP2Correct;
44
+ return OutcomeKind.BothWrong;
45
+ }
46
+ // Heterogeneous: winner takes both stakes; ties break to the first committer.
47
+ if (!p1Correct && !p2Correct)
48
+ return OutcomeKind.BothWrong;
49
+ if (p1Correct === p2Correct) {
50
+ return t.firstCommitter === 1
51
+ ? OutcomeKind.HeteroP1Wins
52
+ : OutcomeKind.HeteroP2Wins;
53
+ }
54
+ return p1Correct ? OutcomeKind.HeteroP1Wins : OutcomeKind.HeteroP2Wins;
55
+ }
56
+ /** Mirrors `cert_schema.rs::derive_outcome_kind` / `CertLib.deriveClaimOutcome` (terminal + timeouts). */
57
+ export function deriveClaimOutcome(t) {
58
+ if (t.stepCount === TERMINAL_STEP_COUNT)
59
+ return deriveTerminalOutcome(t);
60
+ if (t.stepCount === 1) {
61
+ // One commit landed; the committer wins. Inconsistent committer → both forfeit.
62
+ if (t.firstCommitter === 1)
63
+ return OutcomeKind.TimeoutP1Wins;
64
+ if (t.firstCommitter === 2)
65
+ return OutcomeKind.TimeoutP2Wins;
66
+ return OutcomeKind.TimeoutBothForfeit;
67
+ }
68
+ if (t.stepCount === 3) {
69
+ // Both committed, exactly one revealed: the revealer wins. Both-set or
70
+ // both-unset is inconsistent → both forfeit.
71
+ const p1Revealed = t.p1Guess !== UNREVEALED;
72
+ const p2Revealed = t.p2Guess !== UNREVEALED;
73
+ if (p1Revealed === p2Revealed)
74
+ return OutcomeKind.TimeoutBothForfeit;
75
+ return p1Revealed ? OutcomeKind.TimeoutP1Wins : OutcomeKind.TimeoutP2Wins;
76
+ }
77
+ // step 0 (nobody committed) / step 2 (both committed, none revealed).
78
+ return OutcomeKind.TimeoutBothForfeit;
79
+ }
80
+ /**
81
+ * Mirrors `programs/coordination-game/src/payoff.rs::resolve_game` — the
82
+ * same-chain settlement amounts (lamports). `firstCommitter` must be 1 or 2 in
83
+ * the heterogeneous both-correct case. Conserves stake: the three returns sum to
84
+ * `2 * stake`.
85
+ */
86
+ export function resolvePayoff(t) {
87
+ if (t.stake <= 0n)
88
+ throw new Error("stake must be positive");
89
+ const two = t.stake * 2n;
90
+ const half = t.stake / 2n;
91
+ const payoff = t.matchupType === 0
92
+ ? resolveHomogeneous(t.p1Guess, t.p2Guess, t.stake, half, two)
93
+ : resolveHeterogeneous(t.p1Guess, t.p2Guess, t.firstCommitter, two);
94
+ const total = payoff.p1Return + payoff.p2Return + payoff.tournamentGain;
95
+ if (total !== two) {
96
+ throw new Error(`lamport conservation broken: ${total} != ${two}`);
97
+ }
98
+ return payoff;
99
+ }
100
+ function resolveHomogeneous(p1Guess, p2Guess, stake, half, two) {
101
+ const p1Correct = p1Guess === GUESS_SAME_TEAM;
102
+ const p2Correct = p2Guess === GUESS_SAME_TEAM;
103
+ if (p1Correct && p2Correct)
104
+ return { p1Return: stake, p2Return: stake, tournamentGain: 0n };
105
+ if (p1Correct)
106
+ return { p1Return: half, p2Return: 0n, tournamentGain: two - half };
107
+ if (p2Correct)
108
+ return { p1Return: 0n, p2Return: half, tournamentGain: two - half };
109
+ return { p1Return: 0n, p2Return: 0n, tournamentGain: two };
110
+ }
111
+ function resolveHeterogeneous(p1Guess, p2Guess, firstCommitter, two) {
112
+ const p1Correct = p1Guess === GUESS_DIFF_TEAM;
113
+ const p2Correct = p2Guess === GUESS_DIFF_TEAM;
114
+ if (!p1Correct && !p2Correct)
115
+ return { p1Return: 0n, p2Return: 0n, tournamentGain: two };
116
+ if (firstCommitter !== 1 && firstCommitter !== 2) {
117
+ throw new Error("first_committer must be 1 or 2");
118
+ }
119
+ const p1Wins = p1Correct === p2Correct ? firstCommitter === 1 : p1Correct;
120
+ return p1Wins
121
+ ? { p1Return: two, p2Return: 0n, tournamentGain: 0n }
122
+ : { p1Return: 0n, p2Return: two, tournamentGain: 0n };
123
+ }
124
+ /**
125
+ * Split a forfeited tournament gain into (treasury, prize-pool) shares the way
126
+ * the program does at resolution: treasury gets `gain * bps / 10_000` (floored),
127
+ * the prize pool gets the remainder. Bps is bounded to [2000, 8000] on-chain.
128
+ */
129
+ export function splitTournamentGain(gain, treasurySplitBps) {
130
+ const treasuryGain = (gain * BigInt(treasurySplitBps)) / 10000n;
131
+ return { treasuryGain, prizeGain: gain - treasuryGain };
132
+ }
133
+ /** Matchup type encoded in the last bit of the 32-byte preimage (R_matchup[31] & 1). */
134
+ export function matchupTypeFromR(r) {
135
+ return r[31] & 1;
136
+ }
@@ -0,0 +1,3 @@
1
+ export { signDigestForRelay } from "./sign.js";
2
+ export { sendUnsignedCall, sendContractCall, type UnsignedEvmCall, type CallSender, } from "./tx.js";
3
+ export { buildWagmiConfig, getEvmTestWalletKey, getEvmRpcOverride, type ChainWithRpcs, type WagmiConfigOpts, } from "./wallet.js";
@@ -0,0 +1,6 @@
1
+ // Product-agnostic EVM frontend primitives shared across
2
+ // Swarm Tips frontends (coordination-game today; a future shillbot EVM flow
3
+ // just adds this package as a dependency + brings its own ABI/flow).
4
+ export { signDigestForRelay } from "./sign.js";
5
+ export { sendUnsignedCall, sendContractCall, } from "./tx.js";
6
+ export { buildWagmiConfig, getEvmTestWalletKey, getEvmRpcOverride, } from "./wallet.js";
@@ -0,0 +1,12 @@
1
+ import type { Hex } from "viem";
2
+ /**
3
+ * Sign a `0x` 32-byte digest with an EVM private key, returning a `0x` 65-byte
4
+ * `[r||s||v]` secp256k1 signature with `v = 0 | 1`. viem's `sign` emits the EVM
5
+ * `ecrecover` convention `v = 27 | 28`; this normalizes it to the k256
6
+ * `recover_address` form the game-api relay (and the Solana program) verify.
7
+ *
8
+ * NOTE: this is the OFF-CHAIN relay direction (27/28 → 0/1). For a signature an
9
+ * EVM CONTRACT verifies, do NOT normalize — viem's 27/28 is already correct
10
+ * (the inverse axis; see chain-core `sign_digest_eth` / `evm_operator_sig_v_convention`).
11
+ */
12
+ export declare function signDigestForRelay(privateKey: Hex, digestHex: Hex): Promise<Hex>;
@@ -0,0 +1,23 @@
1
+ import { sign } from "viem/accounts";
2
+ /**
3
+ * Sign a `0x` 32-byte digest with an EVM private key, returning a `0x` 65-byte
4
+ * `[r||s||v]` secp256k1 signature with `v = 0 | 1`. viem's `sign` emits the EVM
5
+ * `ecrecover` convention `v = 27 | 28`; this normalizes it to the k256
6
+ * `recover_address` form the game-api relay (and the Solana program) verify.
7
+ *
8
+ * NOTE: this is the OFF-CHAIN relay direction (27/28 → 0/1). For a signature an
9
+ * EVM CONTRACT verifies, do NOT normalize — viem's 27/28 is already correct
10
+ * (the inverse axis; see chain-core `sign_digest_eth` / `evm_operator_sig_v_convention`).
11
+ */
12
+ export async function signDigestForRelay(privateKey, digestHex) {
13
+ const serialized = await sign({ hash: digestHex, privateKey, to: "hex" });
14
+ const body = serialized.slice(2); // 130 hex chars: r(64) || s(64) || v(2)
15
+ if (body.length !== 130) {
16
+ throw new Error(`unexpected signature length: ${serialized}`);
17
+ }
18
+ const rs = body.slice(0, 128);
19
+ let v = parseInt(body.slice(128, 130), 16);
20
+ if (v >= 27)
21
+ v -= 27; // 27|28 -> 0|1 for the relay's k256 recover_address
22
+ return `0x${rs}${v.toString(16).padStart(2, "0")}`;
23
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * localStorage key that opts a page into the E2E test connectors. The
3
+ * `?evmTestWallet=`/`?evmRpc=` query params are honored ONLY when this key is
4
+ * set to "1". A query param alone can't activate the test path — see
5
+ * `e2eTestModeEnabled` for why this closes the prod-phishing hole.
6
+ */
7
+ export declare const E2E_TEST_MODE_KEY = "swarm_e2e_test_mode";
8
+ /**
9
+ * Is this page running under the E2E harness?
10
+ *
11
+ * The deployed prod bundle is the SAME bundle the Playwright e2e drives against
12
+ * coordination.game, so `import.meta.env.DEV` is false in both and can't gate
13
+ * the test connectors. We instead require an explicit localStorage opt-in that
14
+ * ONLY the harness can set: Playwright's `addInitScript` runs on the page origin
15
+ * before any page script, so it can seed `localStorage[E2E_TEST_MODE_KEY]`. A
16
+ * phishing link can carry `?evmTestWallet=` query params but CANNOT write to a
17
+ * victim's coordination.game localStorage (it's origin-scoped, not URL-settable),
18
+ * so a real user clicking such a link never activates the private-key connector
19
+ * or the RPC override. Fail-closed: any localStorage access error → disabled.
20
+ */
21
+ export declare function e2eTestModeEnabled(): boolean;
@@ -0,0 +1,34 @@
1
+ // E2E test-mode gate — intentionally dependency-free (only `window.localStorage`)
2
+ // so the Playwright harness can import the flag key via the
3
+ // `@swarm-tips/client/evm/testing`
4
+ // subpath WITHOUT pulling wagmi/viem into the test package.
5
+ /**
6
+ * localStorage key that opts a page into the E2E test connectors. The
7
+ * `?evmTestWallet=`/`?evmRpc=` query params are honored ONLY when this key is
8
+ * set to "1". A query param alone can't activate the test path — see
9
+ * `e2eTestModeEnabled` for why this closes the prod-phishing hole.
10
+ */
11
+ export const E2E_TEST_MODE_KEY = "swarm_e2e_test_mode";
12
+ /**
13
+ * Is this page running under the E2E harness?
14
+ *
15
+ * The deployed prod bundle is the SAME bundle the Playwright e2e drives against
16
+ * coordination.game, so `import.meta.env.DEV` is false in both and can't gate
17
+ * the test connectors. We instead require an explicit localStorage opt-in that
18
+ * ONLY the harness can set: Playwright's `addInitScript` runs on the page origin
19
+ * before any page script, so it can seed `localStorage[E2E_TEST_MODE_KEY]`. A
20
+ * phishing link can carry `?evmTestWallet=` query params but CANNOT write to a
21
+ * victim's coordination.game localStorage (it's origin-scoped, not URL-settable),
22
+ * so a real user clicking such a link never activates the private-key connector
23
+ * or the RPC override. Fail-closed: any localStorage access error → disabled.
24
+ */
25
+ export function e2eTestModeEnabled() {
26
+ if (typeof window === "undefined")
27
+ return false;
28
+ try {
29
+ return window.localStorage.getItem(E2E_TEST_MODE_KEY) === "1";
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
@@ -0,0 +1,27 @@
1
+ import { type Abi, type Hex } from "viem";
2
+ /** An unsigned EVM call the connected wallet signs + submits. */
3
+ export interface UnsignedEvmCall {
4
+ to: Hex;
5
+ data: Hex;
6
+ /** Native value in wei; defaults to 0. */
7
+ value?: bigint;
8
+ }
9
+ /**
10
+ * Any wallet able to submit a prepared transaction — structurally satisfied by a
11
+ * wagmi / viem `WalletClient` (whose `sendTransaction` accepts a superset of
12
+ * these fields). Loosely typed on the argument so the kit doesn't depend on
13
+ * wagmi's full generic `SendTransactionParameters`.
14
+ */
15
+ export interface CallSender {
16
+ sendTransaction: (args: any) => Promise<Hex>;
17
+ }
18
+ /** Submit an unsigned `{to,data,value}` call via the connected wallet. */
19
+ export declare function sendUnsignedCall(wallet: CallSender, call: UnsignedEvmCall): Promise<Hex>;
20
+ /** Encode a contract call (viem `encodeFunctionData`) and submit it. */
21
+ export declare function sendContractCall(wallet: CallSender, args: {
22
+ abi: Abi;
23
+ functionName: string;
24
+ args: readonly unknown[];
25
+ to: Hex;
26
+ value?: bigint;
27
+ }): Promise<Hex>;
package/dist/evm/tx.js ADDED
@@ -0,0 +1,18 @@
1
+ import { encodeFunctionData } from "viem";
2
+ /** Submit an unsigned `{to,data,value}` call via the connected wallet. */
3
+ export function sendUnsignedCall(wallet, call) {
4
+ return wallet.sendTransaction({
5
+ to: call.to,
6
+ data: call.data,
7
+ value: call.value ?? 0n,
8
+ });
9
+ }
10
+ /** Encode a contract call (viem `encodeFunctionData`) and submit it. */
11
+ export function sendContractCall(wallet, args) {
12
+ const data = encodeFunctionData({
13
+ abi: args.abi,
14
+ functionName: args.functionName,
15
+ args: args.args,
16
+ });
17
+ return sendUnsignedCall(wallet, { to: args.to, data, value: args.value });
18
+ }
@@ -0,0 +1,58 @@
1
+ import { type Config, type CreateConnectorFn } from "wagmi";
2
+ import { type Chain, type Hex } from "viem";
3
+ /**
4
+ * Read an E2E test private key from a `?<param>=<0x-hex>` query param, if
5
+ * present (default param `evmTestWallet`). Mirrors the Solana `?testWallet=`
6
+ * pattern so Playwright can drive the EVM path headlessly. Returns null off the
7
+ * happy path (no window, missing/malformed param) AND whenever the page isn't
8
+ * in E2E test mode — so a crafted prod link can't activate the connector.
9
+ */
10
+ export declare function getEvmTestWalletKey(param?: string): Hex | null;
11
+ /**
12
+ * Read an E2E RPC override from `?evmRpc=<https-url>`. Lets Playwright point the
13
+ * page at a reliable provider (e.g. QuickNode) WITHOUT baking the key into the
14
+ * public bundle. Caller gates this to test mode (test wallet present) so a
15
+ * crafted link can't redirect a real user's wallet to a hostile RPC. Only https
16
+ * URLs are accepted. Also gated on E2E test mode (defense in depth — its only
17
+ * caller already requires a test wallet, which is itself test-mode-gated).
18
+ */
19
+ export declare function getEvmRpcOverride(param?: string): string | null;
20
+ export interface WagmiConfigOpts {
21
+ /** Reown/WalletConnect projectId. When set (and not in test mode) the
22
+ * WalletConnect connector is added alongside `injected`, so mobile + hardware
23
+ * + QR wallets can connect. Omit → injected-only (browser extensions still
24
+ * work via EIP-6963). It is a public client id, safe to ship in the bundle.
25
+ *
26
+ * NOTE: registering the raw wagmi connector is NOT sufficient for a
27
+ * RainbowKit UI — RainbowKit's modal renders its own wallet registry and
28
+ * ignores connectors it did not create, so a WalletConnect connector added
29
+ * here never appears as an option. Apps with a RainbowKit modal must pass
30
+ * `connectors` below instead. */
31
+ walletConnectProjectId?: string;
32
+ /** Connectors supplied by the app, used when NOT in test mode. This is how a
33
+ * RainbowKit app injects `connectorsForWallets(...)` output so its modal and
34
+ * the wagmi config agree on the wallet list. Keeping it an opaque array lets
35
+ * evm-kit stay free of any RainbowKit dependency. */
36
+ connectors?: CreateConnectorFn[];
37
+ }
38
+ /**
39
+ * Choose the connector set for a config. Pure, so the precedence rule is
40
+ * unit-testable: the E2E test connector always wins (a live sweep must never be
41
+ * hijacked by a real wallet), then app-supplied connectors, then the built-in
42
+ * injected/WalletConnect pair.
43
+ */
44
+ export declare function selectConnectors<T>(testConnector: T | null, appConnectors: T[] | undefined, builtIn: T[]): T[];
45
+ /** One chain in a multi-chain config, with its own failover RPCs. */
46
+ export interface ChainWithRpcs {
47
+ chain: Chain;
48
+ rpcUrls?: string[];
49
+ }
50
+ /**
51
+ * Build a wagmi config for one or several EVM chains. Multi-chain lets the
52
+ * wallet switch chains (e.g. Base Sepolia ↔ Ethereum Sepolia) so the same site
53
+ * plays each — each chain gets its own resilient transport. The `injected`
54
+ * connector serves humans; the private-key test connector serves E2E when the
55
+ * test-wallet query param is present (bound to the `?evmChain=<caip2>` chain,
56
+ * or the first listed chain).
57
+ */
58
+ export declare function buildWagmiConfig(chains: ChainWithRpcs[], opts?: WagmiConfigOpts): Config;