@flayerlabs/gamemode-gate 0.4.3 → 0.5.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 +27 -3
- package/README.md +3 -3
- package/dist/chain/chains.d.ts +45 -6
- package/dist/chain/chains.d.ts.map +1 -1
- package/dist/chain/chains.js +56 -5
- package/dist/chain/chains.js.map +1 -1
- package/dist/chain/discover.d.ts +20 -2
- package/dist/chain/discover.d.ts.map +1 -1
- package/dist/chain/discover.js +74 -6
- package/dist/chain/discover.js.map +1 -1
- package/dist/chain/secure.d.ts +11 -1
- package/dist/chain/secure.d.ts.map +1 -1
- package/dist/chain/secure.js +67 -0
- package/dist/chain/secure.js.map +1 -1
- package/dist/claims.d.ts +1 -1
- package/dist/claims.d.ts.map +1 -1
- package/dist/claims.js +2 -2
- 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 +9 -9
- package/dist/demo.js.map +1 -1
- package/dist/economy.d.ts +25 -9
- package/dist/economy.d.ts.map +1 -1
- package/dist/economy.js +63 -22
- package/dist/economy.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +2 -2
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +16 -16
- package/dist/ledger.js.map +1 -1
- package/dist/main.d.ts +30 -5
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +158 -17
- package/dist/main.js.map +1 -1
- package/dist/room.d.ts +7 -4
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +31 -10
- package/dist/room.js.map +1 -1
- package/dist/server.d.ts +33 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +63 -25
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +21 -0
- package/dist/store.js.map +1 -1
- package/package.json +2 -2
- package/src/chain/chains.ts +82 -9
- package/src/chain/discover.ts +87 -7
- package/src/chain/secure.ts +86 -1
- package/src/claims.ts +2 -2
- package/src/demo.ts +10 -10
- package/src/economy.ts +83 -31
- package/src/index.ts +7 -5
- package/src/ledger.ts +18 -18
- package/src/main.ts +181 -19
- package/src/room.ts +41 -14
- package/src/server.ts +107 -35
- package/src/store.ts +21 -0
package/src/server.ts
CHANGED
|
@@ -8,10 +8,12 @@ import type {
|
|
|
8
8
|
EconomyBalance,
|
|
9
9
|
PresenceState,
|
|
10
10
|
RoundTerms,
|
|
11
|
+
SpendToken,
|
|
11
12
|
WireEconomyBalance,
|
|
12
13
|
WireRoundTerms,
|
|
13
14
|
} from '@flayerlabs/gamemode-spec/live';
|
|
14
|
-
import { isReactionId, LIVE_PROTOCOL_VERSION } from '@flayerlabs/gamemode-spec/live';
|
|
15
|
+
import { ETH_SPEND_TOKEN, isReactionId, LIVE_PROTOCOL_VERSION } from '@flayerlabs/gamemode-spec/live';
|
|
16
|
+
import type { FlaunchVariant } from './chain/chains.js';
|
|
15
17
|
import { Room, type ScoreAuthority } from './room.js';
|
|
16
18
|
import { CLAIM_AUTHORIZATION_TTL_MS, Claims, isClaimAmount, isClaimRequestId } from './claims.js';
|
|
17
19
|
import { NotSignedIn, Sessions } from './sessions.js';
|
|
@@ -42,7 +44,15 @@ interface GateBaseOptions {
|
|
|
42
44
|
claims: Claims;
|
|
43
45
|
/** How many points a dollar buys. The only economic number anyone sets by hand. */
|
|
44
46
|
pointsPerDollar: number;
|
|
47
|
+
/** The price of ETH — what an ETH-paired round is priced at. */
|
|
45
48
|
usdPerEth: number;
|
|
49
|
+
/**
|
|
50
|
+
* The price of one whole spend token, for a round whose coin is paired with something other
|
|
51
|
+
* than ETH — or null to refuse the launch as unpriced. Consulted at adoption with the token the
|
|
52
|
+
* chain reported. Omitted, only native ETH is priced (at `usdPerEth`) and every other pairing is
|
|
53
|
+
* refused: a gate that guessed a price would be authorising real money on a made-up number.
|
|
54
|
+
*/
|
|
55
|
+
priceFor?: PriceFor;
|
|
46
56
|
/** Verifies from the chain that a coin really is a game launch. */
|
|
47
57
|
discovery?: Discovery;
|
|
48
58
|
/** Keeps the ledger's view of spending level with the chain's. */
|
|
@@ -85,6 +95,20 @@ interface GateBaseOptions {
|
|
|
85
95
|
reactionIds?: readonly string[];
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
/** A price for one whole spend token in dollars, or null when this gate has none for it. */
|
|
99
|
+
export type PriceFor = (token: SpendToken) => Promise<number | null> | number | null;
|
|
100
|
+
|
|
101
|
+
/** One entry of a gate's announced price list — what launch tooling checks a pairing against. */
|
|
102
|
+
export interface AnnouncedSpendToken {
|
|
103
|
+
address: `0x${string}`;
|
|
104
|
+
symbol: string;
|
|
105
|
+
decimals: number;
|
|
106
|
+
/** The price this gate will fix rounds on that token at. */
|
|
107
|
+
usdPerToken: number;
|
|
108
|
+
/** The per-wallet cap the launch should write, in this token's base units. A decimal string. */
|
|
109
|
+
walletCap: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
/** Resolve one immutable rules config after the launch has been verified. */
|
|
89
113
|
export type VerifiedLaunch = Readonly<Omit<Launch, 'poolKey'>> & { readonly poolKey: Readonly<PoolKey> };
|
|
90
114
|
export type ConfigForLaunch<Config> = (launch: VerifiedLaunch) => Promise<Config>;
|
|
@@ -118,17 +142,25 @@ export interface GateAnnouncement {
|
|
|
118
142
|
contracts: {
|
|
119
143
|
positionManager: `0x${string}`;
|
|
120
144
|
spendGatedCalculator: `0x${string}`;
|
|
145
|
+
/** The approved swap router a paired-token buy goes through. Present on a `v1_3` stack. */
|
|
146
|
+
poolSwap?: `0x${string}`;
|
|
121
147
|
};
|
|
122
148
|
/** The address launches must name as their trusted signer for this gate to adopt them. */
|
|
123
149
|
signer: `0x${string}`;
|
|
124
150
|
/** May lift the gate early or rotate the signer on-chain; the launch writes it into the pool. */
|
|
125
151
|
settler: `0x${string}`;
|
|
126
|
-
/** A decimal string — wei does not survive JSON as a number. */
|
|
127
|
-
|
|
152
|
+
/** The per-wallet cap for an ETH-paired launch, in wei. A decimal string — wei does not survive JSON as a number. */
|
|
153
|
+
walletCap: string;
|
|
128
154
|
roundDurationMs: number;
|
|
129
155
|
minLobbyLeadMs: number;
|
|
130
156
|
gateEndsAtGraceS: number;
|
|
131
|
-
flaunchVariant:
|
|
157
|
+
flaunchVariant: FlaunchVariant;
|
|
158
|
+
/**
|
|
159
|
+
* The non-ETH paired tokens this gate will run rounds on, each with the cap a launch on it should
|
|
160
|
+
* write. Absent or empty means ETH pairings only. Launch tooling refuses a pairing that is not
|
|
161
|
+
* listed here, which is the fail-closed answer for a gate that has not been told the token's price.
|
|
162
|
+
*/
|
|
163
|
+
spendTokens?: AnnouncedSpendToken[];
|
|
132
164
|
requiresEoa: boolean;
|
|
133
165
|
accepting: boolean;
|
|
134
166
|
/**
|
|
@@ -170,22 +202,26 @@ const ADMIT_ALL: Admit = () => ({ ok: true });
|
|
|
170
202
|
/** What the socket says, which is only the truth when nothing is proxying in front of the gate. */
|
|
171
203
|
const ipOf = (request: FastifyRequest): string | null => request.ip || null;
|
|
172
204
|
|
|
173
|
-
/**
|
|
205
|
+
/** Bigint has no JSON form, so terms cross as decimal strings, exactly as the economy balance does. */
|
|
174
206
|
const wireTerms = (terms: RoundTerms): WireRoundTerms => ({
|
|
175
207
|
maxPointsPerPlayer: terms.maxPointsPerPlayer,
|
|
176
|
-
|
|
177
|
-
|
|
208
|
+
walletCap: terms.walletCap.toString(),
|
|
209
|
+
unitsPerPoint: terms.unitsPerPoint.toString(),
|
|
178
210
|
pointsPerDollar: terms.pointsPerDollar,
|
|
179
|
-
|
|
211
|
+
usdPerSpendToken: terms.usdPerSpendToken,
|
|
212
|
+
spendToken: terms.spendToken,
|
|
180
213
|
});
|
|
181
214
|
|
|
215
|
+
/** Only native ETH is priced without being told: everything else needs a `priceFor`. */
|
|
216
|
+
const ethOnly = (usdPerEth: number): PriceFor => (token) => (token.isNative ? usdPerEth : null);
|
|
217
|
+
|
|
182
218
|
/** The same encoding for the per-player balance, wherever it leaves the process. */
|
|
183
219
|
const wireEconomy = (balance: EconomyBalance): WireEconomyBalance => ({
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
220
|
+
unitsPerPoint: balance.unitsPerPoint.toString(),
|
|
221
|
+
earned: balance.earned.toString(),
|
|
222
|
+
held: balance.held.toString(),
|
|
223
|
+
spent: balance.spent.toString(),
|
|
224
|
+
available: balance.available.toString(),
|
|
189
225
|
holdExpiresAt: balance.holdExpiresAt,
|
|
190
226
|
});
|
|
191
227
|
|
|
@@ -389,11 +425,27 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
389
425
|
app.log.error(error);
|
|
390
426
|
return { ok: false, status: 503, message: 'this game is still being prepared' };
|
|
391
427
|
}
|
|
428
|
+
// The token the chain measures this pool's spend in decides the round's units. A launch this
|
|
429
|
+
// gate has no price for is refused rather than priced as ETH: on a six-decimal token that guess
|
|
430
|
+
// authorises a trillion times the intended spend.
|
|
431
|
+
const spendToken = launch.spendToken ?? ETH_SPEND_TOKEN;
|
|
432
|
+
let usdPerSpendToken: number | null;
|
|
433
|
+
try {
|
|
434
|
+
usdPerSpendToken = await (options.priceFor ?? ethOnly(options.usdPerEth))(spendToken);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
app.log.error(error);
|
|
437
|
+
return { ok: false, status: 503, message: 'this game is still being prepared' };
|
|
438
|
+
}
|
|
439
|
+
if (usdPerSpendToken === null || !Number.isFinite(usdPerSpendToken) || usdPerSpendToken <= 0) {
|
|
440
|
+
app.log.error(`${launch.coinAddress}: no price for spend token ${spendToken.symbol} (${spendToken.address})`);
|
|
441
|
+
return { ok: false, status: 409, message: `this game is not set up for ${spendToken.symbol} yet` };
|
|
442
|
+
}
|
|
392
443
|
const economy = resolveEconomy({
|
|
393
444
|
maxPointsPerPlayer,
|
|
394
|
-
|
|
445
|
+
walletCap: launch.walletCap,
|
|
395
446
|
pointsPerDollar: options.pointsPerDollar,
|
|
396
|
-
|
|
447
|
+
usdPerSpendToken,
|
|
448
|
+
decimals: spendToken.decimals,
|
|
397
449
|
});
|
|
398
450
|
if (!economy.ok) {
|
|
399
451
|
app.log.error(
|
|
@@ -409,10 +461,11 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
409
461
|
config,
|
|
410
462
|
seed: Number(BigInt(launch.poolId) % 2147483647n),
|
|
411
463
|
window: { opensAt: launch.opensAt, closesAt: launch.closesAt },
|
|
412
|
-
|
|
464
|
+
unitsPerPoint: economy.economy.unitsPerPoint,
|
|
413
465
|
pointsPerDollar: economy.economy.pointsPerDollar,
|
|
414
|
-
|
|
415
|
-
|
|
466
|
+
spendToken,
|
|
467
|
+
usdPerSpendToken: economy.economy.usdPerSpendToken,
|
|
468
|
+
walletCap: launch.walletCap,
|
|
416
469
|
maxPointsPerPlayer,
|
|
417
470
|
scoreAuthority,
|
|
418
471
|
poolId: launch.poolId,
|
|
@@ -499,22 +552,41 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
499
552
|
*/
|
|
500
553
|
app.get('/stats', async (_request, reply) => {
|
|
501
554
|
void reply.header('access-control-allow-origin', '*');
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
555
|
+
// Spend is summed per spend token: base units of two different tokens do not add. The flat
|
|
556
|
+
// `spendWeiTotal` keeps its original meaning — wei, across ETH rounds — for dashboards built
|
|
557
|
+
// against it; `spendByToken` is the honest figure for a gate that runs more than one pairing.
|
|
558
|
+
const [{ rows }, byToken] = await Promise.all([
|
|
559
|
+
pool.query<{
|
|
560
|
+
rounds_total: string;
|
|
561
|
+
players_total: string;
|
|
562
|
+
spend_wei_total: string;
|
|
563
|
+
last_round_at: string | null;
|
|
564
|
+
}>(
|
|
565
|
+
`select (select count(*) from rounds) as rounds_total,
|
|
566
|
+
(select count(distinct player) from balances) as players_total,
|
|
567
|
+
(select coalesce(sum(b.chain_spent_wei), 0)::text
|
|
568
|
+
from balances b join rounds r on r.id = b.round_id
|
|
569
|
+
where r.spend_decimals = 18) as spend_wei_total,
|
|
570
|
+
(select max(opens_at) from rounds) as last_round_at`,
|
|
571
|
+
),
|
|
572
|
+
pool.query<{ spend_token: string; spend_symbol: string; spend_decimals: number; total: string }>(
|
|
573
|
+
`select r.spend_token, r.spend_symbol, r.spend_decimals, coalesce(sum(b.chain_spent_wei), 0)::text as total
|
|
574
|
+
from rounds r left join balances b on b.round_id = r.id
|
|
575
|
+
group by r.spend_token, r.spend_symbol, r.spend_decimals
|
|
576
|
+
order by r.spend_token`,
|
|
577
|
+
),
|
|
578
|
+
]);
|
|
513
579
|
const totals = rows[0]!;
|
|
514
580
|
return {
|
|
515
581
|
roundsTotal: Number(totals.rounds_total),
|
|
516
582
|
playersTotal: Number(totals.players_total),
|
|
517
583
|
spendWeiTotal: totals.spend_wei_total,
|
|
584
|
+
spendByToken: byToken.rows.map((row) => ({
|
|
585
|
+
address: row.spend_token,
|
|
586
|
+
symbol: row.spend_symbol,
|
|
587
|
+
decimals: Number(row.spend_decimals),
|
|
588
|
+
total: row.total,
|
|
589
|
+
})),
|
|
518
590
|
lastRoundAt: totals.last_round_at === null ? null : Number(totals.last_round_at),
|
|
519
591
|
};
|
|
520
592
|
});
|
|
@@ -672,7 +744,7 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
672
744
|
return reply.code(result.refuse === 'award.invalid' ? 400 : 404).send({ refuse: result.refuse });
|
|
673
745
|
}
|
|
674
746
|
|
|
675
|
-
// `
|
|
747
|
+
// `held` moves on its own when an authorisation expires, so this answer is only true at the
|
|
676
748
|
// moment it is read. Points are monotone; the wei beside them are not.
|
|
677
749
|
const balance = await claims.economyBalance(result.standing.roundId, result.standing.player);
|
|
678
750
|
return reply
|
|
@@ -869,11 +941,11 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
869
941
|
const balance = viewer
|
|
870
942
|
? await claims.economyBalance(id, viewer)
|
|
871
943
|
: {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
944
|
+
unitsPerPoint: room.terms.unitsPerPoint,
|
|
945
|
+
earned: 0n,
|
|
946
|
+
held: 0n,
|
|
947
|
+
spent: 0n,
|
|
948
|
+
available: 0n,
|
|
877
949
|
holdExpiresAt: null,
|
|
878
950
|
};
|
|
879
951
|
const economy = wireEconomy(balance);
|
package/src/store.ts
CHANGED
|
@@ -18,6 +18,15 @@ create table if not exists rounds (
|
|
|
18
18
|
-- was priced at.
|
|
19
19
|
points_per_dollar double precision not null check (points_per_dollar > 0),
|
|
20
20
|
usd_per_eth double precision not null check (usd_per_eth > 0),
|
|
21
|
+
-- The token spend is measured in: the coin's paired token, in whose base units every *_wei column
|
|
22
|
+
-- of this round is denominated. Defaults describe ETH, which is what every round was before the
|
|
23
|
+
-- columns existed, so a pre-existing row reads back exactly as it was priced. usd_per_eth holds
|
|
24
|
+
-- the rate normalised to eighteen decimals (see economy.ts); usd_per_spend_token the token's own
|
|
25
|
+
-- price, null on rows that predate it (for which usd_per_eth is that price).
|
|
26
|
+
spend_token text not null default '0x0000000000000000000000000000000000000000' check (spend_token <> ''),
|
|
27
|
+
spend_symbol text not null default 'ETH' check (spend_symbol <> ''),
|
|
28
|
+
spend_decimals integer not null default 18 check (spend_decimals between 0 and 36),
|
|
29
|
+
usd_per_spend_token double precision check (usd_per_spend_token > 0),
|
|
21
30
|
max_points_per_player bigint not null check (max_points_per_player > 0),
|
|
22
31
|
score_authority text not null check (score_authority in ('rules', 'external-server')),
|
|
23
32
|
pool_id text not null,
|
|
@@ -68,6 +77,18 @@ alter table rounds alter column score_authority set not null;
|
|
|
68
77
|
alter table rounds alter column points_per_dollar set not null;
|
|
69
78
|
alter table rounds alter column usd_per_eth set not null;
|
|
70
79
|
|
|
80
|
+
-- Paired-token rounds (0.5.0). Every earlier round was an ETH round, so the defaults ARE the
|
|
81
|
+
-- backfill: Postgres fills a new not-null column with its default, and ETH/18 is exactly what those
|
|
82
|
+
-- rows were priced in. No guard is needed — nothing here has to be guessed.
|
|
83
|
+
alter table rounds
|
|
84
|
+
add column if not exists spend_token text not null default '0x0000000000000000000000000000000000000000' check (spend_token <> '');
|
|
85
|
+
alter table rounds
|
|
86
|
+
add column if not exists spend_symbol text not null default 'ETH' check (spend_symbol <> '');
|
|
87
|
+
alter table rounds
|
|
88
|
+
add column if not exists spend_decimals integer not null default 18 check (spend_decimals between 0 and 36);
|
|
89
|
+
alter table rounds
|
|
90
|
+
add column if not exists usd_per_spend_token double precision check (usd_per_spend_token > 0);
|
|
91
|
+
|
|
71
92
|
create table if not exists balances (
|
|
72
93
|
round_id text not null references rounds(id) on delete cascade,
|
|
73
94
|
player text not null,
|