@flayerlabs/gamemode-gate 0.1.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/LICENSE +9 -0
- package/README.md +79 -0
- package/dist/chain/discover.d.ts +158 -0
- package/dist/chain/discover.d.ts.map +1 -0
- package/dist/chain/discover.js +169 -0
- package/dist/chain/discover.js.map +1 -0
- package/dist/chain/signer.d.ts +46 -0
- package/dist/chain/signer.d.ts.map +1 -0
- package/dist/chain/signer.js +60 -0
- package/dist/chain/signer.js.map +1 -0
- package/dist/claims.d.ts +62 -0
- package/dist/claims.d.ts.map +1 -0
- package/dist/claims.js +128 -0
- package/dist/claims.js.map +1 -0
- package/dist/demo.d.ts +40 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +90 -0
- package/dist/demo.js.map +1 -0
- package/dist/economy.d.ts +46 -0
- package/dist/economy.d.ts.map +1 -0
- package/dist/economy.js +100 -0
- package/dist/economy.js.map +1 -0
- package/dist/game-registry.d.ts +17 -0
- package/dist/game-registry.d.ts.map +1 -0
- package/dist/game-registry.js +90 -0
- package/dist/game-registry.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +87 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +222 -0
- package/dist/ledger.js.map +1 -0
- package/dist/registry.d.ts +55 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +164 -0
- package/dist/registry.js.map +1 -0
- package/dist/room.d.ts +98 -0
- package/dist/room.d.ts.map +1 -0
- package/dist/room.js +215 -0
- package/dist/room.js.map +1 -0
- package/dist/server.d.ts +92 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +695 -0
- package/dist/server.js.map +1 -0
- package/dist/sessions.d.ts +40 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +144 -0
- package/dist/sessions.js.map +1 -0
- package/dist/settlement.d.ts +24 -0
- package/dist/settlement.d.ts.map +1 -0
- package/dist/settlement.js +69 -0
- package/dist/settlement.js.map +1 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +125 -0
- package/dist/store.js.map +1 -0
- package/dist/turnstile.d.ts +19 -0
- package/dist/turnstile.d.ts.map +1 -0
- package/dist/turnstile.js +61 -0
- package/dist/turnstile.js.map +1 -0
- package/package.json +54 -0
- package/src/chain/discover.ts +243 -0
- package/src/chain/signer.ts +118 -0
- package/src/claims.ts +140 -0
- package/src/demo.ts +149 -0
- package/src/economy.ts +126 -0
- package/src/game-registry.ts +107 -0
- package/src/index.ts +45 -0
- package/src/ledger.ts +355 -0
- package/src/registry.ts +212 -0
- package/src/room.ts +297 -0
- package/src/server.ts +833 -0
- package/src/sessions.ts +155 -0
- package/src/settlement.ts +72 -0
- package/src/store.ts +127 -0
- package/src/turnstile.ts +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flayer Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @flayerlabs/gamemode-gate
|
|
2
|
+
|
|
3
|
+
Run server-authoritative Game Mode rooms, spending allowance, admission and game discovery.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
You need:
|
|
8
|
+
|
|
9
|
+
- Node.js 20 or later
|
|
10
|
+
- PostgreSQL
|
|
11
|
+
- `@flayerlabs/gamemode-spec` at the matching SDK version
|
|
12
|
+
|
|
13
|
+
Install the package and direct database dependency:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @flayerlabs/gamemode-gate @flayerlabs/gamemode-spec pg
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Start a local gate without a chain
|
|
20
|
+
|
|
21
|
+
`createDemoGate` starts the real gate flow with a stand-in wallet and no chain access. It expects
|
|
22
|
+
PostgreSQL at `postgres://postgres:gamemode@127.0.0.1:55439/gamemode` unless you set `DATABASE_URL`.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createDemoGate } from '@flayerlabs/gamemode-gate'
|
|
26
|
+
import { rules, type Config } from './game/rules.js'
|
|
27
|
+
|
|
28
|
+
const config: Config = {
|
|
29
|
+
rounds: 3,
|
|
30
|
+
ceiling: 10,
|
|
31
|
+
points: 500,
|
|
32
|
+
roundMs: 10_000,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const demo = await createDemoGate({
|
|
36
|
+
game: rules,
|
|
37
|
+
config,
|
|
38
|
+
gateOrigin: 'http://127.0.0.1:4000',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await demo.app.listen({ host: '127.0.0.1', port: 4000 })
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This path uses the real room, database, socket and signed spend authorisations. It records purchases
|
|
45
|
+
instead of submitting them to a chain. Do not use `createDemoGate` in production.
|
|
46
|
+
|
|
47
|
+
## Build a production gate
|
|
48
|
+
|
|
49
|
+
Run `migrate(pool)` before the gate accepts traffic. A `createGate` setup needs:
|
|
50
|
+
|
|
51
|
+
- `Sessions` configured with the page domain and the gate's exact public origin
|
|
52
|
+
- `Claims`, `Ledger` and `PayloadSigner` for spending authorisations
|
|
53
|
+
- `pointsPerDollar`, `usdPerEth` and the allowed game origins
|
|
54
|
+
|
|
55
|
+
Add the chain services used by your deployment:
|
|
56
|
+
|
|
57
|
+
- `Discovery` verifies each launch before the gate adopts it
|
|
58
|
+
- `Settlement` updates recorded spending from the chain
|
|
59
|
+
|
|
60
|
+
Run one gate process for each game. The current room writer does not support multiple replicas.
|
|
61
|
+
|
|
62
|
+
Use `createTurnstileAdmit` if the trusted parent supplies Cloudflare Turnstile evidence. The policy
|
|
63
|
+
checks session creation. Game action and claim policy remain the gate operator's responsibility.
|
|
64
|
+
|
|
65
|
+
Read the [production gate guide](https://github.com/flayerlabs/gamemode-sdk/blob/main/docs/guides/run-a-gate.md)
|
|
66
|
+
before deploying a gate.
|
|
67
|
+
|
|
68
|
+
## Run the central game registry
|
|
69
|
+
|
|
70
|
+
`createGameRegistry` provides:
|
|
71
|
+
|
|
72
|
+
- public `GET /games/:chainId/:coin` lookups for trusted Flaunch pages
|
|
73
|
+
- authenticated internal routes for the Flaunch launch backend to pin or disable a registration
|
|
74
|
+
|
|
75
|
+
The registry is a central Flaunch service. Do not run it inside a creator gate. Its
|
|
76
|
+
`writerToken` must contain at least 32 characters and stay on the launch backend.
|
|
77
|
+
|
|
78
|
+
Read the current request and response behaviour in the
|
|
79
|
+
[game registry source](https://github.com/flayerlabs/gamemode-sdk/blob/main/packages/gate/src/game-registry.ts).
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { type PublicClient } from 'viem';
|
|
2
|
+
/**
|
|
3
|
+
* All this needs of a client: the ability to read a contract.
|
|
4
|
+
*
|
|
5
|
+
* Naming the one method rather than taking a whole `PublicClient` also sidesteps viem's chain
|
|
6
|
+
* generics, which make a client built for one chain structurally incompatible with the general
|
|
7
|
+
* type over unrelated methods like `getBlock`.
|
|
8
|
+
*/
|
|
9
|
+
export type ChainReader = Pick<PublicClient, 'readContract'>;
|
|
10
|
+
export declare const positionManagerAbi: readonly [{
|
|
11
|
+
readonly type: "function";
|
|
12
|
+
readonly name: "poolKey";
|
|
13
|
+
readonly stateMutability: "view";
|
|
14
|
+
readonly inputs: readonly [{
|
|
15
|
+
readonly name: "_token";
|
|
16
|
+
readonly type: "address";
|
|
17
|
+
}];
|
|
18
|
+
readonly outputs: readonly [{
|
|
19
|
+
readonly name: "";
|
|
20
|
+
readonly type: "tuple";
|
|
21
|
+
readonly components: readonly [{
|
|
22
|
+
readonly name: "currency0";
|
|
23
|
+
readonly type: "address";
|
|
24
|
+
}, {
|
|
25
|
+
readonly name: "currency1";
|
|
26
|
+
readonly type: "address";
|
|
27
|
+
}, {
|
|
28
|
+
readonly name: "fee";
|
|
29
|
+
readonly type: "uint24";
|
|
30
|
+
}, {
|
|
31
|
+
readonly name: "tickSpacing";
|
|
32
|
+
readonly type: "int24";
|
|
33
|
+
}, {
|
|
34
|
+
readonly name: "hooks";
|
|
35
|
+
readonly type: "address";
|
|
36
|
+
}];
|
|
37
|
+
}];
|
|
38
|
+
}, {
|
|
39
|
+
readonly name: "flaunchesAt";
|
|
40
|
+
readonly type: "function";
|
|
41
|
+
readonly stateMutability: "view";
|
|
42
|
+
readonly inputs: readonly [{
|
|
43
|
+
readonly type: "bytes32";
|
|
44
|
+
readonly name: "_poolId";
|
|
45
|
+
}];
|
|
46
|
+
readonly outputs: readonly [{
|
|
47
|
+
readonly type: "uint256";
|
|
48
|
+
}];
|
|
49
|
+
}];
|
|
50
|
+
export declare const spendGatedAbi: readonly [{
|
|
51
|
+
readonly name: "spendGateSettings";
|
|
52
|
+
readonly type: "function";
|
|
53
|
+
readonly stateMutability: "view";
|
|
54
|
+
readonly inputs: readonly [{
|
|
55
|
+
readonly type: "bytes32";
|
|
56
|
+
readonly name: "_poolId";
|
|
57
|
+
}];
|
|
58
|
+
readonly outputs: readonly [{
|
|
59
|
+
readonly type: "bool";
|
|
60
|
+
readonly name: "enabled";
|
|
61
|
+
}, {
|
|
62
|
+
readonly type: "uint256";
|
|
63
|
+
readonly name: "walletCapWei";
|
|
64
|
+
}, {
|
|
65
|
+
readonly type: "address";
|
|
66
|
+
readonly name: "settler";
|
|
67
|
+
}, {
|
|
68
|
+
readonly type: "uint256";
|
|
69
|
+
readonly name: "endsAt";
|
|
70
|
+
}];
|
|
71
|
+
}, {
|
|
72
|
+
readonly name: "trustedPoolKeySigner";
|
|
73
|
+
readonly type: "function";
|
|
74
|
+
readonly stateMutability: "view";
|
|
75
|
+
readonly inputs: readonly [{
|
|
76
|
+
readonly type: "bytes32";
|
|
77
|
+
readonly name: "_poolId";
|
|
78
|
+
}];
|
|
79
|
+
readonly outputs: readonly [{
|
|
80
|
+
readonly type: "address";
|
|
81
|
+
readonly name: "signer";
|
|
82
|
+
}, {
|
|
83
|
+
readonly type: "bool";
|
|
84
|
+
readonly name: "enabled";
|
|
85
|
+
}];
|
|
86
|
+
}];
|
|
87
|
+
export interface PoolKey {
|
|
88
|
+
currency0: `0x${string}`;
|
|
89
|
+
currency1: `0x${string}`;
|
|
90
|
+
fee: number;
|
|
91
|
+
tickSpacing: number;
|
|
92
|
+
hooks: `0x${string}`;
|
|
93
|
+
}
|
|
94
|
+
export interface Launch {
|
|
95
|
+
coinAddress: `0x${string}`;
|
|
96
|
+
poolId: `0x${string}`;
|
|
97
|
+
poolKey: PoolKey;
|
|
98
|
+
/** When buying opens, in epoch ms. */
|
|
99
|
+
opensAt: number;
|
|
100
|
+
/** When the gate stops enforcing on chain, in epoch ms. */
|
|
101
|
+
closesAt: number;
|
|
102
|
+
walletCapWei: bigint;
|
|
103
|
+
signer: `0x${string}`;
|
|
104
|
+
name: string;
|
|
105
|
+
symbol: string;
|
|
106
|
+
imageUrl?: string | null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A refusal, with the one distinction that matters.
|
|
110
|
+
*
|
|
111
|
+
* `retryable` separates "the chain has not caught up yet" from "this coin is not a game launch".
|
|
112
|
+
* Collapsing the two once cost a real launch its whole window: a coin page loaded the instant a
|
|
113
|
+
* launch was submitted, asked before the pool was readable, was told "not a game launch", and both
|
|
114
|
+
* ends cached that answer for thirty seconds each — against a ninety-second window.
|
|
115
|
+
*/
|
|
116
|
+
export interface Refused {
|
|
117
|
+
ok: false;
|
|
118
|
+
code: string;
|
|
119
|
+
retryable: boolean;
|
|
120
|
+
}
|
|
121
|
+
export type LaunchCheck = ({
|
|
122
|
+
ok: true;
|
|
123
|
+
} & Launch) | Refused;
|
|
124
|
+
export interface Deployment {
|
|
125
|
+
positionManager: `0x${string}`;
|
|
126
|
+
spendGatedCalculator: `0x${string}`;
|
|
127
|
+
/** The signer this gate holds. A launch naming anyone else is not ours to run. */
|
|
128
|
+
signer: `0x${string}`;
|
|
129
|
+
}
|
|
130
|
+
export declare function poolIdOf(key: PoolKey): `0x${string}`;
|
|
131
|
+
/**
|
|
132
|
+
* Verify a coin, from the chain.
|
|
133
|
+
*
|
|
134
|
+
* Every read is wrapped, because an RPC failure is not a statement about the token. Treating one as
|
|
135
|
+
* "not a game launch" is exactly the mistake described on {@link Refused}.
|
|
136
|
+
*/
|
|
137
|
+
export declare function verifyLaunch(client: ChainReader, deployment: Deployment, memecoin: `0x${string}`): Promise<LaunchCheck>;
|
|
138
|
+
/**
|
|
139
|
+
* Verification with a memory, because every coin-page view asks.
|
|
140
|
+
*
|
|
141
|
+
* Unmemoised, each call costs five chain reads against one endpoint from one address — which is a
|
|
142
|
+
* free denial-of-service lever, and public endpoints rate-limit long before a launch does.
|
|
143
|
+
*
|
|
144
|
+
* A positive verdict is cached until the window closes, because a launch that qualifies cannot
|
|
145
|
+
* un-qualify while it is open. A retryable refusal is cached only briefly, and a permanent one for
|
|
146
|
+
* longer — the whole point of the distinction is that "not yet" must not be remembered like "no".
|
|
147
|
+
*/
|
|
148
|
+
export declare class Discovery {
|
|
149
|
+
private readonly client;
|
|
150
|
+
private readonly deployment;
|
|
151
|
+
private readonly now;
|
|
152
|
+
private readonly verdicts;
|
|
153
|
+
private readonly inFlight;
|
|
154
|
+
constructor(client: ChainReader, deployment: Deployment, now?: () => number);
|
|
155
|
+
verify(memecoin: `0x${string}`): Promise<LaunchCheck>;
|
|
156
|
+
private holdFor;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/chain/discover.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4C,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAEnF;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;AAuB7D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUrB,CAAC;AAEX,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKxB,CAAC;AAIH,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,KAAK,MAAM,EAAE,CAAC;IACzB,SAAS,EAAE,KAAK,MAAM,EAAE,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,KAAK,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,MAAM;IACrB,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC;AAE5D,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,KAAK,MAAM,EAAE,CAAC;IAC/B,oBAAoB,EAAE,KAAK,MAAM,EAAE,CAAC;IACpC,kFAAkF;IAClF,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;CACvB;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,MAAM,EAAE,CAOpD;AAID;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,KAAK,MAAM,EAAE,GACtB,OAAO,CAAC,WAAW,CAAC,CA2EtB;AAED;;;;;;;;;GASG;AACH,qBAAa,SAAS;IAKlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG;IANtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8D;IACvF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;gBAGjD,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,UAAU,EACtB,GAAG,GAAE,MAAM,MAAiB;IAGzC,MAAM,CAAC,QAAQ,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC;IAqB3D,OAAO,CAAC,OAAO;CAIhB"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { encodeAbiParameters, keccak256, parseAbi } from 'viem';
|
|
2
|
+
/**
|
|
3
|
+
* Asking the chain whether a coin is really a game launch, and on what terms.
|
|
4
|
+
*
|
|
5
|
+
* This is what makes rooms exist because a LAUNCH exists rather than because somebody asked for
|
|
6
|
+
* one. A room used to be minted on demand, which on a public URL means anyone can invent as many
|
|
7
|
+
* as they like.
|
|
8
|
+
*
|
|
9
|
+
* The authorisation is already on chain: a launch that wants a game names its signer in the
|
|
10
|
+
* launch's own gate parameters, and that is public state the creator paid to write and nobody else
|
|
11
|
+
* can forge. So adoption needs no API key and no allowlist — anyone may ask, and the chain decides.
|
|
12
|
+
* That single property is what makes third-party games permissionless.
|
|
13
|
+
*/
|
|
14
|
+
const poolKeyComponents = [
|
|
15
|
+
{ name: 'currency0', type: 'address' },
|
|
16
|
+
{ name: 'currency1', type: 'address' },
|
|
17
|
+
{ name: 'fee', type: 'uint24' },
|
|
18
|
+
{ name: 'tickSpacing', type: 'int24' },
|
|
19
|
+
{ name: 'hooks', type: 'address' },
|
|
20
|
+
];
|
|
21
|
+
export const positionManagerAbi = [
|
|
22
|
+
{
|
|
23
|
+
type: 'function',
|
|
24
|
+
name: 'poolKey',
|
|
25
|
+
stateMutability: 'view',
|
|
26
|
+
inputs: [{ name: '_token', type: 'address' }],
|
|
27
|
+
outputs: [{ name: '', type: 'tuple', components: poolKeyComponents }],
|
|
28
|
+
},
|
|
29
|
+
// The scheduled open of the buy window; 0 once the pool is tradeable.
|
|
30
|
+
...parseAbi(['function flaunchesAt(bytes32 _poolId) view returns (uint256)']),
|
|
31
|
+
];
|
|
32
|
+
export const spendGatedAbi = parseAbi([
|
|
33
|
+
// Four fields. Decoding a four-word return against a shorter ABI is how a check quietly reads the
|
|
34
|
+
// wrong value, so this must match the DEPLOYED calculator rather than the newest source.
|
|
35
|
+
'function spendGateSettings(bytes32 _poolId) view returns (bool enabled, uint256 walletCapWei, address settler, uint256 endsAt)',
|
|
36
|
+
'function trustedPoolKeySigner(bytes32 _poolId) view returns (address signer, bool enabled)',
|
|
37
|
+
]);
|
|
38
|
+
const erc20 = parseAbi(['function name() view returns (string)', 'function symbol() view returns (string)']);
|
|
39
|
+
export function poolIdOf(key) {
|
|
40
|
+
return keccak256(encodeAbiParameters([{ type: 'address' }, { type: 'address' }, { type: 'uint24' }, { type: 'int24' }, { type: 'address' }], [key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks]));
|
|
41
|
+
}
|
|
42
|
+
const refuse = (code, retryable) => ({ ok: false, code, retryable });
|
|
43
|
+
/**
|
|
44
|
+
* Verify a coin, from the chain.
|
|
45
|
+
*
|
|
46
|
+
* Every read is wrapped, because an RPC failure is not a statement about the token. Treating one as
|
|
47
|
+
* "not a game launch" is exactly the mistake described on {@link Refused}.
|
|
48
|
+
*/
|
|
49
|
+
export async function verifyLaunch(client, deployment, memecoin) {
|
|
50
|
+
let poolKey;
|
|
51
|
+
try {
|
|
52
|
+
poolKey = (await client.readContract({
|
|
53
|
+
address: deployment.positionManager,
|
|
54
|
+
abi: positionManagerAbi,
|
|
55
|
+
functionName: 'poolKey',
|
|
56
|
+
args: [memecoin],
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return refuse('launch.unreadable', true);
|
|
61
|
+
}
|
|
62
|
+
// A coin the protocol has never heard of returns an empty key rather than throwing.
|
|
63
|
+
if (!poolKey || poolKey.hooks === '0x0000000000000000000000000000000000000000') {
|
|
64
|
+
return refuse('launch.not_flaunched', true);
|
|
65
|
+
}
|
|
66
|
+
const poolId = poolIdOf(poolKey);
|
|
67
|
+
let settings;
|
|
68
|
+
let trusted;
|
|
69
|
+
let flaunchesAt;
|
|
70
|
+
try {
|
|
71
|
+
[settings, trusted, flaunchesAt] = (await Promise.all([
|
|
72
|
+
client.readContract({
|
|
73
|
+
address: deployment.spendGatedCalculator,
|
|
74
|
+
abi: spendGatedAbi,
|
|
75
|
+
functionName: 'spendGateSettings',
|
|
76
|
+
args: [poolId],
|
|
77
|
+
}),
|
|
78
|
+
client.readContract({
|
|
79
|
+
address: deployment.spendGatedCalculator,
|
|
80
|
+
abi: spendGatedAbi,
|
|
81
|
+
functionName: 'trustedPoolKeySigner',
|
|
82
|
+
args: [poolId],
|
|
83
|
+
}),
|
|
84
|
+
client.readContract({
|
|
85
|
+
address: deployment.positionManager,
|
|
86
|
+
abi: positionManagerAbi,
|
|
87
|
+
functionName: 'flaunchesAt',
|
|
88
|
+
args: [poolId],
|
|
89
|
+
}),
|
|
90
|
+
]));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return refuse('launch.unreadable', true);
|
|
94
|
+
}
|
|
95
|
+
const [enabled, walletCapWei, , endsAt] = settings;
|
|
96
|
+
const [signer, signerEnabled] = trusted;
|
|
97
|
+
if (!enabled)
|
|
98
|
+
return refuse('launch.not_a_game', false);
|
|
99
|
+
// Not ours to run. Permanent: another gate holds that key, and no amount of waiting changes it.
|
|
100
|
+
if (!signerEnabled || signer.toLowerCase() !== deployment.signer.toLowerCase()) {
|
|
101
|
+
return refuse('launch.another_signer', false);
|
|
102
|
+
}
|
|
103
|
+
if (walletCapWei <= 0n)
|
|
104
|
+
return refuse('launch.no_spending_cap', false);
|
|
105
|
+
const opensAt = Number(flaunchesAt) * 1000;
|
|
106
|
+
const closesAt = Number(endsAt) * 1000;
|
|
107
|
+
if (closesAt <= Date.now())
|
|
108
|
+
return refuse('launch.already_over', false);
|
|
109
|
+
let name = '';
|
|
110
|
+
let symbol = '';
|
|
111
|
+
try {
|
|
112
|
+
[name, symbol] = (await Promise.all([
|
|
113
|
+
client.readContract({ address: memecoin, abi: erc20, functionName: 'name' }),
|
|
114
|
+
client.readContract({ address: memecoin, abi: erc20, functionName: 'symbol' }),
|
|
115
|
+
]));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Cosmetic. A coin with an unreadable name is still a valid launch, and refusing one over a
|
|
119
|
+
// display string would be refusing a round for no reason anyone could act on.
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, coinAddress: memecoin, poolId, poolKey, opensAt, closesAt, walletCapWei, signer, name, symbol };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Verification with a memory, because every coin-page view asks.
|
|
125
|
+
*
|
|
126
|
+
* Unmemoised, each call costs five chain reads against one endpoint from one address — which is a
|
|
127
|
+
* free denial-of-service lever, and public endpoints rate-limit long before a launch does.
|
|
128
|
+
*
|
|
129
|
+
* A positive verdict is cached until the window closes, because a launch that qualifies cannot
|
|
130
|
+
* un-qualify while it is open. A retryable refusal is cached only briefly, and a permanent one for
|
|
131
|
+
* longer — the whole point of the distinction is that "not yet" must not be remembered like "no".
|
|
132
|
+
*/
|
|
133
|
+
export class Discovery {
|
|
134
|
+
client;
|
|
135
|
+
deployment;
|
|
136
|
+
now;
|
|
137
|
+
verdicts = new Map();
|
|
138
|
+
inFlight = new Map();
|
|
139
|
+
constructor(client, deployment, now = Date.now) {
|
|
140
|
+
this.client = client;
|
|
141
|
+
this.deployment = deployment;
|
|
142
|
+
this.now = now;
|
|
143
|
+
}
|
|
144
|
+
async verify(memecoin) {
|
|
145
|
+
const key = memecoin.toLowerCase();
|
|
146
|
+
const cached = this.verdicts.get(key);
|
|
147
|
+
if (cached && cached.until > this.now())
|
|
148
|
+
return cached.verdict;
|
|
149
|
+
// One flight per coin: a burst of coin-page views is one caller as far as the chain is
|
|
150
|
+
// concerned, and without this the first launch of the day is its own thundering herd.
|
|
151
|
+
const existing = this.inFlight.get(key);
|
|
152
|
+
if (existing)
|
|
153
|
+
return existing;
|
|
154
|
+
const flight = verifyLaunch(this.client, this.deployment, memecoin)
|
|
155
|
+
.then((verdict) => {
|
|
156
|
+
this.verdicts.set(key, { verdict, until: this.now() + this.holdFor(verdict) });
|
|
157
|
+
return verdict;
|
|
158
|
+
})
|
|
159
|
+
.finally(() => this.inFlight.delete(key));
|
|
160
|
+
this.inFlight.set(key, flight);
|
|
161
|
+
return flight;
|
|
162
|
+
}
|
|
163
|
+
holdFor(verdict) {
|
|
164
|
+
if (verdict.ok)
|
|
165
|
+
return Math.max(0, verdict.closesAt - this.now());
|
|
166
|
+
return verdict.retryable ? 3_000 : 60_000;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../../src/chain/discover.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issues the signed authorizations SpendGatedSignerFeeCalculator.trackSwap verifies.
|
|
3
|
+
*
|
|
4
|
+
* EIP-712, over the `SpendAuthorization` struct the calculator declares. It used to be an
|
|
5
|
+
* EIP-191 personal-sign over `keccak256(abi.encodePacked(...))`, which committed to the five
|
|
6
|
+
* fields and nothing else — no chain id, no verifying contract. An audit flagged that as the
|
|
7
|
+
* same defect already raised against the parent calculator (M-6, remediated there in the
|
|
8
|
+
* EIP-712 PR but never ported to this fork): the protocol deploys deterministically across
|
|
9
|
+
* chains, so the same poolId genuinely recurs, and one signing key shared between two
|
|
10
|
+
* environments made an authorization issued for one valid on the other. The domain closes it.
|
|
11
|
+
*
|
|
12
|
+
* The digest MUST match `hashSpendAuthorization` on the calculator exactly. That function is
|
|
13
|
+
* public for this reason — a domain drift is otherwise a silent `InvalidSigner` at swap time
|
|
14
|
+
* rather than a loud failure at signing time. Parity is pinned by test/signer-parity.test.ts
|
|
15
|
+
* against vectors generated with Foundry.
|
|
16
|
+
*/
|
|
17
|
+
export interface SignedPayload {
|
|
18
|
+
buyer: `0x${string}`;
|
|
19
|
+
poolId: `0x${string}`;
|
|
20
|
+
deadline: bigint;
|
|
21
|
+
maxSpendWei: bigint;
|
|
22
|
+
nonce: bigint;
|
|
23
|
+
signature: `0x${string}`;
|
|
24
|
+
signer: `0x${string}`;
|
|
25
|
+
}
|
|
26
|
+
export declare class PayloadSigner {
|
|
27
|
+
private readonly chainId;
|
|
28
|
+
private readonly verifyingContract;
|
|
29
|
+
private readonly account;
|
|
30
|
+
/**
|
|
31
|
+
* @param verifyingContract The calculator that will validate these signatures. Binding it is
|
|
32
|
+
* half the point of the domain. Unit tests may use a placeholder because they have no deployment.
|
|
33
|
+
* A production operator must pass the calculator that chain discovery also verifies.
|
|
34
|
+
*/
|
|
35
|
+
constructor(privateKey: `0x${string}`, chainId: number, verifyingContract: `0x${string}`);
|
|
36
|
+
get address(): `0x${string}`;
|
|
37
|
+
/** The digest the calculator reconstructs; mirrors its `hashSpendAuthorization`. */
|
|
38
|
+
messageHash(buyer: `0x${string}`, poolId: `0x${string}`, deadline: bigint, maxSpendWei: bigint, nonce: bigint): `0x${string}`;
|
|
39
|
+
sign(buyer: `0x${string}`, poolId: `0x${string}`, deadline: bigint, maxSpendWei: bigint, nonce: bigint): Promise<SignedPayload>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Encodes a signed payload as the hookData the swap must carry:
|
|
43
|
+
* `abi.encode(address referrer, SignedMessage{buyer, poolId, deadline, maxSpendWei, nonce, signature})`
|
|
44
|
+
*/
|
|
45
|
+
export declare function encodeHookData(payload: SignedPayload): `0x${string}`;
|
|
46
|
+
//# sourceMappingURL=signer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/chain/signer.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,KAAK,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,KAAK,MAAM,EAAE,CAAC;IACzB,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;CACvB;AAED,qBAAa,aAAa;IAUtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAVpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C;;;;OAIG;gBAED,UAAU,EAAE,KAAK,MAAM,EAAE,EACR,OAAO,EAAE,MAAM,EACf,iBAAiB,EAAE,KAAK,MAAM,EAAE;IAKnD,IAAI,OAAO,IAAI,KAAK,MAAM,EAAE,CAE3B;IAED,oFAAoF;IACpF,WAAW,CACT,KAAK,EAAE,KAAK,MAAM,EAAE,EACpB,MAAM,EAAE,KAAK,MAAM,EAAE,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,KAAK,MAAM,EAAE;IAUV,IAAI,CACR,KAAK,EAAE,KAAK,MAAM,EAAE,EACpB,MAAM,EAAE,KAAK,MAAM,EAAE,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,CAAC;CAU1B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,KAAK,MAAM,EAAE,CA4BpE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { encodeAbiParameters, hashTypedData, zeroAddress } from 'viem';
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
import { spendAuthorisationTypedData } from '@flayerlabs/gamemode-spec/embed';
|
|
4
|
+
export class PayloadSigner {
|
|
5
|
+
chainId;
|
|
6
|
+
verifyingContract;
|
|
7
|
+
account;
|
|
8
|
+
/**
|
|
9
|
+
* @param verifyingContract The calculator that will validate these signatures. Binding it is
|
|
10
|
+
* half the point of the domain. Unit tests may use a placeholder because they have no deployment.
|
|
11
|
+
* A production operator must pass the calculator that chain discovery also verifies.
|
|
12
|
+
*/
|
|
13
|
+
constructor(privateKey, chainId, verifyingContract) {
|
|
14
|
+
this.chainId = chainId;
|
|
15
|
+
this.verifyingContract = verifyingContract;
|
|
16
|
+
this.account = privateKeyToAccount(privateKey);
|
|
17
|
+
}
|
|
18
|
+
get address() {
|
|
19
|
+
return this.account.address;
|
|
20
|
+
}
|
|
21
|
+
/** The digest the calculator reconstructs; mirrors its `hashSpendAuthorization`. */
|
|
22
|
+
messageHash(buyer, poolId, deadline, maxSpendWei, nonce) {
|
|
23
|
+
return hashTypedData(spendAuthorisationTypedData({ buyer, poolId, deadline, maxSpendWei, nonce }, this.chainId, this.verifyingContract));
|
|
24
|
+
}
|
|
25
|
+
async sign(buyer, poolId, deadline, maxSpendWei, nonce) {
|
|
26
|
+
const signature = await this.account.signTypedData(spendAuthorisationTypedData({ buyer, poolId, deadline, maxSpendWei, nonce }, this.chainId, this.verifyingContract));
|
|
27
|
+
return { buyer, poolId, deadline, maxSpendWei, nonce, signature, signer: this.account.address };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Encodes a signed payload as the hookData the swap must carry:
|
|
32
|
+
* `abi.encode(address referrer, SignedMessage{buyer, poolId, deadline, maxSpendWei, nonce, signature})`
|
|
33
|
+
*/
|
|
34
|
+
export function encodeHookData(payload) {
|
|
35
|
+
return encodeAbiParameters([
|
|
36
|
+
{ type: 'address' },
|
|
37
|
+
{
|
|
38
|
+
type: 'tuple',
|
|
39
|
+
components: [
|
|
40
|
+
{ name: 'buyer', type: 'address' },
|
|
41
|
+
{ name: 'poolId', type: 'bytes32' },
|
|
42
|
+
{ name: 'deadline', type: 'uint256' },
|
|
43
|
+
{ name: 'maxSpendWei', type: 'uint256' },
|
|
44
|
+
{ name: 'nonce', type: 'uint256' },
|
|
45
|
+
{ name: 'signature', type: 'bytes' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
], [
|
|
49
|
+
zeroAddress,
|
|
50
|
+
{
|
|
51
|
+
buyer: payload.buyer,
|
|
52
|
+
poolId: payload.poolId,
|
|
53
|
+
deadline: payload.deadline,
|
|
54
|
+
maxSpendWei: payload.maxSpendWei,
|
|
55
|
+
nonce: payload.nonce,
|
|
56
|
+
signature: payload.signature,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=signer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/chain/signer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AACvE,OAAO,EAAE,mBAAmB,EAA0B,MAAM,eAAe,CAAC;AAC5E,OAAO,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AA4B9E,MAAM,OAAO,aAAa;IAUL;IACA;IAVF,OAAO,CAAoB;IAE5C;;;;OAIG;IACH,YACE,UAAyB,EACR,OAAe,EACf,iBAAgC;QADhC,YAAO,GAAP,OAAO,CAAQ;QACf,sBAAiB,GAAjB,iBAAiB,CAAe;QAEjD,IAAI,CAAC,OAAO,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;IAC9B,CAAC;IAED,oFAAoF;IACpF,WAAW,CACT,KAAoB,EACpB,MAAqB,EACrB,QAAgB,EAChB,WAAmB,EACnB,KAAa;QAEb,OAAO,aAAa,CAClB,2BAA2B,CACzB,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,EAC/C,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,iBAAiB,CACvB,CACF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CACR,KAAoB,EACpB,MAAqB,EACrB,QAAgB,EAChB,WAAmB,EACnB,KAAa;QAEb,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAChD,2BAA2B,CACzB,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,EAC/C,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,iBAAiB,CACvB,CACF,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAClG,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAsB;IACnD,OAAO,mBAAmB,CACxB;QACE,EAAE,IAAI,EAAE,SAAS,EAAE;QACnB;YACE,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;gBAClC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE;gBACnC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE;gBACrC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE;gBACxC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;gBAClC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE;aACrC;SACF;KACF,EACD;QACE,WAAW;QACX;YACE,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,SAAS,EAAE,OAAO,CAAC,SAAS;SAC7B;KACF,CACF,CAAC;AACJ,CAAC"}
|
package/dist/claims.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
import type { PlayerId } from '@flayerlabs/gamemode-spec';
|
|
3
|
+
import type { EconomyBalance } from '@flayerlabs/gamemode-spec/live';
|
|
4
|
+
import { Ledger, type BalanceKey } from './ledger.js';
|
|
5
|
+
import type { PayloadSigner, SignedPayload } from './chain/signer.js';
|
|
6
|
+
/**
|
|
7
|
+
* Turning points into something a player can actually spend.
|
|
8
|
+
*
|
|
9
|
+
* Two parties have to agree before a buy can happen, and they check different things. The ledger
|
|
10
|
+
* knows what this player earned and what they have already been authorised for. The signer knows
|
|
11
|
+
* whether it is willing to vouch for this pool at all. Only when both say yes does an
|
|
12
|
+
* authorisation exist.
|
|
13
|
+
*
|
|
14
|
+
* The order matters and is not interchangeable: the hold is recorded first, then signed. A
|
|
15
|
+
* signature that exists without a hold is spending nobody is tracking; a hold without a signature
|
|
16
|
+
* is a refund waiting to happen. Only one of those costs money.
|
|
17
|
+
*/
|
|
18
|
+
/** How long an authorisation stays valid. Long enough to approve in a wallet, short enough that an
|
|
19
|
+
* abandoned one does not sit around spendable. */
|
|
20
|
+
export declare const CLAIM_AUTHORIZATION_TTL_MS: number;
|
|
21
|
+
/** Request ids cross an unauthenticated parser before they reach Postgres, so bound them here too. */
|
|
22
|
+
export declare function isClaimRequestId(value: unknown): value is string;
|
|
23
|
+
/** The signed message carries uint256, while JavaScript's bigint has no upper bound. */
|
|
24
|
+
export declare function isClaimAmount(value: unknown): value is bigint;
|
|
25
|
+
export type Authorisation = {
|
|
26
|
+
ok: true;
|
|
27
|
+
payload: SignedPayload;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
refuse: string;
|
|
31
|
+
};
|
|
32
|
+
export declare class Claims {
|
|
33
|
+
private readonly pool;
|
|
34
|
+
private readonly ledger;
|
|
35
|
+
private readonly signer;
|
|
36
|
+
/** Called when a hold could neither be signed nor released, and needs a human. */
|
|
37
|
+
private readonly onStrandedHold?;
|
|
38
|
+
/** An internal seam for exact window-boundary tests. Production uses the process clock. */
|
|
39
|
+
private readonly now;
|
|
40
|
+
constructor(pool: Pool, ledger: Ledger, signer: PayloadSigner,
|
|
41
|
+
/** Called when a hold could neither be signed nor released, and needs a human. */
|
|
42
|
+
onStrandedHold?: ((nonce: bigint, error: unknown) => void) | undefined,
|
|
43
|
+
/** An internal seam for exact window-boundary tests. Production uses the process clock. */
|
|
44
|
+
now?: () => number);
|
|
45
|
+
/**
|
|
46
|
+
* Authorise a player to spend up to `upToWei`, or explain why not.
|
|
47
|
+
*
|
|
48
|
+
* The ledger's ceiling is a conservative mirror of the chain's: the contract tracks
|
|
49
|
+
* `walletSpentWei` per pool per wallet and enforces it whatever we believe. Being ahead of the
|
|
50
|
+
* chain is safe — we simply authorise less than we could. Being behind it would mean issuing a
|
|
51
|
+
* signature that reverts at the swap, which to a player is indistinguishable from being robbed.
|
|
52
|
+
*/
|
|
53
|
+
authorise(roundId: string, player: PlayerId, upToWei: bigint, requestId: string): Promise<Authorisation>;
|
|
54
|
+
private finish;
|
|
55
|
+
/** Give back every authorisation that expired unspent. */
|
|
56
|
+
releaseExpired(): Promise<BalanceKey[]>;
|
|
57
|
+
/** What this player could spend right now. */
|
|
58
|
+
availableWei(roundId: string, player: PlayerId): Promise<bigint>;
|
|
59
|
+
/** Full authoritative balance for live snapshots. */
|
|
60
|
+
economyBalance(roundId: string, player: PlayerId): Promise<EconomyBalance>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=claims.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims.d.ts","sourceRoot":"","sources":["../src/claims.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAErE,OAAO,EAAE,MAAM,EAAE,KAAK,UAAU,EAAc,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEtE;;;;;;;;;;;GAWG;AAEH;mDACmD;AACnD,eAAO,MAAM,0BAA0B,QAAa,CAAC;AAIrD,sGAAsG;AACtG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEhE;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE7D;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,GACpC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,qBAAa,MAAM;IAEf,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAChC,2FAA2F;IAC3F,OAAO,CAAC,QAAQ,CAAC,GAAG;gBANH,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa;IACtC,kFAAkF;IACjE,cAAc,CAAC,GAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,aAAA;IACzE,2FAA2F;IAC1E,GAAG,GAAE,MAAM,MAAiB;IAG/C;;;;;;;OAOG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;YAyBhG,MAAM;IA4BpB,0DAA0D;IAC1D,cAAc,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAIvC,8CAA8C;IAC9C,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,qDAAqD;IACrD,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC;CAG3E"}
|