@flayerlabs/gamemode-gate 0.5.6 → 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 +31 -3
- package/README.md +10 -5
- package/dist/chain/chains.d.ts +14 -6
- package/dist/chain/chains.d.ts.map +1 -1
- package/dist/chain/chains.js +11 -5
- package/dist/chain/chains.js.map +1 -1
- package/dist/chain/discover.d.ts +61 -3
- package/dist/chain/discover.d.ts.map +1 -1
- package/dist/chain/discover.js +181 -21
- package/dist/chain/discover.js.map +1 -1
- package/dist/chain/secure.d.ts +28 -0
- package/dist/chain/secure.d.ts.map +1 -1
- package/dist/chain/secure.js +55 -20
- package/dist/chain/secure.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 +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- 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/main.d.ts +4 -2
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +11 -6
- package/dist/main.js.map +1 -1
- package/dist/room.d.ts +16 -0
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +29 -6
- package/dist/room.js.map +1 -1
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +43 -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 +36 -19
- package/dist/store.js.map +1 -1
- package/dist/version.js +2 -2
- package/package.json +2 -2
- package/src/chain/chains.ts +17 -7
- package/src/chain/discover.ts +212 -21
- package/src/chain/secure.ts +75 -31
- package/src/chain/signer.ts +24 -30
- package/src/claims.ts +54 -78
- package/src/demo.ts +5 -5
- package/src/index.ts +6 -6
- package/src/ledger.ts +100 -248
- package/src/main.ts +11 -6
- package/src/room.ts +29 -3
- package/src/server.ts +58 -34
- package/src/settlement.ts +12 -43
- package/src/standings.ts +9 -1
- package/src/store.ts +36 -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';
|
|
@@ -148,6 +148,12 @@ export interface GameServerGateOptions extends GateBaseOptions {
|
|
|
148
148
|
gameServerOrigins?: readonly string[];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* How a gate decides which PositionManagers it adopts coins from. One value today; the type exists
|
|
153
|
+
* so a client can compare rather than test for presence, and so a later policy has a name.
|
|
154
|
+
*/
|
|
155
|
+
export type PositionManagerPolicy = 'wired-to-calculator';
|
|
156
|
+
|
|
151
157
|
/**
|
|
152
158
|
* The gate's public identity, in the exact shape the flaunch frontend's `GateServerConfig` already
|
|
153
159
|
* parses — served this way so the page that builds launches needs no second config type.
|
|
@@ -169,7 +175,15 @@ export interface GateAnnouncement {
|
|
|
169
175
|
roundDurationMs: number;
|
|
170
176
|
minLobbyLeadMs: number;
|
|
171
177
|
gateEndsAtGraceS: number;
|
|
178
|
+
/** The generation of the PRIMARY manager in `contracts.positionManager`; not a limit on adoption. */
|
|
172
179
|
flaunchVariant: FlaunchVariant;
|
|
180
|
+
/**
|
|
181
|
+
* How this gate decides which PositionManagers it adopts coins from. `'wired-to-calculator'`
|
|
182
|
+
* means any manager whose fee wiring routes to `contracts.spendGatedCalculator`, derived from the
|
|
183
|
+
* coin itself at adoption; `contracts.positionManager` is then the primary manager, not the only
|
|
184
|
+
* one. A gate that omits the field (below 0.6.0) adopts coins on `contracts.positionManager` only.
|
|
185
|
+
*/
|
|
186
|
+
positionManagerPolicy?: PositionManagerPolicy;
|
|
173
187
|
/**
|
|
174
188
|
* The non-ETH paired tokens this gate will run rounds on, each with the cap a launch on it should
|
|
175
189
|
* write. Absent or empty means ETH pairings only. Launch tooling refuses a pairing that is not
|
|
@@ -255,10 +269,8 @@ const ethOnly = (usdPerEth: number): PriceFor => (token) => (token.isNative ? us
|
|
|
255
269
|
const wireEconomy = (balance: EconomyBalance): WireEconomyBalance => ({
|
|
256
270
|
unitsPerPoint: balance.unitsPerPoint.toString(),
|
|
257
271
|
earned: balance.earned.toString(),
|
|
258
|
-
held: balance.held.toString(),
|
|
259
272
|
spent: balance.spent.toString(),
|
|
260
273
|
available: balance.available.toString(),
|
|
261
|
-
holdExpiresAt: balance.holdExpiresAt,
|
|
262
274
|
});
|
|
263
275
|
|
|
264
276
|
/** How often each open room is nudged so a deadline nobody is watching still fires. */
|
|
@@ -281,7 +293,14 @@ const MAX_AUTHORITATIVE_BUFFERED_BYTES = 1_024 * 1_024;
|
|
|
281
293
|
const TERMINAL_DRAIN_DEADLINE_MS = 1_000;
|
|
282
294
|
/** Reactions are also bounded across a whole room, not only per sending socket. */
|
|
283
295
|
const MAX_ROOM_REACTIONS_PER_SECOND = 20;
|
|
284
|
-
|
|
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;
|
|
285
304
|
|
|
286
305
|
type AdoptionResult<Config, State, Event, Action, PublicView, PlayerView> =
|
|
287
306
|
| { ok: true; room: Room<Config, State, Event, Action, PublicView, PlayerView>; created: boolean }
|
|
@@ -376,8 +395,8 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
376
395
|
throw new RangeError('reactionIds must contain only valid reaction identifiers');
|
|
377
396
|
}
|
|
378
397
|
const settlementTailMs = options.settlementTailMs ?? DEFAULT_SETTLEMENT_TAIL_MS;
|
|
379
|
-
if (!Number.isSafeInteger(settlementTailMs) || settlementTailMs
|
|
380
|
-
throw new RangeError(
|
|
398
|
+
if (!Number.isSafeInteger(settlementTailMs) || settlementTailMs <= 0) {
|
|
399
|
+
throw new RangeError('settlementTailMs must be a positive number of milliseconds');
|
|
381
400
|
}
|
|
382
401
|
// A body limit costs nothing and is the cheapest protection against an endpoint that parses.
|
|
383
402
|
const app = Fastify({ logger: process.env.GATE_LOG === '1', bodyLimit: 64 * 1024 });
|
|
@@ -512,12 +531,17 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
512
531
|
name: launch.name,
|
|
513
532
|
symbol: launch.symbol,
|
|
514
533
|
imageUrl: launch.imageUrl ?? null,
|
|
534
|
+
positionManager: launch.positionManager ?? null,
|
|
515
535
|
},
|
|
516
536
|
});
|
|
517
537
|
} catch (error) {
|
|
518
538
|
app.log.error(error);
|
|
519
539
|
return { ok: false, status: 503, message: 'this game is still being prepared' };
|
|
520
540
|
}
|
|
541
|
+
app.log.info(
|
|
542
|
+
`${launch.coinAddress}: adopted ${launch.symbol || 'coin'} on position manager ` +
|
|
543
|
+
`${launch.positionManager ?? 'primary'} (pool ${launch.poolId}, ${spendToken.symbol})`,
|
|
544
|
+
);
|
|
521
545
|
rooms.set(launch.poolId, room);
|
|
522
546
|
return { ok: true, room, created: true };
|
|
523
547
|
}
|
|
@@ -607,8 +631,8 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
607
631
|
*
|
|
608
632
|
* Served by every gate rather than behind an option, because the numbers already exist in the
|
|
609
633
|
* gate's own tables and this is the only place they exist at all — the platform never sees a
|
|
610
|
-
* play. Spend is `chain_spent_wei`, the chain-reconciled figure, not
|
|
611
|
-
* 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
|
|
612
636
|
* number, and `lastRoundAt` is milliseconds, matching every timestamp a round carries.
|
|
613
637
|
*
|
|
614
638
|
* The permissive CORS header mirrors `/config`: the dashboard is a different origin from the
|
|
@@ -861,7 +885,7 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
861
885
|
return reply.code(result.refuse === 'award.invalid' ? 400 : 404).send({ refuse: result.refuse });
|
|
862
886
|
}
|
|
863
887
|
|
|
864
|
-
// `
|
|
888
|
+
// `spent` rises whenever settlement sees a purchase land, so this answer is only true at the
|
|
865
889
|
// moment it is read. Points are monotone; the wei beside them are not.
|
|
866
890
|
const balance = await claims.economyBalance(result.standing.roundId, result.standing.player);
|
|
867
891
|
return reply
|
|
@@ -943,18 +967,17 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
943
967
|
const who = player(request);
|
|
944
968
|
const room = await roomFor(id);
|
|
945
969
|
if (!room) return reply.code(404).send({ message: 'that game is not running' });
|
|
946
|
-
const {
|
|
970
|
+
const { spend, requestId } = request.body as { spend?: string; requestId?: unknown };
|
|
947
971
|
let upTo: bigint;
|
|
948
972
|
try {
|
|
949
|
-
upTo = BigInt(
|
|
973
|
+
upTo = BigInt(spend ?? '0');
|
|
950
974
|
} catch {
|
|
951
975
|
return reply.code(400).send({ message: 'that amount was not understood' });
|
|
952
976
|
}
|
|
953
977
|
if (!isClaimAmount(upTo)) return reply.code(400).send({ message: 'that amount was not understood' });
|
|
954
978
|
// Required, not defaulted. Minting an id here for a caller that omitted one makes every retry a
|
|
955
|
-
// fresh
|
|
956
|
-
//
|
|
957
|
-
// 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.
|
|
958
981
|
if (!isClaimRequestId(requestId)) {
|
|
959
982
|
return reply.code(400).send({ message: 'that request was not understood' });
|
|
960
983
|
}
|
|
@@ -972,10 +995,12 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
972
995
|
buyer: payload.buyer,
|
|
973
996
|
poolId: payload.poolId,
|
|
974
997
|
deadline: payload.deadline.toString(),
|
|
975
|
-
|
|
976
|
-
nonce: payload.nonce.toString(),
|
|
998
|
+
spendCeilingWei: payload.spendCeilingWei.toString(),
|
|
977
999
|
signature: payload.signature,
|
|
978
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(),
|
|
979
1004
|
// The embed passes this opaque value to the Flaunch buy call. Game code never reconstructs
|
|
980
1005
|
// the ABI and therefore cannot accidentally sign or submit a different transaction shape.
|
|
981
1006
|
hookData: encodeHookData(payload),
|
|
@@ -1071,10 +1096,8 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
1071
1096
|
: {
|
|
1072
1097
|
unitsPerPoint: room.terms.unitsPerPoint,
|
|
1073
1098
|
earned: 0n,
|
|
1074
|
-
held: 0n,
|
|
1075
1099
|
spent: 0n,
|
|
1076
1100
|
available: 0n,
|
|
1077
|
-
holdExpiresAt: null,
|
|
1078
1101
|
};
|
|
1079
1102
|
const economy = wireEconomy(balance);
|
|
1080
1103
|
return {
|
|
@@ -1265,23 +1288,14 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
1265
1288
|
try {
|
|
1266
1289
|
const now = Date.now();
|
|
1267
1290
|
const sweeping = Boolean(settlement && now - reconciledAt >= RECONCILE_MS);
|
|
1268
|
-
// Authorisations nobody spent come back to the player who earned them.
|
|
1269
|
-
// A chain-backed gate expires holds atomically with confirmed spend, never ahead of a
|
|
1270
|
-
// lagging/failed RPC read. Demo and legacy adapters retain their existing expiry path.
|
|
1271
|
-
const released = settlement?.usesConfirmedSnapshots ? [] : await claims.releaseExpired().catch((error) => {
|
|
1272
|
-
app.log.error(error);
|
|
1273
|
-
return [];
|
|
1274
|
-
});
|
|
1275
|
-
for (const balance of released) announcePlayer(balance.roundId, balance.player);
|
|
1276
|
-
|
|
1277
1291
|
for (const [id, room] of rooms) {
|
|
1278
|
-
// Gameplay is over, but a buzzer-time
|
|
1279
|
-
//
|
|
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
|
|
1280
1294
|
// example after a stall or restart), so persist every due game wake before deleting it.
|
|
1281
1295
|
if (now > room.launch.booksCloseAt) {
|
|
1282
|
-
//
|
|
1283
|
-
//
|
|
1284
|
-
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;
|
|
1285
1299
|
try {
|
|
1286
1300
|
if (room.nextWakeAt() !== null) await room.tick(Math.min(now, room.window.closesAt));
|
|
1287
1301
|
} catch (error) {
|
|
@@ -1293,8 +1307,18 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
1293
1307
|
if (settlement) {
|
|
1294
1308
|
const final = await settlement.reconcile(id).catch(() => ({ read: 0, failed: 1, changed: [] }));
|
|
1295
1309
|
if (final.failed > 0) app.log.error(`${id}: ${final.failed} final spend reads failed`);
|
|
1296
|
-
if (
|
|
1297
|
-
|
|
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;
|
|
1298
1322
|
}
|
|
1299
1323
|
for (const watcher of watchers.get(id) ?? []) {
|
|
1300
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
|
@@ -89,6 +89,18 @@ alter table rounds
|
|
|
89
89
|
alter table rounds
|
|
90
90
|
add column if not exists usd_per_spend_token double precision check (usd_per_spend_token > 0);
|
|
91
91
|
|
|
92
|
+
-- Any wired PositionManager (0.6.0). The manager a round's coin was launched on, as the gate
|
|
93
|
+
-- resolved it from the coin at adoption. Null on rounds adopted before the gate recorded it, and on
|
|
94
|
+
-- demo and mock rounds; nothing reads it back for gameplay, it is provenance for the operator.
|
|
95
|
+
alter table rounds
|
|
96
|
+
add column if not exists position_manager text check (position_manager <> '');
|
|
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
|
+
|
|
92
104
|
create table if not exists balances (
|
|
93
105
|
round_id text not null references rounds(id) on delete cascade,
|
|
94
106
|
player text not null,
|
|
@@ -99,27 +111,32 @@ create table if not exists balances (
|
|
|
99
111
|
primary key (round_id, player)
|
|
100
112
|
);
|
|
101
113
|
|
|
102
|
-
-- One row per authorisation
|
|
103
|
-
--
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
--
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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)
|
|
120
133
|
);
|
|
121
134
|
|
|
122
|
-
|
|
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;
|
|
123
140
|
|
|
124
141
|
-- Trusted creator servers use one stable event id for every score increment. The event and the
|
|
125
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";
|