@flayerlabs/gamemode-gate 0.4.3 → 0.5.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 (68) hide show
  1. package/DEPLOY.md +27 -3
  2. package/README.md +8 -3
  3. package/dist/chain/chains.d.ts +45 -6
  4. package/dist/chain/chains.d.ts.map +1 -1
  5. package/dist/chain/chains.js +56 -5
  6. package/dist/chain/chains.js.map +1 -1
  7. package/dist/chain/discover.d.ts +20 -2
  8. package/dist/chain/discover.d.ts.map +1 -1
  9. package/dist/chain/discover.js +74 -6
  10. package/dist/chain/discover.js.map +1 -1
  11. package/dist/chain/secure.d.ts +11 -1
  12. package/dist/chain/secure.d.ts.map +1 -1
  13. package/dist/chain/secure.js +67 -0
  14. package/dist/chain/secure.js.map +1 -1
  15. package/dist/claims.d.ts +1 -1
  16. package/dist/claims.d.ts.map +1 -1
  17. package/dist/claims.js +2 -2
  18. package/dist/claims.js.map +1 -1
  19. package/dist/demo.d.ts +1 -1
  20. package/dist/demo.d.ts.map +1 -1
  21. package/dist/demo.js +9 -9
  22. package/dist/demo.js.map +1 -1
  23. package/dist/economy.d.ts +25 -9
  24. package/dist/economy.d.ts.map +1 -1
  25. package/dist/economy.js +63 -22
  26. package/dist/economy.js.map +1 -1
  27. package/dist/index.d.ts +7 -6
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/ledger.d.ts +2 -2
  32. package/dist/ledger.d.ts.map +1 -1
  33. package/dist/ledger.js +16 -16
  34. package/dist/ledger.js.map +1 -1
  35. package/dist/main.d.ts +30 -5
  36. package/dist/main.d.ts.map +1 -1
  37. package/dist/main.js +158 -17
  38. package/dist/main.js.map +1 -1
  39. package/dist/room.d.ts +7 -4
  40. package/dist/room.d.ts.map +1 -1
  41. package/dist/room.js +31 -10
  42. package/dist/room.js.map +1 -1
  43. package/dist/server.d.ts +33 -3
  44. package/dist/server.d.ts.map +1 -1
  45. package/dist/server.js +93 -25
  46. package/dist/server.js.map +1 -1
  47. package/dist/standings.d.ts +34 -0
  48. package/dist/standings.d.ts.map +1 -0
  49. package/dist/standings.js +103 -0
  50. package/dist/standings.js.map +1 -0
  51. package/dist/store.d.ts +1 -1
  52. package/dist/store.d.ts.map +1 -1
  53. package/dist/store.js +21 -0
  54. package/dist/store.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/chain/chains.ts +82 -9
  57. package/src/chain/discover.ts +87 -7
  58. package/src/chain/secure.ts +86 -1
  59. package/src/claims.ts +2 -2
  60. package/src/demo.ts +10 -10
  61. package/src/economy.ts +83 -31
  62. package/src/index.ts +8 -5
  63. package/src/ledger.ts +18 -18
  64. package/src/main.ts +181 -19
  65. package/src/room.ts +41 -14
  66. package/src/server.ts +142 -35
  67. package/src/standings.ts +138 -0
  68. package/src/store.ts +21 -0
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+HrB,CAAC;AAEF,MAAM,cAAc,GAAG;;EAErB,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAI,IAAU,EAAE,EAAkC;IACnF,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5B,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,+BAA+B;QACjC,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,OAAO,EAAE,CAAC;IACf,CAAC;AACH,CAAC;AAED,sGAAsG;AACtG,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAU;IACtC,MAAM,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;QACrC,MAAM,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QAC/B,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAyC,EAAU,EAAE,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoJrB,CAAC;AAEF,MAAM,cAAc,GAAG;;EAErB,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAI,IAAU,EAAE,EAAkC;IACnF,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5B,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,+BAA+B;QACjC,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,OAAO,EAAE,CAAC;IACf,CAAC;AACH,CAAC;AAED,sGAAsG;AACtG,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAU;IACtC,MAAM,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;QACrC,MAAM,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QAC/B,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAyC,EAAU,EAAE,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flayerlabs/gamemode-gate",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "Server-authoritative rooms, economy, admission and registry for Flaunch Game Modes",
5
5
  "license": "MIT",
6
6
  "author": "Flayer Labs",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@fastify/cors": "^11.3.0",
47
47
  "@fastify/websocket": "^11.3.0",
48
- "@flayerlabs/gamemode-spec": "^0.4.3",
48
+ "@flayerlabs/gamemode-spec": "^0.5.1",
49
49
  "@types/node": "^22.10.2",
50
50
  "@types/pg": "^8.21.0",
51
51
  "fastify": "^5.11.3",
@@ -13,33 +13,106 @@
13
13
  * gated on the audit and the multisig handover. Robinhood (4663) is deliberately missing too —
14
14
  * that chain is served by the legacy gate, and this library refusing it keeps the two stacks from
15
15
  * being pointed at each other by a copied env file.
16
+ *
17
+ * One gate serves exactly ONE (positionManager, spendGatedCalculator) pair. The EIP-712 domain of
18
+ * every authorisation binds it to the calculator address, so a signature minted for one pair is
19
+ * meaningless to the other — a gate cannot straddle two stacks, and this book records each stack
20
+ * as its own entry rather than pretending a chain has one.
21
+ */
22
+
23
+ /**
24
+ * Which launch generation the stack's PositionManager belongs to.
25
+ *
26
+ * The names match `FlaunchVersion` in `@flaunch/sdk`, because they name the same generations and
27
+ * two vocabularies for one thing is how a gate ends up configured against the wrong ABI. Each
28
+ * generation takes a different `flaunch()` struct, and getting it wrong is an ABI revert at launch
29
+ * time, so it is recorded per stack rather than guessed.
30
+ *
31
+ * v1_2 — the original Base stack. Carries the two fair-launch fields; flETH pairing only.
32
+ * multichain — the multichain deployment, with the fair-launch fields removed.
33
+ * v1_3 — the current generation, launched through the V1_3 FlaunchZap. The only one that
34
+ * can pair a coin with a token other than ETH, and therefore the only one that can
35
+ * run a paired-token round.
16
36
  */
37
+ export type FlaunchVariant = 'v1_2' | 'multichain' | 'v1_3';
17
38
 
18
39
  export interface GateDeployment {
19
40
  positionManager: `0x${string}`;
20
41
  spendGatedCalculator: `0x${string}`;
42
+ flaunchVariant: FlaunchVariant;
43
+ /**
44
+ * The single-pool swap router the calculator has approved, when the stack has one. A paired-token
45
+ * buy goes through it (ERC20 in, `hookData` carrying the authorisation), and the calculator binds
46
+ * the buyer to its `msgSender()` — so an unapproved router means every smart-wallet buy reverts.
47
+ * Required for `v1_3`; boot refuses a paired stack without it.
48
+ */
49
+ poolSwap?: `0x${string}`;
21
50
  /**
22
- * Which `flaunch()` struct the chain's PositionManager takes. V1_2 deployments carry two extra
23
- * fair-launch fields ('legacy11'); the newer nine-field shape is 'current9'. Getting this wrong
24
- * is an ABI revert at launch time, so it is recorded per chain rather than guessed.
51
+ * The chain's flETH wrapper. A coin paired with it is an ETH round priced at `USD_PER_ETH`, and
52
+ * the wrapper is not the zero address, so the gate has to be told which address means "ETH".
25
53
  */
26
- flaunchVariant: 'legacy11' | 'current9';
54
+ fleth?: `0x${string}`;
27
55
  }
28
56
 
57
+ /** The legacy stack per chain — what `deploymentFor(chainId)` answers with no manager named. */
29
58
  export const DEPLOYMENTS: Record<number, GateDeployment> = {
30
59
  /**
31
- * Base Sepolia — the reference chain. Deployed 2026-08-16 (flaunch-contracts
60
+ * Base Sepolia — the reference chain, V1_2 stack. Deployed 2026-08-16 (flaunch-contracts
32
61
  * SpendGatedGate.s.sol); both PositionManager fee-calculator slots were read back holding the
33
62
  * dispatcher, and the calculator resolves each pool's own paired token (VPT-13).
34
63
  */
35
64
  84532: {
36
65
  positionManager: '0x4E7cB1e6800a7B297B38BddcecAF9Ca5b6616FDC',
37
66
  spendGatedCalculator: '0x8cbbE6b4cFA5Ccf68399Dbf1429d91A21097ebA5',
38
- flaunchVariant: 'legacy11',
67
+ flaunchVariant: 'v1_2',
68
+ fleth: '0x79FC52701cD4BE6f9Ba9aDC94c207DE37e3314eb',
39
69
  },
40
70
  };
41
71
 
42
- /** The deployment for a chain, or undefined for one this library has no address book for. */
43
- export function deploymentFor(chainId: number): GateDeployment | undefined {
44
- return DEPLOYMENTS[chainId];
72
+ /**
73
+ * The paired-token stacks per chain — selected by naming their PositionManager in
74
+ * `POSITION_MANAGER`.
75
+ */
76
+ export const PAIRED_DEPLOYMENTS: Record<number, GateDeployment> = {
77
+ /**
78
+ * Base Sepolia — the V1_3 v1.3.3 paired-token stack (flaunch-contracts regeneration, deployed
79
+ * 2026-09-03, blocks 46349133–46349202; `@flaunch/sdk` 0.11.4 launches here). Read back
80
+ * 2026-09-04: the manager's `feeCalculator()` is the dispatcher
81
+ * `0xd381f8ea57df43c57cfe6e5b19a0a4700396f28c`, which has this calculator registered; the
82
+ * calculator grants that dispatcher the PositionManager role and has this PoolSwap approved as a
83
+ * router. This is the stack that hosts a coin paired with MUSD
84
+ * (`0xd0B4398976572515a269C6331A143e08a70E9ccC`, six decimals, approved on the retained registry
85
+ * `0x23cb441d18ca75c6a14964b06806df668d45a1c6`) — and flETH and native ETH pairings too, so a gate
86
+ * on it serves ETH rounds as well.
87
+ *
88
+ * SUPERSEDED, recorded so nobody re-adds it by mistake: the `.vpt2` stack (2026-08-06) —
89
+ * PositionManager `0x5558e7271ec2e8b2faaf05f0eedab1cd986be5dc`, calculator
90
+ * `0x2c9127654ded3b6b2ba017e84f44b02cafdf9f55`, PoolSwap `0x62eb5b7b066ff80ce5e32ff1ed42b31c485f716b`.
91
+ * Its pools keep trading on their own hook and gate, but new paired launches land on the v1.3.3
92
+ * manager, and router approval is PER calculator: `0x62eb…` is NOT approved on the v1.3.3 gate,
93
+ * so pointing this entry at the old router would make every smart-wallet buy revert.
94
+ */
95
+ 84532: {
96
+ positionManager: '0x8D346f24278C5CD786309161aAC0fC2bbe4c25dc',
97
+ spendGatedCalculator: '0x54cdcf0bcbc3a33f470e07134c10582f93058a32',
98
+ flaunchVariant: 'v1_3',
99
+ poolSwap: '0xf0f388a31a1745a5e2378b812ed51525f70595be',
100
+ fleth: '0x79FC52701cD4BE6f9Ba9aDC94c207DE37e3314eb',
101
+ },
102
+ };
103
+
104
+ /**
105
+ * The deployment for a chain, or undefined for one this library has no address book for.
106
+ *
107
+ * With no manager named, the legacy stack. Naming one selects whichever recorded stack it belongs
108
+ * to — so `POSITION_MANAGER=0x8D34…` on Base Sepolia picks the paired stack, calculator and router
109
+ * included, rather than pairing a paired manager with the legacy calculator by accident.
110
+ */
111
+ export function deploymentFor(chainId: number, positionManager?: `0x${string}` | null): GateDeployment | undefined {
112
+ if (!positionManager) return DEPLOYMENTS[chainId];
113
+ const wanted = positionManager.toLowerCase();
114
+ for (const candidate of [DEPLOYMENTS[chainId], PAIRED_DEPLOYMENTS[chainId]]) {
115
+ if (candidate && candidate.positionManager.toLowerCase() === wanted) return candidate;
116
+ }
117
+ return undefined;
45
118
  }
@@ -1,4 +1,5 @@
1
1
  import { encodeAbiParameters, keccak256, parseAbi, type PublicClient } from 'viem';
2
+ import { ETH_SPEND_TOKEN, type SpendToken } from '@flayerlabs/gamemode-spec/live';
2
3
 
3
4
  /**
4
5
  * All this needs of a client: the ability to read a contract.
@@ -20,6 +21,12 @@ export type ChainReader = Pick<PublicClient, 'readContract'>;
20
21
  * launch's own gate parameters, and that is public state the creator paid to write and nobody else
21
22
  * can forge. So adoption needs no API key and no allowlist — anyone may ask, and the chain decides.
22
23
  * That single property is what makes third-party games permissionless.
24
+ *
25
+ * A gate serves exactly one (PositionManager, calculator) pair and adopts only coins launched on
26
+ * THAT manager: `poolKey(coin)` is asked of the configured manager alone, so a coin on a superseded
27
+ * generation of hooks (which keeps trading on its own gate) reads back as not a launch here. That
28
+ * is deliberate — the EIP-712 domain binds every authorisation to the calculator, so a coin this
29
+ * gate cannot sign for must not be given a room by it.
23
30
  */
24
31
 
25
32
  const poolKeyComponents = [
@@ -39,17 +46,28 @@ export const positionManagerAbi = [
39
46
  outputs: [{ name: '', type: 'tuple', components: poolKeyComponents }],
40
47
  },
41
48
  // The scheduled open of the buy window; 0 once the pool is tradeable.
42
- ...parseAbi(['function flaunchesAt(bytes32 _poolId) view returns (uint256)']),
49
+ ...parseAbi([
50
+ 'function flaunchesAt(bytes32 _poolId) view returns (uint256)',
51
+ // V1_3 only: the token the pool is paired with. A legacy manager has no such function; a revert
52
+ // there is read as "flETH", which is the only pairing that generation can produce.
53
+ 'function pairedToken(bytes32 _poolId) view returns (address)',
54
+ ]),
43
55
  ] as const;
44
56
 
45
57
  export const spendGatedAbi = parseAbi([
46
58
  // Four fields. Decoding a four-word return against a shorter ABI is how a check quietly reads the
47
59
  // wrong value, so this must match the DEPLOYED calculator rather than the newest source.
48
- 'function spendGateSettings(bytes32 _poolId) view returns (bool enabled, uint256 walletCapWei, address settler, uint256 endsAt)',
60
+ 'function spendGateSettings(bytes32 _poolId) view returns (bool enabled, uint256 walletCap, address settler, uint256 endsAt)',
49
61
  'function trustedPoolKeySigner(bytes32 _poolId) view returns (address signer, bool enabled)',
50
62
  ]);
51
63
 
52
- const erc20 = parseAbi(['function name() view returns (string)', 'function symbol() view returns (string)']);
64
+ const erc20 = parseAbi([
65
+ 'function name() view returns (string)',
66
+ 'function symbol() view returns (string)',
67
+ 'function decimals() view returns (uint8)',
68
+ ]);
69
+
70
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
53
71
 
54
72
  export interface PoolKey {
55
73
  currency0: `0x${string}`;
@@ -67,11 +85,17 @@ export interface Launch {
67
85
  opensAt: number;
68
86
  /** When the gate stops enforcing on chain, in epoch ms. */
69
87
  closesAt: number;
70
- walletCapWei: bigint;
88
+ /** In base units of `spendToken`. */
89
+ walletCap: bigint;
71
90
  signer: `0x${string}`;
72
91
  name: string;
73
92
  symbol: string;
74
93
  imageUrl?: string | null;
94
+ /**
95
+ * The token spend is measured in: the coin's paired token, read from the manager. Absent only
96
+ * from hand-built launches (demos, tests), where it means ETH.
97
+ */
98
+ spendToken?: SpendToken;
75
99
  }
76
100
 
77
101
  /**
@@ -166,7 +190,7 @@ export async function verifyLaunch(
166
190
  return refuse('launch.unreadable', true);
167
191
  }
168
192
 
169
- const [enabled, walletCapWei, , endsAt] = settings;
193
+ const [enabled, walletCap, , endsAt] = settings;
170
194
  const [signer, signerEnabled] = trusted;
171
195
 
172
196
  if (!enabled) return refuse('launch.not_a_game', false);
@@ -174,12 +198,15 @@ export async function verifyLaunch(
174
198
  if (!signerEnabled || signer.toLowerCase() !== deployment.signer.toLowerCase()) {
175
199
  return refuse('launch.another_signer', false);
176
200
  }
177
- if (walletCapWei <= 0n) return refuse('launch.no_spending_cap', false);
201
+ if (walletCap <= 0n) return refuse('launch.no_spending_cap', false);
178
202
 
179
203
  const opensAt = Number(flaunchesAt) * 1000;
180
204
  const closesAt = Number(endsAt) * 1000;
181
205
  if (closesAt <= Date.now()) return refuse('launch.already_over', false);
182
206
 
207
+ const spendToken = await resolveSpendToken(client, deployment, memecoin, poolKey, poolId);
208
+ if (!spendToken) return refuse('launch.paired_unreadable', true);
209
+
183
210
  let name = '';
184
211
  let symbol = '';
185
212
  try {
@@ -192,7 +219,60 @@ export async function verifyLaunch(
192
219
  // display string would be refusing a round for no reason anyone could act on.
193
220
  }
194
221
 
195
- return { ok: true, coinAddress: memecoin, poolId, poolKey, opensAt, closesAt, walletCapWei, signer, name, symbol };
222
+ return {
223
+ ok: true,
224
+ coinAddress: memecoin,
225
+ poolId,
226
+ poolKey,
227
+ opensAt,
228
+ closesAt,
229
+ walletCap,
230
+ signer,
231
+ name,
232
+ symbol,
233
+ spendToken,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Which token this pool's spend is measured in.
239
+ *
240
+ * The calculator measures spend on the pool's PAIRED side, so the gate's cap and allocations must
241
+ * be in that token's base units — a six-decimal stablecoin priced as if it were wei authorises a
242
+ * trillion times too much. Asked of the manager first (`pairedToken`, V1_3), and derived from the
243
+ * pool key when the manager predates the function: the key names two currencies and one of them
244
+ * is the coin. Decimals are load-bearing, so unlike the coin's cosmetic name an unreadable token
245
+ * is a refusal — retryable, because an RPC blip is not a statement about the token.
246
+ */
247
+ async function resolveSpendToken(
248
+ client: ChainReader,
249
+ deployment: Deployment,
250
+ memecoin: `0x${string}`,
251
+ poolKey: PoolKey,
252
+ poolId: `0x${string}`,
253
+ ): Promise<SpendToken | null> {
254
+ let paired: `0x${string}`;
255
+ try {
256
+ paired = (await client.readContract({
257
+ address: deployment.positionManager,
258
+ abi: positionManagerAbi,
259
+ functionName: 'pairedToken',
260
+ args: [poolId],
261
+ })) as `0x${string}`;
262
+ } catch {
263
+ paired = poolKey.currency0.toLowerCase() === memecoin.toLowerCase() ? poolKey.currency1 : poolKey.currency0;
264
+ }
265
+ if (paired.toLowerCase() === ZERO_ADDRESS) return ETH_SPEND_TOKEN;
266
+
267
+ try {
268
+ const [symbol, decimals] = (await Promise.all([
269
+ client.readContract({ address: paired, abi: erc20, functionName: 'symbol' }),
270
+ client.readContract({ address: paired, abi: erc20, functionName: 'decimals' }),
271
+ ])) as [string, number];
272
+ return { address: paired, symbol, decimals: Number(decimals), isNative: false };
273
+ } catch {
274
+ return null;
275
+ }
196
276
  }
197
277
 
198
278
  /**
@@ -1,4 +1,5 @@
1
- import type { PublicClient } from 'viem';
1
+ import { keccak256, parseAbi, toHex, type PublicClient } from 'viem';
2
+ import type { FlaunchVariant } from './chains.js';
2
3
 
3
4
  /**
4
5
  * The boot-time refusals {@link PayloadSigner}'s doc comment promises.
@@ -16,11 +17,28 @@ const ZERO_KEY = `0x${'00'.repeat(32)}`;
16
17
  /** The demo's session-secret fallback, which every copy of this repo shares. */
17
18
  export const DEMO_SESSION_SECRET = 'dev-secret-that-is-long-enough-to-pass';
18
19
 
20
+ /** `ProtocolRoles.POSITION_MANAGER` in flaunch-contracts: the role a calculator demands of its caller. */
21
+ export const POSITION_MANAGER_ROLE = keccak256(toHex('PositionManager'));
22
+
23
+ const wiringAbi = parseAbi([
24
+ 'function feeCalculator() view returns (address)',
25
+ 'function registeredCalculators(address) view returns (bool)',
26
+ 'function hasRole(bytes32 role, address account) view returns (bool)',
27
+ 'function approvedRouters(address) view returns (bool)',
28
+ ]);
29
+
19
30
  export interface SecureConfig {
20
31
  chainId: number;
21
32
  signerPrivateKey: string;
22
33
  spendGatedCalculator: `0x${string}`;
23
34
  sessionSecret: string;
35
+ /**
36
+ * When given, the wiring between the manager and the calculator is read back and asserted — see
37
+ * {@link assertWiring}. Optional so a caller that only wants the key and chain checks keeps them.
38
+ */
39
+ positionManager?: `0x${string}`;
40
+ flaunchVariant?: FlaunchVariant;
41
+ poolSwap?: `0x${string}`;
24
42
  }
25
43
 
26
44
  export async function assertSecureConfig(client: PublicClient, config: SecureConfig): Promise<void> {
@@ -46,4 +64,71 @@ export async function assertSecureConfig(client: PublicClient, config: SecureCon
46
64
  if (!code || code === '0x') {
47
65
  throw new Error(`SPEND_GATED_CALCULATOR ${config.spendGatedCalculator} holds no code on chain ${chainId}`);
48
66
  }
67
+
68
+ if (config.positionManager) await assertWiring(client, config);
69
+ }
70
+
71
+ /**
72
+ * The manager must route fees through a dispatcher that knows this calculator, and the calculator
73
+ * must accept calls from that dispatcher. Both are one-time owner actions on chain, and a stack
74
+ * missing either ADOPTS launches whose swaps never reach the gate: the pool trades ungated while
75
+ * the gate believes it is enforcing a cap. That is the whole failure this function refuses.
76
+ *
77
+ * A paired stack additionally needs its PoolSwap approved as a router, because a paired buy arrives
78
+ * through it and the calculator binds a smart wallet's buyer identity to an approved router's
79
+ * `msgSender()` — unapproved, every such buy reverts at the swap.
80
+ */
81
+ async function assertWiring(client: PublicClient, config: SecureConfig): Promise<void> {
82
+ const manager = config.positionManager!;
83
+ const calculator = config.spendGatedCalculator;
84
+
85
+ let dispatcher: `0x${string}`;
86
+ try {
87
+ dispatcher = await client.readContract({ address: manager, abi: wiringAbi, functionName: 'feeCalculator' });
88
+ } catch {
89
+ throw new Error(`POSITION_MANAGER ${manager} does not answer feeCalculator() — is it a Flaunch PositionManager?`);
90
+ }
91
+
92
+ const [registered, hasRole] = await Promise.all([
93
+ client.readContract({
94
+ address: dispatcher,
95
+ abi: wiringAbi,
96
+ functionName: 'registeredCalculators',
97
+ args: [calculator],
98
+ }).catch(() => false),
99
+ client.readContract({
100
+ address: calculator,
101
+ abi: wiringAbi,
102
+ functionName: 'hasRole',
103
+ args: [POSITION_MANAGER_ROLE, dispatcher],
104
+ }).catch(() => false),
105
+ ]);
106
+ if (!registered) {
107
+ throw new Error(
108
+ `the fee dispatcher ${dispatcher} of POSITION_MANAGER ${manager} does not have SPEND_GATED_CALCULATOR ` +
109
+ `${calculator} registered — its owner must call setCalculatorRegistered(${calculator}, true) before ` +
110
+ 'this gate can serve launches on that manager',
111
+ );
112
+ }
113
+ if (!hasRole) {
114
+ throw new Error(
115
+ `SPEND_GATED_CALCULATOR ${calculator} has not granted the PositionManager role to the dispatcher ${dispatcher} ` +
116
+ `— its admin must call grantRole(${POSITION_MANAGER_ROLE}, ${dispatcher})`,
117
+ );
118
+ }
119
+
120
+ if (config.flaunchVariant === 'v1_3') {
121
+ if (!config.poolSwap) {
122
+ throw new Error('a v1_3 stack needs POOL_SWAP — the router a paired-token buy goes through');
123
+ }
124
+ const approved = await client
125
+ .readContract({ address: calculator, abi: wiringAbi, functionName: 'approvedRouters', args: [config.poolSwap] })
126
+ .catch(() => false);
127
+ if (!approved) {
128
+ throw new Error(
129
+ `SPEND_GATED_CALCULATOR ${calculator} has not approved POOL_SWAP ${config.poolSwap} as a router — ` +
130
+ `its owner must call setApprovedRouter(${config.poolSwap}, true)`,
131
+ );
132
+ }
133
+ }
49
134
  }
package/src/claims.ts CHANGED
@@ -116,8 +116,8 @@ export class Claims {
116
116
  }
117
117
 
118
118
  /** What this player could spend right now. */
119
- availableWei(roundId: string, player: PlayerId): Promise<bigint> {
120
- return this.ledger.availableWei(roundId, player);
119
+ available(roundId: string, player: PlayerId): Promise<bigint> {
120
+ return this.ledger.available(roundId, player);
121
121
  }
122
122
 
123
123
  /** Full authoritative balance for live snapshots. */
package/src/demo.ts CHANGED
@@ -41,7 +41,7 @@ export interface DemoOptions<Config, State, Event, Action, PublicView, PlayerVie
41
41
  lobbyMs?: number;
42
42
  roundMs?: number;
43
43
  /** The cap a wallet may spend, as a real launch would set on chain. Default 0.025 ETH. */
44
- walletCapWei?: bigint;
44
+ walletCap?: bigint;
45
45
  pointsPerDollar?: number;
46
46
  usdPerEth?: number;
47
47
  roundId?: string;
@@ -83,7 +83,7 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
83
83
  databaseUrl = process.env.DATABASE_URL ?? 'postgres://postgres:gamemode@127.0.0.1:55439/gamemode',
84
84
  lobbyMs = 8_000,
85
85
  roundMs = 10 * 60_000,
86
- walletCapWei = ETH / 40n,
86
+ walletCap = ETH / 40n,
87
87
  pointsPerDollar = 100,
88
88
  usdPerEth = 3_000,
89
89
  roundId = 'dev',
@@ -96,9 +96,9 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
96
96
  const maxPointsPerPlayer = game.rewardBounds(config).maxPointsPerPlayer;
97
97
  const economy = resolveEconomy({
98
98
  maxPointsPerPlayer,
99
- walletCapWei,
99
+ walletCap,
100
100
  pointsPerDollar,
101
- usdPerEth,
101
+ usdPerSpendToken: usdPerEth,
102
102
  });
103
103
  if (!economy.ok) {
104
104
  throw new Error(
@@ -117,10 +117,10 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
117
117
  config,
118
118
  seed: Math.floor(Math.random() * 1_000_000),
119
119
  window: { opensAt, closesAt: opensAt + roundMs },
120
- weiPerPoint: economy.economy.weiPerPoint,
120
+ unitsPerPoint: economy.economy.unitsPerPoint,
121
121
  pointsPerDollar: economy.economy.pointsPerDollar,
122
- usdPerEth: economy.economy.usdPerEth,
123
- walletCapWei,
122
+ usdPerSpendToken: economy.economy.usdPerSpendToken,
123
+ walletCap,
124
124
  maxPointsPerPlayer,
125
125
  scoreAuthority: 'rules',
126
126
  poolId: DEMO_POOL_ID,
@@ -147,11 +147,11 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
147
147
  contracts: { positionManager: DEMO_CALCULATOR, spendGatedCalculator: DEMO_CALCULATOR },
148
148
  signer: signer.address,
149
149
  settler: signer.address,
150
- walletCapWei: walletCapWei.toString(),
150
+ walletCap: walletCap.toString(),
151
151
  roundDurationMs: roundMs,
152
152
  minLobbyLeadMs: lobbyMs,
153
153
  gateEndsAtGraceS: 10,
154
- flaunchVariant: 'legacy11',
154
+ flaunchVariant: 'v1_2',
155
155
  requiresEoa: false,
156
156
  accepting: true,
157
157
  publicLaunchesOpen: true,
@@ -171,7 +171,7 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
171
171
  app.post('/dev/wallet/buy', (request) => {
172
172
  const { maxSpendWei } = request.body as { maxSpendWei: string };
173
173
  purchases.push({ maxSpendWei, at: Date.now() });
174
- return { spentWei: maxSpendWei };
174
+ return { spent: maxSpendWei };
175
175
  });
176
176
 
177
177
  return { app, roundId, opensAt, walletAddress: wallet.address, purchases, pool };