@flayerlabs/gamemode-gate 0.4.3 → 0.5.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 +27 -3
- package/README.md +8 -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 +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +93 -25
- package/dist/server.js.map +1 -1
- package/dist/standings.d.ts +34 -0
- package/dist/standings.d.ts.map +1 -0
- package/dist/standings.js +103 -0
- package/dist/standings.js.map +1 -0
- 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 +8 -5
- package/src/ledger.ts +18 -18
- package/src/main.ts +181 -19
- package/src/room.ts +41 -14
- package/src/server.ts +142 -35
- package/src/standings.ts +138 -0
- package/src/store.ts +21 -0
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoJrB,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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flayerlabs/gamemode-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.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
|
@@ -13,33 +13,106 @@
|
|
|
13
13
|
* gated on the audit and the multisig handover. Robinhood (4663) is deliberately missing too —
|
|
14
14
|
* that chain is served by the legacy gate, and this library refusing it keeps the two stacks from
|
|
15
15
|
* being pointed at each other by a copied env file.
|
|
16
|
+
*
|
|
17
|
+
* One gate serves exactly ONE (positionManager, spendGatedCalculator) pair. The EIP-712 domain of
|
|
18
|
+
* every authorisation binds it to the calculator address, so a signature minted for one pair is
|
|
19
|
+
* meaningless to the other — a gate cannot straddle two stacks, and this book records each stack
|
|
20
|
+
* as its own entry rather than pretending a chain has one.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Which launch generation the stack's PositionManager belongs to.
|
|
25
|
+
*
|
|
26
|
+
* The names match `FlaunchVersion` in `@flaunch/sdk`, because they name the same generations and
|
|
27
|
+
* two vocabularies for one thing is how a gate ends up configured against the wrong ABI. Each
|
|
28
|
+
* generation takes a different `flaunch()` struct, and getting it wrong is an ABI revert at launch
|
|
29
|
+
* time, so it is recorded per stack rather than guessed.
|
|
30
|
+
*
|
|
31
|
+
* v1_2 — the original Base stack. Carries the two fair-launch fields; flETH pairing only.
|
|
32
|
+
* multichain — the multichain deployment, with the fair-launch fields removed.
|
|
33
|
+
* v1_3 — the current generation, launched through the V1_3 FlaunchZap. The only one that
|
|
34
|
+
* can pair a coin with a token other than ETH, and therefore the only one that can
|
|
35
|
+
* run a paired-token round.
|
|
16
36
|
*/
|
|
37
|
+
export type FlaunchVariant = 'v1_2' | 'multichain' | 'v1_3';
|
|
17
38
|
|
|
18
39
|
export interface GateDeployment {
|
|
19
40
|
positionManager: `0x${string}`;
|
|
20
41
|
spendGatedCalculator: `0x${string}`;
|
|
42
|
+
flaunchVariant: FlaunchVariant;
|
|
43
|
+
/**
|
|
44
|
+
* The single-pool swap router the calculator has approved, when the stack has one. A paired-token
|
|
45
|
+
* buy goes through it (ERC20 in, `hookData` carrying the authorisation), and the calculator binds
|
|
46
|
+
* the buyer to its `msgSender()` — so an unapproved router means every smart-wallet buy reverts.
|
|
47
|
+
* Required for `v1_3`; boot refuses a paired stack without it.
|
|
48
|
+
*/
|
|
49
|
+
poolSwap?: `0x${string}`;
|
|
21
50
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* is an ABI revert at launch time, so it is recorded per chain rather than guessed.
|
|
51
|
+
* The chain's flETH wrapper. A coin paired with it is an ETH round priced at `USD_PER_ETH`, and
|
|
52
|
+
* the wrapper is not the zero address, so the gate has to be told which address means "ETH".
|
|
25
53
|
*/
|
|
26
|
-
|
|
54
|
+
fleth?: `0x${string}`;
|
|
27
55
|
}
|
|
28
56
|
|
|
57
|
+
/** The legacy stack per chain — what `deploymentFor(chainId)` answers with no manager named. */
|
|
29
58
|
export const DEPLOYMENTS: Record<number, GateDeployment> = {
|
|
30
59
|
/**
|
|
31
|
-
* Base Sepolia — the reference chain. Deployed 2026-08-16 (flaunch-contracts
|
|
60
|
+
* Base Sepolia — the reference chain, V1_2 stack. Deployed 2026-08-16 (flaunch-contracts
|
|
32
61
|
* SpendGatedGate.s.sol); both PositionManager fee-calculator slots were read back holding the
|
|
33
62
|
* dispatcher, and the calculator resolves each pool's own paired token (VPT-13).
|
|
34
63
|
*/
|
|
35
64
|
84532: {
|
|
36
65
|
positionManager: '0x4E7cB1e6800a7B297B38BddcecAF9Ca5b6616FDC',
|
|
37
66
|
spendGatedCalculator: '0x8cbbE6b4cFA5Ccf68399Dbf1429d91A21097ebA5',
|
|
38
|
-
flaunchVariant: '
|
|
67
|
+
flaunchVariant: 'v1_2',
|
|
68
|
+
fleth: '0x79FC52701cD4BE6f9Ba9aDC94c207DE37e3314eb',
|
|
39
69
|
},
|
|
40
70
|
};
|
|
41
71
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The paired-token stacks per chain — selected by naming their PositionManager in
|
|
74
|
+
* `POSITION_MANAGER`.
|
|
75
|
+
*/
|
|
76
|
+
export const PAIRED_DEPLOYMENTS: Record<number, GateDeployment> = {
|
|
77
|
+
/**
|
|
78
|
+
* Base Sepolia — the V1_3 v1.3.3 paired-token stack (flaunch-contracts regeneration, deployed
|
|
79
|
+
* 2026-09-03, blocks 46349133–46349202; `@flaunch/sdk` 0.11.4 launches here). Read back
|
|
80
|
+
* 2026-09-04: the manager's `feeCalculator()` is the dispatcher
|
|
81
|
+
* `0xd381f8ea57df43c57cfe6e5b19a0a4700396f28c`, which has this calculator registered; the
|
|
82
|
+
* calculator grants that dispatcher the PositionManager role and has this PoolSwap approved as a
|
|
83
|
+
* router. This is the stack that hosts a coin paired with MUSD
|
|
84
|
+
* (`0xd0B4398976572515a269C6331A143e08a70E9ccC`, six decimals, approved on the retained registry
|
|
85
|
+
* `0x23cb441d18ca75c6a14964b06806df668d45a1c6`) — and flETH and native ETH pairings too, so a gate
|
|
86
|
+
* on it serves ETH rounds as well.
|
|
87
|
+
*
|
|
88
|
+
* SUPERSEDED, recorded so nobody re-adds it by mistake: the `.vpt2` stack (2026-08-06) —
|
|
89
|
+
* PositionManager `0x5558e7271ec2e8b2faaf05f0eedab1cd986be5dc`, calculator
|
|
90
|
+
* `0x2c9127654ded3b6b2ba017e84f44b02cafdf9f55`, PoolSwap `0x62eb5b7b066ff80ce5e32ff1ed42b31c485f716b`.
|
|
91
|
+
* Its pools keep trading on their own hook and gate, but new paired launches land on the v1.3.3
|
|
92
|
+
* manager, and router approval is PER calculator: `0x62eb…` is NOT approved on the v1.3.3 gate,
|
|
93
|
+
* so pointing this entry at the old router would make every smart-wallet buy revert.
|
|
94
|
+
*/
|
|
95
|
+
84532: {
|
|
96
|
+
positionManager: '0x8D346f24278C5CD786309161aAC0fC2bbe4c25dc',
|
|
97
|
+
spendGatedCalculator: '0x54cdcf0bcbc3a33f470e07134c10582f93058a32',
|
|
98
|
+
flaunchVariant: 'v1_3',
|
|
99
|
+
poolSwap: '0xf0f388a31a1745a5e2378b812ed51525f70595be',
|
|
100
|
+
fleth: '0x79FC52701cD4BE6f9Ba9aDC94c207DE37e3314eb',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The deployment for a chain, or undefined for one this library has no address book for.
|
|
106
|
+
*
|
|
107
|
+
* With no manager named, the legacy stack. Naming one selects whichever recorded stack it belongs
|
|
108
|
+
* to — so `POSITION_MANAGER=0x8D34…` on Base Sepolia picks the paired stack, calculator and router
|
|
109
|
+
* included, rather than pairing a paired manager with the legacy calculator by accident.
|
|
110
|
+
*/
|
|
111
|
+
export function deploymentFor(chainId: number, positionManager?: `0x${string}` | null): GateDeployment | undefined {
|
|
112
|
+
if (!positionManager) return DEPLOYMENTS[chainId];
|
|
113
|
+
const wanted = positionManager.toLowerCase();
|
|
114
|
+
for (const candidate of [DEPLOYMENTS[chainId], PAIRED_DEPLOYMENTS[chainId]]) {
|
|
115
|
+
if (candidate && candidate.positionManager.toLowerCase() === wanted) return candidate;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
45
118
|
}
|
package/src/chain/discover.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encodeAbiParameters, keccak256, parseAbi, type PublicClient } from 'viem';
|
|
2
|
+
import { ETH_SPEND_TOKEN, type SpendToken } from '@flayerlabs/gamemode-spec/live';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* All this needs of a client: the ability to read a contract.
|
|
@@ -20,6 +21,12 @@ export type ChainReader = Pick<PublicClient, 'readContract'>;
|
|
|
20
21
|
* launch's own gate parameters, and that is public state the creator paid to write and nobody else
|
|
21
22
|
* can forge. So adoption needs no API key and no allowlist — anyone may ask, and the chain decides.
|
|
22
23
|
* That single property is what makes third-party games permissionless.
|
|
24
|
+
*
|
|
25
|
+
* A gate serves exactly one (PositionManager, calculator) pair and adopts only coins launched on
|
|
26
|
+
* THAT manager: `poolKey(coin)` is asked of the configured manager alone, so a coin on a superseded
|
|
27
|
+
* generation of hooks (which keeps trading on its own gate) reads back as not a launch here. That
|
|
28
|
+
* is deliberate — the EIP-712 domain binds every authorisation to the calculator, so a coin this
|
|
29
|
+
* gate cannot sign for must not be given a room by it.
|
|
23
30
|
*/
|
|
24
31
|
|
|
25
32
|
const poolKeyComponents = [
|
|
@@ -39,17 +46,28 @@ export const positionManagerAbi = [
|
|
|
39
46
|
outputs: [{ name: '', type: 'tuple', components: poolKeyComponents }],
|
|
40
47
|
},
|
|
41
48
|
// The scheduled open of the buy window; 0 once the pool is tradeable.
|
|
42
|
-
...parseAbi([
|
|
49
|
+
...parseAbi([
|
|
50
|
+
'function flaunchesAt(bytes32 _poolId) view returns (uint256)',
|
|
51
|
+
// V1_3 only: the token the pool is paired with. A legacy manager has no such function; a revert
|
|
52
|
+
// there is read as "flETH", which is the only pairing that generation can produce.
|
|
53
|
+
'function pairedToken(bytes32 _poolId) view returns (address)',
|
|
54
|
+
]),
|
|
43
55
|
] as const;
|
|
44
56
|
|
|
45
57
|
export const spendGatedAbi = parseAbi([
|
|
46
58
|
// Four fields. Decoding a four-word return against a shorter ABI is how a check quietly reads the
|
|
47
59
|
// wrong value, so this must match the DEPLOYED calculator rather than the newest source.
|
|
48
|
-
'function spendGateSettings(bytes32 _poolId) view returns (bool enabled, uint256
|
|
60
|
+
'function spendGateSettings(bytes32 _poolId) view returns (bool enabled, uint256 walletCap, address settler, uint256 endsAt)',
|
|
49
61
|
'function trustedPoolKeySigner(bytes32 _poolId) view returns (address signer, bool enabled)',
|
|
50
62
|
]);
|
|
51
63
|
|
|
52
|
-
const erc20 = parseAbi([
|
|
64
|
+
const erc20 = parseAbi([
|
|
65
|
+
'function name() view returns (string)',
|
|
66
|
+
'function symbol() view returns (string)',
|
|
67
|
+
'function decimals() view returns (uint8)',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
53
71
|
|
|
54
72
|
export interface PoolKey {
|
|
55
73
|
currency0: `0x${string}`;
|
|
@@ -67,11 +85,17 @@ export interface Launch {
|
|
|
67
85
|
opensAt: number;
|
|
68
86
|
/** When the gate stops enforcing on chain, in epoch ms. */
|
|
69
87
|
closesAt: number;
|
|
70
|
-
|
|
88
|
+
/** In base units of `spendToken`. */
|
|
89
|
+
walletCap: bigint;
|
|
71
90
|
signer: `0x${string}`;
|
|
72
91
|
name: string;
|
|
73
92
|
symbol: string;
|
|
74
93
|
imageUrl?: string | null;
|
|
94
|
+
/**
|
|
95
|
+
* The token spend is measured in: the coin's paired token, read from the manager. Absent only
|
|
96
|
+
* from hand-built launches (demos, tests), where it means ETH.
|
|
97
|
+
*/
|
|
98
|
+
spendToken?: SpendToken;
|
|
75
99
|
}
|
|
76
100
|
|
|
77
101
|
/**
|
|
@@ -166,7 +190,7 @@ export async function verifyLaunch(
|
|
|
166
190
|
return refuse('launch.unreadable', true);
|
|
167
191
|
}
|
|
168
192
|
|
|
169
|
-
const [enabled,
|
|
193
|
+
const [enabled, walletCap, , endsAt] = settings;
|
|
170
194
|
const [signer, signerEnabled] = trusted;
|
|
171
195
|
|
|
172
196
|
if (!enabled) return refuse('launch.not_a_game', false);
|
|
@@ -174,12 +198,15 @@ export async function verifyLaunch(
|
|
|
174
198
|
if (!signerEnabled || signer.toLowerCase() !== deployment.signer.toLowerCase()) {
|
|
175
199
|
return refuse('launch.another_signer', false);
|
|
176
200
|
}
|
|
177
|
-
if (
|
|
201
|
+
if (walletCap <= 0n) return refuse('launch.no_spending_cap', false);
|
|
178
202
|
|
|
179
203
|
const opensAt = Number(flaunchesAt) * 1000;
|
|
180
204
|
const closesAt = Number(endsAt) * 1000;
|
|
181
205
|
if (closesAt <= Date.now()) return refuse('launch.already_over', false);
|
|
182
206
|
|
|
207
|
+
const spendToken = await resolveSpendToken(client, deployment, memecoin, poolKey, poolId);
|
|
208
|
+
if (!spendToken) return refuse('launch.paired_unreadable', true);
|
|
209
|
+
|
|
183
210
|
let name = '';
|
|
184
211
|
let symbol = '';
|
|
185
212
|
try {
|
|
@@ -192,7 +219,60 @@ export async function verifyLaunch(
|
|
|
192
219
|
// display string would be refusing a round for no reason anyone could act on.
|
|
193
220
|
}
|
|
194
221
|
|
|
195
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
coinAddress: memecoin,
|
|
225
|
+
poolId,
|
|
226
|
+
poolKey,
|
|
227
|
+
opensAt,
|
|
228
|
+
closesAt,
|
|
229
|
+
walletCap,
|
|
230
|
+
signer,
|
|
231
|
+
name,
|
|
232
|
+
symbol,
|
|
233
|
+
spendToken,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Which token this pool's spend is measured in.
|
|
239
|
+
*
|
|
240
|
+
* The calculator measures spend on the pool's PAIRED side, so the gate's cap and allocations must
|
|
241
|
+
* be in that token's base units — a six-decimal stablecoin priced as if it were wei authorises a
|
|
242
|
+
* trillion times too much. Asked of the manager first (`pairedToken`, V1_3), and derived from the
|
|
243
|
+
* pool key when the manager predates the function: the key names two currencies and one of them
|
|
244
|
+
* is the coin. Decimals are load-bearing, so unlike the coin's cosmetic name an unreadable token
|
|
245
|
+
* is a refusal — retryable, because an RPC blip is not a statement about the token.
|
|
246
|
+
*/
|
|
247
|
+
async function resolveSpendToken(
|
|
248
|
+
client: ChainReader,
|
|
249
|
+
deployment: Deployment,
|
|
250
|
+
memecoin: `0x${string}`,
|
|
251
|
+
poolKey: PoolKey,
|
|
252
|
+
poolId: `0x${string}`,
|
|
253
|
+
): Promise<SpendToken | null> {
|
|
254
|
+
let paired: `0x${string}`;
|
|
255
|
+
try {
|
|
256
|
+
paired = (await client.readContract({
|
|
257
|
+
address: deployment.positionManager,
|
|
258
|
+
abi: positionManagerAbi,
|
|
259
|
+
functionName: 'pairedToken',
|
|
260
|
+
args: [poolId],
|
|
261
|
+
})) as `0x${string}`;
|
|
262
|
+
} catch {
|
|
263
|
+
paired = poolKey.currency0.toLowerCase() === memecoin.toLowerCase() ? poolKey.currency1 : poolKey.currency0;
|
|
264
|
+
}
|
|
265
|
+
if (paired.toLowerCase() === ZERO_ADDRESS) return ETH_SPEND_TOKEN;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const [symbol, decimals] = (await Promise.all([
|
|
269
|
+
client.readContract({ address: paired, abi: erc20, functionName: 'symbol' }),
|
|
270
|
+
client.readContract({ address: paired, abi: erc20, functionName: 'decimals' }),
|
|
271
|
+
])) as [string, number];
|
|
272
|
+
return { address: paired, symbol, decimals: Number(decimals), isNative: false };
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
196
276
|
}
|
|
197
277
|
|
|
198
278
|
/**
|
package/src/chain/secure.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { keccak256, parseAbi, toHex, type PublicClient } from 'viem';
|
|
2
|
+
import type { FlaunchVariant } from './chains.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The boot-time refusals {@link PayloadSigner}'s doc comment promises.
|
|
@@ -16,11 +17,28 @@ const ZERO_KEY = `0x${'00'.repeat(32)}`;
|
|
|
16
17
|
/** The demo's session-secret fallback, which every copy of this repo shares. */
|
|
17
18
|
export const DEMO_SESSION_SECRET = 'dev-secret-that-is-long-enough-to-pass';
|
|
18
19
|
|
|
20
|
+
/** `ProtocolRoles.POSITION_MANAGER` in flaunch-contracts: the role a calculator demands of its caller. */
|
|
21
|
+
export const POSITION_MANAGER_ROLE = keccak256(toHex('PositionManager'));
|
|
22
|
+
|
|
23
|
+
const wiringAbi = parseAbi([
|
|
24
|
+
'function feeCalculator() view returns (address)',
|
|
25
|
+
'function registeredCalculators(address) view returns (bool)',
|
|
26
|
+
'function hasRole(bytes32 role, address account) view returns (bool)',
|
|
27
|
+
'function approvedRouters(address) view returns (bool)',
|
|
28
|
+
]);
|
|
29
|
+
|
|
19
30
|
export interface SecureConfig {
|
|
20
31
|
chainId: number;
|
|
21
32
|
signerPrivateKey: string;
|
|
22
33
|
spendGatedCalculator: `0x${string}`;
|
|
23
34
|
sessionSecret: string;
|
|
35
|
+
/**
|
|
36
|
+
* When given, the wiring between the manager and the calculator is read back and asserted — see
|
|
37
|
+
* {@link assertWiring}. Optional so a caller that only wants the key and chain checks keeps them.
|
|
38
|
+
*/
|
|
39
|
+
positionManager?: `0x${string}`;
|
|
40
|
+
flaunchVariant?: FlaunchVariant;
|
|
41
|
+
poolSwap?: `0x${string}`;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
export async function assertSecureConfig(client: PublicClient, config: SecureConfig): Promise<void> {
|
|
@@ -46,4 +64,71 @@ export async function assertSecureConfig(client: PublicClient, config: SecureCon
|
|
|
46
64
|
if (!code || code === '0x') {
|
|
47
65
|
throw new Error(`SPEND_GATED_CALCULATOR ${config.spendGatedCalculator} holds no code on chain ${chainId}`);
|
|
48
66
|
}
|
|
67
|
+
|
|
68
|
+
if (config.positionManager) await assertWiring(client, config);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The manager must route fees through a dispatcher that knows this calculator, and the calculator
|
|
73
|
+
* must accept calls from that dispatcher. Both are one-time owner actions on chain, and a stack
|
|
74
|
+
* missing either ADOPTS launches whose swaps never reach the gate: the pool trades ungated while
|
|
75
|
+
* the gate believes it is enforcing a cap. That is the whole failure this function refuses.
|
|
76
|
+
*
|
|
77
|
+
* A paired stack additionally needs its PoolSwap approved as a router, because a paired buy arrives
|
|
78
|
+
* through it and the calculator binds a smart wallet's buyer identity to an approved router's
|
|
79
|
+
* `msgSender()` — unapproved, every such buy reverts at the swap.
|
|
80
|
+
*/
|
|
81
|
+
async function assertWiring(client: PublicClient, config: SecureConfig): Promise<void> {
|
|
82
|
+
const manager = config.positionManager!;
|
|
83
|
+
const calculator = config.spendGatedCalculator;
|
|
84
|
+
|
|
85
|
+
let dispatcher: `0x${string}`;
|
|
86
|
+
try {
|
|
87
|
+
dispatcher = await client.readContract({ address: manager, abi: wiringAbi, functionName: 'feeCalculator' });
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error(`POSITION_MANAGER ${manager} does not answer feeCalculator() — is it a Flaunch PositionManager?`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [registered, hasRole] = await Promise.all([
|
|
93
|
+
client.readContract({
|
|
94
|
+
address: dispatcher,
|
|
95
|
+
abi: wiringAbi,
|
|
96
|
+
functionName: 'registeredCalculators',
|
|
97
|
+
args: [calculator],
|
|
98
|
+
}).catch(() => false),
|
|
99
|
+
client.readContract({
|
|
100
|
+
address: calculator,
|
|
101
|
+
abi: wiringAbi,
|
|
102
|
+
functionName: 'hasRole',
|
|
103
|
+
args: [POSITION_MANAGER_ROLE, dispatcher],
|
|
104
|
+
}).catch(() => false),
|
|
105
|
+
]);
|
|
106
|
+
if (!registered) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`the fee dispatcher ${dispatcher} of POSITION_MANAGER ${manager} does not have SPEND_GATED_CALCULATOR ` +
|
|
109
|
+
`${calculator} registered — its owner must call setCalculatorRegistered(${calculator}, true) before ` +
|
|
110
|
+
'this gate can serve launches on that manager',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (!hasRole) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`SPEND_GATED_CALCULATOR ${calculator} has not granted the PositionManager role to the dispatcher ${dispatcher} ` +
|
|
116
|
+
`— its admin must call grantRole(${POSITION_MANAGER_ROLE}, ${dispatcher})`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (config.flaunchVariant === 'v1_3') {
|
|
121
|
+
if (!config.poolSwap) {
|
|
122
|
+
throw new Error('a v1_3 stack needs POOL_SWAP — the router a paired-token buy goes through');
|
|
123
|
+
}
|
|
124
|
+
const approved = await client
|
|
125
|
+
.readContract({ address: calculator, abi: wiringAbi, functionName: 'approvedRouters', args: [config.poolSwap] })
|
|
126
|
+
.catch(() => false);
|
|
127
|
+
if (!approved) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`SPEND_GATED_CALCULATOR ${calculator} has not approved POOL_SWAP ${config.poolSwap} as a router — ` +
|
|
130
|
+
`its owner must call setApprovedRouter(${config.poolSwap}, true)`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
49
134
|
}
|
package/src/claims.ts
CHANGED
|
@@ -116,8 +116,8 @@ export class Claims {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/** What this player could spend right now. */
|
|
119
|
-
|
|
120
|
-
return this.ledger.
|
|
119
|
+
available(roundId: string, player: PlayerId): Promise<bigint> {
|
|
120
|
+
return this.ledger.available(roundId, player);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** Full authoritative balance for live snapshots. */
|
package/src/demo.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface DemoOptions<Config, State, Event, Action, PublicView, PlayerVie
|
|
|
41
41
|
lobbyMs?: number;
|
|
42
42
|
roundMs?: number;
|
|
43
43
|
/** The cap a wallet may spend, as a real launch would set on chain. Default 0.025 ETH. */
|
|
44
|
-
|
|
44
|
+
walletCap?: bigint;
|
|
45
45
|
pointsPerDollar?: number;
|
|
46
46
|
usdPerEth?: number;
|
|
47
47
|
roundId?: string;
|
|
@@ -83,7 +83,7 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
|
|
|
83
83
|
databaseUrl = process.env.DATABASE_URL ?? 'postgres://postgres:gamemode@127.0.0.1:55439/gamemode',
|
|
84
84
|
lobbyMs = 8_000,
|
|
85
85
|
roundMs = 10 * 60_000,
|
|
86
|
-
|
|
86
|
+
walletCap = ETH / 40n,
|
|
87
87
|
pointsPerDollar = 100,
|
|
88
88
|
usdPerEth = 3_000,
|
|
89
89
|
roundId = 'dev',
|
|
@@ -96,9 +96,9 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
|
|
|
96
96
|
const maxPointsPerPlayer = game.rewardBounds(config).maxPointsPerPlayer;
|
|
97
97
|
const economy = resolveEconomy({
|
|
98
98
|
maxPointsPerPlayer,
|
|
99
|
-
|
|
99
|
+
walletCap,
|
|
100
100
|
pointsPerDollar,
|
|
101
|
-
usdPerEth,
|
|
101
|
+
usdPerSpendToken: usdPerEth,
|
|
102
102
|
});
|
|
103
103
|
if (!economy.ok) {
|
|
104
104
|
throw new Error(
|
|
@@ -117,10 +117,10 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
|
|
|
117
117
|
config,
|
|
118
118
|
seed: Math.floor(Math.random() * 1_000_000),
|
|
119
119
|
window: { opensAt, closesAt: opensAt + roundMs },
|
|
120
|
-
|
|
120
|
+
unitsPerPoint: economy.economy.unitsPerPoint,
|
|
121
121
|
pointsPerDollar: economy.economy.pointsPerDollar,
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
usdPerSpendToken: economy.economy.usdPerSpendToken,
|
|
123
|
+
walletCap,
|
|
124
124
|
maxPointsPerPlayer,
|
|
125
125
|
scoreAuthority: 'rules',
|
|
126
126
|
poolId: DEMO_POOL_ID,
|
|
@@ -147,11 +147,11 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
|
|
|
147
147
|
contracts: { positionManager: DEMO_CALCULATOR, spendGatedCalculator: DEMO_CALCULATOR },
|
|
148
148
|
signer: signer.address,
|
|
149
149
|
settler: signer.address,
|
|
150
|
-
|
|
150
|
+
walletCap: walletCap.toString(),
|
|
151
151
|
roundDurationMs: roundMs,
|
|
152
152
|
minLobbyLeadMs: lobbyMs,
|
|
153
153
|
gateEndsAtGraceS: 10,
|
|
154
|
-
flaunchVariant: '
|
|
154
|
+
flaunchVariant: 'v1_2',
|
|
155
155
|
requiresEoa: false,
|
|
156
156
|
accepting: true,
|
|
157
157
|
publicLaunchesOpen: true,
|
|
@@ -171,7 +171,7 @@ export async function createDemoGate<Config, State, Event, Action, PublicView, P
|
|
|
171
171
|
app.post('/dev/wallet/buy', (request) => {
|
|
172
172
|
const { maxSpendWei } = request.body as { maxSpendWei: string };
|
|
173
173
|
purchases.push({ maxSpendWei, at: Date.now() });
|
|
174
|
-
return {
|
|
174
|
+
return { spent: maxSpendWei };
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
return { app, roundId, opensAt, walletAddress: wallet.address, purchases, pool };
|