@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.
- package/DEPLOY.md +7 -1
- package/README.md +6 -4
- package/dist/chain/chains.d.ts.map +1 -1
- package/dist/chain/chains.js +3 -1
- package/dist/chain/chains.js.map +1 -1
- package/dist/chain/signer.d.ts +19 -13
- package/dist/chain/signer.d.ts.map +1 -1
- package/dist/chain/signer.js +11 -10
- package/dist/chain/signer.js.map +1 -1
- package/dist/claims.d.ts +14 -30
- package/dist/claims.d.ts.map +1 -1
- package/dist/claims.js +55 -76
- package/dist/claims.js.map +1 -1
- package/dist/demo.d.ts +1 -1
- package/dist/demo.d.ts.map +1 -1
- package/dist/demo.js +3 -3
- package/dist/demo.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +39 -45
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +71 -172
- package/dist/ledger.js.map +1 -1
- package/dist/room.d.ts +9 -0
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +14 -0
- package/dist/room.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +40 -34
- package/dist/server.js.map +1 -1
- package/dist/settlement.d.ts +0 -3
- package/dist/settlement.d.ts.map +1 -1
- package/dist/settlement.js +12 -44
- package/dist/settlement.js.map +1 -1
- package/dist/standings.d.ts.map +1 -1
- package/dist/standings.js +7 -1
- package/dist/standings.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +30 -19
- package/dist/store.js.map +1 -1
- package/dist/version.js +2 -2
- package/package.json +2 -2
- package/src/chain/chains.ts +3 -1
- package/src/chain/signer.ts +24 -30
- package/src/claims.ts +54 -78
- package/src/demo.ts +5 -5
- package/src/index.ts +1 -2
- package/src/ledger.ts +100 -248
- package/src/room.ts +15 -0
- package/src/server.ts +39 -34
- package/src/settlement.ts +12 -43
- package/src/standings.ts +9 -1
- package/src/store.ts +30 -19
- 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 {
|
|
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
|
-
|
|
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
|
|
394
|
-
throw new RangeError(
|
|
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
|
|
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
|
-
// `
|
|
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 {
|
|
970
|
+
const { spend, requestId } = request.body as { spend?: string; requestId?: unknown };
|
|
966
971
|
let upTo: bigint;
|
|
967
972
|
try {
|
|
968
|
-
upTo = BigInt(
|
|
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
|
|
975
|
-
//
|
|
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
|
-
|
|
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
|
|
1298
|
-
//
|
|
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
|
-
//
|
|
1302
|
-
//
|
|
1303
|
-
if (settlement
|
|
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 (
|
|
1316
|
-
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
|
65
|
-
//
|
|
66
|
-
//
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
--
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
--
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
5
|
+
export const GATE_MARKER: string = "@flayerlabs/gamemode-gate@0.7.0";
|