@flayerlabs/gamemode-gate 0.6.0 → 0.7.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 (58) hide show
  1. package/DEPLOY.md +7 -1
  2. package/README.md +6 -4
  3. package/dist/chain/chains.d.ts.map +1 -1
  4. package/dist/chain/chains.js +3 -1
  5. package/dist/chain/chains.js.map +1 -1
  6. package/dist/chain/signer.d.ts +19 -13
  7. package/dist/chain/signer.d.ts.map +1 -1
  8. package/dist/chain/signer.js +11 -10
  9. package/dist/chain/signer.js.map +1 -1
  10. package/dist/claims.d.ts +14 -30
  11. package/dist/claims.d.ts.map +1 -1
  12. package/dist/claims.js +55 -76
  13. package/dist/claims.js.map +1 -1
  14. package/dist/demo.d.ts +1 -1
  15. package/dist/demo.d.ts.map +1 -1
  16. package/dist/demo.js +3 -3
  17. package/dist/demo.js.map +1 -1
  18. package/dist/index.d.ts +1 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/ledger.d.ts +39 -45
  23. package/dist/ledger.d.ts.map +1 -1
  24. package/dist/ledger.js +71 -172
  25. package/dist/ledger.js.map +1 -1
  26. package/dist/room.d.ts +9 -0
  27. package/dist/room.d.ts.map +1 -1
  28. package/dist/room.js +14 -0
  29. package/dist/room.js.map +1 -1
  30. package/dist/server.d.ts +7 -0
  31. package/dist/server.d.ts.map +1 -1
  32. package/dist/server.js +40 -34
  33. package/dist/server.js.map +1 -1
  34. package/dist/settlement.d.ts +0 -3
  35. package/dist/settlement.d.ts.map +1 -1
  36. package/dist/settlement.js +12 -44
  37. package/dist/settlement.js.map +1 -1
  38. package/dist/standings.d.ts.map +1 -1
  39. package/dist/standings.js +7 -1
  40. package/dist/standings.js.map +1 -1
  41. package/dist/store.d.ts +1 -1
  42. package/dist/store.d.ts.map +1 -1
  43. package/dist/store.js +30 -19
  44. package/dist/store.js.map +1 -1
  45. package/dist/version.js +2 -2
  46. package/package.json +2 -2
  47. package/src/chain/chains.ts +3 -1
  48. package/src/chain/signer.ts +24 -30
  49. package/src/claims.ts +54 -78
  50. package/src/demo.ts +5 -5
  51. package/src/index.ts +1 -2
  52. package/src/ledger.ts +100 -248
  53. package/src/room.ts +15 -0
  54. package/src/server.ts +39 -34
  55. package/src/settlement.ts +12 -43
  56. package/src/standings.ts +9 -1
  57. package/src/store.ts +30 -19
  58. package/src/version.ts +2 -2
package/src/server.ts CHANGED
@@ -19,7 +19,7 @@ import { ETH_SPEND_TOKEN, isReactionId, LIVE_PROTOCOL_VERSION } from '@flayerlab
19
19
  import type { FlaunchVariant } from './chain/chains.js';
20
20
  import { Room, type ScoreAuthority } from './room.js';
21
21
  import { Standings } from './standings.js';
22
- import { CLAIM_AUTHORIZATION_TTL_MS, Claims, isClaimAmount, isClaimRequestId } from './claims.js';
22
+ import { Claims, isClaimAmount, isClaimRequestId } from './claims.js';
23
23
  import { NotSignedIn, Sessions } from './sessions.js';
24
24
  import { parsePlayCursor, readPlaySessions, recordPlaySession } from './play-sessions.js';
25
25
  import type { Discovery, Launch, PoolKey } from './chain/discover.js';
@@ -269,10 +269,8 @@ const ethOnly = (usdPerEth: number): PriceFor => (token) => (token.isNative ? us
269
269
  const wireEconomy = (balance: EconomyBalance): WireEconomyBalance => ({
270
270
  unitsPerPoint: balance.unitsPerPoint.toString(),
271
271
  earned: balance.earned.toString(),
272
- held: balance.held.toString(),
273
272
  spent: balance.spent.toString(),
274
273
  available: balance.available.toString(),
275
- holdExpiresAt: balance.holdExpiresAt,
276
274
  });
277
275
 
278
276
  /** How often each open room is nudged so a deadline nobody is watching still fires. */
@@ -295,7 +293,14 @@ const MAX_AUTHORITATIVE_BUFFERED_BYTES = 1_024 * 1_024;
295
293
  const TERMINAL_DRAIN_DEADLINE_MS = 1_000;
296
294
  /** Reactions are also bounded across a whole room, not only per sending socket. */
297
295
  const MAX_ROOM_REACTIONS_PER_SECOND = 20;
298
- export const DEFAULT_SETTLEMENT_TAIL_MS = CLAIM_AUTHORIZATION_TTL_MS;
296
+ /**
297
+ * How long after `closesAt` the books stay open for chain reconciliation.
298
+ *
299
+ * Independent of any authorisation lifetime: a signed ceiling lasts until the books close, and a
300
+ * buy included in the last seconds of play still needs two confirmations and a reconcile pass to
301
+ * reach the standings.
302
+ */
303
+ export const DEFAULT_SETTLEMENT_TAIL_MS = 5 * 60_000;
299
304
 
300
305
  type AdoptionResult<Config, State, Event, Action, PublicView, PlayerView> =
301
306
  | { ok: true; room: Room<Config, State, Event, Action, PublicView, PlayerView>; created: boolean }
@@ -390,8 +395,8 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
390
395
  throw new RangeError('reactionIds must contain only valid reaction identifiers');
391
396
  }
392
397
  const settlementTailMs = options.settlementTailMs ?? DEFAULT_SETTLEMENT_TAIL_MS;
393
- if (!Number.isSafeInteger(settlementTailMs) || settlementTailMs < CLAIM_AUTHORIZATION_TTL_MS) {
394
- throw new RangeError(`settlementTailMs must be at least ${CLAIM_AUTHORIZATION_TTL_MS}`);
398
+ if (!Number.isSafeInteger(settlementTailMs) || settlementTailMs <= 0) {
399
+ throw new RangeError('settlementTailMs must be a positive number of milliseconds');
395
400
  }
396
401
  // A body limit costs nothing and is the cheapest protection against an endpoint that parses.
397
402
  const app = Fastify({ logger: process.env.GATE_LOG === '1', bodyLimit: 64 * 1024 });
@@ -626,8 +631,8 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
626
631
  *
627
632
  * Served by every gate rather than behind an option, because the numbers already exist in the
628
633
  * gate's own tables and this is the only place they exist at all — the platform never sees a
629
- * play. Spend is `chain_spent_wei`, the chain-reconciled figure, not claims: an expired hold is
630
- * not a purchase. `spendWeiTotal` is a decimal string because wei does not survive JSON as a
634
+ * play. Spend is `chain_spent_wei`, the chain-reconciled figure, not authorisations: a signed
635
+ * ceiling is not a purchase. `spendWeiTotal` is a decimal string because wei does not survive JSON as a
631
636
  * number, and `lastRoundAt` is milliseconds, matching every timestamp a round carries.
632
637
  *
633
638
  * The permissive CORS header mirrors `/config`: the dashboard is a different origin from the
@@ -880,7 +885,7 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
880
885
  return reply.code(result.refuse === 'award.invalid' ? 400 : 404).send({ refuse: result.refuse });
881
886
  }
882
887
 
883
- // `held` moves on its own when an authorisation expires, so this answer is only true at the
888
+ // `spent` rises whenever settlement sees a purchase land, so this answer is only true at the
884
889
  // moment it is read. Points are monotone; the wei beside them are not.
885
890
  const balance = await claims.economyBalance(result.standing.roundId, result.standing.player);
886
891
  return reply
@@ -962,18 +967,17 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
962
967
  const who = player(request);
963
968
  const room = await roomFor(id);
964
969
  if (!room) return reply.code(404).send({ message: 'that game is not running' });
965
- const { maxSpendWei, requestId } = request.body as { maxSpendWei?: string; requestId?: unknown };
970
+ const { spend, requestId } = request.body as { spend?: string; requestId?: unknown };
966
971
  let upTo: bigint;
967
972
  try {
968
- upTo = BigInt(maxSpendWei ?? '0');
973
+ upTo = BigInt(spend ?? '0');
969
974
  } catch {
970
975
  return reply.code(400).send({ message: 'that amount was not understood' });
971
976
  }
972
977
  if (!isClaimAmount(upTo)) return reply.code(400).send({ message: 'that amount was not understood' });
973
978
  // Required, not defaulted. Minting an id here for a caller that omitted one makes every retry a
974
- // fresh claim, which is precisely the double-hold idempotency exists to prevent — and it fails
975
- // silently, for the one caller that needed it most. Nothing is live, so there is no old client
976
- // to keep working.
979
+ // fresh signature, and a retry must receive the same bytes it was first given — otherwise the
980
+ // trusted page cannot tell a replayed request from a new one.
977
981
  if (!isClaimRequestId(requestId)) {
978
982
  return reply.code(400).send({ message: 'that request was not understood' });
979
983
  }
@@ -991,10 +995,12 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
991
995
  buyer: payload.buyer,
992
996
  poolId: payload.poolId,
993
997
  deadline: payload.deadline.toString(),
994
- maxSpendWei: payload.maxSpendWei.toString(),
995
- nonce: payload.nonce.toString(),
998
+ spendCeilingWei: payload.spendCeilingWei.toString(),
996
999
  signature: payload.signature,
997
1000
  signer: payload.signer,
1001
+ // Not signed: the exact-in amount for THIS buy, clamped to what was available. The page
1002
+ // spends exactly this; the calculator enforces the ceiling above.
1003
+ spend: authorised.spend.toString(),
998
1004
  // The embed passes this opaque value to the Flaunch buy call. Game code never reconstructs
999
1005
  // the ABI and therefore cannot accidentally sign or submit a different transaction shape.
1000
1006
  hookData: encodeHookData(payload),
@@ -1090,10 +1096,8 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
1090
1096
  : {
1091
1097
  unitsPerPoint: room.terms.unitsPerPoint,
1092
1098
  earned: 0n,
1093
- held: 0n,
1094
1099
  spent: 0n,
1095
1100
  available: 0n,
1096
- holdExpiresAt: null,
1097
1101
  };
1098
1102
  const economy = wireEconomy(balance);
1099
1103
  return {
@@ -1284,23 +1288,14 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
1284
1288
  try {
1285
1289
  const now = Date.now();
1286
1290
  const sweeping = Boolean(settlement && now - reconciledAt >= RECONCILE_MS);
1287
- // Authorisations nobody spent come back to the player who earned them.
1288
- // A chain-backed gate expires holds atomically with confirmed spend, never ahead of a
1289
- // lagging/failed RPC read. Demo and legacy adapters retain their existing expiry path.
1290
- const released = settlement?.usesConfirmedSnapshots ? [] : await claims.releaseExpired().catch((error) => {
1291
- app.log.error(error);
1292
- return [];
1293
- });
1294
- for (const balance of released) announcePlayer(balance.roundId, balance.player);
1295
-
1296
1291
  for (const [id, room] of rooms) {
1297
- // Gameplay is over, but a buzzer-time authorisation and the chart remain honourable through
1298
- // the independent books tail. A process can wake for the first time after this tail (for
1292
+ // Gameplay is over, but a buzzer-time buy and the chart remain honourable through the
1293
+ // independent books tail. A process can wake for the first time after this tail (for
1299
1294
  // example after a stall or restart), so persist every due game wake before deleting it.
1300
1295
  if (now > room.launch.booksCloseAt) {
1301
- // Confirmations may trail a last-second claim's deadline. Keep background accounting
1302
- // alive at the normal cadence until holds settle; socket deadlines remain independent.
1303
- if (settlement?.usesConfirmedSnapshots && !sweeping) continue;
1296
+ // The last reconcile must run at the normal cadence, not on every tick, and a room is
1297
+ // torn down only after one clean pass so the final standings carry every landed buy.
1298
+ if (settlement && !sweeping) continue;
1304
1299
  try {
1305
1300
  if (room.nextWakeAt() !== null) await room.tick(Math.min(now, room.window.closesAt));
1306
1301
  } catch (error) {
@@ -1312,8 +1307,18 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
1312
1307
  if (settlement) {
1313
1308
  const final = await settlement.reconcile(id).catch(() => ({ read: 0, failed: 1, changed: [] }));
1314
1309
  if (final.failed > 0) app.log.error(`${id}: ${final.failed} final spend reads failed`);
1315
- if (settlement.usesConfirmedSnapshots &&
1316
- (final.failed > 0 || await settlement.hasPendingClaims(id).catch(() => true))) continue;
1310
+ if (final.failed > 0) continue;
1311
+ }
1312
+ // The round is now proven final. Persist that before anything is torn down, so a page
1313
+ // reading the standings can tell "ended, still settling" from "final" — and so the
1314
+ // 4410 close below is never sent for a round the database still calls unsettled.
1315
+ try {
1316
+ await room.markSettled(now);
1317
+ } catch (error) {
1318
+ // Retain the room and retry: the standings would otherwise never be marked final,
1319
+ // and the sockets would be told the round ended before the database agreed.
1320
+ app.log.error(error);
1321
+ continue;
1317
1322
  }
1318
1323
  for (const watcher of watchers.get(id) ?? []) {
1319
1324
  if (
package/src/settlement.ts CHANGED
@@ -7,19 +7,17 @@ import type { ChainReader } from './chain/discover.js';
7
7
  /**
8
8
  * Keeping the ledger honest about what was actually spent.
9
9
  *
10
- * The obvious design is to watch for a swap and match it back to the authorisation that permitted
11
- * it. That turns out to be both harder and less useful than it sounds: the event carries the buyer
12
- * and the amount but not the nonce, and a wallet can submit through a relay, so correlating them is
13
- * guesswork.
10
+ * The calculator keeps one cumulative figure per pool per wallet, `walletSpentWei`, and it is the
11
+ * only figure that is true: it is what the contract itself enforces the signed ceiling against.
12
+ * Settlement reads it at a confirmed block and records it, never backwards. There is nothing to
13
+ * match back to an authorisation — a signed ceiling is not consumed, so it has no fate to prove.
14
14
  *
15
- * Read cumulative spend and exact authorisation consumption from the same block. Equal amounts
16
- * are not proof that a hold was consumed. A browser receipt is not a reason to release a hold.
15
+ * A buy that lands after `closesAt` still needs to be folded into `spent` for the standings, which
16
+ * is why reconciliation runs through `booksCloseAt` and the room waits for one clean final pass.
17
17
  */
18
18
 
19
19
  const spendGatedAbi = parseAbi([
20
20
  'function walletSpentWei(bytes32 _poolId, address _wallet) view returns (uint256)',
21
- 'function hashSpendAuthorization(address,bytes32,uint256,uint256,uint256) view returns (bytes32)',
22
- 'function isAuthorizationUsed(bytes32) view returns (bool)',
23
21
  ]);
24
22
 
25
23
  export class Settlement {
@@ -30,16 +28,6 @@ export class Settlement {
30
28
  private readonly calculator: `0x${string}`,
31
29
  ) {}
32
30
 
33
- /** Expiry must use the same confirmed chain snapshot as spend when snapshot reads are available. */
34
- get usesConfirmedSnapshots(): boolean { return typeof this.client.getBlock === 'function'; }
35
-
36
- async hasPendingClaims(roundId: string): Promise<boolean> {
37
- const result = await this.pool.query<{ pending: boolean }>(
38
- `select exists(select 1 from claims where round_id = $1 and status = 'held') as pending`, [roundId],
39
- );
40
- return result.rows[0]?.pending ?? false;
41
- }
42
-
43
31
  /**
44
32
  * Bring one round's view of spending up to date with the chain.
45
33
  *
@@ -61,14 +49,12 @@ export class Settlement {
61
49
 
62
50
  for (const player of players) {
63
51
  try {
64
- // Two blocks behind the observed head, then verify that block is still canonical before
65
- // committing. This follows chain confirmations, not wallet UI completion. Read-only
66
- // legacy adapters retain conservative expiry-only holds until upgraded to a full client.
52
+ // Two blocks behind the observed head when the client can name blocks, so a reorg of the
53
+ // tip cannot record a spend that then vanishes. A read-only legacy adapter reads the tip;
54
+ // `recordChainSpend` never goes backwards, so the worst case is a figure that arrives early.
67
55
  const head = this.client.getBlock ? await this.client.getBlock({ blockTag: 'latest' }) : null;
68
- const block = head?.number != null && head.number >= 2n
69
- ? await this.client.getBlock!({ blockNumber: head.number - 2n }) : null;
70
- if (this.client.getBlock && (block?.number !== head!.number! - 2n || !block?.hash || typeof block.timestamp !== 'bigint')) throw new Error('No confirmed block');
71
- const atBlock = block ? { blockNumber: block.number! } : {};
56
+ if (this.client.getBlock && (head?.number == null || head.number < 2n)) throw new Error('No confirmed block');
57
+ const atBlock = head?.number != null ? { blockNumber: head.number - 2n } : {};
72
58
  const spent = (await this.client.readContract({
73
59
  address: this.calculator,
74
60
  abi: spendGatedAbi,
@@ -77,24 +63,7 @@ export class Settlement {
77
63
  ...atBlock,
78
64
  })) as bigint;
79
65
  if (typeof spent !== 'bigint' || spent < 0n) throw new Error('Invalid chain spend');
80
- const consumed: bigint[] = [];
81
- if (block) {
82
- const outstanding = await this.ledger.outstandingClaims(roundId, player);
83
- for (const claim of outstanding) {
84
- if (claim.deadline < block.timestamp) continue;
85
- const digest = await this.client.readContract({ address: this.calculator, abi: spendGatedAbi,
86
- functionName: 'hashSpendAuthorization',
87
- args: [player as `0x${string}`, poolId as `0x${string}`, claim.deadline, claim.wei, claim.nonce], ...atBlock });
88
- if (typeof digest !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(digest)) throw new Error('Invalid digest');
89
- const used = await this.client.readContract({ address: this.calculator, abi: spendGatedAbi,
90
- functionName: 'isAuthorizationUsed', args: [digest], ...atBlock });
91
- if (typeof used !== 'boolean') throw new Error('Invalid consumption proof');
92
- if (used) consumed.push(claim.nonce);
93
- }
94
- const canonical = await this.client.getBlock!({ blockNumber: block.number! });
95
- if (canonical.hash !== block.hash) throw new Error('Settlement block changed');
96
- }
97
- if (await this.ledger.reconcileSpend(roundId, player, spent, consumed, block?.timestamp)) changed.push(player);
66
+ if (await this.ledger.recordChainSpend(roundId, player, spent)) changed.push(player);
98
67
  read += 1;
99
68
  } catch {
100
69
  // One unreachable read is not a reason to stop the round, but it is counted. Swallowed
package/src/standings.ts CHANGED
@@ -43,13 +43,15 @@ export class Standings {
43
43
  coin_symbol: string;
44
44
  opens_at: string;
45
45
  closes_at: string;
46
+ books_close_at: string;
47
+ settled_at: string | null;
46
48
  spend_token: string;
47
49
  spend_symbol: string;
48
50
  spend_decimals: number;
49
51
  usd_per_spend_token: number | null;
50
52
  usd_per_eth: number;
51
53
  }>(
52
- `select id, pool_id, coin_address, coin_symbol, opens_at, closes_at,
54
+ `select id, pool_id, coin_address, coin_symbol, opens_at, closes_at, books_close_at, settled_at,
53
55
  spend_token, spend_symbol, spend_decimals, usd_per_spend_token, usd_per_eth
54
56
  from rounds
55
57
  where lower(coin_address) = $1 and game_id = $2
@@ -78,6 +80,9 @@ export class Standings {
78
80
  ]);
79
81
 
80
82
  const window = { opensAt: Number(round.opens_at), closesAt: Number(round.closes_at) };
83
+ // `settled_at` is stamped by the gate's ticker after its last clean reconcile, so it is the one
84
+ // fact that separates "ended, still settling" from "final". The phase is clock-based and cannot.
85
+ const settledAt = round.settled_at === null ? null : Number(round.settled_at);
81
86
  return {
82
87
  roundId: round.id,
83
88
  poolId: round.pool_id,
@@ -85,6 +90,9 @@ export class Standings {
85
90
  symbol: round.coin_symbol,
86
91
  phase: roundPhaseAt(window, now),
87
92
  ...window,
93
+ booksCloseAt: Number(round.books_close_at),
94
+ settledAt,
95
+ final: settledAt !== null,
88
96
  spendToken: {
89
97
  address: round.spend_token,
90
98
  symbol: round.spend_symbol,
package/src/store.ts CHANGED
@@ -95,6 +95,12 @@ alter table rounds
95
95
  alter table rounds
96
96
  add column if not exists position_manager text check (position_manager <> '');
97
97
 
98
+ -- Round finalization (0.7.0). The moment the gate finished its last clean settlement pass past
99
+ -- books_close_at, in ms since the epoch: from here chain_spent_wei cannot move. Null while a round is still
100
+ -- settling, and on rounds a gate below 0.7.0 closed — nothing can reconstruct it for those.
101
+ alter table rounds
102
+ add column if not exists settled_at bigint;
103
+
98
104
  create table if not exists balances (
99
105
  round_id text not null references rounds(id) on delete cascade,
100
106
  player text not null,
@@ -105,27 +111,32 @@ create table if not exists balances (
105
111
  primary key (round_id, player)
106
112
  );
107
113
 
108
- -- One row per authorisation ever issued. Nothing is deleted: a signature that exists in the world
109
- -- cannot be un-issued, so the ledger records its fate instead of forgetting it.
110
- create table if not exists claims (
111
- nonce bigint generated always as identity primary key,
112
- round_id text not null references rounds(id) on delete cascade,
113
- player text not null,
114
- request_id varchar(128) not null check (request_id <> ''),
115
- requested_wei numeric(78,0) not null check (requested_wei > 0),
116
- wei numeric(78,0) not null check (wei > 0 and wei <= requested_wei),
117
- -- Held or released, and nothing else. Whether a swap LANDED is the chain's to say, not ours.
118
- status text not null check (status in ('held', 'released')),
119
- deadline bigint not null,
120
- -- Persisted before a payload leaves the gate. Until signing completes, both values are null.
121
- signature text,
122
- signer text,
123
- created_at timestamptz not null default now(),
124
- constraint claims_by_request unique (round_id, player, request_id),
125
- constraint claims_signature_pair check ((signature is null) = (signer is null))
114
+ -- One row per signed authorisation, kept only so a retried or replica-raced request returns the
115
+ -- exact bytes already handed out. Nothing here is a reservation: the signature carries a
116
+ -- cumulative ceiling the calculator compares against its own walletSpentWei, so an unused one
117
+ -- costs nothing and there is no fate to record.
118
+ create table if not exists authorisations (
119
+ round_id text not null references rounds(id) on delete cascade,
120
+ player text not null,
121
+ request_id varchar(128) not null check (request_id <> ''),
122
+ -- What the caller asked for, so a retry with a different amount is refused, not silently served.
123
+ requested_wei numeric(78,0) not null check (requested_wei > 0),
124
+ -- The signed field: min(earned, wallet cap) at issue, in the round's spend-token base units.
125
+ spend_ceiling_wei numeric(78,0) not null check (spend_ceiling_wei > 0),
126
+ -- The exact-in amount the caller intended, clamped to what was available. Echoed, not signed.
127
+ spend_wei numeric(78,0) not null check (spend_wei > 0 and spend_wei <= spend_ceiling_wei),
128
+ deadline bigint not null,
129
+ signature text not null,
130
+ signer text not null,
131
+ created_at timestamptz not null default now(),
132
+ primary key (round_id, player, request_id)
126
133
  );
127
134
 
128
- create index if not exists claims_by_player on claims (round_id, player) where status = 'held';
135
+ -- 0.7.0 hard cutover. Holds no longer exist, and every row the old table could carry was signed
136
+ -- under the version 1 calculator domain that a 0.7.0 gate cannot serve. Dropping it is the honest
137
+ -- migration; a gate should not be upgraded until every live round has passed its books close.
138
+ drop index if exists claims_by_player;
139
+ drop table if exists claims;
129
140
 
130
141
  -- Trusted creator servers use one stable event id for every score increment. The event and the
131
142
  -- balance update commit together, so a retry can recover the first result without paying twice.
package/src/version.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/write-version.mjs from package.json at build time. Do not edit.
2
2
  /** The published version of @flayerlabs/gamemode-gate, as a literal so it survives any bundler. */
3
- export const GATE_VERSION: string = "0.6.0";
3
+ export const GATE_VERSION: string = "0.7.0";
4
4
  /** `@flayerlabs/gamemode-gate@<version>`: the text a build of this package can be searched for. */
5
- export const GATE_MARKER: string = "@flayerlabs/gamemode-gate@0.6.0";
5
+ export const GATE_MARKER: string = "@flayerlabs/gamemode-gate@0.7.0";