@flayerlabs/gamemode-gate 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -4
- package/dist/registry.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 +42 -35
- 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 +38 -20
- 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/registry.ts +6 -3
- package/src/room.ts +15 -0
- package/src/server.ts +41 -35
- package/src/settlement.ts +12 -43
- package/src/standings.ts +9 -1
- package/src/store.ts +38 -20
- package/src/version.ts +2 -2
package/dist/standings.js
CHANGED
|
@@ -27,7 +27,7 @@ export class Standings {
|
|
|
27
27
|
async forCoin(coin, now = Date.now()) {
|
|
28
28
|
if (typeof coin !== 'string' || !isAddress(coin, { strict: false }))
|
|
29
29
|
return null;
|
|
30
|
-
const rounds = await this.pool.query(`select id, pool_id, coin_address, coin_symbol, opens_at, closes_at,
|
|
30
|
+
const rounds = await this.pool.query(`select id, pool_id, coin_address, coin_symbol, opens_at, closes_at, books_close_at, settled_at,
|
|
31
31
|
spend_token, spend_symbol, spend_decimals, usd_per_spend_token, usd_per_eth
|
|
32
32
|
from rounds
|
|
33
33
|
where lower(coin_address) = $1 and game_id = $2
|
|
@@ -50,6 +50,9 @@ export class Standings {
|
|
|
50
50
|
]),
|
|
51
51
|
]);
|
|
52
52
|
const window = { opensAt: Number(round.opens_at), closesAt: Number(round.closes_at) };
|
|
53
|
+
// `settled_at` is stamped by the gate's ticker after its last clean reconcile, so it is the one
|
|
54
|
+
// fact that separates "ended, still settling" from "final". The phase is clock-based and cannot.
|
|
55
|
+
const settledAt = round.settled_at === null ? null : Number(round.settled_at);
|
|
53
56
|
return {
|
|
54
57
|
roundId: round.id,
|
|
55
58
|
poolId: round.pool_id,
|
|
@@ -57,6 +60,9 @@ export class Standings {
|
|
|
57
60
|
symbol: round.coin_symbol,
|
|
58
61
|
phase: roundPhaseAt(window, now),
|
|
59
62
|
...window,
|
|
63
|
+
booksCloseAt: Number(round.books_close_at),
|
|
64
|
+
settledAt,
|
|
65
|
+
final: settledAt !== null,
|
|
60
66
|
spendToken: {
|
|
61
67
|
address: round.spend_token,
|
|
62
68
|
symbol: round.spend_symbol,
|
package/dist/standings.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standings.js","sourceRoot":"","sources":["../src/standings.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,GAGb,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC;;;;;;;;;GASG;AACH,MAAM,OAAO,SAAS;IAED;IACA;IAFnB,YACmB,IAAU,EACV,MAAc;QADd,SAAI,GAAJ,IAAI,CAAM;QACV,WAAM,GAAN,MAAM,CAAQ;IAC9B,CAAC;IAEJ;;;;;OAKG;IACH,KAAK,CAAC,OAAO,CAAC,IAAa,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QACnD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;QAEjF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,
|
|
1
|
+
{"version":3,"file":"standings.js","sourceRoot":"","sources":["../src/standings.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Pool, PoolClient } from 'pg';
|
|
|
6
6
|
* table is how a schema drifts: one adds a column, the other's migration never runs, and the
|
|
7
7
|
* failure appears somewhere neither of them looks.
|
|
8
8
|
*/
|
|
9
|
-
export declare const SCHEMA = "\ncreate table if not exists rounds (\n id text primary key,\n game_id text not null check (game_id <> ''),\n wei_per_point numeric(78,0) not null check (wei_per_point > 0),\n wallet_cap_wei numeric(78,0) not null check (wallet_cap_wei > 0),\n -- The two settings that produced wei_per_point, frozen beside it. They are gate-process config,\n -- so an operator who restarts with a new ETH price must not retroactively change what a round\n -- was priced at.\n points_per_dollar double precision not null check (points_per_dollar > 0),\n usd_per_eth double precision not null check (usd_per_eth > 0),\n -- The token spend is measured in: the coin's paired token, in whose base units every *_wei column\n -- of this round is denominated. Defaults describe ETH, which is what every round was before the\n -- columns existed, so a pre-existing row reads back exactly as it was priced. usd_per_eth holds\n -- the rate normalised to eighteen decimals (see economy.ts); usd_per_spend_token the token's own\n -- price, null on rows that predate it (for which usd_per_eth is that price).\n spend_token text not null default '0x0000000000000000000000000000000000000000' check (spend_token <> ''),\n spend_symbol text not null default 'ETH' check (spend_symbol <> ''),\n spend_decimals integer not null default 18 check (spend_decimals between 0 and 36),\n usd_per_spend_token double precision check (usd_per_spend_token > 0),\n max_points_per_player bigint not null check (max_points_per_player > 0),\n score_authority text not null check (score_authority in ('rules', 'external-server')),\n pool_id text not null,\n opens_at bigint not null,\n closes_at bigint not null,\n books_close_at bigint not null,\n coin_address text,\n coin_name text not null,\n coin_symbol text not null,\n coin_image_url text,\n -- The game's own state, opaque to the platform. Written after every command, so a restart\n -- resumes from here rather than replaying history.\n state jsonb not null,\n -- Commands applied so far. Doubles as the cursor a reconnecting client resumes from.\n seq integer not null default 0 check (seq >= 0),\n constraint rounds_window check (opens_at < closes_at),\n constraint rounds_books_tail check (books_close_at >= closes_at)\n);\n\n-- A developer may already have run the first published schema without creating any rounds. Add\n-- the new authority fields in place for that empty setup. Existing round data needs a deliberate\n-- reset because its original reward bound cannot be reconstructed safely.\nalter table rounds\n add column if not exists game_id text check (game_id <> '');\nalter table rounds\n add column if not exists max_points_per_player bigint check (max_points_per_player > 0);\nalter table rounds\n add column if not exists score_authority text check (score_authority in ('rules', 'external-server'));\nalter table rounds\n add column if not exists points_per_dollar double precision check (points_per_dollar > 0);\nalter table rounds\n add column if not exists usd_per_eth double precision check (usd_per_eth > 0);\ndo $$\nbegin\n if exists (\n select 1 from rounds\n where game_id is null or max_points_per_player is null or score_authority is null\n or points_per_dollar is null or usd_per_eth is null\n ) then\n raise exception 'pre-release round rows cannot be upgraded safely'\n using hint = 'Delete the pre-release round data, then run migrate(pool) again.';\n end if;\nend\n$$;\nalter table rounds alter column game_id set not null;\nalter table rounds alter column max_points_per_player set not null;\nalter table rounds alter column score_authority set not null;\nalter table rounds alter column points_per_dollar set not null;\nalter table rounds alter column usd_per_eth set not null;\n\n-- Paired-token rounds (0.5.0). Every earlier round was an ETH round, so the defaults ARE the\n-- backfill: Postgres fills a new not-null column with its default, and ETH/18 is exactly what those\n-- rows were priced in. No guard is needed \u2014 nothing here has to be guessed.\nalter table rounds\n add column if not exists spend_token text not null default '0x0000000000000000000000000000000000000000' check (spend_token <> '');\nalter table rounds\n add column if not exists spend_symbol text not null default 'ETH' check (spend_symbol <> '');\nalter table rounds\n add column if not exists spend_decimals integer not null default 18 check (spend_decimals between 0 and 36);\nalter table rounds\n add column if not exists usd_per_spend_token double precision check (usd_per_spend_token > 0);\n\n-- Any wired PositionManager (0.6.0). The manager a round's coin was launched on, as the gate\n-- resolved it from the coin at adoption. Null on rounds adopted before the gate recorded it, and on\n-- demo and mock rounds; nothing reads it back for gameplay, it is provenance for the operator.\nalter table rounds\n add column if not exists position_manager text check (position_manager <> '');\n\ncreate table if not exists balances (\n round_id text not null references rounds(id) on delete cascade,\n player text not null,\n points bigint not null default 0 check (points >= 0),\n -- What the chain says this wallet has already spent on this pool. The contract keeps this and\n -- enforces it whatever we believe, so it is the only figure that is actually true.\n chain_spent_wei numeric(78,0) not null default 0 check (chain_spent_wei >= 0),\n primary key (round_id, player)\n);\n\n-- One row per authorisation ever issued. Nothing is deleted: a signature that exists in the world\n-- cannot be un-issued, so the ledger records its fate instead of forgetting it.\ncreate table if not exists claims (\n nonce bigint generated always as identity primary key,\n round_id text not null references rounds(id) on delete cascade,\n player text not null,\n request_id varchar(128) not null check (request_id <> ''),\n requested_wei numeric(78,0) not null check (requested_wei > 0),\n wei numeric(78,0) not null check (wei > 0 and wei <= requested_wei),\n -- Held or released, and nothing else. Whether a swap LANDED is the chain's to say, not ours.\n status text not null check (status in ('held', 'released')),\n deadline bigint not null,\n -- Persisted before a payload leaves the gate. Until signing completes, both values are null.\n signature text,\n signer text,\n created_at timestamptz not null default now(),\n constraint claims_by_request unique (round_id, player, request_id),\n constraint claims_signature_pair check ((signature is null) = (signer is null))\n);\n\ncreate index if not exists claims_by_player on claims (round_id, player) where status = 'held';\n\n-- Trusted creator servers use one stable event id for every score increment. The event and the\n-- balance update commit together, so a retry can recover the first result without paying twice.\ncreate table if not exists award_events (\n round_id text not null references rounds(id) on delete cascade,\n event_id varchar(128) not null check (event_id <> ''),\n player text not null,\n points bigint not null check (points > 0),\n total_points bigint not null check (total_points >= points),\n created_at timestamptz not null default now(),\n primary key (round_id, event_id)\n);\n\n-- Which game a coin plays. Addresses are chain-local, so the pin is one row per (chain, coin).\ncreate table if not exists registrations (\n chain_id bigint not null check (chain_id > 0),\n coin text not null check (coin <> ''),\n game_id text not null check (game_id <> ''),\n gate_origin text not null check (gate_origin <> ''),\n deploy_id text not null check (deploy_id <> ''),\n game_server_origins text[] not null default '{}'::text[],\n disabled_at timestamptz,\n updated_at timestamptz not null default now(),\n constraint registrations_game_server_origins_bounded check (cardinality(game_server_origins) <= 4),\n primary key (chain_id, coin)\n);\n\n-- Accepted play sessions: one row per wallet and round, keyed by a hash of both so a wallet is never\n-- stored in the clear. Independent of scoring and of websocket reconnects. play_tracking records\n-- when this gate began counting, so a dashboard can tell \"no plays\" from \"not yet tracked\".\ncreate table if not exists play_sessions (\n id text primary key,\n round_id text not null references rounds(id) on delete cascade,\n started_at bigint not null\n);\ncreate index if not exists play_sessions_started_id_idx on play_sessions(started_at, id);\ncreate table if not exists play_tracking (\n id boolean primary key default true check (id),\n started_at bigint not null\n);\ninsert into play_tracking(id, started_at) values (true, (extract(epoch from clock_timestamp()) * 1000)::bigint) on conflict do nothing;\n\n-- What actually happened, in order. Not used to rebuild state \u2014 the snapshot does that \u2014 but it is\n-- the record of why a state looks the way it does, which is what an incident needs.\ncreate table if not exists commands (\n round_id text not null references rounds(id) on delete cascade,\n seq integer not null check (seq > 0),\n command jsonb not null,\n primary key (round_id, seq)\n);\n";
|
|
9
|
+
export declare const SCHEMA = "\ncreate table if not exists rounds (\n id text primary key,\n game_id text not null check (game_id <> ''),\n wei_per_point numeric(78,0) not null check (wei_per_point > 0),\n wallet_cap_wei numeric(78,0) not null check (wallet_cap_wei > 0),\n -- The two settings that produced wei_per_point, frozen beside it. They are gate-process config,\n -- so an operator who restarts with a new ETH price must not retroactively change what a round\n -- was priced at.\n points_per_dollar double precision not null check (points_per_dollar > 0),\n usd_per_eth double precision not null check (usd_per_eth > 0),\n -- The token spend is measured in: the coin's paired token, in whose base units every *_wei column\n -- of this round is denominated. Defaults describe ETH, which is what every round was before the\n -- columns existed, so a pre-existing row reads back exactly as it was priced. usd_per_eth holds\n -- the rate normalised to eighteen decimals (see economy.ts); usd_per_spend_token the token's own\n -- price, null on rows that predate it (for which usd_per_eth is that price).\n spend_token text not null default '0x0000000000000000000000000000000000000000' check (spend_token <> ''),\n spend_symbol text not null default 'ETH' check (spend_symbol <> ''),\n spend_decimals integer not null default 18 check (spend_decimals between 0 and 36),\n usd_per_spend_token double precision check (usd_per_spend_token > 0),\n max_points_per_player bigint not null check (max_points_per_player > 0),\n score_authority text not null check (score_authority in ('rules', 'external-server')),\n pool_id text not null,\n opens_at bigint not null,\n closes_at bigint not null,\n books_close_at bigint not null,\n coin_address text,\n coin_name text not null,\n coin_symbol text not null,\n coin_image_url text,\n -- The game's own state, opaque to the platform. Written after every command, so a restart\n -- resumes from here rather than replaying history.\n state jsonb not null,\n -- Commands applied so far. Doubles as the cursor a reconnecting client resumes from.\n seq integer not null default 0 check (seq >= 0),\n constraint rounds_window check (opens_at < closes_at),\n constraint rounds_books_tail check (books_close_at >= closes_at)\n);\n\n-- A developer may already have run the first published schema without creating any rounds. Add\n-- the new authority fields in place for that empty setup. Existing round data needs a deliberate\n-- reset because its original reward bound cannot be reconstructed safely.\nalter table rounds\n add column if not exists game_id text check (game_id <> '');\nalter table rounds\n add column if not exists max_points_per_player bigint check (max_points_per_player > 0);\nalter table rounds\n add column if not exists score_authority text check (score_authority in ('rules', 'external-server'));\nalter table rounds\n add column if not exists points_per_dollar double precision check (points_per_dollar > 0);\nalter table rounds\n add column if not exists usd_per_eth double precision check (usd_per_eth > 0);\ndo $$\nbegin\n if exists (\n select 1 from rounds\n where game_id is null or max_points_per_player is null or score_authority is null\n or points_per_dollar is null or usd_per_eth is null\n ) then\n raise exception 'pre-release round rows cannot be upgraded safely'\n using hint = 'Delete the pre-release round data, then run migrate(pool) again.';\n end if;\nend\n$$;\nalter table rounds alter column game_id set not null;\nalter table rounds alter column max_points_per_player set not null;\nalter table rounds alter column score_authority set not null;\nalter table rounds alter column points_per_dollar set not null;\nalter table rounds alter column usd_per_eth set not null;\n\n-- Paired-token rounds (0.5.0). Every earlier round was an ETH round, so the defaults ARE the\n-- backfill: Postgres fills a new not-null column with its default, and ETH/18 is exactly what those\n-- rows were priced in. No guard is needed \u2014 nothing here has to be guessed.\nalter table rounds\n add column if not exists spend_token text not null default '0x0000000000000000000000000000000000000000' check (spend_token <> '');\nalter table rounds\n add column if not exists spend_symbol text not null default 'ETH' check (spend_symbol <> '');\nalter table rounds\n add column if not exists spend_decimals integer not null default 18 check (spend_decimals between 0 and 36);\nalter table rounds\n add column if not exists usd_per_spend_token double precision check (usd_per_spend_token > 0);\n\n-- Any wired PositionManager (0.6.0). The manager a round's coin was launched on, as the gate\n-- resolved it from the coin at adoption. Null on rounds adopted before the gate recorded it, and on\n-- demo and mock rounds; nothing reads it back for gameplay, it is provenance for the operator.\nalter table rounds\n add column if not exists position_manager text check (position_manager <> '');\n\n-- Round finalization (0.7.0). The moment the gate finished its last clean settlement pass past\n-- books_close_at, in ms since the epoch: from here chain_spent_wei cannot move. Null while a round is still\n-- settling, and on rounds a gate below 0.7.0 closed \u2014 nothing can reconstruct it for those.\nalter table rounds\n add column if not exists settled_at bigint;\n\ncreate table if not exists balances (\n round_id text not null references rounds(id) on delete cascade,\n player text not null,\n points bigint not null default 0 check (points >= 0),\n -- What the chain says this wallet has already spent on this pool. The contract keeps this and\n -- enforces it whatever we believe, so it is the only figure that is actually true.\n chain_spent_wei numeric(78,0) not null default 0 check (chain_spent_wei >= 0),\n primary key (round_id, player)\n);\n\n-- One row per signed authorisation, kept only so a retried or replica-raced request returns the\n-- exact bytes already handed out. Nothing here is a reservation: the signature carries a\n-- cumulative ceiling the calculator compares against its own walletSpentWei, so an unused one\n-- costs nothing and there is no fate to record.\ncreate table if not exists authorisations (\n round_id text not null references rounds(id) on delete cascade,\n player text not null,\n request_id varchar(128) not null check (request_id <> ''),\n -- What the caller asked for, so a retry with a different amount is refused, not silently served.\n requested_wei numeric(78,0) not null check (requested_wei > 0),\n -- The signed field: min(earned, wallet cap) at issue, in the round's spend-token base units.\n spend_ceiling_wei numeric(78,0) not null check (spend_ceiling_wei > 0),\n -- The exact-in amount the caller intended, clamped to what was available. Echoed, not signed.\n spend_wei numeric(78,0) not null check (spend_wei > 0 and spend_wei <= spend_ceiling_wei),\n deadline bigint not null,\n signature text not null,\n signer text not null,\n created_at timestamptz not null default now(),\n primary key (round_id, player, request_id)\n);\n\n-- 0.7.0 hard cutover. Holds no longer exist, and every row the old table could carry was signed\n-- under the version 1 calculator domain that a 0.7.0 gate cannot serve. Dropping it is the honest\n-- migration; a gate should not be upgraded until every live round has passed its books close.\ndrop index if exists claims_by_player;\ndrop table if exists claims;\n\n-- Trusted creator servers use one stable event id for every score increment. The event and the\n-- balance update commit together, so a retry can recover the first result without paying twice.\ncreate table if not exists award_events (\n round_id text not null references rounds(id) on delete cascade,\n event_id varchar(128) not null check (event_id <> ''),\n player text not null,\n points bigint not null check (points > 0),\n total_points bigint not null check (total_points >= points),\n created_at timestamptz not null default now(),\n primary key (round_id, event_id)\n);\n\n-- Which game a coin plays. Addresses are chain-local, so the pin is one row per (chain, coin).\ncreate table if not exists registrations (\n chain_id bigint not null check (chain_id > 0),\n coin text not null check (coin <> ''),\n game_id text not null check (game_id <> ''),\n gate_origin text not null check (gate_origin <> ''),\n deploy_id text not null check (deploy_id <> ''),\n game_server_origins text[] not null default '{}'::text[],\n disabled_at timestamptz,\n updated_at timestamptz not null default now(),\n constraint registrations_game_server_origins_bounded check (cardinality(game_server_origins) <= 32),\n primary key (chain_id, coin)\n);\n\n-- 0.7.1 raised the reviewed origin bound from four to thirty-two: twenty game servers plus the\n-- other-chain gate origins a platform folds into the same list. The create table above only runs\n-- for a new database, so rewrite the check in place for a gate that already has the table.\nalter table registrations drop constraint if exists registrations_game_server_origins_bounded;\nalter table registrations\n add constraint registrations_game_server_origins_bounded check (cardinality(game_server_origins) <= 32);\n\n-- Accepted play sessions: one row per wallet and round, keyed by a hash of both so a wallet is never\n-- stored in the clear. Independent of scoring and of websocket reconnects. play_tracking records\n-- when this gate began counting, so a dashboard can tell \"no plays\" from \"not yet tracked\".\ncreate table if not exists play_sessions (\n id text primary key,\n round_id text not null references rounds(id) on delete cascade,\n started_at bigint not null\n);\ncreate index if not exists play_sessions_started_id_idx on play_sessions(started_at, id);\ncreate table if not exists play_tracking (\n id boolean primary key default true check (id),\n started_at bigint not null\n);\ninsert into play_tracking(id, started_at) values (true, (extract(epoch from clock_timestamp()) * 1000)::bigint) on conflict do nothing;\n\n-- What actually happened, in order. Not used to rebuild state \u2014 the snapshot does that \u2014 but it is\n-- the record of why a state looks the way it does, which is what an incident needs.\ncreate table if not exists commands (\n round_id text not null references rounds(id) on delete cascade,\n seq integer not null check (seq > 0),\n command jsonb not null,\n primary key (round_id, seq)\n);\n";
|
|
10
10
|
/**
|
|
11
11
|
* Run `fn` inside a transaction, committing on success and rolling back on failure.
|
|
12
12
|
*
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,47UA2LlB,CAAC;AAMF;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAiBjG;AAED,sGAAsG;AACtG,wBAAsB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvD;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,MAA4B,CAAC"}
|
package/dist/store.js
CHANGED
|
@@ -93,6 +93,12 @@ alter table rounds
|
|
|
93
93
|
alter table rounds
|
|
94
94
|
add column if not exists position_manager text check (position_manager <> '');
|
|
95
95
|
|
|
96
|
+
-- Round finalization (0.7.0). The moment the gate finished its last clean settlement pass past
|
|
97
|
+
-- books_close_at, in ms since the epoch: from here chain_spent_wei cannot move. Null while a round is still
|
|
98
|
+
-- settling, and on rounds a gate below 0.7.0 closed — nothing can reconstruct it for those.
|
|
99
|
+
alter table rounds
|
|
100
|
+
add column if not exists settled_at bigint;
|
|
101
|
+
|
|
96
102
|
create table if not exists balances (
|
|
97
103
|
round_id text not null references rounds(id) on delete cascade,
|
|
98
104
|
player text not null,
|
|
@@ -103,27 +109,32 @@ create table if not exists balances (
|
|
|
103
109
|
primary key (round_id, player)
|
|
104
110
|
);
|
|
105
111
|
|
|
106
|
-
-- One row per authorisation
|
|
107
|
-
--
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
--
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
112
|
+
-- One row per signed authorisation, kept only so a retried or replica-raced request returns the
|
|
113
|
+
-- exact bytes already handed out. Nothing here is a reservation: the signature carries a
|
|
114
|
+
-- cumulative ceiling the calculator compares against its own walletSpentWei, so an unused one
|
|
115
|
+
-- costs nothing and there is no fate to record.
|
|
116
|
+
create table if not exists authorisations (
|
|
117
|
+
round_id text not null references rounds(id) on delete cascade,
|
|
118
|
+
player text not null,
|
|
119
|
+
request_id varchar(128) not null check (request_id <> ''),
|
|
120
|
+
-- What the caller asked for, so a retry with a different amount is refused, not silently served.
|
|
121
|
+
requested_wei numeric(78,0) not null check (requested_wei > 0),
|
|
122
|
+
-- The signed field: min(earned, wallet cap) at issue, in the round's spend-token base units.
|
|
123
|
+
spend_ceiling_wei numeric(78,0) not null check (spend_ceiling_wei > 0),
|
|
124
|
+
-- The exact-in amount the caller intended, clamped to what was available. Echoed, not signed.
|
|
125
|
+
spend_wei numeric(78,0) not null check (spend_wei > 0 and spend_wei <= spend_ceiling_wei),
|
|
126
|
+
deadline bigint not null,
|
|
127
|
+
signature text not null,
|
|
128
|
+
signer text not null,
|
|
129
|
+
created_at timestamptz not null default now(),
|
|
130
|
+
primary key (round_id, player, request_id)
|
|
124
131
|
);
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
-- 0.7.0 hard cutover. Holds no longer exist, and every row the old table could carry was signed
|
|
134
|
+
-- under the version 1 calculator domain that a 0.7.0 gate cannot serve. Dropping it is the honest
|
|
135
|
+
-- migration; a gate should not be upgraded until every live round has passed its books close.
|
|
136
|
+
drop index if exists claims_by_player;
|
|
137
|
+
drop table if exists claims;
|
|
127
138
|
|
|
128
139
|
-- Trusted creator servers use one stable event id for every score increment. The event and the
|
|
129
140
|
-- balance update commit together, so a retry can recover the first result without paying twice.
|
|
@@ -147,10 +158,17 @@ create table if not exists registrations (
|
|
|
147
158
|
game_server_origins text[] not null default '{}'::text[],
|
|
148
159
|
disabled_at timestamptz,
|
|
149
160
|
updated_at timestamptz not null default now(),
|
|
150
|
-
constraint registrations_game_server_origins_bounded check (cardinality(game_server_origins) <=
|
|
161
|
+
constraint registrations_game_server_origins_bounded check (cardinality(game_server_origins) <= 32),
|
|
151
162
|
primary key (chain_id, coin)
|
|
152
163
|
);
|
|
153
164
|
|
|
165
|
+
-- 0.7.1 raised the reviewed origin bound from four to thirty-two: twenty game servers plus the
|
|
166
|
+
-- other-chain gate origins a platform folds into the same list. The create table above only runs
|
|
167
|
+
-- for a new database, so rewrite the check in place for a gate that already has the table.
|
|
168
|
+
alter table registrations drop constraint if exists registrations_game_server_origins_bounded;
|
|
169
|
+
alter table registrations
|
|
170
|
+
add constraint registrations_game_server_origins_bounded check (cardinality(game_server_origins) <= 32);
|
|
171
|
+
|
|
154
172
|
-- Accepted play sessions: one row per wallet and round, keyed by a hash of both so a wallet is never
|
|
155
173
|
-- stored in the clear. Independent of scoring and of websocket reconnects. play_tracking records
|
|
156
174
|
-- when this gate began counting, so a dashboard can tell "no plays" from "not yet tracked".
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2LrB,CAAC;AAEF,MAAM,cAAc,GAAG;;EAErB,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAI,IAAU,EAAE,EAAkC;IACnF,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5B,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,+BAA+B;QACjC,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,OAAO,EAAE,CAAC;IACf,CAAC;AACH,CAAC;AAED,sGAAsG;AACtG,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAU;IACtC,MAAM,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;QACrC,MAAM,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QAC/B,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAyC,EAAU,EAAE,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC"}
|
package/dist/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 = "0.
|
|
3
|
+
export const GATE_VERSION = "0.7.1";
|
|
4
4
|
/** `@flayerlabs/gamemode-gate@<version>`: the text a build of this package can be searched for. */
|
|
5
|
-
export const GATE_MARKER = "@flayerlabs/gamemode-gate@0.
|
|
5
|
+
export const GATE_MARKER = "@flayerlabs/gamemode-gate@0.7.1";
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flayerlabs/gamemode-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Server-authoritative rooms, economy, admission and registry for Flaunch Game Modes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Flayer Labs",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@fastify/cors": "^11.3.0",
|
|
47
47
|
"@fastify/websocket": "^11.3.0",
|
|
48
|
-
"@flayerlabs/gamemode-spec": "^0.
|
|
48
|
+
"@flayerlabs/gamemode-spec": "^0.7.1",
|
|
49
49
|
"@types/node": "^22.10.2",
|
|
50
50
|
"@types/pg": "^8.21.0",
|
|
51
51
|
"fastify": "^5.11.3",
|
package/src/chain/chains.ts
CHANGED
|
@@ -102,7 +102,9 @@ export const PAIRED_DEPLOYMENTS: Record<number, GateDeployment> = {
|
|
|
102
102
|
*/
|
|
103
103
|
84532: {
|
|
104
104
|
positionManager: '0x8D346f24278C5CD786309161aAC0fC2bbe4c25dc',
|
|
105
|
-
|
|
105
|
+
// v2 (cumulative spend ceilings, domain version 2), deployed 2026-09-14 at block 46822712 on the
|
|
106
|
+
// same dispatcher as v1 0x54cdcf0b…, which stays registered for pools launched before it.
|
|
107
|
+
spendGatedCalculator: '0x8a36826253882c8b505d6d78a71b780ac9c98ea9',
|
|
106
108
|
flaunchVariant: 'v1_3',
|
|
107
109
|
poolSwap: '0xf0f388a31a1745a5e2378b812ed51525f70595be',
|
|
108
110
|
fleth: '0x79FC52701cD4BE6f9Ba9aDC94c207DE37e3314eb',
|
package/src/chain/signer.ts
CHANGED
|
@@ -3,15 +3,19 @@ import { privateKeyToAccount, type PrivateKeyAccount } from 'viem/accounts';
|
|
|
3
3
|
import { gateReadinessTypedData, READINESS_TTL_SECONDS, type GateReadiness, spendAuthorisationTypedData } from '@flayerlabs/gamemode-spec/embed';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Issues the signed
|
|
6
|
+
* Issues the signed authorisations SpendGatedSignerFeeCalculator.trackSwap verifies.
|
|
7
7
|
*
|
|
8
|
-
* EIP-712, over the `SpendAuthorization` struct the calculator declares
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* EIP-712, over the `SpendAuthorization` struct the calculator declares, under the
|
|
9
|
+
* `FlaunchSpendGate` version `2` domain. The struct carries a cumulative ceiling: the most this
|
|
10
|
+
* buyer may EVER have spent on this pool, which the calculator compares against its own
|
|
11
|
+
* `walletSpentWei` accumulator. There is no nonce and no per-purchase amount, because there is
|
|
12
|
+
* nothing to consume — two authorisations with the same ceiling cannot add up, so reusing one is
|
|
13
|
+
* harmless by construction and the gate holds nothing against it.
|
|
14
|
+
*
|
|
15
|
+
* The domain matters: the protocol deploys deterministically across chains, so the same poolId
|
|
16
|
+
* genuinely recurs, and one signing key shared between two environments would otherwise make an
|
|
17
|
+
* authorisation issued for one valid on the other. Version `2` also keeps 0.5.x signatures (a
|
|
18
|
+
* five-field struct with a nonce) from verifying on this calculator and vice versa.
|
|
15
19
|
*
|
|
16
20
|
* The digest MUST match `hashSpendAuthorization` on the calculator exactly. That function is
|
|
17
21
|
* public for this reason — a domain drift is otherwise a silent `InvalidSigner` at swap time
|
|
@@ -22,8 +26,7 @@ export interface SignedPayload {
|
|
|
22
26
|
buyer: `0x${string}`;
|
|
23
27
|
poolId: `0x${string}`;
|
|
24
28
|
deadline: bigint;
|
|
25
|
-
|
|
26
|
-
nonce: bigint;
|
|
29
|
+
spendCeilingWei: bigint;
|
|
27
30
|
signature: `0x${string}`;
|
|
28
31
|
signer: `0x${string}`;
|
|
29
32
|
}
|
|
@@ -62,15 +65,10 @@ export class PayloadSigner {
|
|
|
62
65
|
buyer: `0x${string}`,
|
|
63
66
|
poolId: `0x${string}`,
|
|
64
67
|
deadline: bigint,
|
|
65
|
-
|
|
66
|
-
nonce: bigint,
|
|
68
|
+
spendCeilingWei: bigint,
|
|
67
69
|
): `0x${string}` {
|
|
68
70
|
return hashTypedData(
|
|
69
|
-
spendAuthorisationTypedData(
|
|
70
|
-
{ buyer, poolId, deadline, maxSpendWei, nonce },
|
|
71
|
-
this.chainId,
|
|
72
|
-
this.verifyingContract,
|
|
73
|
-
),
|
|
71
|
+
spendAuthorisationTypedData({ buyer, poolId, deadline, spendCeilingWei }, this.chainId, this.verifyingContract),
|
|
74
72
|
);
|
|
75
73
|
}
|
|
76
74
|
|
|
@@ -78,23 +76,21 @@ export class PayloadSigner {
|
|
|
78
76
|
buyer: `0x${string}`,
|
|
79
77
|
poolId: `0x${string}`,
|
|
80
78
|
deadline: bigint,
|
|
81
|
-
|
|
82
|
-
nonce: bigint,
|
|
79
|
+
spendCeilingWei: bigint,
|
|
83
80
|
): Promise<SignedPayload> {
|
|
84
81
|
const signature = await this.account.signTypedData(
|
|
85
|
-
spendAuthorisationTypedData(
|
|
86
|
-
{ buyer, poolId, deadline, maxSpendWei, nonce },
|
|
87
|
-
this.chainId,
|
|
88
|
-
this.verifyingContract,
|
|
89
|
-
),
|
|
82
|
+
spendAuthorisationTypedData({ buyer, poolId, deadline, spendCeilingWei }, this.chainId, this.verifyingContract),
|
|
90
83
|
);
|
|
91
|
-
return { buyer, poolId, deadline,
|
|
84
|
+
return { buyer, poolId, deadline, spendCeilingWei, signature, signer: this.account.address };
|
|
92
85
|
}
|
|
93
86
|
}
|
|
94
87
|
|
|
95
88
|
/**
|
|
96
89
|
* Encodes a signed payload as the hookData the swap must carry:
|
|
97
|
-
* `abi.encode(address referrer, SignedMessage{buyer, poolId, deadline,
|
|
90
|
+
* `abi.encode(address referrer, SignedMessage{buyer, poolId, deadline, spendCeilingWei, signature})`
|
|
91
|
+
*
|
|
92
|
+
* The referrer word is always zero. The calculator ignores it, but the trusted page checks it, so
|
|
93
|
+
* a creator iframe cannot redirect attribution while keeping a valid signature.
|
|
98
94
|
*/
|
|
99
95
|
export function encodeHookData(payload: SignedPayload): `0x${string}` {
|
|
100
96
|
return encodeAbiParameters(
|
|
@@ -106,8 +102,7 @@ export function encodeHookData(payload: SignedPayload): `0x${string}` {
|
|
|
106
102
|
{ name: 'buyer', type: 'address' },
|
|
107
103
|
{ name: 'poolId', type: 'bytes32' },
|
|
108
104
|
{ name: 'deadline', type: 'uint256' },
|
|
109
|
-
{ name: '
|
|
110
|
-
{ name: 'nonce', type: 'uint256' },
|
|
105
|
+
{ name: 'spendCeilingWei', type: 'uint256' },
|
|
111
106
|
{ name: 'signature', type: 'bytes' },
|
|
112
107
|
],
|
|
113
108
|
},
|
|
@@ -118,8 +113,7 @@ export function encodeHookData(payload: SignedPayload): `0x${string}` {
|
|
|
118
113
|
buyer: payload.buyer,
|
|
119
114
|
poolId: payload.poolId,
|
|
120
115
|
deadline: payload.deadline,
|
|
121
|
-
|
|
122
|
-
nonce: payload.nonce,
|
|
116
|
+
spendCeilingWei: payload.spendCeilingWei,
|
|
123
117
|
signature: payload.signature,
|
|
124
118
|
},
|
|
125
119
|
],
|
package/src/claims.ts
CHANGED
|
@@ -2,26 +2,23 @@ import type { Pool } from 'pg';
|
|
|
2
2
|
import type { PlayerId } from '@flayerlabs/gamemode-spec';
|
|
3
3
|
import type { EconomyBalance } from '@flayerlabs/gamemode-spec/live';
|
|
4
4
|
import { maxUint256 } from 'viem';
|
|
5
|
-
import { Ledger, type
|
|
5
|
+
import { Ledger, type Authorisation as StoredAuthorisation } from './ledger.js';
|
|
6
6
|
import type { PayloadSigner, SignedPayload } from './chain/signer.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Turning points into something a player can actually spend.
|
|
10
10
|
*
|
|
11
11
|
* Two parties have to agree before a buy can happen, and they check different things. The ledger
|
|
12
|
-
* knows what this player earned and what
|
|
13
|
-
*
|
|
14
|
-
* authorisation exist.
|
|
12
|
+
* knows what this player earned and what the chain says they have spent. The signer knows whether
|
|
13
|
+
* it is willing to vouch for this pool at all. Only when both say yes does an authorisation exist.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* What gets signed is a ceiling, not a purchase: the most this wallet may ever have spent on the
|
|
16
|
+
* pool. The calculator compares it against its own running total, so the gate reserves nothing
|
|
17
|
+
* and a signature that is never used costs nobody anything. The one thing this file must get
|
|
18
|
+
* right is idempotency — a retried request must receive the same bytes — and that is a primary
|
|
19
|
+
* key, not a lock.
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
|
-
/** How long an authorisation stays valid. Long enough to approve in a wallet, short enough that an
|
|
22
|
-
* abandoned one does not sit around spendable. */
|
|
23
|
-
export const CLAIM_AUTHORIZATION_TTL_MS = 5 * 60_000;
|
|
24
|
-
const DEADLINE_SECONDS = CLAIM_AUTHORIZATION_TTL_MS / 1_000;
|
|
25
22
|
const REQUEST_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
26
23
|
|
|
27
24
|
/** Request ids cross an unauthenticated parser before they reach Postgres, so bound them here too. */
|
|
@@ -35,7 +32,7 @@ export function isClaimAmount(value: unknown): value is bigint {
|
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
export type Authorisation =
|
|
38
|
-
| { ok: true; payload: SignedPayload }
|
|
35
|
+
| { ok: true; payload: SignedPayload; /** Exact-in amount for this buy, clamped to what was available. */ spend: bigint }
|
|
39
36
|
| { ok: false; refuse: string };
|
|
40
37
|
|
|
41
38
|
export class Claims {
|
|
@@ -43,76 +40,68 @@ export class Claims {
|
|
|
43
40
|
private readonly pool: Pool,
|
|
44
41
|
private readonly ledger: Ledger,
|
|
45
42
|
private readonly signer: PayloadSigner,
|
|
46
|
-
/** Called when a hold could neither be signed nor released, and needs a human. */
|
|
47
|
-
private readonly onStrandedHold?: (nonce: bigint, error: unknown) => void,
|
|
48
43
|
/** An internal seam for exact window-boundary tests. Production uses the process clock. */
|
|
49
44
|
private readonly now: () => number = Date.now,
|
|
50
45
|
) {}
|
|
51
46
|
|
|
52
47
|
/**
|
|
53
|
-
* Authorise a player to spend up to `
|
|
48
|
+
* Authorise a player to spend up to `spendWei`, or explain why not.
|
|
54
49
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
50
|
+
* Returns the largest amount available rather than refusing an over-ask: a player pressing buy
|
|
51
|
+
* wants to spend what they have, and telling them "you asked for too much" when they asked for
|
|
52
|
+
* everything is a worse product than giving them everything. The ceiling in the signature is the
|
|
53
|
+
* same either way — it is what they have earned, not what they asked for.
|
|
54
|
+
*
|
|
55
|
+
* The ledger's view of spend is a conservative mirror of the chain's. Being behind it means
|
|
56
|
+
* signing a ceiling the calculator will refuse at the swap, which to a player is
|
|
57
|
+
* indistinguishable from being robbed; settlement keeps the gap to one reconciliation pass.
|
|
59
58
|
*/
|
|
60
|
-
async authorise(roundId: string, player: PlayerId,
|
|
59
|
+
async authorise(roundId: string, player: PlayerId, spendWei: bigint, requestId: string): Promise<Authorisation> {
|
|
61
60
|
if (!isClaimRequestId(requestId)) return { ok: false, refuse: 'claim.request_invalid' };
|
|
62
|
-
if (!isClaimAmount(
|
|
61
|
+
if (!isClaimAmount(spendWei)) return { ok: false, refuse: 'claim.amount_invalid' };
|
|
63
62
|
|
|
64
63
|
const round = await this.pool.query<{ pool_id: string }>(`select pool_id from rounds where id = $1`, [roundId]);
|
|
65
64
|
const row = round.rows[0];
|
|
66
65
|
if (!row) return { ok: false, refuse: 'claim.no_such_round' };
|
|
66
|
+
const poolId = row.pool_id as `0x${string}`;
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
// still committing at the buzzer, a retry waits for it and recovers it rather than observing
|
|
73
|
-
// no row outside the transaction and being refused as a new, late request.
|
|
74
|
-
const held = await this.ledger.claim(roundId, player, upToWei, requestId, deadline, now);
|
|
75
|
-
if (!held.ok) return { ok: false, refuse: held.refuse };
|
|
68
|
+
// An existing request deliberately wins over the clock and the current balance: it was issued
|
|
69
|
+
// under the earlier call's window, and a retry must get the same answer.
|
|
70
|
+
const existing = await this.ledger.findAuthorisation(roundId, player, requestId);
|
|
71
|
+
if (existing) return this.answer(poolId, existing, spendWei);
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
return { ok: true, payload: payload(row.pool_id, held.claim) };
|
|
80
|
-
}
|
|
81
|
-
if (held.claim.status === 'released') return { ok: false, refuse: 'claim.request_released' };
|
|
82
|
-
return this.finish(row.pool_id, held.claim);
|
|
83
|
-
}
|
|
73
|
+
const quote = await this.ledger.quote(roundId, player, this.now());
|
|
74
|
+
if (!quote.ok) return { ok: false, refuse: quote.refuse };
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return { ok: true, payload: payload(poolId, completed) };
|
|
99
|
-
} catch (error) {
|
|
100
|
-
// The hold was taken and nothing will ever be spent against it, so it goes back. Without
|
|
101
|
-
// this, a signer outage quietly eats a player's allowance for the rest of the round.
|
|
102
|
-
//
|
|
103
|
-
// If the release ALSO fails, the original error still wins. Replacing it with a database
|
|
104
|
-
// error would hide the reason and leave the nonce nowhere in the logs — so it is named here,
|
|
105
|
-
// because that number is what a person needs to give the allowance back by hand.
|
|
106
|
-
await this.ledger.releaseUnsigned(claim.nonce).catch((releaseError: unknown) => {
|
|
107
|
-
this.onStrandedHold?.(claim.nonce, releaseError);
|
|
108
|
-
});
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
76
|
+
const spend = spendWei < quote.available ? spendWei : quote.available;
|
|
77
|
+
const signed = await this.signer.sign(player as `0x${string}`, poolId, quote.deadlineSeconds, quote.ceiling);
|
|
78
|
+
const stored = await this.ledger.storeAuthorisation(roundId, {
|
|
79
|
+
player,
|
|
80
|
+
requestId,
|
|
81
|
+
requestedWei: spendWei,
|
|
82
|
+
spendCeilingWei: signed.spendCeilingWei,
|
|
83
|
+
spendWei: spend,
|
|
84
|
+
deadline: signed.deadline,
|
|
85
|
+
signature: signed.signature,
|
|
86
|
+
signer: signed.signer,
|
|
87
|
+
});
|
|
88
|
+
return this.answer(poolId, stored, spendWei);
|
|
111
89
|
}
|
|
112
90
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return
|
|
91
|
+
private answer(poolId: `0x${string}`, stored: StoredAuthorisation, requestedWei: bigint): Authorisation {
|
|
92
|
+
if (stored.requestedWei !== requestedWei) return { ok: false, refuse: 'claim.request_mismatch' };
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
payload: {
|
|
96
|
+
buyer: stored.player as `0x${string}`,
|
|
97
|
+
poolId,
|
|
98
|
+
deadline: stored.deadline,
|
|
99
|
+
spendCeilingWei: stored.spendCeilingWei,
|
|
100
|
+
signature: stored.signature,
|
|
101
|
+
signer: stored.signer,
|
|
102
|
+
},
|
|
103
|
+
spend: stored.spendWei,
|
|
104
|
+
};
|
|
116
105
|
}
|
|
117
106
|
|
|
118
107
|
/** What this player could spend right now. */
|
|
@@ -125,16 +114,3 @@ export class Claims {
|
|
|
125
114
|
return this.ledger.economyBalance(roundId, player);
|
|
126
115
|
}
|
|
127
116
|
}
|
|
128
|
-
|
|
129
|
-
function payload(poolId: string, claim: Claim): SignedPayload {
|
|
130
|
-
if (!claim.signature || !claim.signer) throw new Error('claim is not signed');
|
|
131
|
-
return {
|
|
132
|
-
buyer: claim.player as `0x${string}`,
|
|
133
|
-
poolId: poolId as `0x${string}`,
|
|
134
|
-
deadline: claim.deadline,
|
|
135
|
-
maxSpendWei: claim.wei,
|
|
136
|
-
nonce: claim.nonce,
|
|
137
|
-
signature: claim.signature,
|
|
138
|
-
signer: claim.signer,
|
|
139
|
-
};
|
|
140
|
-
}
|
package/src/demo.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface Demo {
|
|
|
62
62
|
/** The throwaway wallet standing in for the player's real one. */
|
|
63
63
|
walletAddress: `0x${string}`;
|
|
64
64
|
/** Everything the fake wallet was asked to buy, in order. Empty until somebody buys. */
|
|
65
|
-
purchases: {
|
|
65
|
+
purchases: { spend: string; at: number }[];
|
|
66
66
|
pool: pg.Pool;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -160,7 +160,7 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
const wallet = privateKeyToAccount(DEMO_WALLET_KEY);
|
|
163
|
-
const purchases: {
|
|
163
|
+
const purchases: { spend: string; at: number }[] = [];
|
|
164
164
|
|
|
165
165
|
// Standing in for the page a game is embedded in. In production this is flaunch.gg holding the
|
|
166
166
|
// player's real wallet; here the signature is real and the purchase is only written down.
|
|
@@ -169,9 +169,9 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
|
|
|
169
169
|
signature: await wallet.signMessage({ message: (request.body as { message: string }).message }),
|
|
170
170
|
}));
|
|
171
171
|
app.post('/dev/wallet/buy', (request) => {
|
|
172
|
-
const {
|
|
173
|
-
purchases.push({
|
|
174
|
-
return { spent:
|
|
172
|
+
const { spend } = request.body as { spend: string };
|
|
173
|
+
purchases.push({ spend, at: Date.now() });
|
|
174
|
+
return { spent: spend };
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
return { app, roundId, opensAt, walletAddress: wallet.address, purchases, pool };
|
package/src/index.ts
CHANGED
|
@@ -23,8 +23,7 @@ export { createTurnstileAdmit } from './turnstile.js';
|
|
|
23
23
|
export type { TurnstileAdmitOptions } from './turnstile.js';
|
|
24
24
|
|
|
25
25
|
export { Ledger } from './ledger.js';
|
|
26
|
-
export type {
|
|
27
|
-
export { CLAIM_AUTHORIZATION_TTL_MS } from './claims.js';
|
|
26
|
+
export type { Authorisation, Quote } from './ledger.js';
|
|
28
27
|
export { PayloadSigner, encodeHookData } from './chain/signer.js';
|
|
29
28
|
export type { SignedPayload } from './chain/signer.js';
|
|
30
29
|
export { Discovery, WiringCache, poolIdOf, resolvePositionManager, verifyLaunch } from './chain/discover.js';
|