@flayerlabs/gamemode-gate 0.5.5 → 0.6.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 +37 -9
- package/README.md +7 -1
- package/dist/chain/chains.d.ts +14 -6
- package/dist/chain/chains.d.ts.map +1 -1
- package/dist/chain/chains.js +8 -4
- package/dist/chain/chains.js.map +1 -1
- package/dist/chain/discover.d.ts +61 -3
- package/dist/chain/discover.d.ts.map +1 -1
- package/dist/chain/discover.js +181 -21
- package/dist/chain/discover.js.map +1 -1
- package/dist/chain/registry-prices.d.ts +69 -0
- package/dist/chain/registry-prices.d.ts.map +1 -0
- package/dist/chain/registry-prices.js +157 -0
- package/dist/chain/registry-prices.js.map +1 -0
- package/dist/chain/secure.d.ts +28 -0
- package/dist/chain/secure.d.ts.map +1 -1
- package/dist/chain/secure.js +55 -20
- package/dist/chain/secure.js.map +1 -1
- package/dist/economy.d.ts.map +1 -1
- package/dist/economy.js +12 -3
- package/dist/economy.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +14 -3
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +44 -11
- package/dist/main.js.map +1 -1
- package/dist/room.d.ts +7 -0
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +15 -6
- package/dist/room.js.map +1 -1
- package/dist/server.d.ts +26 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +21 -0
- 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 +6 -0
- package/dist/store.js.map +1 -1
- package/dist/version.js +2 -2
- package/package.json +2 -2
- package/src/chain/chains.ts +14 -6
- package/src/chain/discover.ts +212 -21
- package/src/chain/registry-prices.ts +194 -0
- package/src/chain/secure.ts +75 -31
- package/src/economy.ts +13 -3
- package/src/index.ts +7 -4
- package/src/main.ts +50 -11
- package/src/room.ts +14 -3
- package/src/server.ts +49 -0
- package/src/store.ts +6 -0
- package/src/version.ts +2 -2
package/src/chain/secure.ts
CHANGED
|
@@ -69,57 +69,101 @@ export async function assertSecureConfig(client: PublicClient, config: SecureCon
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* The
|
|
72
|
+
* The verdict of {@link checkPositionManagerWiring}: wired, with the dispatcher the manager routes
|
|
73
|
+
* through, or not, with a reason written for the operator who has to fix it.
|
|
74
|
+
*/
|
|
75
|
+
export type PositionManagerWiring =
|
|
76
|
+
| { ok: true; dispatcher: `0x${string}` }
|
|
77
|
+
| { ok: false; reason: string };
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Is this manager's fee wiring routed to this calculator?
|
|
81
|
+
*
|
|
82
|
+
* The manager must route fees through a dispatcher that knows the calculator, and the calculator
|
|
73
83
|
* must accept calls from that dispatcher. Both are one-time owner actions on chain, and a stack
|
|
74
84
|
* missing either ADOPTS launches whose swaps never reach the gate: the pool trades ungated while
|
|
75
85
|
* the gate believes it is enforcing a cap. That is the whole failure this function refuses.
|
|
76
86
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
87
|
+
* It is also how a manager is APPROVED for a gate. The gate never holds a list of managers: a coin
|
|
88
|
+
* offered for adoption names its own manager, and the manager is accepted when these three reads
|
|
89
|
+
* say its fees reach the gate's calculator. Wiring a new manager to the dispatcher on chain is the
|
|
90
|
+
* approval, and needs no gate release. Boot asks the same question of the primary manager.
|
|
80
91
|
*/
|
|
81
|
-
async function
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
export async function checkPositionManagerWiring(
|
|
93
|
+
client: Pick<PublicClient, 'readContract'>,
|
|
94
|
+
wiring: { positionManager: `0x${string}`; spendGatedCalculator: `0x${string}` },
|
|
95
|
+
): Promise<PositionManagerWiring> {
|
|
96
|
+
const manager = wiring.positionManager;
|
|
97
|
+
const calculator = wiring.spendGatedCalculator;
|
|
84
98
|
|
|
85
99
|
let dispatcher: `0x${string}`;
|
|
86
100
|
try {
|
|
87
|
-
dispatcher = await client.readContract({
|
|
101
|
+
dispatcher = (await client.readContract({
|
|
102
|
+
address: manager,
|
|
103
|
+
abi: wiringAbi,
|
|
104
|
+
functionName: 'feeCalculator',
|
|
105
|
+
})) as `0x${string}`;
|
|
88
106
|
} catch {
|
|
89
|
-
|
|
107
|
+
return { ok: false, reason: `${manager} does not answer feeCalculator() — is it a Flaunch PositionManager?` };
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
const [registered, hasRole] = await Promise.all([
|
|
93
|
-
client
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
client
|
|
112
|
+
.readContract({
|
|
113
|
+
address: dispatcher,
|
|
114
|
+
abi: wiringAbi,
|
|
115
|
+
functionName: 'registeredCalculators',
|
|
116
|
+
args: [calculator],
|
|
117
|
+
})
|
|
118
|
+
.catch(() => false),
|
|
119
|
+
client
|
|
120
|
+
.readContract({
|
|
121
|
+
address: calculator,
|
|
122
|
+
abi: wiringAbi,
|
|
123
|
+
functionName: 'hasRole',
|
|
124
|
+
args: [POSITION_MANAGER_ROLE, dispatcher],
|
|
125
|
+
})
|
|
126
|
+
.catch(() => false),
|
|
105
127
|
]);
|
|
106
128
|
if (!registered) {
|
|
107
|
-
|
|
108
|
-
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason:
|
|
132
|
+
`the fee dispatcher ${dispatcher} of PositionManager ${manager} does not have the spend-gated calculator ` +
|
|
109
133
|
`${calculator} registered — its owner must call setCalculatorRegistered(${calculator}, true) before ` +
|
|
110
|
-
'
|
|
111
|
-
|
|
134
|
+
'a gate on that calculator can serve launches on that manager',
|
|
135
|
+
};
|
|
112
136
|
}
|
|
113
137
|
if (!hasRole) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
reason:
|
|
141
|
+
`the spend-gated calculator ${calculator} has not granted the PositionManager role to the dispatcher ` +
|
|
142
|
+
`${dispatcher} — its admin must call grantRole(${POSITION_MANAGER_ROLE}, ${dispatcher})`,
|
|
143
|
+
};
|
|
118
144
|
}
|
|
145
|
+
return { ok: true, dispatcher };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Boot's assertion about the PRIMARY manager: {@link checkPositionManagerWiring}, thrown rather
|
|
150
|
+
* than returned, because a gate whose own address book is unwired must not come up at all.
|
|
151
|
+
*
|
|
152
|
+
* A paired stack additionally needs its PoolSwap approved as a router, because a paired buy arrives
|
|
153
|
+
* through it and the calculator binds a smart wallet's buyer identity to an approved router's
|
|
154
|
+
* `msgSender()` — unapproved, every such buy reverts at the swap.
|
|
155
|
+
*/
|
|
156
|
+
async function assertWiring(client: PublicClient, config: SecureConfig): Promise<void> {
|
|
157
|
+
const calculator = config.spendGatedCalculator;
|
|
158
|
+
const wiring = await checkPositionManagerWiring(client, {
|
|
159
|
+
positionManager: config.positionManager!,
|
|
160
|
+
spendGatedCalculator: calculator,
|
|
161
|
+
});
|
|
162
|
+
if (!wiring.ok) throw new Error(`POSITION_MANAGER: ${wiring.reason}`);
|
|
119
163
|
|
|
120
|
-
if (config.flaunchVariant === 'v1_3') {
|
|
164
|
+
if (config.flaunchVariant === 'v1_3' || config.flaunchVariant === 'any_v1_3') {
|
|
121
165
|
if (!config.poolSwap) {
|
|
122
|
-
throw new Error(
|
|
166
|
+
throw new Error(`a ${config.flaunchVariant} stack needs POOL_SWAP — the router a paired-token buy goes through`);
|
|
123
167
|
}
|
|
124
168
|
const approved = await client
|
|
125
169
|
.readContract({ address: calculator, abi: wiringAbi, functionName: 'approvedRouters', args: [config.poolSwap] })
|
package/src/economy.ts
CHANGED
|
@@ -81,6 +81,16 @@ function resolveInputs(input: EconomyInput): { usdPerSpendToken: number; decimal
|
|
|
81
81
|
* goes and reads this file.
|
|
82
82
|
*/
|
|
83
83
|
export function resolveEconomy(input: EconomyInput): EconomyResult {
|
|
84
|
+
return evaluate(input, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The check itself. `suggest` is false while {@link minimumPointsPerDollar} is probing candidates:
|
|
89
|
+
* a refusal there must not go looking for a suggestion of its own, or a cap one unit short of
|
|
90
|
+
* fitting recurses until the stack gives out — which is how this function once took a gate down at
|
|
91
|
+
* boot instead of printing the number that fixes it.
|
|
92
|
+
*/
|
|
93
|
+
function evaluate(input: EconomyInput, suggest: boolean): EconomyResult {
|
|
84
94
|
const { maxPointsPerPlayer, walletCap, pointsPerDollar } = input;
|
|
85
95
|
const { usdPerSpendToken, decimals } = resolveInputs(input);
|
|
86
96
|
|
|
@@ -125,7 +135,7 @@ export function resolveEconomy(input: EconomyInput): EconomyResult {
|
|
|
125
135
|
return {
|
|
126
136
|
ok: false,
|
|
127
137
|
problem: 'one point would be worth more than a whole spend token, which cannot be what was meant',
|
|
128
|
-
suggestedPointsPerDollar: minimumPointsPerDollar(input),
|
|
138
|
+
suggestedPointsPerDollar: suggest ? minimumPointsPerDollar(input) : pointsPerDollar,
|
|
129
139
|
};
|
|
130
140
|
}
|
|
131
141
|
|
|
@@ -144,7 +154,7 @@ export function resolveEconomy(input: EconomyInput): EconomyResult {
|
|
|
144
154
|
return {
|
|
145
155
|
ok: false,
|
|
146
156
|
problem: 'a perfect round would earn more than this launch lets one wallet spend',
|
|
147
|
-
suggestedPointsPerDollar: minimumPointsPerDollar(input),
|
|
157
|
+
suggestedPointsPerDollar: suggest ? minimumPointsPerDollar(input) : pointsPerDollar,
|
|
148
158
|
};
|
|
149
159
|
}
|
|
150
160
|
|
|
@@ -179,7 +189,7 @@ function minimumPointsPerDollar(input: EconomyInput): number {
|
|
|
179
189
|
Number.isSafeInteger(decimals) && decimals >= 0 && decimals <= 36 ? unitsPerToken(decimals) : WEI_PER_ETH;
|
|
180
190
|
let candidate = Math.ceil(Number(units / maxUnitsPerPoint) / usdPerSpendToken);
|
|
181
191
|
for (let i = 0; i < 3; i++) {
|
|
182
|
-
const result =
|
|
192
|
+
const result = evaluate({ ...input, pointsPerDollar: candidate }, false);
|
|
183
193
|
if (result.ok) return candidate;
|
|
184
194
|
candidate += 1;
|
|
185
195
|
}
|
package/src/index.ts
CHANGED
|
@@ -27,8 +27,8 @@ export type { BalanceKey, Claim, ClaimResult } from './ledger.js';
|
|
|
27
27
|
export { CLAIM_AUTHORIZATION_TTL_MS } from './claims.js';
|
|
28
28
|
export { PayloadSigner, encodeHookData } from './chain/signer.js';
|
|
29
29
|
export type { SignedPayload } from './chain/signer.js';
|
|
30
|
-
export { Discovery, poolIdOf, verifyLaunch } from './chain/discover.js';
|
|
31
|
-
export type { ChainReader, Deployment, Launch, LaunchCheck, PoolKey, Refused } from './chain/discover.js';
|
|
30
|
+
export { Discovery, WiringCache, poolIdOf, resolvePositionManager, verifyLaunch } from './chain/discover.js';
|
|
31
|
+
export type { ChainReader, Deployment, Launch, LaunchCheck, PoolKey, Refused, WiringCheck } from './chain/discover.js';
|
|
32
32
|
export { Settlement } from './settlement.js';
|
|
33
33
|
export { Standings } from './standings.js';
|
|
34
34
|
export { ETH_DECIMALS, resolveEconomy, unitsPerToken } from './economy.js';
|
|
@@ -66,12 +66,15 @@ export type { VerifiedPlayerJoin, VerifyPlayerJoinTicketOptions } from './player
|
|
|
66
66
|
*/
|
|
67
67
|
export { startGate, resolveGateEnv } from './main.js';
|
|
68
68
|
export type { ResolvedGateConfig, SpendTokenPricing, StartedGate, StartGateOverrides } from './main.js';
|
|
69
|
-
export type { GateAnnouncement, ServedGateAnnouncement } from './server.js';
|
|
69
|
+
export type { GateAnnouncement, PositionManagerPolicy, ServedGateAnnouncement } from './server.js';
|
|
70
70
|
export { GATE_MARKER, GATE_VERSION } from './version.js';
|
|
71
71
|
export { NotSignedIn } from './sessions.js';
|
|
72
72
|
export { DEPLOYMENTS, PAIRED_DEPLOYMENTS, deploymentFor } from './chain/chains.js';
|
|
73
73
|
export type { FlaunchVariant, GateDeployment } from './chain/chains.js';
|
|
74
|
-
export { POSITION_MANAGER_ROLE, assertSecureConfig } from './chain/secure.js';
|
|
74
|
+
export { POSITION_MANAGER_ROLE, assertSecureConfig, checkPositionManagerWiring } from './chain/secure.js';
|
|
75
|
+
export type { PositionManagerWiring } from './chain/secure.js';
|
|
76
|
+
export { RegistryPrices, minimumWalletCap, withHeadroom } from './chain/registry-prices.js';
|
|
77
|
+
export type { AnnounceOptions, RegistryPricesOptions, RegistryQuote, RegistryReader } from './chain/registry-prices.js';
|
|
75
78
|
|
|
76
79
|
export { createGateReadiness } from './readiness.js';
|
|
77
80
|
export type { GateReadinessOptions } from './readiness.js';
|
package/src/main.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { migrate } from './store.js';
|
|
|
15
15
|
import { resolveEconomy } from './economy.js';
|
|
16
16
|
import { deploymentFor, type GateDeployment } from './chain/chains.js';
|
|
17
17
|
import { assertSecureConfig } from './chain/secure.js';
|
|
18
|
+
import { RegistryPrices } from './chain/registry-prices.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* A production gate from an environment, assembled and checked.
|
|
@@ -59,6 +60,12 @@ export interface ResolvedGateConfig {
|
|
|
59
60
|
* refused at adoption — the gate does not guess a token's price.
|
|
60
61
|
*/
|
|
61
62
|
spendTokens: ReadonlyMap<string, SpendTokenPricing>;
|
|
63
|
+
/**
|
|
64
|
+
* Percent added over the smallest cap a flawless round fits under when the gate sizes a cap for a
|
|
65
|
+
* registry-priced pairing (`/config/spend-tokens/:address`). Prices move between the quote and the
|
|
66
|
+
* launch; this is the room left for that.
|
|
67
|
+
*/
|
|
68
|
+
pairedTokenCapHeadroomPercent: number;
|
|
62
69
|
/** May lift the gate early or rotate the signer on-chain. Defaults to the signing key's address. */
|
|
63
70
|
settler: `0x${string}` | null;
|
|
64
71
|
}
|
|
@@ -156,9 +163,11 @@ function address(env: Env, name: string): `0x${string}` | null {
|
|
|
156
163
|
* POSITION_MANAGER optional; selects a recorded stack on the chain (the legacy one when
|
|
157
164
|
* unset), or overrides the address for a chain with no address book
|
|
158
165
|
* SPEND_GATED_CALCULATOR optional address-book override
|
|
159
|
-
* POOL_SWAP optional address-book override; required for a v1_3 stack
|
|
166
|
+
* POOL_SWAP optional address-book override; required for a v1_3 or any_v1_3 stack
|
|
160
167
|
* FLETH optional address-book override; the chain's flETH wrapper
|
|
161
|
-
* FLAUNCH_VARIANT optional; v1_2, multichain or
|
|
168
|
+
* FLAUNCH_VARIANT optional; v1_2, multichain, v1_3 or any_v1_3 — the PRIMARY manager's
|
|
169
|
+
* generation. Adoption is not limited to it: any manager wired to the
|
|
170
|
+
* calculator is served (see chain/discover.ts)
|
|
162
171
|
* SETTLER optional; defaults to the signing key's own address
|
|
163
172
|
* WALLET_CAP_WEI default 0.025 ETH; the cap for ETH-paired launches
|
|
164
173
|
* ROUND_MS default 90000
|
|
@@ -167,7 +176,10 @@ function address(env: Env, name: string): `0x${string}` | null {
|
|
|
167
176
|
* POINTS_PER_DOLLAR default 100
|
|
168
177
|
* USD_PER_ETH default 3000; fractional allowed
|
|
169
178
|
* SPEND_TOKENS optional JSON array of { address, usdPerToken, walletCap } — the non-ETH
|
|
170
|
-
* paired tokens this gate runs rounds on.
|
|
179
|
+
* paired tokens this gate runs rounds on. Pins a price the registry
|
|
180
|
+
* would otherwise quote; tokens neither here nor approved on the
|
|
181
|
+
* manager's PairedTokenRegistry are refused.
|
|
182
|
+
* PAIRED_TOKEN_CAP_HEADROOM_PERCENT default 25; headroom on caps sized for registry-priced pairings
|
|
171
183
|
*/
|
|
172
184
|
export function resolveGateEnv(env: Env): ResolvedGateConfig {
|
|
173
185
|
const chainId = Number(required(env, 'CHAIN_ID'));
|
|
@@ -190,14 +202,16 @@ export function resolveGateEnv(env: Env): ResolvedGateConfig {
|
|
|
190
202
|
variantOverride !== undefined &&
|
|
191
203
|
variantOverride !== 'v1_2' &&
|
|
192
204
|
variantOverride !== 'multichain' &&
|
|
193
|
-
variantOverride !== 'v1_3'
|
|
205
|
+
variantOverride !== 'v1_3' &&
|
|
206
|
+
variantOverride !== 'any_v1_3'
|
|
194
207
|
) {
|
|
195
|
-
throw new Error(`FLAUNCH_VARIANT must be v1_2, multichain or
|
|
208
|
+
throw new Error(`FLAUNCH_VARIANT must be v1_2, multichain, v1_3 or any_v1_3, not ${variantOverride}`);
|
|
196
209
|
}
|
|
197
210
|
const flaunchVariant = variantOverride ?? recorded?.flaunchVariant ?? 'v1_2';
|
|
198
211
|
const poolSwap = address(env, 'POOL_SWAP') ?? recorded?.poolSwap ?? null;
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
// Both V1_3 generations take paired buys through the approved router.
|
|
213
|
+
if ((flaunchVariant === 'v1_3' || flaunchVariant === 'any_v1_3') && !poolSwap) {
|
|
214
|
+
throw new Error(`a ${flaunchVariant} stack needs POOL_SWAP — the approved router a paired-token buy goes through`);
|
|
201
215
|
}
|
|
202
216
|
const fleth = address(env, 'FLETH') ?? recorded?.fleth ?? null;
|
|
203
217
|
|
|
@@ -247,6 +261,7 @@ export function resolveGateEnv(env: Env): ResolvedGateConfig {
|
|
|
247
261
|
pointsPerDollar: integer(env, 'POINTS_PER_DOLLAR', 100),
|
|
248
262
|
usdPerEth: positiveNumber(env, 'USD_PER_ETH', 3_000),
|
|
249
263
|
spendTokens: spendTokens(env),
|
|
264
|
+
pairedTokenCapHeadroomPercent: integer(env, 'PAIRED_TOKEN_CAP_HEADROOM_PERCENT', 25),
|
|
250
265
|
settler: address(env, 'SETTLER'),
|
|
251
266
|
};
|
|
252
267
|
}
|
|
@@ -262,9 +277,11 @@ function isEthEquivalent(token: SpendToken, fleth: `0x${string}` | undefined): b
|
|
|
262
277
|
* The price list a running gate answers adoption with.
|
|
263
278
|
*
|
|
264
279
|
* An override (an oracle, a test) is asked first; the env map second; ETH and its wrapper are the
|
|
265
|
-
* operator's `USD_PER_ETH`;
|
|
280
|
+
* operator's `USD_PER_ETH`; then the manager's own PairedTokenRegistry, which prices every token the
|
|
281
|
+
* protocol has approved through the calculator it was registered with. Only a token none of them
|
|
282
|
+
* know is unpriced, and that launch is refused — it could not have been launched either.
|
|
266
283
|
*/
|
|
267
|
-
function priceList(config: ResolvedGateConfig, override: PriceFor | undefined): PriceFor {
|
|
284
|
+
function priceList(config: ResolvedGateConfig, override: PriceFor | undefined, registry: RegistryPrices): PriceFor {
|
|
268
285
|
return async (token) => {
|
|
269
286
|
if (override) {
|
|
270
287
|
const quoted = await override(token);
|
|
@@ -273,7 +290,7 @@ function priceList(config: ResolvedGateConfig, override: PriceFor | undefined):
|
|
|
273
290
|
const listed = config.spendTokens.get(token.address.toLowerCase());
|
|
274
291
|
if (listed) return listed.usdPerToken;
|
|
275
292
|
if (isEthEquivalent(token, config.deployment.fleth)) return config.usdPerEth;
|
|
276
|
-
return
|
|
293
|
+
return registry.priceFor(token);
|
|
277
294
|
};
|
|
278
295
|
}
|
|
279
296
|
|
|
@@ -365,6 +382,16 @@ export async function startGate<Config, State, Event, Action, PublicView, Player
|
|
|
365
382
|
});
|
|
366
383
|
}
|
|
367
384
|
|
|
385
|
+
// Every pairing the protocol approves, priced by the calculator it was registered with. The
|
|
386
|
+
// registry is read from the manager, so nothing is configured and nothing goes stale when a new
|
|
387
|
+
// token is approved: the next launch on it is adopted like any other.
|
|
388
|
+
const registryPrices = new RegistryPrices(client, {
|
|
389
|
+
positionManager: config.deployment.positionManager,
|
|
390
|
+
...(config.deployment.fleth ? { fleth: config.deployment.fleth } : {}),
|
|
391
|
+
usdPerEth: config.usdPerEth,
|
|
392
|
+
});
|
|
393
|
+
const pairedTokenRegistry = await registryPrices.registryAddress();
|
|
394
|
+
|
|
368
395
|
const pool = new pg.Pool({ connectionString: config.databaseUrl });
|
|
369
396
|
await migrate(pool);
|
|
370
397
|
|
|
@@ -386,7 +413,9 @@ export async function startGate<Config, State, Event, Action, PublicView, Player
|
|
|
386
413
|
minLobbyLeadMs: config.minLobbyLeadMs,
|
|
387
414
|
gateEndsAtGraceS: config.gateEndsAtGraceS,
|
|
388
415
|
flaunchVariant: config.deployment.flaunchVariant,
|
|
416
|
+
positionManagerPolicy: 'wired-to-calculator',
|
|
389
417
|
...(announcedSpendTokens.length > 0 ? { spendTokens: announcedSpendTokens } : {}),
|
|
418
|
+
...(pairedTokenRegistry ? { pairedTokenRegistry } : {}),
|
|
390
419
|
requiresEoa: false,
|
|
391
420
|
accepting: true,
|
|
392
421
|
publicLaunchesOpen: true,
|
|
@@ -399,7 +428,17 @@ export async function startGate<Config, State, Event, Action, PublicView, Player
|
|
|
399
428
|
config: gameConfig,
|
|
400
429
|
pointsPerDollar: config.pointsPerDollar,
|
|
401
430
|
usdPerEth: config.usdPerEth,
|
|
402
|
-
priceFor: priceList(config, overrides.priceProvider),
|
|
431
|
+
priceFor: priceList(config, overrides.priceProvider, registryPrices),
|
|
432
|
+
announceSpendToken: async (address) => {
|
|
433
|
+
// An operator-pinned token is announced exactly as pinned; anything else is the registry's word.
|
|
434
|
+
const pinned = announcedSpendTokens.find((entry) => entry.address.toLowerCase() === address.toLowerCase());
|
|
435
|
+
if (pinned) return pinned;
|
|
436
|
+
return registryPrices.announce(address, {
|
|
437
|
+
maxPointsPerPlayer,
|
|
438
|
+
pointsPerDollar: config.pointsPerDollar,
|
|
439
|
+
headroomPercent: config.pairedTokenCapHeadroomPercent,
|
|
440
|
+
});
|
|
441
|
+
},
|
|
403
442
|
sessions: new Sessions(config.sessionSecret, config.signInDomain, config.gateOrigin),
|
|
404
443
|
claims: new Claims(pool, ledger, signer),
|
|
405
444
|
discovery: new Discovery(client, {
|
package/src/room.ts
CHANGED
|
@@ -42,6 +42,12 @@ export class Room<Config, State, Event, Action, PublicView, PlayerView> {
|
|
|
42
42
|
readonly terms: RoundTerms,
|
|
43
43
|
readonly scoreAuthority: ScoreAuthority,
|
|
44
44
|
private seq: number,
|
|
45
|
+
/**
|
|
46
|
+
* The PositionManager the coin was launched on, as the gate resolved it at adoption. Null for
|
|
47
|
+
* rounds adopted before the gate recorded it (below 0.6.0), and for demo and mock rooms. Kept
|
|
48
|
+
* off `LaunchContext`, which is the wire type games receive.
|
|
49
|
+
*/
|
|
50
|
+
readonly positionManager: string | null = null,
|
|
45
51
|
) {}
|
|
46
52
|
|
|
47
53
|
static async create<Config, State, Event, Action, PublicView, PlayerView>(
|
|
@@ -68,6 +74,7 @@ export class Room<Config, State, Event, Action, PublicView, PlayerView> {
|
|
|
68
74
|
name?: string;
|
|
69
75
|
symbol?: string;
|
|
70
76
|
imageUrl?: string | null;
|
|
77
|
+
positionManager?: string | null;
|
|
71
78
|
};
|
|
72
79
|
},
|
|
73
80
|
): Promise<Room<Config, State, Event, Action, PublicView, PlayerView>> {
|
|
@@ -109,8 +116,8 @@ export class Room<Config, State, Event, Action, PublicView, PlayerView> {
|
|
|
109
116
|
spend_token, spend_symbol, spend_decimals, usd_per_spend_token,
|
|
110
117
|
max_points_per_player, score_authority,
|
|
111
118
|
pool_id, opens_at, closes_at, books_close_at,
|
|
112
|
-
coin_address, coin_name, coin_symbol, coin_image_url, state, seq
|
|
113
|
-
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, 0)`,
|
|
119
|
+
coin_address, coin_name, coin_symbol, coin_image_url, position_manager, state, seq
|
|
120
|
+
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, 0)`,
|
|
114
121
|
[
|
|
115
122
|
round.id,
|
|
116
123
|
game.id,
|
|
@@ -132,6 +139,7 @@ export class Room<Config, State, Event, Action, PublicView, PlayerView> {
|
|
|
132
139
|
launch.name,
|
|
133
140
|
launch.symbol,
|
|
134
141
|
launch.imageUrl,
|
|
142
|
+
round.launch?.positionManager?.toLowerCase() ?? null,
|
|
135
143
|
serialise(game.id, started.snapshot()),
|
|
136
144
|
],
|
|
137
145
|
);
|
|
@@ -145,6 +153,7 @@ export class Room<Config, State, Event, Action, PublicView, PlayerView> {
|
|
|
145
153
|
terms,
|
|
146
154
|
round.scoreAuthority,
|
|
147
155
|
0,
|
|
156
|
+
round.launch?.positionManager?.toLowerCase() ?? null,
|
|
148
157
|
);
|
|
149
158
|
}
|
|
150
159
|
|
|
@@ -165,6 +174,7 @@ export class Room<Config, State, Event, Action, PublicView, PlayerView> {
|
|
|
165
174
|
coin_name: string;
|
|
166
175
|
coin_symbol: string;
|
|
167
176
|
coin_image_url: string | null;
|
|
177
|
+
position_manager: string | null;
|
|
168
178
|
wei_per_point: string;
|
|
169
179
|
wallet_cap_wei: string;
|
|
170
180
|
points_per_dollar: number;
|
|
@@ -181,7 +191,7 @@ export class Room<Config, State, Event, Action, PublicView, PlayerView> {
|
|
|
181
191
|
wallet_cap_wei, points_per_dollar, usd_per_eth,
|
|
182
192
|
spend_token, spend_symbol, spend_decimals, usd_per_spend_token,
|
|
183
193
|
max_points_per_player, score_authority, game_id,
|
|
184
|
-
coin_address, coin_name, coin_symbol, coin_image_url
|
|
194
|
+
coin_address, coin_name, coin_symbol, coin_image_url, position_manager
|
|
185
195
|
from rounds where id = $1`,
|
|
186
196
|
[id],
|
|
187
197
|
);
|
|
@@ -225,6 +235,7 @@ export class Room<Config, State, Event, Action, PublicView, PlayerView> {
|
|
|
225
235
|
terms,
|
|
226
236
|
row.score_authority,
|
|
227
237
|
row.seq,
|
|
238
|
+
row.position_manager,
|
|
228
239
|
);
|
|
229
240
|
}
|
|
230
241
|
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify';
|
|
2
2
|
import websocket from '@fastify/websocket';
|
|
3
3
|
import cors from '@fastify/cors';
|
|
4
|
+
import { isAddress } from 'viem';
|
|
4
5
|
import type { Pool } from 'pg';
|
|
5
6
|
import type { Command, GameModule, PlayerId } from '@flayerlabs/gamemode-spec';
|
|
6
7
|
import { isRefusal } from '@flayerlabs/gamemode-spec';
|
|
@@ -58,6 +59,13 @@ interface GateBaseOptions {
|
|
|
58
59
|
* refused: a gate that guessed a price would be authorising real money on a made-up number.
|
|
59
60
|
*/
|
|
60
61
|
priceFor?: PriceFor;
|
|
62
|
+
/**
|
|
63
|
+
* Serves `GET /config/spend-tokens/:address`: the announced entry for one paired token — price,
|
|
64
|
+
* symbol, decimals and the per-wallet cap a launch on it should write — or null to answer 404 for
|
|
65
|
+
* a token this gate will not run rounds on. Lets launch tooling check any pairing before a coin
|
|
66
|
+
* is paid for, without the gate having listed it in `/config` up front.
|
|
67
|
+
*/
|
|
68
|
+
announceSpendToken?: (address: `0x${string}`) => Promise<AnnouncedSpendToken | null>;
|
|
61
69
|
/** Verifies from the chain that a coin really is a game launch. */
|
|
62
70
|
discovery?: Discovery;
|
|
63
71
|
/** Keeps the ledger's view of spending level with the chain's. */
|
|
@@ -140,6 +148,12 @@ export interface GameServerGateOptions extends GateBaseOptions {
|
|
|
140
148
|
gameServerOrigins?: readonly string[];
|
|
141
149
|
}
|
|
142
150
|
|
|
151
|
+
/**
|
|
152
|
+
* How a gate decides which PositionManagers it adopts coins from. One value today; the type exists
|
|
153
|
+
* so a client can compare rather than test for presence, and so a later policy has a name.
|
|
154
|
+
*/
|
|
155
|
+
export type PositionManagerPolicy = 'wired-to-calculator';
|
|
156
|
+
|
|
143
157
|
/**
|
|
144
158
|
* The gate's public identity, in the exact shape the flaunch frontend's `GateServerConfig` already
|
|
145
159
|
* parses — served this way so the page that builds launches needs no second config type.
|
|
@@ -161,13 +175,27 @@ export interface GateAnnouncement {
|
|
|
161
175
|
roundDurationMs: number;
|
|
162
176
|
minLobbyLeadMs: number;
|
|
163
177
|
gateEndsAtGraceS: number;
|
|
178
|
+
/** The generation of the PRIMARY manager in `contracts.positionManager`; not a limit on adoption. */
|
|
164
179
|
flaunchVariant: FlaunchVariant;
|
|
180
|
+
/**
|
|
181
|
+
* How this gate decides which PositionManagers it adopts coins from. `'wired-to-calculator'`
|
|
182
|
+
* means any manager whose fee wiring routes to `contracts.spendGatedCalculator`, derived from the
|
|
183
|
+
* coin itself at adoption; `contracts.positionManager` is then the primary manager, not the only
|
|
184
|
+
* one. A gate that omits the field (below 0.6.0) adopts coins on `contracts.positionManager` only.
|
|
185
|
+
*/
|
|
186
|
+
positionManagerPolicy?: PositionManagerPolicy;
|
|
165
187
|
/**
|
|
166
188
|
* The non-ETH paired tokens this gate will run rounds on, each with the cap a launch on it should
|
|
167
189
|
* write. Absent or empty means ETH pairings only. Launch tooling refuses a pairing that is not
|
|
168
190
|
* listed here, which is the fail-closed answer for a gate that has not been told the token's price.
|
|
169
191
|
*/
|
|
170
192
|
spendTokens?: AnnouncedSpendToken[];
|
|
193
|
+
/**
|
|
194
|
+
* The manager's PairedTokenRegistry, when it has one. Its presence means this gate prices every
|
|
195
|
+
* token the registry approves, not only `spendTokens`: ask `/config/spend-tokens/:address` for the
|
|
196
|
+
* entry of any pairing before building a launch on it.
|
|
197
|
+
*/
|
|
198
|
+
pairedTokenRegistry?: `0x${string}`;
|
|
171
199
|
requiresEoa: boolean;
|
|
172
200
|
accepting: boolean;
|
|
173
201
|
/**
|
|
@@ -498,12 +526,17 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
498
526
|
name: launch.name,
|
|
499
527
|
symbol: launch.symbol,
|
|
500
528
|
imageUrl: launch.imageUrl ?? null,
|
|
529
|
+
positionManager: launch.positionManager ?? null,
|
|
501
530
|
},
|
|
502
531
|
});
|
|
503
532
|
} catch (error) {
|
|
504
533
|
app.log.error(error);
|
|
505
534
|
return { ok: false, status: 503, message: 'this game is still being prepared' };
|
|
506
535
|
}
|
|
536
|
+
app.log.info(
|
|
537
|
+
`${launch.coinAddress}: adopted ${launch.symbol || 'coin'} on position manager ` +
|
|
538
|
+
`${launch.positionManager ?? 'primary'} (pool ${launch.poolId}, ${spendToken.symbol})`,
|
|
539
|
+
);
|
|
507
540
|
rooms.set(launch.poolId, room);
|
|
508
541
|
return { ok: true, room, created: true };
|
|
509
542
|
}
|
|
@@ -686,6 +719,22 @@ function createGateRuntime<Config, State, Event, Action, PublicView, PlayerView>
|
|
|
686
719
|
const served: ServedGateAnnouncement = { ...announce(), gateVersion: GATE_VERSION };
|
|
687
720
|
return served;
|
|
688
721
|
});
|
|
722
|
+
if (options.announceSpendToken) {
|
|
723
|
+
const announceSpendToken = options.announceSpendToken;
|
|
724
|
+
app.get<{ Params: { address: string } }>('/config/spend-tokens/:address', async (request, reply) => {
|
|
725
|
+
void reply.header('access-control-allow-origin', '*');
|
|
726
|
+
// A quote moves with the market and an approval can be revoked; nothing here may be cached
|
|
727
|
+
// past the response.
|
|
728
|
+
void reply.header('cache-control', 'no-store');
|
|
729
|
+
const { address } = request.params;
|
|
730
|
+
if (!isAddress(address, { strict: false })) {
|
|
731
|
+
return reply.code(400).send({ message: 'not a token address' });
|
|
732
|
+
}
|
|
733
|
+
const entry = await announceSpendToken(address as `0x${string}`);
|
|
734
|
+
if (!entry) return reply.code(404).send({ message: 'this gate does not run rounds on that token' });
|
|
735
|
+
return entry;
|
|
736
|
+
});
|
|
737
|
+
}
|
|
689
738
|
}
|
|
690
739
|
|
|
691
740
|
app.post('/session/challenge', async (request) => {
|
package/src/store.ts
CHANGED
|
@@ -89,6 +89,12 @@ alter table rounds
|
|
|
89
89
|
alter table rounds
|
|
90
90
|
add column if not exists usd_per_spend_token double precision check (usd_per_spend_token > 0);
|
|
91
91
|
|
|
92
|
+
-- Any wired PositionManager (0.6.0). The manager a round's coin was launched on, as the gate
|
|
93
|
+
-- resolved it from the coin at adoption. Null on rounds adopted before the gate recorded it, and on
|
|
94
|
+
-- demo and mock rounds; nothing reads it back for gameplay, it is provenance for the operator.
|
|
95
|
+
alter table rounds
|
|
96
|
+
add column if not exists position_manager text check (position_manager <> '');
|
|
97
|
+
|
|
92
98
|
create table if not exists balances (
|
|
93
99
|
round_id text not null references rounds(id) on delete cascade,
|
|
94
100
|
player text not null,
|
package/src/version.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/write-version.mjs from package.json at build time. Do not edit.
|
|
2
2
|
/** The published version of @flayerlabs/gamemode-gate, as a literal so it survives any bundler. */
|
|
3
|
-
export const GATE_VERSION: string = "0.
|
|
3
|
+
export const GATE_VERSION: string = "0.6.0";
|
|
4
4
|
/** `@flayerlabs/gamemode-gate@<version>`: the text a build of this package can be searched for. */
|
|
5
|
-
export const GATE_MARKER: string = "@flayerlabs/gamemode-gate@0.
|
|
5
|
+
export const GATE_MARKER: string = "@flayerlabs/gamemode-gate@0.6.0";
|